1. 为什么想到用 Nginx 代理 Redis
先说一个我自己的经历。之前负责一个内部平台,后端服务拆了十几个微服务,全都直连一台 Redis 实例。当时 Redis 部署在专属服务器上,只对内网开放,本来挺安全的。但随着服务越来越多,问题开始冒头:每个服务都要单独配 Redis 地址和端口,一旦 Redis 迁移或者加密码,所有服务都得跟着改配置,运维工作量暴增。更头疼的是,有些服务需要临时扩容,新起的节点又不在原本的网络段里,访问 Redis 就成了问题。
当时我的第一反应不是改架构,而是想到了 Nginx。大部分人对 Nginx 的印象就是 Web 服务器、反向代理 HTTP、负载均衡,实际上 Nginx 还有一个很少被提到的能力——支持四层负载均衡和代理。从 1.9.0 版本开始,Nginx 引入了 stream 模块,专门处理 TCP 和 UDP 流量。Redis 虽然也支持 HTTP 协议(通过模块扩展),但最常用的还是原生 TCP 协议。既然 Nginx 能代理 TCP,那它自然也能代理 Redis。
这个思路其实并不新鲜,很多云厂商的 Redis 网关就是这么干的。但自己动手用 Nginx 把 Redis 代理起来,还是有几个实打实的好处:
- 统一入口:所有客户端只连接 Nginx 的地址,不直接感知后端 Redis 的真实 IP。Redis 迁移、扩容对客户端透明。
- 安全隔离:Redis 不用暴露到更广的网络范围,由 Nginx 作为唯一入口,可以配合 access 模块做 IP 白名单。
- 负载均衡:如果你的 Redis 做了主从或者集群,Nginx 的 stream 模块可以按权重分发 TCP 连接。
- 复用已有设施:团队本来就在维护 Nginx,不需要额外引入 HAProxy 之类的组件,运维成本几乎为零。
当然,用 Nginx 代理 Redis 也不是银弹,后面我会详细讲它的局限性和替代方案。但总体来说,这是一个投入产出比很高的方案,适合中小团队快速解决 Redis 访问入口统一的问题。
2. 核心细节解析:连接生命周期与协议穿透
要把 Nginx 代理 Redis 配置写好,首先要理解它工作的本质。和 HTTP 代理不同,Redis 走的是 TCP 长连接。客户端连接 Redis 后,可能执行几十条命令再断开,也可能一直保持空闲等待下一次查询。Nginx 作为中间层,起到的不是"解析协议"的作用,而是"转发字节流"的作用。
2.1 Nginx stream 模块的两种工作模式
stream 模块配置简单,开箱即用。但很多人不知道的是,它对 Redis 的 TCP 转发有两种模式,效果完全不同。
第一种是透传模式。配置里只有proxy_pass,Nginx 收到客户端的 TCP 连接后,立刻和后端 Redis 建立另一条 TCP 连接,然后两边的数据流就完全转发,Nginx 只做搬运工。这种模式对 Redis 命令没有任何感知,Redis 的 AUTH 认证、SELECT 切换库、事务、订阅等全部正常工作。
第二种是内存代理模式。也就是利用proxy_protocol或者一些 Lua 脚本在 Nginx 里解析 Redis 协议。这种模式能对 Redis 命令做干预,比如改 key 前缀、拦截危险命令。但代价是破坏了原始协议流,很多高级特性会被影响,维护成本也非常高。我个人建议,绝大多数场景下老老实实用透传模式,别搞花活。
# 透传模式核心配置 stream { upstream redis_backend { server 127.0.0.1:6379 max_fails=2 fail_timeout=30s; } server { listen 6380; proxy_pass redis_backend; proxy_connect_timeout 5s; proxy_timeout 300s; } }这段配置里顺带解释两个参数:
proxy_connect_timeout是 Nginx 与后端 Redis 建立 TCP 连接的超时时间。如果 Redis 挂了,没有这个参数的话,客户端可能要等很久才能感知到连接失败。proxy_timeout是连接空闲超时。客户端和 Redis 之间一旦没有任何数据传输,超过这个时间 Nginx 会主动断开连接。Redis 客户端一般会自动重连,所以这个参数设成 300 秒是比较稳妥的,兼顾了资源回收和连接稳定性。
2.2 Redis 协议穿透的底层逻辑
很多第一次接触的人会担心:Nginx 转发 TCP 会不会把 Redis 的二进制数据弄坏?答案是基本不会。Redis 的 RESP 协议看起来是文本,但实际可能包含二进制安全的字符串(比如图片的字节数组)。Nginx 的 stream 模块在原生的ngx_stream_proxy_module里,使用的是ngx_buf_t缓冲区直接搬运数据,不做任何内容检查,所以二进制安全是天然保证的。
如果你在代理过程中遇到了数据异常,大概率不是 Nginx 的问题,而是客户端连接池配置导致的。比如有的客户端在代理后端切换时会带上老的连接标识,或者 Nginx 的空闲超时把连接断了,而客户端不知道,还拿旧连接去发命令。这时候最常见的报错是Connection reset by peer或者Broken pipe,排查思路是检查客户端连接池的test_on_borrow配置,以及 Nginx 的proxy_timeout是否设得太短。
2.3 密码认证放哪里?——绕不开的 AUTH 问题
Redis 默认不带密码,生产环境基本都会开启requirepass。在直连模式下,客户端配置密码直接连接即可。但如果走 Nginx 代理,密码处理有两种方式:
- 客户端全量连接 Nginx,Nginx 透传给 Redis。客户端在连接后发 AUTH 命令,Nginx 原样转发,Redis 校验通过即建立会话。这种方式最简单,密码只存在客户端配置里,Nginx 完全不用管。
- Redis 做白名单,Nginx 只转发指定来源。在 Redis 配置里用
bind限制只允许 Nginx 服务器的 IP 访问,客户端密码还是自己发。
这里有一个很容易踩的坑:如果你在 Nginx 层自己定义proxy_pass时,想顺手把 AUTH 也代理掉(比如在 Nginx 配置里写好密码,后端 Redis 不认证,由 Nginx 统一认证),那我强烈建议不要这么做。因为你无法拦截已经被 Nginx 转发的 RESP 协议中的认证状态,会导致有些客户端连接池里混合了已认证和未认证的连接,出现诡异的数据访问错误。认认真真让 Redis 层做认证,Nginx 只做转发,这才是稳定之道。
3. 实操过程与核心环节实现
下面我给出一个完整的操作过程,从 Nginx 安装、Redis 服务端准备,到最终配置验证。我的环境是 Ubuntu 22.04,Nginx 使用官方源编译安装的 1.24 版本,Redis 是 7.0。如果你用的是 CentOS 或宝塔面板,步骤稍微有差异,但核心思想一致。
3.1 环境准备与 Nginx stream 模块确认
首先确认 Nginx 是否带了 stream 模块。很多发行版的默认 Nginx 不带这个模块,需要额外安装。最简单的确认方式是执行:
nginx -V 2>&1 | grep stream如果有输出,比如--with-stream --with-stream_ssl_module,说明已经支持。如果没有,你可以选择重新编译 Nginx,或者直接用包管理器安装 nginx-mod-stream:
# Ubuntu/Debian apt install libnginx-mod-stream # CentOS/RHEL yum install nginx-mod-stream安装完重启 Nginx,再次执行nginx -V确认。这一步不能省略,否则配置写了不生效都不知道。
3.2 配置 Nginx 代理 Redis
我采用的方案是:Redis 在 10.0.0.5:6379,Nginx 在 10.0.0.8,监听 16379 端口对外提供服务。客户端连接 10.0.0.8:16379,所有数据转发到 10.0.0.5:6379。
在 Nginx 配置目录下新建一个专门的文件,比如/etc/nginx/conf.d/redis_proxy.conf:
stream { upstream redis_backend { server 10.0.0.5:6379 max_fails=3 fail_timeout=30s; } server { listen 16379; proxy_connect_timeout 5s; proxy_timeout 300s; proxy_pass redis_backend; } # 如果需要对 Redis 管理端口也做代理,可以再加一个 server 块 # server { # listen 16380; # proxy_pass 10.0.0.5:6380; # } }注意,stream块和http块是平级的,不能写在http里面。你可以把这段配置放在nginx.conf主配置文件的顶层,也可以像我一样放在conf.d目录下然后include进来。确保语法检查:
nginx -t如果报错unknown directive "stream",说明 stream 模块没装好,重新检查第一步。
3.3 Redis 服务端的安全强化
代理设置好了之后,Redis 本身就不用对客户端网络完全敞开了。建议做三件事:
- 修改 Redis 监听地址:如果 Redis 和 Nginx 在同一台机器,
bind 127.0.0.1即可;如果在不同机器,bind到 Nginx 服务器的内网 IP,别用0.0.0.0。 - 设置强密码:在 redis.conf 中启用
requirepass,密码至少 32 位以上,使用独立密码管理工具生成。 - 禁用危险命令:通过
rename-command将FLUSHALL、FLUSHDB、KEYS等重命名或禁用,防止误操作和恶意操作。
完成以上配置后重启 Redis,然后在 Nginx 服务器上测试能否连接:
redis-cli -h 10.0.0.8 -p 16379 -a '你的密码' ping如果返回PONG,说明代理链路已经通了。
3.4 客户端接入的配置调整
客户端这边,原来的 Redis 地址是10.0.0.5:6379,现在改成10.0.0.8:16379。密码不变。如果你用的是 Java 的 Lettuce 或 Jedis,改了 host 和 port 即可。我用一段 Python 示例:
import redis r = redis.Redis( host='10.0.0.8', # Nginx 地址 port=16379, # Nginx 监听端口 password='你的密码', socket_timeout=5, socket_connect_timeout=5, retry_on_timeout=True ) r.set('foo', 'bar') print(r.get('foo'))如果你用的是 Spring Boot 的 RedisTemplate,就是改spring.redis.host和spring.redis.port。所有客户端一个配置类改完,后面 Redis 再怎么迁移,都不用再动客户端了。
3.5 多 Redis 实例的负载均衡扩展
如果你的 Redis 做了主从复制,Nginx 可以轻松做到读写分离。主库负责写,从库负责读。配置如下:
stream { upstream redis_master { server 10.0.0.5:6379; } upstream redis_slaves { server 10.0.0.6:6379 weight=2; server 10.0.0.7:6379 weight=1; } server { listen 16379; proxy_pass redis_master; } server { listen 16380; proxy_pass redis_slaves; } }客户端写数据连接 16379,读数据连接 16380。注意,这种方案是在客户端层面区分读写,Nginx 只负责把连接分发到对应的后端组。Redis 主从之间本身有数据同步延迟,所以读从库要接受一定的数据滞后。如果你的业务对一致性要求极高,建议还是读写都走主库。
3.6 连接 TLS 加密(可选进阶)
如果 Redis 和 Nginx 之间的网络跨越不可信区域,可以额外给 TCP 套一层 TLS。Redis 6.0 以上原生支持 TLS,但配置相对繁琐。我一般会选择在 Nginx 层做 TLS 终结,客户端和 Nginx 走 TLS,Nginx 到 Redis 走内网明文。这样客户端不用支持 Redis TLS 协议,统一用 Nginx 的 SSL 证书。
stream { server { listen 16379 ssl; proxy_pass 10.0.0.5:6379; ssl_certificate /etc/nginx/ssl/redis.crt; ssl_certificate_key /etc/nginx/ssl/redis.key; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers HIGH:!aNULL:!MD5; } }此时客户端连接需要换成rediss://或者按客户端库的 TLS 选项开启 SSL。这个方法特别适合公网环境下安全访问 Redis。
4. 常见问题与排查技巧实录
这一节我汇总了我在实际运维中遇到的典型问题,快查快用。
4.1 客户端连接报 Connection Refused
遇到这个问题先别急着查 Nginx。第一步是确认 Nginx 端口有没有在监听:
ss -lntp | grep 16379如果没有输出,说明 Nginx 配置有问题,可能是 stream 模块没加载,或者配置语法错误,用nginx -t查一下。如果监听正常,再检查防火墙和安全组。很多云服务器默认安全组只开了 80/443,忘了放行 16379,导致外部访问被拒。
4.2 连接正常但 Redis 认证失败
Redis 报NOAUTH Authentication required,但你明明在客户端配置了密码。这种情况大概率是客户端连接池把没有通过认证的连接复用给了其他请求。我排查这类问题的经验是:先用 telnet 手动发一次 AUTH 命令确认链路,再用客户端库的单一连接测试。如果手动可以、程序不行,十有八九是连接池的问题。解决办法是调整客户端连接池参数,比如 Lettuce 的validateConnection设为 true,或者把空闲超时设置短一点。
4.3 连接偶尔超时或断开
这个现象很典型:日志里有周期性报错Read timed out或者Connection reset by peer。原因通常是 Nginx 的proxy_timeout把空闲连接关了,而客户端的连接池还认为连接是好的。要解决这个问题,就是把proxy_timeout调大,比如 3600 秒,同时客户端连接池的空闲回收时间要小于这个值,保证客户端会主动先断开。
我常用的组合是:
- Nginx
proxy_timeout设为 600 秒 - Java Lettuce 空闲超时设为 300 秒
- Python redis-py
socket_keepalive=True并设置socket_keepalive_options
4.4 Redis 慢查询日志看不到真实客户端 IP
这个是一个隐藏比较深的问题。由于 Redis 连接全部经 Nginx 转发,Redis 内部记录的client addr全是 Nginx 的 IP,看不到客户端真实来源。如果你们的排查流程依赖慢查询日志定位业务方,这就麻烦了。
两种解法:
- 在 Redis 7.0 以上使用
CLIENT SETINFO在客户端连接时带上应用名和 IP 信息。 - 或者修改 Redis 源码级的三方插件(不推荐)。
从运维角度讲,只要你能接受"连接来源都是 Nginx"这个事实,问题也不大。业务侧排查慢查询时,多一步去 Nginx 日志按时间窗口交叉比对,也能准确定位。
4.5 高并发场景下 Nginx 代理性能瓶颈
Nginx 代理 TCP 的转发性能通常受两个限制:文件描述符数量和工作进程数。如果你发现高并发下大量连接被拒绝,先调这两个参数:
worker_processes auto; worker_rlimit_nofile 65535; events { worker_connections 65535; accept_mutex off; }另外,如果你的 Redis 本身性能很强,但代理层成了瓶颈,可以考虑开启 Nginx 的multi_accept并配合内核参数优化。实测下来,4 核 8G 的 Nginx 单机可以稳定支撑 5 万个并发连接,再往上就要考虑多 Nginx 节点或者换用 LVS 方案了。
4.6 Redis 集群模式能否走 Nginx
这里必须给一个明确的结论:Nginx 原生 stream 模块不适合代理 Redis Cluster 模式。Redis Cluster 的客户端会直接连接多个节点,还会根据 key 的 CRC16 哈希路由到不同节点。Nginx 只做 TCP 转发,无法感知 key 路由逻辑,所以集群模式下客户端还是直连节点更合适,或者使用官方推荐的 Redis Cluster 代理(比如 Predixy 或 Codis)。
如果你的场景是单机或主从复制,Nginx 完全够用。如果已经上了集群,就别硬用 Nginx 了。
5. 运维实战心得与监控建议
最后分享几个我在长期运行中的心得。
5.1 日志监控怎么做
Nginx 的 stream 模块默认不写访问日志,需要手动开启。在 stream 配置里加上:
log_format redis_proxy '$remote_addr [$time_local] $protocol $status $bytes_sent $bytes_received $session_time'; access_log /var/log/nginx/redis_access.log redis_proxy;这个日志清楚记录了每个连接来自哪里、连接了多久、传了多少字节。利用它做成功率监控很方便。再配合 Nginx 的ngx_stream_status_module,可以从 status 页看到当前活跃连接数,设置监控告警。
5.2 升级维护时的优雅切换
当你需要对后端 Redis 做升级时,利用 Nginx 可以做到无缝切换。具体操作方式:
upstream redis_backend { server 10.0.0.5:6379 max_fails=1 fail_timeout=300s; server 10.0.0.9:6379 backup; }把新 Redis 节点作为备用节点配置。升级时,先在备用节点上完成数据同步,然后平滑把主节点从 upstream 中摘掉。由于客户端始终连接 Nginx,整个过程客户端无感知。这个技巧在大版本 Redis 升级时特别管用。
5.3 到底什么时候不该用 Nginx 代理 Redis
讲了这么多,也该泼点冷水。在以下三种场景下,我不推荐用 Nginx 代理 Redis:
- 已经有 Redis Cluster 集群:Nginx 无法感知 key 路由,硬加一层除了增加延迟没有意义。
- 极高性能要求:多一层代理意味着多两次 TCP 数据拷贝,虽然 Nginx 性能很高,但在极端压测下依然会有 5%~10% 的吞吐损失。
- 团队缺少 Nginx 运维能力:代理层出了问题,排障需要看 Nginx 的连接状态、内核参数,没经验的话反而是负担。
根据我个人的取舍标准,凡是单实例 Redis 或者主从复制方案,我优先考虑 Nginx;凡是集群模式,我不碰 Nginx,直接用官方方案或专门代理。还有一个我个人坚持的细节:Nginx 代理 Redis 时必须把tcp_nodelay开启,避免小数据包在网络上黏滞,Redis 都是小命令,这个优化对延迟改善非常明显。
stream { server { listen 16379; proxy_pass 10.0.0.5:6379; tcp_nodelay on; } }在真实业务里我还喜欢再补一个客户端连接数限制,比如同一 IP 最多允许 256 个连接,防止某个异常客户端把代理层的连接池打满:
server { listen 16379; proxy_pass 10.0.0.5:6379; limit_conn_zone $binary_remote_addr zone=redis_conn:10m; limit_conn redis_conn 256; }这两段配置我踩过不少坑,开始没加tcp_nodelay的时候,偶尔会碰到 Redis 写操作多了之后小包堆积导致延迟抖动,加上之后问题就消失了。连接数限制则是有一次线上客户端连接泄漏,活活把 Nginx 的文件描述符耗尽,从此以后每台代理我都强制配这个限制。
如果你正准备把 Redis 接入 Nginx 代理,按照我上面给的步骤来做,基本可以避开绝大多数隐藏坑。配置本身没什么难度,理解它的转发原理和连接生命周期才是关键。在这些基础上,你就能安心享受统一入口带来的运维便利了。