跳转至

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 个命令。NQUERIES 参数都可以使用 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;
 }
请注意,在上面的第二个示例中,set_unescape_uri 指令由 set-misc-nginx-module 提供。

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 数据库。此命令与其他普通命令没有区别,例如 getset。因此,您可以在 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\nCRLF。也就是说,该模块返回来自远程 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_timeoutredis2_read_timeout

如果您无法忍受这些限制,强烈建议您切换到 lua-resty-redis 库,以便与 lua-nginx-module 一起使用。

性能调优

  • 当您使用此模块时,请确保使用 TCP 连接池(由 HttpUpstreamKeepaliveModule 提供)并尽可能使用 Redis 管道。这些功能将显著提高性能。
  • 在多核机器上使用多个 Redis 服务器实例也有很大帮助,因为单个 Redis 服务器实例的顺序处理特性。
  • 当您使用 abhttp_load 等工具进行性能基准测试时,请确保您的错误日志级别足够高(如 warn),以防止 Nginx 工作进程在刷新 error.log 文件时花费过多周期,该文件始终是非缓冲和阻塞的,因此非常昂贵。

另请参阅

GitHub

您可以在 nginx-module-redis2 的 GitHub 仓库中找到此模块的其他配置提示和文档。