LVSの高負荷対策 その2 ~障害の再現とその原因~
こんにちは。インフラ担当の岡村です。 「LVSの高負荷対策 その1 ~障害発生~」の記事で、大量のSYNパケットを受信した際にロードバランサの再起動が発生したことと、その緊急の対策についてご紹介しました。 今回は、再現確認を行い判明した再起動の原因と、LVSに備わっている高負荷対策の機能についてご紹介します。
検証
前回ご紹介した通り、障害発生時のログからメモリ周りが怪しそうでした。 そこで、ロードバランサにSYNパケットを送り、メモリの使用量の推移を観察しながら、再起動が発生するかどうかを確認しました。 検証環境の構成は次のようになります。
検証環境の構成
パケット送信用サーバを複数台、ロードバランサを1台、Webサーバを1台使用し検証を行いました。 ロードバランサの検証を行う上で、本番環境と同様にロードバランスの処理をさせたかったため、LVSに振り分け先のWebサーバのIPアドレスを複数登録しています。 DSASのロードバランサではDSR(Direct Server Return) 方式を採用しており、Webサーバからの戻りパケットはLVSを経由しません。本検証を行う上ではWebサーバはパケットを返す必要はないので、ロードバランサから振られたパケットはWebサーバでdropしています。 パケット送信にはhping3を使用しました。オプションが豊富で様々なパケットを送信することができます。今回は障害時に検出したパケットに似せて、次のコマンドでwindowサイズ0のSYNパケットをロードバランサのVirtual Service宛に大量に送信しました。
hping3 -S -w 0 “Virtual ServiceのIPアドレス” -p 80 --rand-source --floodDSASのロードバランサは、64bitOSのものだけではなく、32bitOSのものが残っている状態でした。今回の問題が発生したのは32bitOSの方だったため、まず32bitOSで検証を行いました。 32bitOSでの検証
上述の通りhping3を使用して、複数のサーバからロードバランサに向けて一斉にSYNパケットを送信します。 SYNパケットが届くと、IPVS ( LVSのレイヤー4の負荷分散機能を提供するカーネルモジュール ) はSYN_RECV状態のエントリを作成します。そのためSYN_RECV状態のエントリがどんどん増加していき、それに伴いLowメモリの使用量が増加していきました。 Lowメモリの使用量の推移を確認したところ、パケット送信前の状態では約750MB あったLowメモリの空き容量は刻々と減少し、最終的に枯渇してカーネルパニックが起こり、再起動が発生しました。
Kernel panic - not syncing: Out of memory and no killable processes...
64bitOSではLowメモリの制限がなくなるので耐性は向上すると予想できます。しかしメモリが枯渇すれば同様に再起動が起こりそうです。64bitOSを使用して、もう少し詳しくロードバランサの挙動を追っていきます。
64bitOSでの検証結論から言うと、IPVSのエントリが増加しメモリが枯渇すると、やはり再起動が発生しました。ログを確認してみると本番環境での再起動の時と同様、再起動の直前でoom-killerが多発しており、また、カーネルのバージョンの違いから若干出力は異なりますが、IPVSのメモリ割り当てのエラーが出ていました。
IPVS: ip_vs_conn_new(): no memory
本番環境で起きた再起動も、IPVSのエントリが増加することによるメモリ枯渇に起因すると考えて良さそうです。 負荷試験中のSYN_RECV状態のエントリ数と消費メモリの推移を確認し、グラフにしました。(グラフ1) 今回の検証で使用したサーバはメモリを8GB搭載しており、負荷をかける前のMemFree値がおよそ7GBであったことから、以下の全てのグラフの「消費メモリ」の値は、「 7GB - 観測したMemFree値 」で算出しています。

負荷をかけ始めてから約49秒で消費メモリが7GB、すなわち、8GBのメモリを使い切り、再起動が発生しました。 さて、IPVSの挙動を確認するため、先の試験より負荷を抑え、再起動が起きなかった場合のグラフを見てみましょう。(グラフ2)

