背景知识#

APISIX 基于 Openresty 和 Nginx 开发,了解 APISIX 的插件体系也有必要先了解下 Nginx 和 Openresty 的基础知识。

Nginx 请求处理阶段#

Nginx 将一个 HTTP 请求的处理过程精心划分为一系列有序的阶段(Phases),就像一个工厂的流水线。每个阶段都有特定的任务,不同的模块可以将自己的处理程序(handler)注册到感兴趣的阶段。这种设计使得 Nginx 的功能高度模块化、可扩展,并且处理流程非常清晰高效。

一个请求在 Nginx 中主要会经过以下 11 个处理阶段

1. NGX_HTTP_POST_READ_PHASE (请求读取后阶段)#

  • 任务: 接收到完整的请求头(Request Header)之后,第一个被执行的阶段。
  • 常用模块ngx_http_realip_module
  • 作用举例: 在这个阶段,realip 模块会根据 X-Forwarded-For 或 X-Real-IP 等请求头,将客户端的真实 IP 地址替换掉代理服务器的 IP 地址。这对后续的访问控制和日志记录至关重要。

2. NGX_HTTP_SERVER_REWRITE_PHASE (Server 级别地址重写阶段)#

  • 任务: 在 server 配置块中执行 URL 重写。
  • 常用模块/指令rewrite 指令(当它定义在 server 块中时)。
  • 作用举例: 在请求进入具体的 location 匹配之前,对 URL 进行全局的、初步的改写。例如,将所有 http 请求强制重定向到 https
server {
    listen 80;
    server_name example.com;
    # 这个 rewrite 就工作在 SERVER_REWRITE 阶段
    rewrite ^/(.*)$ https://example.com/$1 permanent; 
}

3. NGX_HTTP_FIND_CONFIG_PHASE (配置查找阶段)#

  • 任务: 根据上个阶段处理完的 URI,查找并匹配对应的 location 配置块。
  • 核心功能: 这是 Nginx 路由的核心。Nginx 会用请求的 URI 与 location 指令定义的规则进行匹配,找到最合适的 location。这个阶段没有模块可以注册 handler,是 Nginx 核心自己完成的。

4. NGX_HTTP_REWRITE_PHASE (Location 级别地址重写阶段)#

  • 任务: 在上一步匹配到的 location 块内部,执行 URL 重写。
  • 常用模块/指令rewrite 指令(当它定义在 location 块中时)、set 指令。
  • 作用举例: 对特定 location 的请求进行更精细的 URL 改写。这个阶段的 rewrite 可能会导致 Nginx 重新回到 FIND_CONFIG 阶段去匹配新的 location,可能会有循环,最多执行 10 次以防止死循环。
location /app/ {
    # 这个 rewrite 工作在 REWRITE 阶段
    rewrite ^/app/(.*)$ /v2/app/$1 break; 
}

5. NGX_HTTP_POST_REWRITE_PHASE (地址重写后阶段)#

  • 任务: 防止 rewrite 阶段的重写指令导致死循环。如果 URI 在上一个阶段被重写,这个阶段会把重写后的 URI 交给 Nginx,让其重新开始 FIND_CONFIG 阶段,查找新的 location。这是一个内部阶段,用户通常不直接感知。

6. NGX_HTTP_PREACCESS_PHASE (访问权限控制前置阶段)#

  • 任务: 在正式的访问权限检查之前,做一些准备工作。
  • 常用模块ngx_http_limit_conn_module (连接数限制), ngx_http_limit_req_module (请求速率限制)。
  • 作用举例: 在检查用户名密码之前,先检查客户端的请求速率是否过快,如果过快,直接拒绝,不必再执行后面的阶段。

7. NGX_HTTP_ACCESS_PHASE (访问权限控制阶段)#

  • 任务: 对请求进行权限验证。
  • 常用模块/指令ngx_http_access_module (allowdeny), ngx_http_auth_basic_module (HTTP 基本认证)。
  • 作用举例: 检查客户端 IP 是否在允许列表中,或者验证用户提供的用户名和密码是否正确。如果这个阶段有任何模块拒绝了请求,处理就会立即中断并返回错误(如 403 Forbidden)。

