ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

从VM到容器:千万QPS架构下容器化改造的关键路径与实践

从VM到容器:千万QPS架构下容器化改造的关键路径与实践 在千万级 QPS 的架构演进中很多团队会遇到一个很现实的问题业务代码明明已经拆成了微服务但部署和交付却还是老一套。开发环境用 VMware 开一台虚机测试环境手工装 JDK生产环境上线要预留整整一个下午。真正把整个架构压垮的往往不是接口性能而是环境差异、扩容速度、资源利用率这几座大山。这篇文章要谈的就是千万 QPS 架构系列第 218 讲的核心主题从 VM 到容器。我会先对比虚拟机和容器在架构层面的本质区别再讲清楚为什么高并发场景下容器能成为更优解然后给出业务系统容器化改造的具体路径包括镜像制作、Kubernetes 部署、镜像安全和容器安全。读完你会得到一个明确判断容器化不只是换一种部署方式它改变的是整个架构演进和团队协作的底层逻辑。1. 先搞清楚千万 QPS 架构为什么绕不开容器化先看一个真实场景。假设你负责一个日活千万的内容平台核心链路包括用户服务、内容服务、推荐服务、支付服务。系统早期是单体应用一台物理机部署完所有代码数据库单独跑一台机器Redis 和消息队列各占一台。那时候一台机器挂了运维手工切流量业务影响范围可控。随着业务增长单体应用拆成几十个微服务问题开始暴露。每个微服务依赖不同的 JDK 版本、不同版本的第三方组件、不同的配置文件。测试环境用的是 CentOS 7.4生产环境却是 CentOS 7.6一个小版本差异导致字节码兼容问题线上直接报错。扩容更是痛苦新环境要从装机开始装系统、配网络、装中间件、部署代码一套流程下来快也要半小时慢则半天。这就是典型的高并发架构演进困境。当 QPS 从十万量级走到千万量级系统瓶颈已经不只是应用代码本身而是整个基础设施的弹性能力。虚拟机能解决资源隔离问题但虚拟机的粒度太大、启动太慢、资源占用太高。容器化则把隔离和调度的粒度从操作系统级别压缩到进程级别让一套物理机资源可以被大量服务共享。千万 QPS 架构的核心不是某一台机器能扛多少流量而是整个集群能够在流量高峰到来之前快速扩容在流量回落后迅速缩容。容器化恰恰提供了这种能力。所以在实际的架构方案里容器化不是可选项而是高并发系统的必选项。2. 从物理机到虚拟机虚拟化解决了什么虚拟机不是新概念。最早的大规模部署应用直接跑在物理机上这种方式的问题在于资源边界固定一台物理机被某个应用独占即使应用只用了 10% 的 CPU其他应用也无法复用。为了提升资源利用率虚拟化技术开始普及。虚拟机的核心是 Hypervisor也就是虚拟机监视器。它运行在物理硬件和虚拟机之间负责把物理资源抽象成多份虚拟资源。VMware 的 ESXi 属于 Type 1 虚拟化直接跑在裸金属上VMware Workstation 和 Oracle VM VirtualBox 属于 Type 2 虚拟化跑在宿主操作系统之上。每一台虚拟机里都有一套完整的操作系统包括内核、系统库、文件系统、应用进程。从资源利用角度看虚拟机相比物理机是巨大进步。一台 32 核 128GB 的物理服务器可以切成多台 4 核 16GB 的虚拟机不同团队可以共用同一台物理机团队之间通过虚拟机做隔离。但这种隔离付出了相当大的资源代价每台虚拟机都要运行一个完整操作系统操作系统本身要占用内存和 CPU 资源启动一台虚拟机需要几十秒甚至几分钟因为内核要初始化、系统服务要启动、应用要加载。虚拟机适合什么场景如果你需要运行多个不同操作系统的环境比如一台物理机上既要跑 Windows 又要跑 Linux虚拟机是最好的选择如果你需要严格的内核级隔离虚拟机也更有优势。但在高并发微服务架构里虚拟机的痛点非常明显资源浪费严重100 个微服务就意味着 100 个操作系统实例在空转启动速度太慢无法支撑秒级弹性扩容。3. 容器到底是什么从 VM 到容器的关键跳跃容器和虚拟机的核心区别在于虚拟机虚拟的是硬件容器虚拟的是操作系统。容器与宿主机共享同一个操作系统内核但它通过 Linux 内核的 Namespace 和 Cgroups 技术实现了进程级别的隔离和资源限制。Namespace 的作用是让容器里的进程看到独立的系统视图。每个容器可以拥有自己的 PID、网络、文件系统、用户等命名空间。比如 PID Namespace 使容器内进程的 PID 从 1 开始它看不到宿主机上其他进程Mount Namespace 让容器拥有自己的根文件系统容器里执行的ls /看到的是镜像里的目录不是宿主机的根目录。Cgroups 的作用是限制和统计资源使用。通过 Cgroups可以给每个容器设置 CPU 配额、内存上限、磁盘 IO 和网络带宽。如果没有 Cgroups一个容器的死循环会拖垮整台物理机有了 Cgroups容器最多只能用分配给它的那部分资源。很多人会问容器既然与宿主机共享内核那隔离性是不是比虚拟机差答案是肯定的。虚拟机有独立内核容器共用内核如果宿主机内核出现漏洞容器都可能受影响。这也是为什么容器安全被单独强调后面我会专门讲镜像安全和容器安全。容器的启动为什么快因为容器不需要启动操作系统它只是创建几个 Namespace 和 Cgroups然后启动进程。一个 JAR 包镜像从docker run到应用接受请求正常场景下只需要几秒到十几秒。这种启动速度是千万 QPS 架构实现弹性扩容的基础。4. VM 与容器的深度对比一张表看清差异为了帮助你快速理解 VM 和容器的差异我整理了一张关键对比表。它适用于架构评审和技术选型阶段。维度VM虚拟机容器隔离级别内核级隔离每台 VM 有独立内核进程级隔离共享宿主机内核启动速度几十秒到几分钟毫秒级到秒级资源占用每台 VM 需完整操作系统内存占用高只包含应用和依赖库镜像体积小密度一台物理机通常运行几台到十几台 VM一台物理机可运行几十到上百个容器内核依赖不依赖宿主机内核版本依赖宿主机内核兼容性安全边界独立内核隔离更强共享内核隔离相对较弱迁移性模板导出导入通常需要停机镜像拉取即运行环境一致性高适用场景多操作系统需求、强隔离要求微服务、弹性扩容、持续交付典型工具VMware、VirtualBox、KVMDocker、containerd、Podman从这张表可以看出容器并不是完全替代虚拟机。在需要严格多租户隔离的公有云场景反而要使用裸金属服务器或者虚拟机来承载容器也就是容器套在虚拟机里跑。但从业务系统容器化改造的角度看应用层服务的部署方式会逐步从虚拟机迁移到容器。还需要注意一个混淆点很多人把 Docker 等同于容器。Docker 只是容器的一种实现容器运行时的标准化接口是 OCIOpen Container Initiative。常见的容器运行时包括 containerd、CRI-O、Docker Engine。在 Kubernetes 集群里早期普遍使用 Docker 作为运行时现在更多推荐 containerd因为它在 Kubernetes 集群内更轻量、更稳定。5. 业务系统怎么容器化改造从 VM 迁移到容器的完整路径业务系统容器化改造是实际操作中最难的一环也是很多开发团队最先踩坑的地方。很多人以为把项目写一个 Dockerfile构建成镜像推到镜像仓库然后用docker run跑起来就算容器化。这个理解太表面了。在千万 QPS 架构下容器化改造要考虑到应用状态、日志、配置、网络、存储、健康检查和弹性伸缩等多方面问题。这里以一个典型的 Java 微服务为例完整走一遍容器化改造流程。5.1 梳理应用依赖与环境差异第一步明确应用运行需要什么。Java 应用需要 JRE通常会用到环境变量、配置文件、日志目录。如果你之前的配置是写在application.properties里里面包含数据库地址、Redis 地址、各种中间件账号密码说明配置和代码没有分离。容器化之后这些配置应该通过环境变量或者 ConfigMap 注入否则镜像一旦被推送配置就写死在里面了换个环境还得重新构建镜像。建议做法是将所有环境差异项抽取为环境变量在启动脚本里替换或者使用 Spring Cloud Config、Apollo 这类配置中心统一管理。容器化改造的另一个价值就在于此它倒逼你把配置管理规范起来。5.2 编写 Dockerfile以 Spring Boot 项目为例一个基础但可用的 Dockerfile 如下# 文件路径Dockerfile # 第一阶段构建 FROM maven:3.8-openjdk-11 AS builder WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn clean package -DskipTests # 第二阶段运行 FROM openjdk:11-jre-slim WORKDIR /app COPY --frombuilder /app/target/*.jar app.jar ENV JAVA_OPTS EXPOSE 8080 ENTRYPOINT [sh, -c, java $JAVA_OPTS -jar app.jar]这段 Dockerfile 使用了多阶段构建。第一阶段用 Maven 镜像编译项目第二阶段用精简 JRE 镜像运行。这样最终镜像不包含 Maven、编译器和源代码体积会小很多。需要强调的是上面用的是openjdk:11-jre-slim作为基础镜像。但在生产环境中更推荐使用 Eclipse Temurin、Liberica 或 Adoptium 提供的镜像因为它们维护更规范安全更新更及时。基础镜像选择是容器化安全的重要一环后面会展开。5.3 构建镜像并推送私有仓库构建镜像的命令很简单docker build -t registry.example.com/shop/user-service:v1.0.0 .最关键的是registry.example.com/shop/user-service:v1.0.0这个镜像命名它决定了镜像的归属。生产环境不要使用 Docker Hub 公共仓库建议搭建私有镜像仓库企业内部使用 Harbor 是比较普遍的做法。Harbor 不仅支持私有镜像存储还内置了镜像扫描、签名和权限管理对生产环境更友好。构建完成后推送到仓库docker push registry.example.com/shop/user-service:v1.0.05.4 处理日志与持久化数据容器的一个重要特性是临时性。容器删除后容器内写入的文件也会丢失。如果应用把日志写到容器里的/logs目录容器重启或重新调度后日志就没了。生产环境应该把日志输出到标准输出由容器运行时统一收集或者挂载到宿主机目录再由 Filebeat 等工具采集到 Elasticsearch。对于需要持久化的数据比如上传文件、数据库数据必须使用持久化卷。在 Kubernetes 中对应的是 PersistentVolumeClaim它可以绑定到云硬盘、Ceph、NFS 等存储后端。需要注意的是如果应用本来就是无状态的那么不需要持久化卷容器化改造的目标之一就是把有状态的部分尽量抽离出来交给数据库、Redis 这类专用组件让应用服务保持无状态。5.5 JVM 参数与容器资源限制Java 应用在容器里运行有一个非常经典的坑JVM 默认的堆内存设置。早期的 JVM 不能正确识别 Cgroups 限制它会根据宿主机物理内存大小来计算默认堆内存。比如宿主机有 128GB 内存而容器限制为 2GBJVM 却按宿主机内存设置堆大小结果容器被 Cgroups 杀掉应用不停 OOM。解决办法有两个第一个使用支持容器内存感知的 JDK 版本Java 10 之后 JVM 默认能识别 Cgroups 限制第二个显式设置 JVM 参数比如-Xmx512m -Xms512m并配合容器内存限额。推荐使用JAVA_OPTS环境变量在 Dockerfile 中声明例如ENV JAVA_OPTS-Xms512m -Xmx512m -XX:MaxMetaspaceSize256m同时设置 Kubernetes 的资源限额确保容器内存不超过限制。还有更精细的方案JDK 10 以后的版本支持-XX:MaxRAMPercentage75.0这类参数让 JVM 根据容器配额自动计算堆大小。但实际项目中固定堆大小更容易把控特别是做压测和容量规划时数值确定结果可复现。5.6 健康检查与优雅下线容器编排平台会通过健康检查来判断容器是否存活、是否就绪。传统虚拟机部署时运维通过端口探测判断应用是否正常容器环境下这个机制要升级为 HTTP 接口探针。Spring Boot 自带 Actuator可以暴露健康检查端点。Kubernetes 的配置一般如下# 文件路径user-service-deployment.yaml apiVersion: apps/v1 kind: Deployment metadata: name: user-service namespace: production spec: replicas: 6 selector: matchLabels: app: user-service template: metadata: labels: app: user-service spec: containers: - name: user-service image: registry.example.com/shop/user-service:v1.0.0 ports: - containerPort: 8080 readinessProbe: httpGet: path: /actuator/health/readiness port: 8080 initialDelaySeconds: 20 periodSeconds: 10 livenessProbe: httpGet: path: /actuator/health/liveness port: 8080 initialDelaySeconds: 60 periodSeconds: 30 resources: requests: cpu: 500m memory: 1Gi limits: cpu: 1 memory: 2Gi这里有两个探针readinessProbe判断容器是否还能接收流量如果接口返回 5xxKubernetes 就把容器从 Service 的 Endpoints 里摘除livenessProbe判断容器是否需要重启如果接口连续失败Kubernetes 会杀掉容器并重新创建。在实际项目中readiness 和 liveness 不要共用同一个接口因为它们的语义不同。readiness 应该反映应用的依赖是否就绪比如数据库连接、Redis 连接liveness 只反映应用进程是否还活着。如果把两者混在一起Redis 短暂故障导致 readiness 失败应用会被摘流量但 liveness 不应该因为这个原因重启应用。5.7 Deployment 与 Service 编排当镜像构建完毕且探针配置妥当后下一步就是通过 Kubernetes Deployment 来运维容器。Kubernetes 的调度核心是 Pod。Pod 是 Kubernetes 的最小调度单元一个 Pod 里可以有一个或多个容器。Deployment 负责管理一组相同的 Pod它保证了期望的副本数量、支持滚动更新、支持回滚。Service 负责提供稳定的访问入口。Pod 的 IP 是动态变化的Service 通过标签选择器找到一组 Pod并提供一个稳定的 ClusterIP 和 DNS 名称其他服务可以通过user-service.production.svc.cluster.local:8080来访问它。这在微服务架构里非常关键服务间调用不再依赖固定 IP。6. 从 VM 到容器的架构演进编排与调度如果只是把虚拟机里的应用搬到容器里那容器化的价值只发挥了一小半。真正让容器在高并发场景下发挥威力的是容器编排平台。Kubernetes 是目前事实上的标准容器编排平台。千万 QPS 架构中服务数量往往成百上千。没有编排平台你手动管理几百个容器的创建、删除、扩容、缩容人力完全支撑不住。Kubernetes 做的事情其实可以总结为声明期望状态不断调谐当前状态。以弹性扩容为例在 Kubernetes 里只需要设置一个 HPAHorizontalPodAutoscaler当 CPU 使用率超过阈值时自动扩容。配置如下# 文件路径user-service-hpa.yaml apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: name: user-service-hpa namespace: production spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment name: user-service minReplicas: 6 maxReplicas: 30 metrics: - type: Resource resource: name: cpu target: type: Utilization averageUtilization: 60这个 HPA 的意思是user-service这个 Deployment 最少保持 6 个副本当 CPU 平均使用率超过 60% 时自动扩到更多副本最多 30 个。流量高峰过去后Kubernetes 会自动收缩副本数。这种能力直接决定了架构的 QPS 上限。在传统 VM 环境中扩容要经历申请资源、安装系统、部署应用、接入负载均衡这一整套流程通常是小时级别容器环境下HPA 触发的扩容是分钟级别而且扩容出的 Pod 与原有 Pod 完全一致因为它们是同一个镜像创建出来的。这也解释了为什么容器是千万 QPS 架构中弹性能力的基础设施级保障。7. 镜像安全与容器安全千万 QPS 架构的底线在千万 QPS 架构中容器带来的安全和虚拟机有一个重要差异共享内核。攻击者一旦通过应用漏洞进入容器再结合内核漏洞提权可能影响宿主机上的其他容器。所以容器安全不能当成普通运维问题它必须贯穿镜像构建、镜像仓库、容器运行、编排调度的整个生命周期。7.1 镜像安全镜像安全是整个容器安全的第一道门也是很多人最容易忽视的地方。第一不依赖踩坑版本镜像。很多公共仓库的镜像已经存在被弃用的标签或者已知漏洞基础镜像要选官方维护的版本。第二尽量使用精简基础镜像减少攻击面。以 Java 应用为例基础镜像可以用eclipse-temurin:11-jre而不是ubuntu加手动安装 JDK。镜像里不需要 shell 工具链、包管理器甚至不需要curl所有这些都会扩大潜在的攻击面。第三用非 root 用户运行容器。默认情况下容器内进程以 root 用户运行如果应用被攻破攻击者就拥有了容器内最高权限。应该在 Dockerfile 中显式指定运行用户RUN groupadd -r app useradd -r -g app app USER app第四定期扫描镜像漏洞。Harbor 内置了 Trivy 或 Clair可以扫描镜像的 CVE 漏洞。CI 阶段也应该接入漏洞扫描高危漏洞直接阻断发布。7.2 容器运行安全运行时安全可以从三个层面下功夫。第一Kubernetes 的 RBAC 权限控制。不要给普通服务分配 cluster-admin 权限。Pod 的 ServiceAccount 权限也应该最小化。第二限制容器能力。容器默认继承了 Linux capabilities但你可以显式删除不需要的 capabilitiessecurityContext: capabilities: drop: [ALL] add: [NET_BIND_SERVICE]第三使用镜像签名验证。从仓库拉取的镜像要经过签名校验防止镜像被篡改。Kubernetes 在拉取镜像时会校验签名无法通过验证的镜像不会运行。另外还有一个实践不要在容器内保存敏感信息。数据库密码、云密钥、证书等应该通过 Kubernetes Secret 或外部密钥管理服务注入避免写死在镜像层或环境变量里。镜像一旦被推送到仓库任何人都可以查看镜像的构建历史和层内容明文密钥等于直接泄露。8. 从 VM 到容器改造的常见问题与排查思路在实际业务系统容器化改造过程中遇到问题的概率非常高。我把高频问题整理成一个排查表你可以直接收藏使用。问题现象可能原因排查方式解决方案容器启动后立即退出应用启动失败或启动命令错误查看容器日志docker logs container检查启动参数、依赖服务是否可用Java 应用 OOM容器被杀JVM 堆内存超过容器限额查看dmesg或 Kubernetes 事件的 OOMKilled调整 JVM 堆参数匹配 Cgroups 限制容器内时间不准镜像未挂载时间同步配置执行date查看容器时间挂载/etc/localtime或运行时区设置日志文件越来越大应用直接写日志文件且未轮转查看df -h定位日志目录改为标准输出或配置日志轮转Pod 一直处于 Pending 状态资源不足或调度约束不满足kubectl describe pod pod检查集群节点资源和亲和性配置容器启动慢基础镜像过大或启动脚本阻塞拉取镜像耗时查看应用启动日志优化镜像体积检查初始化脚本外网访问异常网络策略限制或 Service 类型不对检查 NetworkPolicy 和 Service YAML调整网络策略或将 Service 改为 LoadBalancer配置不生效环境变量未注入或 ConfigMap 未更新检查 Pod 环境变量和 ConfigMap修改后滚动重启 Pod排查容器问题要善用两类命令。第一类kubectl describe pod它可以查看 Pod 的事件包括调度失败原因、镜像拉取失败原因、探针失败信息第二类kubectl logs直接查看容器输出日志。大多数容器问题都能从这两处找到线索。还有一个容易忽略的点镜像拉取策略。Kubernetes 的imagePullPolicy默认是IfNotPresent如果本地存在同名的旧镜像就不会重新拉取。在测试环境联调时经常出现代码改了但镜像没更新Pod 还在用旧镜像。建议测试环境的imagePullPolicy设置为Always。9. 容器化的最佳实践与工程建议综合前面所有内容这里给出几条容器化改造的工程建议这些经验来自实际微服务架构落地过程适用于中大型业务系统。第一先梳理无状态服务再动有状态服务。容器化的理想对象是无状态服务也就是服务本身不保存数据所有数据都存放在外部存储。如果一个服务的内存里有会话数据或者本地磁盘有临时文件应该先改造这些点把会话交给 Redis把文件交给对象存储或共享文件系统然后再容器化。有状态组件如数据库、Redis 集群建议在云上使用托管服务或者由专业的中间件团队加容器方案管理不要一开始就搞 Kubernetes 上的高可用数据库。第二配置与代码彻底分离。容器镜像应该是不可变的同一个镜像在不同环境下只通过配置区分。不要在构建时把生产环境配置写进去。环境变量、ConfigMap、配置中心三种方式按场景选择。环境变量适合简单少量配置ConfigMap 适合 Kubernetes 内部的批量配置配置中心适合多团队共享的动态配置。第三镜像版本管理要规范。镜像 tag 必须包含版本信息禁止使用latest作为生产镜像 tag。latest的问题是它不固定今天拉取的镜像和明天拉取的镜像可能不是同一个。版本回滚时需要精确到具体版本所以 tag 建议格式服务名-版本号-构建序号例如user-service-v1.2.0-b20240618。第四建立多环境交付流水线。容器化工程上要打通 CI/CD。开发提交代码后CI 自动编译、单测、构建镜像、漏洞扫描CD 自动部署到测试环境测试通过后逐级发布到预发和生产环境。这里建议引入 Argo Rollouts 做金丝雀发布部分流量切到新版本验证通过后全量发布失败则自动回滚。千万 QPS 的系统任何一次全量发布都是巨大风险金丝雀发布能力是企业级架构的标配。第五资源限额必须显式设置。生产环境中所有容器都要配置 requests 和 limits。requests 用于调度决策limits 防止容器超用资源拖垮节点。不设置 limits 的容器在流量高峰期可能吃光整台机器内存导致节点上的其他容器连环故障。这一点在容量规划和故障隔离中非常关键。第六基础设施能力建设要跟上。容器化不是把 Dockerfile 写好就完事还需要服务发现、配置中心、日志采集、监控告警、链路追踪等配套能力。Kubernetes Service 解决了服务发现后端还需要配套的日志系统ELK/Loki、监控系统Prometheus Grafana、链路追踪系统SkyWalking/Jaeger。这些组件与 YAML 或 Agent 集成后才算完整的生产级容器化。10. 总结与后续学习方向从 VM 到容器表面上变化的部署单位从一台操作系统变成了一段进程本质上变化的是资源利用率、交付速度、弹性能力和运维方式。千万 QPS 架构不能依赖手工搭建环境因为手动的速度跟不上流量的变化。容器化让环境构建变得可编程配合 Kubernetes 实现了声明式、自动化的基础设施管理这才是高并发系统能够在流量高峰安全度过的底层原因。对于正在做业务系统容器化改造的团队建议按这个顺序推进从最简单的无状态服务开始完成第一个镜像、第一份 Deployment 配置搭建私有镜像仓库接入漏洞扫描和镜像签名逐步把配置迁移到 ConfigMap 和配置中心部署 Prometheus 监控建立容器级别的告警体系引入 CI/CD 流水线把构建、扫描、部署流程自动化最后再考虑 HPA 弹性伸缩、金丝雀发布等高级能力。容器化落地的过程也是开发团队对系统边界和组件依赖重新理解的过程。这个过程中没有完全相同的方案但遵循最小化权限、不可变镜像、依赖外部化、显式资源限制这几个原则大方向不会偏。如果你正在做千万 QPS 架构的容量规划和基础设施升级建议把容器化和 Kubernetes 排进近期学习计划。下一篇可以继续深入 Kubernetes 集群的 Ingress 网关与流量治理或者聊聊容器环境下 JVM 调优的完整实践。
返回列表