Expressで1GB以上のAPIレスポンスを返すテクニック
以下で使っているExpressの技をまとめた
HelpfeelとCosenseの間のデータ連携
scrapboxの
Auto Project Backup
Project JSON export
全部やるとメモリ使用量が超安定する
before / after
https://gyazo.com/5eff4d7dca17b4a63439dfd3a7779f86https://nota.gyazo.com/6ecb627aa37143bbe8d84c3ae2fce5d4
サーバーの数がCOVID-19以前に戻った
基本方針:メモリ上に大きな文字列(JSONなど)を保持しない
これがとても重要ですshokai.icon*5
node.jsが確保できるメモリ量は--max-old-space-sizeで指定できるが、それも上限2.5GBまで
それ以上のデータは扱えない
オーバーするとJavaScript heap out of memoryが発生し、プロセスが強制終了してしまう
巨大な文字列ができる所
JSON.parseの引数やJSON.stringifyの返り値
streamの送信バッファ
これらを対策すると解決できる
chunkに分割して少しずつ送信する
BAD
res.json(object)はダメ
大きな1つのJSON stringが作られてしまう
GOOD
chunk分割
JSONの中の小さなパーツを作って送出していく
HTTPのコネクションはつなぎっぱなし
DBから1件readする毎に、res.write(string)で少しずつレスポンス
最後にres.end()で閉じる
詳しい解説
/nota-techconf/急にリクエストが1.5倍に増えてクソデカJSONと戦った
stream/pipeline処理しやすいデータフォーマットを採用する
chunk分割はJSONでも可能だが、もっと良い方法もある
JSON LinesやLTSV等の行指向フォーマットを使う
受信側にも優しい
JSONは最後までダウンロードしないとparseできない
事前にschemaが決まっていればparseできなくもないが
行指向データなら1行ダウンロードする毎にparseできる
parse前の文字列の方は破棄できる
ダウンロードとparseとその後の処理を並行して実行できる
JSON Linesならダウンロードした部分から処理を開始できる
JSONだと30分まってダウンロード完了してからじゃないと処理できない
行指向フォーマットはMapReduceとも相性が良いshokai.icon
将来的にもっと大きなデータ量になっても対応できる余地がある
クライアント側がダウンロードをキャンセルするとstream.Writableが切断されるので、すぐにexpress handlerをcloseする
DBから1件readする毎にres.write(string)する方式は、リクエストが中断されても止まらない
streamが閉じたらres.end()する
code:js
router.get('/export', (req, res) => {
let streamClosed = false
res.once('error', () => (streamClosed = true))
res.once('close', () => (streamClosed = true))
req.once('close', () => (streamClosed = true))
// 略
if (streamClosed) return res.end()
クライアントが切断した時、reqとresのcloseイベントはどちらも同時に発火する
なので片方で十分なはずだが、念の為両方を見ておく
Expressの送信バッファが詰まったらDBからのreadを一時停止する
DB→appの方がapp→インターネットよりも当然速い
最速でDBからreadしてres.write(string)してると、送信バッファが巨大になっていく
メモリを逼迫し、JavaScript heap out of memoryを発生させる
送信バッファ
res.writableLengthで確認できる
送信バッファが空くまでres.writeを止める
code:js
import delay from '@notainc/delay'
async function writeLine(line) {
while (!streamClosed && res.writableLength > 0) {
await delay(10)
}
res.write(line)
res.write('\n')
}
マイコンのレジスタみたいな考え方をしている
訂正:Node.jsのstream.Writableへの書き込みは送信バッファが完全に空になるまで待たなくてもよいshokai.icon