Saturday, February 9, 2013

TCPの再送タイムアウトを制御したい


TCPの再送タイムアウトで最近まで知らなかったことがあったのでメモ。

たとえば、APサーバとDBサーバがあるとする。
AP-DB間のDBCPで使うTCPコネクションは、DBサーバがノードダウンしたのであればさっさと再送リトライをあきらめて切れてほしいと思うのが普通だろう。
このTCPの再送リトライ処理は、何もいじらないと15分以上続くので、
できればTCPコネクション単位で細かく調整させてほしいと思うのは人情だと思う。

Linux の場合、昔はこの調整をしようと思うと、sysctl を使って /proc/sys/net/ipv4/tcp_retries2 の値を書き換えるしかなかった。
これは、リトライ回数でしか指定できず、そのリトライ間隔が回数ごとに変わっていく(長くなる)のでわかりにくい上に、そのOS上の全TCPコネクションで有効になってしまうという問題があった。つまり、このオプションは「TCPコネクション切れやすさ」を調整しているとも言えるので、例えばインターネット向けと内部LAN向けのTCPコネクションは別の切れやすさの設定にしたい…といった場合に困ったことになるのだった。

なお、tcp_retries2 の指定値によるタイムアウト時間の変化については、日本語でも詳しい解説がたくさんある。例えば以下の記事とか。(あれ?良く見てみたら、@int128 さんの記事じゃん... :o)

http://d.hatena.ne.jp/int128/20100514/1273865819


さて、Stackoverflow で見つけた以下の記事に紹介があるのだが、

http://stackoverflow.com/questions/5907527/application-control-of-tcp-retransmission-on-linux

linux-2.6.37 から、setsockopt(2)で指定可能なオプションに TCP_USER_TIMEOUT というものが追加されている。

upstream の commit log はこれ。

http://git.kernel.org/?p=linux/kernel/git/torvalds/linux-2.6.git;a=commitdiff;h=dca43c75e7e545694a9dd6288553f55c53e2a3a3


これを使うと、TCPコネクション単位で、しかもリトライ回数ではなくミリ秒単位で、TCPの再送処理をあきらめて ETIMEDOUT でエラーにするまでの時間を調整できる。
socket API としては、Linux 固有機能になるので移植性はなくなるのだが、現実問題としてはとても便利な機能である。


ここからはやや余談。

setsockopt() で IP や TCP のパラメータをいじる時には、

       #include <sys/types.h>
       #include <sys/socket.h>
       #include <netinet/in.h>
       #include <netinet/tcp.h>

といったシステムヘッダを使うことになるのだが、少なくとも Ubuntu 12.10 (kernel は3.5系)では、本来あるべき /usr/include/netinet/tcp.h で TCP_USER_TIMEOUT は未定義なので、コンパイルエラーになってしまう。

ではどこで定義されているのか?と言えば、 以下のように /usr/include/linux/tcp.h に定義が存在する。

 /usr/include/linux/tcp.h より

102 #define TCP_QUICKACK            12      /* Block/reenable quick acks */
103 #define TCP_CONGESTION          13      /* Congestion control algorithm */
104 #define TCP_MD5SIG              14      /* TCP MD5 Signature (RFC2385) */
105 #define TCP_COOKIE_TRANSACTIONS 15      /* TCP Cookie Transactions */
106 #define TCP_THIN_LINEAR_TIMEOUTS 16     /* Use linear timeouts for thin streams*/
107 #define TCP_THIN_DUPACK         17      /* Fast retrans. after 1 dupack */
108 #define TCP_USER_TIMEOUT        18      /* How long for loss retry before timeout */
109 #define TCP_REPAIR              19      /* TCP sock is under repair right now */
110 #define TCP_REPAIR_QUEUE        20
111 #define TCP_QUEUE_SEQ           21
112 #define TCP_REPAIR_OPTIONS      22

ただし、ここで問題があって、/usr/include/linux/tcp.h は、device driver 等の kernel module が使うものであって、基本的にユーザプログラムが使ってはいけないことになっている。

ではどうすればよいのか?

実は、TCP_USER_TIMEOUT はマクロでしかないので、18 と直書きする…のがあんまりだと思えば、以下のようにしておくと、将来 netinet/tcp.h を include するだけで TCP_USER_TIMEOUT が使えるようになった時にもそのままビルドできるし、可読性も確保できる(はず)

(動作確認用に作った toy sample より)
  1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <string.h>
  4 #include <unistd.h>
  5 #include <sys/socket.h>
  6 #include <netinet/in.h>
  7 #include <netinet/tcp.h>
★ここ↑で代わりに <linux/tcp.h> を include してはいけない。
  8 #include <arpa/inet.h>
  9 #include <errno.h>
 10
 11 int main(int argc, char *argv[])
 12 {
 13         int sockfd, ret, option;
 14         struct sockaddr_in saddr;
   :
 32 #ifndef TCP_USER_TIMEOUT
 33 #define TCP_USER_TIMEOUT 18
 34 #endif
 35         option = 5 * 1000;  /* 5 seconds */
 36         ret = setsockopt(sockfd, IPPROTO_TCP, TCP_USER_TIMEOUT,
 37                          (void *)&option, sizeof(option));
 38         printf("setsockopt TCP_USER_TIMEOUT returns %d/%d\n", ret, errno);
 39         if (ret < 0) exit(0);


参考までに Ubuntu 12.10 に付属している /usr/include/netinet/tcp.h は

itoumsn@gateway:~/src/linux-3.3.1-tag$ dpkg -S /usr/include/netinet/tcp.h
libc6-dev:amd64: /usr/include/netinet/tcp.h
itoumsn@gateway:~/src/linux-3.3.1-tag$ dpkg -l | grep libc6-dev
ii  libc6-dev:amd64   2.15-0ubuntu20  amd64  Embedded GNU C Library: Development Libraries and Header Files

という感じなのだが、いつごろ取り込んでもらえますかねぇ...

あともうひとつ気になるのは、RHEL6 の kernel にバックポートされてくれると嬉しいのだがなぁ...
まだ確認していないのだが、どうかなぁ...

TCP_USER_TIMEOUT 未サポートの kernel の場合は、setsockopt(2)が ENOPROTOOPT (92) でエラーになるようだ。
(fade out)

No comments:

Post a Comment