HN 日本語サマリー

← 一覧へ戻る
プログラミング

.join() がバグであるべき理由

The .join() that should be a bug (kronotop.com)

5 pointsby mastabadtomm0 コメント

要約

Kronotopは、データベースシステムにおける接続管理の課題に対処するため、独自のアーキテクチャを採用しています。従来のRedis型(シングルスレッド、接続は安価だがブロッキング不可)とPostgres型(プロセス毎、ブロッキング可能だが接続コスト高)のモデルの利点を組み合わせ、Nettyベースのイベントループスレッドで接続を管理し、I/O処理は仮想スレッドにオフロードすることで、多数の接続を効率的に処理しつつ、ブロッキング処理も可能にしています。

全文翻訳

セクション「Serving thousands of connections on a backend where every call blocks」 データベースシステムにおける接続管理には、それぞれコストのかかる2つのよく知られた実装方法があります。Kronotopは3番目のアプローチを採用しています。まず、私たちが抱えている問題から始めましょう。KronotopはメタデータをFoundationDBに格納し、ドキュメント本体はローカルファイルシステムに格納しています。そのため、クライアントが要求するほぼすべての操作は、FDBへのネットワーク呼び出しとディスクからの読み取りになります。ネットワーク呼び出しはミリ秒単位であり、ナノ秒ではありません。ドキュメントの読み取り、トランザクションのコミット、インデックスの検索:すべてI/Oを待ちます。私たちのコマンドパスはインメモリ処理ではありません。それは待機によって支配されています。 セクション「Two classic models」 Redis(6.0以前)はシングルスレッドで動作します。そのスレッドはすべての接続をリッスンし、コマンドを一度に1つずつ処理します。これは接続のスケーリングには優れていますが、ブロッキングを許しません。コマンドパスのいかなる部分も待機することは許されません。もし1つのコマンドが待機した場合、他のすべてのクライアントも一緒に待機することになります。データがすべてメモリ内にある場合は問題ありません。コマンドがより多くのことを行う必要がある場合は機能しません。私たちのコマンドはネットワーク経由でトランザクションストアを呼び出し、ディスクにファイルを追記し、fsyncを呼び出します。これらの各ステップはブロッキングします。 Postgresは逆の方法をとります。各接続に独自のプロセスを与えます。これにより、ブロッキングは問題ではなくなります。各接続は独立して実行されるため、通常の順次コードを書くことができます。しかし、代償があります!接続はオペレーティングシステムプロセスとなり、それは重いです。数千の接続は多すぎます。これが、Postgresのデプロイメントがほぼ常に別のコネクションプーラーの後ろに配置される理由です。 一方のモデルは接続をスケーリングしますが待機できません。もう一方は待機できますが、多数のアイドル/オープン接続を安価にスケーリングできません。Kronotopは両方を必要とします。 セクション「Splitting the connection from the work」 トリックは、「接続」と「作業」を同じものとして扱わないことです。接続側はRedisのように動作します。私たちはNetty上に構築しています。少数のイベントループスレッドが多数のソケットをリッスンします。それらは準備ができたソケットに反応します。それらは着信コマンドを解析し、応答を書き戻します。この部分は決してブロッキングせず、決して待機しません。そのため、数個のスレッドで数千の接続を維持できます。 作業側は、ディスクとネットワークI/Oを行う部分です。それはどこか別の場所、仮想スレッド上で実行されます。仮想スレッドにより、I/O呼び出しを通常のブロッキングコードのように、トップからボトムまで書くことができます。これはPostgresスタイルですが、Postgresのコストはありません。仮想スレッドは、FoundationDBまたはディスクを待機している間ブロックします。ブロックすると、Javaランタイムはそれをアンマウントし、下のキャリアスレッドを他の作業のために解放します。数千の仮想スレッドが同時にI/Oを待機できますが、少数の実際のスレッドだけが何かを行います。I/Oが完了すると、仮想スレッドは中断したところから処理を再開します。これにより、ネットワークスレッドは自由なままになります。それらは他のすべての接続にサービスを提供できます。遅い部分は脇で発生し、待機コストは低くなります。結果は接続スレッドに返され、応答が書き込まれます。私たちは意図的に書き込みをそこに保持します。そのハンドオフは、私たちが厳格に守る唯一のルールです。 CONNECTION SIDE WORK SIDE clients… Netty event loop A few threads serve thousands of sockets. Parse and write. Never blocks, never waits. the reply is always written here virtual thread tr.get(…).join() blocks on FDB or disk The runtime parks it. The carrier thread is freed for other work. 1offload supplyAsync(…, vtExecutor) 2result thenAcceptAsync(…, nettyExecutor) コードでは、オフロード全体は2つの動く部分に集約されます。 CompletableFuture.supplyAsync(supplier, context.getVirtualThreadPerTaskExecutor()) // virtual threadで実行 .thenAcceptAsync(action, response.getCtx().executor()); // Netty threadで再開 2つのエグゼキュータ、2つのフェーズ。サプライヤーは遅い部分です。それは仮想スレッドエグゼキュータ上で実行され、FoundationDBでのブロッキングが許可されます。アクションは応答です。それはresponse.getCtx().executor()、この接続を所有するNettyイベントループ上で実行されます。したがって、待機する作業と移動してはならない書き込みは、別々のスレッドに残ります。後者は前者完了後にのみ開始されます。読み取りコマンドはまさにその形状に対して書かれます。最初のブロックは値をフェッチし、2番目はそれを送信します。 public void execute(Request request, Response response) { AsyncCommandExecutor.supplyAsync(context, response, () -> { // virtual thread: open the transaction and wait on FDB Session session = request.getSession(); Transaction tr = TransactionUtil.getOrCreateTransaction(context, session); DirectorySubspace subspace = openZMapSubspace(tr, session); return tr.get(subspace.pack(message.getKey())).join(); }, (value) -> { // Netty thread: write the reply response.writeFullBulkString(toMessage(value)); }); } 中間の.join()が興味深い行です。通常のスレッドでは無駄になります。スレッドはそこでブロックされ、役に立たなくなります。仮想スレッドでは、それがポイントです。ランタイムは呼び出しを一時停止し、実際のスレッドを解放します。FDBが応答すると、呼び出しが再開されます。ハンドラは通常のブロッキングコードのように、トップからボトムまで読み取られ、待機のコストはなくなります。サプライヤーが戻ると、エグゼキュータは応答フェーズに移行します。コマンドが自動コミットだった場合、トランザクションをクリーンアップします。何かが例外をスローした場合、クライアントにエラーを書き戻します。以下のリンクから関連する実装部分を確認できます。 AsyncCommandExecutor ZGetRangeHandler BucketQueryHandler セクション「What lives on a connection」 接続は、開いている間、少量の状態を保持します。クライアントが誰であるか、どの名前空間が開いているか、認証されているかを知っています。また、進行中のトランザクションの詳細も保持します。最後の部分は重要です。Kronotopでは、トランザクションは接続に属します。トランザクションを開始し、その中でいくつかのコマンドを実行し、コミットまたはロールバックします。その間ずっと、そのトランザクションはセッションに紐付けられています。接続が途中で切断された場合、開いているトランザクションはキャンセルされ、クリーンアップされます。何もぶら下がったままになりません。接続を切断せずにセッション状態をリセットすることもできます。これにより、カーソル、監視中のキー、および半完成のトランザクションが一度にクリアされます。そのリセットは、クライアントサイドプールで接続を簡単に再利用できるようにするものでもあります。接続を返却し、リセットし、再び渡します。 A connection is a session client identity open namespaces auth state transaction in flight Connection drops the open transaction is cancelled and cleaned up, nothing dangles RESET clears cursors, watched keys, and any half-finished transaction セクション「Try Kronotop」 この投稿を最後まで読んだ方は、Kronotopを試してみてはいかがでしょうか?Kronotopは、FoundationDB上に構築された分散型トランザクションドキュメントデータベースです。数分でクラスターを開始するには、クイックスタートガイドを参照してください。 Tags: Architecture Java Kronotop Internals Kronotop developer preview is out!