リクエストの解剖学:RewriteからLogまでのKong Runloop
前提知識
- ›第1回:アーキテクチャとNginxインテグレーション
- ›第2回:起動と初期化
- ›Nginxのフェーズ順序と各フェーズの制約に関する理解
- ›ngx.ctx、cosocketモデル、ngx_luaフェーズ制約への習熟
リクエストの解剖学:RewriteからLogまでのKong Runloop
第1回・第2回でKongの初期化とワーカーの起動を確認しました。今回はいよいよ、1つのHTTPリクエストが処理の各フェーズをどのように通過するかを追っていきます。ここでアーキテクチャが実際に動き出します。ルーターがRouteをマッチさせ、プラグインが認証・変換を処理し、バランサーがアップストリームのターゲットを選択し、レスポンスがフィルターやロガーを経由してクライアントへと返っていきます。
この設計の核心にあるのが before/afterサンドイッチ と呼ぶべきパターンです。各フェーズでは、まずRunloopハンドラがインフラ側のロジック(before)を実行し、次にプラグインが動き、最後にRunloopがクリーンアップ処理(after)を行います。このパターンを理解することが、Kongの拡張性を理解する鍵になります。
before/afterサンドイッチパターン
kong/init.lua におけるリクエスト処理の各フェーズは、すべて同じ構造に従っています。
function Kong.access()
local ctx = ngx.ctx
-- timing setup...
ctx.KONG_PHASE = PHASES.access
runloop.access.before(ctx) -- infrastructure
local plugins_iterator = runloop.get_plugins_iterator()
execute_collecting_plugins_iterator(plugins_iterator, "access", ctx) -- plugins
-- delayed_response handling...
runloop.access.after(ctx) -- infrastructure
-- timing finalization...
end
beforeフックが担うのは、プラグインが意識すべきでないインフラ処理です。具体的にはルーターの実行、X-Forwarded-*ヘッダーのセットアップ、プロトコルネゴシエーションなどが含まれます。afterフックはバランサーの準備とgRPC向けの内部リダイレクトを担当します。プラグインはその中間で優先度順に実行され、kong.response.exit()を呼ぶことでパイプラインをショートサーキットできます。
このパターンは kong/runloop/handler.lua のReturnテーブルで定義されています。
return {
-- ...
init_worker = { before = function() ... end },
rewrite = { before = function(ctx) ... end },
access = { before = function(ctx) ... end, after = function(ctx) ... end },
-- ...
}
sequenceDiagram
participant Phase as Kong.<phase>()
participant Before as runloop.<phase>.before()
participant Plugins as plugins_iterator
participant After as runloop.<phase>.after()
Phase->>Phase: Set timing markers
Phase->>Before: Infrastructure logic
Before-->>Phase: Router match, context setup
Phase->>Plugins: Iterate in priority order
Plugins->>Plugins: Plugin 1 (PRIORITY=1250)
Plugins->>Plugins: Plugin 2 (PRIORITY=910)
Plugins->>Plugins: Plugin N (PRIORITY=...)
Plugins-->>Phase: All plugins executed
Phase->>After: Cleanup, balancer prep
After-->>Phase: Ready for next phase
Phase->>Phase: Finalize timing
RewriteとAccess:ルーティングとサービス解決
rewriteフェーズ(Kong.rewrite())は比較的シンプルです。リクエストコンテキストの初期化、フェーズをまたいだ参照のためのコンテキスト保存、ワークスペースの設定、そしてグローバルスコープのプラグイン(特定のRoute・Service・Consumerに紐付いていないもの)の実行を行います。1177行目のrewrite.before()では、サーバーポートの取得とトレーシングの初期化が行われます。
なお、rewriteフェーズでは collecting イテレータではなく execute_global_plugins_iterator を使います。この時点ではまだルートマッチングが行われていないため、どのRoute・Serviceスコープのプラグインを適用すべきかKongが判断できないからです。
accessフェーズ(Kong.access())がメインの処理を担います。1184〜1404行目のaccess.before()では、以下の処理が順に行われます。
- ルーター実行:
router:exec(ctx)がリクエストをすべての設定済みRouteに対してマッチさせ、マッチしたRoute・Service・アップストリームURL・URI変換を含むmatch_tテーブルを返す - ワークスペースの割り当て:
ctx.workspace = match_t.route.ws_id X-Forwarded-*のセットアップ:信頼済みIPの検出とForwardedヘッダーの伝播- プロトコルの適用:HTTPSリダイレクト、gRPC/HTTP2のバリデーション
- バランサーの準備:805〜856行目の
balancer_prepare()が、リトライ回数・タイムアウト・アップストリームのホスト/ポートを持つbalancer_dataテーブルを作成する
access.before()の後は、collectingイテレータを通じてプラグインが実行されます(詳細は第4回で解説)。プラグインが kong.response.exit(403) を呼び出した場合は、delayed responseメカニズムによってリクエストがショートサーキットされます。
flowchart TD
A[access.before] --> B[router:exec]
B --> C{Match found?}
C -->|No| D[404 No Route matched]
C -->|Yes| E[Set workspace]
E --> F[Parse X-Forwarded-* headers]
F --> G{HTTPS required?}
G -->|Yes, HTTP request| H[Redirect/426]
G -->|No| I[balancer_prepare]
I --> J[Set upstream vars]
J --> K{gRPC service?}
K -->|Yes| L[Internal redirect @grpc]
K -->|No| M[Continue to plugins]
ヒント:
balancer_prepareで作られるctx.balancer_dataテーブルは、リクエストライフサイクルの中で最も重要なデータ構造の1つです。スキーム・ホスト・ポート・リトライ回数・タイムアウト、そして各バランサー試行を記録するtries配列を保持しています。バランサーが実行される前であれば、プラグインからこれらの値を変更することも可能です。
Balancerフェーズ:アップストリーム選択とリトライ
Kong.balancer()は他のフェーズとは異なる特性を持っています。1つのリクエストに対して複数回呼ばれる可能性がある(アップストリームへの試行ごとに1回)うえに、yieldやcosocket I/Oが使えないというNginxの厳しい制約下で動作します。
balancerフェーズは、Nginxの proxy_pass が balancer_by_lua_block をトリガーすることで開始されます。初回の試行では、1380行目の set_more_tries(retries) で最大リトライ回数を設定します。リトライ時には、前回の試行の失敗を記録してからバランサーを再実行します。
if try_count > 1 then
local previous_try = tries[try_count - 1]
previous_try.state, previous_try.code = get_last_failure()
-- report to health checker ...
local ok, err, errcode = balancer.execute(balancer_data, ctx)
-- ...
end
kong/runloop/balancer/init.lua の balancer.execute() 関数がDNS解決とターゲット選択を担当します。複数のターゲットを持つUpstreamエンティティを使うサービスでは、設定されたロードバランシングアルゴリズム(ラウンドロビン、コンシステントハッシュ、最小コネクション数)が適用されます。
コネクションのKeepAliveは1388〜1451行目で管理されています。KeepaliveプールのキーはIP・ポート・SNI・TLS検証設定・クライアント証明書の組み合わせで構成されており、セキュリティパラメータがすべて一致する場合にのみコネクションが再利用されます。
pool = fmt("%s|%s|%s|%s|%s|%s|%s",
balancer_data_ip, balancer_data_port,
var.upstream_host,
service.tls_verify and "1" or "0",
service.tls_verify_depth or "",
service.ca_certificates and concat(service.ca_certificates, ",") or "",
service.client_certificate and service.client_certificate.id or "")
flowchart TD
A[Kong.balancer] --> B{First try?}
B -->|Yes| C[set_more_tries]
B -->|No| D[Record previous failure]
D --> E[Report to health checker]
E --> F[balancer.execute - re-resolve]
C --> G[set_current_peer IP:port]
F --> G
G --> H[set_timeouts]
H --> I{Keepalive enabled?}
I -->|Yes| J[enable_keepalive with pool key]
I -->|No| K[Skip]
J --> L[Record timing]
K --> L
レスポンス処理:header_filterとbody_filter
Nginxがアップストリームからレスポンスを受け取ると、Kong.header_filter() がレスポンスヘッダーを処理します。このフェーズでは execute_collected_plugins_iterator を使います。accessフェーズのcollecting反復処理で構築済みのプラグインリストを再利用するため、どのプラグインを適用するかを再解決する必要がありません。
Runloopの header_filter.before() は X-Kong-Proxy-Latency や X-Kong-Upstream-Latency といったKong固有のレスポンスヘッダーを付加します。header_filter.after() では Via および Server ヘッダーを設定します。この間にプラグインはレスポンスヘッダーの追加・変更・削除を自由に行えます。
Kong.body_filter() はレスポンスボディを処理します。重要なポイントとして、チャンク転送のレスポンスではこのフェーズが複数回呼ばれます。各呼び出しで1チャンクを処理し、arg[2]フラグ(eof)が最後のチャンクかどうかを示します。
1716行目には、ctx.response_body を設定したプラグインを処理する特別なケースがあります。
if ctx.response_body then
arg[1] = ctx.response_body
arg[2] = true
end
この仕組みにより、ai-proxy のようなプラグインが複数チャンクを単一の出力にまとめ、レスポンスボディ全体を変換済みの内容に置き換えることができます。
Logフェーズとリクエストコンテキストのライフサイクル
Kong.log() は最終フェーズです。この時点でレスポンスはすでにクライアントへ送信済みであり、logフェーズはコネクションが閉じた後(あるいは少なくともレスポンスが完全にバッファリングされた後)に非同期で実行されます。
kong/init.lua 全体で使われているタイミング計測は、ctx 上の KONG_*_START および KONG_*_ENDED_AT マーカーを利用しています。1761〜1842行目のlogフェーズでは、その空白を埋める処理が行われます。エラー発生などの理由で前フェーズが終了時刻を記録していなかった場合、logフェーズが遡って計算します。主な計測値は以下のとおりです。
KONG_PROXY_LATENCY:リクエスト開始からバランサー完了までの時間KONG_WAITING_TIME:アップストリームのレスポンス待ち時間KONG_RECEIVE_TIME:header_filter + body_filterの処理時間KONG_RESPONSE_LATENCY:プロキシを経由しないレスポンスの合計時間
プラグインの log ハンドラが実行された後、1849〜1856行目でコンテキストがクリーンアップされます。
plugins_iterator.release(ctx)
runloop.log.after(ctx)
-- ...
release_table(CTX_NS, ctx)
release_table の呼び出しは、ctx テーブルをプール(tablepool)へ返却して再利用できるようにするものです。後続のリクエストでGCの負荷を抑えるための工夫です。
sequenceDiagram
participant Access as access phase
participant Balancer as balancer phase
participant Upstream as upstream
participant HF as header_filter
participant BF as body_filter
participant Log as log phase
Note over Access: KONG_ACCESS_START
Access->>Access: plugins execute
Note over Access: KONG_ACCESS_ENDED_AT
Balancer->>Upstream: connect & send
Note over Balancer: KONG_BALANCER_ENDED_AT
Upstream-->>HF: response headers
Note over HF: KONG_WAITING_TIME = now - BALANCER_ENDED_AT
HF->>BF: headers processed
BF->>BF: body chunks
Note over BF: KONG_RECEIVE_TIME computed
BF->>Log: final chunk (eof)
Log->>Log: fill timing gaps
Log->>Log: execute plugin log handlers
Log->>Log: release ctx to tablepool
Delayed Responseメカニズム
377行目および414行目に実装されている delayed_response メカニズムは、特に注目に値します。collecting処理(accessフェーズ)では ctx.delay_response が true に設定されています。プラグインが kong.response.exit() を呼び出すと、即座にレスポンスを送信する代わりに、終了ステータスとボディが ctx.delayed_response に格納されます。
ctx.delayed_response = {
status_code = 403,
content = { message = "Forbidden" },
}
イテレータ内の後続プラグインは ctx.delayed_response を確認してスキップされます。イテレータが完了した後、flush_delayed_response が実際のレスポンスを送信します。この設計により、リクエストがショートサーキットされた場合でも、すべてのプラグインがdownstreamのフェーズハンドラ(header_filter、body_filter、log)を実行できることが保証されます。
ヒント:
execute_collecting_plugins_iterator(399〜400行目)でのコルーチンラッピングは、単なるエラーハンドリング以上の意味を持ちます。各プラグインをコルーチン内で実行することで、ランタイムエラーが発生してもリクエストパイプライン全体がクラッシュするのを防げます。エラーはログに記録され、delayed_response経由で500がキューに積まれ、次のプラグインの処理へと進みます。
次回のテーマ
今回は1つのリクエストがすべてのフェーズを通過する過程と、RunloopとプラグインがどのようにInteractするかを追いました。しかし、重要な疑問をまだ掘り下げていません。あるリクエストに対してどのプラグインが、どの順序で実行されるのでしょうか?第4回ではプラグインシステムを深掘りします。collecting/collectedイテレータパターン、8段階の設定解決、プラグインハンドラの構造を詳しく見ていきます。