OpenResty -- Nginx + Lua 访问 Redis

摘要

OpenResty 简介

  • OpenResty® 是一个基于 Nginx 与 Lua 的高性能 Web 平台,其内部集成了大量精良的 Lua 库、第三方模块以及大多数的依赖项。用于方便地搭建能够处理超高并发、扩展性极高的动态 Web 应用、Web 服务和动态网关。

  • OpenResty® 通过汇聚各种设计精良的 Nginx 模块(主要由 OpenResty 团队自主开发),从而将 Nginx 有效地变成一个强大的通用 Web 应用平台。这样,Web 开发人员和系统工程师可以使用 Lua 脚本语言调动 Nginx 支持的各种 C 以及 Lua 模块,快速构造出足以胜任 10K 乃至 1000K 以上单机并发连接的高性能 Web 应用系统。

  • OpenResty® 的目标是让你的Web服务直接跑在 Nginx 服务内部,充分利用 Nginx 的非阻塞 I/O 模型,不仅仅对 HTTP 客户端请求,甚至于对远程后端诸如 MySQL、PostgreSQL、Memcached 以及 Redis 等都进行一致的高性能响应。

OpenResty 安装

MacOS

  • MacOS 版本:15.7.3

  • 通过 Homebrew 安装

1
2
brew tap openresty/brew
brew install openresty
  • 安装时报错

1
2
3
4
5
6
7
8
checking for GeoIP library ... not found
checking for GeoIP library in /usr/local/ ... not found
checking for GeoIP library in /usr/pkg/ ... not found
checking for GeoIP library in /opt/local/ ... not found
checking for GeoIP library in /opt/homebrew/ ... not found

./configure: error: the GeoIP module requires the GeoIP library.
You can either do not enable the module or install the library.
  • 解决办法

1
2
3
4
5
6
7
brew edit openresty/brew/openresty

# 将下面的行注释掉后保存并退出
# args << "--with-http_geoip_module"

# 重新安装
brew reinstall openresty
  • OpenResty 命令说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
openresty -h
nginx version: openresty/1.29.2.1
Usage: nginx [-?hvVtTq] [-s signal] [-p prefix]
[-e filename] [-c filename] [-g directives]

Options:
-?,-h : this help
-v : show version and exit
-V : show version and configure options then exit
-t : test configuration and exit
-T : test configuration, dump it and exit
-q : suppress non-error messages during configuration testing
-s signal : send signal to a master process: stop, quit, reopen, reload
-p prefix : set prefix path (default: /usr/local/Cellar/openresty/1.29.2.1_1/nginx/)
-e filename : set error log file (default: /usr/local/var/log/nginx/error.log)
-c filename : set configuration file (default: /usr/local/etc/openresty/nginx.conf)
-g directives : set global directives out of configuration file
参数 完整写法 中文含义 典型使用场景 示例
-h / -? openresty -h 显示帮助信息并退出 快速查看可用参数 openresty -h
-v openresty -v 显示版本号 确认运行版本 openresty -v
-V openresty -V 显示版本号 + 编译参数 排查模块、编译选项、依赖 openresty -V
-t openresty -t 校验配置文件合法性并退出 修改配置后验证语法 openresty -t
-T openresty -T 校验配置并打印完整配置内容 排查 include 文件、调试配置加载顺序 openresty -T
-q openresty -t -q 测试配置时只输出错误信息 CI / 自动化脚本 openresty -t -q
-s openresty -s reload 向 master 进程发送控制信号 服务管理(重载、停止等) openresty -s reload
stop 立即停止服务(强制) 紧急停服 openresty -s stop
quit 优雅停止服务(处理完请求后退出) 平滑下线 openresty -s quit
reload 平滑重载配置 发布配置变更 openresty -s reload
reopen 重新打开日志文件 日志切割后使用 openresty -s reopen
-p openresty -p /path 指定运行前缀目录(prefix) 多实例部署、定制目录结构 openresty -p /opt/openresty
-e openresty -e file 指定错误日志路径 临时调试错误日志 openresty -e /tmp/error.log
-c openresty -c file 指定配置文件路径 使用非默认配置启动 openresty -c ./nginx.conf
-g openresty -g "daemon off;" 设置全局指令(覆盖配置文件) 容器化 / 临时调试 openresty -g "daemon off;"
  • 安装后的目录

