TCP/IP経路で利用するソケット通信におけるTIME_WAITを理解するためのメモ
自分では、インフラを見て回ることが多くなりつつあります。さすがに、小手先の対応が多くて、ちゃんとした体系的知識を持たないとなあと思いつつも、なかなかとそういう勉強を保つモチベーションが保てなかったりして、本当にグズだなあと自分のことを思う日々です。
ここ最近のことなのですが、何かしらのデータベースとのやり取りをするにあたって、多くの場合はsocket
を介してやりとりするパターンが多い気がします。Pythonには標準ライブラリとしてsocket
というそのものがあったりするのですが、どうもこのsocket
を利用したライブラリで、変な使い方をすると困ったことが起きてしまいます。
とはいえ、socket
自体に問題があるのではなく、自分なりに調べた感じですと、それはある変な使い方と組み合わさると問題がおきる、ということがわかってきました。
自分がぶち当たったのは、サーバーのリソースが十分に確保されているのにも関わらず、やたらとAPIの反応速度が遅い状態が続くことでした。色々コマンドを叩き、頭を悩ませ、netstat
を叩いてみると、たくさんのTIME_WAIT
がコネクションを専有していたのでした。
確かに、普段からパソコンをいじっている人間にとって、「サーバーリソース」という点で思いつきやすいのは、「CPU」であったり「メモリ」であったり、あるいは「ディスクI/O」であったり、という部分に関しては思いつきやすかったのですが、例えばこのの記事にあるように、第四のボトルネックである「TCPコネクション」には思い至らず、その周囲をぐるぐると回り続けていたのでした。
これに関しては、本当に自分のインフラ力の無さを実感したのと同時に、実際のところ、この大量のTIME_WAIT
は何なのかも理解していない。いったい、これは接続側でなんとか出来るのか、それともTCPの関係上、必然的に生まれてしまうものなのか、ということをまず調べる必要が出てきました。TIME_WAIT
だから、たぶんなんかすればcloseするんじゃないかと。
で、手始めにそのあたりの資料を集めてみました。例えば下の三つの記事が参考になりました。
- http://vincent.bernat.im/en/blog/2014-tcp-time-wait-state-linux.html
- http://www.kt.rim.or.jp/~ksk/sock-faq/unix-socket-faq-ja-2.html#time_wait
- http://qiita.com/kuni-nakaji/items/c07004c7d9e5bb683bc2
問題はTCP
が「確実に通信できること」を目的に実装されている、と理解しました。もうぼんやりとして忘れてしまっていますが、職業訓練を受けてたさいに、「TCPがスリーウェイハンドシェイクを行って、お互いに通信できる状態になってから、通信を開始する」という説明を聞いたことがありますが、それもTCP
の確実性を高めるための方法だという風に理解しています(逆にUDP
は、「ある程度パケットは喪失してもいいから、とにかくデータを送信したい」という場合に利用されることが多い、という風に理解しています。例としてはIP電話や生配信系などはそういう印象です)。
そこで、TCP
はお互いのやりとりを確実にするために、お互いがひっきりなしに「何を受け取ったのか」「何が欲しいのか」ということをやりとりしている。例えば、データを送受信するさいに、「シーケンス番号」と「確認応答番号」を交換しあうことで、確かにお互いのパケットがちゃんと欠落なく届いていることを確認している。
とはいえ、どんな通信も繋げっぱなしにすることなく、ある時点で終了する必要があります。そこで、パケットとして「もう必要なものは出揃ったので切断してくださいよ」というパケットをクライアント側が送る必要がある。TCP
は慎重さを要求するので、サーバー側がちゃんとそのパケットを受け取ったかを、クライアント側で受け取る必要がある。そのあとに、クライアント側は「切断要求が届いたことを確認しましたよ」という合図を送る必要がある。
とはいえ、切断されたあとに「切断しましたよ」というパケットは送信できない筈です。サーバー側が接続を終了した時点で、その接続が終了したことをクライアントに伝えるすべは無いわけです。クライアント側は、サーバー側が本当に接続を閉じたのか、ということが解らないという問題が出てきます。
そこで、本当に閉じられそうな間、クライアント側はどうしても待機する時間が出てくる。また、さきほど、データをやりとりするさいに、まだ到達していなかったデータというのが存在していたりもする。そのデータがいるのにも関わらず、新しくコネクションを貼ると問題が出てきてしまう可能性もありうる。
なものだから、クライアント側において、TIME_WAIT
が発生するのは、その特質上仕方ないことである、と理解しました。
しかし、単純なコードでは、余程のアクセス数が無い限り、TIME_WAIT
はたまりません。問題になるのは、そのつどTCP接続を行うためのクライアントを生成している場合です。口で説明するのもあれなので、実際にTIME_WAIT
を大量に発生させるスクリプトを書いてみます。
import socket
def main():
print "Generate Socket !!!"
for i in range(10000):
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("127.0.0.1", 11211))
print "Done."
if __name__ == "__main__":
main()
たったこれだけのコードなのですが、netstat | grep "TIME_WAIT" -c
すると、10000以上のカウントが見れるようになる。
自分の理解が正しければ、クライアント側が暫くTIME_WAIT
になるのは、要件として必要であることはわかったのですが、しかしとはいえ目の前のTIME_WAIT
を裁かないことには死んでしまうというパターンも存在していることは間違いない。
自分が調べた限りですと、どうも三つの方法がある様子。
まず一つ。諦めてコードを直せ。クライントを再利用するように書きなおせ、というパターンです。それはそうで、クライアントを無駄に生成しているから、その分TIME_WAIT
が生まれているんだから、それを直す必要はあります。とはいえ、例えばdjango
ならばキャッシュのバックエンドを経由するほうが、あとあとサービス構成を変更するときにも便利です。
また、「俺はdjangoを利用してねーよ、bottleだbottle」みたいに、軽量フレームワークを使う場合には、何かのパッケージでグローバルにクライアントを貼っておく。そういう泥くさい方法が嫌いならば、それ専用のクラスを作り、シングルトンっぽく利用出来るようにしておいたほうがいいのかなあとか考えたりします。
少なくとも、クライアントをその都度生成していると、変なcall回数になれば途端にコネクションが足りなくなって死ぬので、クライアント数を抑える工夫は必要だと感じます。
とはいえ、コードを書き換えることで簡単に対処出来たり、あるいは「そんなの当たり前じゃねーか、それでもコネクションが圧迫するんだよ」というパターンも多いと思います。
二つ目の方法は、それこそサーバー側の設定を見直すことです。
三つ目の方法。これは自分みたいな人間だとうめいてしまうのですが、カーネルのリビルトです。コネクション数がネックになる場合、割とさっくりとリビルトしてしまう記事が多い印象を受けます。最終的にはカーネルをリビルトして反映させるのも理解する必要がありそうです……。
確かに、Webサービスを開発している最中というのは、あんまりこういったサーバーのコネクション数というのを意識しないし、普段の生活においても、足りなくて困るという経験もほぼ無いので、確かに意識しずらい部分ではあったなあというのは反省です。そういう通信周りは「まー、どうせライブラリ側でよしなにしてくれるだろう」みたいな考えてじゃぶじゃぶとコネクションを貼っては、あとで困ったことになったりする。本当にそういうのはよくない。
とはいえ、いろんなサービスが通信しあってサービスができているのは事実だし、それらの通信について何も知らないというのは色々と困ったことになるんだな、ということも実感しました。いくらそこらへんが抽象化されているから、といったようにアグラをかいていると、今回みたいなときにあたふたしてしまうなあと思いました。