Next.jsでkeepAliveTimeoutを設定する

2022/02/19に公開
2

Client -> AWS ALB -> Node.js 構成をとった場合、稀にALBが502を返す場合があります。
これは、Node.jsのkeepAliveTimeoutのデフォルト値が5秒[1]であり、ALBのConnection idle timeoutのデフォルト値が60秒[2]であることによって引き起こされます。

この問題の詳細はこちらの記事が詳しいです。
https://shuheikagawa.com/blog/2019/04/25/keep-alive-timeout/

問題点

Next.jsでも上記の問題が発生するのですが、厄介なのはkeepAliveTimeout値を変更する方法が提供されていないことです。
例外的に、Custom Serverを利用すればkeepAliveTimeoutを変更可能ですが、Next.jsによる最適化を一部失うことからCustom Serverの利用を避けたいケースもあるはずです。

問題提起はGitHub Discussionsで行われていますが、進展はみられません。
https://github.com/vercel/next.js/discussions/19618
node_modules配下のコードを書き換える方法が提案されていますが、あまり良い解決策にも思えません。

解決策

http.createServerの振る舞いを変更します。
下記コードの通り、任意のkeepAliveTimeout値をセットしてhttp.Serverオブジェクトを返すようにするわけです。

next-patch.js
const http = require('http');

const HTTP_KEEPALIVE_TIMEOUT_MS = 61_000;

function patchCreateServer() {
  const createServer = http.createServer;
  http.createServer = (handler) => {
    const server = createServer(handler);
    server.keepAliveTimeout = HTTP_KEEPALIVE_TIMEOUT_MS;
    return server;
  };
}

patchCreateServer();

上記ファイルが yarn next start 時に実行されるようにします。nodeの -r オプションを利用します。

--- a/package.json
+++ b/package.json
@@ -5,7 +5,7 @@
   "scripts": {
     "dev": "next dev",
     "build": "next build",
-    "start": "next start",
+    "start": "node -r ./next-patch.js ./node_modules/.bin/next start",
     "lint": "next lint"
   },
   "dependencies": {

実験

まずNext.jsのアプリケーションを作成し、起動してみます。

$ yarn create next-app --typescript nextjs-keepalivetimeout
$ cd nextjs-keepalivetimeout/
$ yarn build
$ yarn start

ncでリクエストを送ります。
すると、レスポンス受信後、コマンドは終了しません。HTTP Keep-Aliveが有効なためです。
そのまま放置すると5秒後にncコマンドが終了します。この5秒がkeepAliveTimeoutです。

$ nc localhost 3000
GET /api/hello HTTP/1.1

HTTP/1.1 200 OK ←ここからレスポンス
Content-Type: application/json; charset=utf-8
ETag: "13-5j0ZZR0tI549fSRsYxl8c9vAU78"
Content-Length: 19
Vary: Accept-Encoding
Date: Sat, 19 Feb 2022 03:39:23 GMT
Connection: keep-alive
Keep-Alive: timeout=5

{"name":"John Doe"}

解決策に提示したやり方で起動してみます。
レスポンス受信後、コマンドが終了しないのは同じです。
そのまま放置すると、今度は61秒後にncコマンドが終了します。
Keep-Aliveヘッダーの値も変化しています。

$ nc localhost 3000
GET /api/hello HTTP/1.1

HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
ETag: "13-5j0ZZR0tI549fSRsYxl8c9vAU78"
Content-Length: 19
Vary: Accept-Encoding
Date: Sat, 19 Feb 2022 03:56:21 GMT
Connection: keep-alive
Keep-Alive: timeout=61

{"name":"John Doe"}

Next.jsがHTTPサーバを起動するまでの処理の流れ

Next.js v12.1.0のコードで、HTTPサーバが起動されるまでの処理の流れを追ってみます。

1. packages/next/bin/next.ts

https://github.com/vercel/next.js/blob/v12.1.0/packages/next/bin/next.ts

yarn next start の起点となるファイルです。
20行目を読むと、 packages/next/cli/next-start.ts のnextStart関数を呼び出しています。

  start: () => Promise.resolve(require('../cli/next-start').nextStart),

2. packages/next/cli/next-start.ts

https://github.com/vercel/next.js/blob/v12.1.0/packages/next/cli/next-start.ts

61行目を読むと、 packages/next/server/lib/start-server.ts のstartServer関数を呼び出しています。

  startServer({
    dir,
    hostname: host,
    port,
  })

3. packages/next/server/lib/start-server.ts

https://github.com/vercel/next.js/blob/v12.1.0/packages/next/server/lib/start-server.ts

http.Serverのオブジェクトを生成し、listen開始します。

export function startServer(opts: StartServerOptions) {
  let requestHandler: RequestHandler

  const server = http.createServer((req, res) => {
    return requestHandler(req, res)
  })

  return new Promise<NextServer>((resolve, reject) => {
    // ...(snip)...
    server.on('listening', () => {
      const addr = server.address()
      const hostname =
        !opts.hostname || opts.hostname === '0.0.0.0'
          ? 'localhost'
          : opts.hostname

      const app = next({
        ...opts,
        hostname,
        customServer: false,
        httpServer: server,
        port: addr && typeof addr === 'object' ? addr.port : port,
      })

      requestHandler = app.getRequestHandler()
      resolve(app)
    })

    server.listen(port, opts.hostname)
  })
}

keepAliveTimeを設定する場合は、ここで server.keepAliveTime = 61000 としたいわけですが、その方法が提供されていません。

4. packages/next/server/next.ts

https://github.com/vercel/next.js/blob/v12.1.0/packages/next/server/next.ts

next.config.jsを読み込んだりハンドラを登録したりします。

今回重要なのは、next.config.jsの読み込みがHTTPサーバ起動よりも後だということです。
http.createServer の振る舞いを変えるための記述はnext.config.jsに書いてもダメだということであり、ですので解決策にあげたようにnode起動後の早い段階で http.createServer の振る舞いを変える必要があります。

脚注
  1. https://nodejs.org/dist/latest-v16.x/docs/api/http.html#serverkeepalivetimeout ↩︎

  2. https://docs.aws.amazon.com/elasticloadbalancing/latest/application/application-load-balancers.html#connection-idle-timeout ↩︎

Discussion

stefafafanstefafafan

こちらの記事とても参考になりました。ありがとうございます。

一点、自分のブログにも書いたのですが、Next.js 12.2以降はパッチは不要で以下のように簡単に設定できるようになっているようです。(この記事を後から参照した方向けにもコメントしています)。

next start --keepAliveTimeout 61000

詳しくはこちらのPull Requestをご確認ください。
feat(cli): allow configuration of http-server's timeout configuration by Miikis · Pull Request #35827 · vercel/next.js

mythosilmythosil

ありがとうございます!ようやく正規の機能として入ったんですね。
記事冒頭にも追記しました。