前回、OS の動きや、ユーザーランドプログラムの動きから、「送信のターンにある方から切断する方がスムーズ」ということを説明しました。
今回は、ALB の動きを単純化した C 言語*1のコードに起こしてみます。
リバースプロキシとしての挙動
ALB のロードバランサーとしての機能を無視すると、中核はリバースプロキシとしての動作と考える事ができます。
さらに、通常のリバースプロキシなら当然の機能であるパスのマッピングなども省略し、
- クライアント届いた HTTP リクエストをターゲットへ送信し、
- ターゲットから届いたレスポンスをクライアントに送信する。
という感じにします。
更に省略
本当は、リクエスト・レスポンスがそれぞれ1行で、改行が終端となるような仮想的なプロトコルを考えた方が単純化できるのですが、そこまですると、元の話が ALB でプロトコルが HTTP であることからも離れてしまうので、一応、HTTP を扱っている ふう にします。
なので、実際には別途、コードを書く必要があるけど、省略して関数の呼び出しだけを書いている部分があります。先のそれを説明しておくと下記のようなものがあります。
merge_request、merge_response- recv() で取得したデータをマージして HTTP リクエスト/レスポンスのデータを作り、HTTP リクエスト/レスポンスとしての終端*2に達しているかを返す。
pic_target_fd、return_target_fd- ターゲットとのコネクションに対応するソケットのファイルディスクリプタの取得/返却をする。
- ターゲットとの新規コネクションを作成する処理は、
pic_target_fdの中でうまい具合にやっている、と思ってください。
send_http_response- クライアントへ HTTP レスポンスを送信する関数で、クライアント側のファイルディスクリプタと返すコンテンツ、 HTTP ステータスを引数に取る。
- この関数のエラーハンドリングは勘弁してください。
reset_socket_timer- ALB のアイドルタイムアウトを実現するタイマーをリセットする。
クライアントから接続された後の処理
クライアントから接続された後に呼ばれる関数をイメージした処理です。
void http_handling(int client_fd) { unsigned char recv_buf[1024]; unsigned char http_req_buff[1024 * 1024]; unsigned char http_response_buff[1024 * 1024]; ssize_t received_size, req_size, sent_size, sent_total; int end_of_turn; int error_in_send; int target_fd; /* * クライアントからの受信待ちループ (ブロッキング recv) * クライアント側から FIN が来ると recv は 0 を返し、このループを抜けます */ while ((received_size = recv(client_fd, recv_buf, sizeof(recv_buf), 0)) > 0) { /* 受信したデータのマージとリクエスト終端チェック */ end_of_turn = merge_request(http_req_buff, sizeof(http_req_buff), recv_buf, received_size); if (!end_of_turn) { /* リクエストの終端に達していないので再び recv() へ */ continue; } req_size = strnlen((char *)http_req_buff, sizeof(http_req_buff)); sent_total = 0; /* ターゲットとのコネクションを保持しているソケットを取得 */ target_fd = pic_target_fd(); /* ターゲットへのリクエスト送信のターン */ error_in_send = 0; while (sent_total < req_size) { /* * ターゲットとのコネクションが閉じられていたら、 * ここで初めて書き込みエラー(EPIPE等)が発生して気づく */ sent_size = send(target_fd, http_req_buff + sent_total, req_size - sent_total, 0); if (sent_size < 0) { perror("send() 中にエラー (ターゲットが既に切断していたなど)"); error_in_send = 1; break; } sent_total += sent_size; } if (error_in_send) { /* ターゲットへの送信中にエラーが発生 */ send_http_response(client_fd, "ターゲットへのリクエスト送信中にエラー", 502); continue; } /* アイドルタイムアウト用のタイマーをリセット */ reset_socket_timer(target_fd); /* ターゲットからのレスポンス受信のターン */ while ((received_size = recv(target_fd, recv_buf, sizeof(recv_buf), 0)) > 0) { end_of_turn = merge_response(http_response_buff, sizeof(http_response_buff), recv_buf, received_size); if (!end_of_turn) { continue; } break; /* レスポンスの受信完了 */ } /* ターゲットからの受信中にエラー、または0(相手からの切断)が発生した場合 */ if (received_size <= 0) { send_http_response(client_fd, "ターゲットからのレスポンス受信中にエラー", 502); continue; } /* * 正常系:クライアントへレスポンスを返し、 * アイドルタイムアウト用のタイマーをリセットし、 * ターゲットのソケットをプールへ返却 */ send_http_response(client_fd, http_response_buff, 200); reset_socket_timer(target_fd); return_target_fd(target_fd); } if (received_size < 0) { perror("client recv() エラー"); } }
ポイントは、前回の記事でも書きましたが、
- ターゲットへのリクエスト送信のターンで send() を実行したときに初めて切断されていることを知る。
です*3。
アイドルタイムアウトで切断
先のコードで、ターゲットに対して送受信が完了したときにタイマーをリセットする処理を書いています。じゃぁ、タイマーの時間が来てソケットをクローズしに行く処理がどこかにあるはず、ですが、おそらくは HTTP の処理をやっているのとは別のスレッドでチェックして、ソケットのクローズとコネクションをプールしている情報からの削除を実施しているのではないかと思います。
実際のコネクションプールの仕組みはもっと複雑だったり、もうちょっと効率の良い手段を使っているかもしれませんが、簡単に実装すると、下記のような物になると思います。
/* * メイン処理とは並行して(別スレッド等で)常に動き続けている * コネクションプールの監視・タイムアウト処理のイメージ */ void connection_pool_timer_watcher(void) { const int IDLE_TIMEOUT_SEC = 60; time_t idle_time; int i, fd; while (1) { /* プール内にあるすべてのターゲット用ソケットをチェック */ for (i = 0; i < max_pool_connections; i++) { fd = connection_pool[i]; if (fd == -1) continue; /* 最後に通信した時刻からの経過時間を計算 */ idle_time = time(NULL) - get_socket_last_active_time(fd); if (idle_time > IDLE_TIMEOUT_SEC) { /* 経過時間がアイドルタイムアウトの時間を超えていたのでクローズ */ close(fd); connection_pool[i] = -1; } } sleep(1); } }
まとめ
実際の ALB の動作を実装しているコードはもっと複雑だと思いますが、おそらくは Amazon Linux の上に ALB として動作する機能を実装しているのではないか、と考えると、使われていないソケットを整理するときに、ターゲットより先にクローズしたい、という意図が見えてくると思います。
ALB も魔法の箱ではなく、普通に OS 上で動くプログラムだと考えると、ドキュメントに書かれた注意書きの意味が見えてくる事があります。
*1:個人的に最近は Python で書く事が多いし、Python の方が読める人は多いと思うのですが、OS カーネルとユーザーランドの話をしているときに余計な抽象化を挟みたくない、と思って C 言語にしました。もっとも、Python の socket は、ほとんど C 言語とのインタフェースと違わないですが。
*2:連続する2つの改行(改行コードは CRLF)が終端になります。
*3:send() にはもう一つトラップがあって、送信したデータが OS カーネルのバッファに入ってしまえば、送信側としては正常に送信したことになります。なので、受信側のプログラムが正常に動作しいなくても、受信側のプログラムに問題が発生していることを知るのは、送信完了後に recv() している時、ということもあります。