8. NGX_HTTP_POST_ACCESS_PHASE (访问权限控制后置阶段)#

  • 任务: 主要用于配合 ACCESS 阶段的 satisfy 指令。
  • 作用举例: 当 satisfy any; 被使用时,如果 ACCESS 阶段有模块允许了访问(例如 IP 匹配成功),POST_ACCESS 阶段的处理就可以被跳过。如果 ACCESS 阶段没有明确允许,这个阶段的处理(例如 HTTP 基本认证)就会被执行。

9. NGX_HTTP_TRY_FILES_PHASE (Try_files 阶段)#

  • 任务try_files 指令专属的特殊阶段。
  • 常用模块/指令try_files
  • 作用举例: 按顺序检查文件或目录是否存在,如果找到,则内部重定向到该文件,如果都找不到,则执行最后一个参数(通常是内部重定向到一个 location 或返回一个状态码)。
location / {
    # 这个指令工作在 TRY_FILES 阶段
    try_files $uri $uri/ /index.html;
}

10. NGX_HTTP_CONTENT_PHASE (内容生成阶段)#

  • 任务核心阶段,负责生成最终的响应内容并发送给客户端。
  • 重要特性: 每个 location 只有一个模块能成为“内容处理模块”(Content Handler)。
  • 常用模块:
    • ngx_http_static_module: 处理静态文件。
    • ngx_http_proxy_module: 将请求反向代理到后端服务器 (proxy_pass)。
    • ngx_http_fastcgi_module: 将请求转发给 FastCGI 应用(如 PHP-FPM)。
    • ngx_http_index_module: 处理目录索引文件。
    • return: 直接返回指定的状态码或内容。
  • 执行逻辑: Nginx 会调用在 location 中找到的内容处理模块。例如,如果配置了 proxy_pass,那么 proxy_module 就会接管请求。如果没有特定的内容处理模块,默认会由 static_module 尝试提供静态文件服务。

11. NGX_HTTP_LOG_PHASE (日志记录阶段)#

  • 任务: 请求处理完成后,记录访问日志。
  • 常用模块/指令ngx_http_log_module (access_log)。
  • 作用举-例: 无论请求成功还是失败,这个阶段都会被执行(除非有严重错误),用于将请求的相关信息(如客户端IP、请求时间、状态码等)写入到日志文件中。

过滤器模块 (Filter Modules)#

CONTENT 阶段生成了响应内容后,在发送给客户端之前,响应数据还会经过一系列的 过滤器(Filter) 链处理。过滤器负责对响应头和响应体进行修改或加工。

  • Header Filters (头部过滤器): 修改响应头。例如,添加 ServerDate 等头信息,或者处理 Cookie
  • Body Filters (包体过滤器): 修改响应体。例如:
    • ngx_http_gzip_filter_module: 对响应内容进行 Gzip 压缩。
    • ngx_http_ssi_filter_module: 解析并处理服务端包含(SSI)指令。
    • ngx_http_chunked_filter_module: 将响应体分块(Chunked)传输。

重要特点: 过滤器的执行顺序与它们在 Nginx 编译时定义的顺序 相反

总结流程图#

客户端请求
      ↓
[NGX_HTTP_POST_READ_PHASE]      (获取真实IP)
      ↓
[NGX_HTTP_SERVER_REWRITE_PHASE] (Server级URL重写)
      ↓
[NGX_HTTP_FIND_CONFIG_PHASE]    (查找Location)
      ↓
[NGX_HTTP_REWRITE_PHASE]        (Location级URL重写, 可能循环)
      ↓
[NGX_HTTP_PREACCESS_PHASE]      (限速、限流)
      ↓
[NGX_HTTP_ACCESS_PHASE]         (IP准入、用户认证)
      ↓
[NGX_HTTP_TRY_FILES_PHASE]      (处理 try_files)
      ↓
[NGX_HTTP_CONTENT_PHASE]        (生成响应内容: 静态文件、反向代理...)
      ↓
  响应内容生成
      ↓
[Header Filters]                (修改响应头: Gzip头, Cookie等)
      ↓
