VIP 通了,客户端还是连不上:Cluster 把 Pod IP 直接给了你
telnet 通、PONG 也通,应用报的却是连不上 10.244 开头的地址。集群在重定向里把节点自报的 Pod IP 交了出去,入口做多少 NAT 都没用。
本地开发要连测试环境的缓存集群,这件事折腾了整整一个下午。
集群是三主三从,跑在容器里。我先给它建了一个 LoadBalancer,拿到 192.0.2.250,从开发机 telnet 过去,端口是通的。可应用一启动就报错,日志里那行地址让我愣了一下:
Connection refused: /10.244.186.57:6372
10.244 是集群的 Pod 网段,从开发机根本不可路由。端口明明通了,客户端却在连另一个地址。
一、VIP 通不代表客户端能用
先做最基础的排除:VIP 上到底有没有服务在听。
nc -vz 192.0.2.250 6371
# Connection to 192.0.2.250 6371 port [tcp/*] succeeded!
redis-cli -h 192.0.2.250 -p 6371 ping
# PONG
两条都通过。看起来"网络没问题",于是很自然地怀疑是应用配置写错了端口,或者是密码不对。查了半小时配置,一处没错。
真正有用的是让客户端自己说出它要去哪:
redis-cli -h 192.0.2.250 -p 6371 -c set foo bar
# -> Redirected to slot [12182] located at 10.244.186.57:6372
# Could not connect to Redis at 10.244.186.57:6372: Connection refused
-> Redirected to slot ... located at ... 这一行是关键。客户端没有连错,是集群主动告诉它去连另一个地址。而这个地址是 Pod IP。
二、CLUSTER SLOTS 返回的就是节点自报地址
redis-cli -h 192.0.2.250 -p 6371 cluster slots
输出里每个节点都是 10.244.x.x 加端口。原因是 Redis Cluster 的节点地址不是从连接来源推断的,而是每个节点在 nodes.conf 里记录的自己认为的地址,通过 gossip 协议在集群内互相通告。容器里如果没有显式指定对外地址,它就报自己的 Pod IP。
于是链路变成这样:
- 客户端连 VIP,VIP 转发到某个 Pod;
- Pod 回一句"你要的 key 在 10.244.186.57:6372";
- 客户端丢下 VIP,直接去连
10.244.186.57; - 开发机没有到 Pod 网段的路由,连接被拒。
整个过程 VIP 只参与了第一步。它不是代理,不做转发,只是把第一个连接送到了节点上。
三、客户端有两种反应,都不是"自动修好"
这里要分清两件事。客户端收到重定向后,一种反应是立刻改连新地址(MOVED / ASK),另一种是刷新整个拓扑缓存,把六个节点的地址重新拉一遍。两种反应遇到 Pod IP 都会失败,但失败的表现不一样,值得记住。
直连新地址失败,日志里是 Connection refused 或者 No route to host,报错地址就是 10.244.x.x,指向性很强。
拓扑刷新失败则隐蔽得多。客户端每隔一段时间主动发一次 CLUSTER SLOTS,拿到的全是不可达地址,于是把整个集群标记成不可用,报的是 Unable to connect to Redis cluster 或者 Cannot retrieve initial cluster partitions,日志里可能一个 10.244 都看不到。我们第一版应用报的就是后者,所以我一开始根本没往地址上想。
| 客户端行为 | 触发时机 | 失败时的日志特征 |
|---|---|---|
| 按 MOVED 直连 | 每次 key 落错槽 | Connection refused: /10.244.x.x:637x |
| 定期刷新拓扑 | 启动时、定时任务、连接断开重连 | Cannot retrieve initial cluster partitions |
| 刷新后重建连接池 | 拓扑版本变化 | 大量 Unable to connect,无具体地址 |
这也解释了一个现象:同一个应用,有人本地能跑、有人不能。能跑的那位大概是之前连过测试环境,本地 hosts 里还留着一条到 Pod 网段的静态路由,早就忘了。
四、为什么 SSH 隧道也救不了
我之前的思路是"隧道能转发端口,那就把 6371 到 6376 全转发出来"。这个方案在单机 Redis 上一直很好用,在 Cluster 上必然失败。
隧道是静态映射:-L 6371:10.96.0.11:6371 表示"本地 6371 永远转发到 10.96.0.11:6371"。但集群返回的地址是动态的 —— 六个节点、六个 Pod IP,而且 Pod 重建后 IP 会变。要让隧道覆盖,得给每个节点各开一条,并且每次 Pod 漂移后重新配一遍。这不是运维,这是手工维护一张随时过期的表。
同理,VIP 方案也只解决了入口。任何"在入口做 NAT"的思路都绕不过一个问题:重定向是客户端行为,客户端拿到什么地址就连什么地址。
五、改节点自报地址的代价
Redis 提供了 cluster-announce-ip / cluster-announce-port,可以让节点对外报另一个地址。理论上把六个节点都报成 VIP 加不同端口就行。
我没有这么做,原因有三个:
- 这是线上缓存,改
cluster-announce-*要重启节点或者至少触发一次配置重载,六节点滚动重启期间集群处于部分不可用; - 改完之后集群内部通信也会用这个地址,如果 VIP 只对开发网段开放,节点之间可能互相连不上,问题会从"开发连不上"升级成"集群脑裂";
- 这些配置会写进
nodes.conf,回退要改回来再重启一轮。
对一个只是"想让本地能调试"的需求来说,风险和收益完全不成比例。
六、最小暴露:给六个 Pod 各加一条 /32 路由
真正可行的是承认现实 —— 客户端就是要直连 Pod IP,那就让开发机具备到这几个 Pod IP 的可达性,范围精确到单个地址。
先确认转发链路成立,这一步不能跳:
# 在节点上确认转发开启、FORWARD 未被默认拒绝
sysctl net.ipv4.ip_forward
# net.ipv4.ip_forward = 1
iptables -S FORWARD | head -5
# -P FORWARD ACCEPT
# 确认业务命名空间没有 NetworkPolicy 拦 Pod 入站
kubectl -n app get networkpolicy
# No resources found in app namespace.
三条都满足,才轮到加路由。以其中一个节点为例:
# 开发机(需要管理员权限)
sudo ip route add 10.244.186.57/32 via 192.0.2.10
sudo ip route add 10.244.220.164/32 via 192.0.2.10
192.0.2.10 是一台能转发到 Pod 网段的节点。逐条加、逐条验:
for a in 10.244.186.57 10.244.220.164; do
nc -vz -w2 $a 6371 && echo "$a ok" || echo "$a fail"
done
我一开始图省事写了 10.244.0.0/16 via ...,被同事拦住了。整段放开等于把集群里所有 Pod 都对开发网段暴露,包括数据库、配置中心、控制面组件。而这些 Pod 大多没有认证。精确到 /32 只是多敲几行,暴露面差了几个数量级。
代价也要说清楚:Pod 重建后 IP 会变,路由会失效。所以这条路只适合"调试窗口内临时可用",脚本里要能一键生成当前 Pod IP 对应的路由命令:
kubectl -n app get pod -l app=redis -o jsonpath='{range .items[*]}{.status.podIP}{"\n"}{end}' \
| while read ip; do echo "sudo ip route add $ip/32 via 192.0.2.10"; done
七、四种方案摆在一起比
| 方案 | 能否解决重定向 | 代价 | 适用场景 |
|---|---|---|---|
| LoadBalancer VIP | 不能,只解决入口 | 低 | 单机 Redis、哨兵模式 |
| SSH 隧道 | 不能,无法覆盖动态地址 | 中,需逐节点维护 | 固定地址的单点服务 |
改 cluster-announce-ip |
能 | 高,动线上集群 | 长期需要外部接入的正式环境 |
/32 路由到节点 |
能 | 低,但 Pod 重建后失效 | 本地调试窗口 |
| 本地起三主三从 | 能 | 中,占内存 | 需要稳定复现集群行为 |
最后我们实际用的是组合:日常调试用本地容器起一套三主三从(端口 6371–6376,和测试环境保持一致),需要连测试环境数据时用 /32 路由临时开十分钟。VIP 保留着,因为单机实例和哨兵模式还在用,只是文档里明确写了"Cluster 不要连 VIP"。
小结
- Cluster 客户端拿到的节点地址来自
nodes.conf的自报值,容器里默认就是 Pod IP。 - VIP 和隧道都只影响入口,重定向发生在客户端,入口怎么改都没用。
- 判断方法就一条:
redis-cli -c看Redirected to slot ... located at后面那个地址。 - 改
cluster-announce-ip能治本,但要动线上集群、会写进nodes.conf,调试需求不值得。 - 临时方案用
/32路由,别开整段 Pod 网段;Pod 重建后路由失效,脚本要能一键重算。 - 长期方案是本地起一套同端口的集群,行为可复现,也不碰线上。
正在加载评论…