ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

OpenResty+Redis高性能Web服务实战指南

OpenResty+Redis高性能Web服务实战指南 简介本资源是一套面向Web后端开发与高性能服务架构学习者的OpenResty实战案例集聚焦NginxLuaRedis协同开发场景适用于具备基础Linux和Web服务器知识的中级开发者解决高并发下动态逻辑嵌入、缓存联动与轻量级业务编排等典型问题。压缩包共174个文件含78个核心Lua脚本实现请求拦截、会话管理、Redis交互等、55篇Markdown技术笔记覆盖Nginx 11个处理阶段、Location匹配规则、worker调优及常见陷阱、12张架构示意图与配置流程图辅以Shell部署脚本、PDF原理文档及少量PHP/HTML测试页整体体积11.58MB结构清晰、即拿即用。目前已有468人学习下载内容源自一线实践包含可直接运行的conf配置模板如lua_request_01_location.conf、WebSocket集成示例、微信接口模拟及Redis-Iresty封装方案助读者快速掌握OpenResty生态下的工程化落地路径。1. 为什么用 Lua OpenResty Redis 组合做高性能 Web 服务不是为了炫技而是解决真实瓶颈当你在 Nginx 层需要实时校验用户权限、限流计数、动态路由或缓存穿透防护又不想把请求打到后端应用服务器——比如 Java 或 Python 服务——那传统proxy_pass就成了性能瓶颈。这时lua-nginx-openresty-redis不是“可选方案”而是绕过应用层、在边缘节点完成业务逻辑的刚需路径。OpenResty 把 Lua 嵌入 Nginx 事件循环Redis 提供毫秒级原子读写两者结合能实现每秒数万次的令牌桶限流、会话状态同步、API 签名验证等操作且全程不阻塞 Nginx worker 进程。它适合 API 网关、多租户 SaaS 的租户隔离、游戏登录态校验如热更新配置下发、以及需要低延迟响应的 IoT 设备管理接口。新手常误以为这只是“Nginx 写点 Lua 脚本”实际难点在于如何避免redis:connect()阻塞、怎样复用连接池、为何init_by_lua*不能访问ngx.var、以及 Redis pipeline 在access_by_lua*中的正确触发时机——这些细节直接决定服务能否扛住 5000 QPS 而不抖动。2. 搭建最小可运行环境从 OpenResty 安装到 Redis 连通性验证2.1 OpenResty 安装与基础配置结构确认OpenResty 并非 Nginx 插件而是基于 Nginx 源码深度定制的发行版自带lua-resty-redis、lua-resty-core等模块。不要用apt install nginx-lua-module或yum install openresty一键包——CentOS 7/8 和 Ubuntu 22.04 的官方仓库版本普遍滞后如 OpenResty 1.19.x而lua-resty-redis的连接池自动回收机制在 1.21.4.2 才稳定。推荐使用官方预编译包# 下载并解压以 Linux x64 为例替换为最新版链接 wget https://openresty.org/download/openresty-1.21.4.2.tar.gz tar -xzf openresty-1.21.4.2.tar.gz cd openresty-1.21.4.2 ./configure --prefix/usr/local/openresty \ --with-http_ssl_module \ --with-http_realip_module \ --with-http_v2_module \ --with-stream \ --with-stream_ssl_module \ --with-stream_realip_module make sudo make install安装后验证路径和模块加载/usr/local/openresty/nginx/sbin/nginx -V 21 | grep -o lua # 应输出 lua表示 Lua 模块已编译进内核 ls /usr/local/openresty/lualib/resty/ | grep redis # 应看到 redis.lua即 lua-resty-redis 模块存在注意OpenResty 的lualib目录是 Lua 模块默认搜索路径/usr/local/openresty/lualib/resty/redis.lua是核心驱动所有redis:new()实例都依赖它。若手动修改lua_package_path必须包含该路径否则require resty.redis会报module resty.redis not found。2.2 Redis 服务就绪与连接池参数设计Redis 推荐使用 6.2 版本支持 RESP3 协议和更细粒度的客户端命令统计本地开发可用 Docker 快速启动docker run -d --name my-redis -p 6379:6379 -e REDIS_PASSWORDdev123 redis:6.2-alpine # 验证连通性 redis-cli -h 127.0.0.1 -p 6379 -a dev123 ping # 返回 PONG 即成功关键不是“能连上”而是连接池配置是否匹配 Nginx worker 数量与业务并发模型。lua-resty-redis的set_keepalive()方法将连接放回池中但池大小由pool_size和pool_timeout控制。常见错误是设pool_size 100却只启动 2 个 worker导致连接争抢或pool_timeout 1秒太短频繁重建连接。合理值需按公式估算pool_size ≥ (单 worker 平均并发请求数) × (worker 数)例如4 核机器开 4 个 worker每个 worker 平均处理 50 个并发 Redis 请求则pool_size ≥ 200。pool_timeout建议设为 60 秒避免空闲连接长期占用。2.3 Nginx 配置文件结构与 Lua 加载时机OpenResty 的配置分四层执行时机必须严格对应业务逻辑位置init_by_lua_blockMaster 进程启动时执行一次适合初始化全局变量、加载配置文件如 JSON、预编译正则init_worker_by_lua_block每个 Worker 进程启动时执行适合创建共享内存 zone、初始化 Redis 连接池set_by_lua_block在location内执行用于设置变量如set $user_id ...不可调用阻塞 I/Oaccess_by_lua_block认证/鉴权逻辑入口可调用 Redis 同步操作如查 tokencontent_by_lua_block生成响应体适合复杂业务逻辑如拼装 JSON。一个典型nginx.conf片段# /usr/local/openresty/nginx/conf/nginx.conf worker_processes 4; events { worker_connections 1024; } http { # 全局 Lua 模块路径可选因 OpenResty 默认已包含 lua_package_path /usr/local/openresty/lualib/?.lua;;; # 初始化 Redis 连接池每个 worker 独立池 init_worker_by_lua_block { local redis require resty.redis local red redis:new() red:set_timeouts(1000, 1000, 1000) -- connect, send, read timeout (ms) -- 将连接池存入全局 table供后续 access_by_lua 复用 ngx.ctx.redis_pool red } server { listen 8000; location /api/user { # 认证阶段查 Redis 获取用户信息 access_by_lua_block { local red ngx.ctx.redis_pool local ok, err red:connect(127.0.0.1, 6379) if not ok then ngx.log(ngx.ERR, failed to connect to redis: , err) return ngx.exit(500) end -- 设置密码若 Redis 配置了 requirepass red:auth(dev123) -- 查询 key此处用 user:123 模拟 local res, err red:get(user: .. ngx.var.arg_id) if not res then ngx.log(ngx.WARN, redis get failed: , err) return ngx.exit(401) end -- 将结果存入 ngx.ctx供 content_by_lua 使用 ngx.ctx.user_data res } # 响应生成 content_by_lua_block { ngx.say(User data: , ngx.ctx.user_data or not found) } } } }提示ngx.ctx是每个请求的上下文表生命周期与 request 绑定比ngx.shared.DICT更轻量适合临时传递数据。但切记ngx.ctx不能跨location传递——access_by_lua和content_by_lua在同一location内才共享。3. 实现高可用 Redis 操作连接池复用、超时控制与错误降级3.1lua-resty-redis连接池的正确打开方式直接调用redis:new():connect()每次新建 TCP 连接QPS 超过 1000 就会触发Too many open files错误。必须通过set_keepalive()将连接归还池中。标准流程如下local redis require resty.redis local red redis:new() -- 1. 设置超时单位毫秒必须在 connect 前设置 red:set_timeouts(100, 100, 100) -- connect/send/read 各 100ms -- 2. 连接 Redis失败时返回 nil err local ok, err red:connect(127.0.0.1, 6379) if not ok then ngx.log(ngx.ERR, redis connect error: , err) return end -- 3. 认证如果 Redis 配置了密码 ok, err red:auth(dev123) if not ok then ngx.log(ngx.ERR, redis auth error: , err) return end -- 4. 执行命令如 GET local res, err red:get(key:123) if not res then ngx.log(ngx.WARN, redis get error: , err) -- 此处可降级为本地缓存或默认值 res default_value end -- 5. 关键将连接放回池中而非 close() -- 第一个参数空闲连接最大存活时间秒第二个连接池最大容量 local ok, err red:set_keepalive(60000, 200) -- 60s, 最多 200 个连接 if not ok then ngx.log(ngx.ERR, failed to set keepalive: , err) endset_keepalive(60000, 200)中的60000是毫秒单位即 60 秒200是该 worker 进程内连接池的最大连接数。若池满set_keepalive()会立即关闭连接并返回false此时应记录日志并考虑扩容pool_size。3.2 Redis 命令批量执行与 pipeline 优化单次GET/SET往返耗时约 0.5~2ms当需查 10 个 key 时串行调用 10 次就是 5~20ms。改用 pipeline 可压缩为一次往返-- 错误示范10 次独立请求 for i 1, 10 do local res, err red:get(user: .. i) end -- 正确pipeline 批量获取 local results {} local ok, err red:init_pipeline() if not ok then ngx.log(ngx.ERR, pipeline init failed: , err) return end -- 添加 10 个 GET 命令到 pipeline for i 1, 10 do red:get(user: .. i) end -- 执行所有命令返回结果数组 results, err red:commit_pipeline() if not results then ngx.log(ngx.ERR, pipeline commit failed: , err) return end -- results 是 table索引 1~10 对应各 GET 结果 for i, res in ipairs(results) do ngx.log(ngx.INFO, user:, i, data: , res) endcommit_pipeline()返回的是结果数组每个元素对应 pipeline 中第 n 条命令的返回值。若某条命令出错如 key 不存在对应位置为nil不会中断整个 pipeline。相比串行pipeline 可将 10 次操作的 RTT 从 10×RTT 降至 1×RTT实测 QPS 提升 3~5 倍。3.3 连接失败时的优雅降级策略生产环境 Redis 可能瞬时不可用网络抖动、主从切换。硬抛 500 错误会直接导致 API 不可用。应实现三级降级本地内存缓存用ngx.shared.DICT存最近成功结果TTL 缩短为 10 秒静态默认值如用户信息缺失时返回{ id: 0, name: guest }异步上报记录错误到日志并触发告警如调用内部 HTTP 告警接口。示例降级代码local dict ngx.shared.my_cache local cache_key user: .. ngx.var.arg_id -- 1. 先查本地共享字典 local cached dict:get(cache_key) if cached then ngx.log(ngx.INFO, hit local cache for , cache_key) ngx.ctx.user_data cached return end -- 2. 再查 Redis local red ngx.ctx.redis_pool local res, err red:get(cache_key) if not res then ngx.log(ngx.WARN, redis get failed: , err) -- 3. 降级返回默认用户 ngx.ctx.user_data {id:0,name:guest,role:visitor} -- 4. 异步上报非阻塞 ngx.timer.at(0, function() ngx.log(ngx.ERR, Redis failover triggered for key: , cache_key) end) return end -- 5. 写入本地缓存TTL 10 秒 dict:set(cache_key, res, 10) ngx.ctx.user_data resngx.shared.DICT是基于 slab 分配器的共享内存线程安全且零拷贝比ngx.var或 Lua table 更适合缓存。set(key, value, exptime)的exptime单位是秒支持小数如0.1表示 100ms。4. 调试与排错定位 Lua 脚本阻塞、Redis 连接泄漏与 Nginx 日志分析4.1 Lua 脚本阻塞检测用ngx.timer.every监控执行时长Lua 代码若含死循环或未设超时的socket:receive()会阻塞整个 worker 进程。OpenResty 提供ngx.on_abort()和ngx.timer.every辅助诊断-- 在 init_worker_by_lua_block 中注册监控 init_worker_by_lua_block { local delay 1 -- 每秒检查一次 local timer nil local function check_long_running() -- 获取当前 worker 的活跃 timer 数量异常值 100 表示可能泄漏 local active_timers ngx.worker.count_active_timers() if active_timers 100 then ngx.log(ngx.ERR, too many active timers: , active_timers) end end timer ngx.timer.every(delay, check_long_running) }更直接的方式是开启nginx -t时的 debug 日志error_log /usr/local/openresty/nginx/logs/error.log debug;然后在access_by_lua_block开头加local start_time ngx.now() -- ... 业务逻辑 ... local cost ngx.now() - start_time if cost 0.1 then -- 超过 100ms 记录警告 ngx.log(ngx.WARN, slow lua execution: , cost, s for , ngx.var.uri) end4.2 Redis 连接泄漏排查netstat与redis-cli client list双验证连接池未正确set_keepalive()会导致 ESTABLISHED 连接数持续增长。先查 Nginx 侧# 查看 nginx worker 进程打开的 socket 数量 sudo lsof -p $(pgrep -f nginx: worker) | grep TCP.*:6379 | wc -l # 若持续 200假设 pool_size200说明连接未归还再查 Redis 侧redis-cli -h 127.0.0.1 -p 6379 -a dev123 client list | wc -l # 正常应 ≈ worker 数 × pool_size若远大于此确认 Lua 是否漏掉 set_keepalive关键检查点所有red:connect()后必须有对应的red:set_keepalive()即使connect()失败也要red:close()避免 fd 泄漏local red redis:new() local ok, err red:connect(127.0.0.1, 6379) if not ok then red:close() -- 必须关闭未成功的连接 return end -- ... 后续操作 ... red:set_keepalive(60000, 200) -- 成功后归还4.3 Nginx 日志字段定制与 Redis 错误归因默认log_format不包含 Lua 变量需自定义格式捕获 Redis 状态log_format detailed $remote_addr - $remote_user [$time_local] $request $status $body_bytes_sent $http_referer $http_user_agent rt$request_time uct$upstream_connect_time uht$upstream_header_time urt$upstream_response_time redis_status$redis_status redis_cost$redis_cost; server { access_log /usr/local/openresty/nginx/logs/access.log detailed; location /api/data { access_by_lua_block { local start ngx.now() local red ngx.ctx.redis_pool local res, err red:get(data: .. ngx.var.arg_id) local cost ngx.now() - start -- 将 Redis 状态注入日志变量 ngx.var.redis_status res and success or fail ngx.var.redis_cost string.format(%.3f, cost) } } }重启 Nginx 后日志中会出现redis_statusfail和redis_cost0.123字段配合grep redis_status\fail\ /path/to/access.log | awk {print $NF} | sort | uniq -c | sort -nr可快速定位高频失败接口。5. 生产级进阶技巧基于 Redis 的分布式限流与 Lua 脚本原子操作5.1 使用 Redis EVAL 实现令牌桶限流无第三方模块OpenResty 自带redis:eval()支持 Lua 脚本原子执行规避GET INCR的竞态问题。以下是一个每分钟最多 100 次请求的令牌桶脚本-- 限流脚本保存为 /usr/local/openresty/lua/rate_limit.lua local key KEYS[1] -- 如 rate:192.168.1.100 local limit tonumber(ARGV[1]) -- 每分钟限额 local window tonumber(ARGV[2]) -- 时间窗口秒数60 local now tonumber(ARGV[3]) -- 当前时间戳秒 -- Redis 中存储 {last_time, tokens}用 HINCRBY 模拟浮点数 local last_time, tokens unpack(redis.call(HMGET, key, last_time, tokens)) last_time last_time and tonumber(last_time) or 0 tokens tokens and tonumber(tokens) or limit -- 计算已过去时间补充令牌 local elapsed now - last_time local new_tokens math.min(limit, tokens elapsed * limit / window) -- 判断是否允许请求 if new_tokens 1 then -- 消费一个令牌 redis.call(HSET, key, last_time, now, tokens, new_tokens - 1) return 1 -- 允许 else -- 拒绝并设置过期避免 key 永久存在 redis.call(EXPIRE, key, window) return 0 -- 拒绝 end在access_by_lua_block中调用local red ngx.ctx.redis_pool local key rate: .. ngx.var.remote_addr local limit 100 local window 60 local now ngx.time() local allowed, err red:eval([[ local key KEYS[1] local limit tonumber(ARGV[1]) local window tonumber(ARGV[2]) local now tonumber(ARGV[3]) local last_time, tokens unpack(redis.call(HMGET, key, last_time, tokens)) last_time last_time and tonumber(last_time) or 0 tokens tokens and tonumber(tokens) or limit local elapsed now - last_time local new_tokens math.min(limit, tokens elapsed * limit / window) if new_tokens 1 then redis.call(HSET, key, last_time, now, tokens, new_tokens - 1) return 1 else redis.call(EXPIRE, key, window) return 0 end ]], 1, key, limit, window, now) if not allowed or allowed 0 then ngx.status 429 ngx.header[X-RateLimit-Remaining] 0 ngx.say(Too Many Requests) return ngx.exit(429) end注意redis:eval()的KEYS参数必须是 Redis key 名不能含表达式ARGV传数值。脚本在 Redis 服务端原子执行无需担心并发覆盖。5.2 Redis 数据类型选择指南何时用 Hash、Sorted Set 或 Bitmap场景推荐类型Lua 操作示例优势用户属性存储name/age/emailHashred:hset(user:123, name, Alice, age, 25)字段级更新节省内存实时排行榜按分数排序Sorted Setred:zadd(leaderboard, score, user:123)O(log N) 插入/查询支持范围扫描日活用户去重百万级Bitmapred:setbit(active:20240501, user_id, 1)单 bit 存储100 万用户仅占 ~125KB简单开关配置feature flagStringred:set(feature:payment, on)最简结构读写最快Bitmap 特别适合统计类场景red:bitcount(active:20240501)可秒级返回当日 DAU比SCARD集合节省 90% 内存。5.3 OpenResty 刷新 404 问题的根因与修复“OpenResty 刷新 404” 是高频问题本质是location匹配失败或content_by_lua未输出内容。排查步骤确认location正则是否贪婪location ~ ^/api/.*会匹配/api/但location /api/是前缀匹配更安全检查content_by_lua_block是否有ngx.say()或ngx.print()若逻辑走到末尾无输出Nginx 默认返回 404验证init_by_lua是否误用了ngx.exit()init_by_lua中调用ngx.exit()会导致整个进程退出查看error.log中是否有no resolver defined若 Lua 脚本中用了http.request()但未配resolver会静默失败。最小修复模板location /api/test { # 显式指定 resolver避免 DNS 问题 resolver 8.8.8.8 valid30s; content_by_lua_block { ngx.say(Hello from OpenResty Redis!) -- 必须有输出否则 404 } }若仍 404用curl -v http://localhost:8000/api/test查看响应头确认Content-Length是否为 0。本文还有配套的精品资源点击获取
返回列表