およそ60秒間はエントリ数とメモリ消費量は増え続けますが、それ以降は一定の値で推移しているのがわかります。 これは、SYNパケットが届くとIPVSはSYN_RECV状態のエントリを作成しますが、SYN_RECV状態のままだと60秒でタイムアウトになり、エントリが削除されるためです。 今回の試験では一定の強さの負荷をかけ続けたため、60秒以降は「タイムアウトで削除されるエントリ数」と「新規に作成されるエントリ数」が釣り合い、一定になったと考えられます。 (ピークが60秒から少しずれているのは、複数のサーバからパケットを送って負荷をかけているため、それぞれのサーバで負荷の開始時刻の誤差があったためです)
対策について
64bitOSでも再起動は発生したものの、32bitOSの場合と比較すると大幅に負荷耐性が向上しました。32bitOSでLowメモリが制限されてしまっている環境の場合は、64bitOSに変更することで耐性を上げられます。その上でメモリを追加すれば更に負荷耐性が上がり、再起動の対策になりますね。 では、他に有効な対策はないでしょうか? 調べてみると、LVSは高負荷対策の機能をいくつか備えているようです。(参考) 今回は、そのうちの機能の一つであるdrop entry機能を使用してみたのでご紹介します。
drop entry 機能の紹介IPVSのエントリをランダムに削除してくれる機能です。 SYN-RECV状態とSYNACK状態のエントリを削除するアルゴリズムは、毎秒IPVSのコネクションハッシュテーブルからランダムに選んだ範囲(全体の32分の1)をスキャンし、その中のSYN-RECV状態、SYNACK状態のエントリを削除する、というもののようです。 ESTABLISHED状態のエントリとUDPのエントリは共に、次の2つの条件を両方とも満たしている場合に削除される可能性があります。 ・最後のパケットが届いてから60秒以上経過 ・最初のパケットが届いてからの受信パケットの合計数が8以下 受信パケットが8以下の場合でも、受信パケット数が大きくなるに連れて削除される可能性は低くなるようです。 drop entryは、使用可能なメモリ量が設定した閾値を下回ったときに、自動で有効にすることが可能です。その場合は、以下に1(または2)を設定します。
/proc/sys/net/ipv4/vs/drop_entry
メモリの空きが閾値を下回ると自動的に値が2になり、機能が有効になります。 ( 閾値を下回っていないときに、2を設定すると、自動で1になります。) また、閾値によらず常にdrop entryを有効にしたい場合は、3 を設定します。 閾値は以下で設定可能です。
/proc/sys/net/ipv4/vs/amemthresh
単位は memory pageなので、例えばメモリが残り1GB(=1048576KB)を下回った時に有効にしたい場合は、"1048576KB÷(ページサイズの)4KB"を計算して、262144 を設定します。
drop entry有効時の動作確認閾値を6GB,3GB,1GBとし、SYNパケットを送って負荷をかけたときの、SYN_RECV状態のエントリ数と消費メモリの値の推移をそれぞれグラフにしました。


負荷は上の64bitOSの検証で再起動を発生させたときと同じ強さなので、drop entryなしでは再起動してしまいます(グラフの青線)。しかし、drop entryを有効にすると、3つのどの閾値の場合も消費メモリを抑えることができ、再起動は起こりませんでした! 例えば閾値を3GBに設定したときの推移(グラフの黄線)を見ると、消費メモリが4GB、すなわち残りのメモリが3GBになった時点から、エントリ数とメモリ消費量の増加が緩やかになっているのがわかります。 閾値を大きくすればするほど、消費メモリの最大値は小さくなり、耐性が向上することも見て取れます。 ただ、drop entryを有効にすれば必ずしも再起動を防げるというわけではなく、負荷に対して閾値を小さくし過ぎると、例えば上の環境で閾値を150MBにすると、drop entryが間に合わず再起動が起きました。また、負荷と比較して搭載メモリが少ない場合は、drop entryを常に有効にしていてもメモリが枯渇する可能性がある点も注意が必要です。 しかし、メモリ増強などの追加投資なしで、すぐに耐性を上げることができるので、とても便利な機能です。 もちろんdrop entry有効時には、IPVSを経由するどの接続もエントリ削除の影響を受ける可能性がありますが、ロードバランサが落ちてサービス停止してしまうよりはずっと良いのではないかと思います。LVSを使用されている方は是非お試しください。 次回、LVSの高負荷対策 その3でも、引き続きdrop entryの検証結果を紹介しようと思っています。