14 ポイント 投稿者 GN⁺ 2025-01-09 | 5件のコメント | WhatsAppで共有
  • Node.js/TypeScriptベースのバックエンドで、大規模なリアルタイム更新を処理しなければならない状況だった
  • バックエンドとしてPostgreSQLを使用し、数百個のワーカーノードが新しいジョブを継続的に確認し、エージェントが実行およびチャットの状態更新を受け取る必要があった
  • WebSocketの検討から始まったが、驚くほど効果的な「昔ながらの」解決策にたどり着いた
    → "Postgresを使ったHTTP Long Polling"

問題状況: 大規模リアルタイム更新

  • ワーカーノードの更新 :
    • Node.js/Golang/C# SDKを実行する数百のワーカーノードがある
    • 新しいジョブが提供されるとすぐにそれを把握する必要があったため、Postgresデータベースをダウンさせないクエリ戦略が必要だった
  • エージェント状態の同期 :
    • エージェントには実行およびチャット状態に関するリアルタイム更新が必要で、それを効率的にストリーミングしなければならなかった

ロングポーリングとWebSocketの比較

  • ショートポーリングは、時刻表どおりに厳密に出発する列車のようなもので、乗客がいるかどうかに関係なく決まった間隔で出発する
  • ロングポーリングは、サーバーが応答を待機し、データが発生したらすぐに返し、一定時間が経つとタイムアウトとして応答を返す
    • つまり、「待っていてデータが生じたら出発する」列車のようなもの。特定の時間(TTL)内に乗客が現れないときだけ空のまま出発する
    • データ(乗客)があるときは即座に出発し、ないときはリソースを効率的に使えるという2つの利点を同時に提供する
  • WebSocketは、接続を常時維持して双方向にデータをやり取りする方式である
    • 組織環境、インフラ、ファイアウォールの問題などから、WebSocket構成よりロングポーリングのほうがシンプルで互換性が高い

ロングポーリング実装の詳細

  • getJobStatusSync 関数が重要な役割を担う
    • jobId, owner, ttl などのパラメータを受け取り、特定のジョブ状態を一定時間のあいだ繰り返し照会する
  • 次の条件のいずれかが満たされるまで繰り返し照会を行う
    • ジョブ状態が success または failure になる
    • ttl(タイムアウト)が経過する
  • 500ms間隔でデータベースを照会し、結果が確定していなければ待って再度照会する
  • タイムアウト超過時にはエラーを投げ、成功時には結果を返す

データベース最適化

  • Postgresに適切なインデックスを置いて照会コストを最小化する
  • 例: CREATE INDEX idx_jobs_status ON jobs(id, cluster_id);

ロングポーリングの利点

  • モニタリングの維持が容易 : 既存のHTTPベースのロギング、モニタリングスタックをそのまま活用できる
  • 認証の単純さ : 新しい認証方式を実装せず、既存のHTTP認証をそのまま使用できる
  • インフラ互換性 : ファイアウォールやロードバランサーに別途設定が不要で、通常のHTTPトラフィックとして扱われる
  • 運用の単純性 : サーバー再起動時にも接続状態を別途処理する必要がなく、デバッグしやすい
  • クライアント実装の容易さ : 標準的なHTTPリクエスト/レスポンス構造に再試行ロジックを加えるだけで動作する

ElectricSQLとの比較

  • ElectricSQLは、Postgresデータをフロントエンドと同期するソリューションである
  • WebSocketの代わりにHTTPを使いながらもリアルタイム性を保証する構造を備えている
  • 実際にリアルタイム更新を処理するために極端な制御や低レベル構造が必要ない場合は、ElectricSQLを推奨する

私たちがRaw Long Pollingを選んだ理由

  • メッセージ配信メカニズムは単なる実装の詳細ではなく、製品の中核要素である
  • 中核機能をサードパーティライブラリに依存することはできない(どれほど優れたライブラリであっても)
  • 要件
    • 中核製品の制御 : メッセージ配信メカニズムを完全に制御する必要がある。インフラレベルではなく製品そのものだからだ
    • 外部依存性の排除 : セルフホスティングを簡素化するため、外部依存性を最小化する
    • 低レベル制御 : ポーリングメカニズムおよび接続管理を直接制御する
    • 最大限の制御可能性 : 動的なポーリング間隔の実装など、細部をきめ細かく調整できなければならない
    • コードの単純性 : ユーザーがコードベースを容易に理解・修正できるよう、シンプルに設計する
  • 結論として、シンプルなHTTP Long Polling実装を選ぶことで、直接的な制御単純性を確保した

ロングポーリング実装時の注意点

  • TTL設定 : サーバー側で必ず最大TTLを強制し、クライアントが要求したTTLがそれを超えないように処理する
  • インフラのタイムアウトを考慮 : ロードバランサー、エッジサーバー、プロキシなどのタイムアウト設定より十分短いTTLである必要がある
  • DBポーリング間隔 : 500ms程度の遅延を設けてDB負荷を減らす
  • バックオフ戦略(オプション) : 徐々にポーリング間隔を伸ばす方式により、システム資源をさらに効率的に使用できる

WebSocketを検討すべき状況

  • WebSocket自体が誤りというわけではなく、別の側面では有用である
    • 状態を多く持つ接続を監視し、複雑なイベントを常時やり取りしなければならない場合
    • 認証、インフラ、観測性の問題を解決するためのリソースと時間が十分にある場合
  • 運用やロギング、再接続処理、認証メカニズムなどを自前で構築しなければならない複雑さが存在する

