ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Nginx stream模块代理Redis:统一入口与运维实践

Nginx stream模块代理Redis:统一入口与运维实践 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_passNginx 收到客户端的 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_fails2 fail_timeout30s; } 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 代理密码处理有两种方式客户端全量连接 NginxNginx 透传给 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.04Nginx 使用官方源编译安装的 1.24 版本Redis 是 7.0。如果你用的是 CentOS 或宝塔面板步骤稍微有差异但核心思想一致。3.1 环境准备与 Nginx stream 模块确认首先确认 Nginx 是否带了 stream 模块。很多发行版的默认 Nginx 不带这个模块需要额外安装。最简单的确认方式是执行nginx -V 21 | 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:6379Nginx 在 10.0.0.8监听 16379 端口对外提供服务。客户端连接 10.0.0.8:16379所有数据转发到 10.0.0.5:6379。在 Nginx 配置目录下新建一个专门的文件比如/etc/nginx/conf.d/redis_proxy.confstream { upstream redis_backend { server 10.0.0.5:6379 max_fails3 fail_timeout30s; } 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( host10.0.0.8, # Nginx 地址 port16379, # Nginx 监听端口 password你的密码, socket_timeout5, socket_connect_timeout5, retry_on_timeoutTrue ) 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 weight2; server 10.0.0.7:6379 weight1; } 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 走 TLSNginx 到 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 秒同时客户端连接池的空闲回收时间要小于这个值保证客户端会主动先断开。我常用的组合是Nginxproxy_timeout设为 600 秒Java Lettuce 空闲超时设为 300 秒Python redis-pysocket_keepaliveTrue并设置socket_keepalive_options4.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_fails1 fail_timeout300s; 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 zoneredis_conn:10m; limit_conn redis_conn 256; }这两段配置我踩过不少坑开始没加tcp_nodelay的时候偶尔会碰到 Redis 写操作多了之后小包堆积导致延迟抖动加上之后问题就消失了。连接数限制则是有一次线上客户端连接泄漏活活把 Nginx 的文件描述符耗尽从此以后每台代理我都强制配这个限制。如果你正准备把 Redis 接入 Nginx 代理按照我上面给的步骤来做基本可以避开绝大多数隐藏坑。配置本身没什么难度理解它的转发原理和连接生命周期才是关键。在这些基础上你就能安心享受统一入口带来的运维便利了。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进