[Body Filters]                  (修改响应体: Gzip压缩, 内容替换)
      ↓
[NGX_HTTP_LOG_PHASE]            (记录访问日志)
      ↓
  响应发送给客户端

Openresty Lua 处理阶段#

https://openresty-reference.readthedocs.io/en/latest/Directives/

6a520bcb1707362129c5117bea1f0f49_MD5

Openresty 在 Nginx 的几乎所有关键处理阶段都植入了钩子(Hooks),允许开发者通过编写 Lua 脚本来介入和控制请求的处理流程。

相比于原生 Nginx,Openresty 利用 Lua 提供了更强大的动态能力,简化了定制化功能的开发流程。

以下是 OpenResty 提供的核心 *_by_lua 指令,按照它们在 Nginx 请求生命周期中的执行顺序列出:

1. 初始化阶段 (Master 和 Worker 进程启动时)#

  • init_by_lua*
    • 对应 Nginx 阶段: Master 进程加载配置时。
    • 作用: 在 Nginx Master 进程启动时执行。通常用于初始化全局配置、预加载 Lua 模块、或注册定时任务。此时还不能操作网络。
  • init_worker_by_lua*
    • 对应 Nginx 阶段: 每个 Worker 进程启动时。
    • 作用: 每个 Nginx Worker 进程启动时执行一次。非常适合做一些每个 Worker 进程独立的初始化工作,比如创建定时器、初始化共享内存数据、或建立到数据库的连接池。

2. SSL 阶段#

  • ssl_certificate_by_lua*
    • 对应 Nginx 阶段: SSL 握手期间。
    • 作用: 允许动态加载 SSL 证书和私钥。可以根据客户端的 SNI (Server Name Indication) 信息,从 Redis、数据库或其他服务中动态获取对应的证书,实现大规模虚拟主机的 HTTPS 服务。

3. 请求处理阶段 (按执行顺序)#

  • set_by_lua*
    • 对应 Nginx 阶段NGX_HTTP_REWRITE_PHASE (地址重写阶段)。
    • 作用: 专门用于设置 Nginx 变量。它的执行时机很早,但功能受限,主要是为了给其他 Nginx 模块(如 proxy_pass)提供动态变量值。它不能产生输出或中断请求。
  • rewrite_by_lua*
    • 对应 Nginx 阶段NGX_HTTP_REWRITE_PHASE (地址重写阶段)。
    • 作用: 这是进行复杂重写、重定向、内部跳转的核心阶段。可以执行 API 调用、访问数据库等网络操作来决定如何处理请求。功能比 set_by_lua 强大得多。
  • access_by_lua*
    • 对应 Nginx 阶段NGX_HTTP_ACCESS_PHASE (访问控制阶段)。
    • 作用: 用于实现复杂的访问控制逻辑。例如:自定义身份认证、API 密钥验证、动态黑白名单、权限校验等。如果在这里拒绝访问,请求会立即中断。
  • content_by_lua*
    • 对应 Nginx 阶段NGX_HTTP_CONTENT_PHASE (内容生成阶段)。
    • 作用: 作为内容处理器,直接生成响应并发送给客户端。这个阶段的脚本将接管 Nginx 的内容生成。非常适合用来构建动态 API 服务、Web 应用的后端逻辑等。如果一个 location 中使用了 content_by_lua,那么像 proxy_pass 这样的其他内容处理指令就不会执行。
  • balancer_by_lua*
    • 对应 Nginx 阶段: Upstream 负载均衡阶段。
    • 作用: 当使用 proxy_pass 指向一个 upstream 块时,可以用这个钩子实现自定义的负载均衡策略。可以根据请求的特定参数(如用户 ID、请求头)来动态选择后端服务器。

