说真的,Redis连接失败这类问题,大多数时候半小时就能定位:要么网络不通,要么密码错了,要么服务根本没起来。但我这次碰到的这个案例,前前后后折腾了整整三天,最后发现跟网络、密码、进程统统没关系,纯粹是配置文件的锅——准确点说,是配置文件里一个很不起眼的include指令,把我已经检查过、并且"自认为没问题"的配置给悄悄覆盖了。如果你也有过"Redis明明在跑,配置里看着都正常,但客户端就是连不上"的经历,建议把这篇看完,大概率能帮你省下三次熬夜排查的时间。
先交代一下当时的场景。线上有两台服务器,一台跑应用,一台跑 Redis 6.2。某个下午应用开始刷连接异常,日志里反复出现Connection refused。我跑到 Redis 那台机器上看,ps里进程活得好好的,但默认端口 6379 的redis-cli ping却也连不上;后来无意中试出 6380 端口能通,我还以为那是个历史遗留的老实例。这个"我以为",让后面的排查一直走偏。这篇文章会把三天排查的完整路线、踩坑过程、真正根因,以及最后沉淀出的排查清单都写出来,适合正在被 Redis 连接问题折磨的运维、后端开发,以及所有和 Redis 配置打过交道的人。
1. 故障现场:进程活着、默认端口不通、应用持续告警
1.1 最初的报错长什么样
业务侧最先暴露问题的,是应用日志里疯狂刷JedisConnectionException: Could not get a connection from the pool,Caused by 是java.net.ConnectException: Connection refused。这个报错太常见了,常见到第一反应就是"Redis 挂了或者网络断了"。
我当时的处理路径很常规:先去看 Redis 进程,发现redis-server还活着;再本地测redis-cli ping,因为没带-p参数,默认走 6379,结果是Could not connect to Redis at 127.0.0.1:6379: Connection refused。这就有点诡异了,因为进程在,端口却不通。更关键的是,我后来用redis-cli -p 6380 ping居然返回了PONG。换谁第一反应都是:6380 可能是另一个旧实例,跟这次故障没关系,先查网络。
这里有个很重要的教训:"进程在"只能说明程序还跑着,不能说明它跑在你以为的端口上。如果你一上来就默认 6379 是 Redis 的"本命端口",后面很容易带着预设答案去排查,而忽略实际监听信息。
1.2 本机与外网表现不一致带来的误导
紧接着我做了一组对照测试:
- Redis 本机执行
redis-cli -p 6380 ping:返回 PONG - Redis 本机执行
redis-cli -p 6379 ping:Connection refused - 应用服务器执行
redis-cli -h <RedisIP> -p 6379 ping:Connection refused - 应用服务器
ping <RedisIP>:正常 - 应用服务器
telnet <RedisIP> 6379:不通
这套结果组合在一起,最容易得出的结论就是"网络有问题"或者"防火墙挡住了",因为本机某个端口通、外部不通,很符合防火墙拦截的表现。但问题在于:本机真正通的是 6380,不是 6379。我在那一刻忽略了"最该连接的端口其实从一开始就不存在"这个可能性,而是把注意力全部投向了网络层。
如果你现在也遇到类似的组合,我建议先停下来问一个问题:我准备连的那个端口,Redis 到底有没有在监听?这一个问题能省掉后面一大半的弯路。
2. 三天排查路线:从网络到端口再到配置的逐层排雷
2.1 第一天:网络层所有指标全部正常,问题却依然存在
第一天的排查重点全在网络。我先在应用服务器上pingRedis 服务器,通;再用traceroute看链路,正常;接着检查 Redis 服务器上的防火墙,iptables -L -n和firewall-cmd --list-all都看了,没有针对 6379 的拦截规则;云平台的安全组也确认放行了 6379。也就是说,从网络策略层面看,6379 是完全开放的。
为了让"网络有问题"这个假设尽量成立,我甚至用nc -vz <RedisIP> 6379从应用服务器探测端口,结果也是失败。当时脑子里的想法已经飘到"是不是内核参数问题""是不是云平台底层隔离"这种方向上去了,甚至把/etc/sysctl.conf里的net.ipv4.tcp_tw_recycle都翻出来看了一眼。
下班前我做了最后一件事:把 Redis 重启了一次。重启之后 6379 还是连不上,这让我更加确信问题不在 Redis 本身,因为"重启都没用"嘛,多数人会自然地把锅甩给环境。但这个判断其实是错的,重启对"配置文件里端口被覆盖"这种问题毫无意义,它加载的还是同一份配置。
2.2 第二天:端口监听信息里的异常,被我当成"历史遗留"忽略
第二天开始前,我又重新理了一遍思路。既然进程活着、本机能通 6380、外部连不上 6379,那问题一定出在监听端口上。于是我认真看了一下端口监听情况:
ss -lntp | grep 6379 # 没有任何输出 ss -lntp | grep 6380 # 有 redis-server 在监听逻辑上到这里已经很清楚了:Redis 进程实际监听的是 6380,而不是 6379。但我当时的内心活动是:6379 是 Redis 默认端口,配置里怎么会错呢?肯定是历史机器上留下过一个老实例占着 6380,而这个新装 Redis 又是按 6379 来用的,所以问题一定出在网络。
为了验证"配置里确实是 6379",我打开了/etc/redis/redis.conf,执行grep "port",看到的确实是port 6379。这下更坐实了我的判断:配置文件明明写的 6379,肯定没问题,那 6380 那个进程指定是别的。我还特意systemctl status redis看了一眼启动命令,ExecStart里加载的配置文件确实是/etc/redis/redis.conf,没毛病。
更让我困惑的是,我做了个对比实验:在另一台全新的云主机上装了一个临时 Redis,应用立刻就能连上。这说明应用侧和网络侧都没问题,真正有问题的还是 Redis 这台机器本身。于是第二天的结论变成了"这台机器上应该有什么系统级配置把 6379 屏蔽了",我又花了很多时间折腾 SELinux、系统防火墙、内核参数,全部无果。
现在回头看,第二天最大的错误不是没发现 6380,而是发现 6380 之后没有去问"这个 6380 到底是不是业务使用的那个 Redis 实例",而是想当然地把它归类为了"无关的历史进程"。一个"看起来合理"的预设,足以让人在错误方向上多走一天。
2.3 第三天:把所有"看起来对"的检查质疑一遍,才看到真相
第三天早上,我强迫自己回到最基础的问题上:到底怎么判断 Redis 实例的真实配置?答案是:别猜,别读文件猜,直接问运行中的实例。
我执行了这几条命令:
redis-cli -p 6380 CONFIG GET port # 返回:port 6380 redis-cli -p 6380 INFO server | grep config_file # 返回:config_file:/etc/redis/redis.conf看到port 6380的那一刻,说实话我是有点懵的。因为config_file明明指向/etc/redis/redis.conf,而这个文件里写的明明是port 6379。同一个文件的解析结果,为什么和文件内容对不上?
于是我做了至今都觉得最该早一点做的动作:递归搜索整个 Redis 配置目录。
grep -rn "port" /etc/redis/结果在/etc/redis/conf.d/下面发现了一个叫db_port.conf的文件,里面写着一行:
port 6380再回头看/etc/redis/redis.conf的最后几行,发现这样一行配置:
include /etc/redis/conf.d/*.conf真相终于浮出水面:主配置里确实写了port 6379,但这行写在 include 语句的前面;include 引入的子配置文件里写了port 6380,而 Redis 解析配置文件的规则是顺序执行、后者覆盖前者。所以最终生效的端口,不是主配置里"第一眼看上去"的 6379,而是被 include 文件覆盖后的 6380。应用一直连 6379,当然拒绝。
这里还得提一个细节:为什么用redis-cli不带-p在 Redis 本机也连不上?因为不带-p默认就是 6379,而实例实际在 6380,所以本机默认端口连接一样失败。当时我们的所有客户端、连接池、管理工具,全都默认走 6379,于是一锅端地挂了。
3. 真凶落网:include 把"看着正确的配置"覆盖了
3.1 一个配置片段讲清楚"为什么主配置明明是6379,实际却是6380"
我把核心配置片段简化出来,大概是这个结构:
# /etc/redis/redis.conf port 6379 ... include /etc/redis/conf.d/*.conf而conf.d/db_port.conf里只有一行:
port 6380Redis 配置文件解析和很多人的直觉不一样:它不是"主配置优先,子配置补充",而是从上到下逐行解析,同名配置项以最后一次出现的值为准。由于include写在文件末尾,include 文件里的port 6380就会把文件前面写的port 6379覆盖掉。反过来,如果 include 写在配置文件的顶部,port 6379在后面再次出现,最终就以 6379 为准。
这个机制本身不难,难的是它在真实环境里藏得很深。因为你打开主配置文件,grep port看到的就是 6379,谁会想到在文件末尾还有一个"隐藏的覆盖者"?
3.2 include 的顺序语义,很多人一开始就理解反了
我给团队讲这个案例的时候,发现 90% 的人第一反应都是"include 引入的配置怎么会盖掉主配置?它不应该只是补充吗?" 这里有几个点需要说透:
- Redis 的配置解析不是"合并"语义,而是"顺序执行"语义。像读脚本一样,读到哪一行,那一行的配置就生效;后面再读到同一个参数,就覆盖前面的值。
- include 指令可以出现在配置文件任意位置。写在前面,主配置后面的内容可以覆盖它;写在后面,它就可以覆盖主配置前面出现过的内容。
- include 文件里也可以再 include 其他文件,嵌套时同样遵循"后者覆盖前者"。
CONFIG REWRITE这个命令不会改写 include 行本身。这就会带来一个更隐蔽的坑:如果你在运行期用CONFIG SET修改了某个被 include 覆盖的参数,然后执行CONFIG REWRITE,新的值会被写回主配置文件,但 include 行还在原地;下次重启,include 里的旧值会再次覆盖你刚写好的新值。表现出来就是"明明改对了,一重启又变回去",非常像灵异事件。
提示:判断一个配置项到底生不生效,永远以运行中实例的
CONFIG GET输出为准,不要只信静态配置文件里的内容。
3.3 为什么这个问题能让人查三天:三个干扰项复盘
事后我复盘,觉得这个 Case 之所以拖了三天,主要是三个干扰项叠加:
第一,配置检查方式太浅。我查配置时只grep "port"主配置文件,看到port 6379就默认"配置正确"。正确的做法是递归检查整个配置目录,并且追踪 include 链路,看最后生效的值。
第二,默认端口的心理锚定。所有人谈到 Redis 就默认 6379,以至于看到 6380 时第一反应不是"服务跑错端口了",而是"这是别的实例"。这个心理锚定会让人主动忽略真实监听信息。
第三,重启无果带来的误导。重启 Redis 后发现问题还在,我当时就认为"不是配置的问题",因为配置改动一般重启后就会生效。但"重启了也不行"恰恰证明"配置文件和实际生效值不一致"的状态是持续存在的,这不是排除配置的理由,反而更应该去查运行期真实配置。
4. 盘点Redis配置界那些"看着对其实错"的经典陷阱
既然这次栽在配置上,我干脆把 Redis 配置相关的常见坑一起整理出来,里面有好几个都是平时看配置时"眼睛会骗人"的典型场景。
4.1 include 顺序与 CONFIG REWRITE 的组合坑
include 的顺序问题我在上一节已经讲了,这里再补充一个它和 CONFIG REWRITE 组合后的实际场景。
很多团队会用 include 来做环境差异化配置,比如基础模板redis.conf统一维护,各环境的覆盖配置放到conf.d/下面。这个思路本身没问题,但一旦有人用CONFIG SET临时调整参数,比如调大maxmemory,再执行CONFIG REWRITE,Redis 会把新值写回主配置文件。可是如果这个参数原本是在 include 文件里配置的,主配置里的新值只会在"include 之前的段落"中生效,include 文件里的旧值在解析时又会覆盖它。于是出现一个很反直觉的结果:你 CONFIG SET 之后查CONFIG GET,当前实例是对的;一重启,又变回旧值。很多团队遇到这种情况会以为是"配置没保存",实际上是 include 的覆盖顺序在作怪。
4.2 bind、protected-mode 和 requirepass 的三角关系
这次问题出在端口覆盖上,但 Redis 外部连接失败还有一个更常见的三角配置组合:bind、protected-mode和requirepass。我把典型场景整理成了表格,方便对照排查:
| 配置组合 | 外部连接表现 | 本机表现 |
|---|---|---|
bind 127.0.0.1 | 外部 Connection refused,端口像没开 | 本机 redis-cli 正常 |
protected-mode yes+ 未设置密码 + 未 bind 非回环地址 | 外部被拒绝,日志出现 "DENIED Redis is running in protected mode" | 本机正常 |
requirepass设置了密码,但客户端没带或密码不对 | 报 NOAUTH Authentication required | 本机也要认证 |
密码中包含@、:等特殊字符,客户端连接串未转义 | 认证失败或 URL 解析异常 | 本机直接-a可能正常 |
protected-mode这个尤其容易踩。很多发行版默认安装的 Redis 就是protected-mode yes且没有密码,如果没显式配置bind,外部客户端连上来时 Redis 会直接拒绝,但本机用redis-cli又一切正常。这个表现和网络不通很像,非常容易让人误判成防火墙问题。排查时建议直接用redis-cli CONFIG GET protected-mode和CONFIG GET bind确认,而不是反复检查防火墙。
4.3 启动方式、配置文件路径与多实例的混淆
还有一个很常见的"配置没生效"原因:启动时加载的根本不是你以为的那份配置文件。
- 使用 systemd 管理时,
systemctl cat redis可以看到完整的ExecStart行,确认到底加载了哪个配置文件。 - 如果手动执行
redis-server不加任何参数,Redis 会使用内置默认配置,跟/etc/redis/redis.conf完全没关系。这时候你改配置文件改到天亮也没用。 - 多实例部署时,通常每个实例有独立的配置文件和独立端口,但如果多个实例共用了一个带 include 的模板配置,端口参数很容易互相覆盖。我这次就是单个实例情况下被 include 覆盖,多实例场景只会更乱。
建议所有人在排查时先执行这条命令,确认当前的config_file到底是谁:
redis-cli -p <端口> INFO server | grep config_file如果输出为空,说明实例启动时根本没有加载配置文件,这时候应该去查启动命令和 systemd 服务文件,而不是继续改配置。
4.4 timeout、maxclients 这类运行期参数的"伪连接失败"
除了静态配置和启动方式,还有一类参数不会让你"一开始连不上",但会在运行一段时间后制造出"连接失败"的假象,典型的是timeout、maxclients和tcp-keepalive。
timeout:如果设置成 60,空闲连接超过 60 秒会被服务端主动断开。客户端连接池如果没有自动重连机制,就会出现"用着用着一会儿失败一会儿好"的现象。maxclients:最大客户端连接数,达到上限后新连接会直接报ERR max number of clients reached,这个报错字面意思很清楚,但容易被误认为连接被防火墙阻断。tcp-keepalive:默认 300 秒,调整 TCP 探活包频率。在跨机房、跨网络环境下,这个值太小会导致连接被中间网络设备静默断开。
这几个参数建议在初始排查时也顺手看一眼,特别是报错信息里没有明确写"refused"或"timeout"的情况。
5. 从这次三天教训里沉淀出的Redis连接排查清单
5.1 一条命令拿到运行期真相
现在遇到 Redis 连接问题,我的第一步已经不再关心静态配置文件里写了什么,而是直接对运行中的实例"问话"。
# 先看实际监听端口,确认端口到底是谁在听 ss -lntp | grep redis # 连上实例后,直接查关键配置项 redis-cli -p <实际端口> CONFIG GET port redis-cli -p <实际端口> CONFIG GET bind redis-cli -p <实际端口> CONFIG GET protected-mode redis-cli -p <实际端口> CONFIG GET requirepass # 确认实例加载的配置文件路径 redis-cli -p <实际端口> INFO server | grep config_file把CONFIG GET的输出和应用侧实际连接的 host、port、password 逐一比对,基本能覆盖 80% 的"莫名连接失败"。特别是当config_file显示的是一个明确路径时,再用grep -rn去递归查整个配置目录,把 include 链路翻到底。
5.2 客户端连接串与工具侧的常见遗漏
服务端排查得差不多之后,客户端侧的检查清单也不能漏。我在用 Redis Desktop Manager、Another Redis Desktop Manager 这类图形工具排查时,经常会发现几个固定的坑:
- 连接时填的端口是 6379,但服务端实际监听端口是 6380,这类问题用图形工具时最容易发现,因为界面上端口一目了然。
- 连接串格式写错,尤其以
redis://:password@host:port/db这种形式连接时,如果密码里含有@、:、/等特殊字符,必须做 URL 编码,否则密码会被截断,表现为"密码明明对但认证失败"。 - 客户端连接池的
maxTotal、maxWait设置太小,在瞬时流量下会先于 Redis 端报"连接失败",这类错误往往是池资源耗尽,而不是 Redis 不可用。
我把三类最常见的失败现象和对应的排查方向列成了一张自查表:
| 报错类型 | 典型提示 | 优先排查方向 |
|---|---|---|
| 连接被拒绝 | Connection refused | 端口是否监听、bind 是否限制、防火墙 |
| 连接超时 | connect timed out | 网络不通、安全组未放行、路由问题 |
| 认证失败 | NOAUTH / invalid password | requirepass 与客户端密码是否一致、特殊字符转义 |
5.3 从配置审计角度避免下一次"查三天"
最后说点预防性的经验。这次问题本质上是"配置文件与现实不一致"没有被及时发现,所以团队的配置管理和审计机制需要补位。
我现在的建议是:
- 所有 Redis 实例的配置文件纳入版本管理,
conf.d里的覆盖文件也要一起管,不能只管主配置。 - 配置下发后,在 launch 脚本里增加一次"期望值对账":比如期望端口是 6379,就自动执行
redis-cli CONFIG GET port,不一致直接告警。 - 定期巡检时保存
CONFIG GET *的快照作为基线,变更后做 diff,能很快发现"某个参数被谁改了"。 - 代码审查时,凡是涉及 Redis 配置的改动,都要检查 include 的放置位置和覆盖顺序,避免"看起来没错"的配置混进去。
这次三天排查给我个人最大的改变,就是不再相信"肉眼看到一个配置文件就下结论"。所有"感觉没问题"的检查项,都必须有运行期证据支撑。Redis 配置文件的 include 顺序,听起来是个非常基础的知识点,但在生产环境里,基础知识点犯起错来,代价一点不比架构缺陷小。后来我给团队留了一条规矩:排查 Redis 连接问题,第一件事永远是CONFIG GET和ss -lntp对账,而不是先讨论网络。希望这篇复盘也能让你少走一次弯路。