1. 三天排查路的起点:那些"看起来很正常"的报错
先把场景还原一下,因为这决定了后面所有排查方向。一个跑了小半年的服务,某天开始间歇性报连接异常,日志里大概率是这么几行:Cannot get Jedis connection、Unable to connect to Redis、Connection refused、Read timed out,或者更含糊的Pool exhausted。诡异的地方在于——重启服务就好,跑一段时间又犯,本地用客户端连上去一切正常,ping返回PONG,info也能看到数据,redis-cli敲命令毫无卡顿。
这就是最折磨人的一类问题:单点测试通过,集群环境失败;低频操作正常,高频操作崩溃。你打开监控,CPU、内存、QPS 曲线全都平滑得像教科书,偏偏业务侧就是连不上。
我先把结论摆在前面,省得你看到一半着急:在我经手的这类案例里,最终定位到的根因,十次里有六到七次跟"配置"直接相关,而不是什么 Redis 自身的玄学 Bug。剩下的三四次,一半是网络中间设备,一半是客户端连接池参数与业务并发量不匹配。真正 Redis 服务端代码级的问题,占比极低。
为什么配置问题这么难查?因为它有个非常要命的特征——它不会立刻报错,而是"某条件下才触发"。比如timeout配了 0 表示永不超时,看起来最安全,实际上会让空闲连接被中间设备悄悄掐断后,客户端还傻乎乎地拿去用;又比如maxmemory-policy配成noeviction,内存写满后写入命令直接返回错误,表现出来却像是"连接失败"。
所以第一步要做的不是去翻配置,而是把"连接失败"这四个字拆开。它至少包含四类完全不同的故障:
| 表象关键词 | 真实含义 | 大概率归属 |
|---|---|---|
| Connection refused | TCP 都没握上手 | 端口、绑定地址、防火墙、服务没起 |
| 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写具体的内网网卡 IP,protected-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 为例:
GenericObjectPoolConfig<Jedis> 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 "MyP@ss#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 2>&1 \ || 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,看似很大,但如果应用侧连接池配了maxTotal=200,开了 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 与并发线程数的错配
配置应用连接池时,很多人拍脑袋填maxTotal=10,而应用的业务线程池配了 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_memory=1 sysctl net.core.somaxconn=1024这几条几乎是 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_clients"rejected_connections只要不为 0,就说明曾经因为连接数达到上限而拒绝过连接,这是一个非常明确的信号,但很多人从来不看。
第六条,别急着怪 Redis。三天排查的最后,往往不是 Redis 出问题,而是我们在某一个配置组合下,恰好触发了一个边界行为。Redis 本身很稳定,不稳定的是我们的部署方式、网络路径和参数组合。把"怀疑对象"从"Redis 有 Bug"切换到"我的配置在什么条件下会出问题",排查效率会有质的变化。
回头看我那三天的排查记录,超过一半的时间花在了"证明网络没问题"上,而真正有价值的时间是最后那几个小时——当我把应用侧的连接池参数和服务端的timeout放在一起对照,问题的轮廓一下就清楚了。配置问题的本质,从来不是某一项配错了,而是几项配置之间、配置与运行环境之间,形成了没人预料到的组合。看清这一点,下次再遇到类似的"莫名连接失败",你至少知道该从哪个抽屉开始翻。