2P by GN⁺ | ★ favorite | 댓글 1개
  • Bluesky atproto의 PDS 리팩터링 PR #1705는 PDS가 단일 테넌트 SQLite 데이터스토어를 사용하도록 바꾸고, 사용자별 repo와 비공개 계정 상태를 각자의 SQLite 파일에 저장하도록 변경함
  • 사용자 DB는 /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did} 경로 구조로 저장되며, 각 repo의 서명 키는 해당 SQLite 파일 옆에 함께 보관됨
  • 기존 사용자 데이터 접근 추상화는 ActorStore로 대체되며, SQLite가 동시 트랜잭션을 지원하지 않기 때문에 쓰기 작업은 명시적으로 store와 트랜잭션을 맺어야 함
  • 열린 DB 파일 핸들과 서명 키는 LRUCache로 관리되며, 최대 30k개의 열린 파일 핸들과 30k개의 키를 메모리에 유지하고 캐시에서 DB가 밀려나면 파일 핸들을 닫음
  • 서비스 상태 관리를 위해 별도 SQLite DB 3개를 도입하고 WAL 모드로 실행해 동시 읽기와 스트리밍 복제를 가능하게 하며, PDS 배포판에는 Litestream 또는 유사 도구를 포함할 계획임

PR의 핵심 변경

  • PR #1705는 PDS를 단일 테넌트 SQLite 데이터스토어 기반으로 리팩터링함
  • 각 사용자는 자기 전용 SQLite 파일을 가지며, 이 파일에는 해당 사용자의 repo와 비공개 계정 상태가 저장됨
  • 사용자 DB는 DID 해시를 이용한 계층형 경로에 저장됨
    • 경로 형식: /${dbDirectory}/${sha256Hex(did).slice(0,2)}/${did}
  • 각 repo의 repo signing key는 SQLite 파일과 같은 위치에 저장됨

ActorStore와 트랜잭션 모델

  • 사용자 데이터 접근 추상화가 기존 “services”에서 ActorStore로 바뀜
  • ActorStore의 주요 차이는 읽기와 쓰기를 위한 클래스가 분리되어 있다는 점임
  • SQLite는 동시 트랜잭션을 지원하지 않기 때문에, 쓰기 작업을 하려면 명확하게 store와 트랜잭션을 맺어야 함
  • 커밋 로그에는 reader와 transactor 재작업, actor store 트랜잭션 race 처리, store 인터페이스 정리 등이 포함됨

캐시와 파일 핸들 관리

  • 서명 키와 데이터베이스를 위한 LRUCache가 유지됨
  • 설정된 한도는 다음과 같음
    • 열린 파일 핸들 최대 30k
    • 메모리에 유지되는 키 최대 30k
  • 데이터베이스가 캐시에서 밀려나면 파일 핸들을 닫도록 처리함
  • 관련 커밋에는 actor store in lru cache, fix open handles가 포함됨

서비스 상태용 SQLite DB 3개

  • 사용자별 DB 외에 서비스 상태 관리를 위한 별도 SQLite 데이터베이스 3개가 도입됨
    • service DB: 계정 정보, 초대 코드, refresh token 등을 관리
    • did cache DB: DID resolution 캐싱을 위한 단일 테이블만 포함
    • sequencer DB: 한 서비스의 모든 repo 업데이트 순서를 관리하는 단일 테이블만 포함
  • 각 SQLite 파일은 WAL mode로 실행됨
  • WAL mode의 목적은 동시 읽기와 스트리밍 복제를 가능하게 하는 것임
  • PDS 배포판에는 Litestream 또는 유사 도구를 포함할 계획이 있음

리뷰와 병합 상태

  • 이 PR은 총 143 commits로 구성되어 pds-sqlite-refactor 브랜치에서 pds-v2 브랜치로 병합됨
  • 병합일은 2023년 11월 1일이며, 병합 커밋은 8449ceb
  • 리뷰어 devinivy는 여러 노트와 코멘트를 남긴 뒤 변경을 승인함
  • devinivy는 리팩터링에 대해 “많은 훌륭한 단순화”가 있고 전체적으로 정돈된 느낌이라고 평가함
  • 병합 후 pds-sqlite-refactor 브랜치는 삭제됨

