2 ポイント 投稿者 GN⁺ 2023-11-08 | 1件のコメント | WhatsAppで共有
  • 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: 1 つのサービスのすべての 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 側の回答は含まれていない

1件のコメント

 
GN⁺ 2023-11-08
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ファイルは1つしかないため、同時に書き込める書き手は1つだけ
      元記事で言っていたのは、更新処理は逐次実行されなければならないという意味だと思う

    • トラフィックが少なければ動くが、トランザクションが大きくなったり同時書き込み数が増えたりすると、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 ファイルとして blob ストレージに保存していたが、アクセスパターンにはかなり合っていた

    • 変更後に再アップロードするとき、ファイルロックをどう扱っていたのか気になる
  • 表面的には、最悪とひどさの組み合わせのように見える
    誰かが実際の数値を示して利点を説明し、想定される欠陥を分析する良い記事を書いてくれたらいいのに。きちんと学べば本当に興味深いテーマになりそう

    • なぜこれが「最悪とひどさの組み合わせ」のように見えるのか説明してもらえる?
      表面的には、特に専門のシステム管理者ではない多数のユーザーが実行・デプロイする分散システムを作ると仮定するなら、かなり合理的な選択に見える
      ここでの目標もそうあるべきだと思うし、追加のデータベースや別のサーバーを設定・構成・管理する必要を避けることが設計目標なのだろうと期待している
  • Bluesky をもっとよく知っている人に、SQLite にどのデータが保存され、どのデータは保存されないのか説明してほしい
    ユーザー間メッセージのようなものではないのだろうと想定している

    • ユーザー間メッセージもそれらの SQLite データベースに保存されると思う
      メールを思い浮かべればいい。メールを送って 5 人を CC に入れると、7 人がそれぞれ自分のメールサーバーに同じメールのコピーを保存する
      つまり、1 通のメールを格納し、他の人たちが参照する中央データベースがある構造ではない
      リレーショナルデータベースのシャーディングも基本的にはこのように動作する
      このようなデータの非正規化は、アプリケーションがスケールするほど、特に書き込みに対して読み取りの比率が高い多対多アプリケーションでは、ほぼ必須に近い
      書き込みに対して読み取りが少ないなら、単一マスターと複数スレーブのリレーショナルデータベース構成でも、驚くほど多くのリクエストとデータを処理できる
    • ユーザーとして投稿したすべての投稿と返信が入る
      現在は Bluesky が事実上唯一の PDS を自前でホストしているが、最終目標はすべてのエンドユーザーが自分の PDS を持つこと
      Inrupt/SOLID はこの概念を「pod」と呼んでいる
      実際には昨日 2 つ目のプロダクション PDS をオンボーディングしたので、進展はある
    • メッセージというのがダイレクトメッセージ、つまり二者間の非公開メッセージを指すなら、Bluesky には現在その機能はない
      世界中にブロードキャストされる公開メッセージだけがある
      ダイレクトメッセージの計画があるかは、別途調べていない
  • ユーザーを sha256 でハッシュして2 文字の対象ディレクトリに分ける理由は何だろう?
    md5 の方がずっと速いし、同じ問題を解決できるのでは?

    • 推測だが、そのハッシュは比較的少ない回数しか実行されないので、性能差はノイズに埋もれる
      「なぜ安全でないハッシュを使ったのか」という質問に答えなくてよくなり、セキュリティ問題の一類型の可能性を排除、または最小化できる価値の方が大きい
    • その規模では衝突を心配するかもしれない
      あるいは私のように会社のセキュリティツールに埋もれていて、md5 の使用ごとに個別の例外を作りたくないのかもしれない
    • 壊れた暗号学的ハッシュをそこら中に置いておくのは健全ではない
      セキュアなハッシュが不要なら、高速な非暗号学的ハッシュはたくさんある
    • これは衝突というよりファイルシステムの制限、つまり 1 つのディレクトリ内の最大ファイル数が理由である可能性が高い
  • Bluesky はまだ招待制なの?

    • そうだが、「グロースハック」のような理由ではない
      バックエンドと不正利用対策の面でシステムを拡張している間、成長を制限するための方法だ
      開発者向けの専用ウェイトリストがあり、かなり早くアクセス権を得られる: https://atproto.com/blog/call-for-developers