Read OSS

リクエストの解剖学: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()では、以下の処理が順に行われます。

  1. ルーター実行router:exec(ctx) がリクエストをすべての設定済みRouteに対してマッチさせ、マッチしたRoute・Service・アップストリームURL・URI変換を含む match_t テーブルを返す
  2. ワークスペースの割り当てctx.workspace = match_t.route.ws_id
  3. X-Forwarded-*のセットアップ:信頼済みIPの検出とForwardedヘッダーの伝播
  4. プロトコルの適用:HTTPSリダイレクト、gRPC/HTTP2のバリデーション
  5. バランサーの準備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_passbalancer_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.luabalancer.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-LatencyX-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_responsetrue に設定されています。プラグインが 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段階の設定解決、プラグインハンドラの構造を詳しく見ていきます。