上个月陪一个团队做架构复盘他们把单体应用拆成二十多个模块机器从三台涨到四十台账单翻了四倍发布故障率却没降。这类事我见得不少。云原生构建这套东西——微服务、容器化、容器编排——经常被当成三个独立的技术名词来讲好像学会写 Dockerfile、会敲几条 kubectl 命令就算入门。实际上它是一条完整的链路业务边界决定微服务怎么切微服务的运行形态决定容器镜像怎么打镜像的运行特征又反过来决定编排层的调度、探针、扩缩容参数怎么配。任何一环拍脑袋后面都要用故障和账单来还。这篇内容面向正在做或准备做云原生改造的后端、运维和架构同学从拆分判断、镜像构建、编排配置到线上排查把我踩过的坑和能直接照抄的参数一并写出来尽量让刚接触的同学少走两圈弯路。1. 云原生改造前先把账算明白1.1 微服务不是目的可独立演进才是我见过太多团队把微服务当成绩指标仿佛服务数量越多越先进。但拆分的本质是解耦变更让一个需求的修改范围收敛在一个小团队手里而不是把一个大泥球切成二十个小泥球再通过网络调用粘回去。判断一个模块该不该独立我通常看三个信号变更频率是否显著高于周边、伸缩需求是否与周边差异大、故障影响面是否需要隔离。比如商品详情页里的价格计算规则变动频繁且需要独立的灰度节奏这类适合剥离而像用户表的基础字段查询几乎不变又高频拆出去只会平白增加一次网络往返和一次序列化开销。还有一个常被忽略的信号是团队边界。服务边界如果和团队边界错位跨服务的接口协商会变成跨团队的会议拆分带来的不是效率而是沟通成本。业界的经验是让一个团队端到端负责一到三个服务从开发、发布到值班全包这样服务粒度自然会收敛到合理区间。一个团队管十几个服务的结局通常是没人敢动其中任何一个。注意组织架构没准备好之前不要为了技术先进性先拆。拆完再补组织中间那段时间的故障率会非常难看。1.2 容器化与编排的隐性成本清单容器化带来的直接收益很好讲环境一致、交付标准化、资源利用率提升。隐性成本很少有人提前列出来。我把实际项目里最常见的几项整理成表方便你在立项时对照评估。成本项具体表现应对方式镜像治理成本镜像仓库膨胀、基础镜像版本散乱、漏洞扫描告警堆积统一基础镜像、固定 tag 规范、定期重建网络排查成本跨节点通信问题定位链路变长抓包不再简单提前部署链路追踪统一日志采集存储与状态有状态服务上容器后持久化方案复杂先用托管中间件状态服务晚一步容器化学习曲线编排层概念多配置项动辄上百个只启用当前需要的对象禁止一次性全上成本可见性资源申请虚高账单不可归因强制设置 requests按命名空间分摊成本这里面最容易被低估的是最后一行的资源申请。默认不设 requests 的 Pod 会被调度器当作零需求节点上可以塞进远超实际承载能力的副本数一旦流量上来就集体争抢 CPU表现为全站延迟毛刺。我在一个项目里见过一台 8 核节点跑了七十多个未设 requests 的 Pod高峰期节点负载冲到 60 以上谁看监控都以为是应用代码慢。1.3 演进路线别一上来就全量重构比较稳妥的路线是绞杀者模式新功能直接以新服务形态长出来老系统通过一层适配入口承接剩余流量逐步把功能搬走最后老系统自然萎缩。这条路线的好处是任何时刻都有可回退的路径坏处是需要维护一段时间的双写或流量分流逻辑。具体节奏我建议分四步走先把无状态、依赖少的边缘服务容器化验证镜像构建流水线和编排基础能力再把核心无状态服务迁进来重点打磨探针、资源配额、滚动发布然后处理有状态服务优先用托管中间件替代自建最后才考虑服务网格、多集群这类进阶议题。每一步之间留出至少两个迭代的观察期别指望一个季度全干完。2. 微服务拆分的判断标准与落地节奏2.1 按业务能力切不按技术分层切最典型的分错方式是按 controller、service、dao 各起一个服务。这种切法的直接后果是任何一次业务改动都要同时改三个服务、发三次版事务还跨了三次网络分布式事务的复杂度全吃满收益接近零。正确的切法围绕业务能力和数据归属展开一个服务应该拥有它管理的数据数据的写入权只能有一个服务持有其他服务通过接口读取。这条规则听起来简单但它能挡掉九成的错误拆分。我评审新服务时只问一句这个服务独占哪张表答不上来的大概率是技术分层的产物。领域事件是另一种可用的切分线索。订单创建后需要通知库存、积分、消息推送这三件事彼此独立且失败可重试天然适合拆成独立的消费者。它们的共性是被通知而不是被调用走异步消息比同步调用更稳也顺便解决了扇出调用带来的超时叠加问题。2.2 服务粒度与调用深度的平衡粒度并没有统一答案但有个可量化的观察指标一次用户请求经过的同步调用链深度。我一般控制在三层以内超过四层就要警惕。每多一跳P99 延迟就会叠加一次故障概率也乘一次链路上一旦有个慢服务整条请求全被拖住。如果发现调用链太深处理方式通常是合并或并行化。合并是把强关联、总是一起变更的服务合回一个进程用模块化的方式保持代码边界并行化是把串行调用改成并发发起用整体超时兜底。两者都比继续往下拆更划算。还有一个参考维度是团队认知负荷。一个服务如果接口数超过三十、配置文件超过两百行、新人入职两周还摸不清它干什么那它要么该拆要么该重写。反过来一个只有两个接口、每次发版都要跟着别人一起动的服务就该考虑合回去。2.3 通信、超时与重试的默认约定微服务一旦落地通信约定就必须统一否则每个服务各写一套排查时心态会崩。我的默认约定是这样的同步调用只用于强一致的查询场景超时时间统一设成上游可接受预算的一半重试最多两次且必须带指数退避与抖动写操作默认不重试。超时预算的算法值得展开。假设端到端 SLO 是 800 毫秒网关层预留 100 毫秒业务层可用 700 毫秒其中主服务自身计算 200 毫秒剩下 500 毫秒给下游调用。如果主服务要并行调三个下游每个下游的超时设 400 毫秒重试一次就是 800 毫秒已经超预算所以这三个调用必须手动设置总超时上限或者干脆不重试。很多线上雪崩就是重试次数乘以超时时间超过了上游承受能力。提示重试策略一定要配熔断或并发限制否则一个下游抖动会引来三倍流量把抖动的服务直接打挂形成正反馈。2.4 数据一致性的务实处理跨服务的数据一致性我的建议是尽量别用强一致的分布式事务改成状态机加事件驱动的最终一致。下单扣库存这个经典场景做法是先在本地事务里写入订单和一条扣减事件再由消息投递到库存服务库存服务幂等消费。中间任何一步失败都有补偿或对账兜底。关键在幂等。消费者必须用业务唯一键去重比如订单号加事件类型落地到一张去重表或者用消息本身的幂等语义。别指望消息队列的精确一次那是个容易让人放松警惕的说法实际业务里还是要自己做去重。对账是最后一道防线也是被忽略最多的一环。每天跑一次差异比对把订单状态和库存流水对齐差出来的单据人工或自动修正。这套机制平时看起来没什么产出真出事的时候是唯一能兜住的东西。3. 容器化实操从能跑到跑得稳3.1 镜像分层原理与缓存命中率镜像是一层层叠出来的每条构建指令生成一层某一层的内容一旦变化它之后的所有层缓存全部失效。这个机制决定了 Dockerfile 里指令的顺序比指令本身更影响构建速度。最常见的浪费是把整个源码目录先 COPY 进去再执行依赖安装。源码里随便改一个注释依赖就要重新下载一遍构建从两分钟变成十分钟。正确顺序是先拷贝依赖声明文件执行安装再拷贝源码。依赖文件的改动频率远低于业务代码缓存命中率一下就上来了。下面是我常用的一个 Java 服务构建片段思路对其他语言同样适用。FROM maven:3.9-eclipse-temurin-17 AS builder WORKDIR /build # 先只拷贝依赖声明让依赖层单独缓存 COPY pom.xml . RUN mvn -B dependency:go-offline # 再拷贝源码并打包 COPY src ./src RUN mvn -B clean package -DskipTests FROM eclipse-temurin:17-jre-jammy WORKDIR /app RUN useradd -r -u 1001 appuser COPY --frombuilder /build/target/app.jar ./app.jar USER appuser EXPOSE 8080 ENTRYPOINT [java,-XX:MaxRAMPercentage75,-jar,/app.jar]两个细节值得说。一是用了独立阶段构建、只把产物拷进运行镜像最终镜像从八百多兆降到两百兆左右拉取时间直接影响扩容速度二是用了非 root 用户很多集群的安全策略默认禁止 root 运行早设好省得后面被拦。3.2 基础镜像选型别只看体积体积最小的镜像不一定最合适。精简发行版用的是另一套 C 库某些依赖系统本地库的组件会出现找不到符号或者 DNS 解析行为差异的问题这类问题在构建阶段完全看不出来上线后才炸。我建议的取舍是对稳定性要求高的核心服务用成熟的完整版精简镜像对体积敏感的边缘任务再用极小镜像。无 shell 的镜像还有个隐性代价是排障困难。容器里进不去、没法看进程、没法测网络连通性只能靠日志盲猜。折中方案是保持生产镜像精简排障时用临时调试容器注入到同一个 Pod 的命名空间里或者平时留一个带工具的调试镜像版本出问题时临时替换。注意无论选哪种基础镜像都必须固定到具体的版本标签禁止使用浮动标签。浮动标签意味着同一个提交今天构建和下周构建出来的镜像内容不同回滚时你根本不知道回到了什么状态。3.3 资源申请与限制的参数算法这部分是容器化里最容易拍脑袋的地方而它直接决定了稳定性和成本。先说结论requests 按实测 P90 用量设定limits 按峰值加余量设定内存的 limits 建议等于 requestsCPU 的 limits 可以放宽甚至不设。具体算法以 Java 服务为例。假设压测下单实例稳定处理 QPS 200堆内存稳定在 1.2G加上元空间、线程栈、直接内存实际内存占用约 1.8G那么 requests 设 2Gi 比较合适。limits 如果设成 2Gi一旦有内存泄漏或者大对象分配就会触发 OOM 被杀进程退出码是 137如果设成 4Gi 而 requests 是 2Gi节点会按 2Gi 调度实际用了 4Gi 的时候节点内存就超卖了。所以内存这一项我的实践是 requests 与 limits 相等宁可给足也不要留下超卖空间。CPU 则不同。CPU 是可压缩资源用超了只会被限流不会被杀。requests 按 P90 设比如 500mlimits 设成 2 核这种宽松值让突发流量能吃到节点的空闲算力。但要注意CPU limits 会带来节流表现为延迟尾部毛刺对延迟敏感的服务我倾向于干脆不设 limits只靠 requests 保障基本份额同时用命名空间配额防止整体失控。JVM 在容器里还有个坑老版本 JDK 认不出容器限制会按宿主机核数去分配线程池和 GC 线程结果在 2 核的容器里起了几十个 GC 线程互相抢时间片。较新的 JDK 已经支持容器感知但为了保险我通常还是显式指定最大堆比例参数让 JVM 自己按容器内存上限换算避免写死数值在换规格时忘记改。3.4 探针设计决定自愈能力的三个开关探针配错比不配更危险。三种探针的职责必须分清存活探针回答这个进程是不是死了就绪探针回答它现在能不能接流量启动探针回答它启动完了没有。最常见的事故是把下游依赖写进存活探针。比如应用的健康检查接口里查了一次数据库数据库某次短暂抖动所有实例的存活探针同时失败编排层认定全部实例已死开始集体重启重启期间又连不上数据库形成一个自我放大的雪崩。存活探针只该检查进程自身的状态能响应 HTTP 就算活。就绪探针则相反它可以检查依赖是否可用因为它只影响流量分配不会触发重启。但阈值要给宽一点比如连续失败五次才摘流量避免一次网络抖动就把整个实例踢出服务。启动探针是给启动慢的应用准备的。没有它只能把首次延迟设得很大代价是运行期的检测变迟钝。有了它可以设置最多容忍 300 秒启动期间存活检测不生效启动成功后无缝切换到正常的检测节奏。三个参数的关系要算清楚首次延迟加上周期乘以失败阈值得到的就是最大容忍故障时间这个时间必须小于业务能接受的中断时长。startupProbe: httpGet: { path: /healthz, port: 8080 } failureThreshold: 30 periodSeconds: 10 # 最多容忍 300 秒启动 livenessProbe: httpGet: { path: /livez, port: 8080 } periodSeconds: 10 failureThreshold: 3 # 30 秒内连续失败才重启 readinessProbe: httpGet: { path: /readyz, port: 8080 } periodSeconds: 5 failureThreshold: 5 # 25 秒内摘除流量3.5 优雅退出被忽略的 502 制造机滚动更新时出现零星 502九成是优雅退出没做。流程是这样的编排层决定下线一个实例先把它从服务端点里摘掉同时给容器发终止信号然后等一段宽限期宽限期到了还没退就强杀。如果应用收到终止信号立刻关闭进程正在处理的请求全断而流量摘除在部分实现里是异步的就会有一小段时间新请求还打过来直接 502。解决办法是两步。应用侧注册终止回调收到信号后停止接收新请求等待正在处理的请求完成再退出。编排侧加一个停机前钩子先睡几秒让负载均衡完成摘流再让应用开始关闭。宽限期要大于摘流延迟加最长请求处理时间一般设 30 秒够用。提示加了停机前钩子之后总下线时间等于钩子时长加重启时间滚动更新的整体耗时变长发布窗口要相应放宽别让流水线的等待超时先触发。4. 容器编排把配置写对比写多更重要4.1 滚动更新参数的计算方法发布策略的核心是保证可用副本数始终达标。两个参数配合使用一个控制超量创建的上限一个控制不可用的下限。副本数少的时候百分比取整的规则容易出意外。举例8 个副本超量比例 25%按向上取整得到 2不可用比例 0。更新过程是先新建 2 个新版本实例等它们就绪再销毁 2 个旧实例循环往复。全程可用副本不低于 8容量不降代价是峰值多占 25% 资源。反过来如果设置不可用比例为 25%则允许先停 2 个旧实例再补新的全程资源占用不变但容量会短暂下降。哪种更好取决于业务无法接受容量波动的用前者资源紧张到极致的用后者。副本数是 2 或 3 的小服务要特别小心。3 个副本、不可用比例 25%向上取整是 1可以接受但如果写的是 20%向下取整就成了 0此时两个参数的行为会导致更新卡死必须靠超量创建来推进。所以小规模服务我建议直接把超量设成 1、不可用设成 0用绝对值而不是百分比。还有一个必配项是中断预算。节点维护、集群升级时会主动驱逐 Pod如果没有中断预算保护可能一次性把某个服务的所有副本赶到别的节点期间必然不可用。预算的配置原则是保证至少一半副本或至少一个副本存活配合滚动更新的参数一起看才能保证任何主动操作都不破坏可用性。4.2 服务暴露从集群内到集群外内部访问用集群内虚拟地址就够做负载均衡和基本服务发现。这里要提醒一点服务发现依赖 DNS容器内的 DNS 解析超时设置会影响故障恢复速度。如果超时设成默认的 5 秒下游实例全部不可用时上游会卡在解析上一次一次重试表现为看似无关的全面变慢。把解析超时和重试次数调小能明显加快故障感知。对外暴露要看流量规模。小规模直接给节点端口就能用缺点是端口管理混乱、无法按域名路由。正规做法是用入口控制器它是个独立部署的组件负责监听外部流量并按规则转发到内部服务。这里有个高频踩坑点只写了入口规则对象没部署入口控制器规则永远不生效查半天以为是配置写错了。入口规则本身只描述路由不负责 TLS 终止以外的流量治理。跨域、限流、重写路径这些能力要么在入口控制器层用注解配置要么放到网关层统一做。我的建议是别把业务逻辑塞进入口注解里不同控制器的注解互不兼容换实现时全是隐形成本。4.3 配置与密钥更新不生效的根因配置对象挂载进容器有两种方式环境变量和文件挂载。环境变量的特点是一次性注入配置改了必须重建 Pod 才生效文件挂载支持热更新但应用得自己监听文件变化。最常见的困惑是我改了配置Pod 里还是旧值。原因就是用了环境变量注入而工作负载对象本身没变编排层认为不需要重建。解决办法有两个一是在工作负载模板里加一个配置的校验和注解配置一变注解值就变自然会触发滚动更新二是接受重建把配置变更纳入发布流程。密钥的处理要更谨慎。密钥对象在集群里只是做了编码而不是加密任何有读取权限的人都能还原。所以权限控制必须收紧按命名空间限定可读范围敏感值尽量不在配置里出现改为启动时从外部配置中心拉取。另外很多团队把密钥明文提交到代码仓库这个习惯必须改掉仓库里只放模板和占位符。4.4 自动扩缩容的触发条件与陷阱自动扩缩容看起来是最省事的特性实际用起来限制不少。它基于资源用量做决策就意味着必须先设置 requests没有 requests 的实例永远不会被纳入计算。指标采集组件没装或者没跑起来扩缩容也不会工作而且不报错只是安静地不动作。算法本身很朴素期望副本数等于当前副本数乘以当前指标值除以目标值再向上取整。举个具体数当前 4 个副本CPU 平均用量是请求值的 90%目标是 60%那么期望值是 4 乘以 90 除以 60 等于 6。这里注意分母用的是 requests 而不是 limits所以 requests 设小了会导致频繁扩容。扩容快、缩容慢是刻意的设计缩容有冷却窗口避免流量波动引起副本数反复横跳。这个窗口一般设 5 分钟太短会震荡太长会浪费。对于流量有明显波峰波谷的业务纯靠指标触发往往滞后更好的做法是定时扩缩容打底、指标触发兜底把已知的大促时间段提前排进去。注意数据库连接池大小必须和最大副本数对齐。扩缩容把实例数从 4 拉到 20每个实例 20 个连接瞬时就是 400 个连接数据库端很可能直接拒绝。5. 可观测性与故障排查实操5.1 三件套怎么搭才不白搭指标、日志、链路这三样东西价值不在采集全而在能串起来。我的最低要求是每个请求有统一的标识日志里带上它链路的根 span 带上它指标异常时能通过标识反查到具体请求。指标层面容器维度的资源和应用维度的业务指标要分开看。资源指标回答是不是机器不够业务指标回答用户是不是感知到了。很多告警只盯资源CPU 到 90% 就告警结果天天误报因为短时高峰是正常的而真正该告警的是错误率和延迟分位数这些直接对应业务损失。延迟指标必须看分位数而不是平均值。平均值 50 毫秒的服务P99 可能是 2 秒用户感受到的是后面那一批。我一般同时盯 P50、P95、P99 三条线P99 和 P50 的差距突然拉开通常意味着有慢查询、锁竞争或者下游某个实例变慢。链路的采样率要按流量定。低流量服务全采高流量服务采百分之一到十分之一但错误请求要全采所以需要尾部采样策略。这块配置不复杂坑在于跨进程的上下文透传异步线程池、消息队列、定时任务三处最容易丢标识链路断在中间排查时就只能靠时间戳猜。5.2 故障速查表这张表是我从实际处理过的问题里整理出来的按现象归类遇到问题先对照再动手。现象高频原因排查动作实例反复重启存活探针误判、启动超时、内存超限看上次退出原因的退出码137 是内存被杀1 是应用异常镜像拉不下来标签写错、仓库凭证过期、网络策略阻断在节点上手动拉一次区分是凭证问题还是网络问题实例一直挂起节点资源不足、亲和性规则冲突、卷挂载失败看调度事件里的失败原因多半写着具体哪条约束不满足间歇性 502优雅退出缺失、就绪探针过早通过对照发布时间和错误时间是否吻合配置改了不生效环境变量注入、工作负载未重建进容器打印实际环境变量确认集群内解析失败解析超时设置过长、服务无就绪实例在容器里手动解析一次域名看返回地址列表全站延迟毛刺CPU 限流、节点超卖、GC 停顿对比限流指标与延迟曲线的时间点是否重合扩容不触发未设 requests、指标组件异常直接查扩缩容对象的事件记录5.3 几个必须提前设的防护第一是命名空间配额。没有配额一个跑飞的批处理任务能把整个集群的资源吃干净连监控组件都调度不上。配额按环境分开发环境给低额度强制大家精简生产环境留足余量。第二是网络访问控制。默认情况下集群内所有 Pod 可以互相访问一个被入侵的边缘服务能直接连到数据库。用网络策略做默认拒绝、按需放通一开始配会麻烦但这是唯一低成本的控制面。第三是镜像准入检查。禁止无标签、禁止来自非白名单仓库、禁止高危漏洞等级以上的镜像进入集群。这三条能挡掉大量低级事故配置一次长期受益。第四是发布前的自愈演练。随机杀掉一个实例看服务是否在预期时间内恢复故意把一个下游停掉看熔断是否生效、错误是否被正确降级。演练的价值在于把应该会自愈变成确实自愈过。6. 我踩过的坑与实操心得头一个坑是过早引入服务网格。它解决的是细粒度流量治理和零信任通信但代价是每个 Pod 多一个代理容器、多一跳网络、多一层配置。我在一个十几个服务的项目里上过结果排查链路问题的时间比排查业务问题还长最后撤了把重试、超时、熔断直接做在客户端库里简单可控。流量治理的需求真正复杂到需要独立控制面时再上别为了架构图好看提前背成本。第二个坑是镜像标签用分支名。分支名会变同一个标签在不同时间指向不同内容回滚时回到的根本不是你以为的版本。后来统一改成提交号配合一份发布记录回滚就是改一个数字清清楚楚。第三个坑是资源配额一刀切。全公司统一都是 1 核 2G结果 Java 服务天天内存告警而几个轻量任务闲置一半资源。后来按语言和角色分了几个档位允许团队在档位内自选超出档位要写理由资源浪费明显下降也没人再因为配额不够去偷偷改大。第四个坑最隐晦容器时区。基础镜像默认是世界标准时间日志时间戳和业务时间差了八小时排查时死活对不上。修复很简单构建时设好时区或者挂载时区文件但这种问题不踩一次很难想到。最后一个经验是关于指标体系的自建。别一上来就追求全覆盖先把四个数拉出来请求量、错误率、延迟分位数、饱和度。这四个数能覆盖绝大多数线上问题的第一判断等它们稳定了再往上叠业务指标。我见过采集了两百多个指标、告警规则写了三百条最后没人看告警全在等用户投诉。指标的价值在于被使用不在于被采集。至于后续还能怎么往下走我的建议是把重心放在两件事上一是把发布流程里的手动步骤彻底消灭包括数据库变更、配置刷新、缓存预热全部纳入流水线二是把容量规划从经验值变成压测数据每次大版本发布前跑一次基准压测用数据决定副本数和资源规格。这两件事做完云原生的收益才真正落到日常。
阅读完成 · 觉得有帮助?