4. 响应过滤阶段#

  • header_filter_by_lua*
    • 对应 Nginx 阶段: 响应头过滤阶段 (Header Filter)。
    • 作用: 修改从后端服务器返回(或由 content_by_lua 生成)的响应头。例如,添加/删除/修改 Cookie、CORS 相关的头等。
  • body_filter_by_lua*
    • 对应 Nginx 阶段: 响应体过滤阶段 (Body Filter)。
    • 作用: 流式地处理和修改响应体。例如,内容替换、数据脱敏、在页面底部动态插入 JavaScript 代码等。
  • log_by_lua*
    • 对应 Nginx 阶段NGX_HTTP_LOG_PHASE (日志记录阶段)。
    • 作用: 实现自定义的日志记录。可以将请求的详细信息以 JSON 格式发送到 Kafka、Elasticsearch 或其他日志聚合服务,而不是简单地写入本地文件。

Lua 插件#

31162774fd572a06a629559aec999166_MD5

APISIX 官方提供了如上的流程图描述 Lua 插件的调用流程。从命名上可以很清楚地和 Openresty Lua 体系相对应。

而实现的核心代码在 ngx_tpl.luainit.lua 中。

ngx_tpl.lua 用于生成底层 nginx 的 conf 文件。从模版中,可以看到 apisix 在多处地方插入了调用点,以 http 为例:


	-- upstream 
    upstream apisix_backend {
	    -- ... 
        balancer_by_lua_block {
            apisix.http_balancer_phase()
        }
    }
    
    -- 模块初始化
    init_by_lua_block {
        require "resty.core"
        {% if lua_module_hook then %}
        require "{* lua_module_hook *}"
        {% end %}
        apisix = require("apisix")

        local dns_resolver = { {% for _, dns_addr in ipairs(dns_resolver or {}) do %} "{*dns_addr*}", {% end %} }
        local args = {
            dns_resolver = dns_resolver,
        }
        apisix.http_init(args)

        -- set apisix_lua_home into constants module
        -- it may be used by plugins to determine the work path of apisix
        local constants = require("apisix.constants")
        constants.apisix_lua_home = "{*apisix_lua_home*}"
    }

	-- worker 进程初始化
    init_worker_by_lua_block {
        apisix.http_init_worker()
    }

	-- worker 退出
    exit_worker_by_lua_block {
        apisix.http_exit_worker()
    }
    
    -- server 块配置
    server {
	    -- ssl phase
        {% if ssl.enable then %}
        ssl_client_hello_by_lua_block {
            apisix.ssl_client_hello_phase()
        }

        ssl_certificate_by_lua_block {
            apisix.ssl_phase()
        }
        {% end %}
        
        location / {
            access_by_lua_block {
                apisix.http_access_phase()
            }
            
            header_filter_by_lua_block {
                apisix.http_header_filter_phase()
            }

            body_filter_by_lua_block {
                apisix.http_body_filter_phase()
            }

            log_by_lua_block {
                apisix.http_log_phase()
            }
        }
    }

可以看到,apisix 在 Openresty 请求的主要阶段都设置了 hook 点调用。

这些调用的 *_phase 都放置在 init.lua 中。http_access_phase 实现了对 rewrite plugin 和 access plugin 的调用:

function http_access_phase()
		-- ...
        local plugins = plugin.filter(api_ctx, route)
        api_ctx.plugins = plugins

        plugin.run_plugin("rewrite", plugins, api_ctx)
        if api_ctx.consumer then
            local changed
            local group_conf

            if api_ctx.consumer.group_id then
                group_conf = consumer_group.get(api_ctx.consumer.group_id)
                if not group_conf then
                    core.log.error("failed to fetch consumer group config by ",
                        "id: ", api_ctx.consumer.group_id)
                    return core.response.exit(503)
                end
            end

            route, changed = plugin.merge_consumer_route(
                route,
                api_ctx.consumer,
                group_conf,
                api_ctx
            )

            core.log.info("find consumer ", api_ctx.consumer.username,
                          ", config changed: ", changed)

            if changed then
                api_ctx.matched_route = route
                core.table.clear(api_ctx.plugins)
                local phase = "rewrite_in_consumer"
                api_ctx.plugins = plugin.filter(api_ctx, route, api_ctx.plugins, nil, phase)
                -- rerun rewrite phase for newly added plugins in consumer
                plugin.run_plugin(phase, api_ctx.plugins, api_ctx)
            end
        end
        plugin.run_plugin("access", plugins, api_ctx)
        
        -- ...

    _M.handle_upstream(api_ctx, route, enable_websocket)

    set_upstream_x_forwarded_headers(api_ctx)
