Nginx 与 Lua 模块开发:构建动态网关服务

在微服务与云原生架构日益普及的今天,API 网关作为流量的统一入口,承担着路由转发、鉴权、限流、日志记录等关键职责。传统的 Nginx 配置虽然强大,但在面对动态、细粒度的业务逻辑时,往往需要频繁 reload 且难以编写复杂逻辑。而 ngx_lua 模块的出现,让 Nginx 拥有了脚本级编程能力,成为构建高性能动态网关的理想选择。

为什么选择 Nginx + Lua 构建网关

Nginx 本身具备高并发、低内存占用、事件驱动等特性,而 LuaJIT 的引入带来了接近 C 语言的执行效率。两者结合,可以在不牺牲性能的前提下实现:

  • 动态路由:根据请求头、参数、路径实时决定上游服务。
  • 细粒度鉴权:JWT 校验、签名验证、IP 黑白名单。
  • 限流与熔断:基于令牌桶或漏桶算法,在 Lua 层快速拒绝过载请求。
  • 响应改写:动态修改响应头、响应体,甚至聚合多个后端接口。
  • 可观测性:实时上报日志、指标到 Prometheus 或 Kafka。

与 OpenResty 相比,原生 Nginx 编译 ngx_lua 模块同样可行,且更轻量。下面我们从环境搭建开始,逐步实现一个动态网关的核心功能。

环境准备与模块编译

假设你使用 Ubuntu 22.04,推荐直接使用 OpenResty,它集成了 Nginx、LuaJIT、ngx_lua 及大量常用 Lua 库。若坚持原生 Nginx,则需下载 lua-nginx-modulengx_devel_kit 并重新编译 Nginx。

# 安装 OpenResty
wget https://openresty.org/package/ubuntu/pool/main/o/openresty/openresty_1.21.4.1_amd64.deb
sudo dpkg -i openresty_1.21.4.1_amd64.deb

安装完成后,/usr/local/openresty/nginx/sbin/nginx 即为可执行文件。在 nginx.conf 中通过 content_by_lua_blockaccess_by_lua_file 嵌入 Lua 代码。

核心指令与执行阶段

理解 Nginx 的请求处理阶段是编写 Lua 网关的关键。ngx_lua 提供了多个指令,对应不同阶段:

  • init_by_lua_block:Master 进程启动时执行,用于加载全局配置、预编译正则。
  • init_worker_by_lua_block:每个 Worker 启动时执行,适合启动定时器、连接池。
  • set_by_lua_block:设置变量,可用于简单计算。
  • rewrite_by_lua_block:重写阶段,适合动态路由改写。
  • access_by_lua_block:访问控制,鉴权、限流的最佳位置。
  • content_by_lua_block:生成响应内容,适合直接返回 JSON。
  • log_by_lua_block:日志阶段,异步上报。

一个典型的动态网关会在 access_by_lua_file 中完成鉴权与限流,在 balancer_by_lua_block 中动态选择上游节点。

实战:动态路由与鉴权网关

下面实现一个简化网关:根据请求头 X-Service 将请求转发到不同后端,并校验 Authorization 中的 JWT。

1. 配置结构

http {
    lua_package_path "/etc/nginx/lua/?.lua;;";
    lua_shared_dict jwt_cache 10m;

    init_by_lua_block {
        local cjson = require "cjson"
        -- 预加载配置
    }

    server {
        listen 8080;

        location /api/ {
            access_by_lua_file /etc/nginx/lua/auth.lua;
            proxy_pass http://backend;
        }
    }

    upstream backend {
        server 0.0.0.0;  # 占位,由 balancer_by_lua 动态选择
        balancer_by_lua_file /etc/nginx/lua/balancer.lua;
    }
}

2. 鉴权 Lua 脚本 auth.lua

local cjson = require "cjson"
local jwt = require "resty.jwt"

-- 从 Header 获取 Token
local auth_header = ngx.req.get_headers()["Authorization"]
if not auth_header or not auth_header:match("Bearer%s+(.+)") then
    ngx.status = 401
    ngx.say(cjson.encode({error = "missing token"}))
    return ngx.exit(401)
end

local token = auth_header:match("Bearer%s+(.+)")
local jwt_obj = jwt:verify("your-secret", token)

if not jwt_obj.verified then
    ngx.status = 403
    ngx.say(cjson.encode({error = "invalid token"}))
    return ngx.exit(403)
end

-- 将用户信息注入请求头,供后端使用
ngx.req.set_header("X-User-Id", jwt_obj.payload.sub)

3. 动态负载均衡 balancer.lua

local upstream = require "ngx.upstream"
local balancer = require "ngx.balancer"

-- 根据 X-Service 选择后端池
local service = ngx.var.http_x_service or "default"
local backends = {
    user    = {{host="10.0.0.1", port=8001}, {host="10.0.0.2", port=8001}},
    order   = {{host="10.0.0.3", port=8002}},
    default = {{host="10.0.0.9", port=9000}}
}

local pool = backends[service] or backends.default
-- 简单轮询(生产可用一致性哈希)
local idx = ngx.worker.id() % #pool + 1
local target = pool[idx]

local ok, err = balancer.set_current_peer(target.host, target.port)
if not ok then
    ngx.log(ngx.ERR, "failed to set peer: ", err)
    return ngx.exit(502)
end

4. 限流保护

利用 lua_shared_dictresty.limit.req 实现令牌桶限流:

local limit_req = require "resty.limit.req"
local limiter = limit_req.new("jwt_cache", 100, 50)  -- 100r/s,突发50

local key = ngx.var.binary_remote_addr
local delay, err = limiter:incoming(key, true)
if not delay then
    if err == "rejected" then
        return ngx.exit(503)
    end
    ngx.log(ngx.ERR, "limit error: ", err)
    return ngx.exit(500)
end

if delay > 0 then
    ngx.sleep(delay)
end

性能优化与注意事项

  1. 避免阻塞操作:Lua 代码运行在 Nginx Worker 中,任何阻塞(如同步 HTTP 请求、磁盘 IO)都会拖慢整个 Worker。务必使用 ngx.location.captureresty.http 的异步接口。
  2. 共享内存字典lua_shared_dict 是跨 Worker 共享数据的唯一方式,适合缓存 JWT、限流计数器。但注意其大小有限,且操作是原子的但非事务性。
  3. 代码缓存:使用 lua_code_cache on(默认开启),生产环境切勿关闭,否则每次请求都会重新编译 Lua。
  4. 错误处理:始终用 pcall 包裹可能出错的逻辑,并通过 ngx.log 记录到 error.log。
  5. 连接池:对于后端 HTTP 调用,使用 resty.http 的连接池,避免频繁握手。

总结

Nginx 与 Lua 的结合,让网关从静态配置进化为可编程的动态服务。通过 access_by_luabalancer_by_lua 等阶段指令,我们可以在毫秒级内完成鉴权、路由、限流等操作,同时保持 Nginx 的高并发优势。本文展示的示例虽简,但已涵盖动态网关的核心骨架。生产环境中,你还可以集成 Consul 做服务发现、用 Redis 做分布式限流、通过 OpenTracing 做链路追踪。掌握 ngx_lua,等于为你的微服务架构装上了一扇智能、高效的大门。

未经允许不得转载:任鹏个人博客 » Nginx 与 Lua 模块开发:构建动态网关服务

赞 (0) 打赏

评论 0

取消
  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信扫一扫打赏