2014年9月16日火曜日

リクエスト処理の流れ: start_lingering_close_blocking

process_socket()でのソケットを閉じる処理を見ておく。

(httpd-2.4.9/server/mpm/event/event.c)
    881 static void process_socket(apr_thread_t *thd, apr_pool_t * p, apr_socket_t * sock,
    882                           event_conn_state_t * cs, int my_child_num,
    883                           int my_thread_num)
    884 {
     :
   1024     if (cs->pub.state == CONN_STATE_LINGER) {
   1025         if (!start_lingering_close_blocking(cs))
   1026             return;
   1027     }

1025行目のstart_lingering_close_blocking()が、ソケットを閉じる処理(shutdown(SHUT_WR))を行う。
引数はcs(event_conn_state_t情報)のみだが、cs->cは、conn_rec情報を持っている。



(9) start_lingering_close_blocking


(httpd-2.4.9/server/mpm/event/event.c)
    813 /*
    814  * Close our side of the connection, flushing data to the client first.
    815  * Pre-condition: cs is not in any timeout queue and not in the pollset,
    816  *                timeout_mutex is not locked
    817  * return: 0 if connection is fully closed,
    818  *         1 if connection is lingering
    819  * May only be called by worker thread.
    820  */
    821 static int start_lingering_close_blocking(event_conn_state_t *cs)
    822 {
    823     if (ap_start_lingering_close(cs->c)) {
    824         apr_pool_clear(cs->p);
    825         ap_push_pool(worker_queue_info, cs->p);
    826         return 0;
    827     }
    828     return start_lingering_close_common(cs);
    829 }

大きく2つの処理からなっている。
  • ap_start_lingering_close()
    shutdown(SHUT_WR)を実行する
  • start_lingering_close_common()
    LINGER待ちタイマーに登録され、ソケットをlistenerスレッドの監視対象に追加する。
start_lingering_close_blocking()の戻り値は0か1になる。
  • 1は、正常に処理
  • 0は、何らかの不具合が発生した場合
不具合発生時にはソケットは閉じてしまっている(close()された)状態となる。

823行目のap_start_lingering_close()が失敗した場合は、
transactionメモリプールを返す処理がここで行われ(上記824~825行目)、return 0で返る。
一方、828行目のstart_lingering_close_common()が0を返す場合は、
start_lingering_close_common()関数内で同等の処理が実行済みとなっている(下記の806~807行め)。

2つの関数の内容を順に見てみる。

(10) ap_start_lingering_close


(httpd-2.4.9/server/connection.c)
     95 #define SECONDS_TO_LINGER  2
     96
     97 AP_DECLARE(int) ap_start_lingering_close(conn_rec *c)
     98 {
     99     apr_socket_t *csd = ap_get_conn_socket(c);

conn_rec構造体のconn_configが持っているcore情報にある、apr_socket_t情報を取得している。
遡ってみると、この情報は、core_pre_connection()関数の4676行めでセットされている。

    100
    101     if (!csd) {
    102         return 1;

apr_socket_t情報が取得できない場合は、何もしないで戻る。
shutdown()もできないので、監視対象にもならない。

    103     }
    104
    105     if (c->sbh) {
    106         ap_update_child_status(c->sbh, SERVER_CLOSING, NULL);

これはスコアボードのステータスを更新している。

    107     }
    108
     :

    115     /* Close the connection, being careful to send out whatever is still
    116      * in our buffers.  If possible, try to avoid a hard close until the
    117      * client has ACKed our FIN and/or has stopped sending us data.
    118      */
    119
    120     /* Send any leftover data to the client, but never try to again */
    121     ap_flush_conn(c);

ap_flush_conn()関数では、次の処理が行われる。
  • bucket brigadeを作成。
  • FLUSHメタデータバケットを作成し、bucket brigadeに追加
  • bucket brigadeを conn_recのoutput_filtersに引き渡す(ap_pass_brigade)。
つまり、ここでも送信されていないデータがあれば吐き出そうとしている。

    122
    123     if (c->aborted) {

この分岐は処理異常時。
この分岐ではソケットを閉じてしまう。

    124         apr_socket_close(csd);

apr_socket_closeでは、次の処理が行われる。

(apr/network_io/unix/sockets.c)
    172 apr_status_t apr_socket_close(apr_socket_t *thesocket)
    173 {
    174     return apr_pool_cleanup_run(thesocket->pool, thesocket, socket_cleanup);
    175 }

これは、socket_cleanup(thesocket)を行うということだ。
ただし、その前に、thesocket->poolにプールを削除する際に自動実行するように登録していたsocket_cleanup(thesocket)の情報を削除している。
この登録処理はapr_socket_create()やapr_socket_accept()等々でapr_socket_t情報領域が作成された際に、そのメモリを確保したメモリプールに対して自動的に行われている。
放っておいてもこのメモリプールが破棄されるときには、実行されるが、ここではメモリ領域を破棄しないで、socket_cleanup()だけを実行したいということだ。

    125         return 1;
    126     }
    127
    128     /* Shut down the socket for write, which will send a FIN
    129      * to the peer.
    130      */
    131     if (apr_socket_shutdown(csd, APR_SHUTDOWN_WRITE) != APR_SUCCESS

この関数は、shutdown()関数を実行している。APR_SHUTDOWN_WRITEは、1(SHUT_WR)。

(apr/network_io/unix/sockets.c)
    166 apr_status_t apr_socket_shutdown(apr_socket_t *thesocket,
    167                                  apr_shutdown_how_e how)
    168 {
    169     return (shutdown(thesocket->socketdes, how) == -1) ? errno : APR_SUCCESS;
    170 }

shutdown(SHUT_WR)を行うと、私の知る限りは、FINが通知される。
自身のソケットはFIN-WAIT(FIN-WAIT-2)状態になり、通信相手のソケットはFINを受け取るので、CLOSE-WAITになる。
通信相手(ブラウザ側)は、CLOSE-WAITであっても、データの送信が可能な状態だ。
通常、この通信相手側で適切にデータ受信を行えば、ここの処理で行われたFIN通知、つまり通信断(EOF)が検出できるはずである。そうすると、相手側でも通信を終了するはずだ。

こちら側でshutdown(SHUT_WR)を行った後、通信相手も通信終了の処理を行って(相手からFINが届いて)、それを処理してからソケットのclose()を実行したいというのが、この後の処理だ。

    132         || c->aborted) {

c->abortedはあちこちで見られる、コネクション異常を示すフラグだ。
このフラグが立っていると、コネクションは再利用されない。
つまり、shutdown(SHUT_WR)に失敗したか、コネクションが異常な場合に、この分岐に入る。

    133         apr_socket_close(csd);

apr_socket_close()は、124行目で見た。

    134         return 1;
    135     }
    136
    137     return 0;

この関数は正常終了時に0が返り、何らかの異常時には1が返る。


    138 }

なお、apr_socket_close()で実行される socket_cleanup()関数は次の通り、close()を実行している。

(apr/network_io/unix/sockets.c)
     31 static apr_status_t socket_cleanup(void *sock)
     32 {
     33     apr_socket_t *thesocket = sock;
     34     int sd = thesocket->socketdes;
     35
     36     /* Set socket descriptor to -1 before close(), so that there is no
     37      * chance of returning an already closed FD from apr_os_sock_get().
     38      */
     39     thesocket->socketdes = -1;
     40
     41     if (close(sd) == 0) {
     42         return APR_SUCCESS;
     43     }
     44     else {
     45         /* Restore, close() was not successful. */
     46         thesocket->socketdes = sd;
     47
     48         return errno;
     49     }
     50 }


(11) start_lingering_close_common


(server/mpm/event/event.c)
    760 static int start_lingering_close_common(event_conn_state_t *cs)
    761 {
    762     apr_status_t rv;
    763     struct timeout_queue *q;
    764     apr_socket_t *csd = cs->pfd.desc.s;
     :
    771     apr_socket_timeout_set(csd, 0);

ソケットのタイムアウト値を0に更新し、 イベントを待たずに、すぐにタイムアウトするようにしている。

     :
    773     /*
    774      * If some module requested a shortened waiting period, only wait for
    775      * 2s (SECONDS_TO_LINGER). This is useful for mitigating certain
    776      * DoS attacks.
    777      */
    778     if (apr_table_get(cs->c->notes, "short-lingering-close")) {

cs->cにはconn_rec情報が入っている。 conn_recのnotes配列に"short-lingering-close"が登録されている場合がこの経路となる。
その場合、event MPMのタイムアウトキューの4つのタイムアウトキューのうちのshort_linger_qにクローズ待ちが登録される。
grepすると、mod_reqtimeoutモジュールでこのキーワードがセットされているのが見つかるが、他にはないようだ。

    779         cs->expiration_time =
    780             apr_time_now() + apr_time_from_sec(SECONDS_TO_LINGER);
    781         q = &short_linger_q;
    782         cs->pub.state = CONN_STATE_LINGER_SHORT;
    783     }
    784     else {

上記以外の場合、linger_qにクローズ待ちが登録される。
タイムアウトまでの待ち時間が異なっている。

    785         cs->expiration_time =
    786             apr_time_now() + apr_time_from_sec(MAX_SECS_TO_LINGER);
    787         q = &linger_q;
    788         cs->pub.state = CONN_STATE_LINGER_NORMAL;
    789     }

以下、LINGER待ちをタイムアウトキューに登録して、このソケットをlistenerスレッドの監視対象に加える。
cs->pub.senseによって読み込みか書き出しかを判定している。
この関数自体は他の箇所からも利用されているが、process_socket() -> start_lingering_close_blocking()では、APR_POLLINだろう。

    790     apr_atomic_inc32(&lingering_count);
    791     apr_thread_mutex_lock(timeout_mutex);
    792     TO_QUEUE_APPEND(*q, cs);
    793     cs->pfd.reqevents = (
    794             cs->pub.sense == CONN_SENSE_WANT_WRITE ? APR_POLLOUT :
    795                     APR_POLLIN) | APR_POLLHUP | APR_POLLERR;
    796     cs->pub.sense = CONN_SENSE_DEFAULT;
    797     rv = apr_pollset_add(event_pollset, &cs->pfd);
    798     apr_thread_mutex_unlock(timeout_mutex);
    799     if (rv != APR_SUCCESS && !APR_STATUS_IS_EEXIST(rv)) {
    800         ap_log_error(APLOG_MARK, APLOG_ERR, rv, ap_server_conf,
    801                      "start_lingering_close: apr_pollset_add failure");
    802         apr_thread_mutex_lock(timeout_mutex);
    803         TO_QUEUE_REMOVE(*q, cs);
    804         apr_thread_mutex_unlock(timeout_mutex);
    805         apr_socket_close(cs->pfd.desc.s);
    806         apr_pool_clear(cs->p);
    807         ap_push_pool(worker_queue_info, cs->p);
    808         return 0;
    809     }
    810     return 1;

この関数では、正常終了が1、何らかのエラーの場合は0が返っている。

    811 }


(12) worker MPM: process_socket


worker MPMを比較のために見てみると
process_socket()はシンプルだ。

リクエストを処理するという機能は、event MPMと変わらないが、
worker MPMでは、Keep-Aliveなリクエストの場合、その接続で扱われるリクエストがこのprocess_socket内で継続的に処理される(接続を切るまでこの関数から復帰しない)という特徴がある。

(httpd-2.4.9/server/connection.c)
    605 static void process_socket(apr_thread_t *thd, apr_pool_t *p, apr_socket_t *sock,
    606                            int my_child_num,
    607                            int my_thread_num, apr_bucket_alloc_t *bucket_alloc)
    608 {
    609     conn_rec *current_conn;
    610     long conn_id = ID_FROM_CHILD_THREAD(my_child_num, my_thread_num);
    611     ap_sb_handle_t *sbh;
    612
    613     ap_create_sb_handle(&sbh, p, my_child_num, my_thread_num);
    614
    615     current_conn = ap_run_create_connection(p, ap_server_conf, sock,
    616                                             conn_id, sbh, bucket_alloc);
    617     if (current_conn) {
    618         current_conn->current_thread = thd;
    619         ap_process_connection(current_conn, sock);
    620         ap_lingering_close(current_conn);
    621     }
    622 }

619行目のap_process_connection()は、ap_run_pre_connection()処理と、ap_run_process_connection()処理が行われている。
event MPMでは、ap_run_pre_connection()処理は、event_conn_state_t情報が未設定の場合にのみ実行されるようになっている。
worker MPMでは、その情報は常に未設定な状態に相当する。
ap_run_process_connection()は、procses_connectionフック関数の実行だ。既にevent MPMで見た。 フック関数の実体は、ap_process_http_connection()だった。
これは非同期型かそうでないかで呼び出す関数が分かれていた。worker MPMは同期型となる。したがって、ここで実行される関数がevent MPMとは異なる。 ap_process_http_sync_connection()だ。
これは追いかけないが、Keep-Alive状態の処理が繰り返されるのは、この処理の違いによる。

続く620行めに、ap_lingering_close()という処理がある。
リクエスト処理後には、必ず実行される処理だ。

これを見ると、shutdown(SHUT_WR)を実行後(FINが対向に送信される)、FIN受信まで最大で30秒間データを受信し続ける可能性があるようになっている。
event MPMでは、この処理は実行されずに、shutdown(SHUT_WR)を行うと、タイムアウトキューにタイムアウトを登録して、ソケットをlistenerスレッドの監視対象に追加している。
つまり、同様の処理は、別の処理として切り出されている。

通常、この経路ではコネクションを閉じるので、クライアントはレスポンス受信後、直ちにソケットを閉じるだろうから、あまり待つことなく、処理は終わるはずだ。
それでも、クライアントがレスポンスを受信し終わり、処理を正常に終えるまでこの処理内でブロックしているということの影響が多少はあるかもしれない。

event MPM と worker MPM の比較まとめではKeep-Alive無効時にもevent MPMの方が性能が優位なのはこのあたりも影響しているのではないか、と書いた。

以下、この処理の内容を見る。

(13) ap_lingering_close



(httpd-2.4.9/server/connection.c)
    140 AP_DECLARE(void) ap_lingering_close(conn_rec *c)
    141 {
    142     char dummybuf[512];
    143     apr_size_t nbytes;
    144     apr_time_t timeup = 0;
    145     apr_socket_t *csd = ap_get_conn_socket(c);
    146
    147     if (ap_start_lingering_close(c)) {

これは、event MPMでもstart_lingering_close_blocking()関数で呼ばれていた処理だ。
正常の場合は、shutdown(SHUT_WR)が実行され、0で返る。
問題があれば、ソケットはclose()されて、1で返る。

    148         return;

ap_start_lingering_close()で問題があれば、ここでreturnする。

    149     }
    150
    151     /* Read available data from the client whilst it continues sending
    152      * it, for a maximum time of MAX_SECS_TO_LINGER.  If the client
    153      * does not send any data within 2 seconds (a value pulled from
    154      * Apache 1.3 which seems to work well), give up.
    155      */
    156     apr_socket_timeout_set(csd, apr_time_from_sec(SECONDS_TO_LINGER));
    157     apr_socket_opt_set(csd, APR_INCOMPLETE_READ, 1);

ソケットタイムアウト時間をSECONDS_TO_LINGER(2秒)に更新し、
ソケットオプションでAPR_INCOMPLETE_READをセットし、
以下の処理が行われる。
この設定は、165行目の apr_socket_recv()の挙動に影響する。

    158
    159     /* The common path here is that the initial apr_socket_recv() call
    160      * will return 0 bytes read; so that case must avoid the expensive
    161      * apr_time_now() call and time arithmetic. */
    162
    163     do {
    164         nbytes = sizeof(dummybuf);
    165         if (apr_socket_recv(csd, dummybuf, &nbytes) || nbytes == 0)

ソケットを読み込んで正常にデータが受信されなかった場合(タイムアウトかエラー)、
EOFを検知した場合、
このif文にはいる(breakする)。

    166             break;

データを受信した場合、処理が継続される。

    167
    168         if (timeup == 0) {
    169             /*
    170              * First time through;
    171              * calculate now + 30 seconds (MAX_SECS_TO_LINGER).
    172              *
    173              * If some module requested a shortened waiting period, only wait for
    174              * 2s (SECONDS_TO_LINGER). This is useful for mitigating certain
    175              * DoS attacks.
    176              */

conn_recのnote配列に"short-lingering-close"がセットされていた場合には、
現在時刻からSECONDS_TO_LINGER(2)秒後がtimeupにセットされる。
それ以外の場合には、
現在時刻からMAX_SECS_TO_LINGER(30)秒後がtimeupにセットされる。

    177             if (apr_table_get(c->notes, "short-lingering-close")) {
    178                 timeup = apr_time_now() + apr_time_from_sec(SECONDS_TO_LINGER);
    179             }
    180             else {
    181                 timeup = apr_time_now() + apr_time_from_sec(MAX_SECS_TO_LINGER);
    182             }
    183             continue;
    184         }
    185     } while (apr_time_now() < timeup);

データ受信が続く場合は、timeupになるまで受信が続けられる。

    186
    187     apr_socket_close(csd);

最後に、ソケットがclose()される。

    188     return;
    189 }

worker MPMでは、このソケットのclose()までがprocess_socket()処理内で実行された。
これはここまで。

event MPMで、shutdown(SHUT_WR)を行ったソケットの処理の続きはlistenerスレッド側で行われている。
そこで、listenerスレッドの処理を少し見ておきたい。
今回はここまで。



0 件のコメント:

コメントを投稿