云容器和云中间件这两个词,单独拎出来谁都能说两句,真放到一个项目里让它们协同工作,问题就全冒出来了。我最近半年连着参与了三个团队的上云改造,规模从二十几个服务到上百个服务不等,每一次都是容器这一层铺得挺顺,卡壳全卡在中间件上——连接池打满、消息重复、配置改了不生效、滚动发布的时候把流量打进正在关闭的实例。这篇就把我踩过的坑和摸出来的套路摊开讲:云容器解决的是应用"跑在哪、怎么调度、怎么扩缩",云中间件解决的是应用之间"怎么连、怎么缓存、怎么异步、怎么限流"。适合正准备把单体拆成微服务、或者已经把服务塞进容器但中间件还是一堆散落虚拟机的人看,也适合已经上了容器平台但被稳定性问题反复捶的团队参考。全文不聊虚的,都是我实际调过的参数、抄得走的配置和排查路径。
1. 先把边界划清楚:云容器和云中间件到底各管什么
1.1 用"毛坯房和物业"打个比方
我最喜欢拿房子做类比。云容器就像一套标准化的毛坯房加物业管理:房子规格统一,水电气接口固定,今天住进来的是做电商的,明天换成做内容审核的,换个家具(镜像)就能换租客,物业(编排系统)负责分配房间、拉电闸、坏了自动换个新房客进来。但房子本身不提供邻里关系——你家做饭要买燃气、要连自来水公司、要收快递、要跟对门打招呼,这些"对外连接"的能力,就是云中间件干的活。
这个比方能解释一个很常见的误解:很多人以为把服务容器化之后,服务之间的调用关系会自动变简单。恰恰相反。容器让实例变得"朝生夕死",IP 随时在变,副本数随时在扩缩,原来写死在配置文件里的192.168.1.20:3306这种地址全废了。中间件在这里承担的角色,是把这种"不确定"重新收敛成"确定"——注册中心告诉你现在有哪几个实例活着,配置中心保证一百个副本拿到的是同一份配置,消息队列把瞬时洪峰削平,网关挡住不该进来的流量。
所以划边界很简单:容器负责生命周期,中间件负责连接语义。一个请求从进入集群到落到具体代码,前半段归容器和编排,后半段归中间件和应用自己。搞混这条线,就会出现"用容器解决业务耦合"这种注定失败的尝试——我见过有团队试图靠 Pod 之间的亲和性调度来保证调用顺序,最后被节点漂移折腾到放弃。
1.2 容器管"跑在哪",中间件管"怎么连"
把职责再拆细一点,容器这一层实际承担四件事:镜像的构建与分发、运行时的资源隔离、编排系统的调度与自愈、以及网络与存储的抽象。你写 Dockerfile 定的是"我这应用需要什么环境",你写资源声明定的是"给我多少 CPU 和内存",你配探针定的是"怎么判断我活着",你设副本数定的是"同时跑几份"。这四件事都围绕一个核心目标:让实例变成可以随时替换的耗材。
中间件这一层承担的是另外四件事:服务寻址(注册中心、DNS、服务网格)、流量治理(网关、限流、熔断、灰度)、数据缓冲(缓存、消息队列)、以及配置与状态的集中管理。它的核心目标是:让耗材之间的协作保持稳定语义。
这两层的目标其实是矛盾的。容器追求"随时可以杀掉重建",中间件追求"连接是稳定的、会话是连续的"。矛盾点就是所有坑的来源。举个最典型的:滚动发布的时候,编排系统会先起新 Pod 再杀旧 Pod,如果你的服务在收到终止信号后立刻断开数据库连接池和消息消费者,而那些连接上还有未提交的事务、未确认的消息,数据就出问题了。这个问题的解法不在容器侧,而在于你要在应用里加优雅停机:先摘流量、再停消费者、再等存量请求处理完、最后关连接池。
1.3 上云顺序里最容易踩的那个坑
我见过最多的错误顺序是:先把应用容器化,中间件原封不动放在几台老虚拟机上,用固定 IP 连过去。短期看很省事,跑两三个月就开始出问题。
第一层问题是容量错配。应用侧因为容器化变得弹性了,大促时副本从 10 个扩到 100 个,每个副本默认开 20 个数据库连接,一瞬间就是 2000 个连接打到那台老数据库上,直接拒绝服务。中间件没跟着做连接数规划,容器的弹性反而变成了攻击自己的武器。
第二层问题是配置漂移。应用副本从 10 个变 100 个,如果配置靠改环境变量、改 YAML,那 100 个副本里必然有几个拿的是旧配置。我遇到过最离谱的一次是灰度发布,新旧两版服务读的缓存 key 前缀不一样,导致缓存命中率掉到 3%,数据库差点被打穿。
第三层问题是可观测性断裂。容器里的日志默认打到标准输出,中间件在虚拟机上写本地文件,两边的日志时间戳时区还不一致,出了问题根本对不上时间线。
所以我现在的建议顺序是反过来的:先规划中间件的连接模型和容量模型,再做应用容器化。具体说,先把"每个服务实例最多占多少中间件连接"这个数字定死,写进规范,然后再去动容器。这个数字会反过来约束你的副本数上限和扩缩容策略,是整个方案的锚点。
2. 云容器的核心机制与几个关键决策
2.1 镜像、运行时、编排:三层各自的责任
镜像这一层,绝大多数团队的问题出在"太大"和"不确定"两件事上。基础镜像动辄 1G 多,拉取一个镜像要两三分钟,节点扩容的时候新 Pod 卡在 ImagePullBackOff 上,HPA 触发扩容却半天起不来,弹性形同虚设。我的做法是统一用精简基础镜像,多阶段构建,把编译工具链全部丢在构建阶段,最终镜像只留运行时和产物。一个常规的 Java 服务,镜像能压到 200M 以内;Go 或者静态编译的服务能压到几十 M。
还有一个坑是镜像标签。用 latest 是自找麻烦,同一个标签在不同时间拉下来内容可能不一样,回滚的时候你根本不知道自己回滚到了哪个版本。我的习惯是标签用"版本号 + 提交短哈希",配合镜像仓库的不可变标签策略,谁覆盖标签谁负责。
运行时这一层,核心是资源隔离的两个维度:CPU 和内存。CPU 是压缩型资源,超了会被限流,表现为延迟抖动;内存是不可压缩资源,超了直接被内核杀掉,表现为 OOMKilled。这两种失败模式完全不同,排查思路也完全不同,后面第 5 节会细说。
编排这一层,真正需要你操心的其实就三件事:调度约束(让 Pod 落在合适的节点上)、自愈策略(探针怎么配)、扩缩策略(按什么指标扩)。其余的都交给平台。我见过太多团队在调度约束上过度设计,写了一堆亲和性和反亲和性规则,结果集群资源碎片化,明明剩 30% 的空闲 CPU,就是调度不上去。
2.2 requests 和 limits 到底怎么算
这是我最想展开讲的一块,因为算错的代价非常直接:算小了频繁 OOM 和限流,算大了账单翻倍。
先说结论性的原则:request 决定调度和成本,limit 决定上限和失败模式。request 是编排系统做调度时认为你至少要占多少资源,节点上所有 Pod 的 request 之和不能超过节点可分配资源;limit 是运行时的硬上限,超了就被限流或被杀。request 设得比实际用量高太多,节点上明明还有余量却调度不进去,资源利用率上不去;设得太低,节点被超卖,多个 Pod 抢资源互相影响。
给一个我正在用的算法,以 Java 服务为例。假设堆内存规划 2G,元空间 256M,直接内存(Netty 之类的堆外缓冲)预留 256M,线程栈按 200 个线程 × 1M 算 200M,JIT 代码缓存和 GC 自身开销预留 300M 左右,加上一些零碎的本地内存,总计大约 3G。那么 limit 给 3.5G,留 15% 左右的缓冲;request 给 2.5G,因为实际稳态很少真的用满 limit。
对应到 JVM 参数,关键是让 JVM 认识容器限制:-XX:MaxRAMPercentage=70,这样堆会按容器 limit 的 70% 自动算,而不是按宿主机物理内存算。老版本 JDK 不识别容器 cgroup 限制,会按宿主机内存去设堆,容器 limit 是 4G 但它按 64G 的机器去开堆,结果必然 OOMKilled,这个问题我至少在三个项目里遇到过。
CPU 这边,我倾向于 request 按服务的 P95 实际用量来给,limit 给 request 的 2 倍或者干脆不设 limit(取决于集群策略)。原因是 CPU 限流是 100ms 一个周期结算的,一个单线程的突发任务如果需要 1.5 核而 limit 只有 1 核,它每个周期都会被打断,P99 延迟会莫名其妙地飙上去,而平均值看起来完全正常。这种问题的排查成本极高,不如一开始就不设死 limit。
提示:CPU limit 造成的长尾延迟,在监控上表现为 P99 抖动但 CPU 使用率曲线平滑,别盯着平均值看,要看每个 Pod 的 CPU throttling 指标。
2.3 有状态和无状态怎么划界
教科书上说"无状态服务上容器,有状态服务用虚拟机",这话对了一半。现在的编排系统对有状态负载的支持已经很成熟了,有状态工作负载、持久卷、稳定的网络标识这些能力都有。真正要判断的是另一个维度:故障恢复的代价。
无状态服务的实例挂了,重新拉一个起来,接上流量就行,代价是几十秒。缓存类的有状态服务挂了,重新拉起来,缓存是空的,所有请求瞬间穿透到数据库,代价可能是数据库被打挂。消息队列的节点挂了,重新拉起来,要跟其他节点做数据同步和选举,代价是几分钟的不可用。数据库主节点挂了,切换代价更大。
所以我的划分标准是这样:
| 类型 | 典型组件 | 容器化建议 | 理由 |
|---|---|---|---|
| 无状态代理类 | 网关、限流器、链路采集代理 | 强烈建议容器化 | 副本可随意扩缩,故障恢复代价极低 |
| 缓存类 | 分布式缓存 | 可容器化,但必须配持久化和预热 | 冷启动的穿透风险需要额外防护 |
| 消息类 | 消息队列 | 可容器化,但建议独立节点池 | 磁盘 IO 密集,与业务 Pod 混布会互相干扰 |
| 注册配置类 | 注册中心、配置中心 | 建议容器化 + 至少三副本 | 数据量小但可用性要求极高 |
| 存储类 | 关系型数据库 | 建议用托管服务 | 备份、恢复、升级的复杂度远超编排能覆盖的范围 |
这张表我自己用了很久,基本能覆盖八成场景。关键判断点就一句话:故障恢复过程中,会不会产生二次伤害。会产生二次伤害的(比如缓存冷启动打穿数据库、数据库切换丢数据),就不要只依赖编排系统的自愈能力,要么用托管服务,要么额外加保护层。
3. 云中间件的分类与容器化适配度
3.1 六类主流中间件及其真实职责
把中间件按职责分类,比按产品名字分类有用得多。我一般分成六类。
服务注册与发现,解决"现在有哪些实例活着、地址是什么"。它的核心不是存地址,而是健康检查机制——怎么判断一个实例真的能接流量,而不是进程活着但业务线程池已经满了。这是个很微妙的问题,后面第 5 节会讲探针和注册心跳的配合。
配置中心,解决"一百个副本怎么拿到同一份配置、改了怎么生效"。它的价值在于把配置从镜像里剥离出来,做到"一份镜像多环境跑"。但要注意它带来的新问题:配置变更现在是运行时行为,出错的影响面比发布还大。
API 网关,解决"外部流量怎么进来、怎么鉴权、怎么限流、怎么做灰度"。它在架构里的位置很特殊,是所有流量的必经之路,所以它的可用性要求比任何业务服务都高,而且它本身的性能直接决定了整个系统的吞吐上限。
分布式缓存,解决"热点数据不要每次都查数据库"。它的核心矛盾是"缓存和数据库的一致性",而不是性能。性能是附带的,一致性才是要花心思的地方。
消息队列,解决"同步调用改异步、削峰填谷、解耦"。它的核心矛盾是"消息不丢"和"消息不重"不能同时靠中间件保证,最终还是要业务侧做幂等。
可观测性组件(日志采集、指标采集、链路追踪),解决"出问题的时候能不能快速定位"。这一类经常被当成附属品,最后上线才发现没数据可查,被迫回滚重来。
3.2 哪些适合跑在容器里,哪些不建议
按容器化适配度从高到低排,我的实际经验是这样的。
适配度最高的是无状态代理类:网关、限流器、日志采集代理、链路追踪的边车。这类组件本身不存数据,一个实例挂了对整体没影响,扩缩容就是改副本数,跟业务服务共享同一套发布流程,运维成本极低。
其次是注册中心和配置中心。它们的存储量很小(注册中心存的是实例列表,配置中心存的是几百 KB 的配置文本),但可用性要求极高,所以要至少三副本、跨可用区打散、配好持久化。容器化之后的好处是升级和回滚变得非常轻量。
再次是缓存和消息队列。这两个都能容器化,但有几个硬性约束要满足:必须用本地盘或者高性能网络盘做持久化,必须用有状态工作负载保证网络标识稳定,必须配好资源隔离避免和业务 Pod 抢 IO。我一般会给它们单独划一个节点池,打上污点,只允许中间件 Pod 调度上去。
最不建议容器化的是关系型数据库。不是说技术上做不到,而是备份、恢复、版本升级、参数调优这些事情背后需要大量领域知识,编排系统提供的自愈能力解决不了"数据坏了怎么办"这个问题。用托管服务的成本,通常比自己维护一套高可用数据库要低。
3.3 三种部署形态的取舍
中间件在云上的部署形态,实际上有三种:完全自建、半托管、全托管。
完全自建就是自己在容器里跑一套开源组件。优势是可控性最强、成本最低、没有厂商绑定;劣势是所有运维责任都在你身上,包括容量规划、版本升级、故障处理、安全补丁。适合有专职中间件运维团队、或者有特殊定制需求的场景。
半托管是指用云上的托管服务,但保留一定的参数控制权。优势是基础运维(节点故障、备份、监控)由平台负责,你只需要关注业务配置;劣势是有些参数动不了,出问题的时候排查链路会断在平台侧。适合大多数中小团队。
全托管是纯粹的按量使用,你只管连上去用。优势是省事;劣势是成本随规模线性增长,而且深度优化空间很小。
我的实际选择是分层的:注册配置、缓存、消息队列用半托管,网关和可观测性组件自己容器化部署(因为需要跟业务代码深度集成),数据库用托管服务。这个组合在三个项目里都验证过,性价比比较高。
注意:不管选哪种形态,都要在架构图上明确标出"这一层挂了会影响什么",并且至少演练一次单点故障。我见过一个团队,注册中心用的托管服务,从来没人想过它挂了会怎样,结果一次平台侧维护,所有服务瞬间互相找不到,全站不可用。
4. 一套可复现的落地流程
4.1 资源与网络规划
动手之前必须先把三个数字定下来,这三个数字决定了后面所有配置。
第一个数字:单个服务实例的中间件连接总数上限。这个要反推。假设数据库最大连接数是 1000,要预留 20% 给运维工具、批处理任务和平台自身的连接,剩 800 可用。假设有 10 个服务会连数据库,平均分下来每个服务 80 个连接上限。如果这个服务正常运行时是 20 个副本,那么每个副本的连接池上限就是 4。这个数字通常小于默认连接池大小(很多框架默认 10 或 20),所以必须显式调小。
第二个数字:扩缩容的副本上限。上面算出每副本 4 个连接,那么当 HPA 把这个服务扩到 20 个副本时,正好用满 80 个连接。副本上限必须设成 20,超过就违反连接数约束了。如果业务真的需要更多副本,那就得回头去加数据库连接数,或者引入连接池代理层(在应用和数据库之间加一层连接复用代理,把大量应用连接收敛成少量数据库连接)。
第三个数字:单个中间件节点的资源规格。以缓存为例,假设总数据量 8G,副本数 3(一主两从或者三主分片),每个节点要给 4G 内存,其中 3G 给数据、1G 给连接缓冲和内部开销。容器 limit 设 4.5G,request 设 3.5G。磁盘用本地 SSD,容量按数据量的 2.5 倍给(持久化文件 + 重写时的临时文件)。
网络规划上,最重要的是把"集群内通信"和"外部访问"分开。业务服务之间、服务与中间件之间的通信走集群内网络,不需要暴露到外部;只有网关需要对外暴露。我见过不少团队为了图方便,把数据库端口直接暴露出去,安全风险极高。
4.2 配置与密钥的分层管理
配置管理这块,我的原则是分三层,不混用。
第一层是镜像内置配置,只放那些"跟环境无关、跟代码版本强绑定"的东西,比如框架的默认参数、日志格式模板。这些东西改了就应该重新发布,不应该热更新。
第二层是环境配置,通过配置中心下发,包括中间件地址、超时时间、线程池大小、开关。这一类可以热更新,但要有版本和灰度能力——先推 1 个副本,观察 5 分钟,指标正常再推全量。配置变更引发的事故我见过太多次了,最典型的是把限流阈值改错,直接全站 503。
第三层是密钥,绝对不能放在镜像里,也不能明文写在 YAML 里。用密钥管理系统,通过挂载文件或者运行时注入的方式给到应用,并且要有轮换机制。
给一个我认为比较健康的配置分层示例:
# 只展示结构,字段名按实际平台调整 apiVersion: apps/v1 kind: Deployment spec: replicas: 8 template: spec: containers: - name: order-service image: registry.example/order-service:1.4.2-a3f9c1 env: # 非敏感、随环境变化的配置走配置中心,这里只放地址 - name: CONFIG_CENTER_ADDR value: "config-center:8848" - name: APP_ENV value: "prod" envFrom: # 敏感信息从密钥对象整体注入 - secretRef: name: order-service-secrets resources: requests: cpu: "500m" memory: "2.5Gi" limits: cpu: "2" memory: "3.5Gi" readinessProbe: httpGet: path: /health/ready port: 8080 initialDelaySeconds: 20 periodSeconds: 5 failureThreshold: 3 startupProbe: httpGet: path: /health/live port: 8080 periodSeconds: 5 failureThreshold: 30 lifecycle: preStop: exec: command: ["sh", "-c", "sleep 8"]这段配置里有两个细节值得单独说。startupProbe的failureThreshold: 30乘以periodSeconds: 5,等于给应用 150 秒的启动时间,这个时间窗口内其他探针不生效。Java 服务冷启动慢,没有这个配置的话,就绪探针会在应用还没起完的时候就判定失败,然后编排系统不停地重启,永远起不来。preStop里的 sleep 8 秒是为了让负载均衡器有时间把流量从这个实例上摘掉,同时让存量请求处理完。这两个配置是我在每个服务里都必须加的,缺一个就会在发布期间出问题。
4.3 服务发现与流量接入
服务发现的配置,核心是让"注册心跳"和"就绪探针"对齐。逻辑上应该是:应用启动完成 → 就绪探针通过 → 编排系统把 Pod 标记为 Ready → 注册中心收到注册 → 网关开始往这个实例转发流量。下线反过来:先标记 NotReady → 注册中心摘除 → 等待存量请求结束 → 进程退出。
这里最常见的错配是:应用启动时立刻向注册中心注册,但此时数据库连接池还没建好、缓存还没预热完,网关已经把流量打过来说,一批请求直接失败。解法是注册动作绑定在就绪探针返回成功的那个时刻,而不是应用进程启动的时刻。
流量接入这一层,要看规模。几十个服务的时候,用一个网关做南北向流量,服务之间直连(靠注册中心寻址)就够了。上百个服务的时候,考虑上服务网格,把东西向流量的治理能力(熔断、重试、超时、链路加密)下沉到边车代理里,业务代码不用管。
上服务网格之前要算清楚代价:每个 Pod 多一个边车容器,按 100m CPU / 128Mi 内存算,1000 个 Pod 就是 100 核和 128G 内存的额外开销。网络上也多了一跳,延迟增加 1 到 3 毫秒,对延迟敏感的服务要有评估。我的建议是先从网关和关键链路上,不要一上来就全量注入边车。
4.4 可观测性三件套
日志、指标、链路,这三样必须在服务上线之前就配好,不能等出问题再补。
日志这块,我的规范是:全部打到标准输出,不要在容器内写文件;统一 JSON 格式,至少包含时间戳、服务名、实例标识、链路 ID、日志级别;采集代理以守护进程方式在每个节点上跑,负责收集和转发。关键点是链路 ID 必须从入口一路透传到所有下游调用和中间件操作,否则出了问题你没法把一次请求的所有日志串起来。
指标这块,最少要暴露四类:请求量、错误率、延迟分布(P50/P95/P99,不要只看平均值)、以及资源使用(CPU、内存、连接数、队列长度)。中间件的指标要单独采集,尤其是连接池的活跃连接数、等待队列长度、消息队列的堆积量。这几个指标是排查问题的第一现场。
链路这块,采样率要按需调。全量采集的成本很高,通常生产环境设 1% 到 10% 的采样率,但错误请求要 100% 采集。这个组合既能控制成本,又不会漏掉问题。
5. 线上问题排查实录
5.1 启动慢与探针误杀
这是容器化初期最高频的问题,表现是 Pod 反复重启,日志里看不出明显错误,状态一直是 CrashLoopBackOff 或者一直不 Ready。
排查顺序是这样:先看事件(describe出来的 Events 部分),确认是探针失败还是进程退出。如果是探针失败,看探针配置的超时和阈值够不够;如果是进程退出,看退出码,137 是 OOMKilled,1 是应用自身异常,143 是收到 SIGTERM。
我遇到过一个特别隐蔽的案例:应用启动需要加载本地缓存文件,这个文件有 2G,从网络存储挂载进来,加载时间需要 90 秒,而当时就绪探针的初始延迟只给了 30 秒。表现是永远起不来。解法有两个方向,要么加长启动窗口,要么把缓存文件做小。我选了后者,改成按需分片加载,启动时间降到 12 秒。
还有一类问题是优雅停机没做,滚动发布的时候出现零星 502。原因是 Pod 收到 SIGTERM 后立刻退出,但负载均衡器还没反应过来,还有流量在往这个实例打。解法就是前面配置里那个 preStop 加 sleep,再配合应用侧的信号处理:收到终止信号后先把健康检查接口返回改成不健康,再等待 10 秒,再关闭连接池和消费者。
5.2 连接数打满与连接风暴
表现是应用侧报"获取连接超时",数据库侧连接数打满,重启应用能缓解但很快复发。
这个问题的根因通常有三个。一是连接池上限设置大于数据库能承受的总量,前面 4.1 节算过,必须做除法。二是连接泄漏,代码里借了连接没还,这种问题会随着时间推移逐渐恶化,最后必然打满,解法是开连接池的泄漏检测(设置借用超时时间,超过就打印堆栈)。三是扩缩容瞬间的连接风暴,100 个新 Pod 同时启动,每个都建满连接池,瞬时连接数暴增。
第三种情况我遇到过最严重的一次,是节点故障导致大量 Pod 同时重建,重建过程中所有实例同时向数据库建连,数据库的连接数瞬间被打满,然后所有请求超时,应用侧重试又加剧了这个问题,形成雪崩。后来我们做了两件事:一是连接池启动时只建最小连接数,按需增长;二是给连接建立加重试和抖动(每个实例的首次建连时间随机延迟 0 到 5 秒),把这个瞬间的峰值摊平。
提示:如果中间件侧已经有连接复用代理,应用侧的连接池上限就可以设得比较小,这时候瓶颈在代理层,需要单独监控代理的连接数。
5.3 消息重复与幂等
消息队列的语义是"至少一次"投递,所以重复消费是必然的,不是异常。这一点必须在设计阶段就跟业务方讲清楚,别等到上线后开始对账才发现钱算重了。
我处理重复消费的通用做法是三步:第一,每条消息带一个全局唯一的业务键,由生产端生成,不要用队列自己生成的 ID;第二,消费端建一张去重表(或者用缓存),记录已处理的业务键,处理前先查、处理时插入、处理失败要能回滚;第三,业务操作本身设计成幂等,比如"设置状态为已支付"是幂等的,"余额加 100"不是,要改成"根据流水号记账,最终余额由流水汇总得出"。
还有一种情况很容易被忽略:消息顺序。如果业务依赖消息顺序(比如订单状态流转),那么多分区消费就要做好分区键设计,把同一个订单的消息路由到同一个分区。这一点在消息量上来之后特别重要,我见过订单先收到"已取消"再收到"已支付",最后状态错乱。
5.4 排查速查表
整理一张我常用的速查表,遇到问题先按这个过一遍,能覆盖大部分症状。
| 症状 | 优先怀疑 | 排查动作 | 常见解法 |
|---|---|---|---|
| Pod 反复重启,退出码 137 | 内存超 limit | 看容器内存监控曲线是否顶到 limit | 调大 limit,或调 JVM 堆比例 |
| Pod 一直不 Ready | 就绪探针阈值太严 | 看应用实际启动耗时 | 加 startupProbe,延长启动窗口 |
| 请求 P99 抖动,均值正常 | CPU 被限流 | 看 CPU throttling 指标 | 放大 CPU limit 或去掉 limit |
| 服务间调用偶发超时 | DNS 解析慢 | 抓包看解析耗时 | 调整解析配置,减少不必要的搜索域 |
| 发布期间零星 502 | 优雅停机缺失 | 看是否有 preStop | 加 preStop 和信号处理 |
| 获取连接超时 | 连接池超配或泄漏 | 比对应用总连接数和数据库上限 | 重算连接池大小,开启泄漏检测 |
| 缓存命中率骤降 | 配置漂移或 key 变更 | 对比新旧版本配置 | 统一配置源,加灰度 |
| 消息堆积 | 消费能力不足或消费卡住 | 看消费者线程状态和 TPS | 扩消费者,或优化单条处理耗时 |
| 中间件地址解析不到 | 注册中心与就绪探针不同步 | 看注册列表与 Ready 实例是否一致 | 注册动作绑定就绪探针 |
这张表放在团队文档里,新人遇到问题先自查一遍,能省下大量沟通成本。
6. 成本、稳定性与团队协作
6.1 资源利用率与账单
容器化之后账单结构会发生变化,从"买几台大机器"变成"按资源规格付费"。这时候资源利用率就成了一个真金白银的指标。
我见过最普遍的现象是 request 设得远高于实际用量。原因很简单:设低了怕出事,设高了只是多花钱。一个服务的实际 CPU 用量是 200m,request 却设了 1 核,整个集群的 request 总量是实际用量的三四倍,等于花了三四倍的钱。
优化路径是分步走的。第一步,先采集两周的实际用量数据,看 P95;第二步,把 request 按 P95 调整,limit 保留充足余量;第三步,对有明显波峰波谷的服务配上弹性伸缩,把低谷期的副本降下来。这一步做完,通常能省 30% 到 40% 的成本,而且不会影响稳定性。
但要注意,弹性伸缩不是万能的。伸缩本身有延迟(从检测到指标超标,到新 Pod 就绪接流量,通常要 30 秒到 2 分钟),对秒级突发的流量无能为力。所以核心服务还是要保持一定的常驻副本,弹性伸缩只负责应对可预测的波峰。
6.2 中间件是谁的活儿
这是个组织问题,但会直接影响技术方案。如果中间件完全由业务团队自己维护,那大概率会出现"每个团队各搭一套缓存"的碎片化局面,最后没人知道全公司到底有多少套中间件在跑。如果中间件完全由一个平台团队统一维护,那业务团队的定制需求就排不上队,被迫绕开平台自己搞。
我见到过效果比较好的一种分工:平台团队负责中间件的"形态"(包括选型、部署、升级、容量基线、监控大盘),业务团队负责"用法"(连接池大小、超时参数、key 设计、幂等逻辑)。前者管的是"这套东西能不能稳定运行",后者管的是"我们怎么用它不把自己的服务搞挂"。边界清晰,责任也好追。
配套的机制是两个:一是中间件接入评审,新服务要用某个中间件,必须提交一份说明,写清楚连接数上限、超时设置、降级方案;二是季度容量复盘,把所有服务的中间件用量拉出来看一遍,找出资源浪费和潜在风险。
我个人在实际操作中的体会是,容器和中间件这两件事,技术难度其实都不算高,难的是让它们在一个团队里形成一致的规范并且长期执行。工具能解决的问题都是小问题,人和流程带来的问题才是真正消耗精力的地方。我现在的习惯是,每上一个新服务,先花半小时把连接数上限、资源规格、探针参数、降级开关这四项写成一张纸,跟相关的人对一遍,比事后排障省事得多。