仙石浩明の日記

2006年4月1日

stone 2.3 が遅い理由

epoll 化にあたって stone 2.3 のコードの全面的な見直しを 行っていたところ、性能低下を引き起こす問題点を発見。

stone 2.3 において、TCP 接続を accept し、 中継先へ connect する部分の構造は、 次のようになっている(説明のため大幅に単純化している)。

fd_set rin;

main() {
    ....
    for (;;) repeater();
}

void repeater(void) {
    ....
    rout = rin;
    ret = select(FD_SETSIZE, &rout, &wout, &eout, timeout);
    ....
    if (FD_ISSET(stone->sd, &rout)) {
        FD_CLR(stone->sd, &rin);
        ....
        ASYNC(asyncAccept, stone);
    }
}

void asyncAccept(Stone *stone) {
    ....
    nsd = accept(stonep->sd, from, &fromlen);
    FD_SET(stonep->sd, &rin);
    ....
}

つまり、select(2) によって listen しているポート (stone->sd) が accept 可能であることが判明すると、 まず rin の該当ビットをクリア(FD_CLR)し、 accept 処理を行う子スレッドを立てる(asyncAccept)。 このスレッドは accept(2) を呼び出し、 rin の該当ビットを再セット(FD_SET)する。

ところが、子スレッドと親スレッドのどちらが先に処理されるか、 という問題がある。 子スレッドが先に実行されるなら、 rin の該当ビットが再セットされた後、 親スレッドの select(2) が実行されるので問題は起きないが、 普通のスレッド実装では、 親スレッドの実行が優先されるようだ。

つまり、親スレッドの処理が先に実行されると、 rin の該当ビットがクリアされたままで、 親スレッドが select に突入してしまう。 親スレッドが select 待ちになってから、 子スレッドが rin を再セットするが、 時すでに遅し、 親スレッドが呼び出した select では、 listen しているポート (stone->sd) は 監視しない状態になってしまっている。

このため、この select が timeout して、 次回の select 待ちに入るまで、 接続を受け付けない状態が続いてしまう。 timeout は 0.1秒なので、 秒あたり高々10回の接続しか受け付けられなくなってしまう。

この問題を回避するには、 子スレッドで accept するのではなく、 親スレッドで accept すればよい。 この場合、rin の該当ビットをクリアする必要もなくなる。

で、なぜ epoll 化したことによって高速化されたかというと、 epoll の場合、rin に相当するものがカーネル空間にあるわけで、 子スレッドで EPOLL_CTL_MOD (FD_SET に相当) すると、 即それが epoll_wait 待ち (select 待ちに相当) している 親スレッドに反映するためだろう。

Filed under: stone 開発日記 — hiroaki_sengoku @ 18:37

No Comments »

No comments yet.

RSS feed for comments on this post.

Leave a comment