이후 질문

  • 2025년 2월 28일, npetrangelo가 이 PR의 변경 규모를 살펴보며 이전 Postgres 아키텍처와 이 PR로 도입된 SQLite 아키텍처 사이의 트레이드오프 요약을 요청함
  • 제공된 본문에는 해당 질문에 대한 Bluesky 측 답변이 포함되어 있지 않음

댓글과 토론

Hacker News 의견들
  • SQLite는 좋아하지만, 테넌트마다 스키마나 데이터베이스를 따로 두는 방식은 일반적으로 어려움이 많음
    공유 인스턴스에서 행 수준 보안(RLS)을 쓰면 마이그레이션이 실패해도 전체 롤백이 가능하지만, 테넌트별 스키마에서는 예상 못 한 데이터 때문에 데이터 마이그레이션이 실패하면 원인을 찾을 때까지 사용자들이 서로 다른 스키마 버전에 남게 됨
    샤딩 규모에 도달하면 어차피 비슷한 일이 생길 수 있지만, 그 전까지는 단일 데이터베이스가 가장 쉽고, 나중에 데이터를 합치거나 리소스 소유권을 원자적으로 옮겨야 할 수도 있음
    이 구성을 반대하는 건 아니고 쓸 곳은 있지만, 회사에서는 테넌트별 스키마에서 전속력으로 벗어나는 중임. 제대로 투자하지 않으면 문제가 너무 많고, 처음 아이디어를 낼 때 그 준비가 된 경우는 드물다고 봄
    재미있는 건 10년 전쯤 앱이 테넌트별 SQLite로 시작해 PostgreSQL의 테넌트별 스키마로 옮겼고, 이제는 RLS가 있는 단일 스키마로 가는 중이라 완전히 반대 방향으로 진행됐다는 것

    • 프로덕션에서 거대한 데이터베이스를 다뤄본 입장에서는 다시는 하고 싶지 않음
      부하가 충분히 커지면 모든 변경이 위험해지는데, 성능의 모든 극단 상황을 완전히 테스트할 수 없기 때문임
      무료 티어 사용자가 인덱스 없는 코드 경로를 찾아내 프로덕션을 망가뜨리는 것도 흔한 패턴임

    • 데이터 마이그레이션 실패로 일부 사용자가 다른 스키마 버전에 남는 건 큰 문제가 아닐 수 있음
      그 정도로 크고 복잡한 서비스라면 보통 스키마 업그레이드를 단계적으로 함: 1. 코드를 미래 스키마와 호환되게 만들고, 2. 데이터를 마이그레이션하고, 3. 예전 스키마 지원을 제거함
      그래서 보통 1단계와 2단계 사이 상태로 오래 운영해도 안전해야 함. 물론 새 버그는 예외지만, 운영 관점에서는 이런 절차를 쓰는 한 마이그레이션 중간 상태로 돌아가는 시스템도 괜찮게 봄

    • 제품 고객이 100명 미만이라면 사용자별로 다른 스키마 버전에 있는 게 오히려 좋을 수도 있음
      고객마다 업그레이드 일정과 요구가 다를 수 있고, 일부 고객용으로 맞춤 작업을 해서 사실상 같은 코드도 안 돌리는 사업도 알고 있음
      결국 비즈니스 구조에 따라 다름

    • 공정하게 말하면 10년 전에는 RLS가 아직 없었음. PostgreSQL 9.5에서 2016년에 등장했음

    • https://blog.turso.tech/introducing-embedded-replicas-deploy...

      https://electric-sql.com/

  • “SQLite는 동시 트랜잭션을 지원하지 않는다”는 말이 무슨 뜻인지 모르겠음
    .db 파일을 UNC나 NFS 같은 파일 공유를 통해 접근하지 않는 한 지원하는 걸로 알고 있음: https://www.sqlite.org/wal.html
    같은 머신의 여러 스레드/프로세스에서 데이터베이스를 읽고 갱신하는 데 써왔고, 일관된 뷰가 필요하거나 트랜잭션을 오래 잡고 싶지 않다면 sqlite backup API로 스냅샷도 가능함
    뭔가 놓친 것일 수도 있고, SQLite를 몇 년간 건드리지 않아서 확실하진 않음

    • 아니었음. 착각했음. 실제로는 다중 읽기, 단일 쓰기에 가까움
      그동안 가정하고 있었고 충분히 꼼꼼히 확인하지 않았던 듯함. 다만 SQLite로 만든 데이터베이스 대부분이 쓰기보다 읽기 위주였음
      정정함

    • 기다리다 보면 hctree [1]가 안정화되고, 전통적인 백엔드 메커니즘과 새로 구현된 동시성 지원 백엔드 중 선택할 수 있게 될 것임

      [1] https://sqlite.org/hctree/doc/hctree/doc/hctree/index.html

    • 문서에 따르면 작성자는 WAL 파일 끝에 새 내용을 덧붙일 뿐이라 읽기와 쓰기가 동시에 가능하지만, WAL 파일이 하나뿐이므로 동시에 쓸 수 있는 작성자는 하나뿐임
      원글에서 말한 건 갱신 작업이 순차적으로 실행돼야 한다는 뜻 같음

    • 트래픽이 낮으면 동작하지만, 트랜잭션이 커지거나 동시 쓰기 수가 늘어나면 WAL을 켜도 어느 시점에는 database locked 문제가 생김
      애플리케이션 레벨에서 어느 정도 우회할 수는 있지만, 일반적으로 그 지점에 도달했다면 다른 데이터베이스 백엔드를 진지하게 고려해야 함

    • 적어도 마지막으로 확인했을 때는 행 수준 잠금이 없고, 테이블 수준 잠금도 매우 제한적이라는 뜻일 가능성이 큼
      문서상 작성자는 여전히 전체 데이터베이스에 잠금을 잡음

  • 흥미롭고, 사용자 1명과 데이터베이스 1개를 1:1로 두는 전략이 마음에 듦
    다만 사용자 간 집계가 필요한 데이터는 어떻게 처리하는지 궁금함. 다른 사용자를 구독 중이고 그 사용자가 글을 올리면 내 데이터베이스는 어떻게 새 글로 갱신되는지, 아니면 프로필 데이터나 팔로우 관계 같은 영속 데이터만 대상으로 하고 피드 같은 상호작용 데이터는 따로 처리하는 구조인지 궁금함
    “연결 풀링”이 열린 핸들 수를 LRU 캐시로 제한하는 것에 불과하다는 점도 좋음. 각 DB 연결이 단일 스레드라 연결 수준이 아니라 테넌시 수준에서 동시성을 다룬다는 점도 흥미로움
    이 위에 데이터베이스별 속도 제한을 쉽게 얹어서 특정 사용자의 남용도 막을 수 있을 것 같음
    Litestream을 임의 개수의 데이터베이스에 대해 설정하는 간단한 방법이 있는지도 궁금함

  • 서버에서 SQLite/Litestream 채택이 늘어나는 걸 보면 늘 반가움. 우리도 새 앱을 만들 때 쓰고 있음
    SQLite + Litestream은 테넌트 데이터베이스에 더 좋은 선택이고, 비싼 클라우드 관리형 데이터베이스보다 S3/R2에 복제·백업하는 비용이 훨씬 쌈 [1]
    Azure의 SQLServer 대비 최대 3900% 저렴함

    [1] https://docs.servicestack.net/ormlite/litestream

    • 3900% 더 저렴하다는 게 무슨 뜻인지 모르겠음
  • 이전 핀테크 직장에서 회사가 고객 계정을 암호화된 sqlite3 파일로 블롭 스토리지에 저장했는데, 접근 패턴에는 꽤 잘 맞았음

    • 수정 후 다시 업로드할 때 파일 잠금은 어떻게 처리했는지 궁금함
  • 겉으로만 보면 최악과 끔찍함의 조합처럼 보임
    누군가 실제 수치를 담아 장점을 설명하고 예상되는 결함을 분석하는 좋은 글을 써줬으면 좋겠음. 제대로 배우면 정말 흥미로운 주제일 수 있음

    • 왜 이게 “최악과 끔찍함의 조합”처럼 보이는지 설명해줄 수 있음?
      겉으로 보기에는, 특히 전문 시스템 관리자가 아닌 다수의 사용자가 실행하고 배포할 분산 시스템을 만든다고 가정하면 꽤 합리적인 선택으로 보임
      여기서 목표도 그래야 할 것 같고, 추가 데이터베이스나 다른 서버를 설정·구성·관리해야 하는 필요를 피하는 게 설계 목표일 거라고 기대함
  • Bluesky를 더 잘 아는 사람이 SQLite에 어떤 데이터가 저장되고 어떤 데이터는 아닌지 설명해줬으면 함
    사용자 간 메시지 같은 건 아닐 거라고 가정하고 있음

    • 사용자 간 메시지도 그 SQLite 데이터베이스들에 저장된다고 생각함
      이메일을 떠올리면 됨. 이메일을 보내고 다섯 명을 참조로 넣으면 일곱 명이 각자 자기 이메일 서버에 같은 이메일 사본을 저장함
      즉, 하나의 이메일을 담고 다른 사람들이 참조하는 중앙 데이터베이스가 있는 구조가 아님
      관계형 데이터베이스 샤딩도 기본적으로 이런 식으로 동작함
      이런 데이터 비정규화는 애플리케이션이 확장될수록, 특히 쓰기 대비 읽기 비율이 높은 다대다 애플리케이션에서는 거의 필수에 가까움
      쓰기 대비 읽기가 낮다면 단일 마스터와 여러 슬레이브 관계형 데이터베이스 구조로도 놀라울 정도로 많은 요청과 데이터를 처리할 수 있음
    • 사용자로서 올린 모든 게시물과 답글이 들어감
      현재는 Bluesky가 사실상 유일한 PDS를 직접 호스팅하고 있지만, 최종 목표는 모든 최종 사용자가 자기 PDS를 갖는 것임
      Inrupt/SOLID는 이 개념을 “pod”라고 부름
      실제로는 어제 두 번째 프로덕션 PDS를 온보딩했으니 진전은 있음
    • 메시지라는 게 직접 메시지, 즉 두 당사자 사이의 비공개 메시지를 뜻한다면 Bluesky에는 현재 그런 기능이 없음
      전 세계에 방송되는 공개 메시지만 있음
      직접 메시지 계획이 있는지는 따로 조사해보지 않았음
  • 사용자를 sha256 해시해서 두 글자 대상 디렉터리로 나누는 이유가 뭘까?
    md5가 훨씬 빠르고 같은 문제를 해결하지 않나?

    • 추측하자면 그 해시는 비교적 적게 수행되므로 성능 차이는 잡음 수준에 묻힘
      “왜 안전하지 않은 해시를 썼느냐”는 질문에 답하지 않아도 되고, 보안 문제의 한 부류 가능성을 제거하거나 최소화하는 가치가 더 큼
    • 그 규모에서는 충돌을 걱정할 수도 있음
      아니면 나처럼 회사의 보안 도구에 파묻혀 있어서, md5 사용마다 예외를 따로 만들고 싶지 않은 것일 수도 있음
    • 깨진 암호학적 해시를 돌아다니게 두는 건 건강하지 않음
      보안 해시가 필요 없다면 빠른 비암호학적 해시가 많이 있음
    • 이건 충돌보다는 파일시스템 제한, 즉 한 디렉터리 안의 최대 파일 수 때문일 가능성이 큼
  • Bluesky는 아직 초대제임?

    • 맞지만 “성장 해킹” 같은 이유는 아님
      백엔드와 남용 방지 측면에서 시스템을 확장하는 동안 성장을 제한하기 위한 방법임
      개발자용 전용 대기열이 있고, 꽤 빨리 접근 권한을 받을 수 있음: https://atproto.com/blog/call-for-developers