end

handle_upstream 中,会对 upstream 进行预处理,如 dns 解析,预选 node(load_balancer.pick_server)等。同时对于某些插件(如 ai 相关),会直接使用 http client 访问 upstream,设置 bypass_nginx_upstream 跳过 APISIX Upstream 的处理。

在这些做完后,会对 before_proxy plugin 实施调用。

function _M.handle_upstream(api_ctx, route, enable_websocket)
    -- some plugins(ai-proxy...) request upstream by http client directly
    if api_ctx.bypass_nginx_upstream then
        common_phase("before_proxy")
        return
    end
    
    -- ...
    local code, err = set_upstream(route, api_ctx)
    if code then
        core.log.error("failed to set upstream: ", err)
        core.response.exit(code)
    end

    local server, err = load_balancer.pick_server(route, api_ctx)
    if not server then
        core.log.error("failed to pick server: ", err)
        return core.response.exit(502)
    end

    api_ctx.picked_server = server

    set_upstream_headers(api_ctx, server)
    
    -- run the before_proxy method in access phase first to avoid always reinit request
    common_phase("before_proxy")

end

RPC 插件#

12e494401d05e9925fa86b52b7769831_MD5

APISIX 支持启动独立的 plugin runner,worker 进程通过 RPC 与之交互,实现多语言插件的支持。目前已经支持的语言 runner 包括 java,go,nodejs,python 等。

从上面官方的架构图可以看到,RPC 插件支持 3 个 Hook 点:

  • ext-plugin-pre-req: 接收请求时调用
  • ext-plugin-post-req:请求处理完成后调用
  • ext-plugin-post-resp:接收响应后调用

相关的执行框架以插件的形式注入到 apisix 的执行流程中。如 ext-plugin-pre-req

local core = require("apisix.core")
local ext = require("apisix.plugins.ext-plugin.init")


local name = "ext-plugin-pre-req"
local _M = {
    version = 0.1,
    priority = 12000,
    name = name,
    schema = ext.schema,
}


function _M.check_schema(conf)
    return core.schema.check(_M.schema, conf)
end


function _M.rewrite(conf, ctx)
    return ext.communicate(conf, ctx, name)
end


return _M

ext-plugin-post-reqext-plugin-pre-req 类似,只不过注册在 access 阶段。

对于 ext-plugin-post-resp,它的执行阶段是 before-proxy,它直接使用 handle_upstream 中预选的 server,使用 http client 请求后端获取响应结果,绕过了 nginx 本身的 forward 机制。另外,目前插件中还存在很多 TODO,实现并不完善。

local function get_response(ctx, http_obj)
    local ok, err = http_obj:connect({
        scheme = ctx.upstream_scheme,
        host = ctx.picked_server.host,
        port = ctx.picked_server.port,
    })
    -- ...
end

Wasm 插件#

Wasm 插件目前看起来实现了跟 lua 插件一样的处理阶段模式,可能还不够完善。优点是可以使用非 lua 语言来编写插件。

function _M.require(attrs)
    if not support_wasm then
        return nil, "need to build APISIX-Runtime to support wasm"
    end

    local name = attrs.name
    local priority = attrs.priority
    local plugin, err = wasm.load(name, attrs.file)
    if not plugin then
        return nil, err
    end

    local mod = {
        version = 0.1,
        name = name,
        priority = priority,
        schema = schema,
        check_schema = check_schema,
        plugin = plugin,
        type = "wasm",
    }

    if attrs.http_request_phase == "rewrite" then
        mod.rewrite = function (conf, ctx)
            return http_request_wrapper(mod, conf, ctx)
        end
    else
        mod.access = function (conf, ctx)
            return http_request_wrapper(mod, conf, ctx)
        end
    end

    mod.header_filter = function (conf, ctx)
        return header_filter_wrapper(mod, conf, ctx)
    end

    mod.body_filter = function (conf, ctx)
        return body_filter_wrapper(mod, conf, ctx)
    end

    -- the returned values need to be the same as the Lua's 'require'
    return true, mod
end

文档参考:wasm.md