読者です 読者をやめる 読者になる 読者になる

Dockerホストのパフォーマンスを引き出すTCPカーネルパラメータチューニング

Docker Linux

もう半年くらいフルDockerでmicroservicesなサービスを運用してるんですが、イマイチパフォーマンスを出し切れていないなという面がありまして、今回DockerホストのTCPカーネルパラメータを抜本的に見直しました。

そしたら劇的に症状が改善して、インスタンス数も削減できた上に安定してメシウマ状態になったので紹介します。実際効果があったのでチューニングポイントとしてはある程度正解であったと考えていますが、もちろん扱ってるアプリケーションの特性にもよるはずなので一つのケーススタディであることをご了承頂ければと。

前提

まずは今回のお話の前提を。こんな環境です。

  • EC2 c3.xlarge
  • ホストはUbuntu(EC2 Optimized AMIは未使用)
  • Docker 1.11.2
  • MySQL(HAProxy経由)やRedisへのデータストアの通信、各microservicesへの通信多数
  • fluentdでelasticsearchやs3へのログの転送
  • ECSのTask構成はNginx + Node + Go + fluentd + HAProxy
  • REST APIやReactでのサーバサイドレンダリングを行う

秘伝のカーネルパラメータ

一言で言うと、社内の秘伝のカーネルパラメータでは足りなかったということに尽きます。実績のあるものでしたがそれなりに陳腐化してきており、今回のようなフルDocker構成で十分なパフォーマンスを出すには少し足りなかったということです。秘伝のタレがまったくダメというよりはもうちょっと高いレベルの話ですね。

具体的な症状は以下のような感じ。

スパイクにちょっと弱かった

スマートフォンにALLプッシュみたいなことをすると、一気にユーザーがやってくるためスローダウンやエラー率が上昇していた。ELBはPrewarmingしていて十分なリソースがあっても、バックエンドが足りないという状態。これはPushの時間が基本的にわかるので、Scheduled Autoscalingで回避していた。

ローカルポートが枯渇する

こちらもスケールや、アプリケーション側で通信を減らすチューニングで回避。Dockerを利用すると必然的にImmutable Infrastructure志向になって、fluentdで片っ端からログを転送することになります。また、microservicesだとどうしても外部通信も増えがちになります。また、どこかにボトルネックがあって場合にTIME_WAITが増えるようになった場合、通信の多いアプリケーションでは致命傷を負いやすくなります。

DockerホストでTCPカーネルパラメータを変更する

とりあえず今回変更したものはこちら。

net.core.somaxconn

TCPソケットの接続要求キューのキャパシティ。こちらは4096 -> 65535に。net.core.somaxconnの引き上げはISUCONでも効果的なチューニングポイントとして知られていますね。

net.core.somaxconn = 65535
net.ipv4.tcp_max_syn_backlog

一度に受け入れ可能なTCPセッション数の値。net.core.somaxconnと同じに設定。これはEC2だとデフォルトが128と少ないので潤沢に設定した方が良いと思われる。

net.ipv4.tcp_max_syn_backlog = 65535
net.core.netdev_max_backlog

キューに繋げるパケット最大数。こちらは2048 -> 16384に。net.core.somaxconn、net.ipv4.tcp_max_syn_backlog、net.core.netdev_max_backlogを揃って引き上げたことでスパイクへの耐性が増したと見ています。

net.core.netdev_max_backlog = 16384
net.ipv4.ip_local_port_range

この設定のデフォルトは32768 61000ですが、エフェメラルポートを目一杯利用できるように限界まで広げています。これはローカルポート枯渇対策の一つですね。

net.ipv4.ip_local_port_range = 1024 65535
net.ipv4.tcp_tw_reuse

デフォルトOFFだが、ONにするとコネクションを再利用するためTIME_WAITを減らす効果があると思われるが、見た感じそこまで変化は無かった。

net.ipv4.tcp_tw_reuse = 1

といった感じでチューニングしたところ、劇的に改善しました。

コンテナ側でsysctlを設定する是非について

基本的にDockerはホストのカーネルリソースをコンテナ側で共有します。そのためホストで目一杯リソースを開放すれば、そのぶんのリソースを上手くシェアしてくれるはずです。

コンテナのカーネルパラメータを上げるエントリがちょくちょく散見されていますが、ホストのリソースを十分に開放しないとコンテナ側でパラメータを上げても恩恵は得られないのではないでしょうか。今回最初に検証したときにコンテナ側のパラメータをprivilegedやvolume mountするなどして強引にあげるのも試してみましたが効果は見られませんでした。

また、Docker1.12系からdocker run時に--sysctlでカーネルパラメータが渡せるようになりましたが、じゃあこれの意義はいったい何なのかという話になるわけですが、どちらかというと十分なカーネルリソースを確保できているDockerホスト上で稼働する各コンテナのリソースを適切に制限する用途なのではないかと個人的には見ています。なのでコンテナ内でガツッと上げるという使い方はちょっと違うのかなと。

今回の変更はホストだけに行い、コンテナ内では何も施していませんが満足のいく成果が得られました。

まとめ

コンテナ化でアプリの作り方もガラッと変わったので、カーネルパラメータの設定を今一度見直しましょう💪