ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

配置双重人格:Kubernetes hostPort残留导致节点端口占用的排查实录

配置双重人格:Kubernetes hostPort残留导致节点端口占用的排查实录 先说两句背景别让标题里的“Ks”把人绕晕。Ks是我们内部对一个容器管理配置平台的简称底层跑的是Kubernetes日常工作负载的创建、变更、回收基本都在这个平台里完成。同事之间说“Ks配置”意思就是“通过平台页面看到的容器配置状态”。这一篇记录的是我某次排查一个诡异问题——配置页面里明明没有任何hostPort痕迹节点上的端口却被占用了而且这个现象删一次、来一次特别像一个有“双重人格”的配置在跟我玩捉迷藏。整个过程牵扯到Ks平台配置与Kubernetes底层Spec的不一致、CNI端口映射规则残留以及Deployment滚动更新时配置“复活”的机制。如果你也遇到过“明明删了配置行为却还在明明看不到字段它却在后端生效”这种问题这篇排查记录应该能给你一些线索。1. 事故现场配置清单里没有hostPort节点端口却被悄悄占领事情从一个周三下午的告警开始。监控平台弹出一条端口监听告警node-02这台宿主机上8080端口出现了非预期监听。按照平时的运维习惯我们第一时间登录Ks平台打开对应生产工作负载web-app-prod的配置页面把网络相关配置从头到尾看了两遍容器端口8080协议TCP,主机端口那一栏是空的。也就是说从Ks平台页面看这个工作负载根本没有配置hostPort。但告警不会凭空出现。我接着ssh登录node-02执行ss -lntp | grep 8080结果非常明确8080端口确实有进程在监听而且监听地址是0.0.0.0说明不是容器Network Namespace内部的业务监听而是宿主机网络命名空间上的端口映射。那一刻我的第一反应是hostPort一定被配置了只是Ks平台的界面上没有展示出来。这就是所谓“双重人格”的第一现场——一份配置存在两个独立副本Ks平台页面展示的是“没有hostPort”的人格而底层运行环境里生效的是“有hostPort”的人格。这种不一致短期内最直接的后果是端口被其他服务误占用时会发生冲突Pod反复重启长期看安全扫描、端口基线管理、服务调用关系梳理全部会被带偏。更要命的是这个问题具有“复现性”我第一次手动把底层的hostPort字段清掉端口释放了结果过了几天一次滚动更新之后8080端口又出现了。这不是偶发而是有机制在反复“复活”它。2. hostPort的“生命轨迹”从配置到行为它要穿过多少道门先说清楚hostPort这个东西在Kubernetes里到底是什么。它是Pod的spec.containers[].ports下面的一个可选字段作用是告诉kubelet把这个容器的某个端口直接映射到宿主机IP的指定端口上。注意它和NodePort完全不同NodePort是Service层面的端口映射走的是kube-proxy的转发规则hostPort是Pod层面的端口映射发生在容器被调度到节点之后由容器运行时和网络插件负责落地。一条hostPort配置真正生效链路是这样的配置提交到API Server后写入EtcdScheduler把Pod调度到某个节点kubelet watch到Pod后调用CRI容器运行时接口创建sandbox容器CRI再调用CNI插件配置Pod网络。如果你用的是containerd加bridge/portmap这类CNI插件组合hostPort映射最终会表现为portmap插件在宿主机iptables的NAT表里写入DNAT规则如果你用的是Docker作为运行时表现则更接近docker run -p hostPort:containerPort。理解了这条链路你就能明白“配置”和“行为”为什么会分家。因为在一条链路上配置信息会在多个地方留下痕迹Etcd里的Pod一份kubelet内存/检查点里一份容器运行时的配置文件里一份iptables规则里一份。任中一环有缓存、残留或者覆盖写入逻辑不彻底“配置没了但行为还在”或者“配置看不到但实际存在”就都可能出现。“神秘复现”则有另外两条典型路径。路径一Etcd里Pod的Spec本身就带着hostPort那么kubelet每次重建Pod都会把它应用一遍表现为“删了又回来”。路径二Deployment的Pod template里存有hostPort字段某次滚动更新时被重新应用哪怕你之前手动清理过运行中的Pod也没用。也就是说真正的问题不一定在运行时更可能藏在“配置源头”里。3. 实地取证从Ks配置到宿主机端口我按这个顺序抓“幽灵”这类问题不能靠猜得把整条链路每一层的证据都拿到手。下面是我的排查顺序每一层都能筛掉一批可能性。第一步先看Ks平台。我不仅在页面上看还通过平台提供的API把工作负载的详细配置导了出来确认页面展示确实是“hostPort为空”的状态。这一步的核心目的是确认平台视角的“人格A”长什么样拿到完整的界面证据。第二步直连底层Kubernetes看Etcd里存的实际Spec。执行kubectl get deployment web-app-prod -n production -o yaml重点查看Pod template里的ports部分。结果当场发现hostPort: 8080赫然写在spec.template.spec.containers[0].ports下面。这就是“双重人格”的第一现场Ks平台页面说没有Etcd里的Spec说有。第三步看运行中的Pod和宿主机端口占用。执行kubectl get pod -n production -l appweb-app-prod -o wide找到Pod所在节点再上节点执行ss -lntp | grep 8080和lsof -i :8080确认PID对应的进程落在哪个容器cgroup里。这一步能判断端口是谁在监听、监听在哪个网络命名空间。第四步深入容器运行时。由于节点用的是containerd我执行crictl ps -a查看所有容器再用crictl inspect看具体容器的端口绑定信息。这里重点观察有没有“已经退出但sandbox没清理”的僵尸容器或者有没有旧版本Pod遗留下来的容器实例。第五步检查网络层的规则残留。执行iptables -t nat -L CNI-HOSTPORT-DNAT -n -v看看portmap插件写入的DNAT规则。注意看规则的目标地址是不是已经失效的Pod IP如果是说明容器虽然删了但端口映射规则没有被同步回收。如果你用了ipvs模式的kube-proxy还要执行ipvsadm -L -n确认Service转发规则没有异常叠加。第六步查kubelet日志和盘上的检查点文件。执行journalctl -u kubelet --since 3 hours ago | grep -i hostport看kubelet在最近一次Pod创建/删除时对hostPort做了什么。某些kubelet实现会把端口分配记录写到检查点文件里路径通常在/var/lib/kubelet/下比如kubelet_internal_checkpoint里面的端口分配信息也是排查时的重要线索。这一套走下来证据链就完整了。我当时梳理出的时间线是这样的某次版本发布时Ks平台生成的新Deployment YAML是从“工作负载模板”渲染出来的模板里保留了历史上一版配置中的hostPort: 8080。这个字段进入Etcd后K8s层面一切正常生效但Ks平台的表单编辑器只渲染了它认识的字段hostPort这种不在白名单里的字段被原样保存却不在界面上展示于是“删不掉、看不见、但一直存在”的局面就形成了。4. 根因复盘配置“双重人格”是怎么炼成的找到了直接原因之后我更关心的是机制层面的根因否则修一次管一阵迟早还会再犯。复盘下来这个“双重人格”背后其实是三个因素叠加。第一个因素是平台表单与底层YAML的字段映射不对称。Ks平台这种管理平台为了兼容Kubernetes生态里各种不常见字段一般不会把用户提交的YAML字段从头到尾重新洗一遍而是采取“非破坏性保留”策略表单能识别的字段按表单逻辑处理表单识别不了的字段原样保留在底层Spec里。这个设计本身是合理的避免误删用户自定义内容。但它有一个副作用如果某个字段不在表单的白名单里用户就无法在页面上看到它更无法通过页面删除它。hostPort当时就属于这种情况。第二个因素是滚动更新机制放大了配置残留的影响。我手动执行kubectl edit deployment把hostPort字段从Pod template里删除后运行中的Pod被重建、端口确实释放了。但这份Deployment的修改只影响当前Etcd里的对象并没有修正Ks平台后台存的那份“工作负载模板”。下一次任何人通过平台触发滚动更新平台就会用模板重新渲染一份YAML把hostPort重新写进Deployment。这就解释了为什么端口会“神秘复现”——它其实一直在模板里等着只是之前没有被重新应用罢了。第三个因素是操作入口不统一给排查增加了干扰。团队里有人习惯直接在平台页面上改配置有人习惯用kubectl edit绕过平台直接操作。当两份配置人格不一致时你很难判断“当前到底是哪一份配置在真正生效”。如果所有变更都走同一个入口并且每次变更后有自动校验这种分裂状态本可以在第一时间被识破。5. 止血、修正与长期预防先讲紧急止血。如果你也遇到端口被hostPort残留占住的情况第一件事是确认它到底来自“Etcd中的字段”还是“iptables规则残留”。如果是前者执行kubectl edit deployment或kubectl patch把Pod template里的hostPort字段删除等滚动更新完成再用ss -lntp | grep 端口确认释放。如果是后者需要查看iptables NAT表里CNI-HOSTPORT相关链找到指向已删除Pod IP的无效规则并清掉在确认安全的前提下也可以重启节点上的kubelet或网络插件触发规则重建但生产环境这个操作要格外谨慎。然后是配置修正。光是删掉运行中的字段还不够必须修正平台侧的模板。我把Ks平台后台那条工作负载模板找出来把模板中历史遗留的hostPort字段彻底清理掉同时检查了同批次其他工作负载的模板确认没有类似字段。这一步做完才算真正断掉“复活”的根。最后是长期预防。我在这次复盘后给团队加了几个机制在这里一并分享给表单映射逻辑加“字段遗忘告警”当平台检测到Etcd中的工作负载Spec包含平台表单不认识的字段时自动打一条审计日志并提醒管理员避免字段在页面不可见的情况下悄悄生效。统一变更入口所有工作负载端口类配置变更必须走平台或GitOps流程禁止直接kubectl edit绕过平台操作如果确有紧急情况需要绕过事后必须回填平台配置确保两份配置人格一致。定期配置漂移扫描用脚本定时对比Ks平台展示的期望配置与Etcd中的实际Spec发现差异即触发告警。扫描频率不用太高每天一次足够。端口监听基线监控把宿主机上每个端口“应当由哪个工作负载监听”做成基线端口监听状态与基线不一致时告警这样下次有类似问题会在第一时间暴露而不是等安全扫描或用户反馈才发现。为了方便后来人排查我把“双重人格”的检查清单整理成了五个维度检查维度关键命令/路径重点观察什么Ks平台配置平台UI、平台API页面展示的hostPort状态Kubernetes Speckubectl get deploy -o yamlPod template中的ports字段容器运行时crictl ps -a/docker ps -a存活/退出的容器端口绑定网络规则iptables -t nat -L CNI-HOSTPORT-DNATDNAT规则是否指向存活Podkubelet状态journalctl -u kubelet、检查点文件端口分配与释放日志这五层都确认一致才能说这个问题真正翻篇了。6. 最后聊两句实在话这类问题遇到几次之后我最大的感触是在Kubernetes生态里“配置看不见但实际存在”这类怪现象绝大多数不是Kubernetes本身有bug而是上层平台在Spec的生成、展示、同步上不够严谨。越是方便的可视化界面越可能在“人性化展示”和“完整保真”之间丢掉一些不起眼的字段。hostPort只是其中一个例子像dnsPolicy、terminationGracePeriodSeconds、nodeSelector这些字段同样可能成为“双重人格”的藏身处。我的建议很简单不要盲目相信任何管理平台的页面展示涉及端口映射、调度约束、生命周期这类关键配置必须保留直接读底层Spec的能力。排查时把Ks平台配置和Etcd实际Spec放在一起对比往往一眼就能看出问题在哪。另外给团队里所有能操作Kubernetes的人立一条规矩改完配置顺手用kubectl diff或类似的对比工具看一眼期望状态和当前状态几秒钟的时间能省下后面一整天的排查功夫。如果你手里也有一份“怎么删都删不掉”的配置不妨先问自己一句它是真的还存在还是只是我看不见它。
返回列表