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。

于是链路变成这样:

  1. 客户端连 VIP,VIP 转发到某个 Pod;
  2. Pod 回一句"你要的 key 在 10.244.186.57:6372";
  3. 客户端丢下 VIP,直接去连 10.244.186.57;
  4. 开发机没有到 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 重建后路由失效,脚本要能一键重算。
  • 长期方案是本地起一套同端口的集群,行为可复现,也不碰线上。
查看「数据库与缓存」全部文章 →

同分类文章

查看 0 评论 0

评论

正在加载评论…

留言提交后自动审核:正常内容立即显示,含敏感信息的会转人工复核。