这是我们平台工程与 SRE 团队 14 个人耗时 87 天,把一套用了七年的"古老交付运维体系 + 手动 SSH 上服务器跑命令部署 + 在我机器上能跑物理机手装环境 + 手动管理进程没有编排 + 手点云控制台开机器无基础设施代码化 + 手改配置文件到处漂移 + 没有 CI 靠人肉构建测试 + 停机部署中断用户 + 出事手动翻日志手动回滚 + 没有监控告警靠用户打电话才知道挂了 + 密钥明文写在配置文件里"的粗放运维体系,整体重构到 2026 年"容器化 Docker 统一环境 + Kubernetes 编排调度 + Terraform/Ansible 基础设施即代码 + GitHub Actions CI/CD 流水线全自动 + 不可变基础设施 + 蓝绿与金丝雀零停机发布 + ArgoCD GitOps 声明式交付 + Prometheus/Grafana/Loki/Jaeger 可观测三件套 + Vault 密钥集中管理 + 自动回滚"现代交付运维体系的真实战役复盘。重构前,我们的交付是典型的"上线要手动登服务器一条条敲命令、环境不一致总有机器跑不起来、部署必停机用户骂声一片、出了事故靠用户打电话才知道、回滚靠人肉记忆翻日志"的危局;一次手抖敲错命令就能让线上服务全挂。重构后,我们用容器统一了环境、用 K8s 接管了编排、用流水线把上线变成一键、用金丝雀让发布零停机、用监控让故障无所遁形。这 87 天里我们沉淀了 47 套工程修法、7 个 P0 事故复盘和 6 条工程哲学,本文毫无保留地分享出来。
需要先说明:DevOps 现代化不是"装个 Docker、搭个 Jenkins"这么简单——它是从"手动登机器、环境靠手装、上线靠人肉、出事靠运气"的粗放运维,跃迁到"环境容器化、编排自动化、交付流水线化、基础设施代码化、发布零停机、系统可观测"的工程化交付的范式更替。下面这张表,概括了我们重构前后在十个核心维度上的对比,每一行背后都是数周攻坚。
| 维度 | 重构前(古老粗放运维) | 重构后(2026 现代工程化) |
|---|---|---|
| 部署方式 | 手动 SSH 跑命令 | CI/CD 流水线自动化 |
| 环境一致性 | 在我机器上能跑 | 容器化 Docker 镜像 |
| 编排调度 | 手动管理进程 | Kubernetes 编排 |
| 基础设施 | 手点控制台开机器 | IaC Terraform/Ansible |
| 配置管理 | 手改配置到处漂移 | 版本化声明式配置 |
| 发布策略 | 停机部署中断用户 | 蓝绿/金丝雀零停机 |
| 基础设施变更 | 可变改出漂移 | 不可变重建替换 |
| 交付模型 | 人肉 kubectl 点操作 | GitOps 声明式 ArgoCD |
| 可观测 | 出事靠用户反馈 | Metrics/Logs/Traces |
| 密钥管理 | 明文写在配置里 | Vault 集中管理 |
一、容器化:从"在我机器上能跑"到 Docker 镜像
重构的第一仗,是消灭环境不一致。古早时代我们的应用直接跑在物理机或虚拟机上,环境靠运维手动安装——这台机器装了 JDK 8、那台不小心装成了 JDK 11,这台的系统库版本和那台又差一截,于是经典的"在我开发机上跑得好好的,一上线就挂"反复上演,排查半天发现是某个依赖版本对不上。容器化(Docker)用镜像把应用连同它的运行时、依赖库、系统环境一起打包成一个不可变的标准单元——镜像在哪跑都一模一样,开发、测试、生产环境彻底一致。下面是一个多阶段构建的 Dockerfile:
# 重构前:应用裸跑在机器上,环境靠手装,这台 JDK8 那台 JDK11,"我机器上能跑"
# 重构后:用 Dockerfile 把应用 + 运行时 + 依赖打包成不可变镜像,到哪跑都一样
# 多阶段构建:第一阶段编译,产物拷到第二阶段,最终镜像不含构建工具体积小
FROM maven:3.9-eclipse-temurin-21 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline # 先拉依赖,利用 Docker 层缓存加速后续构建
COPY src ./src
RUN mvn package -DskipTests # 编译打包
# 第二阶段:只拷贝编译产物到精简运行时镜像,镜像小、攻击面小
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=build /app/target/app.jar ./app.jar
EXPOSE 8080
# 固定基础镜像版本,保证每次构建出的镜像环境完全一致(不可变)
ENTRYPOINT ["java", "-jar", "app.jar"]
容器化让我们的环境管理从"应用裸跑机器上、环境靠手装、版本各不相同、我机器能跑你机器就挂"进化到了"应用与依赖打包成不可变镜像、到哪跑都一模一样":过去应用直接跑在物理机或虚拟机上,每台机器的 JDK 版本、系统库、环境变量都靠运维一台台手动安装,装着装着就出现了这台 JDK8、那台 JDK11、这台少装了个系统依赖的差异,于是开发机上跑得好好的代码一部署到测试或生产就莫名其妙崩溃,排查大半天发现只是某个库版本差了一个小版本,这种'环境不一致'的鬼故事每个迭代都在上演、吃掉大量本不该花的排查时间;现在我们把应用连同它依赖的运行时、库、系统环境一起用 Dockerfile 打包成一个标准化的镜像,这个镜像是不可变的、自包含的,无论是在开发者的笔记本、CI 的构建机、还是生产的服务器上拉起来,跑的都是完全相同的环境,'在我机器上能跑'这句开发者最爱甩的锅从根上消失了。我们的纪律是"一切应用一律容器化交付、基础镜像版本必须固定不用 latest、用多阶段构建让运行镜像精简、镜像一旦构建即不可变"。容器化的本质认知是:'环境不一致'是交付领域最古老也最普遍的痛苦之源——同一份代码在不同环境表现不同,根因从来不是代码而是它脚下那片悄悄发生了漂移的环境;容器的智慧在于把'应用'和'它运行所需的整个环境'打包成一个不可分割、不可变更的整体一起交付,让环境从一个需要小心翼翼手工维护、随时可能漂移的变量,变成一个随代码一起版本化、构建一次处处一致的常量,这是现代交付的第一块基石。
二、编排:从手动管理进程到 Kubernetes 调度
第二仗,是容器的编排。有了镜像还不够——线上要跑几十上百个容器实例,谁来决定它们部署在哪台机器、谁来在容器崩了之后自动拉起、谁来在流量大时自动扩容、谁来做滚动更新和健康检查?古早时代这些全靠人:手动 SSH 到每台机器上 docker run、手动盯着进程别挂了、挂了手动重启、扩容手动加机器再一台台部署,几十个实例就让运维疲于奔命。Kubernetes 是容器编排的事实标准:你只需用声明式的 YAML 描述"我想要这个服务跑 3 个副本、用这个镜像、要这些资源",K8s 就自动把容器调度到合适的节点、持续保证副本数、容器挂了自动重建、还能滚动更新和自动扩缩容。下面是一个 Deployment 的声明:
# 重构前:手动 SSH 到每台机器 docker run,挂了手动重启,扩容手动一台台部署
# 重构后:用声明式 YAML 告诉 K8s "我要什么状态",它自动维持、自愈、调度
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-app
spec:
replicas: 3 # 声明:我要 3 个副本,K8s 持续保证(挂一个自动补一个)
selector:
matchLabels: { app: my-app }
template:
metadata:
labels: { app: my-app }
spec:
containers:
- name: my-app
image: registry/my-app:v1.4.2 # 固定版本镜像,不可变交付
resources:
requests: { cpu: "250m", memory: "256Mi" } # 调度依据
limits: { cpu: "500m", memory: "512Mi" } # 资源上限,防止单容器吃垮节点
readinessProbe: # 就绪探针:没就绪不给它导流量
httpGet: { path: /healthz, port: 8080 }
initialDelaySeconds: 5
livenessProbe: # 存活探针:探测失败自动重启该容器(自愈)
httpGet: { path: /healthz, port: 8080 }
periodSeconds: 10
Kubernetes 让我们的运维从"手动 docker run、人肉盯进程、挂了手动重启、扩容一台台部署"进化到了"声明期望状态、自动调度自愈扩缩、人只管描述不管操作":过去几十上百个容器实例全靠运维用手伺候——上线要 SSH 到每台机器上一条条 docker run,平时要时刻盯着进程别崩、崩了赶紧手动重启,流量大了要手动申请机器再一台台把容器部署上去,流量小了又要手动下线,几十个实例就把运维折腾得焦头烂额、还经常因为漏盯导致某个挂掉的实例几小时没人发现;现在我们只需用一份声明式 YAML 告诉 K8s 我们想要的最终状态——这个服务要跑几个副本、用哪个镜像、需要多少资源、怎么做健康检查,K8s 就自动把容器调度到合适的节点上、持续不断地保证实际副本数等于我们声明的期望数,某个容器崩了它立刻重建一个补上(自愈)、某个节点宕了它把上面的容器迁移到别的节点,配合 HPA 还能根据负载自动扩缩容,运维从'亲手执行每一个操作'变成了'声明想要的结果'、把繁重易错的操作全交给了控制循环。我们的纪律是"一切工作负载跑在 K8s 上、一律声明式 YAML 纳入版本管理、必须配 readiness/liveness 探针让 K8s 能正确导流和自愈、必须设 resources 防止资源争抢"。编排的本质认知是:运维海量容器的核心矛盾,是'人的操作能力'跟不上'实例的数量和变化频率'——靠人手动管理几个进程还行,管理成百上千个不断变化的容器就必然失控;K8s 的精髓是'声明式 + 控制循环':你只声明想要的期望状态、而非一步步的操作步骤,系统则用一个永不停歇的控制循环持续比对'期望'与'现实'、自动采取行动消除差异,把运维从'命令式地亲手操作'升维成'声明式地表达意图',这是驾驭大规模容器的根本范式。
三、CI/CD:从手动 SSH 部署到流水线全自动
第三仗,是交付流程本身。古早时代上线是一套提心吊胆的人肉仪式:开发在本地手动构建出包、用 scp 传到服务器、SSH 上去停掉旧进程、解压新包、改配置、启动、再手动点几下看看活没活——整个过程几十步、全靠人记忆和手感,一步敲错就是事故,而且每次上线都要拉一个熟手盯着、深夜上线更是煎熬。CI/CD 把这套流程彻底自动化:代码一推送,CI 就自动拉代码、跑测试、构建镜像、推到仓库(持续集成),通过后 CD 自动部署到环境(持续交付/部署),全程无需人工干预、每一步都可追溯。下面是一个 GitHub Actions 流水线:
# 重构前:本地手动构建 → scp 传包 → SSH 停旧进程 → 解压 → 改配置 → 启动,几十步全靠手
# 重构后:代码一推,流水线自动跑测试→构建镜像→推仓库→部署,全程零人工
name: CI/CD Pipeline
on:
push:
branches: [main] # 推到 main 自动触发
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests # 持续集成:测试不过直接卡住,绝不让坏代码上线
run: mvn test
- name: Build image # 构建不可变镜像,用 commit SHA 打唯一标签
run: docker build -t registry/my-app:${{ github.sha }} .
- name: Push to registry
run: docker push registry/my-app:${{ github.sha }}
- name: Deploy to K8s # 持续部署:更新镜像版本,K8s 自动滚动更新
run: kubectl set image deployment/my-app my-app=registry/my-app:${{ github.sha }}
# 每次上线都是同一条流水线跑出来的,可重复、可追溯、无需熬夜盯人肉操作
CI/CD 流水线让我们的交付从"本地手动构建、scp 传包、SSH 一条条敲命令、几十步全靠人记忆"进化到了"代码一推全自动跑测试构建部署、可重复可追溯零人工":过去每次上线都是一场心惊肉跳的人肉接力——开发在自己机器上手动 mvn 打个包,用 scp 把包传到服务器,SSH 登上去先停掉正在跑的旧进程,解压新包、手动改几处配置、再启动,然后手忙脚乱地点几下确认服务还活着,整个流程几十个步骤、每一步都依赖操作者的记忆和手感,中间任何一步漏了、敲错了、配置改漏了就是一次线上事故,所以每次上线都得拉个熟手全程盯着、赶上深夜上线更是熬人,且每个人的操作还略有不同、出了问题都难复现;现在代码一推送到 main 分支,流水线就自动接管:拉代码、跑全套测试(测试不通过直接红灯卡死、坏代码根本进不了生产)、构建出用 commit SHA 唯一标记的不可变镜像、推到镜像仓库、再触发部署,全程没有一个手动步骤,每一次上线都是同一条流水线产出的、完全可重复、每一步都有日志可追溯,再也不用熬夜盯人、不用怕谁手抖。我们的纪律是"一切上线必须走流水线、严禁手动 SSH 改生产、测试不过流水线必须红灯卡死、镜像用不可变唯一标签绝不复用 latest"。CI/CD 的本质认知是:人肉操作是不可重复、不可追溯、高度易错的——同样一套上线步骤,十个人会做出十个略有差异的版本,而任何一步的手抖都可能酿成事故,且事后根本无法精确复现到底哪一步出了错;流水线的智慧是把交付流程'代码化'——把那一长串原本靠人记忆和执行的步骤固化成一份确定的、版本化的、机器自动执行的脚本,让每一次交付都精确一致、全程留痕、把人从繁琐易错的重复操作中彻底解放出来,这是把'上线'从一门玄学手艺变成一项可靠工程的关键。
四、IaC:从手点控制台到 Terraform 基础设施即代码
第四仗,是基础设施的管理方式。古早时代我们的服务器、网络、负载均衡、数据库实例全是运维在云厂商控制台上手点出来的——点几十下创建一台机器、配一堆参数、开几个端口,纯靠记忆和文档,结果是没人说得清线上到底有哪些资源、是怎么配的,换个人接手两眼一抹黑,想在另一个区域复制一套一模一样的环境更是难如登天,只能照着模糊的记忆再手点一遍、还点得到处不一样。基础设施即代码(IaC,如 Terraform)把基础设施也变成代码:用声明式的配置文件描述你想要的资源(几台机器、什么规格、什么网络),Terraform 自动调用云 API 把它创建出来,配置纳入 Git 版本管理。下面是一段 Terraform 配置:
# 重构前:在云控制台手点几十下开机器、配网络,没人说得清线上有啥、怎么配的
# 重构后:用 Terraform 把基础设施写成代码,声明想要的资源,自动创建、版本化
resource "aws_instance" "web" {
count = 3 # 声明:我要 3 台一模一样的机器
ami = "ami-0abc123" # 固定镜像,保证每台环境一致
instance_type = "t3.medium"
subnet_id = aws_subnet.private.id
tags = { Name = "web-server", Env = "prod" }
}
resource "aws_lb" "app" { # 负载均衡也代码化
name = "app-lb"
load_balancer_type = "application"
subnets = [aws_subnet.public.id]
}
# terraform plan:先预览将要做的变更(增/改/删),确认无误再 apply
# terraform apply:自动调用云 API 把声明的资源创建/更新到位
# 整套基础设施纳入 Git:谁改了什么一目了然,换区域复制只需 apply 一遍
IaC 让我们的基础设施管理从"控制台手点、没人说得清有啥、换人接手抓瞎、复制环境靠手点重来"进化到了"基础设施写成代码、声明即创建、纳入版本管理、一键复制全一致":过去线上的每一台服务器、每一个负载均衡、每一条网络规则、每一个数据库实例,都是运维当年在云厂商的控制台上一下一下手点出来的,点的时候凭的是记忆和零散文档,点完也没有任何权威记录,日积月累下来根本没人能说清楚线上到底有哪些资源、每个资源是怎么配置的、为什么这么配,老员工一走新人接手就是一团乱麻,想在灾备区域复制一套和生产一模一样的环境更是噩梦,只能对着模糊的记忆和过期的文档再手点一遍、结果点出来的环境处处和生产对不上、灾备时才发现根本切不过去;现在我们用 Terraform 把全部基础设施都写成声明式的代码——要几台什么规格的机器、什么样的网络拓扑、什么负载均衡,全部明明白白写在配置文件里,terraform apply 一下就自动调用云 API 把这些资源精确创建出来,而这套代码纳入 Git 版本管理后,线上有什么、谁在何时改了什么、为什么改全都一清二楚,换个区域复制环境只需把代码 apply 一遍、产出的环境和生产分毫不差。我们的纪律是"一切基础设施一律 IaC 管理、严禁手点控制台改线上资源、变更前必须 terraform plan 预览、IaC 代码纳入 Git 走评审"。IaC 的本质认知是:手点出来的基础设施是'不可知、不可复现、不可审计'的——它的真实状态只存在于云控制台的某个角落和某个人的记忆里,无法被审阅、无法被版本化、无法被可靠地重建;IaC 的智慧是把基础设施这个原本'手工操作的对象'也纳入'代码'的世界,让它和应用代码一样可以被版本控制、被代码评审、被一键复现,基础设施从此从一堆没人说得清的'手工艺品'变成了一份人人可读、处处可复制、变更有迹可循的'源代码',这是让大规模基础设施变得可管理、可审计、可灾备的根本。
五、发布策略:从停机部署到金丝雀/蓝绿零停机
第五仗,是发布的方式。古早时代我们发布新版本是'停机部署':先挂个'系统维护中'的公告、停掉所有旧实例、部署新版本、再启动——这期间服务完全不可用,用户访问就是一片空白,只能挑半夜业务低谷期硬着头皮上,即便如此也总有用户被中断;更要命的是万一新版本有问题,得手忙脚乱地再停机一次回滚旧版本,故障时间成倍拉长。现代做法是零停机发布:蓝绿部署准备两套完全相同的环境(蓝、绿),新版本先在绿环境部署好、验证通过后流量瞬间从蓝切到绿;金丝雀发布则更稳健——新版本先只接一小撮(如 5%)流量当'金丝雀',盯着监控没问题再逐步放量到全量,有问题立刻切回。下面是金丝雀发布的流量切分:
# 重构前:停机部署——挂维护公告→停全部旧实例→部署新版→启动,期间服务全挂
# 重构后:金丝雀发布——新版先接 5% 流量,盯监控没问题再逐步放量,有问题秒回切
# 用 Argo Rollouts 做金丝雀:流量按比例渐进切给新版本,每步都自动看监控
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: my-app
spec:
strategy:
canary:
steps:
- setWeight: 5 # 第一步:新版只接 5% 流量当"金丝雀"探路
- pause: { duration: 5m } # 暂停 5 分钟,盯着错误率和延迟监控
- setWeight: 25 # 没问题:放量到 25%
- pause: { duration: 5m }
- setWeight: 50 # 继续放量到 50%
- pause: { duration: 5m }
- setWeight: 100 # 全量切换,老版本下线
# 任何一步监控指标异常(错误率飙升)就自动暂停并回滚到老版本
# 全程老版本一直在服务,新版本逐步接管,用户无感知、零停机
零停机发布让我们的上线从"停机部署、服务全挂、只能半夜上、回滚还要再停一次"进化到了"金丝雀渐进放量、老版本一直在、用户无感知、有问题秒回切":过去发布新版本就是一场必然中断用户的'停机仪式'——先在页面挂出'系统维护中'的公告,然后把所有旧实例统统停掉、部署新版本、再重新启动,这中间几分钟到几十分钟服务是彻底不可用的,用户访问看到的就是一片空白或维护页,所以我们只敢挑凌晨业务最低谷的时候硬着头皮上、运维深夜值守,可即便是低谷也总有用户正好撞上被中断,而最怕的是新版本上去发现有 bug,这时只能手忙脚乱地再来一次停机、把旧版本部署回去,一来一回故障时间成倍拉长、用户骂声一片;现在我们用金丝雀发布,新版本上线时老版本一直好好地在服务着,新版本先只接 5% 的流量当'金丝雀'去探路,我们紧盯着这 5% 流量的错误率和延迟监控,确认新版本健康无恙才把流量逐步放大到 25%、50%、直至 100% 全量接管、老版本才下线,整个过程用户完全无感知、服务零停机,而一旦在某一步发现监控指标异常,系统立刻自动暂停放量并把流量秒切回老版本、爆炸半径被牢牢控制在那一小撮金丝雀流量之内。我们的纪律是"一律零停机发布、重要服务用金丝雀渐进放量、放量每一步都必须有监控门禁、回滚必须是一键秒级而非再来一次部署"。发布策略的本质认知是:'停机部署'把'发布'和'可用性'对立了起来——为了换上新版本,就必须牺牲掉这段时间的服务可用,而且把全部用户一次性暴露给未经生产验证的新版本、风险拉满;零停机发布的智慧在于'新老并存、渐进切换':让新旧两个版本在一段时间内同时存在,用流量的逐步迁移替代'一刀切'的全量替换,既保证了切换过程中服务从不中断,又把新版本的风险用'先小流量验证再放量'的方式控制在最小范围,把发布从一次孤注一掷的豪赌,变成了一个步步为营、随时可退的可控过程。
六、GitOps:从人肉 kubectl 到声明式的 ArgoCD
第六仗,是交付的控制模型。即便上了 K8s,古早时代我们改线上状态还是靠人手动 kubectl apply、手动 set image——谁有权限谁就能直接改集群,改了什么没记录、和代码仓库里的配置渐渐对不上(配置漂移),出了问题都不知道当前线上到底是哪个版本的配置。GitOps 把 Git 仓库作为'唯一可信源':所有期望状态(部署什么版本、什么配置)都声明在 Git 里,ArgoCD 这类工具持续监控集群实际状态与 Git 里声明的期望状态,一旦发现不一致就自动把集群拉回到 Git 描述的样子。改线上不再是手动操作集群,而是提交一个 Git PR、合并后自动同步。GitOps 让我们的交付控制从"谁有权限谁手动 kubectl 改集群、改了没记录、配置漂移、不知线上是啥状态"进化到了"Git 是唯一可信源、声明即生效、漂移自动纠正、全程可审计可回滚":过去就算工作负载已经跑在 K8s 上,改动线上状态还是靠人手动执行 kubectl apply 或 set image,这意味着任何有集群权限的人都能直接动生产、改完没有任何权威记录、时间一长集群里实际跑的配置和代码仓库里存的配置就悄悄对不上了(配置漂移),真出了事谁也说不清当前线上到底是哪一版配置、是谁在什么时候改的;现在我们落地 GitOps,把'线上应该是什么状态'这件事全部声明在 Git 仓库里、让 Git 成为唯一可信源,ArgoCD 持续地比对集群实际状态和 Git 里的期望状态,一旦发现有人手动改了集群导致漂移、它就自动把集群拉回到 Git 描述的样子,而我们要改线上不再是登上集群手动操作、而是提交一个 Git PR 走评审、合并后由 ArgoCD 自动同步生效,每一次变更都是一个有作者、有时间、有评审、可追溯、可一键回滚的 Git 提交。我们的纪律是"线上期望状态一律声明在 Git、严禁手动 kubectl 改生产、变更一律走 PR 评审、用 ArgoCD 自动纠正一切配置漂移"。GitOps 的本质认知是:当'线上的真实状态'和'代码里记录的状态'可以不一致时,系统就进入了一种没人说得清、没法复现、无从审计的混沌——手动操作集群正是制造这种不一致的源头;GitOps 的智慧是确立'Git 是唯一可信源'这一铁律,让 Git 仓库里声明的期望状态成为线上状态的唯一权威定义、并用自动化的同步与纠偏机制保证现实永远向这个定义看齐,于是'改线上'这个原本危险、随意、无痕的操作,被收敛成了'改 Git'这个安全、规范、全程留痕的工程动作。
七、可观测:从靠用户反馈到 Metrics/Logs/Traces 三件套
第七仗,是系统的可观测性。古早时代线上出了问题,我们往往是最后一个知道的——服务挂了、变慢了,我们浑然不觉,直到用户打电话来投诉、或者老板在群里问'是不是又挂了',才手忙脚乱地 SSH 上一台台机器 grep 日志,凭经验和运气猜哪里出了问题,故障定位动辄几个小时。现代可观测性建立在三大支柱上:Metrics(指标,如 QPS、错误率、延迟、资源使用,用 Prometheus 采集、Grafana 可视化,配告警主动通知)、Logs(结构化日志,用 Loki 集中收集检索)、Traces(分布式追踪,用 Jaeger 串起一个请求在各服务间的完整调用链)。可观测三件套让我们的故障应对从"用户打电话才知道挂了、SSH 一台台 grep 日志靠猜、定位几小时"进化到了"指标告警主动推送、日志集中检索、追踪一键还原调用链、分钟级定位":过去线上服务挂了或变慢了,我们这些本该最先知道的人却往往蒙在鼓里,因为根本没有主动监控,只能等到用户忍无可忍打来投诉电话、或者老板在群里发问的时候才如梦初醒,然后开始手忙脚乱地 SSH 登上一台又一台机器、用 grep 在浩如烟海的日志里大海捞针、凭着经验和运气去猜到底是哪个环节出了问题,一次故障的定位经常要耗掉几个小时、用户早就跑光了;现在我们建起了可观测三件套——用 Prometheus 持续采集每个服务的 QPS、错误率、延迟、CPU 内存等关键指标并用 Grafana 做成大盘、配上告警规则在指标异常的第一时间就主动把告警推到我们手机上(而不是等用户来告诉我们),用 Loki 把所有服务的结构化日志集中收集起来一处检索、再不用一台台机器去 grep,用 Jaeger 做分布式追踪把一个请求流经的所有服务的调用链完整串起来、一眼就能看出是哪个环节慢了或错了,故障定位从几小时缩短到几分钟。我们的纪律是"核心服务必须暴露 Metrics 并配关键告警、日志一律结构化集中收集、跨服务调用必须传递 trace 上下文、告警要先于用户发现故障"。可观测的本质认知是:你无法管理你看不见的东西——一个没有可观测性的系统就像一个没有仪表盘的黑箱,它健康还是带病、快还是慢、为什么出问题,运维全靠事后猜测和用户投诉;可观测的智慧是用 Metrics、Logs、Traces 这三个互补的视角,分别回答'系统现在状态如何(指标)、具体发生了什么(日志)、一个请求到底经历了什么(追踪)',把系统从一个只能事后扒拉日志去猜的黑箱,变成一个内部运行状态随时可被观测、问题能被主动发现和快速定位的透明系统,这是从'被动救火'走向'主动运维'的前提。
八、迁移策略:容器化试点、双轨并行与逐步收口
第八仗,是迁移本身。把一套跑了七年的祖传运维体系整体搬到云原生,牵扯到几十个服务、一堆有状态的中间件,绝不能推倒重来、一步到位。我们的策略处处求稳:第一,先挑一两个边界清晰、无状态的非核心服务做容器化和上 K8s 的试点,跑通整条流水线、踩平坑、沉淀规范,再逐步推广到更多服务;第二,新老体系双轨并行——老的物理机部署和新的 K8s 部署同时存在一段时间,流量逐步从老链路灰度切到新链路,新链路出问题随时切回老的;第三,有状态服务(数据库、消息队列)最后才动、且优先用云托管服务而非自己在 K8s 里硬扛,降低风险。稳健的迁移策略让我们在不影响线上业务的前提下,把祖传运维体系平滑搬到了云原生:先拿无状态非核心服务做试点跑通流水线、踩平坑、立好规范,再分批推广;新老体系双轨并行、流量逐步灰度切换、新链路有问题随时切回老链路;有状态的数据库消息队列放到最后、优先用云托管服务而非自建硬扛,把最危险的部分风险降到最低。最关键的纪律是"迁移一律分批试点而非一刀切、新老双轨并行留足回退余地、有状态服务最后动且优先托管、每一批都跑通验证才推进下一批"。DevOps 迁移的本质智慧是:基础设施和交付体系是支撑所有业务运行的地基,在地基上施工的最大风险是动静太大震塌了上面的房子——因此迁移的最高准则是'小步试点、双轨过渡、风险后置',用试点把未知的坑在小范围内提前踩平、用新老并行的双轨给自己留足随时回退的退路、把风险最高的有状态部分放到经验最足、规范最全的最后阶段去做,把一次伤筋动骨的整体迁移,拆解成一连串可验证、可回退、风险可控的平滑过渡。
九、7 个 P0 事故复盘
7 事故:(1) 手动 SSH 改生产配置敲错命令致全站宕机,一切变更收口到流水线和 GitOps、禁手动改生产;(2) 镜像用 latest 标签导致回滚时拉到的还是问题版本,镜像一律用 commit SHA 唯一标签、永不复用 latest;(3) 容器没设 resources limits 单实例内存泄漏吃垮整个节点、连累同节点其他服务,所有容器强制设 requests/limits;(4) 停机部署窗口新版本有 bug 来不及回滚故障拉长,全面改金丝雀渐进发布、回滚一键秒级;(5) 密钥明文写在配置仓库被泄露,密钥全部迁入 Vault 集中管理、配置里只存引用;(6) 没有告警服务挂了俩小时靠用户投诉才发现,核心服务全部配指标告警、必须先于用户发现;(7) 手动 kubectl 改了集群与 Git 配置漂移、重启后变更丢失,落地 ArgoCD 让 Git 成唯一可信源自动纠偏。每个 P0 都做 5-Why 复盘,固化成上线审查清单、发布门禁规范或告警基线,确保同类问题不再复发。
十、平台工程师的 6 条工程哲学
6 哲学:(1) 一切皆代码——应用、基础设施、配置、流水线都应版本化、可评审、可复现,绝不靠手点和记忆;(2) 声明而非操作——描述你想要的最终状态、把如何达成交给系统的控制循环,人管意图、机器管执行;(3) 不可变胜过可变——基础设施宁可重建替换也不原地手改,杜绝漂移;(4) 自动化一切重复——凡是人要重复做第二遍的操作都应该被自动化,人只做决策不做体力活;(5) 你无法管理看不见的东西——可观测性是运维的前提,告警要先于用户发现故障;(6) 为失败而设计——发布要能秒级回滚、依赖要能优雅降级、变更要留足退路。这 6 条哲学,是我们用 7 个 P0 事故和 87 天攻坚换来的集体共识。它们共同指向一个认知:DevOps 现代化的价值不在于"上了 K8s 还是用了 ArgoCD"这个动作本身,而在于把"交付的快、稳、可控"从依赖运维个人的细心、记忆和运气,前移成了由工程机制(容器化、编排、流水线、IaC、GitOps、可观测)结构性保障——会做现代平台工程的团队,是在用机制把一整类"环境不一致、手抖事故、配置漂移、停机中断、故障盲区、密钥泄露"的问题从源头消除,而不只是在事后救火。
十一、重构收益的量化:7 个关键数字
7 数字:(1) 一次上线耗时:手动几十步小时级 → 流水线全自动后分钟级;(2) 部署频率:怕出事一周才敢发一次 → 自动化后可日发多次;(3) 环境不一致引发的故障:每迭代都有 → 容器化后归零;(4) 发布导致的停机时间:每次停机几十分钟 → 金丝雀后零停机;(5) 故障平均定位时间(MTTR):靠 grep 猜几小时 → 可观测三件套后分钟级;(6) 配置漂移导致的诡异问题:频发 → GitOps 自动纠偏后归零;(7) 密钥泄露风险:明文写配置 → Vault 集中管理后归零。这些数字背后,是 87 天里 14 个人无数次的容器化改造、流水线搭建、IaC 编写、发布策略打磨和可观测建设,但每一个都实打实地转化成了交付效率、稳定性和安全性的提升。当我们把这份数据汇报给管理层时,最有说服力的不是任何云原生名词,而是"上线从提心吊胆熬夜变成一键自动、出事故能在用户投诉前就发现并秒级回滚"这两条。
十二、留给后来者的最后一句话
87 天的 DevOps 现代化战役,我们走过的不只是一条从手动 SSH 到流水线、从裸跑到容器、从手管进程到 K8s、从手点控制台到 IaC、从停机部署到金丝雀、从人肉 kubectl 到 GitOps、从靠用户反馈到可观测三件套的技术升级路,更是一次从"靠运维记得每一步命令、记得改每一处配置、记得盯每一个进程"到"靠工程机制和自动化结构性兜底"的运维范式跃迁。当上线从一场需要熬夜值守、提心吊胆的人肉仪式变成代码一推就自动完成的一键流水线、当'在我机器上能跑'的甩锅因为容器化而彻底消失、当发布在金丝雀的渐进放量中对用户悄无声息零停机、当一个故障在用户还没察觉时告警就已推到我们手机上并自动回滚、当线上的每一处状态都和 Git 里声明的分毫不差再无漂移的那一刻,真正点燃我们的,不是用了 K8s 还是 ArgoCD 本身,而是"交付的快、稳和可控,终于从依赖运维个人的细心和运气,变成了由机制和自动化强制保障"的踏实与笃定。DevOps 现代化没有银弹,关键是理解容器、编排、流水线、IaC、GitOps、可观测各自解决什么问题、又各自带来什么代价,然后从环境一致和交付自动化的地基起步、用试点双轨可回退的方式落地——尤其要克制"图省事手动 SSH 上去改一下、图省事用个 latest 标签、图省事把密钥明文写进配置"的旧习惯,因为每一次手动改生产、每一个偷懒的 latest、每一处明文的密钥,都是在亲手埋下未来某次上线的事故或某个深夜的告警。愿每一位还在和环境不一致、手抖事故、配置漂移、故障盲区搏斗的同行,都能早日让自己的交付被工程机制稳稳地守护。共勉,后会有期。
—— 别看了 · 2026