1
2
3
4
5
6
# 安装目录
/usr/local/opt/openresty
# 配置文件目录
/usr/local/etc/openresty/
# 命令目录
/usr/local/bin/openresty

示例

  • vim test-nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
worker_processes  1;
events {
worker_connections 1024;
}
http {
server {
listen 8080;
location / {
default_type text/html;
content_by_lua_block {
ngx.say("<p>hello, world</p>")
}
}
}
}
  • 启动

1
openresty -p `pwd` -c ./test-nginx.conf
  • 访问

1
2
3
curl http://127.0.0.1:8080
# 结果
<p>hello, world</p>

Nginx + Lua + Redis 限流完整示例

限流设计说明

  • 限流规则

1
2
3
4
5
维度:客户端 IP
窗口:60 秒
阈值:10 次
算法:固定窗口计数器
存储:Redis
  • Redis Key 设计

1
2
rate:{client_ip}:{minute_timestamp}
# TTL:70 秒(防止残留)
  • vim redis-nginx.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
worker_processes  1;

events {
worker_connections 1024;
}

http {
lua_package_path "/usr/local/openresty/lualib/?.lua;;";

# ----------------------------
# Redis 原子限流 Lua 脚本
# ----------------------------
lua_shared_dict redis_scripts 1m;

init_worker_by_lua_block {
-- 将 Lua 脚本加载到 Nginx Worker 内存
local script = [[
local cnt = redis.call("INCR", KEYS[1])
if cnt == 1 then
redis.call("EXPIRE", KEYS[1], ARGV[1])
end
return cnt
]]

local dict = ngx.shared.redis_scripts
dict:set("rate_limit_lua", script)
}

server {
listen 8080;

location /api {
content_by_lua_block {

local redis = require "resty.redis"
local red = redis:new()

-- 超时(毫秒)
red:set_timeout(500)

-- 连接 Redis
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.log(ngx.ERR, "redis connect failed: ", err)
return ngx.exit(500)
end

-- ACL 认证(仅新连接)
if red:get_reused_times() == 0 then
local ok, err = red:auth("admin", "123456")
if not ok then
ngx.log(ngx.ERR, "redis auth failed: ", err)
return ngx.exit(500)
end
end

-- 客户端 IP
local client_ip = ngx.var.remote_addr or "unknown"

-- 当前分钟窗口
local now = ngx.time()
local minute = math.floor(now / 60)

-- Redis Key
local key = "rate:" .. client_ip .. ":" .. minute

-- 从共享字典读取 Lua 脚本
local dict = ngx.shared.redis_scripts
local script = dict:get("rate_limit_lua")

-- 执行 Redis Lua(原子)
local ttl = 70
local cnt, err = red:eval(script, 1, key, ttl)
if not cnt then
ngx.log(ngx.ERR, "redis eval failed: ", err)
return ngx.exit(500)
end

-- 限流判断
local limit = 10
if cnt > limit then
ngx.status = 429
ngx.say("Too Many Requests, limit=", limit)
return ngx.exit(429)
end

-- 放回连接池
red:set_keepalive(10000, 100)

-- 正常返回
ngx.say("OK, request count=", cnt)
}
}
}
}
  • 启动

1
openresty -p `pwd` -c ./redis-nginx.conf
  • 访问

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i in {1..15}; do
curl http://localhost:8080/api
done
# 结果
OK, request count=1
OK, request count=2
OK, request count=3
OK, request count=4
OK, request count=5
OK, request count=6
OK, request count=7
OK, request count=8
OK, request count=9
OK, request count=10
Too Many Requests, limit=10
Too Many Requests, limit=10
Too Many Requests, limit=10
Too Many Requests, limit=10
Too Many Requests, limit=10