
把监控服务的入口从 NLB 迁到 ALB表面上只是改一条 Kubernetes Service 的类型或加一段 Ingress 配置实际上却牵扯到健康检查、安全组、DNS 切换、证书管理和长连接收敛。我这次迁的是 EKS 集群里的一套监控全家桶包含 Grafana、Prometheus 和 Alertmanager目标一开始就定成零停机迁移——切换过程不出现 5xx、Grafana 面板不闪断、用户无感知。最终靠双轨并行加 Route53 权重递进完成中间踩了几个很典型的坑下面把完整过程和原理一次讲清楚给准备做同类迁移的团队当参考。1. 为什么迁移比换个入口复杂NLB 的 L4 转发留了多少历史包袱1.1 NLB 和 ALB 的差异不只是四层和七层AWS 的 NLB 工作在四层只关心 IP、端口和 TCP/UDP 协议本身。它把一个客户端连接原封不动地转到后端节点不解析 HTTP 头部不理解 Host、Path、Cookie 这些东西。ALB 工作在七层能读懂 HTTP 协议可以按域名、URL 路径、请求头甚至查询参数做路由也天然支持 TLS 终结、WebSocket、HTTP/2 这些特性。这两者在转发能力上的差距平时可能不觉得一旦你管理的是多个监控组件差异会被急速放大。监控服务通常不是一个单体而是 Grafana、Prometheus、Alertmanager、Loki 等一系列组件。如果每个组件都用一个type: LoadBalancer的 Service 暴露那就要为 Grafana 开一个 NLB、为 Prometheus 开一个 NLB、为 Alertmanager 再开一个 NLB。每多一个 NLB 就意味着多一组监听器、多一组目标组、多一份安全组规则和 DNS 记录。更麻烦的是NLB 做不了基于路径的路由。同一个域名下你没法让 NLB 把/grafana分给 Grafana、把/prometheus分给 Prometheus。做监控入口的同学应该都有印象最后只能每个服务一个域名、一个 NLBDNS 记录一多排障时连这个流量到底走到哪了都要先翻半天记录。1.2 监控场景对证书和可观测性的要求NLB 满足不了NLB 在 TLS 层面基本是透传或者把证书卸载这件事完全交给后端。这意味着证书的管理分散在每个服务里Grafana 自己要挂证书Prometheus 自己要挂证书升级证书时得逐个服务改非常容易漏。ALB 可以把 ACM 证书统一挂在监听器层后端只跑 HTTP证书的签发、轮换都集中在一处。还有一个监控团队内部很在意的点NLB 的 CloudWatch 指标只有流量层面的东西比如 ActiveFlowCount、ProcessedBytes。你很难从 NLB 的指标里看到某个接口的 4xx、5xx 比例或者端到端请求延迟。ALB 本身就暴露 RequestCount、TargetResponseTime、HTTPCode_Target_5XX_Count 这些七层指标而且可以开 Access Log 到 S3后面想做逐请求分析也方便。对做监控的人来说自己的入口连请求成功率都看不到这说不过去。所以这次迁移的核心诉求很清楚把多个监控组件的七层路由能力、证书统一管理能力、以及请求级可观测性全部收到 ALB 这一层。2. 迁移前三类流量盘点哪些能断几秒哪些一秒都不能断零停机迁移不是把入口换掉就结束真正关键的是先想清楚业务上有哪几类流量在走这个入口每一类对中断的容忍度是什么。我这里分了三条线实际处理方式完全不同。2.1 第一类用户发起的普通 HTTP 请求Grafana 打开面板、切换数据源、执行 PromQL 查询、Alertmanager 创建静默规则这些都属于短连接请求。每次请求相互独立中间没有跨请求状态一个请求断了客户端重试一次就能恢复。这类流量是必须做到无感切换的主体也是最容易实现的因为短连接天然适应 DNS 切换新请求会按照新的 DNS 解析结果走到 ALB旧连接已经在 NLB 上处理完就结束了。只要 ALB 侧的 Ingress、后端 Service、Pod 都健康这类流量切换几乎没风险。真正需要注意的坑在别的地方客户端的长连接池。很多 HTTP 客户端会把 TCP 连接复用起来也就是 keep-alive。如果某个浏览器或者内部客户端在 DNS 切换前就和 NLB 建立了一条 keep-alive 连接切换后它不会主动断开而是在这条旧连接上继续发起请求直到这条连接超时或被服务端关闭。这意味着 DNS 切换完成后NLB 上仍会残留一段时间的流量。这不是故障但你要知道这是正常现象预留观察窗口。2.2 第二类WebSocket 和长轮询连接监控系统里几乎都有实时视图比如 Grafana Live、日志实时 tail、Prometheus 页面里某些自动刷新接口。Grafana 的 Live 功能底层就是 WebSocket。这类连接的生命周期很长可能持续几十分钟甚至几小时。DNS 切换对已经建立的 WebSocket 没有任何作用客户端不会因为 DNS 变了就主动重连。所以在切换前一定要想清楚这些长连接允许断吗断掉之后客户端能自动恢复吗实际经验是绝大多数监控前端的 WebSocket 都实现了自动重连断一次之后几秒内会自动连到新的地址。真正要做的是在迁移前检查前端的重连机制而不是幻想切换时所有长连接都无缝平滑。2.3 第三类有状态组件和内部采集链路监控服务是有状态的。Prometheus 把 WAL 和 TSDB 数据写在 PVC 上Grafana 的 dashboard、用户、数据源配置存在数据库里Alertmanager 的静默规则也依赖持久化存储。这次迁移没有动任何底层存储也没有重启有状态 Pod只是改了入口链路所以我并不担心数据丢失。真正要别的是迁移过程中会不会因为某些误操作触发 Deployment 滚动更新导致 Pod 重建。比如你在给新 Service 打标签的时候误改了 selector把监控 Pod 的标签覆盖掉可能直接引发后端 Pod 被重建。迁移期间有一条铁律只动入口相关资源不碰业务工作负载本身。在动手之前我还做了两件事。一是把当前的 DNS TTL 调低到 60 秒提前一天生效。二是用脚本记录了一个迁移前的基线包括 Grafana/api/health的响应时间、几个核心查询接口的状态码分布。没有基线你就没法判断切换后到底有没有变差。3. 双轨并行落地同一套 Pod 背后同时挂 NLB 与 ALB3.1 架构设计双入口单后端零停机迁移的核心做法不是切换而是并行。我让 ALB 和 NLB 在同一个时间段内同时存在两个入口都指向同一组监控 Pod。客户端继续通过 NLB 访问测试时通过 ALB 访问等 ALB 完全验证通过后再逐步把真实流量从 NLB 挪到 ALB。这个方案在 Kubernetes 里的实现非常自然。老的type: LoadBalancerService 走的是集群自带的 AWS 云控制器生成的负载均衡器是 NLB。新方案只需要新建一个type: ClusterIP的 Serviceselector 指向同一组 Pod然后写一条 Ingress 让 ALB 注册这个 ClusterIP Service 的端点。两条入口链路互不干扰但后端的 Pod 集合完全一致。这里有一个容易绕晕的细节ALB 使用target-type: ip时AWS Load Balancer Controller 会读取 ClusterIP Service 背后的 Endpoints把每个 Pod 的 IP 直接注册进 ALB 的目标组。所以你不需要为 ALB 单独维护一套 Deployment 副本它天然会跟随你现有的 Pod 伸缩。3.2 Ingress 配置的注意点annotations 决定行为在 EKS 里ALB 由 Ingress 生成控制器是 aws-load-balancer-controller。下面这条 Ingress 是我迁移时用的模板apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: monitoring-alb namespace: monitoring annotations: kubernetes.io/ingress.class: alb alb.ingress.kubernetes.io/scheme: internal alb.ingress.kubernetes.io/target-type: ip alb.ingress.kubernetes.io/listen-ports: [{HTTPS:443}] alb.ingress.kubernetes.io/ssl-policy: ELBSecurityPolicy-TLS-1-2-2021-06 alb.ingress.kubernetes.io/certificate-arn: arn:aws:acm:ap-southeast-1:123456789012:certificate/xxxx alb.ingress.kubernetes.io/healthcheck-path: /api/health alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds600 spec: rules: - host: grafana.internal.example.com http: paths: - path: / pathType: Prefix backend: service: name: grafana-alb-svc port: number: 80几个关键点解释一下。alb.ingress.kubernetes.io/target-type: ip是基于 VPC CNI 的 EKS 集群最常用模式。VPC CNI 给每个 Pod 分配的是 VPC 内的真实 IPALB 可以直接把流量送到 Pod IP少一层 NodePort 转发。这个模式下 ALB 目标组注册的是 Pod IP探活也直接打到 Pod 端口。如果你们集群用的不是 Amazon VPC CNI而是 Flannel、Calico 这类非 AWS 网络插件那target-type: ip很可能不可用需要退回instance模式让 ALB 把流量打到节点端口。这一点必须先确认否则 ALB 建出来目标组里全是 unhealthy。load-balancer-attributes里的 idle timeout 我特意调成了 600。ALB 默认的空闲超时只有 60 秒监控页面上如果有个长轮询或者 WebSocket闲置超过 60 秒就会被 ALB 掐断。这个坑后面专门说。健康检查路径也值得认真选。Grafana 的/api/health返回 200Prometheus 的/-/healthy返回 200Alertmanager 也有自己的/-/healthy。不要所有组件都默认用/有些 Web 应用根路径会返回 302 跳转AWS Load Balancer Controller 默认认为 200 和 302 都健康但如果你的应用返回 301 或 404探活就会失败。3.3 目标组健康检查死活不过安全组链的问题双轨并行最让我头疼的不是 Ingress 配置而是 ALB 目标组里的 Pod 长时间处于 unhealthy 状态。排查下来根因是安全组链。原来的 NLB 用的是instance目标模式流量通过 NLB 打到节点再经 kube-proxy 转发到 Pod所以只需要节点的安全组放行对应端口。现在 ALB 用ip目标模式探活流量直接从 ALB 的弹性网卡发到 Pod IP这条链路要保证两点节点安全组放行来自 ALB 的入站流量Pod 所在的安全组也放行探活端口。很多 EKS 集群默认只给节点安全组放行了 VPC CIDR 范围内的流量但 ALB 的安全组和节点安全组并不一定在同一个可信任关系里。如果探活不通去节点上看kubectl get endpoints -n monitoring确认 Pod IP然后从另外一台 EC2 上直接curl http://PodIP:3000/api/health看底层端口通不通。确认 Pod 本身 OK 之后再去检查节点安全组的入站规则加一条来源为 ALB 安全组、端口为 3000 的规则探活立刻转绿。4. 正式切换动作Route53 权重递进与连接自然收敛4.1 DNS 从单点 A 记录改成加权记录集双轨建好、ALB 目标组全部 healthy 之后就进入正式切换。这里我用的是 Route53 的加权记录集把同一个域名同时解析到 NLB 和 ALB通过调整权重来控制流量比例。但有个细节必须提醒原有域名通常是一条普通的 A 记录或别名记录Route53 不允许把一条记录直接改成 weighted 策略必须先删掉旧记录再创建 weighted 记录集。这个删掉再创建的间隙理论上有极小的概率让解析短暂找不到记录。我的处理方式是提前把原解析的 TTL 调低到 60 秒等待至少一个 TTL 周期过去让所有递归解析器缓存刷新。然后在低峰期一秒内完成删除和重建先建一条 weighted 记录权重 100 指向 NLB。因为递归解析器手里还有旧缓存即使重建帧内新查询短暂失败也不会大面积影响用户。之后再加一条指向 ALB 的 weighted 记录初始权重设为 0。从这一步开始双轨就在 DNS 层面成型了。4.2 权重递进的节奏从 10% 到 50% 再到 100%我没有一上来就把 50% 流量甩给 ALB而是按 10%、50%、0/100 三步走每一步间隔大概 5 到 10 分钟给监控指标留出观察时间。具体操作是每次在 Route53 的 weighted 记录里修改权重值无需删除记录。举例阶段NLB 权重ALB 权重观察重点初始1000ALB 保持 healthy无流量第一步9010ALB 出现请求检查 5xx 和延迟第二步5050对比两侧错误率和延迟差异第三步0100NLB 流量归零观察最终状态10% 这一步很有价值。流量不大就算 ALB 配置有隐藏问题影响面也只有十分之一且能马上通过 ALB 的 RequestCount 和 TargetResponseTime 指标看出来。确认稳定后再放大比例。怎么看有没有异常我的验证脚本很简单反复请求https://grafana.internal.example.com/api/health同时抓取返回码和耗时。另开一个窗口看 CloudWatch 里 ALB 的HTTPCode_Target_5XX_Count只要那个数字不增长后端就是稳的。4.3 长连接怎么处理DNS 切完不等于 NLB 立刻没流量这里要明确一个反直觉的点DNS 权重切到 0/100 之后NLB 上不会瞬间归零。已经建立的长连接和 keep-alive 连接还会继续走 NLB直到这些连接自然超时或被关闭。所以我在权重切到 0/100 后并没有马上删除 NLB而是等了大约一个完整的长连接超时周期。这个周期取决于应用的连接池配置短则 60 秒长则可能要几百秒。我在 CloudWatch 上盯着 NLB 的ActiveFlowCount和ProcessedBytes看到这两个指标稳定降到接近 0 之后才确认 NLB 已经没有实际流量。WebSocket 连接不会因为你改了 DNS 就自动重连。如果某些用户正好挂着一个 Grafana Live 会话切换后这条会话还会残留在旧的 NLB 上。好消息是传统监控页面前端基本都有自动重连会话会在断开后重连到 ALB。迁移窗口我放在了工作时间的低峰期实测就只有几个用户反馈界面短暂转圈随后自动恢复。5. 实测踩坑记录健康检查、空闲超时和 DNS 缓存三座山5.1 ALB 目标组一直 unhealthy不是路径写错是安全组链断路我在前面已经提到了安全组问题这里再给一个完整排查链路以后遇到同类问题可以直接抄作业。现象是 ALB 创建成功监听器也有但目标组里的 Pod 全部显示 unhealthy前端访问 ALB 域名直接 502。排查步骤先看目标组健康检查设置路径/api/health端口 3000返回码 200。这个没问题。手动从一台同 VPC 的跳板机 curl Pod IPcurl http://10.0.3.11:3000/api/health能拿到 200。说明 Pod 本身正常。从健康检查的视角再想一层ALB 的探活流量来自 ALB 所在子网的弹性网卡 IP它到 Pod IP 这条路径上必须被节点和 Pod 的安全组放行。登录 AWS 控制台找到 ALB 关联的安全组 ID再到 EC2 安全组里给节点安全组加一条入站规则来源选 ALB 安全组协议 TCP端口 3000保存后目标组几十秒内全部变绿。这个坑特别典型尤其对于从 NLB 迁移过来的团队因为 NLB 模式根本没暴露过这条链路。我建议大家在做双轨并行时第一件事就先验证探活不要等到真实流量进来了才发现要放行安全组。5.2 ALB 空闲超时 60 秒WebSocket 周期性掉线的隐藏杀手迁移后第一天就有同事反馈 Grafana 的实时日志面板每隔一段时间就会断开刷新一下又能恢复。我第一反应是前端重连逻辑有问题查了半天才发现是 ALB 空闲超时。NLB 的空闲超时默认是 350 秒而 ALB 默认是 60 秒。对于短连接请求这个值无所谓但对于 WebSocket 或者长轮询一旦连接空闲超过 60 秒ALB 就会主动断开。Grafana 的 Live 通道在用户没有滚动日志的时候就是一条闲置的 WebSocket很容易撞上这个限制。修复方式就是在 Ingress 上把 idle timeout 调大我调到 600 秒已经够用。如果你有更极端的可视化场景ALB 上限可以到 4000 秒但我不建议设太大因为太大会占用大量连接资源正常情况下 600 到 900 秒足够覆盖所有交互式会话。alb.ingress.kubernetes.io/load-balancer-attributes: idle_timeout.timeout_seconds600改完注解放到集群里生效后WebSocket 掉线问题没有再出现。5.3 DNS TTL 没提前调小造成的切了但没完全切这个坑发生在我第一次做类似迁移的时候这次专门避开了但它值得单独写出来。如果 DNS 记录 TTL 是默认的 300 秒甚至更长你把权重改成 90/10 之后理论上要等 5 分钟以上所有客户端才会拿到新解析结果。问题在于企业内部客户端种类很多有些浏览器会做 DNS 缓存有些中间 DNS 解析器会忽略 TTL 自行缓存更长时间。最终效果就是比例看起来不准甚至有些用户半个小时还在访问 NLB。正确做法是迁移前至少一天把 DNS TTL 调成 60 秒让所有缓存先刷新一轮。等到整个迁移和观察期结束再决定要不要恢复一个合理的 TTL比如 300 秒。TTL 调太小长期看会让解析请求变多对内部域名来说压力不大但没必要一直保持 60 秒。5.4 双轨期间 Service 选择器不能乱动还有一次差点翻车是我在给新 Service 打标签时习惯性地想改一下 selector 的值加一个环境标识。如果真这么做了ALB 的目标组注册的 Endpoints 就会变成空而 NLB 这边的老 Service 也会跟着受影响因为 selector 是同一个 Pod 集合。迁移期间的原则是入口相关的资源可以加但不能改。新建 ClusterIP Service 时selector 务必和原 LoadBalancer Service 保持一致。你可以在新资源上通过 name、annotations 区分但不要动选择器本身。任何对 Pod 标签的修改都可能触发后端重建或目标组清空这对零停机是致命的。6. 不急于删除 NLB回滚开关和最后的清理清单6.1 双轨结构最大的价值回滚只是改权重双轨并行最大的收益是回滚成本极低。只要 NLB 还留着任何时刻发现 ALB 有异常我只需要在 Route53 里把 ALB 权重改回 0把 NLB 权重改回 100流量会在几分钟内全部回到旧链路。整个过程不涉及代码回滚、不涉及后端变更纯 DNS 层面的一个操作。这也是我建议至少要保留 NLB 到迁移完成后第二天原因。你在第一天的观察期里可能会发现一些只在真实流量下才会暴露的问题比如某个监控接口的请求体过大、某些老版本浏览器对 ALB 的 HTTP/2 处理有问题、或者内部防火墙只放行了 NLB 的 IP。这些问题都需要时间才会浮出来。6.2 删 NLB 前必须核对的检查项确认 ALB 稳定运行满 24 小时后我才会开始清理。删除顺序和检查项如下先在 Route53 里确认权重已经是 ALB 100、NLB 0并且保持了一段时间。在 CloudWatch 里看 NLB 的 ActiveFlowCount确认已经连续多个周期为 0。确认没有外部系统直接通过 NLB 的域名或 IP 访问。这一步容易漏比如有些 CronJob 配置的是 NLB 域名而不是内部标准域名你 DNS 切了但它绕过 DNS 直接用 IP删掉 NLB 就出问题。执行kubectl delete svc old-loadbalancer-serviceAWS 云控制器会释放对应的 NLB 和监听器。最后回 EC2 安全组里检查是否需要清理之前为 ALB 加临时放行规则如果当时是手工加的源 IP 白名单记得更新成正式的安全组策略。删除 NLB 是不可逆的一旦删了再想回滚就只能重新创建所以一定要等到权重为 0 且 NLB 流量长时间归零后再动手。6.3 这次迁移留下的一套通用操作模式回头总结这套方法不只适用于监控服务几乎所有从 NLB 迁到 ALB的 Kubernetes 服务都可以套用盘点流量类型区分短连接、长连接、有状态依赖。建双轨ALB Ingress 指到同一组 Pod用 ClusterIP Service 作为后端。先验证 ALB 的健康检查和业务功能确认目标组全绿用curl --resolve或临时 hosts 测试域名。调低 DNS TTL等待一个完整 TTL 周期。Route53 改成双加权记录按 10%、50%、100% 渐进切换。观察指标直到 NLB 流量归零并稳定一段时间。清理旧资源同时保留回滚开关到确认无异常之后。整个过程里我最深的体会是零停机的关键不在切换那一瞬间而在于你敢不敢在切换前把每一条链路都验证到位以及切换后能不能沉住气等待流量自然收敛。监控服务自己就是做观测的这次迁移的每一个验证步骤本身也是在测试我们自己的监控链路是否足够可靠。