ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

K8S多副本下Sentinel 8719端口失联?自定义心跳IP+NodePort彻底解决

K8S多副本下Sentinel 8719端口失联?自定义心跳IP+NodePort彻底解决 先说个我上个月真实遇到的场景业务服务在K8S里做了多副本部署ReplicaSet 3副本Sentinel控制台却开始整片整片地显示“机器失联”实时监控拉不到数据限流规则下发也经常没反应。登录到控制台看在线列表机器IP后面跟的端口千奇百怪有的能通、有的死活连不上。当时第一反应是Sentinel配置没改对但翻了一堆文档都只说“默认端口8719”没人告诉你K8S里多副本部署时这个端口会变成什么样。这篇文章就把我踩过的坑、最终落地的方案、以及排查思路完整写出来给同样被K8S多副本环境下的Sentinel 8719端口通信折磨过的人一个可以直接抄的作业。1. 多副本下的8719通信问题是怎么发生的1.1 先搞懂Sentinel的通信模型Sentinel的客户端也就是你的业务应用做流量统计、限流熔断然后需要跟控制台交互。这个交互不是简单的“上报”就完了其实拆开有两条链路心跳上报链路客户端 → 控制台客户端每10秒左右向控制台的HTTP接口发送一个心跳包告诉控制台“我在这里我的IP是xxx端口是8719”。规则推送和实时监控链路控制台 → 客户端控制台需要反向连接客户端的8719端口才能实时拉取监控数据、推送限流规则。关键点在于第二条链路。很多人以为只要客户端能连上控制台就行实际上控制台也得能连回客户端。默认情况下Sentinel客户端会在启动时监听一个端口默认就是8719这个端口就是留给控制台反向连接的。有个特别容易被忽略的细节Sentinel的端口不是写死在8719的如果8719被占用它会在启动时自动尝试87191、87192一直往上递增找可用端口。也就是说客户端实际监听的端口可能是8719也可能是8720、8721……但这个变化是自动的你要是不去查日志根本不知道它到底用的哪个端口。1.2 为什么单机部署没问题K8S多副本就出事如果还是传统的单机部署一台机器上跑一个Sentinel客户端8719就绑定在这台机器的IP上控制台访问机器IP:8719一切正常。但到了K8S里局面完全变了第一个问题是IP不固定。Pod每次重建都会拿到一个新的Pod IP而且这个IP是Overlay网络Flannel、Calico之类分配的跟宿主机IP不在同一个局域网。具体来说在一个典型的Flannel VXLAN部署里Pod IP是10.244.x.x而宿主机IP是192.168.x.x这两个网段根本不互通。控制台如果部署在集群外的服务器上它根本不知道10.244.x.x是谁自然就连接失败了。第二个问题是端口冲突。多副本的情况下每个Pod里跑的Sentinel客户端都想监听8719但因为每个Pod有自己独立的网络命名空间端口不会真的冲突——监听互不干扰。问题是控制台那边看到的机器列表会有多个10.244.x.x:8719或者宿主机IP:xxxx一旦Pod重建导致IP变化控制台里的旧IP就成了永远连不上的“僵尸机器”。第三个问题是多副本的IP和端口组合可能完全一样。举个典型场景如果你的Service用的是NodePort模式多个副本从宿主机视角看可能就是同一个宿主机IP:NodePort。如果Service暴露了8719端口NodePort映射到了30080那两个调度在不同节点上的Pod上报给控制台的地址如果都是宿主机IP:30080控制台就会把它们当成同一台机器规则下发只对其中一台生效另一台永远不更新——这是多副本下最隐蔽的故障。1.3 一句话总结问题本质K8S多副本环境下Sentinel 8719端口通信的核心矛盾是控制台需要以“IP:Port”为唯一标识反向连接客户端但K8S天然让Pod的IP不稳定、端口映射不透明导致控制台拿到的地址要么连不通要么连错机器。整个解决方案的出发点都是让控制台拿到的IP:Port足够稳定、足够唯一、并且从网络上是可路由可达的。2. 三种主流解决思路对比2.1 思路一让Pod用宿主机网络最暴力的方法在Pod定义里加上hostNetwork: true让Pod直接共享宿主机的网络命名空间。这样Sentinel监听的8719端口就直接落在宿主机上上报的IP也是宿主机IP控制台访问就变成了访问一台普通服务器完全绕开了Pod网络和Overlay的复杂度。这个思路的好处很明显配置简单、控制台一定连得通只要宿主机IP能通、Pod IP也不会变了用的是宿主机IP。但坏处也很扎心一台宿主机上只能调度一个使用固定8719端口的Pod副本否则多个Pod都想监听同一个宿主机端口直接冲突。这就违背了K8S多副本弹性的初衷除非你手动给每个副本设置不同的Sentinel端口那又要解决“控制台怎么知道每个副本端口是多少”的问题。2.2 思路二自定义心跳IP NodePort这是我在生产环境最终采用的方案。核心原理是不让Sentinel上报它真实的Pod IP而是上报宿主机IP同时把Sentinel的8719端口通过NodePort映射到宿主机的一个固定端口上。控制台看到的机器地址是宿主机IP:NodePort这个地址在控制台侧是能路由到的宿主机IP在业务内网而且通过kube-proxy的转发流量能从宿主机端口转到Pod的8719。具体要解决的细节有几个后面实操部分我会逐个展开。这个方案能保住K8S多副本的灵活性——每个副本还是独立的Pod只是上报地址统一打到了宿主机上。前提是每个宿主机上不能调度多个副本否则多个Pod上报的还是同一个宿主机IP:NodePort又变成“同一台机器”了。所以必须结合反亲和性podAntiAffinity强制Pod分散到不同节点。2.3 思路三端口偏移逐个分配不同端口这个方法比较“原始”但在控制台和Pod网络本来就互通的情况下也能用。做法是给每个Pod副本配置不同的Sentinel端口比如副本1用8719、副本2用8720、副本3用8721然后用StatefulSet保证Pod的标识稳定控制台侧把这些端口手动加进去。适用场景很狭窄主要是控制台能直接访问Pod IP的情况比如控制台就跑在集群里而且你得有办法管理每个副本的端口号。端口偏移不能解决IP漂移问题Pod重建后IP变了还是要去控制台改地址运维成本很高。所以我一般不建议用但作为理解原理的补充还是值得说清楚。2.4 方案对比速查维度hostNetwork方案自定义心跳IPNodePort方案端口偏移方案配置复杂度很低中等中等控制台可达性高高依赖Pod网络是否可达多副本支持差同节点端口冲突好需配反亲和好但需管理端口清单IP漂移问题基本解决解决未解决生产环境推荐度不推荐除非单副本强烈推荐不推荐3. 实操落地自定义心跳IP NodePort完整配置3.1 第一步Downward API注入宿主机IP要让Sentinel上报宿主机IP需要把这个IP注入到Pod的环境变量里。K8S原生的Downward API就能做到不需要额外装啥东西apiVersion: apps/v1 kind: Deployment metadata: name: biz-server spec: replicas: 3 selector: matchLabels: app: biz-server template: metadata: labels: app: biz-server spec: containers: - name: biz-server image: registry.example.com/biz-server:latest env: - name: HOST_IP valueFrom: fieldRef: fieldPath: status.hostIP - name: POD_IP valueFrom: fieldRef: fieldPath: status.podIP ports: - containerPort: 8719 name: sentinel这里有个重要细节status.hostIP获取宿主机IP不需要任何权限FieldRef是内置能力所有K8S版本都支持。我见过有人用Downward API取不到IP十有八九是把fieldPath写错了注意是status.hostIP而不是spec.nodeNamenodeName拿到的只是节点名称不是IP还要额外查一次。3.2 第二步JVM参数覆盖Sentinel的IP配置Sentinel的Spring Cloud Alibaba适配层底层读写的是csp.sentinel.*这一组系统属性。有四个关键的配置项需要理解csp.sentinel.dashboard.server控制台地址格式是IP:port这个一般你早配了。csp.sentinel.api.port客户端监听端口默认8719。csp.sentinel.heartbeat.client.ip心跳上报的IP如果不设置默认取本机网卡IP这就是出问题的根源。csp.sentinel.heartbeat.interval.ms心跳间隔默认10秒一般不用动。我们要做的就是把csp.sentinel.heartbeat.client.ip显式设置为宿主机IP。Spring Cloud Alibaba里可以直接配spring.cloud.sentinel.transport.port和spring.cloud.sentinel.transport.dashboard但客户端IP的心跳配置用起来比较别扭我更推荐在启动命令里直接传JVM参数这样最直观、也最容易排查。Java进程的启动命令里加上java -Xms2g -Xmx2g \ -Dcsp.sentinel.api.port8719 \ -Dcsp.sentinel.heartbeat.client.ip${HOST_IP} \ -jar biz-server.jar注意${HOST_IP}这个变量是通过上一节的Downward API注入的。在K8S的Deployment里容器启动命令可以写成command: - sh - -c - | java -Xms2g -Xmx2g \ -Dcsp.sentinel.api.port8719 \ -Dcsp.sentinel.heartbeat.client.ip${HOST_IP} \ -jar /app/biz-server.jar有同事问过我Spring Cloud Alibaba不是有spring.cloud.sentinel.transport.client-ip这个配置项吗确实有底层也是落到csp.sentinel.heartbeat.client.ip但不同版本之间配置名有过变化为了稳定和通用性我统一用JVM参数至少不依赖框架版本。3.3 第三步NodePort映射控制台的访问路径宿主机IP搞定了还差端口。如果只设置心跳IP心跳包会打去宿主机IP:8719但宿主机上其实并没有服务监听8719流量根本到不了Pod里真正监听的8719端口。所以必须用Service把宿主机端口映射到Pod端口apiVersion: v1 kind: Service metadata: name: biz-server-sentinel spec: type: NodePort selector: app: biz-server ports: - port: 8719 targetPort: 8719 nodePort: 30719注意这里几个端口的语义portService的虚拟端口集群内部访问用。targetPortPod内容器监听的端口也就是Sentinel的api.port。nodePort宿主机上暴露的端口范围默认是30000-32767。这个端口是每个节点都会监听的外部访问任意节点IP:30719都能被转发到Pod的8719。如果你有多个服务的Sentinel要暴露每个服务的NodePort必须唯一否则会创建失败。建议把端口规划写进运维手册里比如第一个服务用30719、第二个用30720以此类推。这里还有个小提醒NodePort的取值范围可以改但默认的30000-32767已经够大部分场景用了不太建议为了好看去调整kube-apiserver的配置。3.4 第四步反亲和性保证端口不冲突这是很多人容易漏掉的一步。假设三个副本恰好被调度到同一台宿主机上它们的心跳IP都是同一个宿主机IPNodePort也是同一个30719那在控制台看来就是三台一模一样的机器限流规则不知道下发给哪台。这种情况必须提前规避。用podAntiAffinity确保同一服务的副本尽量分散到不同节点spec: affinity: podAntiAffinity: preferredDuringSchedulingIgnoredDuringExecution: - weight: 100 podAffinityTerm: labelSelector: matchLabels: app: biz-server topologyKey: kubernetes.io/hostname这里我用的是“preferred”而不是“required”。原因是如果集群节点数少于副本数required模式会导致后面的Pod一直Pending而preferred只是尽量分散实在没节点还是会调度到一起。生产环境我更倾向于preferred保证弹性可用性优先反正节点数足够的情况下它基本都能分散开。3.5 一个进阶细节多副本下的机器名区分解决了IP和端口还有一个容易分散注意力的点控制台的机器列表里多副本如果连应用名都没区分你会看到好几台同样名称的机器有时候分不清哪台对应哪个Pod。这个虽然不是通信问题但排查故障时候很影响效率。解决方案很简单把Spring Cloud Alibaba的spring.cloud.sentinel.transport.port配置配合-Dproject.name传参区分。实际上Sentinel控制台机器列表的名称取自应用的project.name属性可以把它设置为biz-server-${HOSTNAME}这样每台机器名称带上了Pod名一眼就能认出来-Dproject.namebiz-server-${HOSTNAME}HOSTNAME环境变量是K8S自动注入的不需要额外处理。4. 排查实录与常见问题速查4.1 问题一控制台显示机器失联但业务一切正常这个现象最典型。业务没挂限流照常生效本地规则只是控制台看不到实时监控。排查步骤我一般按这个顺序来首先确认客户端启动日志里实际监听的端口是多少。Sentinel启动时会打Sentinel token server started at port 8719之类的日志如果看到8719被占用自动递增到了8720而你的NodePort还映射在8719那NodePort转发过来的流量到不了8720一定失联。这个坑我踩过一次排查了半天最后发现是日志里那个端口号不对。然后从控制台所在机器手动telnet 宿主机IP NodePort测链路。能通说明网络没问题不通就查防火墙和安全组看是否放行了NodePort段一般云厂商需要放行30000-32767这个区间。最后看控制台自己的日志。控制台推送规则失败时会记录“downstream handler compute finished, but some rules may not be pushed to client”之类的日志它会告诉你具体哪个IP连不上。还有个隐蔽原因控制台版本跟客户端版本不匹配心跳报文格式解析失败表现为控制台能看到心跳但不更新这种只能统一升级版本。4.2 问题二规则下发只有部分副本生效如果规则下发后有的Pod生效了有的没生效优先检查机器列表里有几台机器。如果控制台里同一个服务的机器数量小于Pod副本数十有八九是IP:Port标识重复了控制台把多副本看成了一台机器。这个问题的解法就是上面说的反亲和性确保每个副本在独立的宿主机上。副作用是如果你的节点数不够某些副本还是会挤在一起我建议你在监控里加上节点数的告警至少心里有数。还有一个容易忽略的情况如果Pod被驱逐重建到了新节点新节点不一定有空闲NodePort其实NodePort是每个节点都监听同一个端口不会冲突但宿主机IP变了控制台里会出现机器下线又上线看起来像是丢了几台。这种不用慌把客户端重新注册的心跳包时间跟Pod创建时间对一下能对上就是正常重调度。4.3 问题三Pod重建后控制台出现僵尸实例控制台不会因为客户端不心跳就立即清理机器它有几轮心跳周期没收到才会判定失联然后标记下线但保留历史记录。多副本频繁重建时控制台列表会堆很多失联机器看起来很乱。说实话这个问题不太影响使用但运维看着难受。我的做法是定时检查控制台的机器列表接口把连续N个周期失联的机器调用控制台接口清理掉。具体接口可以看控制台源码的com.alibaba.csp.sentinel.dashboard.repository.machine相关代码生产环境我封装了一个每小时跑一次的清理任务实测效果挺好。4.4 常见问题速查表症状可能原因排查优先级控制台显示失联客户端实际端口和映射端口不一致高控制台显示失联宿主机防火墙/安全组未放行NodePort高控制台显示失联心跳IP还是Pod IP高规则推送到部分副本IP:Port重复控制台视为同一台机器中规则推送失败但无报错控制台和客户端版本不匹配中Pod重建后机器列表混乱心跳IP漂移或僵尸实例堆积低4.5 排查操作实录如果你用的是Spring Cloud Alibaba最好的排查入口是看客户端日志。在日志里搜“sentinel”关键词能看到心跳注册和规则拉取的明细。我一般这样操作kubectl logs -f pod-name -n namespace | grep -i sentinel重点关注这几行有没有Sentinel token server started at port xxx确认实际端口。有没有heartbeat to xxx确认心跳目的地和控制台地址一致。当你在控制台点击“实时监控”时客户端日志会不会出现monitor fetch请求如果连请求都没收到说明控制台压根没连过来问题在控制台到客户端的链路上。另外推荐一个很多人不知道的小工具思路直接用kubectl exec进Pod在Pod里执行curl http://localhost:8719/api看Sentinel本地的API是否正常。Sentinel在客户端暴露了一些内部HTTP接口比如/api/node能返回节点信息如果本地有响应说明Sentinel进程本身是健康的问题出在外部访问链路。4.6 最后再分享一个我个人的经验这套方案我在生产环境跑了半年多三个副本稳定在线、控制台实时监控和规则推送都正常。中间踩过最深的一个坑是我最初没有把csp.sentinel.api.port固定为8719想着让Sentinel自己递增就行结果有一个副本的端口自动变成了8720NodePort映射的却是8719导致那台机器永远失联。后来我固定端口、统一用环境变量注入心跳IP问题才彻底消失。所以核心经验就一句话在K8S环境里别依赖Sentinel的默认行为和自动探测所有端口、IP都要显式固定并且保证网络路径可控可预期。
返回列表