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

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ホスト上で稼働する各コンテナのリソースを適切に制限する用途なのではないかと個人的には見ています。なのでコンテナ内でガツッと上げるという使い方はちょっと違うのかなと。

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

まとめ

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

Kibanaのグラフ作成をYoutubeの動画で学習する

Log Elasticsearch Kibana

サーバのログをElasticsearchに送信し、Kibanaでログを閲覧したり可視化するっていうのはもはや当たり前になった感がありますね。

www.elastic.co

Visualizeを使いこなしてこそKibanaの本領発揮なわけですが、KibanaではArea ChartやLine Chart、Pie Chart等の様々なグラフを自由に作成できるというのもある一方、グラフ作成に慣れるまでは少し大変かもしれません。

グラフの作り方に関する記事は各所に出回っていますがグラフを作るという性質上、論を学ぶより実際に作っている様子を見たほうが習得は圧倒的に早いです。

Elastic公開のチュートリアル動画

実はYoutubeでElasticの公式アカウントが、Kibana 4 Tutorialsというリストを公開しています。

www.youtube.com

リストには現在11本の動画が公開されていて、各種のグラフの作り方やケーススタディを視覚的に学習できます。長い動画ではないのでポイントをかいつまんで効率的にグラフ作成の間隔を掴めるかと思います。

その他オススメ動画

公式ではないですが、このような学習動画は他にもあるので何個か雑に紹介しておきましょう。さらっと見て雰囲気を掴めればOKです。

www.youtube.com

www.youtube.com

www.youtube.com

okhttpでSocks Proxy経由のHTTPリクエストをする

Tips HTTP Java Kotlin

備忘録的小ネタ。

動機

  • ネットワーク的に制限されている社内基盤のAPIを利用するプログラムを書いているが、オフィスのネットワークからじゃ直接アクセスできないので通信が許可されているサーバからじゃないとHTTPでアクセスできないのでタルい
  • Mock使ってテスト書くわけだけど、それ以前に実際にリクエスト投げてトライアンドエラーしたい。

このようなネットワーク制限を飛び越えてHTTPアクセスするためによく利用されるのがHTTP Proxyであり、それをSSH経由でセキュアに実現するのがSocks Proxyです。

そもそもSocks Proxyとは

素のHTTP Proxyとは違い、踏み台まではSSHトンネルすることでクライアントから目的地までセキュアにHTTPリクエストをすることができる仕組みです。イメージは以下のような感じですね。

クライアント <==SSH===>踏み台<===HTTP/HTTPS===>目的の閉じられたサーバ

Socks Proxyを立てる

まずローカルにSocks Proxyを立てます。そして目的地となるAPIサーバに実際にHTTPリクエストを投げる踏み台となるサーバを用意します。 以下のようなsshコマンドで1080ポートがSocks ProxyのポートとしてListenします(-f -Nオプションをつけてバックグラウンド実行させてます)

ssh -f -N -D localhost:1080 username@your_fumidai

1080がListenしたら、curl等で実際にリクエストできるかを確認してみるとよいですね。

curl --socks5 localhost:1080 http://your_closed_host/api/hogehoge

okhttpでリクエストする

okhttpといえばJava界(というかAndroidメインだと思うけど)で広く利用されているHTTPクライアントです。

square.github.io

ちょっと今JVM系言語であるKotlinを使って書いているので、こんな感じでSocks Proxy経由でHTTPリクエストができるようになります。

class SocksTest {

    @Test
    fun testRequest() {

        val socketAddress = InetSocketAddress("localhost", 1080)
        val proxy = Proxy(Proxy.Type.SOCKS, socketAddress)

        val client = OkHttpClient.Builder().proxy(proxy).build()
        val request = Request.Builder()
            .url("http://your_closed_host/api/hogehoge")
            .build()

        val response = client.newCall(request).execute()
        println(response.body().string())
    }
}

OkHttpClientにproxyで、Socks Proxy情報を持つjava.net.Proxyオブジェクトを渡すだけです。あとはいつものokhttp。Javaでやりたい場合はJavaに置き換えてください。 別にokhttpじゃなくとも、proxyサポートしてるHTTPクライアントであれば同じ感じで応用が効くと思われます。

おわり。