双十一流量退潮后,我们那个跑了三年的电商订单系统终于撑不住了。当时用的是 8 台 ECS 裸机部署,Docker Compose 管理容器,每次大促前都要手动扩容,运维同学基本 72 小时连轴转。我记得最夸张的一次,凌晨两点订单量突然冲到 1.2 万 QPS,ECS 的 CPU 直接飙到 98%,重启容器花了 15 分钟,导致 3000 多笔订单状态丢失,客服电话被打爆。
那时候我就意识到,再这么裸跑下去,团队迟早得累垮。我们决定迁移到 Kubernetes,当时刚好 K8s v1.32.0 刚发布(2024 年 12 月 11 日),我看了下 Release Notes,里面针对大规模集群的调度性能优化挺多,比如 Pod 调度延迟降低了 18%,这对我们这种订单峰值波动大的场景太重要了。
迁移第一步是拆分应用。原来的订单系统是个单体,我把核心的下单、支付回调、库存扣减拆成了三个微服务。这里有个坑,一开始没注意 Pod 的资源限制,直接按 ECS 的配置给了 4C8G,结果调度器一直找不到合适节点,后来才发现 K8s 的 Requests 和 Limits 是分开算的,我给的 Requests 太高,集群资源碎片化严重。最后调整为 Requests 2C4G、Limits 4C8G,调度才顺畅。
部署的时候用了 Deployment,因为订单服务是无状态的(状态都存在 Redis 和数据库)。这里有个细节,我们之前在 ECS 上用 supervisor 管理进程,容器重启后日志会丢,迁移到 K8s 后我直接把日志目录挂载到了 EmptyDir,再通过 Fluentd 收集到 ES,现在查日志比以前快多了,原来 grep 一个订单 ID 要翻 10 台机器,现在 Kibana 里 3 秒出结果。
最头疼的是服务发现。原来 ECS 之间调用靠 Nginx upstream 写死 IP,迁移的时候我一开始想用 Headless Service,后来发现订单服务需要频繁扩缩容,Pod IP 变来变去,Headless Service 的 DNS 解析延迟太高(实测平均 120ms),最后换成了普通的 ClusterIP Service,配合 CoreDNS 的缓存优化,解析延迟降到了 15ms 以内。
迁移完第一周,我们做了一次压测。原来 ECS 部署时,1.2 万 QPS 下接口平均响应 800ms,迁移到 K8s v1.32.0 后,同样的资源(12 台 4C8G 节点),响应时间降到了 120ms,而且自动扩缩容终于生效了——HPA 基于 CPU 使用率(阈值 70%)自动调整 Pod 副本,从原来的 10 个副本扩容到 35 个只用了 40 秒,比手动扩容快了 20 倍。
这里贴一下我们订单服务的 Deployment 配置,当时调了好几次才稳定:
有个实际排查过的问题:迁移后第三天,凌晨 3 点突然收到告警,订单服务的 Pod 频繁重启。我看了下监控,发现是 readinessProbe 配置太激进——原来我设的 initialDelaySeconds 是 5 秒,服务启动加载 Spring 上下文要 12 秒,导致探针失败,Pod 被反复重启。后来改成 15 秒,又加了 timeoutSeconds 3 秒的判断,就稳定了。现在想想,裸金属时代我们根本不会做这么细的健康检查,容器挂了全靠人工重启,哪有这效率。
去年做技术选型的时候,我们团队吵了快两周。当时有三个选项:Docker Swarm、Nomad、Kubernetes。我一开始其实偏向 Swarm,因为团队对 Docker 熟,Swarm 学习成本几乎为零,一条 docker swarm init 就能起来。但实测下来,Swarm 的调度能力太弱——我们订单服务需要按节点亲和性部署(比如把支付回调服务部署在有 Redis 缓存的节点),Swarm 的 placement constraints 只能按标签硬匹配,不支持软亲和性,有一次 Redis 节点故障,支付服务全漂移到其他节点,延迟直接涨了 300ms。
Nomad 是 HashiCorp 的,我之前在创业公司用过,部署简单,资源占用低(单个 Server 节点只要 50MB 内存)。但有个致命问题:生态太弱。我们需要做 CI/CD 集成,Jenkins 对 Nomad 的支持只有第三方插件,而且更新慢;想做服务网格,Consul Connect 虽然能用,但配置复杂度比 Istio 高太多。更关键的是,我们后来要做 AI 推理服务(用 TensorFlow Serving 跑商品推荐模型),Nomad 对 GPU 调度的支持还在实验阶段,而 K8s v1.32.0 已经把 Device Plugin 的 GPU 调度稳定性提升到了生产级,实测调度 A10 GPU 的延迟只有 2 秒,Nomad 要 8 秒。
K8s 的学习曲线确实陡,但架不住生态全。我们用的阿里云 ACK,直接集成了日志、监控、存储,原来要自己搭 ELK、Prometheus,现在控制台点几下就行。而且社区活跃,2024 年 K8s 的 GitHub 提交量比 Swarm 多 12 倍,遇到问题 Stack Overflow 一搜就有答案。比如我们之前需要动态挂载 NAS 存储,Swarm 要手动改 volume 配置,K8s 用 PV/PVC 加 StorageClass,自动创建存储,我们订单服务的文件上传模块,原来在 ECS 上要提前挂载 NAS,现在 Pod 启动自动挂载,省了运维 80% 的存储配置时间。
还有个细节,我们团队有 15 个开发,原来 Swarm 的权限管理只有简单的角色划分,K8s 的 RBAC 能精确到 Namespace 和 Pod 级别。比如测试同学只能访问 test 命名空间,不能碰 prod,原来 Swarm 时代,测试误删过生产环境的容器,现在用 RBAC 限制后,这种事再也没发生过。
这里贴一下我们当时对比三个工具的实测数据(基于 100 节点集群,部署 500 个 Pod):
| 指标 | Docker Swarm | Nomad | Kubernetes v1.32.0 |
|---------------------|--------------|---------|---------------------|
| Pod 调度延迟(平均) | 450ms | 280ms | 120ms |
| 自动扩缩容响应时间 | 不支持 | 60秒 | 40秒 |
| GPU 调度成功率 | 65% | 82% | 99.5% |
| 社区插件数量 | 120+ | 300+ | 5000+ |
最后选 K8s 还有一个原因:2024 年平台工程(Platform Engineering)火了,我们想基于 K8s 做内部开发者平台(IDP),让开发自己能部署服务,不用每次找运维。Swarm 和 Nomad 根本撑不起这个场景,K8s 的 CRD 和 Operator 能自定义资源,我们现在用 Helm Chart 打包服务,开发只要在 GitLab 提交代码,流水线自动构建镜像、部署到 K8s,原来部署一次要 1 小时,现在 10 分钟搞定。
有个实际例子:我们之前用 Nomad 部署一个 Go 微服务,要写 HCL 配置文件,还要自己写健康检查脚本,后来迁移到 K8s,用 Deployment 加探针,配置量减少了一半。而且 K8s 的滚动更新比 Nomad 稳,原来 Nomad 更新时会有 5% 的请求失败,K8s 的 maxUnavailable 设为 1 后,更新期间零失败。
我们现在的订单系统日活 120 万,配置项有 300 多个,包括数据库地址、Redis 密码、第三方支付接口密钥、限流阈值等等。原来这些配置都写在代码里,每次改个 Redis 密码,要重新打包镜像、重启所有服务,耗时 40 分钟,还容易漏改环境。后来用 ConfigMap 解耦后,改配置只要 kubectl apply 一下,30 秒生效,不用重启 Pod。
这里有个关键:ConfigMap 只放非敏感配置,敏感信息(比如支付密钥)用 Secret。我之前犯过傻,把 MySQL 密码放 ConfigMap 里,后来审计说不安全,才改到 Secret。ConfigMap 的数据是明文存储的,虽然 K8s v1.32.0 支持对 ConfigMap 做加密存储(用 KMS 插件),但我们非敏感配置(比如日志级别、接口超时时间)没必要加密,省点性能。
我们订单服务的配置分三层:基础配置(所有环境通用)、环境配置(dev/test/prod 不同)、动态配置(比如限流阈值,随时调)。基础配置放在 order-common-config ConfigMap,环境配置放在 order-prod-config,动态配置用 Apollo 管理,但 Apollo 的配置也会同步到 ConfigMap,避免 Apollo 挂了服务起不来。
这里有个实际问题:有一次我们改了 prod 环境的 ConfigMap,把 order.timeout 从 500ms 改成 300ms,结果部分 Pod 没生效。排查下来发现,ConfigMap 更新后,Pod 里的环境变量不会自动刷新,只有挂载文件的方式才会触发容器重新读取。原来我用的是 env.valueFrom.configMapKeyRef,这种环境变量注入方式,ConfigMap 更新后不会同步到 Pod,后来改成 volume 挂载,把配置写成文件,服务里加个文件监听,配置更新后自动 reload,才解决这个问题。
现在我们的 ConfigMap 配置是这样的,把订单服务的所有非敏感配置都放进去:
服务里读取配置的代码(Go 语言)是这样的,监听配置文件变化:
现在改配置太方便了,比如大促前要调高限流阈值,我只要改 ConfigMap 里的 rate.limit.qps,然后 kubectl apply -f order-config.yaml,30 秒后所有 Pod 的配置都生效了,不用重启服务。原来用 ECS 部署时,改个配置要登录 10 台机器,手动改配置文件,再重启进程,至少要 20 分钟,还容易改错。
还有个好处,ConfigMap 支持版本控制,我们每次改配置都会提交到 Git,出问题能一键回滚。上个月有一次,我把 order.timeout 改成 100ms,结果订单回调超时率飙升到 15%,我直接 kubectl apply 回滚到上一个版本的 ConfigMap,1 分钟就恢复了。如果用原来的方式,还要重新打包镜像,回滚要 10 分钟,损失就大了。
现在我们的微服务配置管理已经标准化了:所有非敏感配置放 ConfigMap,敏感信息放 Secret,动态配置用 Apollo 同步到 ConfigMap,开发改配置只要提 Git MR,运维审核后自动 apply。原来 15 个开发,每次上线要 3 个运维配合改配置,现在 1 个运维就够了,效率提升 70%。
去年我们团队把那个跑了三年的老电商系统往 v1.32.0 上迁的时候,最头疼的不是业务代码,是流量入口。原来的 Ingress 配置已经变成了“面条代码”,一个 ingress.yaml 里面塞了二十多个 path,正则匹配规则写得跟天书一样,新来的同事改个路由能改出线上故障。那时候我就意识到,Ingress 这种基于注解(Annotation)的扩展方式,在复杂业务面前真的太脆弱了。
我们当时有个具体的痛点:订单服务和商品服务都需要做 A/B 测试,要求根据 HTTP Header 里的 X-User-Group 把流量分到不同的 Service 版本。用 Ingress 做的话,得靠 Nginx Ingress Controller 的 nginx.ingress.kubernetes.io/canary-by-header 注解,不仅配置分散,而且一旦规则多了,谁也搞不清流量到底是怎么走的。
后来我研究了一下社区动态,发现大家都在聊 Gateway API。这东西不是简单的替代品,它把流量管理拆成了三个角色:厂商(GatewayClass)、集群运维(Gateway)和业务开发(HTTPRoute)。这种职责分离太对胃口了,我直接决定在新项目里试水。
我现在的做法是,把流量入口的定义权收归平台组,业务组只管自己的路由规则。比如我们现在的 Gateway 定义,只负责监听 443 端口和绑定证书,长这样:
这里我特意选了 Cilium 作为 gatewayClassName,因为现在社区里 eBPF 技术 这么火,Cilium 1.16 版本对 Gateway API 的支持已经 GA 了,性能比传统的 iptables 模式强太多,我们压测的时候 QPS 能提升 15% 左右。
业务侧怎么用呢?以前改 Ingress 得找运维合并配置,现在他们自己写 HTTPRoute 就行。比如那个订单服务的灰度发布,配置非常清晰,不用再去猜注解的优先级:
这个配置的逻辑一目了然:如果 Header 里带了 X-User-Group: beta,流量就进 v2 版本;否则走 v1。如果不做这种拆分,像以前那样把所有规则堆在一个 Ingress 里,一旦某个正则写错,整个入口就挂了。而且 Gateway API 支持多租户隔离,业务 A 的 Route 挂了不会影响业务 B,这在 Ingress 时代是很难做到的。
今年 3 月份,我们那个负责用户画像推荐的服务突然开始抽风。用户反馈说进入首页时,推荐栏加载特别慢,有时候直接转圈圈超时。我看了一下监控,Pod 的 CPU 使用率才 40%,内存也很稳,但接口的 P99 延迟从平时的 120ms 飙升到了 800ms。这就很诡异了,资源没用完,为什么慢?
当时我第一反应是代码里有死锁或者数据库慢查询,但翻了一圈日志,DB 查询都在 10ms 以内。后来我盯着 Grafana 里的容器监控看,突然注意到一个指标:container_cpu_cfs_throttled_seconds_total。这个数值在疯狂跳动。
这就要说到 Linux 的 CFS(Completely Fair Scheduler)调度器了。我们在 K8s 里给这个服务设置的 requests.cpu 是 2,limits.cpu 也是 2。这意味着容器被严格限制只能使用 2 个核心的计算时间。CFS 会把 CPU 时间切成一段一段的“配额(Quota)”。如果容器在很短的时间内把配额用完了,哪怕物理机上的 CPU 还很空闲,它也会被“限流(Throttling)”,强制进入等待状态,直到下一个调度周期。
为什么这么做会出问题? 因为我们的推荐服务是个 Java 应用,JVM 的 JIT 编译和 GC 线程都是 CPU 密集型的。当流量突增时,这些线程瞬间把 2 个核的配额吃满,结果 JVM 还没来得及把请求处理完,就被 CFS 掐断了 CPU 使用权。线程在等待 CPU,请求就卡住了。如果不解决这个问题,扩容是没用的,因为新 Pod 也一样会被限流。
排查过程我用了 perf 工具在宿主机上抓了一下,发现确实有很多 runnable 的进程在等 CPU。
解决办法其实挺反直觉的。我并没有直接把 Limit 调大,而是调整了 CPU 的 request 和 limit 策略。对于这种对延迟敏感的计算型服务,我建议去掉 limits,或者把 limits 设得比 requests 大很多,甚至是只设 requests 不设 limits(当然这得看集群的监控能力)。
我最后改的 Deployment 配置是这样的:
去掉 Limit 后,CPU 节流现象立刻消失了。P99 延迟从 800ms 降回到了 150ms 左右。但这有个副作用,就是这个 Pod 可能会吃掉更多节点资源,所以我在节点层面开启了 QoS 保障,确保这种高优先级的 Pod 不会被驱逐。
这里有个细节,K8s v1.32.0 里对调度器的默认行为其实已经优化了一些,但 CFS 的底层逻辑没变。我的经验是,对于计算密集型应用,如果你设了 limit,一定要确保 limit 稍微大于 request,给 JVM 的 GC 和 JIT 留点喘息空间,否则这种莫名其妙的超时会让你查到怀疑人生。
前年我们公司做架构复盘,老板问了一个很尖锐的问题:“我们上了 K8s,机器费是省了,但运维的人为啥没少?” 当时我算了一笔账,发现虽然不用半夜起来重启机器了,但人工盯着监控、手动扩缩容、处理发布回滚这些事,还是占用了我们 SRE 团队 30% 的时间。
后来我推动了一次“自动化改造”,核心就是利用 K8s 的 自愈能力 和 HPA(Horizontal Pod Autoscaler)。改造完之后,我们统计了半年的数据,运维工单量直接降了 40%,大促期间的故障率也从 0.5% 降到了 0.1% 以下。
拿我们那个秒杀系统举例。以前大促前,我们得提前一周写扩容预案,手动改 Deployment 的 replicas 数量,从 10 个扩到 50 个。活动结束再手动缩回来。这中间有两个风险:一是人工操作容易手抖输错数字;二是如果流量比预期来得更早,人工扩容根本来不及,系统直接被打挂。
现在我们用 HPA,完全交给 K8s 自己判断。我们现在的配置是基于 CPU 和自定义 QPS 指标来做的。因为光看 CPU 不准,有时候 CPU 很低但网络包量很大,服务也扛不住。
这是我们在生产环境跑的 HPA 配置:
这个配置里有个关键细节:behavior 字段。我特意设置了缩容的 stabilizationWindowSeconds 是 300 秒(5分钟),而扩容只要 30 秒。为什么?因为扩容一定要快,缩容一定要慢。如果不设置这个,流量一波动,Pod 数量就会像抽风一样反复扩缩,这叫“抖动”,会导致服务频繁重启,反而增加故障率。
自愈能力 的威力在一次机房网络抖动中体现得淋漓尽致。那天晚上 10 点,某个节点突然 NotReady。如果是以前,那台机器上的 10 个 Pod 就全挂了,我们得收到告警,然后手动调度或者等机器恢复。现在呢?K8s 在 5 分钟内自动把这 10 个 Pod 驱逐并重新调度到了其他健康的节点上。整个过程业务侧只感知到了极短暂的延迟(因为连接断开重连),没有任何人工介入。
从数据上看,自从用了这套机制,我们 SRE 团队从“救火队员”变成了“平台开发者”。以前 3 个人盯着 50 个服务,现在 1 个人就能看住 200 个服务。这省下来的不仅是人力成本,更是避免了因为人为操作失误导致的事故。毕竟,人是最不可靠的组件,让 K8s 去处理那些重复性的“脏活累活”,才是云原生的正确打开方式。
去年双十一前,我接了个苦差事:把公司核心的订单状态同步服务从 ECS 部署迁移到 K8s。当时业务量暴涨,ECS 扩缩容太慢,大促时经常挂。我拍板上了 K8s v1.32.0,用了 Deployment + HPA。
结果上线第一周就出了幺蛾子。凌晨两点,告警炸了,订单同步延迟飙到 5 分钟。我迷迷糊糊登上集群,发现 Pod 一直在重启,但 CPU 和内存看着还没到上限。折腾了半小时,才定位到是 CPU Throttling 的问题。我给容器设的 Request 太低,Limit 又卡得死死的,导致进程虽然没超限,但一直被内核限制调度,活活憋死了。最后我把 Request 调大,并给 JVM 加了 -XX:+UseContainerSupport 参数,延迟瞬间掉回 200ms 以内。
说实话,K8s 不是银弹。
* 适合上的场景:你的服务超过 10 个,或者需要频繁发版、做灰度。像我这种需要应对突发流量(比如大促)的,K8s 的自愈和 HPA 确实能救命。
* 没必要上的场景:如果你就两三个服务,流量常年不变,别折腾。我见过小团队为了赶时髦上 K8s,结果光维护 Etcd 和 Calico 网络就耗掉半个运维的人力,纯属给自己找罪受。
* 选型坑:当初我也纠结过 Swarm,简单是简单,但生态太弱。Nomad 虽然轻量,可周边工具链(监控、日志)得自己拼。最后选 K8s,就是看中它生态全,虽然复杂点,但招人好招,文档也好找。
学习 K8s 千万别一上来就死磕源码或者各种网络插件原理。先拿个自己的小项目,比如一个 Spring Boot 的 Demo,试着用 Deployment 跑起来,再配个 Ingress 暴露出去。遇到报错别慌,那是你长经验最快的时候。记住,工具是为了解决问题,别为了用而用。