redis2: NGINX 上游模块用于 Redis 2.0 协议
安装
您可以在任何基于 RHEL 的发行版中安装此模块,包括但不限于:
- RedHat Enterprise Linux 7、8、9 和 10
- CentOS 7、8、9
- AlmaLinux 8、9
- Rocky Linux 8、9
- Amazon Linux 2 和 Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install nginx-module-redis2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install nginx-module-redis2
通过在 /etc/nginx/nginx.conf 顶部添加以下内容来启用该模块:
load_module modules/ngx_http_redis2_module.so;
本文档描述了 nginx-module-redis2 v0.15,于 2018 年 4 月 19 日发布。
location = /foo {
set $value 'first';
redis2_query set one $value;
redis2_pass 127.0.0.1:6379;
}
# GET /get?key=some_key
location = /get {
set_unescape_uri $key $arg_key; # 这需要 ngx_set_misc
redis2_query get $key;
redis2_pass foo.com:6379;
}
# GET /set?key=one&val=first%20value
location = /set {
set_unescape_uri $key $arg_key; # 这需要 ngx_set_misc
set_unescape_uri $val $arg_val; # 这需要 ngx_set_misc
redis2_query set $key $val;
redis2_pass foo.com:6379;
}
# 多个管道查询
location = /foo {
set $value 'first';
redis2_query set one $value;
redis2_query get one;
redis2_query set one two;
redis2_query get one;
redis2_pass 127.0.0.1:6379;
}
location = /bar {
# $ 在这里不是特殊的...
redis2_literal_raw_query '*1\r\n$4\r\nping\r\n';
redis2_pass 127.0.0.1:6379;
}
location = /bar {
# 下面可以使用变量,$ 是特殊的
redis2_raw_query 'get one\r\n';
redis2_pass 127.0.0.1:6379;
}
# GET /baz?get%20foo%0d%0a
location = /baz {
set_unescape_uri $query $query_string; # 这需要 ngx_set_misc 模块
redis2_raw_query $query;
redis2_pass 127.0.0.1:6379;
}
location = /init {
redis2_query del key1;
redis2_query lpush key1 C;
redis2_query lpush key1 B;
redis2_query lpush key1 A;
redis2_pass 127.0.0.1:6379;
}
location = /get {
redis2_query lrange key1 0 -1;
redis2_pass 127.0.0.1:6379;
}
描述
这是一个 Nginx 上游模块,使 nginx 以非阻塞方式与 Redis 2.x 服务器进行通信。完整的 Redis 2.0 统一协议已实现,包括 Redis 管道支持。
该模块返回来自 Redis 服务器的原始 TCP 响应。建议使用我的 lua-redis-parser(用纯 C 编写)将这些响应解析为 lua 数据结构,结合使用 lua-nginx-module。
不过,当与 lua-nginx-module 结合使用时,建议使用 lua-resty-redis 库,而不是这个模块,因为前者更灵活且内存效率更高。
如果您只想使用 get redis 命令,可以尝试 HttpRedisModule。它返回 Redis 响应的解析内容部分,因为只需要 get 来实现。
另一个选择是在客户端自己解析 redis 响应。
指令
redis2_query
语法: redis2_query cmd arg1 arg2 ...
默认: 无
上下文: location, location if
通过以类似于 redis-cli 工具的方式指定其单独参数(包括 Redis 命令名称本身)来指定 Redis 命令。
在单个 location 中允许多个此指令的实例,这些查询将被管道化。例如,
location = /pipelined {
redis2_query set hello world;
redis2_query get hello;
redis2_pass 127.0.0.1:$TEST_NGINX_REDIS_PORT;
}
那么 GET /pipelined 将产生两个连续的原始 Redis 响应
+OK
$5
world
这里的换行符实际上是 CR LF (\r\n)。
redis2_raw_query
语法: redis2_raw_query QUERY
默认: 无
上下文: location, location if
指定原始 Redis 查询,并在 QUERY 参数中识别 nginx 变量。
在 QUERY 参数中只允许 一个 Redis 命令,否则将收到错误。如果您想在单个查询中指定多个管道命令,请使用 redis2_raw_queries 指令。
redis2_raw_queries
语法: redis2_raw_queries N QUERIES
默认: 无
上下文: location, location if
在 QUERIES 参数中指定 N 个命令。N 和 QUERIES 参数都可以使用 Nginx 变量。
以下是一些示例
location = /pipelined {
redis2_raw_queries 3 "flushall\r\nget key1\r\nget key2\r\n";
redis2_pass 127.0.0.1:6379;
}
# GET /pipelined2?n=2&cmds=flushall%0D%0Aget%20key%0D%0A
location = /pipelined2 {
set_unescape_uri $n $arg_n;
set_unescape_uri $cmds $arg_cmds;
redis2_raw_queries $n $cmds;
redis2_pass 127.0.0.1:6379;
}
redis2_literal_raw_query
语法: redis2_literal_raw_query QUERY
默认: 无
上下文: location, location if
指定原始 Redis 查询,但其中的 Nginx 变量将不会被识别。换句话说,您可以在 QUERY 参数中自由使用美元符号字符 ($)。
在 QUERY 参数中只允许一个 Redis 命令。
redis2_pass
语法: redis2_pass <upstream_name>
语法: redis2_pass <host>:<port>
默认: 无
上下文: location, location if
阶段: content
指定 Redis 服务器后端。
redis2_connect_timeout
语法: redis2_connect_timeout <time>
默认: 60s
上下文: http, server, location
连接到 Redis 服务器的超时时间,默认以秒为单位。
最好始终明确指定时间单位以避免混淆。支持的时间单位有 s(秒)、ms(毫秒)、y(年)、M(月)、w(周)、d(天)、h(小时)和 m(分钟)。
此时间必须少于 597 小时。
redis2_send_timeout
语法: redis2_send_timeout <time>
默认: 60s
上下文: http, server, location
发送 TCP 请求到 Redis 服务器的超时时间,默认以秒为单位。
最好始终明确指定时间单位以避免混淆。支持的时间单位有 s(秒)、ms(毫秒)、y(年)、M(月)、w(周)、d(天)、h(小时)和 m(分钟)。
redis2_read_timeout
语法: redis2_read_timeout <time>
默认: 60s
上下文: http, server, location
从 Redis 服务器读取 TCP 响应的超时时间,默认以秒为单位。
最好始终明确指定时间单位以避免混淆。支持的时间单位有 s(秒)、ms(毫秒)、y(年)、M(月)、w(周)、d(天)、h(小时)和 m(分钟)。
redis2_buffer_size
语法: redis2_buffer_size <size>
默认: 4k/8k
上下文: http, server, location
此缓冲区大小用于读取 Redis 回复,但不需要大于可能的最大 Redis 回复。
此默认大小为页面大小,可能为 4k 或 8k。
redis2_next_upstream
语法: redis2_next_upstream [ error | timeout | invalid_response | off ]
默认: error timeout
上下文: http, server, location
指定哪些故障条件应导致请求转发到另一个上游服务器。仅在 redis2_pass 中的值为两个或多个服务器的上游时适用。
这是一个人工示例:
upstream redis_cluster {
server 127.0.0.1:6379;
server 127.0.0.1:6380;
}
server {
location = /redis {
redis2_next_upstream error timeout invalid_response;
redis2_query get foo;
redis2_pass redis_cluster;
}
}
连接池
您可以使用出色的 HttpUpstreamKeepaliveModule 与此模块一起提供 Redis 的 TCP 连接池。
一个示例配置片段如下所示
http {
upstream backend {
server 127.0.0.1:6379;
# 一个最多 1024 个连接的池
# 并且不区分服务器:
keepalive 1024;
}
server {
...
location = /redis {
set_unescape_uri $query $arg_query;
redis2_query $query;
redis2_pass backend;
}
}
}
选择 Redis 数据库
Redis 提供 select 命令来切换 Redis 数据库。此命令与其他普通命令没有区别,例如 get 或 set。因此,您可以在 redis2_query 指令中使用它们,例如,
redis2_query select 8;
redis2_query get foo;
Lua 互操作性
此模块可以作为 lua-nginx-module 的非阻塞 redis2 客户端(但现在建议使用 lua-resty-redis 库,因为它更简单且大多数情况下更高效)。 以下是使用 GET 子请求的示例:
location = /redis {
internal;
# set_unescape_uri 由 ngx_set_misc 提供
set_unescape_uri $query $arg_query;
redis2_raw_query $query;
redis2_pass 127.0.0.1:6379;
}
location = /main {
content_by_lua '
local res = ngx.location.capture("/redis",
{ args = { query = "ping\\r\\n" } }
)
ngx.print("[" .. res.body .. "]")
';
}
然后访问 /main 会产生
[+PONG\r\n]
其中 \r\n 是 CRLF。也就是说,该模块返回来自远程 Redis 服务器的 原始 TCP 响应。对于基于 Lua 的应用程序开发人员,他们可能希望利用 lua-redis-parser 库(用纯 C 编写)将此类原始响应解析为 Lua 数据结构。
当将内联 Lua 代码移动到外部 .lua 文件时,重要的是直接使用转义序列 \r\n。我们在上面使用 \\r\\n 只是因为 Lua 代码本身在放入 Nginx 字符串字面量时需要引用。
您还可以使用 POST/PUT 子请求通过请求体传输原始 Redis 请求,这不需要 URI 转义和解转义,从而节省一些 CPU 周期。以下是这样的示例:
location = /redis {
internal;
# $echo_request_body 由 ngx_echo 模块提供
redis2_raw_query $echo_request_body;
redis2_pass 127.0.0.1:6379;
}
location = /main {
content_by_lua '
local res = ngx.location.capture("/redis",
{ method = ngx.HTTP_PUT,
body = "ping\\r\\n" }
)
ngx.print("[" .. res.body .. "]")
';
}
这产生的输出与之前的(GET)示例完全相同。
还可以使用 Lua 根据一些复杂的哈希规则选择具体的 Redis 后端。例如,
upstream redis-a {
server foo.bar.com:6379;
}
upstream redis-b {
server bar.baz.com:6379;
}
upstream redis-c {
server blah.blah.org:6379;
}
server {
...
location = /redis {
set_unescape_uri $query $arg_query;
redis2_query $query;
redis2_pass $arg_backend;
}
location = /foo {
content_by_lua "
-- 随机选择一个服务器
local servers = {'redis-a', 'redis-b', 'redis-c'}
local i = ngx.time() % #servers + 1;
local srv = servers[i]
local res = ngx.location.capture('/redis',
{ args = {
query = '...',
backend = srv
}
}
)
ngx.say(res.body)
";
}
}
通过 Lua 管道化 Redis 请求
以下是一个完整示例,演示如何使用 Lua 通过此 Nginx 模块发出多个管道化的 Redis 请求。
首先,我们在 nginx.conf 文件中包含以下内容:
location = /redis2 {
internal;
redis2_raw_queries $args $echo_request_body;
redis2_pass 127.0.0.1:6379;
}
location = /test {
content_by_lua_file conf/test.lua;
}
基本上,我们使用 URI 查询参数来传递 Redis 请求的数量,并使用请求体传递管道化的 Redis 请求字符串。
然后我们创建 conf/test.lua 文件(其路径相对于 Nginx 的服务器根目录),包含以下 Lua 代码:
-- conf/test.lua
local parser = require "redis.parser"
local reqs = {
{"set", "foo", "hello world"},
{"get", "foo"}
}
local raw_reqs = {}
for i, req in ipairs(reqs) do
table.insert(raw_reqs, parser.build_query(req))
end
local res = ngx.location.capture("/redis2?" .. #reqs,
{ body = table.concat(raw_reqs, "") })
if res.status ~= 200 or not res.body then
ngx.log(ngx.ERR, "failed to query redis")
ngx.exit(500)
end
local replies = parser.parse_replies(res.body, #reqs)
for i, reply in ipairs(replies) do
ngx.say(reply[1])
end
在这里,我们假设您的 Redis 服务器在本地主机的默认端口(6379)上监听。我们还利用 lua-redis-parser 库为我们构建原始 Redis 查询,并使用它解析回复。
通过 HTTP 客户端(如 curl)访问 /test 位置会产生以下输出
OK
hello world
更现实的设置是为我们的 Redis 后端使用适当的上游定义,并通过 keepalive 指令启用 TCP 连接池。
Redis 发布/订阅支持
此模块对 Redis 发布/订阅功能的支持有限。由于 REST 和 HTTP 模型的无状态特性,无法完全支持。
考虑以下示例:
location = /redis {
redis2_raw_queries 2 "subscribe /foo/bar\r\n";
redis2_pass 127.0.0.1:6379;
}
然后在 redis-cli 命令行中为键 /foo/bar 发布一条消息。然后您将从 /redis 位置收到两个多重回复。
如果您使用 Lua 访问此模块的位置,您当然可以使用 lua-redis-parser 库解析回复。
Redis 发布/订阅的限制
如果您想使用此模块的 Redis pub/sub 功能,则必须注意以下限制:
- 您不能将 HttpUpstreamKeepaliveModule 与此 Redis 上游一起使用。只有短暂的 Redis 连接会有效。
- 可能会出现一些竞争条件,导致在 nginx 的 error.log 中产生无害的
Redis server returned extra bytes警告。这种警告可能很少见,但请做好准备。 - 您应该调整此模块提供的各种超时设置,如 redis2_connect_timeout 和 redis2_read_timeout。
如果您无法忍受这些限制,强烈建议您切换到 lua-resty-redis 库,以便与 lua-nginx-module 一起使用。
性能调优
- 当您使用此模块时,请确保使用 TCP 连接池(由 HttpUpstreamKeepaliveModule 提供)并尽可能使用 Redis 管道。这些功能将显著提高性能。
- 在多核机器上使用多个 Redis 服务器实例也有很大帮助,因为单个 Redis 服务器实例的顺序处理特性。
- 当您使用
ab或http_load等工具进行性能基准测试时,请确保您的错误日志级别足够高(如warn),以防止 Nginx 工作进程在刷新error.log文件时花费过多周期,该文件始终是非缓冲和阻塞的,因此非常昂贵。
另请参阅
- Redis 服务器主页。
- Redis 线协议:http://redis.io/topics/protocol
- Lua 的 Redis 响应解析器和请求构造器:lua-redis-parser。
- lua-nginx-module
- ngx_openresty bundle。
- 基于 lua-nginx-module cosocket API 的 lua-resty-redis 库。
GitHub
您可以在 nginx-module-redis2 的 GitHub 仓库中找到此模块的其他配置提示和文档。