1. 先理清三件事的关系,再动手
把一个跑了两年多的单体应用拆成十几个服务,装进容器,推上编排平台,结果上线第一周就出了一堆状况:节点内存被打满、服务之间互相调用超时雪崩、发布瞬间大量 502、日志里全是 DNS 解析超时。这些问题的根源往往不在技术本身,而在于顺序和边界搞错了。云原生构建这件事,微服务是切法,容器化是封装方式,容器编排是调度与治理方式,三者是一条流水线上的三个工位,任何一环偷懒,后面都会以故障的形式还回来。
这篇内容面向的是正准备或正在做服务化改造的工程团队:后端开发、运维、SRE、技术负责人。如果你还在犹豫要不要拆服务、镜像为什么越做越大、编排平台上的 Pod 为什么反复重启、发布为什么总有损,下面的内容基本都能对应上。我会按“思路拆解—微服务拆分—容器化落地—容器编排—上线治理”的顺序讲,每个环节都给出可抄的参数、可复现的配置,以及那些文档里不会写、只在故障复盘会上才会出现的教训。
先给一个定调:云原生不是一套必须全上的技术栈,而是一种用声明式描述目标状态、由平台持续把实际状态拉回目标状态的工程方法。理解了这句话,后面所有的选型和参数取舍都有了判断依据。
1.1 三者各自解决什么问题
很多人把这三件事混为一谈,导致拆分的时候想着部署,部署的时候又回头改拆分。拆开看会清晰很多:
| 维度 | 微服务 | 容器化 | 容器编排 |
|---|---|---|---|
| 解决的问题 | 组织协作边界与变更影响面 | 环境一致性与交付物标准化 | 调度、自愈、发布与容量 |
| 核心交付物 | 服务契约(接口 + 数据所有权) | 不可变镜像 | 声明式配置清单 |
| 失败时的典型信号 | 跨服务事务混乱、链路越来越长 | 本地能跑线上跑不了、镜像几个 G | Pod 反复重启、发布有损、资源争抢 |
| 主要成本 | 分布式复杂度 | 构建与镜像治理成本 | 学习曲线与抽象泄漏 |
我见过最常见的错误,是把这三个维度当成三个独立项目,由三拨人分头推进:架构组画服务拆分图,运维组搞镜像规范,平台组搭集群。结果服务边界按组织架构切,镜像按人肉约定打,编排配置按经验拍参数,三份东西互相不认。正确的做法是让服务边界决定镜像粒度,镜像粒度决定部署单元,部署单元决定编排对象,一条线串下来。
1.2 为什么顺序不能反
顺序这件事我必须单独强调,因为反着做的代价极高。
先拆服务再容器化,好处是每个服务的依赖、启动参数、配置文件在拆分过程中就被摸清楚了,容器化只是把这些东西固化进镜像。反过来,先上集群再拆服务,你会得到一个“把所有单体塞进一个镜像、跑十几个副本”的集群,除了多了一层调度,什么收益都没有,反而多了一堆网络和存储的抽象成本。
先容器化再编排,好处是你能在单机上把镜像跑通、把启动参数、健康检查、优雅停机、日志输出方式全部调好,上编排时只是把这些已知量翻译成声明式配置。反过来,镜像本身还有问题就上编排,排查会变成噩梦:到底是镜像里的进程没起来,还是编排平台的探针配错了、资源给少了、调度没成功?两个变量同时存在,定位成本翻倍。
我的建议是把每个阶段都做成“可回退的最小闭环”:微服务阶段先做到“服务能独立部署、独立回滚”;容器化阶段先做到“同一镜像在开发机和测试机行为一致”;编排阶段先做到“单服务滚动发布零中断”。每一步都有验收标准,不要带着问题往下走。
1.3 什么时候该上编排,什么时候不该
不是所有团队都需要编排平台。下面这张表是我自己用来做判断的,供参考:
| 场景特征 | 建议方案 | 理由 |
|---|---|---|
| 服务数 ≤ 3,团队 ≤ 5 人,发布频率每周一次 | 单机容器 + 进程守护 + 反向代理 | 编排的调度与自愈价值体现不出来,运维成本反而更高 |
| 服务数 5~15,有多个环境,发布频繁 | 小规模编排集群,单集群多命名空间隔离 | 滚动发布、资源隔离、配置管理的收益开始明显 |
| 服务数 > 20,多团队并行 | 多集群 + 统一发布流水线 + 服务治理层 | 单集群的爆炸半径太大,需要按业务域隔离故障 |
| 有强状态依赖(数据库、消息中间件) | 有状态工作负载单独规划,或用托管服务 | 状态类负载的编排复杂度远高于无状态服务 |
判断的核心不是“别人都在用”,而是你的变更频率和故障恢复要求是否已经到了人工处理不过来的程度。如果一周发布一次、挂了十分钟也没人投诉,那编排带来的自愈能力对你价值有限,而它带来的网络抽象、存储抽象、调试难度是实打实的成本。
2. 微服务拆分:先划边界,再谈技术
拆分是整个链路里最不可逆的一步。镜像可以重打,编排参数可以调,但服务边界一旦切错,后面所有的通信、事务、数据一致性问题都会围绕这个错误边界反复发作。我见过一个团队按“controller 层一个服务、service 层一个服务、dao 层一个服务”来切,结果是每一次业务变更都要同时改三个服务、发三次版,链路长度翻了三倍,性能还掉了一半。这不是微服务,这是把单体拆成了分布式单体。
2.1 边界判定的三个实用维度
理论上讲限界上下文,实践里我更依赖三个可操作的判断维度:
第一,数据所有权。一个服务应该是某几张表的唯一写入方。如果两个服务都要写同一张表,那它们本质上是一个服务,硬拆只会带来写冲突和状态不一致。判断方法很朴素:画出表与服务的对应关系,一张表被两个服务写入,就是边界画错了。
第二,变更频率。经常一起改的代码应该待在同一个服务里。你可以拉最近半年的提交记录,看哪些模块总是出现在同一个提交或同一个需求里,这些模块拆开就是自找麻烦。反过来,一年改不了两次的模块,即使业务上相关,也可以独立出去降低主服务的复杂度。
第三,故障影响面。一个服务的崩溃是否会导致核心交易链路不可用?如果会,那它就不该被拆成独立进程,或者拆出去之后必须有降级路径。这个维度最容易被忽略,很多团队拆完之后才发现,一个边缘服务挂掉把主流程拖死了,因为调用链上没做隔离。
2.2 通信方式:同步和异步的取舍
拆开之后,服务之间怎么说话就是个绕不开的问题。同步调用(HTTP 或 RPC)写起来最直观,链式调用一目了然,但它是故障传播的高速公路:A 调 B、B 调 C,C 慢了一秒,A 的线程池在几十个并发下就被打满,然后 A 也挂了。异步消息解耦效果好,但引入了消息重复、顺序、堆积、幂等这一整套问题。
我的经验是按“是否需要立即返回结果”来分:
| 场景 | 推荐方式 | 关键配置 |
|---|---|---|
| 用户请求需要实时结果(查询、下单校验) | 同步调用 | 超时 ≤ 上游超时的 1/2,重试仅限幂等接口,最多 2 次 |
| 通知、状态同步、日志类 | 异步消息 | 消费端必须幂等,设置死信队列与最大重试次数 |
| 长耗时任务(导出、计算) | 异步 + 任务状态查询 | 任务 ID 落库,前端轮询或推送 |
同步调用必须配齐三样东西:超时、重试、熔断。超时时间不要拍脑袋,从上游往下逐层收敛,比如网关 3 秒、服务 A 调 B 设 800 毫秒、B 调 C 设 400 毫秒,保证最底层先超时,避免上游已经放弃了下游还在傻等。重试只对幂等接口开,而且必须加退避和抖动,否则下游刚恢复就被重试流量二次打挂。熔断阈值按错误率算,我一般设 50% 错误率、10 秒统计窗口、熔断 30 秒后放少量探针流量。
注意:分布式事务不要一上来就上强一致方案。绝大多数业务场景用“本地事务 + 消息最终一致 + 定时对账补偿”就够了,强一致方案的性能和运维代价在早期团队里通常承受不起。
2.3 横切关注点必须收口
服务数量上去之后,配置、发现、认证、限流这些东西如果每个服务各写一套,维护成本会指数级上升。我的做法是三个收口:
配置收口到配置中心,按“环境 / 服务 / 是否灰度”三级维度组织,配置变更走审计,支持热更新但要区分“可热更”和“必须重启”的配置项。这里有个坑:数据库连接池大小这类参数热更新会导致连接泄漏,必须在代码层面标记为不可热更。
服务发现交给平台,不要在代码里硬编码 IP 或用配置文件维护实例列表。编排平台本身提供了服务名到地址的解析能力,应用层只需要用服务名调用即可。
入口统一到网关,认证、鉴权、限流、灰度路由、协议转换全部在网关做,服务内部只信任网关透传的身份信息。这样服务本身不用关心认证逻辑,安全策略变更也只改一处。
3. 容器化落地:镜像怎么构建才又小又稳
容器化的目标不是“能跑起来”,而是“在任何一台机器上都跑出一样的行为,并且镜像足够小、启动足够快、漏洞足够少”。我见过一个 Java 服务的镜像做到 1.8G,光是拉镜像就要两分钟,滚动发布时新副本迟迟起不来,直接导致发布超时回滚。镜像体积这个问题,在单机时代只是磁盘占用,到了编排时代直接变成发布速度和扩容速度。
3.1 多阶段构建是必选项
多阶段构建的核心思路是:编译环境和运行环境分离,最终镜像里只留运行期需要的东西。以一个 Go 服务为例:
# 构建阶段 FROM golang:1.22-alpine AS builder WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags="-s -w" -o /out/api ./cmd/api # 运行阶段 FROM alpine:3.20 RUN apk add --no-cache ca-certificates tzdata && \ addgroup -g 10001 app && adduser -u 10001 -G app -D app WORKDIR /app COPY --from=builder /out/api /app/api USER 10001:10001 EXPOSE 8080 ENTRYPOINT ["/app/api"]这个写法有几个讲究。COPY go.mod go.sum单独一行放在源码之前,是为了让依赖层在源码变更时命中缓存,构建时间能从几分钟降到十几秒。CGO_ENABLED=0生成静态链接的二进制,避免运行镜像缺 glibc 导致的启动失败。-ldflags="-s -w"去掉符号表和调试信息,体积通常能减 20%~30%。运行阶段装ca-certificates和tzdata是必须的,否则调用外部 HTTPS 接口会报证书错误,日志时间会变成 UTC 让人对不上时间线。
基础镜像的选择上,我的优先级是:官方精简版 > 发行版精简版 > 完整发行版。用 musl 的精简镜像要留意 DNS 解析行为的差异,某些场景下解析超时会比 glibc 更明显,如果服务对 DNS 敏感,老老实实用 glibc 版本,体积大一点换稳定是值得的。
3.2 分层、缓存与忽略文件
镜像的每一层都是增量,层数越少、变动越靠后,构建和分发越快。几个实操要点:
变动频率低的放前面,高的放后面。系统依赖、证书、时区在基础镜像里一次装好;依赖清单在源码之前;源码最后复制。
用忽略文件排除无用内容。构建上下文里带上.git、node_modules、测试数据、本地日志,不仅拖慢构建,还可能把敏感文件打进镜像。忽略文件至少包含版本控制目录、本地依赖目录、构建产物、密钥文件、编辑器配置。
标签规范要定死。禁止使用latest,用“服务名 + 版本号 + 提交短哈希”的组合,保证任何一次部署都能溯源到确切代码。同时给镜像打上构建时间标签,排查“线上跑的是哪版”时非常有用。
| 常见问题 | 现象 | 处理方式 |
|---|---|---|
| 每次构建都重新装依赖 | 构建时间 3 分钟以上 | 依赖清单单独 COPY,先装依赖再 COPY 源码 |
| 镜像体积异常大 | 超过 500MB(非解释型语言) | 检查是否把构建工具链、测试数据、缓存目录带进去了 |
| 构建上下文过大 | 构建开始前卡很久 | 检查忽略文件,排除依赖目录和版本控制目录 |
| 镜像无法溯源 | 不知道线上跑的是哪个提交 | 强制标签规范,流水线自动注入提交哈希 |
3.3 运行时的那些细节坑
这部分是真正区分“能跑”和“跑得稳”的地方。
不要用 root 运行。容器内默认 root 虽然受命名空间隔离,但一旦有目录挂载或逃逸风险,后果会被放大。创建固定 UID 的非 root 用户,在构建文件里USER指定,同时确保应用写入的目录(如临时目录、日志目录)对该用户可写。
根文件系统建议只读。配上只读根文件系统后,任何试图写入根目录的行为都会立刻暴露,这反过来会逼你把需要写的路径显式挂载出来(比如/tmp挂一个临时卷)。这个约束在排查“容器重启后本地缓存丢失”类问题时特别有用。
信号处理要正确。容器里的 1 号进程负责转发信号,如果你的启动脚本用 shell 形式(CMD app而不是ENTRYPOINT ["app"]),信号可能传不到应用进程,导致优雅停机失效、编排平台等满超时时间后强杀。用 exec 形式,或者加一个轻量的 init 进程。
时区和证书。前面提过了,但值得再强调一次:这是线上事故里排名前列的“低级错误”。日志时间差 8 小时会直接干扰故障定位,证书缺失会让外部调用在特定时间点集体失败。
4. 容器编排:把“能跑”变成“跑得稳”
编排平台的价值就三句话:声明目标状态、持续调谐、自动恢复。你描述“我要 3 个副本、每个 400Mi 内存、就绪后接流量”,平台负责把现实拉到这个状态。但平台不知道你的业务语义,所有参数都得你自己给,给错了它也会忠实地执行错误指令,然后以 CrashLoopBackOff 的形式还给你。
4.1 工作负载对象怎么选
| 负载类型 | 适用场景 | 关键注意点 |
|---|---|---|
| 无状态副本集 | Web API、业务服务 | 副本数按峰值 QPS 除以单副本容量,留 30% 余量 |
| 有状态副本集 | 数据库、消息中间件、需要稳定标识的服务 | 存储卷、启动顺序、脑裂风险要单独设计 |
| 节点级守护 | 日志采集、监控代理、网络插件 | 注意资源占用,避免挤占业务 Pod |
| 定时任务 | 对账、清理、报表 | 配置并发策略和历史保留数,否则会堆积 |
| 一次性任务 | 数据迁移、初始化 | 设好重试上限和超时,避免无限重试 |
选择的核心判断是:这个服务有没有本地状态、需不需要稳定网络标识、启动有没有顺序要求。三个都没有,就用无状态副本集,别想复杂了。
4.2 资源请求与限制的计算过程
这是编排里最容易拍脑袋、也最容易出事的地方。requests影响调度(平台按它找节点)和弹性伸缩(按它算利用率),limits影响运行时的实际上限。给少了会被杀,给多了浪费资源。
我的计算方法是这样,以一个 Go 写的 API 服务为例:
- 压测出单副本在目标 QPS 下的内存稳态占用和峰值占用。假设稳态 210Mi,压测峰值 320Mi。
requests取峰值上浮 25%,即 400Mi。这个值保证调度时能找到足够内存的节点,也给突发留了余量。limits取requests的 1.3~1.5 倍,即 512Mi。不要把limits压得和requests一样,否则任何一次 GC 抖动都会触发 OOMKilled。- CPU 的
requests按压测得到的单副本 CPU 核数上浮 20%,假设 0.3 核就给 400m。CPU 的limits我给得相对宽松,因为 CPU 被限流表现为延迟升高而不是崩溃,而且限得太死会让 GC 和后台任务抢不到时间片。 - 上线后观察一周的实际利用率,如果长期低于
requests的 40%,就下调;如果频繁接近limits,就上调。
提示:内存
limits一定要设,不设的话单个服务内存泄漏会把整台机器拖垮;CPU 的limits可以酌情不设,让空闲算力被其他 Pod 借用,但前提是你的服务能容忍调度延迟。
4.3 三种探针,各管各的事
探针配错是编排环境里最典型的事故来源。三种探针职责完全不同,混用会引发雪崩:
启动探针负责“给慢启动应用留时间”。配了它之后,存活探针在启动成功前不生效,避免应用还在初始化就被判死重启。参数上,失败阈值乘以周期要大于最慢启动时间,比如周期 5 秒、失败阈值 24 次,就是给 120 秒。
就绪探针负责“能不能接流量”。它应该检查应用自身是否完成了初始化(端口监听、缓存预热完成),但不要检查下游强依赖。我见过就绪探针里查数据库连通性的写法,结果数据库抖了一下,全集群的 Pod 同时变成未就绪,流量瞬间掉零,故障被放大了一个数量级。
存活探针负责“要不要重启”。它只该检查进程本身是否还活着、有没有死锁,绝对不要检查外部依赖。依赖故障应该由熔断和降级处理,重启解决不了依赖问题,只会让情况更糟。
| 探针 | 检查内容 | 推荐周期 | 失败阈值 | 常见错误 |
|---|---|---|---|---|
| 启动 | 进程是否监听端口 | 5s | 12~24 次 | 没配,慢启动应用被反复重启 |
| 就绪 | 自身初始化是否完成 | 5~10s | 3 次 | 检查下游依赖,导致级联不可用 |
| 存活 | 进程是否死锁 | 10~20s | 3 次 | 检查数据库,依赖抖动引发全量重启 |
4.4 滚动更新与优雅停机
发布有损,八成是这两件事没对上:新的还没就绪就切流量,旧的还没处理完请求就被杀。
滚动更新的两个参数:maxSurge控制最多超出期望副本多少个,maxUnavailable控制最多允许多少个不可用。追求零中断就设maxSurge=25%, maxUnavailable=0,代价是发布期间多占资源、发布时间变长。资源紧张就调成maxSurge=0, maxUnavailable=1,但要接受容量短暂下降。
优雅停机的时序必须严格对齐。编排平台删除 Pod 时,先发终止信号,同时从服务端点里摘除,这两件事是并行的,所以你的应用必须在收到信号后继续服务一小段时间,等摘除生效。典型配置:
spec: template: spec: terminationGracePeriodSeconds: 60 containers: - name: api lifecycle: preStop: exec: command: ["/bin/sh", "-c", "sleep 10"]这段的意思是:收到终止信号后先睡 10 秒,让路由层把流量切走,然后再进入应用自身的关停流程。应用侧要监听终止信号,停止接受新请求,等待进行中的请求处理完(设个上限,比如 15 秒),最后退出。整个链路的时间必须满足:
preStop 10s + 应用排空最长 20s + 缓冲 5s ≤ terminationGracePeriodSeconds 60s这个不等式不成立,就会出现“请求处理到一半连接被重置”的 502。我踩过最隐蔽的一次坑是应用的排空超时设了 30 秒,但terminationGracePeriodSeconds只给了 30 秒,加上 preStop 的 10 秒,40 秒后平台强杀,正好卡在长请求上,每周发布都有零星报错,查了两周才定位到。
4.5 弹性伸缩与调度约束
弹性伸缩看起来很美好,实际有两个必须知道的限制:扩容有延迟(指标采集 + 决策 + 调度 + 启动 + 就绪,通常 30~90 秒),缩容容易抖(指标回落就缩,流量再来又扩,来回震荡)。我的做法是:核心服务保留一个能扛住基线流量的最小副本数(比如 3 个),弹性只负责应对峰值;缩容加稳定窗口(比如 5 分钟),避免抖动。
调度约束方面,反亲和性用来打散副本,避免所有副本落在同一台机器上,机器挂掉就全灭。写法上优先用“尽量打散”(软约束),强制打散(硬约束)在节点数不足时会直接导致 Pod 调度不上去。节点亲和性用来把服务放到特定规格的机器上(比如高内存机型)。污点与容忍配合使用,把专用节点留给特定负载,避免被普通服务抢占。
注意:一定给无状态服务配 PodDisruptionBudget,限制自愿中断时的最小可用副本数。没有它,节点维护时的批量驱逐可能一次性干掉大半副本。
5. 上线之后:可观测性、发布流程与故障排查
编排平台把故障形态改变了:以前是“机器挂了”,现在是“Pod 反复重启”“就绪探针一直失败”“调度不上去”。排查手段也必须跟着变,登录机器看进程那套基本失效了。
5.1 日志、指标、链路的最小落地
三件套不用一步到位,但顺序有讲究:日志先做结构化,输出 JSON,包含时间、级别、服务名、追踪 ID、关键业务字段,采集端统一收集并按服务名打标签。这一步做完,80% 的排查需求就满足了。
指标按 RED 和 USE 两套模型建。服务维度看请求速率、错误率、响应时间分布(RED);资源维度看 CPU、内存、磁盘、网络的使用率、饱和度、错误(USE)。告警不要按单点阈值拍,按“错误率连续 3 分钟超过 1%”这类组合条件,否则天天误报,最后没人看。
链路追踪的采样率要按流量调。低流量服务全采样,高流量服务 1%~5% 采样,同时保证错误请求 100% 采样(尾部采样)。链路数据不做采样会撑爆存储,做了采样又要保证能抓到问题请求,这个平衡点只能靠实际数据量试出来。
三者的关联靠一个东西:追踪 ID 贯穿日志、指标标签和链路。没有这个关联,出问题时你会在三个系统里来回跳,拼不出完整现场。
5.2 发布流程与回滚
发布流程的核心原则是镜像不可变 + 配置声明式 + 回滚即改版本号。具体做法:流水线负责构建镜像、跑测试、打标签、推仓库;部署清单和镜像版本一起提交到配置仓库;部署动作由平台对比期望状态和实际状态完成,不做手工改配置。
灰度发布我一般做三层:先发一个副本观察 10 分钟(看错误率、延迟、日志异常),再发 25%,最后全量。每层之间设人工确认或自动判断条件。回滚就是把镜像标签改回上一个版本,重新应用,整个过程不超过两分钟。前提是你必须在发布前确认上一个版本的镜像还在仓库里,有些团队配了镜像保留策略,只留最近 10 个版本,结果高频发布时旧版本被清理掉,想回滚都回不去。
5.3 常见问题速查表
这张表是我这几年攒下来的,基本覆盖了编排环境里 80% 的故障:
| 现象 | 可能原因 | 排查动作 | 处理方式 |
|---|---|---|---|
| 反复重启 | 应用启动即退出、配置缺失 | 查看上一次容器日志和退出码 | 补配置、修启动逻辑;退出码 137 是内存超限被杀 |
| 镜像拉取失败 | 仓库地址错、凭证过期、标签不存在 | 检查事件里的具体错误 | 修凭证、确认标签存在 |
| 一直处于等待调度 | 资源不足、亲和性冲突、污点未容忍 | 查看调度事件的具体原因 | 调整资源请求、放宽亲和性或加容忍 |
| 就绪探针一直失败 | 探针路径错、端口错、初始化太慢 | 手动在容器内请求探针地址 | 修探针配置或加启动探针 |
| 内存被杀 | 实际用量超 limits | 看历史内存曲线和重启时间点 | 上调 limits 或修内存泄漏 |
| 节点驱逐 | 节点资源压力 | 查看节点状态和驱逐记录 | 调整资源分配、加节点、配优先级 |
| 发布期间零星 502 | 优雅停机时序不对 | 核对 preStop 与排空超时 | 对齐时间链路,见 4.4 节 |
| 调用超时集中在整秒 | DNS 解析超时(常见 5 秒) | 抓包看解析耗时 | 调大解析超时、启用本地缓存 |
5.4 几条踩出来的经验
第一,先做减法再做加法。云原生生态里的组件太多了,每引入一个就多一个故障点和一套运维知识。我的原则是:能用平台原生能力解决的,绝不引入新组件;能用一个组件覆盖的,绝不引入第二个。可观测性先上日志和指标,链路追踪等真的需要跨服务定位时再上。
第二,把所有默认值当成陷阱。编排平台给的默认值是为了“能跑”,不是为了“跑得好”。默认没有资源限制、默认探针不配、默认优雅停机 30 秒,这些默认值在高流量场景下全是雷。上线前过一遍清单:资源、探针、优雅停机、副本数、中断预算、日志输出、健康检查端口,一个都不能漏。
第三,故障演练比文档有用。我们每季度做一次演练:随机杀一个 Pod、随机让一个节点不可用、随机让某个下游服务超时。第一次演练大概率能发现一堆配置问题,比如反亲和性没配导致副本集中在同一台机器、中断预算没配导致驱逐时全挂。这些在真实故障发生前发现,成本是最低的。
第四,给每个服务定一个“负责人 + 关键指标 + 应急手册”。服务数量上去之后,最大的问题不是技术,是“出事了不知道找谁、不知道看什么、不知道先做什么”。一页纸的应急手册(这个服务是干什么的、依赖谁、被谁依赖、挂了影响什么、第一步看哪个面板、怎么快速回滚)比任何架构文档都实用。
6. 演进路线:从最小可用到完整体系
如果让我给一个正在起步的团队排路线,我会这么排:
第一阶段(1~2 个月):把服务打成镜像,统一基础镜像和构建流程,做到“一条命令构建、一条命令本地起全套”。这个阶段的产出是标准化的镜像和可复现的本地环境。
第二阶段(2~3 个月):单集群部署无状态服务,把资源请求、探针、优雅停机、滚动更新四件事配齐,做到“发布零中断、故障自动恢复”。这个阶段的验收标准是随便杀一个 Pod,服务无感知。
第三阶段(3~6 个月):建可观测性和发布流水线,做到“出问题五分钟内定位到服务、十分钟内回滚”。配置管理、日志、指标、告警、灰度发布在这一阶段补齐。
第四阶段(按需):服务治理(限流、熔断、链路追踪)、多集群、成本优化。这一阶段的问题通常是规模带来的,不到规模就别提前上,否则就是给自己找麻烦。
我个人在实际操作中的体会是:云原生这套东西真正的门槛不在工具,而在工程纪律。镜像标签规范化、配置声明式管理、探针认真配、发布流程走完、故障复盘写下来——这些事情每一件都很枯燥,但少做一件,前面搭的所有基础设施都会在某个深夜以故障的形式提醒你。技术选型可以抄,纪律抄不来,只能一点点磨。反过来,一个纪律良好的小团队,用最简单的工具组合,也能把系统跑得很稳;而一个纪律松散的团队,堆再多组件也只是把故障点变多而已。