Nginx Lua 脚本编程基础 – wiki基地


Nginx Lua 脚本编程基础:释放 Nginx 的无限潜能

Nginx 以其高性能、高稳定性和丰富的功能集,已成为现代 Web 服务架构中不可或缺的核心组件。它不仅仅是一个静态文件服务器或反向代理,更是一个强大的应用交付平台。然而,Nginx 的标准模块有时难以满足日益复杂的业务需求,例如精细化的访问控制、动态的请求/响应处理、与其他服务的深度集成等。这时,Nginx Lua 脚本编程就应运而生,它为 Nginx 插上了动态编程的翅膀,极大地扩展了其能力边界。

本文将深入探讨 Nginx Lua 脚本编程的基础知识,带你领略这个强大组合的魅力,并为你开启利用 Lua 脚本定制和增强 Nginx 功能的大门。

一、 为什么选择 Nginx + Lua?

将 Lua 嵌入 Nginx 并非偶然,这种组合带来了诸多显著优势:

  1. 极致的性能: Lua 语言本身以轻量、快速著称。更重要的是,通过 LuaJIT(Just-In-Time Compiler),Lua 代码可以被编译成本地机器码执行,性能接近甚至超越 C 语言编写的 Nginx 模块。同时,Nginx Lua 模块(ngx_http_lua_module)基于 Nginx 的事件驱动、非阻塞 I/O 模型设计,使得 Lua 代码能够在 Nginx 的 worker 进程中高效运行,不会阻塞 Nginx 的核心事件循环,从而保持 Nginx 的高并发处理能力。
  2. 高度的灵活性: Lua 是一种简洁、易学且功能强大的脚本语言。使用 Lua,开发者可以在 Nginx 的请求处理周期的不同阶段注入自定义逻辑,实现传统 Nginx 配置难以完成的复杂任务。从动态路由、API 聚合、访问鉴权、请求/响应内容修改,到与数据库、缓存(如 Redis, Memcached)的交互,Lua 都能轻松应对。
  3. 强大的生态与 OpenResty: Nginx Lua 编程的流行很大程度上得益于 OpenResty 项目。OpenResty(或 Nginx + ngx_http_lua_module + lua-resty-* 库)是一个基于 Nginx 与 Lua 的高性能 Web 平台,它将标准的 Nginx 核心、许多有用的 Nginx 模块(特别是 ngx_http_lua_module)以及大量高性能的 Lua 库打包在一起。这些 lua-resty-* 库提供了丰富的功能,如非阻塞的数据库驱动、缓存客户端、HTTP 客户端、WebSocket 支持等,极大地简化了基于 Nginx Lua 的开发。
  4. 易于开发和维护: 相较于编写 C 语言的 Nginx 模块,使用 Lua 进行开发更为快捷、简单。Lua 语法简洁,调试相对容易。代码可以直接嵌入 Nginx 配置文件或放在独立的 .lua 文件中,便于管理和维护。

二、 准备环境:安装 OpenResty

虽然可以手动编译 Nginx 并集成 ngx_http_lua_module,但最推荐、最便捷的方式是直接安装 OpenResty。OpenResty 已经为你打包好了一切,包括优化过的 Nginx 核心、LuaJIT、ngx_http_lua_module 以及众多 lua-resty-* 库。

