运行时选型最常见的错误不是选错技术,而是默认往上走一层:单机能跑就上容器,容器多了就上编排,最后一套 8 个实例的系统养着一个控制面。运行时形态不是先进性的度量,它买的是"环境一致性、交付速度、扩缩能力",代价是"多一层抽象、多一层排障距离"。
判据只有一条:你要解决的问题,是不是这一层独有能解的。
1. 三层选择:各自解决什么,又引入什么
| 形态 | 独有能力 | 引入的新问题 | 谁该选它 |
|---|---|---|---|
| 裸机 / 虚机 | 直接掌控内核、IO、网络栈 | 环境漂移、交付靠手工 | IO 敏感、需要特殊内核模块 |
| 容器 | 打包一致性、秒级启动、资源视图 | 网络与存储抽象、镜像治理、内核共享 | 无状态服务、多环境交付 |
| 编排 | 声明式副本、自动恢复、调度 | 控制面开销、概念负担、排障链路变长 | 实例多、发布频繁、要自动恢复 |
判断顺序(从下往上问,任一层"不需要"就停):
1) 这台机器上的东西,会经常换版本吗? → 不会,裸机/虚机即可
2) 需要多环境一致、快速回滚吗? → 需要,容器化
3) 实例数会超过一双手,且发布是常态吗? → 是,才考虑编排
关键:每一层都只能解决它自己那一层的问题。容器不会让你的服务更有弹性——弹性来自无状态与快速启动;编排不会让你的发布更安全——安全来自可回滚与灰度。
2. 容器的四个收益与四个代价
| 收益 | 前提条件 | 代价 | 表现 |
|---|---|---|---|
| 环境一致 | 镜像自包含、依赖不外挂 | 网络与存储变复杂 | 端口映射、overlay、卷挂载 |
| 快速交付 | 构建流水线可靠 | 镜像治理 | 标签混乱、体积膨胀、基底过期 |
| 资源隔离 | 正确设置 limit | 调试变难 | 容器内看不到宿主,工具缺失 |
| 扩缩 | 无状态、启动快 | 内核共享风险 | 一个容器打爆页缓存,全体受损 |
FROM eclipse-temurin:17-jre
COPY app.jar /app/app.jar
# 关键决策:进程 PID 1、时区、JVM 感知容器内存
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75", "-jar", "/app/app.jar"]
# 隔离的开关在这里,不在容器里
services:
app:
mem_limit: 1g
cpus: 1.5
pids_limit: 512
-XX:MaxRAMPercentage 这类参数的存在,本身就是"抽象漏了"的证据:JVM 曾经按宿主内存算堆,容器化后必须重新教它看 cgroup。任何依赖"看到真实硬件"的组件,容器化都要额外验证。
3. 什么时候不该上容器
| 场景 | 症状 | 更合适的做法 |
|---|---|---|
| IO 敏感 | 磁盘吞吐打满,容器层叠加放大延迟 | 裸机 + 直连设备 |
| 特殊内核模块 | 需要加载自定义模块、调整内核参数 | 虚机或裸机 |
| 团队无运维能力 | 出问题只会重启 | 虚机 + 脚本化部署 |
| 单实例常驻小服务 | 一年不重启、不扩容 | 物理机 + systemd |
| 离线批处理集群 | 任务排队、资源独占 | 队列 + 裸机 |
判断容器是否值得,问三个问题:版本更换频率、环境数量、是否需要隔离故障域。三个都低,容器只是给运维加了一层需要学习的东西。
4. 编排的门槛判据
| 判据 | 未达标的迹象 | 达标的迹象 |
|---|---|---|
| 实例数量 | 一只手数得过来 | 多服务 × 多副本,手工登记已不可行 |
| 发布频率 | 月级 | 周级或更高,需要灰度与回滚 |
| 故障恢复要求 | 人工介入可接受 | 要求自愈、自动重调度 |
| 团队规模 | 1–2 人全包 | 有明确的分工与值班 |
荒谬场景的最小反例:
一台 4C8G 的机器上跑单节点编排,控制面吃掉约四分之一资源,
换来的是——一个本来用 systemd 就能管好的进程。
判断:编排的收益随实例数增长,成本却是固定的(控制面 + 学习曲线)。
这就是判据的本质:编排是固定成本 + 规模收益。实例数没到规模,成本那一项永远算不过来。
5. 调度策略:requests、limits 与抢占
| 配置 | 对调度的影响 | 对稳定性的影响 |
|---|---|---|
| requests 设小、limits 设大 | 调度器以为很空闲,超卖严重 | 高峰时集体限流与 OOM |
| requests = limits | 可预测、可装箱 | 资源利用率偏低 |
| 不设 limits | 无上限,邻居互相伤害 | 一个服务拖垮整台机器 |
| 亲和性过强 | Pod 无法调度,长期 Pending | 扩容失效,故障域集中 |
resources:
requests: { cpu: "500m", memory: "512Mi" } # 调度依据
limits: { cpu: "2", memory: "1Gi" } # 运行上限
# CPU 超限 = 被限流(变慢),内存超限 = 被杀(OOMKilled),两者故障表现完全不同
这条差别值得记住:CPU 超限是降速,内存超限是死亡。所以内存 limit 应该按实测峰值留余量,CPU limit 则常常干脆不设,让它在争抢中自然排队。
6. 有状态服务与发布方式的匹配
| 服务类型 | 容器化边界 | 关键约束 |
|---|---|---|
| 无状态 API | 完全适合 | 启动快、健康检查准确 |
| 有状态(数据库) | 谨慎,需卷与备份方案 | 迁移慢、恢复依赖快照 |
| 消息中间件 | 可容器化,但数据目录必须持久 | 卷性能直接决定吞吐 |
| 定时任务 | 适合,但要防重复执行 | 需要分布式锁或单例语义 |
| 发布方式 | 对基础设施的要求 | 适用形态 |
|---|---|---|
| 滚动 | 多副本 + 就绪探针 | 编排环境 |
| 蓝绿 | 两套环境 + 流量切换 | 编排或负载均衡 |
| 金丝雀 | 按比例分流能力 | 需要网关支持 |
| 原地重启 | 可接受停机 | 单机、无编排 |
# 发布安全性的最小前提:探针不说谎
readinessProbe:
httpGet: { path: /health/ready, port: 8080 }
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
httpGet: { path: /health/live, port: 8080 }
failureThreshold: 3
探针设计错了,滚动发布就变成"一边启动一边被杀"。就绪探针回答"能不能接流量",存活探针回答"要不要重启",两者混用是自愈变自杀的常见原因。
7. 超卖、隔离与可观测
| 问题 | 机制 | 观测方式 |
|---|---|---|
| CPU 争抢 | cgroup 配额与限流 | 容器 CPU throttling 指标 |
| 内存超卖 | OOM Killer 按分值与用量选目标 | OOMKilled 事件、重启计数 |
| 邻居效应 | 页缓存、磁盘 IO、网络带宽共享 | 宿主级 IO 等待与丢包 |
| 日志收集 | 容器内文件随生命周期消失 | 统一采集到外部存储 |
| 指标维度 | 容器名会变,实例会漂 | 以服务 + 版本打标 |
# 容器里看不到宿主,排障要回到宿主视角
docker stats --no-stream
cat /sys/fs/cgroup/memory.max # cgroup v2;v1 为 memory.limit_in_bytes
kubectl top pod -n app
可观测的第一原则:指标标签不要用实例名或容器 ID——它们会变,一变历史曲线就断。用"服务 + 版本 + 区域",实例身份只作为附加维度。
8. 成本结构与迁移路径
| 成本项 | 固定/变动 | 容易忽略的部分 |
|---|---|---|
| 控制面开销 | 固定 | 每个节点上的常驻进程与内存预留 |
| 闲置资源 | 随规模 | requests 虚高导致的装箱浪费 |
| 镜像存储与拉取 | 随版本数 | 大镜像让扩容变慢 |
| 人力 | 固定 | 学习与排障能力的建立周期 |
迁移路径(每步都可回退):
1) 无状态服务容器化,仍用脚本或 compose 部署
2) 引入镜像流水线 + 版本化标签 + 回滚演练
3) 有状态组件评估:能外置的(缓存、对象存储)先外置
4) 实例数与发布频率都过线后,再上编排
5) 最后才谈自动扩缩与多集群
顺序反了会怎样:先上编排再补镜像治理,等于把"环境不一致"从机器级搬到镜像级,问题没消失,只是更难查。
9. 排错
| 现象 | 原因 | 处理 |
|---|---|---|
| 容器频繁重启 | 存活探针过严或内存 limit 过低 | 看退出码:137 多为 OOM |
| 启动慢、流量打不进 | 就绪探针早于应用就绪 | 加启动探针或延长初始延迟 |
| 容器内 CPU 用不满 | CPU limit 限流 | 去掉 limit 或按峰值设 |
| 写入变慢但磁盘不满 | 卷性能或 IO 争抢 | 看宿主 IO 等待 |
| Pod 长期 Pending | 资源请求超出节点可分配 | 查 requests 与亲和性 |
| 日志缺失 | 采集器未覆盖或容器已销毁 | 改为采集到外部存储 |
10. 边界
| 场景 | 不该做的事 | 理由 |
|---|---|---|
| 单机小服务 | 上编排 | 固定成本永远收不回 |
| 长期稳定、不扩容的系统 | 为"先进"而容器化 | 收益为零,风险为正 |
| 离线批处理 | 用编排做任务队列 | 调度语义不匹配,排队与重试难控 |
| 强 IO 依赖组件 | 叠多层存储抽象 | 每层都加延迟 |
| 团队无运维能力 | 引入需要专人维护的平台 | 故障时会失去排障能力 |
一句话收束:运行时形态应该由"故障恢复与交付节奏"的需求拉动,而不是由技术潮流推动。选低一层的代价通常是手工,选高一层的代价通常是复杂度——哪种代价你更能长期承担,答案就出来了。
11. 自测
- 容器的"环境一致"收益,依赖哪个前提才成立?
- 为什么 CPU 超限和内存超限的故障表现完全不同?
- 编排的成本是固定的还是随规模变化的,这如何影响门槛判据?
- 就绪探针与存活探针混用,会把自愈变成什么?
- 有状态服务容器化,最先要解决的是卷、备份还是迁移?
- 迁移路径为什么强调"先无状态、后有状态、最后编排"?