運用しているサービス(Perl)でなんかconnectionを貼るまでの時間がかかっているケースがあったので調べてみた。
memcachedのアーキテクチャ
memcachedはマルチスレッドでクライアントからのgetやsetを処理していきます。基本的なやり取りはこのワーカスレッドが行います。これに加えていろいろな管理スレッドもバックグラウンドで動作します。evictionの処理を行ったり統計情報を取ったりLRU Tuningとかいろいろあります(詳細はこちらかソースを読むしか無いみたいです)。この内今回取り上げるのはmainスレッドになります。(memcachedがそう呼んでるわけではないですがここでは便宜上そういう名前をつけてます)。
tcpサーバのaccept
マルチプロセスやマルチスレッドもでるのtcpサーバではacceptをどこでやるかでアーキテクチャ次第です。apacheやnginxでは同一ポートを子プロセス全員でaccept(2)を実施します。今回取り上げるmemcachedではこのmainスレッドがacceptを実施しています。仕組みとしてはmainスレッドがepoll_wait(2)でクライアントからな接続を待ち、接続が来たらaccept(2)を実行します。workerスレッドはこのaccept(2)されたsocketが届くのをepoll_wait(2)で待ち到着したらクライアントとの通信を行い始めるという仕組みです。
遅延してたのは何故?
コネクションを大量に接続&切断するプログラムがいたためメインスレッドの処理が追いついていなかったためと言う結論で調査を終えました。メインスレッドは1スレッドで頑張ってaccept(2)を実行するので大量に接続&切断をされるとconnect(2)するのに時間がかかるケースがあります。その線で調査していくとどうやらめちゃめちゃクエリを実行するクライアントがコネクション永続化の処理を行えていないことがわかりました(Perl難しい...)。それを直したら再発しなくなりました。よかったよかった。
ちなみにnet.core.somaxconn
あたりを大きくすることでconnect(2)の遅延はなくなるので最終手段に取っておきましたが使わずに済みましたw(大きくしたところでOSでコネクションを持ってくれるだけでユーザー的には何も嬉しくないですがエラーは減るという)。 あとはTCP_NODELAYあたりが使えるかも?と思ったがmemcachedはすでに設定済みだった。
ソースを読む
mainスレッドは以下のループでイベントを処理していきます。
int main (int argc, char **argv) { /* enter the event loop */ while (!stop_main_loop) { if (event_base_loop(main_base, EVLOOP_ONCE) != 0) { retval = EXIT_FAILURE; break; } }
3-way-handshake済みのsocketがある場合は以下のコールバックが呼ばれます
// コールバックはこの辺で設定されている conn *conn_new(const int sfd, enum conn_states init_state, const int event_flags, const int read_buffer_size, enum network_transport transport, struct event_base *base, void *ssl, uint64_t conntag, enum protocol bproto) { conn *c; // 省略 event_set(&c->event, sfd, event_flags, event_handler, (void *)c); event_base_set(base, &c->event); c->ev_flags = event_flags; // 実際のコールバック処理 void event_handler(const evutil_socket_t fd, const short which, void *arg) { conn *c; c = (conn *)arg; // c->state = conn_listeningが入ります assert(c != NULL); c->which = which; /* sanity */ if (fd != c->sfd) { if (settings.verbose > 0) fprintf(stderr, "Catastrophic: event fd doesn't match conn fd!\n"); conn_close(c); return; } drive_machine(c); // accept(2)はこちらで呼び出されます /* wait for next event */ return; }
event_handler関数からdrive_machine関数が実行され、ここでコネクションにおける状態遷移等を管理を行っています。
static void drive_machine(conn *c) { bool stop = false; int sfd; socklen_t addrlen; struct sockaddr_storage addr; int nreqs = settings.reqs_per_event; int res; const char *str; #ifdef HAVE_ACCEPT4 static int use_accept4 = 1; #else static int use_accept4 = 0; #endif while (!stop) { switch(c->state) { case conn_listening: // 今回は新規接続なのでこのケースになります addrlen = sizeof(addr); #ifdef HAVE_ACCEPT4 if (use_accept4) { sfd = accept4(c->sfd, (struct sockaddr *)&addr, &addrlen, SOCK_NONBLOCK); } else { sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen); } #else sfd = accept(c->sfd, (struct sockaddr *)&addr, &addrlen);