访问 OpenResty 官方网站(https://openresty.org/)可以找到针对各种操作系统的预编译包和安装指南。例如,在 CentOS/RHEL 系统上,可以通过添加 OpenResty 的 YUM 仓库来安装:

“`bash

添加 OpenResty YUM 仓库

sudo yum install yum-utils
sudo yum-config-manager –add-repo https://openresty.org/package/centos/openresty.repo

安装 OpenResty

sudo yum install openresty

(可选)安装开发工具,用于调试

sudo yum install openresty-resty openresty-doc openresty-debuginfo
“`

安装完成后,OpenResty 的可执行文件通常位于 /usr/local/openresty/nginx/sbin/nginx(或其他指定路径)。其配置文件结构与标准 Nginx 类似,默认位于 /usr/local/openresty/nginx/conf/

三、 Nginx 配置中的 Lua 指令

ngx_http_lua_module 提供了多个指令,允许你在 Nginx 配置文件的不同上下文中嵌入或引用 Lua 代码。这些指令通常以 *_by_lua 结尾,并根据它们在 Nginx 请求处理生命周期中的执行阶段来命名。

1. 代码嵌入方式:

  • *_by_lua_block { lua code }:直接在 Nginx 配置文件中嵌入 Lua 代码块。适用于简短的代码逻辑。
    nginx
    location /lua_block {
    default_type 'text/plain';
    content_by_lua_block {
    ngx.say("Hello from Lua block!")
    }
    }
  • *_by_lua_file /path/to/lua/script.lua:指定一个外部 Lua 脚本文件。这是推荐的方式,尤其对于复杂的逻辑,可以保持 Nginx 配置文件的整洁,并利用 Lua 的模块化特性。
    “`nginx
    # /path/to/lua/hello.lua
    ngx.say(“Hello from Lua file!”)

    nginx.conf

    location /lua_file {
    default_type ‘text/plain’;
    content_by_lua_file /path/to/lua/hello.lua;
    }
    “`

2. Lua 代码缓存:

  • lua_code_cache on | off:控制是否缓存 *_by_lua_file 加载的 Lua 代码。默认为 on。在生产环境中应保持开启以获得最佳性能。开发环境中可以设置为 off,这样修改 Lua 文件后无需重启 Nginx 即可生效。

3. Lua 模块搜索路径:

  • lua_package_path "/path/to/lua/libs/?.lua;;";:指定 Lua 模块(.lua 文件)的搜索路径。? 是模块名的占位符,;; 表示附加到默认路径。
  • lua_package_cpath "/path/to/lua/clibs/?.so;;";:指定 Lua C 模块(.so 文件)的搜索路径。

四、 理解 Nginx 请求处理阶段与 Lua 执行点

Nginx 处理一个 HTTP 请求会经历多个阶段。ngx_http_lua_module 允许你在其中的关键阶段执行 Lua 代码,从而实现对请求处理流程的干预。

  • init_by_lua* / init_worker_by_lua*:

    • init_by_lua*: 在 Nginx Master 进程加载配置时执行。通常用于初始化全局配置、预加载 Lua 模块或启动定时任务(仅限 Master)。
    • init_worker_by_lua*: 在每个 Nginx Worker 进程启动时执行。适用于初始化 Worker 级别的状态、建立长连接池、启动 Worker 级别的定时器等。
  • set_by_lua*:

    • 运行在 Rewrite 阶段之后,用于计算 Nginx 变量的值。返回值会赋给指定的 Nginx 变量。功能类似于 map 指令,但更灵活。
  • rewrite_by_lua*:

    • 运行在 Nginx 的 Rewrite 阶段(server-rewrite phase)。可以执行 URL 重写、内部重定向、访问控制等操作。
  • access_by_lua*:

    • 运行在 Nginx 的 Access 阶段。主要用于访问控制、权限验证、API 密钥检查等。如果 Lua 代码显式调用 ngx.exit(ngx.HTTP_FORBIDDEN) 或类似操作,可以阻止请求继续处理。
  • content_by_lua*:

    • 运行在 Nginx 的 Content 阶段。此阶段的 Lua 代码负责生成响应内容并发送给客户端。一旦使用了 content_by_lua*,Nginx 将不再查找静态文件或将请求代理到上游,而是完全由 Lua 代码接管响应生成。这是构建动态内容、API 端点的主要方式。
  • header_filter_by_lua*:

    • 运行在 Nginx 的 Header Filter 阶段。允许你检查和修改即将发送给客户端的响应头。
  • body_filter_by_lua*:

    • 运行在 Nginx 的 Body Filter 阶段。允许你检查和修改即将发送给客户端的响应体数据块。可以用于内容替换、数据压缩/解压等。
  • log_by_lua*:

    • 运行在 Nginx 的 Log 阶段。在请求处理完成(或中断)后执行。通常用于记录自定义格式的日志、将日志发送到外部系统等。此阶段的代码不应执行耗时操作,因为它仍会影响请求的总体处理时间。

理解这些阶段对于正确地使用 Lua 脚本至关重要,你需要将你的逻辑放在最合适的阶段执行。

五、 Nginx Lua API 核心 (ngx.*)

ngx_http_lua_module 提供了一个强大的 Lua API,通过全局只读表 ngx 暴露给 Lua 代码。这个 API 是与 Nginx 交互的桥梁,允许 Lua 代码访问请求信息、控制处理流程、进行 I/O 操作等。以下是一些最常用的 API:

1. 输出与流程控制:

  • ngx.say(...): 向客户端发送数据,并自动添加换行符。
  • ngx.print(...): 向客户端发送数据,不添加换行符。
  • ngx.exit(status_code): 提前中断当前请求处理,并设置 HTTP 响应状态码。例如 ngx.exit(ngx.HTTP_OK)ngx.exit(404)
  • ngx.eof(): 显式告知 Nginx 响应体已发送完毕。通常在 content_by_lua* 中,如果你没有使用 ngx.sayngx.print 输出任何内容,或者你想在所有内容发送后执行一些清理工作,可能会用到。

2. 请求信息访问 (ngx.req.*):

  • ngx.req.get_method(): 获取请求方法(”GET”, “POST” 等)。
  • ngx.req.get_uri_args(): 获取 URL 查询参数,返回一个 Lua table。
  • ngx.req.get_post_args(): 获取 POST 表单参数(application/x-www-form-urlencodedmultipart/form-data)。需要先调用 ngx.req.read_body()
  • ngx.req.get_headers(): 获取请求头,返回一个 Lua table。
  • ngx.req.set_header(name, value): 设置(或覆盖)请求头。
  • ngx.req.set_uri(uri, jump?): 修改请求的 URI。如果 jump 为 true,会触发一次内部重定向。
  • ngx.req.read_body(): 读取客户端请求体。对于大请求体,这是非阻塞操作。
  • ngx.req.get_body_data(): 获取已读取的请求体数据(字符串)。
  • ngx.req.get_body_file(): 如果请求体被缓存到临时文件,获取文件名。

3. Nginx 变量访问 (ngx.var.*):

  • ngx.var.VARIABLE_NAME: 访问 Nginx 变量。例如 ngx.var.uri 获取当前请求 URI,ngx.var.remote_addr 获取客户端 IP。
  • ngx.var.VARIABLE_NAME = value: 设置 Nginx 变量的值。注意,并非所有 Nginx 变量都是可写的。

4. 响应控制:

  • ngx.status = status_code: 设置响应的 HTTP 状态码。例如 ngx.status = ngx.HTTP_NOT_FOUND
  • ngx.header.HEADER_NAME = value / {value1, value2, ...}: 设置响应头。例如 ngx.header.Content_Type = 'application/json'ngx.header["X-My-Header"] = "value"

5. 子请求 (ngx.location.*):

  • ngx.location.capture(uri, options?): 发起一个非阻塞的 Nginx 内部子请求。这是与其他 location 或上游服务交互的关键。options 可以包含 method, body, args 等。返回一个包含 status, header, body 的 Lua table。
  • ngx.location.capture_multi({ {uri1, options1?}, {uri2, options2?}, ... }): 并发地发起多个子请求。

6. 共享内存字典 (ngx.shared.*):

  • 需要在 http 配置块中预先定义 lua_shared_dict my_cache 10m;
  • local dict = ngx.shared.my_cache: 获取共享字典对象。
  • dict:set(key, value, exptime?, flags?): 存储键值对,可选过期时间(秒)和用户标志。
  • dict:get(key): 获取值。
  • dict:delete(key): 删除键。
  • dict:incr(key, value): 原子地增加计数器。
  • … 其他操作如 add, replace, flush_all, get_keys 等。
    共享内存字典是 Nginx Worker 进程间共享数据、实现限速、缓存等功能的利器。

7. 定时器 (ngx.timer.*):

  • ngx.timer.at(delay, callback, user_arg1, ...): 在指定的延迟(秒)后,在 Nginx 的事件循环中以低优先级执行 callback 函数。通常在 init_worker_by_lua* 中启动后台任务、定时刷新缓存等。
  • ngx.timer.every(delay, callback, user_arg1, ...): (需要 lua-resty-core 库)创建周期性定时器。

8. 正则表达式 (ngx.re.*):

  • 提供基于 PCRE 的非阻塞正则表达式匹配、替换等功能。
  • ngx.re.match(subject, regex, options?): 匹配。
  • ngx.re.gsub(subject, regex, replacement, options?): 替换。

9. 时间 (ngx.now(), ngx.time(), ngx.update_time())

  • 获取 Nginx 缓存的时间,性能极高。ngx.update_time() 用于强制更新缓存时间。

10. 日志 (ngx.log(log_level, ...)):

  • 向 Nginx 的 error log 输出日志。log_level 可以是 ngx.STDERR, ngx.EMERG, ngx.ALERT, ngx.CRIT, ngx.ERR, ngx.WARN, ngx.NOTICE, ngx.INFO, ngx.DEBUG

这只是 ngx.* API 的一部分,完整的 API 文档可以在 OpenResty 官网或 lua-nginx-module 的 GitHub 页面找到。熟练掌握这些 API 是进行 Nginx Lua 开发的关键。

六、 Lua 代码组织与 require

当 Lua 逻辑变得复杂时,将所有代码放在一个文件或 _by_lua_block 中是不现实的。Lua 提供了 require 函数来加载模块。

“`lua
— my_module.lua
local _M = {} — 标准模块定义方式

function _M.do_something(name)
return “Hello, ” .. name .. “!”
end

return _M
“`

“`lua
— main.lua (in content_by_lua_file)
— 假设 my_module.lua 在 lua_package_path 定义的路径下
local my_module = require(“my_module”) — 无需 .lua 后缀

local message = my_module.do_something(“Lua User”)
ngx.say(message)
“`

确保在 nginx.conf 中配置了正确的 lua_package_path,指向你的 Lua 库目录。使用模块可以提高代码的可重用性、可维护性和组织性。

七、 常见应用场景示例

  1. 动态路由/API 网关:
    “`nginx
    location /api/ {
    access_by_lua_block {
    — 鉴权逻辑 (e.g., check JWT token from header)
    local auth_ok = require(“my_auth”).check(ngx.req.get_headers()[“Authorization”])
    if not auth_ok then
    ngx.exit(ngx.HTTP_UNAUTHORIZED)
    end
    }

    content_by_lua_block {
        local uri = ngx.var.uri
        local backend_service = nil
    
        if string.match(uri, "^/api/users") then
            backend_service = "http://user_service" .. string.sub(uri, 10)
        elseif string.match(uri, "^/api/products") then
            backend_service = "http://product_service" .. string.sub(uri, 13)
        else
            ngx.exit(ngx.HTTP_NOT_FOUND)
        end
    
        local res = ngx.location.capture(backend_service, {
            method = ngx.req.get_method(),
            args = ngx.req.get_uri_args(),
            body = ngx.req.get_body_data(),
            copy_all_vars = true -- 复制 Nginx 变量到子请求
        })
    
        ngx.status = res.status
        for k, v in pairs(res.header) do
            ngx.header[k] = v
        end
        ngx.print(res.body)
    }
    

    }
    “`

  2. 基于 Redis 的限速:
    “`nginx
    # nginx.conf http block
    lua_shared_dict redis_limit_cache 10m;

    nginx.conf server block

    需要 lua-resty-redis 库 (OpenResty 自带)

    lua_package_path “/path/to/resty/library/?.lua;;”;

    location /limited/ {
    access_by_lua_block {
    local redis = require “resty.redis”
    local red = redis:new()

        red:set_timeout(1000) -- 1 second
        local ok, err = red:connect("127.0.0.1", 6379)
        if not ok then
            ngx.log(ngx.ERR, "failed to connect to redis: ", err)
            ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
            return
        end
    
        local client_ip = ngx.var.remote_addr
        local limit_key = "limit:" .. client_ip
    
        local current_count, err = red:get(limit_key)
        if err then
            ngx.log(ngx.ERR, "failed to get limit from redis: ", err)
            -- Maybe allow request if redis fails? Or deny?
            ngx.exit(ngx.HTTP_INTERNAL_SERVER_ERROR)
            return
        end
    
        local limit = 10 -- 每分钟允许10次请求
        if current_count and tonumber(current_count) >= limit then
            ngx.log(ngx.WARN, "Rate limit exceeded for ", client_ip)
            ngx.exit(ngx.HTTP_TOO_MANY_REQUESTS)
            return
        end
    
        -- Use INCR with EXPIRE for atomic operation and setting TTL
        local _, err = red:eval([[
            local current = redis.call('INCR', KEYS[1])
            if tonumber(current) == 1 then
                redis.call('EXPIRE', KEYS[1], 60) -- Set expire only on first increment
            end
            return current
        ]], 1, limit_key)
    
        if err then
            ngx.log(ngx.ERR, "failed to incr limit in redis: ", err)
            -- Handle error, maybe allow?
        end
    
        -- Put Redis connection back to pool if using keepalive
        local ok, err = red:set_keepalive(10000, 100)
        if not ok then
            ngx.log(ngx.WARN,"failed to set keepalive: ", err)
        end
    }
    
    # ... proxy_pass or other content handling ...
    proxy_pass http://my_backend;
    

    }
    “`

  3. 修改响应内容:
    “`nginx
    location /modify_response {
    proxy_pass http://upstream_service;

    header_filter_by_lua_block {
        ngx.header["X-Powered-By"] = "Nginx + Lua"
    }
    
    body_filter_by_lua_block {
        local chunk, eof = ngx.arg[1], ngx.arg[2]
        local buffered = ngx.ctx.buffered -- Use context table to buffer chunks
    
        if not buffered then
            buffered = {}
            ngx.ctx.buffered = buffered
        end
    
        if chunk ~= "" then
            buffered[#buffered + 1] = chunk
            ngx.arg[1] = "" -- Clear original chunk to prevent immediate sending
        end
    
        if eof then
            local whole_body = table.concat(buffered)
            -- Example: Replace "foo" with "bar" in the response body
            whole_body = string.gsub(whole_body, "foo", "bar")
            ngx.arg[1] = whole_body -- Set the modified body as the final chunk
        end
    }
    

    }
    “`

八、 性能考虑与最佳实践

  • 拥抱非阻塞: 核心原则!避免在 Lua 代码中执行任何可能阻塞 Nginx worker 进程的操作,如同步的文件 I/O、同步的系统调用、CPU 密集型计算等。使用 OpenResty 提供的 lua-resty-* 库进行网络 I/O(HTTP, Redis, MySQL 等),它们都是基于 Nginx 的事件模型实现的非阻塞操作。
  • 利用 LuaJIT: OpenResty 默认使用 LuaJIT。了解 LuaJIT 的优化技巧(如避免频繁创建 table、使用 FFI)可以进一步提升性能。
  • 代码缓存: 生产环境务必开启 lua_code_cache on
  • 共享字典(Shared Dict)谨慎使用: 共享字典有锁竞争问题,在高并发写场景下可能成为瓶颈。评估是否需要,并考虑使用原子操作(如 incr)。
  • 合理使用 ngx.ctx ngx.ctx 是一个与当前请求关联的 Lua table,可以在同一请求的不同处理阶段间传递数据。它比 Nginx 变量更适合存储复杂的 Lua 数据结构。
  • 代码组织: 使用 require 组织代码,编写可测试、可复用的模块。
  • 错误处理: 在 Lua 代码中添加健壮的错误处理逻辑(使用 pcallxpcall),并通过 ngx.log 记录错误信息,防止异常导致 worker 进程崩溃。
  • 资源管理: 对于需要手动管理的资源(如使用 resty.lock 获取的锁),确保在各种执行路径下都能正确释放。使用 lua-resty-* 库时,通常它们会自动处理连接池等资源。
  • 安全: 对来自客户端的输入(如 URL 参数、请求体)进行严格的校验和清理,防止注入攻击。谨慎使用 loadstring 或类似执行任意代码的功能。

九、 调试

  • ngx.log 最常用的调试手段,将调试信息输出到 Nginx 的 error.log。确保 Nginx 的 error_log 指令设置了足够高的日志级别(如 infodebug)。
  • print / ngx.say 在开发阶段,可以直接将变量或调试信息输出到响应体,但在生产环境应移除。
  • OpenResty 的 resty 命令行工具: 可以单独运行 Lua 脚本,方便测试不依赖 Nginx 请求上下文的 Lua 模块。
  • 动态调试: 有一些第三方工具或方法支持对运行中的 Nginx Lua 代码进行断点调试,但这通常比较复杂,适用于疑难问题排查。

十、 总结

Nginx Lua 脚本编程为我们提供了一种极其强大和高效的方式来扩展 Nginx 的功能。通过在 Nginx 的请求处理流程中嵌入 Lua 代码,结合 OpenResty 提供的丰富 ngx.* API 和 lua-resty-* 库,开发者可以轻松实现复杂的业务逻辑,构建高性能的 Web 应用、API 网关、安全防火墙等。

掌握 Nginx Lua 的基础知识,理解其执行阶段、核心 API 和非阻塞编程模型,是释放 Nginx 全部潜能的关键。虽然入门可能需要一些学习曲线,但其带来的灵活性和性能优势,使得 Nginx Lua 成为现代 Web 架构中一个值得深入研究和应用的利器。希望本文能为你打下坚实的基础,开启你的 Nginx Lua 探索之旅。


发表评论

您的邮箱地址不会被公开。 必填项已用 * 标注

滚动至顶部