ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Redis连接失败排查:配置项与连接池的深度拆解

Redis连接失败排查:配置项与连接池的深度拆解 1. 三天排查路的起点那些看起来很正常的报错先把场景还原一下因为这决定了后面所有排查方向。一个跑了小半年的服务某天开始间歇性报连接异常日志里大概率是这么几行Cannot get Jedis connection、Unable to connect to Redis、Connection refused、Read timed out或者更含糊的Pool exhausted。诡异的地方在于——重启服务就好跑一段时间又犯本地用客户端连上去一切正常ping返回PONGinfo也能看到数据redis-cli敲命令毫无卡顿。这就是最折磨人的一类问题单点测试通过集群环境失败低频操作正常高频操作崩溃。你打开监控CPU、内存、QPS 曲线全都平滑得像教科书偏偏业务侧就是连不上。我先把结论摆在前面省得你看到一半着急在我经手的这类案例里最终定位到的根因十次里有六到七次跟配置直接相关而不是什么 Redis 自身的玄学 Bug。剩下的三四次一半是网络中间设备一半是客户端连接池参数与业务并发量不匹配。真正 Redis 服务端代码级的问题占比极低。为什么配置问题这么难查因为它有个非常要命的特征——它不会立刻报错而是某条件下才触发。比如timeout配了 0 表示永不超时看起来最安全实际上会让空闲连接被中间设备悄悄掐断后客户端还傻乎乎地拿去用又比如maxmemory-policy配成noeviction内存写满后写入命令直接返回错误表现出来却像是连接失败。所以第一步要做的不是去翻配置而是把连接失败这四个字拆开。它至少包含四类完全不同的故障表象关键词真实含义大概率归属Connection refusedTCP 都没握上手端口、绑定地址、防火墙、服务没起Read timed out握手成功读响应超时阻塞命令、慢查询、网络抖动、超时配置Pool exhausted / 无法获取连接拿不到池里的连接连接池参数、连接泄漏、并发突增NOAUTH / WRONGPASS连上了但认证失败requirepass、ACL、密码含特殊字符这四类的排查路径完全不同。我见过太多人一上来就telnet一下端口通了然后陷入网络没问题啊的死胡同实际上人家卡在第三类——连接池压根没给你连接。提示先把日志里出现的错误原文完整复制出来去搜精确字符串不要搜Redis 连接失败这种泛化词。错误类名、异常栈第一行才是真正的线索。我自己踩过的最典型的一个坑是本地开发环境一切正常上了测试环境就零散报错。当时第一反应是网络问题查了两天交换机、路由、安全组最后发现是测试环境 Redis 配置里bind只写了127.0.0.1而应用通过内网 IP 访问。本地能连是因为开发机上装了本地 Redis。配置和环境的组合才是问题的完整图景。2. 把配置文件摊开看哪几行才是真正的高危区redis.conf有好几百行注释比配置还多。全看一遍不现实但有几个配置项必须逐字核对因为它们直接决定连接行为。2.1 bind 与 protected-mode新版本最容易被默认值坑从 Redis 3.2 起引入了protected-mode默认是yes。它的作用简单粗暴如果没有显式配置bind也没有设置密码那么只接受来自本机的连接。很多人的配置里bind那一行是注释掉的或者写成了bind 127.0.0.1然后从另一台机器连必然失败。# 只监听本机外部一律拒 bind 127.0.0.1 # 想被内网访问得写实际网卡地址或 0.0.0.0 bind 0.0.0.0 protected-mode no这里有个取舍要讲清楚bind 0.0.0.0加protected-mode no是能用的配置但前提是你必须同时设置强密码并配合安全组限制来源。否则就是把数据库裸露在网络上这是运维事故的经典开局。我的习惯是bind写具体的内网网卡 IPprotected-mode保持默认yes密码单独配。这样即使配置出错兜底逻辑还在。判断方法很直接在应用所在的机器上执行# 看端口到底监听在哪个地址 ss -lntp | grep 6379 # 期望看到 0.0.0.0:6379 或 内网IP:6379 # 如果只看到 127.0.0.1:6379说明只监听本机这一条命令能直接干掉一半的连接失败。2.2 timeout 设成 0一个看起来最安全、实则最危险的值timeout这个配置项控制的是空闲连接多久被服务端主动断开单位秒0表示永不主动断开。很多人的直觉是永不超时 最稳定于是填了 0。真实情况恰恰相反。当timeout 0时服务端不会主动关闭空闲连接。但它和客户端之间的路径上通常还有一层设备——负载均衡、防火墙、云厂商的内网网关。这些设备有自己的空闲连接回收时间常见是 300 秒、600 秒或 900 秒。于是出现这样的时序应用建了一条连接用完放回池里。业务低谷连接空闲超过 600 秒。中间设备认为这条连接死了静默回收但不通知两端。应用从池里取出这条僵尸连接发命令发出去石沉大海读超时。表现出来就是偶尔连不上而且业务低谷期、隔夜之后的第一个请求最容易触发。这类问题在压测时反而不复现因为压测压力大连接一直活跃。合理的做法是让服务端主动回收早于中间设备# 服务端 300 秒没活动的连接就断开 timeout 300而客户端侧要配合两个参数连接池的空闲检测和最小空闲连接数。以 Java 的 Jedis 为例GenericObjectPoolConfigJedis poolConfig new GenericObjectPoolConfig(); poolConfig.setMaxTotal(50); poolConfig.setMaxIdle(20); poolConfig.setMinIdle(5); // 关键取连接时校验借出前 ping 一下 poolConfig.setTestOnBorrow(true); // 空闲连接定期被回收器检测避免长期滞留僵尸连接 poolConfig.setTimeBetweenEvictionRunsMillis(30000); poolConfig.setMinEvictableIdleTimeMillis(60000);testOnBorrow有性能代价每次借出多一次往返但对偶发连接异常这类问题几乎是必需的。折中方案是用testWhileIdle配合后台驱逐线程代价小很多代价是可能有一次请求踩到失效连接。低频业务用testOnBorrow高频业务用testWhileIdle这是我在不同项目里反复验证过的经验分界。2.3 密码里的特殊字符一个字符引发的认证失败requirepass和 ACL 体系下密码里如果含有#、空格、、!这类字符会出现非常隐蔽的问题。redis.conf里#之后的内容会被当作注释密码直接被截断shell 脚本里!会触发历史扩展URL 连接串里会被解析成参数分隔符。现象是redis-cli手动输入密码能连应用就是报WRONGPASS或NOAUTH。查半天以为是权限问题其实是密码字符串在传递过程中被改写了。规避方式很朴素密码只用大小写字母加数字长度够 16 位以上即可把安全性交给长度而不是特殊符号。如果非要用特殊字符配置文件中用双引号包裹requirepass MyPss#2024!word连接串里则必须做 URL 编码。这一条我吃过亏后来统一规定所有中间件密码禁用特殊字符省下的排查时间远超那点复杂度收益。2.4 maxmemory 与淘汰策略内存满了也会像连接失败maxmemory设置不当配合maxmemory-policy noeviction当内存写满后所有写命令会返回OOM command not allowed when used memory maxmemory。如果客户端把这类错误统一包装成操作失败再往外抛很容易被误读成连接问题。maxmemory 4gb maxmemory-policy allkeys-lrunoeviction只适合明确不允许丢数据的场景且必须配合业务层的容量告警。绝大多数缓存场景allkeys-lru或volatile-lru才是合理选择。判断是否命中这一点看一眼info memory里的used_memory和maxmemory关系就清楚了redis-cli info memory | grep -E used_memory_human|maxmemory_human如果两者已经非常接近甚至相等那方向就找到了。这类问题的隐蔽性在于它不影响连接建立只影响写命令而很多框架的异常包装把这些都归成了Redis 操作异常。3. 从现象反推我实际用的分层排查链路配置项看完了但现实是——你不能一上来就把所有配置逐条读一遍那效率太低。真正高效的方式是按现象分层收敛。下面这条链路是我这几年反复用、基本能在半天内定位到方向的流程。3.1 第一层确认是完全不通还是偶发不通这一步决定后面的所有动作。完全不通意味着资源、端口、网络层面的问题是确定性的偶发不通基本锁定在连接池、超时、网络设备这几个方向。压测工具是最快的判定手段。用redis-benchmark直接打或者写个循环脚本持续连接# 持续 60 秒每 100ms 连接一次观察失败率 for i in $(seq 1 600); do redis-cli -h 10.0.1.20 -p 6379 -a password ping /dev/null 21 \ || echo FAIL at $(date %T) sleep 0.1 done如果单机redis-cli高频打不出现失败而应用侧持续报错那问题就不在网络上而在应用侧的连接管理上。这一条判断省了我无数次无谓的网络排查。3.2 第二层看服务端到底看到了什么服务端视角是最容易被忽略的一环。info clients里有几个关键指标redis-cli info clients # connected_clients: 当前连接数 # blocked_clients: 正在阻塞BLPOP 等的连接数 # client_recent_max_input_buffer如果connected_clients已经顶到maxclients新连接会被直接拒绝报错就是连接被拒绝。而maxclients默认是 10000看似很大但如果应用侧连接池配了maxTotal200开了 60 个实例再加各种监控、运维连接突破 10000 并不难。更隐蔽的是blocked_clients。如果有人用了BLPOP、BRPOP、SUBSCRIBE这类阻塞命令而这些连接长期不释放它们会一直占着连接数。这类连接不消耗 CPU监控上完全看不到异常但连接数会被慢慢吃掉。我曾遇到一个案例一个订阅消息的消费者进程因为某次异常没有正常关闭订阅之后每次重启都新建一条订阅连接旧连接因为客户端没断、服务端timeout 0也不回收攒了一周后把连接数吃满。现象就是整个集群的服务陆续连不上。3.3 第三层日志与慢查询的交叉验证slowlog是排查读写超时类问题的关键。有时候连接是通的但某个命令执行了几百毫秒客户端配置的socketTimeout只有 200ms于是报读超时。表现出来也叫连接失败实质是慢命令拖垮了整条链路的响应时间。redis-cli slowlog get 10 redis-cli config set slowlog-log-slower-than 10000 # 记录超过10ms的命令常见的罪魁祸首是KEYS *、大 key 的HGETALL、集合的SMEMBERS、没有游标的SCAN全量遍历。这些命令单机测试时数据量小毫无问题一到生产环境数据涨起来就从很快变成卡死。提示KEYS *在生产环境是绝对禁忌任何需要用它的场景都应该换成SCAN游标遍历。我在评审代码时看到KEYS一律打回。把slowlog、latency monitor、客户端报错时间点三条时间线对齐往往能瞬间看出因果关系。这个动作比读十遍配置文件都有效。3.4 第四层抓一次真实的网络交互到这一步如果还没定位就得上抓包了。用tcpdump在应用机器上抓 TCP 层tcpdump -i eth0 -nn port 6379 -w redis.pcap # 抓够几十兆后停止用 Wireshark 打开分析重点看三种模式三次握手就没完成说明网络层或服务端监听有问题。握手完成发命令后没响应RST 或 FIN 出现中间设备掐连接典型的空闲连接被回收。响应很慢但最终有慢命令或网络延迟问题。抓包是最重的手段但也是最不会骗人的。所有前面基于日志的猜测在这一步都能被证实或推翻。4. 那些把我坑了三天的具体配置组合排查链路讲完了这一节专门讲我自己真实踩过的坑。这些不是理论推演是实实在在花掉时间的案例。4.1 场景一容器环境下的 bind 与网络命名空间容器化部署后Redis 跑在 Pod 里bind 127.0.0.1意味着只接受来自同一网络命名空间的连接。但同一 Pod 内通常没有客户端应用在另一个 Pod通过 Service 访问。这种情况下连接必然失败而你在 Pod 内exec进去redis-cli ping却是通的——因为那就是网络命名空间内部。正确做法是配置成bind 0.0.0.0靠 Service 和 NetworkPolicy 做访问控制。这个坑的迷惑性极强因为在容器里测是通的。4.2 场景二连接池 maxTotal 与并发线程数的错配配置应用连接池时很多人拍脑袋填maxTotal10而应用的业务线程池配了 200 个线程。当 200 个线程同时要访问 Redis只有 10 个连接可用其余全在borrowObject处排队等maxWaitMillis超时后抛异常。这个案例的特征是错误集中在业务高峰期低峰期完全正常且错误信息里常有Pool exhausted或Timeout waiting for idle object。合理的容量估算方式是所需连接数 ≈ 峰值 QPS × 平均单次操作耗时(秒) ÷ 冗余系数比如峰值 5000 QPS单次操作平均 2ms那理论上 10 个连接就能扛住。但实际要考虑网络抖动、慢命令、批量操作通常留 2 到 3 倍冗余。关键不是拍一个数字而是这个数字要能被计算和验证。4.3 场景三主从切换期间的连接风暴主从架构下发生故障切换时所有客户端的连接会被断开然后同时重连新主节点。如果客户端没有配置合理的重试退避会出现重连风暴新主瞬间被打满拒绝新连接于是切换后集体连不上。需要配置的项包括客户端重试次数与退避策略指数退避优于固定间隔哨兵/集群模式下的拓扑刷新间隔连接池的blockWhenExhausted行为这类问题在平时不复现只在故障演练或真实切换时爆发属于必须通过演练暴露的配置缺陷。我现在的习惯是任何生产环境的 Redis 上线前必须做一次主从切换演练专门观察客户端行为。4.4 场景四DNS 解析与连接地址写法的耦合这个坑比较偏但值得说。客户端连接地址如果写成域名容器环境里 DNS 解析结果可能随 Pod 重建而变化。如果客户端缓存了旧 IP或者 DNS 解析超时就会报连接失败。而用 IP 直连则完全没有这个问题。判断方法是看应用日志里有没有UnknownHostException、Name or service not known这类字样。如果有问题就在解析环节和 Redis 本身无关。5. 配置之外那几个容易被冤枉的真凶讲完配置也得说清楚另外几种情况否则容易形成什么问题都往配置上靠的思维定势。它们同样会伪装成连接失败但根因不在配置项里。5.1 中间设备的空闲连接回收前面提过这是timeout配置问题的一体两面。有时候你的timeout已经配得很合理了但云环境的内网网关回收时间比你还短或者某个安全策略会定期清理长连接。这种情况下客户端必须靠testOnBorrow或保活心跳来兜底。保活心跳是个值得单独说的话题。配置tcp-keepalive让系统层面定期发送探测包tcp-keepalive 60单位是秒。它能防止连接被判定为空闲但对应用层的连接被静默回收帮助有限因为中间设备可能只做 TCP 层探测而不管应用层状态。保活是辅助手段不是银弹。5.2 客户端版本与服务端协议的兼容性Redis 6 开始支持 RESP3 协议部分新客户端默认协商 RESP3。如果服务端是老版本或者中间有代理只认 RESP2就可能出现握手失败或响应解析异常。现象是连接建立后立刻断开或响应格式错误。排查方式是显式指定协议版本redis-cli -3 ping # 强制 RESP3 redis-cli -2 ping # 强制 RESP2如果其中一个能通、另一个不通方向就明确了。5.3 大 key 与阻塞式操作引发的连锁反应一个超大 key 的删除操作会阻塞主线程数秒。这期间所有命令排队客户端大量超时。从客户端视角看就是一片连接失败。但根因是数据设计问题不是配置。这类问题的排查要结合latency命令redis-cli --latency-history -i 1 redis-cli --bigkeys--bigkeys能扫描出各类型中最大的 key虽是采样但足够发现异常。我在新接手一个 Redis 实例时第一件事就是跑一遍--bigkeys和slowlog摸清数据健康度。5.4 系统层面的资源限制ulimit -n太小会导致打开文件过多错误vm.overcommit_memory未设置会在大内存场景下触发 fork 失败somaxconn太小会影响连接积压队列。这些严格说不算 Redis 配置但同样会引发连接异常而且报错信息往往很隐晦。ulimit -n 65535 sysctl vm.overcommit_memory1 sysctl net.core.somaxconn1024这几条几乎是 Redis 部署的标准前置动作。我在 Dockerfile 或部署脚本里都会固定加上避免每次换机器重新踩。6. 让人少走弯路的几条硬经验最后这部分是我从这些案例里提炼出的、能直接拿去用的判断准则。它们不解决某一个具体问题但能显著缩短定位时间。第一条永远先确认是连接问题还是命令问题。能建立连接但命令执行失败和连连接都建不起来是两条完全不同的路。前者看慢查询、内存、阻塞命令后者看端口、绑定、防火墙、连接池。混在一起查是最浪费时间的。第二条配置排查要对着环境看而不是对着文件看。同一个redis.conf本地跑、容器跑、跨机跑效果完全不同。bind、protected-mode、DNS 地址都是环境强相关的。我在确认配置问题前一定会在应用所在的那台机器上做连通性测试而不是在自己的开发机上。第三条任何偶发的问题都要往超时和连接池上先想。偶发意味着存在时间窗口而连接相关的配置问题几乎都和时间窗口有关——空闲回收、超时重连、淘汰触发。确定性失败反而好查偶发性失败才是配置问题的典型特征。第四条把关键配置项做成上线检查清单。我维护了一份自己的清单包含bind、protected-mode、timeout、maxmemory、maxmemory-policy、maxclients、tcp-keepalive七项外加客户端连接池的maxTotal、maxIdle、minIdle、maxWaitMillis、testOnBorrow五项。每次上线逐条勾。第五条监控要覆盖连接维度。大部分人的 Redis 监控只看 QPS、内存、CPU唯独不看连接数。而连接数恰恰是这类问题的第一手指标。至少要有connected_clients、blocked_clients、rejected_connections三个指标的曲线和告警。redis-cli info stats | grep -E rejected_connections|total_connections_received redis-cli info clients | grep -E connected_clients|blocked_clientsrejected_connections只要不为 0就说明曾经因为连接数达到上限而拒绝过连接这是一个非常明确的信号但很多人从来不看。第六条别急着怪 Redis。三天排查的最后往往不是 Redis 出问题而是我们在某一个配置组合下恰好触发了一个边界行为。Redis 本身很稳定不稳定的是我们的部署方式、网络路径和参数组合。把怀疑对象从Redis 有 Bug切换到我的配置在什么条件下会出问题排查效率会有质的变化。回头看我那三天的排查记录超过一半的时间花在了证明网络没问题上而真正有价值的时间是最后那几个小时——当我把应用侧的连接池参数和服务端的timeout放在一起对照问题的轮廓一下就清楚了。配置问题的本质从来不是某一项配错了而是几项配置之间、配置与运行环境之间形成了没人预料到的组合。看清这一点下次再遇到类似的莫名连接失败你至少知道该从哪个抽屉开始翻。
RELATED READING

延伸阅读

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