Node.js(Express) サーバ運用と ELB タイムアウト

この記事は Node.js Advent Calendar 2019、15 日目の記事です。


こんにちは。ものづくり推進部の武田(@tkdn)です。

先日 11/30, 12/1 に弊社がスポンサードさせていただいた JSConf.jp に参加してきました。当日参加したセッションの雑多なメモはパブリックに残し、社内のコンフルには整理したものを展開し知見を持ち帰って実務にいかそうと思います。

会場廊下では、お世話になった方、知り合いのエンジニア、発表された方と立ち話する機会もありまして、情報交換や普段オンラインでのみやりとりしている方ともオフラインでコミュニケーションできて非常に充実したカンファレンスでした。

初日にスポンサートークの枠で 5 分程度ですが、mediba での フロントエンド, JavaScript についてお話させていただきました。表面的なことばかりだったのでもう少し泥臭い話もすればよかったかなと感じています(こういうのとかこういうのとか)。

日本での初開催に向けて尽力された運営の皆さま、本当にありがとうございます。


さて本題の記事ですが、今年度から Node.js でのサーバ運用をはじめてつまづきのあった、ロードバランサーと Express そのタイムアウト、ランタイムバージョンアップ後の問題、そして問題に対する課題感について書いています。

Node.js(Express) サーバ運用が始まる

2019 年 3 月に実施した au Webポータルのリニューアルには Next.js を利用していますが、もちろんそこには Express の存在も要るわけで、それに伴う新しい運用が始まるということでもあります。

正直なところ Node.js のサーバ運用経験があるメンバーが豊富にいたわけではないので、負荷試験・性能試験等で安全はもちろん担保の上で、運用に乗ってからいろいろ粗(というと言い方はよくないですが)は出るだろうなと思っていました。

ELB, Express のタイムアウトはきちんと確認しよう

au Webポータルの現アーキテクチャは CDN, AWS を利用しておりリクエストを受ける前段は

Akamai CDN -> ELB -> ECS クラスタ(Node.js コンテナ)

という形なのですが、リリース後すぐに Datadog のアラートとログから、極稀に ELB が 504 を返すことがあるということが分かりました。全体の1パーセントに満たないログです。

504 だとユーザ面に影響があるのではというツッコミが入りそうですが、最前段にある Akamai でオリジンが 5xx のステータスコードを返却する場合には正常時の stale cache を返却する構成になっているため、ユーザ面への影響はありません。ありませんが、これはこれで問題です。

504 を返していた原因としては Express 側の keepAliveTimeout を ELB に合わせたものにしていなかったというのが原因になります。リリース前に気付くべきことなのかも知れませんが、この問題については性能試験においても検出はされておらず、運用に乗せてから検出されたケースになります。

解決方法としては、ELB のアイドルタイムアウトのデフォ値が 60 秒なので、express 側は 60 秒以上にしておくといいかもしれません。

const app = express();
const httpServer = app.listen(3000, () => console.log('Example app listening on port 3000!'));

/** @note タイムアウトの設定 */
httpServer.keepAliveTimeout = 70000;

参考: HTTP | Node.js v12.13.1 Documentation(デフォルト値は 5000 となっています)

Node.js v8.x -> v10.x バージョンアップ

Node.js v8 の EOL は年内(2019-12-31)となっていますが、皆さんきちんとアップデートできていますか?

弊チームはギリギリでバタバタとやりたくなかったため、au Webポータルチームが抱えているプロジェクト群(一つではありません)で利用している Lambda の Node.js ランタイムを v10 へ移行するところから始めました。割と早い段階でランタイムアップデートを行うに至った経緯としては、Lambda ランタイムの AMI が更新 というアナウンスがあったため、同時に確認し追ってかかる運用コストを減らそうという意図もありました。

アップデートによるパッケージの影響や動作確認などを終え、Lambda AMI 変更・ランタイムバージョンアップはいくつか課題があったものの解消し 7 月段階で全て v10 に切り替えを完了しています。

その後 8月に Express を運用しているコンテナ内のランタイムアップデートへと作業を進めたのですが、Node.js のバージョンアップはアプリが依存するパッケージも多く影響範囲の調査だけで相当時間がかかります。そのため、ローカル環境コンテナ内ランタイムバージョンアップ、ステージング環境へのデプロイなどで実動作から確認するのが一番手っ取り早く、動作確認とステージング環境からのアラートがないことを確認し、バージョンアップ後の担保としました。

JavaScript = Node.js はブラウザのランタイムとしてスタートしている言語であること、Chrome に搭載されている V8 をエンジンとしていること、などから後方互換性がある程度保ったままメジャーバージョンが上がっていきます。そして偶数バージョンが LTS のリリースラインにあり、奇数はすぐ EOL をむかえるリリースプロセスになっています。

バージョンアップリリース後にタイムアウト再発

実働しているコンテナのランタイムバージョンアップのリリース後、見覚えのあるアラートが上がるようになりました。

前述の ELB タイムアウト問題の再発です。

この時も数パーセントのかなり低い割合で現象が発生していました。おそらく同様の問題であるような気がしていたのですが、いろいろ調べたところ下記の記事にあたり大変助かりました。

記事にある内容は前述のロードバランサーのタイムアウトと Exress のタイムアウト見直しと同じになりますが、末尾に重要な情報とリンクがあり、二つ目の issue に詳細が書かれています。

結果から言うと v10.15.2 以上の場合は server.headersTimeout の指定を上記の keepAliveTimeout で指定した数値より大きくする必要があります(指定がない場合、デフォルト値は 40000 です)。

const app = express();
const httpServer = app.listen(3000, () => console.log('Example app listening on port 3000!'));

/** @note タイムアウトの設定 */
httpServer.keepAliveTimeout = 70000;
httpServer.headersTimeout = 80000;

該当の issue で話されている内容ではありますが、Slowloris HTTP DoS 攻撃(不完全なヘッダーを送り続けてサーバのプロセスを圧迫するような攻撃です)に対する防止策のコミット 1a7302b から変更があるようです。

今回の件から見えてくる課題

運用の知見がなかったものは蓄積する他ありませんが、今回その中でも課題と感じることが出てきました。

  • バージョンアップの担保とは
  • 環境差異による再現性の低さ

ひとつめ、バージョンアップについてです。これは EOL がついてまわる言語を扱う以上向かい合わなくてはいけませんし、どこで安全性を担保するのか難しいところです。アップデートにより obsolute された API 等は調査で知り得るものの、既存で存在しユースケースの中で発生しえる現象については実働で確認するしかありません。

担保できる機能テストの自動化やステータスチェック等考えられることは多くありますが、何年も保持できる LTS バージョンはなく、比較的早いリリースサイクルに対してどう向き合うかは課題だと感じます。

ふたつめ、環境差異による再現性について。こちらについては、ステージング環境でタイムアウトの現象を検出できなかった点が課題と感じています。今段階でも再現せよというお題に明確な解答が出せていません。プロダクション環境で稀に出たアラートだったというところで片付けてもいいのかも知れませんがなんだかモヤります。


具体的な回避策については記述したとおりではあるものの、あまり決定的な解がなくもんやりしつつ、以上 ELB のアイドルタイムアウトと Express(Node.js)サーバのタイムアウトについて武田が書きました。

mediba ではエンジニアだけでなく一緒にプロダクトを良くしていくメンバーを募集中です。特に TypeScript でサーバサイドを書ける方や React に取り組みたい方がいらっしゃるとわたしが一方的に嬉しいのでぜひよろしくお願いいたします。

1 note

  1. nekonyanko reblogged this from mediba-ce
  2. mediba-ce posted this