Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Streams APIとTCPフロー制御 / Web Streams API and TCP ...

tasshi
October 31, 2024

Streams APIとTCPフロー制御 / Web Streams API and TCP flow control

Nihonbashi.js #9 のLT資料です。
イベント情報はこちら↓
https://nihonbashi-js.connpass.com/event/332328/

tasshi

October 31, 2024
Tweet

More Decks by tasshi

Other Decks in Programming

Transcript

  1. Stream APIとTCPフロー制御 - Nihonbashi.js #9 はじめに ⽥代雅治 (@tasshi_me) • 仕事

    • サイボウズ株式会社 • kintone拡張基盤🧩チーム EM • 好きなもの • ⾃転⾞ 🚲 • ゲーム 🎮 • ⽝ 🐶 • 2024年9⽉に結婚しました💍 ⾃⼰紹介 2
  2. Stream APIとTCPフロー制御 - Nihonbashi.js #9 Streams API • データ⼊出⼒を逐次的に、効率よく扱うためのAPI •

    データを分割された断⽚(chunk)の連続した流れとして扱う • 概念⾃体はかなり古くからあり、多くの開発⾔語でストリーム操作はあります • 3種類のストリームオブジェクト(ReadableStream, WritableStream, TransformStream) • Stream同⼠をパイプ接続すると、chunkは流れるように終端まで処理される(パイプチェーン) Streams APIの概要 5 Transform Stream Readable Stream Writable Stream chunk データの読み込み データの変換 (読み込み+書き込み) データの書き込み chunk Source Sink
  3. Stream APIとTCPフロー制御 - Nihonbashi.js #9 Streams API • ストリーム(オブジェクト)は未処理チャンクを保持する内部キューを持っている •

    パイプチェーン上のストリーム間で処理速度に差がある場合、 内部キューにチャンクが溜まっていくことになる • => メモリを圧迫する︖ ストリームオブジェクト間の処理速度の違いとメモリ使⽤量 6 Transform Stream Readable Stream Writable Stream chunk chunk チャンクが溜まっていく︖ ここの処理が遅い場合 (DBアクセスなど)
  4. Stream APIとTCPフロー制御 - Nihonbashi.js #9 Streams API • チャンクを受け⼊れられない場合、ストリームは上流に停⽌信号を出す •

    停⽌信号を受けた上流のストリームはデータの送信を停⽌する • 下流のストリームが送信を指⽰(pull)すると再び処理が再開する => チャンクの流速が調整されてメモリを圧迫せずに処理できる 背圧 (Backpressure) 7 Transform Stream Readable Stream Writable Stream chunk chunk 内部キューが満杯 STOP! 内部キューが満杯 STOP!
  5. Stream APIとTCPフロー制御 - Nihonbashi.js #9 Streams API • 背圧は内部キューの状態から、キューイング戦略に基づいて通知される •

    現在は2種類のキューイング戦略が利⽤可能 キューイング戦略と最⾼⽔準点(highWaterMark) 8 キューに格納されたチャンク数で判定 キューに格納されたバイト数で判定 chunk chunk chunk chunk chunk chunk chunk chunk 3 最⾼⽔準点 (highWaterMark) 10KB CountQueuingStrategy ByteLengthQueuingStrategy
  6. Stream APIとTCPフロー制御 - Nihonbashi.js #9 メモリリークと調査 • Webアプリケーションから全レコードを取得し、加⼯してDBに書き込んでいく • アプリケーションは全レコードをJSONL形式で返すAPIを持つ

    • ストリームを使えばメモリに全レコードを載せずに効率よく処理できるはずだが、、、 全レコードへのバッチ処理 10 Text Decoder Stream Response JSON Parser Stream Line Splitter Stream ⾏ごとに 再分割 UTF-8 デコード DBに 書き込み レスポンスの 読み込み Writable Stream DBに 書き込み fetch DB
  7. Stream APIとTCPフロー制御 - Nihonbashi.js #9 メモリリークと調査 • 終端ストリームを置き換え(DB => fs,

    setTimeout)=> 効果なし • 中間ストリームを置き換え => 効果なし • 始端ストリームを置き換え(fetch => fs)=> 効果あり 原因箇所の特定 12 Response fetch Text Decoder Stream ここより前に原因がありそう
  8. Stream APIとTCPフロー制御 - Nihonbashi.js #9 メモリリークと調査 • APIレスポンスはJSONL形式 (Content-Type: application/jsonl;

    charset=utf-8) • HTTP/1.1 • Transfer-Encoding: chunked • ↑レスポンスはチャンク単位で送られてくる • => この通信に背圧が反映されないとResponse(ReadableStream)にチャンクが溜まるのでは︖ 通信の詳細 13 chunk Server chunk chunk Response STOP! STOP!…できてる︖
  9. Stream APIとTCPフロー制御 - Nihonbashi.js #9 BackpressureとTCP通信 • Server: JSONLを70MB返却するAPI •

    Batch Runner: • APIを呼び出し、レスポンスをストリーム処理する • 終端のWritableStreamはチャンクを受け取ると⼀定時間待機する(背圧を発⽣させる) 検証環境 15 chunk Server (Express) Batch Runner chunk chunk HTTP WireShark キャプチャ
  10. Stream APIとTCPフロー制御 - Nihonbashi.js #9 BackpressureとTCP通信 • Zero Windowが送信されていた •

    ZeroWindow: ウィンドウサイズが0(通信⼀時停⽌) • ZeroWindowProveAck: 通信⼀時停⽌中の接続維持のためのACK • TCP Window Update: ウィンドウサイズが更新されて通信再開 • => よりキューの処理に時間がかかっていると考えられる 終端ストリームの遅延時間1000msの場合 18
  11. Stream APIとTCPフロー制御 - Nihonbashi.js #9 BackpressureとTCP通信 • Zero Window送信後、⻑時間に渡って通信再開せず •

    => 内部キューが処理できていないと考えられる 終端ストリームの遅延時間10000msの場合 19
  12. Stream APIとTCPフロー制御 - Nihonbashi.js #9 BackpressureとTCP通信 • Streams APIの背圧はネットワークリクエストにも反映されると考えられる •

    ウィンドウサイズが⼩さくなり、受信可能データ量を調整する • それでも処理しきれず受信を⽌める場合は、 サーバーにzero windowパケットが送信される • (HTTP/2での挙動も⾒てみたい) まとめ 20 chunk Server chunk chunk Response STOP! STOP! (zero window)
  13. Stream APIとTCPフロー制御 - Nihonbashi.js #9 メモリリークの原因 • Node.js v22.7.0 (undici

    v6.19.7)で修正された (nodejs/undici#3445) • v22ではメモリ使⽤量が300MB付近で横ばい • LTSへのbackport状況 • v20.x => v20.18.0にbackport済み、だが計測結果は× • v18.x => backportの予定は⾒つけられず undiciがメモリリークしていた 23