naoyaの日記 RSSフィード

 | 

2007-09-29

Gearmanのやつ#2 12:59

もう少し深追い。id:naoya:20070928:1190974874 の通り、Gearman::Worker がリクエストを最後まで受信できないのを直すこと自体は sysread() 呼びだし一回で済ませようとしてるところを(無期限ブロックを回避しつつ)ループにするなり後述する MSG_WAITALL を使うなりすればよさそう。

ここから先に調べたのは二点

  • そもそも sysread() 1回で指定したサイズを読めないのは何で?
  • ソケットの受信バッファって結局何とかその周辺

というところ。以下、うんちくです。

sysread 1回で指定したサイズを読めないのは

「ソケットからの read(2) が、その指定したサイズどおりに読めないことがあってそこは不定」というのは FAQ だけども、じゃあ具体的にどういうときに指定したサイズどおりに読めないのか。ちゃんと説明できないなあ、ということで、深追い。

最初は id:naoya:20070928:1190974874UNIXネットワークプログラミング〈Vol.1〉ネットワークAPI:ソケットとXTI から引用した箇所にもあるとおり、

  • ソケットの受信バッファが溢れたとき(とあと例外時)に限り一回で読めない

という風に思ってたのだけど、それだけじゃないですね。もちろんソケットの受信バッファが溢れたときもそうなる(その場合 TCP の流量制御でクライアント側も送信で待たされる) んだけど、

TCP socket ではデータは単にバイトストリームとして扱われるので、例えば送り側が 2048 Bytes を 1 回の send() で送ったからと言って、受信側が 1 回の recv() で受け取れる保証はない。

転送の都合で send したデータが適宜切り分けられ、複数のパケットで送られる場合がある。そして recv() は「読め」と言われたバイト数が溜るまで待たず、少しでもデータが送られてくれば return する。相手が何バイト送ったかなどをチェックするようにはなっていないということである。

一般的には、これから送るデータのサイズを通知して、受け手の方でこのサイズを受け取るまで繰り返して recv() するという方法がとられるだろう。また、fdopen(sock) しておいて、fread() を使うという手もある。

プログラミング系 Q&A 2

ということでした。

もうひとつ Perlネットワークプログラミング―ソケットの使い方からクライアント/サーバーシステムの開発まで の P.22 から引用すると

sys* () 関数による stdio のバイパスには、実際のデータ量が要求されたデータ量に満たなかった場合に、read() 関数と sysread() 関数で振る舞いが異なるという効果もある。read() 関数の場合には、リクエストされたデータを正確に取得できるまで、関数は無期限にブロックされる。唯一の例外は、ファイルハンドルリクエストを完全に満たす前にファイルの終端に達した場合である。この場合、read() 関数ファイルの終端までのデータを返す。対照的に、sysread() 関数では部分的な読み取りが可能である。この関数は、リクエストされたデータをすぐに読み取れない場合、その時点で取得できるデータを返す。データがまったくない場合、sysread() 関数は最低でも1バイトを読み取るまでブロックされる。このため、sysread() 関数は、データが不確定なサイズで送信されることが多いネットワーク通信に欠かせない存在である。

とある。(上記は Perl の read() / sysread() の差異の話。sysread は C の read(2) に相当)

この二点を合わせると

という動きになる。

まとめると sysread() (もしくは read(2)) が指定したサイズ通りに帰らない条件は

の 5 つ。

ループで何度か read(2) を発行する意外に、この 5 つの条件を 3 つに減らす方法、つまり例外時以外は指定したバイト数分読むまでブロックすることもできて、recv(2) で MSG_WAITALL 使うなどがあるそう。(via UNP)

例えば Gearman::Util::reas_res_packet() の sysread のところを

        my $rv = recv($sock, $buf, $len, MSG_WAITALL);
        die $! if not defined $rv;
        my $buflen = length $buf;
        return $err->("short_body") unless $buflen == $len;        

と変更すると、件の問題はひとまず発生しなくなる。(がこの実装だと $len が意図しない値だった場合に無期限ブロックしてしまうのでその対処は必要です。)

ソケットの受信バッファって結局何とかその周辺

ソケットの受信バッファは setsockopt / getsockopt で SOL_SOCKET, SO_RECVBUF で見たりセットしたりできる。

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/socket.h>

int main ()
{
    int val;
    int fd = socket(AF_INET, SOCK_STREAM, 0);
    int len = sizeof(val);

    if (getsockopt(fd, SOL_SOCKET, SO_RCVBUF, &val, &len) == -1 ) {
        perror("failed to getsockopt");
        exit(1);
    }

    printf("%d\n", val);
    return 0;
}

こんな具合で。

% ./rcvbuf_size
87380

この 87380 という値は Linux の場合

% cat /proc/sys/net/ipv4/tcp_rmem
4096    87380   174760

と、ここで決まる。真ん中がデフォルトの値。TCP の流量制御の中でこの値は OS自動デフォルト値から減らしたり増やしたり調整をするようです。

この SO_RCVBUF の値がいつどういう風に使われるのか。TCP パケットの「Window フィールド」に埋め込まれて、パケットやりとりの中でサーバー / クライアントで交換されます。これでお互いに、相手にはあとどれぐらいの受信バッファが残っていてどのぐらい送れるか、というのが調整される。これ以上相手には送れない、という状態になったら送り手側はそこでブロック。これが TCPウィンドウ通知の機能。

この Window フィールドhttp://www.fuwafuwa.org/Lecture/learn/network_nepc/course2/chapter04/section05.html などを見てもわかるとおり 16 ビットしかありません。とういことは普通整数値をやりとりするのでは 65,535 バイトまでしか表現できない。そこでウィンドウスケール。TCP 接続確認時の ACK パケットに Options フィールドに3バイトデータ

を埋め込んで送りつけてやると、バイナリシフトカウンタで指定した値 (0 ~ 14) に従って Window サイズをビットシフトして計算するようになる。これで 65,535 x 2^14 の SO_RCVBUF の値をやりとりできるようになります。ウィンドウスケールに関しては TCPのしくみと実装―RFCの詳細から実装系の解析まで (TCPIP基礎シリーズ) が詳しかった。

ところで、このウィンドウスケールは TCP の 3ウェイハンドシェイクの時点で何ビットシフトするかを決定します。ということは、setsockopt(2) で明示的に SO_RECVBUF に 16 ビット以上の値を指定したい場合、SYN セグメントを飛ばす前 = connect する前ににセットする必要がある、とのことです。(ただ、Linux の場合はデフォルトで 65,535 以上の値を使ってるので、特に意識しなくてもバイナリシフトカウンタに正の値が指定されて3ウェイハンドシェイクが始まってるんではないかと思う。)

更に、SO_RECVBUF に指定できる値は Linux の場合その上限が

% cat /proc/sys/net/core/rmem_max
131071

で制限されています。また、ウィンドウスケールの有効/無効自体は

% cat /proc/sys/net/ipv4/tcp_window_scaling
1

と、ここで決まる。

最初はソケットの受信バッファ周りを疑っていて、そんで調べていたらこの辺にたどり着きました。せっかく調べたのでこちらもまとめてみました。

ということで

Gearman::Util のパッチを、と思ったけど Gearman 全体の処理の中でタイムアウトとかパケットサイズの上限をどう実装すべきか、というのが。

hideokihideoki2008/01/26 15:03てすと

stanakastanaka2008/01/26 15:04テスト2

 | 
この日記のはてなブックマーク数