千万QPS架构演进:从VM到容器的确定性之路

发布时间:2026/9/6 23:53:34
千万QPS架构演进:从VM到容器的确定性之路 千万 QPS 架构系列讲到第 218 讲话题落到“容器化从 VM 到容器”。很多人看到这个题目第一反应是“这还有什么好讲的容器不就是比虚拟机更轻的虚拟化吗”但如果你真的在千万 QPS 的业务场景里做过架构演进就会知道从 VM 到容器真正改变的不是部署形态而是整个系统的成本结构、故障边界和运维模型。我这里想先给出一个贯穿全文的判断千万 QPS 架构下的容器化不是为了省几台机器而是为了获得更高的确定性——让调度更快、让故障半径更小、让扩容从“采购流程”变成“一条命令”。如果你只是在小业务里用 Docker 跑几个服务可能体会不到这种确定性有多重要但当你面对流量洪峰、发布频率和故障恢复速度的压力时容器化几乎是必经之路。这篇文章我不会只讲“VM 和容器的十个区别”这类入门内容。我会从架构视角出发讲清楚三件事第一VM 在高并发场景下到底卡在哪里第二容器为什么能解开这个结以及它的代价是什么第三真实业务做容器化改造时按什么顺序、避开什么坑、需要补哪些工程能力。1. 先搞清楚千万 QPS 场景下VM 架构的瓶颈到底在哪里1.1 不是性能不够而是“确定性与弹性”不够很多团队在没有遇到大规模流量之前会觉得虚拟机用得很好。CPU、内存、网络隔离看起来都很完善稳定性也不错。那问题出在哪出在“峰值的不可预测性”和“扩容的线性成本”上。假设你的业务平时只有几万 QPS某天运营活动突然把流量推到千万 QPS。这时候你面对的问题清单是这样的现有 VM 集群的 CPU 水位还能扛多久需要新增多少台 VM每台 VM 从申请、初始化、部署代码、挂载监控到加入负载均衡需要多长时间如果扩容脚本写得不够自动化是不是需要运维同学半夜手动点一圈扩容之后流量回落这些 VM 是不是还得继续付费从工程经验看虚拟机的扩容链路里最慢的环节通常不是机器启动本身而是“初始化”和“接入”的过程。一个虚拟机实例创建之后要装系统、配网络、装 Agent、拉代码、启动进程、跑健康检查、注册到服务发现这一串动作往往需要分钟级甚至十分钟级。而这种速度在流量曲线陡峭上扬的时候是跟不上的。更麻烦的是VM 的资源隔离是“硬隔离”一台 VM 的规格往往在创建时就固定了。流量涨了你不能只给某个容量的进程加 200MB 内存你得再开一台机器或者迁移到更大规格的实例。这就导致资源利用率很难做得很满。经常出现的情况是每台 VM 都预留了 30%-40% 的缓冲区但这些缓冲在大部分时间都是空闲的真正的流量峰值来了又觉得哪里都不够。1.2 故障半径一台 VM 挂掉和几十个进程优先级挂掉处理方式完全不同还有一个容易被低估的点故障半径。一台物理机上如果跑了多个 VM某个 VM 出了故障物理机可能还能兜住。但 VM 里的故障往往是“整机不可用”——进程崩溃、内核 panic、负载高到 SSH 都连不上你只能重启或重建。重启一台 VM意味着上面的所有进程一起重启恢复时间以分钟计。在千万 QPS 场景下故障恢复时间每多一分钟损失都是巨大的。如果业务层没有做好重试、降级和熔断一次非预期的 VM 故障就可能引发雪崩。所以VM 时代的高并发架构并不是不能支撑千万 QPS而是支撑得“很累”。你需要在容量规划上留足裕量在扩容速度上接受分钟级延迟在故障处理上依赖更厚重的高可用设计。你可以用很多办法把这种架构做得很好但每加一层复杂度维护成本就会非线性上升。2. 容器到底改变了什么不是“轻量级 VM”而是一套新的运行哲学2.1 镜像、进程隔离、命名空间容器比 VM 少了什么又多了什么先做一个最简单的对比。虚拟机虚拟的是硬件所以每个 VM 里需要有一个完整的操作系统内核容器虚拟的是操作系统多个容器共享宿主机的内核只是通过 Namespace 和 Cgroups 做资源隔离和视图隔离。这个差别的直接结果就是容器启动非常快。因为启动容器本质上是启动一个进程组而不是启动一个系统。从实践来看一个 Java 服务容器从拉取镜像到 Ready通常也就是几秒到十几秒的状态而一台 VM 从冷启动到业务就绪往往需要几十秒到几分钟。不过这里必须说清楚容器不是“更轻的 VM”两者不适合直接做替换式对比。容器的隔离级别比 VM 弱同一个宿主机上的多个容器共享内核一旦内核出问题影响的不是单个容器而是整个宿主机上的所有容器。这意味着在千万 QPS 架构里容器化之后反而需要更关注宿主机的稳定性和内核兼容性。容器真正多出来的东西是镜像。镜像把代码、运行时、依赖、配置“打包”成了一个不可变单元。过去在 VM 上部署应用最怕环境漂移生产环境的 JDK 版本和测试环境不一致某个动态库少装了一个某条 cron 忘了配置。容器镜像把这些问题基本抹平了。只要镜像能在测试环境跑起来在任意一台装了容器运行时的机器上它都能以同样的方式跑起来。2.2 容器调度从“人工找机器”到“声明式地告诉集群我要跑什么”VM 时代你要部署一个服务基本流程是找运维要一台机器SSH 上去装环境拉代码启动进程然后记下 IP配到负载均衡或注册中心里。容器时代尤其是用了 Kubernetes 之后你做的是“声明式部署”你只需要描述清楚我要多少个副本、用什么镜像、需要多少 CPU 和内存、暴露哪些端口、健康检查怎么探集群会自动把 Pod 调度到合适的节点上并持续保证期望状态。这两种模式的差异用一句话概括就是VM 时代是“找机器、装环境、跑服务”容器时代是“描述需求、交给调度器解决”。在千万 QPS 架构里这个差异非常关键。因为你已经不可能像管理几十台机器那样一台一台地手工维护。你维护的是一个集群一个抽象的资源池。新增流量时你调整副本数宿主机故障时控制平面自动把 Pod 重新调度发布新版本时滚动更新的策略由平台保证。整个过程的“可预期性”远远好于人工操作。2.3 容器化不等于 Kubernetes但千万 QPS 通常绕不开编排单机版的 Docker只能解决镜像分发和单机运行的问题解决不了跨机器的调度、服务发现、存储编排、配置管理和故障自愈问题。要支撑千万 QPS容器化通常要配套容器编排平台。Kubernetes 是当前最主流的选择但注意它不是一个开箱即用的高并发平台。默认配置的 Kubernetes在千级 QPS 的流量下可能已经够用但到了万级、十万级、百万级 QPS你需要调的东西就非常多了kube-proxy 模式、DNS 并发、etcd 性能、节点资源预留、Pod 数量上限、镜像拉取策略、监控指标采集频率等等。这些细节后面我会专门展开讲。所以在开篇我想先打破一个观念很多人以为“容器化改造”就是把应用打成 Docker 镜像再写几个 YAML 文件往集群里一扔。这种理解只完成了 20%剩下 80% 的工程量在稳定性、可观测性、安全性和运维体系上。3. 从 VM 到容器的底层机制镜像、命名空间与上午架构的关系3.1 镜像分层为什么容器化能让发布变得更快、更可控容器镜像的分层设计可能比很多人想象中重要得多。一个典型 Java 服务镜像大概分这么几层基础系统层比如带有 glibc、时区信息、CA 证书的 Linux 根文件系统JRE 层应用依赖层jar 包应用配置层。打包时每一层都可能被缓存并被多个镜像复用。这个设计带来的好处很直接发布新版本时如果只改了应用代码那其他层都不变构建和推送只需要处理最上面的一层拉取时同理宿主机上如果已经有基础镜像和依赖层只需要下载新增的那一小层。配合上镜像加速器或离线镜像仓库发布速度会快很多。在千万 QPS 架构里发布速度意味着什么意味着你能更快地完成故障修复。线上出了问题你改了一行代码理论上两三分钟内新镜像就能构建完并滚动上线。而如果你还在用 VM 方式可能要先把包传到所有机器再逐台重启进程整个过程不仅慢还容易因为机器环境差异出现“这台成功、那台失败”的局面。3.2 Cgroups 与资源视图为什么容器能更精细地装箱Cgroups控制组是 Linux 内核提供的资源限制能力可以限制、记录和隔离一组进程对 CPU、内存、磁盘 I/O、网络的资源使用。容器的资源配额底层就是靠 Cgroups 实现的。在 VM 时代CPU 和内存规格通常是固定的、不连续的你要么用 4C8G要么用 8C16G很少能精确到“这个服务只需要 1.5 个 CPU900MB 内存以后还能动态调”。但容器化之后资源配额可以更细粒度地设置同一个宿主机上可以混布不同资源需求的容器装箱率会比传统 VM 模式高很多。从成本角度看这可能是容器化为数不多能直接算出来的好处。但换一个角度看粒度变细也带来了新的风险你以为限了 512MB 内存实际 JVM 的堆外内存、线程栈、Metaspace、Direct Memory 加起来可能轻松超过配额结果容器被 OOMKilled。应对办法也很明确做容器化改造时不要只设置内存上限还要配合 JVM 参数调整、HeapDump 采集和内存监控。3.3 网络模型的变化IP 变得“不值钱”服务发现变得尤为重要VM 时代每台 VM 有一个相对稳定的 IP你的监控系统、日志系统、白名单机制几乎都是围绕 IP 设计的。你可以通过 IP 快速判断“这个请求是哪个机器处理的”也可以用 IP 来配置数据库访问白名单。但容器化的世界IP 是临时资源。Pod 可以被销毁、重建、被调度到其他节点上IP 会随之变化。如果你还在容器里用“固定 IP 手动配置”的思维做网络规划会非常痛苦。所以容器化之后服务之间的调用关系不再依赖“知道对方的 IP”而是依赖“服务名 服务发现”。Kubernetes 内部的 Service 和 DNS 解决了大部分流量路由问题服务之间的调用走的是 Service DNS 或 Service Mesh。过去那种“出问题我先看哪台机器”的方式会变成“先看服务实例列表里有哪些副本日志集中在什么地方”。这个变化看着简单实际影响深远。如果你的团队还停留在“SSH 到 VM 上 tail 日志”的排查习惯容器化之后会非常不适。日志必须集中采集指标必须按服务维度聚合链路追踪必须成为标配。否则你面对几百个随时变化的 Pod根本没有办法做问题定位。4. 千万 QPS 容器化的关键工程点从“能用”到“稳如磐石”4.1 资源配额与 JVM容器世界里最容易翻车的地方很多从 VM 迁移到容器的 Java 团队遇到的第一个坑就是内存。在 VM 上跑得好好的 Java 应用放进容器里却经常被 OOMKilled。原因也不复杂JVM 默认的 MaxHeapSize 是根据宿主机内存来算的不是根据容器配额。如果你不给 JVM 显式设置 -Xmx它可能会申请超过容器限额的内存。JVM 的堆外内存包括 Metaspace、线程栈、Direct Buffer、JIT 编译器内存这些在容器里都属于容器配额的一部分。解决思路是显式设置 JVM 堆和容器内存配额的关系通常建议 -Xmx 设置为容器内存上限的 50%-70%。使用 Java 10对于较新的 JDK容器可以识别 CPU 限制如果你还在用 Java 8建议升级到 Java 8u191并配合 -XX:UseContainerSupport。预留 25%-30% 的内存给非堆内存避免因为 JVM 动态扩展或 JIT 编译导致 OOM。如果原始业务的 QPS 比较高一定要先做一次压测观察容器在限制下的 Full GC 频率、堆使用率和响应时间曲线再决定合适的配额和副本数。4.2 健康检查、优雅退出与滚动更新发布不再是玄学容器化对发布流程最大的改造在于发布变成了“声明新的期望状态等待集群自我调整”。但如果你没有配置好健康检查这个过程会变成灾难。Kubernetes 里有三种探针启动探针startupProbe、就绪探针readinessProbe和存活探针livenessProbe。在千万 QPS 架构里这三种探针的配置都不可省略启动探针解决服务启动慢的问题。如果你的应用启动需要 60 秒但存活探针的 initialDelaySeconds 设成了 5 秒那容器还没起来就被杀掉了。就绪探针决定流量是否进入 Pod。新 Pod 没有 Ready 之前Service 不会把流量打进去。存活探针负责处理“进程活着但业务卡死”的情况比如死锁、线程池耗尽的死循环。实际发布时如果你只做了“杀掉旧 Pod、启动新 Pod”而没有做优雅退出可能出现存量请求被硬断造成非常明显的报错率和服务不可用。严谨的做法是配置 preStop让容器在终止前先摘除流量等几秒让存量请求处理完然后再真正停止进程。注意不要把优雅退出这一步省掉。千万 QPS 场景下哪怕只有 0.1% 的请求在发布瞬间被断开也会放大成大量业务报错。4.3 镜像仓库、并发拉取与带宽大集群扩容时的隐形瓶颈假设你有 1000 个节点某个大版本发布时所有节点需要同时拉新镜像。如果镜像很大比如 1GB而仓库出口带宽只有 1Gbps那理论上并发拉取会直接把带宽打满发布时间会被拉得非常长。常见的解法镜像瘦身尽量使用 alpine 或精简基础镜像Java 服务只带 JRE 不带 JDK。分层缓存把依赖较多的层尽量放在前面保证只有最上面的应用层需要变动。并发控制通过容器编排的配置限制集群内同时拉取镜像的节点数避免带宽打满。私有仓库 就近部署在物理区域内自建镜像仓库或者通过 P2P 分发组件减少源仓库的压力。这个问题平时不明显但大促前扩容时经常爆发。你会在控制台看到一堆 Pod 处于 ImagePullBackOff 或者 ContainerCreating 状态原因不是网络不通而是拉取镜像排队太长。4.4 日志、监控与链路追踪排查问题的方式必须升级容器化之后应用日志不能在容器内写文件了。原因很简单容器随时可能被调度走宿主机上的日志文件会随之丢失。而且多副本情况下你想看某个请求在某台机器上的日志基本没法手工“登上去找”因为实例本身可能是动态的。所以做容器化改造时日志方案必须改为“统一采集、集中存储、检索分析”。常见路径是应用把日志写到 stdout/stderr由日志采集 Agent比如 Filebeat、Fluent Bit采集到 Kafka再到 ES 或 ClickHouse或者在应用日志框架里配置为发送到统一日志服务比如 Loki、ELK 等。监控和链路追踪在这个阶段也会变成刚需。在 VM 时代你还能通过 CPU 曲线和流量曲线大致猜出瓶颈在容器时代如果连调用链都没有遇到跨服务的慢请求排查问题会非常痛苦。建议从改造的第一天就确定好监控指标的最小集请求量、错误率、响应延迟P50/P95/P99、容器 CPU/内存使用率、节点水位、发布变更记录。5. 容器化改造的真实路径不是“全量迁移”而是“分步验证流量灰度”5.1 第一步先给业务做“容器化可行性画像”不是所有业务都适合立刻容器化。我建议先做一个分类无状态服务Web 服务、API 网关、RPC 服务、消息消费者。这类业务的容器副本之间没有状态关联杀掉重启任何副本都无所谓是最适合容器化的类型。有状态服务MySQL、Redis、ES、Kafka。这类服务的数据和服务实例强绑定直接跑在普通容器里会有数据丢失风险通常需要使用 StatefulSet、PV/PVC、本地盘调度或者其他状态化方案复杂度比无状态服务高一个量级。定时任务CronJob 在 Kubernetes 里能跑但要注意并发策略和补跑机制不要多个副本同时执行同一个任务。批处理任务适合用 Job 模式跑跑完即结束比常驻服务更灵活。从改造优先级来看应该先挑无状态、流量可灰度、故障容忍度高的业务跑通整套流程。不要一上来就把核心数据库塞进容器里。5.2 第二步用“流量灰度”替代“机房整体切换”我见过很多团队做容器化改造时喜欢采用“一刀切”的方式隔离出一个专门的新集群把应用整个迁移过去等稳定了再切换流量。这种做法的风险在于如果迁移过程中出现一个你没想到的问题切换不了影响的不是某个服务而是整条链路。更稳妥的方式是“边跑边换”让容器版本和 VM 版本并行运行一段时间通过网关逐步灰度流量。比如第一阶段容器副本只接收 5% 的测试流量主要验证基础功能和链路连通性。第二阶段放出 10% 的线上流量观察错误率、延迟、GC、CPU、内存等指标是否和 VM 版本一致。第三阶段逐步扩大到 50%再 100%最后下线 VM 副本。这个思路的核心是不要用“彻底搬迁”的心态做改造要用“新老系统并存、逐步替代”的心态做改造。容器的调度、网络、存储、监控体系都需要时间磨合灰度期间暴露的问题是成本最低的学习机会。5.3 第三步补齐容器化缺少的工程能力容器和编排平台解决的是“调度和部署”问题但它不自动解决以下工程问题配置管理配置项应该用 ConfigMap / Secret 管理不能直接打进镜像。否则每次改配置都要重新构建镜像。权限控制容器内部不要以 root 用户运行业务容器要有独立的非 root 用户Dockerfile 里需要用 USER 指令切换。安全扫描镜像上线前要做漏洞扫描不信任的第三方镜像不要直接拉进生产集群。镜像安全和容器安全是两个不同维度镜像扫描解决的是打包阶段的问题运行时安全解决的是逃逸和异常访问的问题。资源隔离很多团队在 Kubernetes 里只设置 requests不设置 limits。这种配置在流量低时看不出来流量高时一个 Pod 会把整台节点打爆。合理做法是先做压测再设置 requests 和 limits并且对 CPU 与内存分别设置。备份与恢复对于有状态服务如果最终还是决定容器化必须提前设计好持久化存储和备份策略不能依赖“容器还在”这个状态。6. 千万 QPS 场景下的容器化调优默认配置跑不出高并发6.1 kube-proxy 与 iptables/ipvs连接与负载均衡能不能扛住Kubernetes 默认的 kube-proxy 模式如果是 iptables在大规模高并发场景下会遇到两个问题Service 数量、Endpoint 数量非常大时iptables 规则会非常庞大更新规则时可能造成连接闪断。iptables 的规则匹配是顺序匹配规则多了之后转发性能会有明显下降。生产环境通常建议改用 IPVS 模式。IPVS 在内核态实现了负载均衡支持多种调度算法性能比 iptables 好很多而且规则同步更稳定。切换后ClusterIP 对应的负载均衡转发能力会有明显提升。另外还有一个容易被忽略的点如果服务规模很大DNS 解析 QPS 也会非常高。Kubernetes 默认的 CoreDNS 配置在大量 Pod 并发解析时会成为瓶颈需要调整副本数、部署模式比如 daemonset和缓存参数。6.2 节点水位与调度策略不要让任何一台宿主机成为“落单热点”容器化之后宿主机故障的影响面取决于这台机器上跑了多少个业务 Pod。如果调度策略不关注 Pod 的“反亲和性”某个核心服务的所有副本可能同时落在同一台宿主机上那这台机器一挂整个服务就瞬间不可用。在千万 QPS 场景下调度策略至少要考虑核心服务多副本尽量打散到不同节点、不同可用区。混合部署低优先级任务与核心业务时要设置优先级和抢占策略避免低优先级任务抢占高优先级业务的资源。设置节点资源预留kube-reserved、system-reserved不要把节点资源 100% 分配给业务容器防止宿主机自身系统和组件异常。6.3 大促扩容容器数量多了问题也会跟着变多千万 QPS 的大促场景下系统会自动扩容出大量副本。这个时候出问题的往往不是应用本身而是容量侧的基础设施节点数增加容器集群控制面 API Server 的请求量会上升etcd 的写入 QPS 也会涨。需要提前评估集群规格必要时拆分多个集群。Pod 数量增加后监控 Agent 采集的指标数量也会增长监控系统自身需要扩容。服务发现和注册中心需要同步扩容尤其是如果你的服务仍然依赖注册中心做 RPC 路由注册中心会成为新的瓶颈。如果新的 Pod 需要挂载存储存储系统的并发能力也要提前压测避免扩容后因存储 IO 跟不上导致服务失败。简单说容器化把“机器维度”的扩容问题变成了“平台维度”的扩容问题。机器不够用变成集群不够用带宽不够用变成仓库或 DNS 不够用连接数不够变成 Service 转发或注册中心不够用。你需要用新的视角去看容量规划和压测。7. 落地前的自检清单一套可以直接拿走的容器化改造框架到这里我已经把从 VM 到容器的架构逻辑讲得差不多了。但我知道大部分人看到最后真正需要的是一个能落地执行的检查表。下面这个清单是这套方法论的核心沉淀也是我建议你在动手改造前先逐条过一遍的东西。第一层服务画像[ ] 服务是否有状态数据是否存在本地文件、本地磁盘或内存中[ ] 服务是否支持优雅退出进程收到 SIGTERM 后能否在有限时间内处理完存量请求[ ] 服务启动时间是多少是否超过 90 秒是否需要启动探针兜底[ ] 服务的内存占用峰值是多少JVM 的堆内和堆外内存是否都统计过第二层镜像与供应链[ ] 基础镜像是否精简能否去掉编译工具链、包管理器和缓存[ ] 镜像中是否存在敏感信息密码、密钥、Token[ ] 是否用了私有镜像仓库镜像上传后是否做漏洞扫描[ ] 是否配置了镜像拉取策略Always / IfNotPresent第三层编排与配置[ ] 配置是否已经从镜像中剥离能否用 ConfigMap / Secret 管理[ ] 是否设置了 requests 和 limits是否经压测验证过[ ] 就绪探针、存活探针、启动探针的阈值是否和服务真实启动时间匹配[ ] 优雅退出是否正确配置preStop 是否执行第四层可观测性[ ] 日志是否已改为集中采集能否在日志平台按 traceId 串联全部链路[ ] 监控是否覆盖请求量、错误率、P99 延迟、容器资源、节点水位和服务发现状态[ ] 是否有按服务维度而不是机器维度的告警规则[ ] 链路追踪是否已经接入核心业务链路第五层稳定性与容量[ ] 是否对核心服务做过容器资源限制下的压测[ ] 是否对镜像拉取并发、DNS 并发、Service 转发能力和注册中心容量做过评估[ ] 核心服务多副本是否配置了反亲和性避免打到同一台宿主机[ ] 大促扩容脚本是否演练过控制面是否撑得住流量洪峰这个清单看起来很长但每一项背后几乎都有真实事故在支撑。你在小规模场景里可能感受不到它们的作用但一旦流量涨到千万级任何一个“看起来不重要的配置项”都可能成为压垮系统的最后一根稻草。8. 容器化之后架构的长期方向平台能力才是真正的分水岭聊完了改造步骤和调优细节我想把视野拉远一点说一说容器化这件事对架构演进的长期意义。从 VM 到容器起初看起来只是部署方式的改变。但真正坚持做下来的团队会发现这件事的长期价值在于它逼着你把部署、配置、监控、日志、安全、权限、容量管理和发布流程全部“产品化”和“平台化”。当这些能力沉淀为平台服务之后任何团队上线新服务时不再需要重复造轮子而是直接通过平台申请资源、配置流水线、选择监控模板。到了这个阶段业务系统的性能瓶颈已经不完全取决于单机或单服务的能力而是取决于平台对资源的调度效率、对故障的恢复速度和对外部流量变化的响应速度。这也正是千万 QPS 架构能够持续演进的关键不是靠一次性的性能优化而是靠一套可以让复杂系统稳定运转的机制。从工程经验看这个平台化的过程通常不是一蹴而就的。先有一个小的容器集群跑几个非核心服务然后慢慢扩大范围接入网关、核心业务、存储层最后再建设多集群、多可用区、统一发布和弹性伸缩体系。每个阶段都有各自的坑但只要方向是对的每走一步系统的响应能力和确定性都会上一个台阶。回到文章开头那个判断容器化改造的核心价值不是“从 VM 换成容器”这件事本身而是通过容器和编排技术让整个技术团队拥有了更快的响应速度、更小的故障半径和更确定的运维流程。如果你所在的业务已经接近或超过百万 QPS并且还在为扩缩容速度、发布效率和故障恢复时间焦虑那么容器化不是一道“要不要做”的选择题而是一个“什么时候做、以什么顺序做”的工程问题。希望这篇文章能给你一个相对完整的认知框架让你在做技术决策时不至于被工具和概念牵着走。这也是我认为“千万 QPS 架构”系列里容器化这一讲最值得沉淀下来的内容。