サーバーを凍結させた、スワップファイルの欠如
9月24日、本番サーバーの1台が応答しなくなりました。ロードバランサーはそのサーバーを異常とみなし、リモートコマンドのツールも接続できませんでした。しかし、何かがクラッシュしたわけでも、メモリの使いすぎでプロセスが殺されたわけでもありませんでした。サーバーはスラッシングしていたのです。スワップ領域がなく、コンテナがマシンの持つメモリより多くを使うことを許されていたため、Linuxは実行に必要なファイルの追い出しと読み直しを繰り返し、ほとんど進まなくなっていました。すべてのサーバーに2 GBのスワップファイルを追加すると、問題は解決しました。より興味深い教訓は、ヘルスチェックに関するものでした。
「フリーズ」の見え方
- ロードバランサーのヘルスチェックが、そのサーバーでタイムアウトしました。
- AWS Systems Managerを通して送ったコマンドは「Pending」のまま、実行されませんでした。
- ログにはメモリ不足によるkillがありませんでした。プロセスは1つも死んでいませんでした。
1週間前、私たちは本番環境を、2 GBのメモリを搭載した小さなARMサーバーに移しました。ステージング環境のサーバーはさらに小さく、デプロイ中のコンテナイメージの展開中に、同じように凍結していました。
なぜメモリのエラーが出なかったのか?
Linuxは、回収できるメモリが本当に尽きたときにしかプロセスを殺しません。それより前に、ページキャッシュからメモリを回収します。ページキャッシュとは、RAMに保持されているファイルのコピーのことで、実行中のすべてのプログラムのコードも含みます。
私たちのサーバーでは、コンテナのメモリ上限の合計が約3.3 GBでしたが、使えるのは約1.8 GBのマシンでした。通常、すべてが上限まで使われることはありません。しかし、そうなると、カーネルはメモリを空けるためにプログラムのコードをキャッシュから追い出し、そのコードが再び実行される瞬間に、ディスクから読み直す必要が生じます。このサイクル、つまりスラッシングは、技術的にはメモリ不足にならないまま、マシンをほぼ完全に無反応にしてしまいます。そのため、何も殺されず、何も回復しません。
スワップがなければ、カーネルは割り当て済みだがめったに使われないメモリを置く場所がありません。そのためキャッシュから奪うしかないのです。
修正
サーバーのブートストラップスクリプトは、サーバーが起動するたび、または修復されるたびに、2 GBのスワップファイルを、低いswappiness(10)の設定とともに作成するようになりました。swappinessが低いとは、カーネルが作業用のメモリをRAMに保持することを依然として優先する、ということです。ただし、めったに触られないページはディスクに移せるため、プログラムのコードを無理にキャッシュから追い出す必要がなくなります。これを、本番の2台とステージングに稼働中のまま適用しました。さらに、本番デプロイのたびにデプロイパイプラインがブートストラップスクリプトを再公開するため、置き換えられたサーバーにも適用されます。
より私たちを不安にさせたこと:応答しないサーバーが残っていた
私たちのサーバーグループは、EC2のヘルスチェック、つまり「マシンは起動しているか?」を使っていて、ロードバランサーのヘルスチェック、つまり「リクエストに応答しているか?」は使っていませんでした。そのため、ロードバランサーが見限ったサーバーも、グループから見れば正常なままでした。そのサーバーは決して置き換えられませんでした。誰かが気づいて、もう1台を再起動するまで、本番は静かに1台だけで動き続けていたでしょう。
グループの設定をロードバランサーのヘルスチェックに切り替えるのが、一見すると明白な修正です。しかし、私たちにとっては簡単ではありません。デプロイ中、サーバーは意図的にヘルスエンドポイントでエラーを返し、ロードバランサーがトラフィックを切り離せるようにしています(デプロイで502をゼロにした方法を参照)。その信号に応じてサーバーを置き換えるグループは、すべてのデプロイの途中でサーバーを置き換えてしまうことになります。
そこで当面は、デプロイのチェックリストに手順を1つ加えました。デプロイのたびに、デプロイジョブが成功したかどうかだけでなく、ロードバランサー上で各ターゲットのヘルスを確認します。サーバーへのコマンドが「Pending」のまま残っていれば、そのサーバーはスラッシングしています。再起動してください。すでにローテーションから外れているので、それで問題ありません。
得られた教訓
- 「クラッシュしていない」は「健全」とは違います。 スラッシングはエラーを出しません。応答しなくなるほどの極端な遅さを生むだけです。
- 小さなサーバーにはスワップが必要です。 わずかな量でも、swappinessを低くすれば、カーネルは使われていないメモリを置く場所を得られます。
- 合計が実際のマシンを超えるメモリ上限は賭けです。 時にはその賭けに負けます。賭けたことを自覚しておいてください。
- オートスケーリングがどのヘルスチェックを信頼しているかを知ってください。 「VMは起動しているか?」しか尋ねないヘルスチェックでは、機能していないサーバーは置き換えられません。