WebSockets: もう一つの選択肢について

  • Long Pollingは私たちの要件に適していたが、WebSocketsも十分に検討する価値がある
  • WebSockets自体が悪いわけではなく、多くの注意と管理を必要とするだけだ
  • WebSocketsの主な課題と解決の方向性
    • 可視性 : WebSocketsは状態ベースであるため、継続的な接続に対するロギングとモニタリングの追加が必要
    • 認証 : WebSocket接続のための新しい認証メカニズムの実装が必要
    • インフラ : WebSocketをサポートするため、ロードバランサーやファイアウォールなどのインフラを適切に構成する必要がある
    • 運用管理 : WebSocket接続および再接続の管理。接続タイムアウトとエラー処理
    • クライアント実装 : クライアント側のWebSocketライブラリ実装。再接続および状態管理機能を含む

5件のコメント

 
jhj0517 2025-01-10

MLモデルのサービングで、ここでいう「ショートポーリング」構造を使っているのですが、何が効率的なのかかなり悩んでいます。自分なりにあちこち調べた範囲では、WebSocketやSSEなどの再接続処理にかかる大きなコストのため、ショートポーリングのほうが一般的により安全だという話があって、ショートポーリングを選んではみたのですが…… 😭

 
bbulbum 2025-01-10

Long polling は少し hacky に感じられるので、敬遠されがちな気がしますね。ブラウザではおそらくずっとリクエストが完了していないように見えるでしょうし。時々、読み込みが終わらないサイトがありますが、私は「コンテンツが全部読み込めていないのかな?」と思ってしまって、あまり好きではありません。
アプリケーションでも結局、どこかで hang させてレスポンスを待つ状態になるわけで……ちょっと不自然に見えますね。

 
joyfui 2025-01-09

「エージェントが実行およびチャットの状態更新を受け取る必要がある」
これを見てすぐにSSEを思い浮かべましたが、やはりHacker Newsの意見でもSSEへの言及が多いですね。

 
GN⁺ 2025-01-09
Hacker Newsのコメント
  • Long polling にはそれ自体の問題がある

    • Second Life はクライアントとサーバー間で HTTPS long polling チャネルを使用している
    • クライアント側では libcurl を使用し、タイムアウトが発生する可能性がある
    • サーバーがタイムアウトと次のリクエストの間にメッセージを送ろうとすると競合状態が発生し、メッセージが失われる可能性がある
    • Apache サーバーが前段に配置されて不要なリクエストを遮断するが、タイムアウトが発生する可能性がある
    • ミドルボックスやプロキシサーバーは long polling を嫌うことがある
    • HTTP 接続を長時間維持することを嫌う要素が多い
    • 結果として信頼できないメッセージチャネルになり、重複を検出するためにシーケンス番号が必要になり、メッセージを失う可能性がある
    • 元の記事で "loop" と表示されたチャートのセクションでは、タイムアウト処理に言及していない
    • long polling を使うなら、数秒ごとにデータを送って接続を維持する必要がある
  • Phoenix と LiveView を毎日使えるのがうれしい

    • WebSockets を使うので気にする必要がない
  • Server-Sent Events (SSE) を使うより技術的な利点があるのか気になる

    • どちらも HTTP 接続を開いたままにでき、シンプルな HTTP という利点がある
    • SSE は更新や結果をストリーミングできる場合により適しているように見える
    • 適したユースケースは、特定のクライアントの代わりにすべての作業 ID を監視する場合かもしれない
  • この記事は "Websocket" と "Long-polling" を独立した選択であるかのように結びつけている

    • long-polling サーバーは少し追加作業をすれば websocket クライアントを処理できる
    • 既存アーキテクチャが websocket の場合、long-polling クライアントをサポートするには 2 つのサーバー層が必要になる
  • Node.js で setTimeout を使うより簡単な方法

    • import { setTimeout } from "node:timers/promises"; await setTimeout(500); を使う
  • long polling は好きだ。理解しやすく、クライアントの観点では非常に遅い接続のように動作する

    • リトライと、クライアント側でキャンセルされた接続を追跡する必要がある
    • コード例で繰り返しデータを問い合わせるループはぎこちなく見える
  • Server-Sent Events や WebSockets は long polling のすべてのユースケースを置き換えられるわけではない

    • SSE の接続制限はしばしば問題になる
    • WebSockets はほとんどの環境で信頼できない
    • バックエンドで変更を検知し、適切なクライアントに伝播するという問題は依然として解決されない
  • Postgres の非同期通知機能を使うのがよい

    • サーバーがチャネルを LISTEN し、データ変更時に PG が TRIGGER と NOTIFY を実行できる
  • 短いタイムアウトと適切に終了されるリクエストを伴う long polling に、まだ意味があるのか分からない

    • HTTP/2 や QUIC が使われていない場合、このトリックにはまだ意味があるかもしれない
  • WebSockets の比較的シンプルな代替手段を思い出させてくれるのが新鮮だ

    • WebSockets を選んだスタートアップで働いていたが、ホテルやレストランの Wi‑Fi でテストするのが難しかった
 
luminance 2025-01-10

Elixir、Phoenix framework、LiveViewを通じてWebSocketsを使ってみたいですね。