本页定位 · DevOps / SRE Advanced

FDE 模块 02/08 讲了 Docker、K8s、CI/CD、Prometheus 的"怎么做"。本页把它们串成工程方法论:可观测性三支柱(指标/日志/链路)与 OpenTelemetry 标准、GitOps、Terraform 状态深入、SLO/错误预算、混沌工程、事故复盘。学完你不再是"会敲 kubectl",而是能设计一条"可观测、可复用、可回滚"的可靠交付链路。

1

CI/CD 流水线:阶段、制品、门禁、GitOps

Pipeline · Artifact · Quality Gate · GitOps

CI(持续集成)让每次提交都自动构建测试;CD(持续交付/部署)把产物自动推向环境。但真正的成熟标志是"可回滚"——任何一次发布都应该能一键退回上一版,而不是"上线即赌命"。这一章把流水线拆成可复用的零件。

一条流水线的典型阶段

把交付想成一条传送带:代码进左端,能放心跑在生产就是出右端。中间每一段做一件事、且失败就截断。

阶段做什么失败后果
① 构建 Build编译/打镜像代码都编不过,立刻红
② 测试 Test单测+集成+静态质量门禁拦截
③ 制品 Artifact出不可变版本产物无法复现
④ 部署 Deploy推到环境回滚到上一版本
⑤ 验证 Verify健康检查/冒烟自动回滚

阶段之间用"门"隔开:任一段失败,后续段不执行,并通知提交者。这保证"坏变更走不到生产"。

把阶段写进 CI:可抄的配置

下面是一条覆盖"构建→测试→出镜像"的 GitHub Actions 流水线。注意它把测试失败设成硬门槛,且镜像用 commit SHA 打标签(不可变)。

# .github/workflows/ci.yml on: [push, pull_request] jobs: build-test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: docker build -t app:${{ github.sha }} . # 用 SHA 打不可变标签 - run: docker run app:${{ github.sha }} pytest # 容器内跑测试 publish: needs: build-test # 测试过才发 runs-on: ubuntu-latest steps: - run: docker push registry/app:${{ github.sha }}

论为什么用 commit SHA 而不是 latest

latest 是移动的靶子——你不知道生产跑的是哪次构建。每个制品用唯一版本(SHA/语义版本)标记,部署和回滚都能精确定位"这一份",也便于审计"哪个版本引入了 bug"。

制品与不可变版本:部署的是"同一份"

"不可变制品"指:构建一次,测试通过,之后无论部署到测试/预发/生产,用的都是同一个二进制/镜像,绝不在环境间重新编译。否则"测试环境好的"和"生产跑的"可能不是同一份,排查无从下手。

做法:构建时打唯一版本(如 1.4.2 或 commit SHA),"晋级"时只改引用、不重新构建。测试环境验过的那一份,原封不动推上生产。

# 晋级而非重建:同一镜像,不同环境只换 tag 引用 image: registry/order:1.4.2 # 测试、预发、生产都用这同一份 # 环境差异靠环境变量 / ConfigMap 注入,绝不重新打镜像
新手最常犯:环境里现场改配置

SSH 上服务器改个配置"先救火"——下次部署这份改动就丢了,还和代码库不一致。配置要么进版本库(GitOps),要么进配置中心,永远别让生产状态脱离代码。

质量门禁嵌在流水线

门禁是流水线的"闸":覆盖率、安全扫描、复杂度任一不过,就不许进下一阶段。它把质量规则从"人自觉"变成"机器强制"。

# 门禁示例:测试 + 覆盖率 + 镜像扫描,全过才发 pytest --cov=app --cov-fail-under=80 trivy image app:${{ github.sha }} # 扫镜像已知漏洞,高危则非零退出 # 任一步退出码非 0,流水线即红,无法继续部署

蓝绿 / 金丝雀:把"全量故障"变成"可控试错"

论两种发布策略的本质

蓝绿:两套环境,切流量瞬间完成,回滚也只切回去(秒级)。金丝雀:先放 1% 流量验证,再逐步放大到 10%/50%/100%。两者目的都是把"全量故障"变成"可控的小范围试错",出问题影响面小、回退快。

GitOps:以 Git 为唯一事实源

传统做法是"人敲命令改集群";GitOps 把期望状态写进 Git,由控制器(ArgoCD/Flux)持续把集群拉向 Git 里的状态。好处:每次变更有 PR 记录、可审计、可一键回滚(回退 commit 即可)。

# ArgoCD Application:声明"集群该长什么样" apiVersion: argoproj.io/v1alpha1 kind: Application spec: source: repoURL: https://github.com/org/gitops path: apps/order # Git 里存着期望的 K8s 清单 destination: server: https://k8s.prod namespace: order syncPolicy: automated: { prune: true, selfHeal: true } # 自动对齐 + 自愈漂移

论GitOps vs 普通 CI/CD

CI/CD 管"怎么构建发布"(推 push),GitOps 管"集群该是什么样、并自动维持"(拉 pull + 持续对齐)。后者每次变更可审计、回退 commit 即回滚,且自动修正配置漂移,更抗"生产被手改坏"。

流水线反模式:这些坑别踩

CI/CD 常见反模式

① 半成品合并:直接往 main 推、不跑 PR 检查,靠"反正能跑"。② 门禁形同虚设:覆盖率设了但 --cov-fail-under 写 0,永远绿。③ 手工改生产:部署后 SSH 上去改配置,下次部署覆盖、还和 Git 不一致。④ 巨型单体流水线:改一行前端也要等后端全量测完,反馈慢到没人等。

论小步快跑才是王道

把大流水线拆成"快门禁(秒级)+ 慢验证(分钟级)"两层,主分支保护靠 PR 检查,长任务放合并后异步跑。目标是"提交后几分钟内给可信反馈"。

2

容器 Docker:分层、多阶段、体积与安全

Image Layers · Multi-stage · Size · Security

FDE 模块 02 给了基础 Dockerfile。这一章讲清镜像为什么分层、多阶段构建怎么把镜像砍小、体积优化技巧、以及镜像安全(别用 root、别用 latest、要扫描)——这些直接决定部署速度与攻击面。

镜像分层原理:为什么顺序很重要

Docker 镜像由一层层(layer)叠成,每层是"上层的差异"。层是缓存单元:某层不变,构建就复用缓存;某层一变,它之后所有层都失效重跑。所以"变动少的放前面,变动多的放后面"。

论依赖放前面 = 构建快十倍

把"装依赖"(很少变)和"拷源码"(常变)分开:先 COPY package.json 装依赖并缓存,再 COPY 源码。改一行业务代码时,依赖层命中缓存,不用重装几百个包。

# 顺序的艺术:依赖层在前、源码层在后 FROM python:3.12-slim WORKDIR /app COPY requirements.txt . # ① 先拷依赖清单 RUN pip install -r requirements.txt # ② 装依赖(很少变,被缓存) COPY . . # ③ 再拷源码(常变,层失效无妨) CMD ["python", "app.py"]

多阶段构建:构建环境和运行环境分离

编译需要一整套工具链(JDK、gcc),但运行只需要产物。多阶段构建用"胖阶段"编译、把产物拷进"瘦阶段"运行,最终镜像不含编译器,体积和漏洞都大减。

# 多阶段:第一阶段编译,第二阶段只跑 jar FROM maven:3.9-eclipse-temurin-21 AS build # 胖:含 JDK+ Maven WORKDIR /src COPY . . RUN mvn -q package -DskipTests FROM eclipse-temurin:21-jre # 瘦:只有 JRE COPY --from=build /src/target/app.jar /app.jar CMD ["java", "-jar", "/app.jar"]
新手最常犯:把构建工具带进生产镜像

一个 FROM maven 直接 CMD java 的镜像可能 800MB+ 且带着 git/ssh 密钥。多阶段后常能砍到 1/10,启动更快、被攻破的面更小。

体积优化技巧清单

技巧效果
多阶段构建去掉编译器/构建依赖
用 slim/alpine 基础镜像去文档与多余工具
合并 RUN + 清缓存减少层内垃圾
.dockerignore别把 .git/node_modules 打进上下文
非 root 运行降权限(见下节)
# .dockerignore:别把没必要的一起发给 docker daemon .git node_modules *.log venv

镜像安全:别用 root、别用 latest、要扫描

镜像默认常以 root 跑、拉 latest 不可复现、还可能带已知 CVE。三条铁律:固定基础镜像版本、非 root 运行、CI 里扫漏洞。

# 安全基线:固定版本 + 建普通用户 + 降权运行 FROM python:3.12-slim RUN useradd -m appuser USER appuser # 关键:不用 root 跑业务进程 COPY --chown=appuser . /app CMD ["python", "/app/app.py"]
latest 是运维噩梦

FROM python:latest 意味着"下次构建可能悄悄换了大版本",今天绿明天红。永远钉版本(python:3.12.4-slim),并定期升级而非被动漂移。

容器运行时的隐藏陷阱

镜像对了,运行时还可能翻车:PID 1 不转发信号导致优雅停机失效、写进容器层的文件重启即丢、资源不设限被一个容器拖垮整台机。

论有状态别放容器里

容器文件系统是易逝的——重启/调度走,数据就没。数据库、缓存的持久化必须挂卷(volume)或走外部存储。把容器当"随时可换的无状态积木",状态交给专门的基础设施。

镜像供应链:签名与 SBOM

镜像不只是"能跑",还要"可信"。SBOM(软件物料清单)列清镜像里每一层装了什么依赖,签名(cosign)证明"这镜像确实出自我们的流水线、没被篡改"。供应链攻击(投毒依赖)的防线就在这。

# cosign:给镜像签名,部署时校验来源 cosign sign registry/app:1.4.2 # 用密钥签名 cosign verify registry/app:1.4.2 # 集群侧校验,未签名拒绝运行 # SBOM:生成物料清单,配合漏洞扫描溯源 syft registry/app:1.4.2 -o spdx-json > sbom.json

论零信任也适用于镜像

别假设"内网拉的镜像就安全"。依赖可能已被投毒。签名 + SBOM + 定期重扫,让你在"某依赖曝出 CVE"时一秒查出"哪些镜像受影响、该先修谁"。

BuildKit 与缓存加速:别每次重装依赖

默认 docker build 在 CI 里每次都从头来,慢。开启 BuildKit + 挂载缓存(cache mount),让依赖下载跨构建复用,常能把"装依赖"从几分钟压到几秒。

# 用 --mount=type=cache 复用 pip/npm 缓存目录 RUN --mount=type=cache,target=/root/.cache/pip \ pip install -r requirements.txt # 配合 --build-arg BUILDKIT_INLINE_CACHE=1 还能推拉层缓存

论构建慢是反馈慢的源头

"改一行等十分钟构建"的团队,会本能地少提交、少测。把镜像构建压到秒级,CI 才敢高频跑,快速反馈闭环才转得起来。

3

编排 Kubernetes:Pod / Deployment / Service / Ingress

K8s · HPA · 存储 · 探针

FDE 模块 02 给了最小 K8s 例子。这一章把核心对象讲透:Pod 是最小调度单位、Deployment 管副本与滚动更新、Service 做稳定寻址、Ingress 管外部流量,再加 HPA 自动扩缩、存储与探针。目标:能读懂一份真实清单。

核心对象:Pod 与 Deployment

Pod 是"一个或多个共享网络的容器"的最小运行单元;Deployment 在它之上管"我要几个副本、怎么滚动更新、挂了怎么拉起"。你几乎不直接操作 Pod,而是操作 Deployment。

# Deployment:保证始终有 3 个 order 副本在跑 apiVersion: apps/v1 kind: Deployment metadata: { name: order } spec: replicas: 3 selector: { matchLabels: { app: order } } template: metadata: { labels: { app: order } } spec: containers: - name: order image: registry/order:1.4.2 # 固定版本,别用 latest ports: [{ containerPort: 8080 }]

Service 与 Ingress:流量怎么进来

Pod 的 IP 会随重启变,不能直接对外暴露。Service 给一组 Pod 一个稳定虚拟 IP 和 DNS 名(按标签选后端);Ingress 在集群边缘把外部 HTTP 路由到内部 Service。

# Service:给 order 副本一个稳定入口 apiVersion: v1 kind: Service metadata: { name: order } spec: selector: { app: order } # 自动负载到匹配的 Pod ports: [{ port: 80, targetPort: 8080 }] --- # Ingress:外部域名 → 内部 Service apiVersion: networking.k8s.io/v1 kind: Ingress spec: rules: - host: order.example.com http: paths: - path: / pathType: Prefix backend: { service: { name: order, port: { number: 80 } } }

论为什么需要 Service 这层

没有 Service,调用方得自己跟踪"现在哪几个 Pod 活着、IP 是多少"。Service 用标签选择器 + 端点自动同步,把"寻址"从应用里抽出来交给集群,应用只认一个稳定名字。

HPA:按指标自动扩缩

流量有峰谷,人工调副本数不现实。Horizontal Pod Autoscaler 按 CPU/内存或自定义指标自动增减副本,峰值来了自动扩容、闲时自动缩容省钱。

# HPA:CPU 超 70% 就扩,最少 3 最多 20 个 apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler spec: scaleTargetRef: { kind: Deployment, name: order } minReplicas: 3 maxReplicas: 20 metrics: - type: Resource resource: { name: cpu, target: { type: Utilization, averageUtilization: 70 } }

存储:PV / PVC / ConfigMap / Secret

容器易逝,状态要外置。PVC(持久卷声明)向集群要一块持久盘;ConfigMap 存非机密配置;Secret 存密码/令牌(虽叫 Secret,默认只 base64,需配合加密)。

# PVC 挂给 Pod,数据跨重启存活 apiVersion: v1 kind: PersistentVolumeClaim metadata: { name: order-data } spec: accessModes: [ReadWriteOnce] resources: { requests: { storage: 20Gi } } # Pod 里:volumes: [{ name: d, persistentVolumeClaim: { claimName: order-data } }]
Secret 不是加密保险箱

K8s Secret 默认只是 base64 编码,谁有读权限就能解码。生产要开 etcd 静态加密或接外部密钥管理(Vault),且别把 Secret 写进镜像或 Git。

就绪与存活探针:让更新不中断

就绪探针(readiness):没准备好就不接流量(滚动更新时新 Pod 没热好不进流量)。存活探针(liveness):挂了就重启。两者分清,否则"启动慢"会被误杀、"假死"却还在接流量。

# 探针:/healthz 探活,/readyz 探就绪 livenessProbe: httpGet: { path: /healthz, port: 8080 } initialDelaySeconds: 5 readinessProbe: httpGet: { path: /readyz, port: 8080 } periodSeconds: 5

论探针是"零停机发布"的底座

滚动更新时,K8s 先起新 Pod,等它就绪探针过了才把旧 Pod 摘下——用户全程无感知。没配探针,新 Pod 一启动就被灌流量,可能返回 500。

常见坑:就绪探针只探"端口通了"而非"能服务",于是缓存还没热就被放流量。探针要探真实依赖(如能连上 DB),别只探端口。

常见 K8s 事故清单

新手最常踩的 K8s 坑

① 没设资源 limit:一个 Pod 吃光节点,邻居全被挤走。② 用 latest:滚动更新拉到不同版本。③ 探针路径错:Pod 一直 not ready,副本永远 0。④ 把数据库跑在容器里没挂卷:Pod 一重建数据没了。

资源 Request / Limit:配错就崩或就浪费

Request 是"我至少要多少"(调度器据此排节点、保证预留);Limit 是"最多能用多少"(超了被 throttle 或 OOMKill)。两者都没设,节点会被一个 Pod 撑爆;设太宽,资源利用率低、成本高。

# 合理设 request/limit:保证够用、又留突发空间 resources: requests: { cpu: 250m, memory: 256Mi } # 调度保底 limits: { cpu: 500m, memory: 512Mi } # 封顶,防单 Pod 拖垮节点
只设 limit 不设 request 的坑

只写 limit 时,K8s 会把 request 默认成 0,调度器以为这 Pod 不占资源,可能把一堆都塞到同一节点,实际一起跑就抢 CPU、相互 throttle。生产务必 request/limit 成对配。

4

IaC:Terraform / Ansible、声明式与漂移

Infrastructure as Code · State · Drift

FDE 模块 02 给了 Terraform 示意。深入点:IaC 用代码描述基础设施,让环境可版本化、可重现。本章讲清声明式 vs 命令式、Terraform 的 plan/apply 工作流、状态文件管理、模块化复用、配置漂移,以及 Ansible 适合什么。

声明式 vs 命令式

命令式是"一步步告诉我怎么做"(跑脚本),声明式是"告诉我终态长啥样,工具自己想办法达到"。IaC 主流(Terraform/K8s)都是声明式。

命令式声明式
你写操作步骤期望状态
重跑可能重复创建幂等,反复应用同态
代表Shell/AnsibleTerraform/K8s

Terraform 工作流:plan / apply

plan 先算"现实 vs 期望"的差异并预览变更(不动手);apply 才真正执行。永远先 plan 看清楚再 apply——这是 IaC 防手滑的核心纪律。

# 标准流程:写 .tf → 预览 → 执行 terraform init # 装 provider、初始化 terraform plan -out=tfplan # 预览将要发生的变更 terraform apply tfplan # 执行(用 plan 产物,避免中途改意)

论为什么 apply 前必看 plan

plan 会告诉你"要新建 3 个资源、替换 1 个、删除 0 个"。看到"替换/删除"生产数据库这类字样,立刻停手——可能是一个参数写错会把库重建。IaC 让灾难在 plan 阶段就可见。

状态管理:远程存储 + 锁

Terraform 用状态文件(state)记录"现实长什么样",每次 plan 对比期望与现实。本地 state 多人协作会冲突、易丢。必须远程存(S3/GCS)+ 加锁(DynamoDB)。

# 后端配置:state 放远端并加锁,避免多人同时改 terraform { backend "s3" { bucket = "my-tf-state" key = "prod/network.tfstate" region = "ap-east-1" dynamodb_table = "tf-locks" # 并发锁 } }
state 丢失 = 灾难

state 是 Terraform 的"记忆"。本地 state 误删会导致它以为资源不存在、下次 apply 重建一遍(可能覆盖线上)。务必远程存 + 加锁 + 备份,且绝不上 Git(含密钥)。

模块与变量:基础设施也能 DRY

把"一个标准 Web 服务"抽成模块,不同环境只传不同变量(实例数、域名),避免上百行重复 .tf。

# 调用模块,环境间只换变量 module "web" { source = "./modules/web-service" name = "order" replicas = 3 domain = "order.example.com" } # 预发环境:同样的模块,只改 replicas / domain

配置漂移:现实偷偷偏离了代码

漂移指"生产实际状态和 Git/Terraform 里写的不一样"——通常因为有人手动改了控制台。后果:下次 apply 可能"纠正"掉你手动加的东西,引发事故。

论怎么消灭漂移

① 禁止控制台手动改(除真正紧急的事故)。② 定时 terraform plan,有差异就报警。③ GitOps 自愈:集群被手改后,控制器自动拉回 Git 声明态(呼应 ch1)。漂移的本质是"生产脱离了代码",治法就是"一切变更走代码"。

实战:设一个每晚跑的 terraform plan 流水线,把 diff 发到值班群。一旦出现非预期的"will be updated/destroyed",说明有人手改了,立刻有人跟进。

Ansible 适合什么:配置管理

Terraform 管"资源存在不存在"(建 VM/网络),Ansible 管"VM 内部装了什么、怎么配"(命令式+幂等)。两者常配合:Terraform 拉起机器,Ansible 进去装软件。

# Ansible playbook:在一组主机上保证 nginx 装好并运行 - hosts: web tasks: - name: 安装 nginx ansible.builtin.apt: name: nginx state: present # 幂等:已装就不动 - name: 启动 nginx ansible.builtin.service: name: nginx state: started enabled: true

模块版本化与 Registry:基础设施也能发版

Terraform 模块写好后,别让各团队复制粘贴。把它发布到私有 Module Registry 并打版本,谁要用就 source = "app.terraform.io/org/vpc/aws/1.2.0"——基础设施和代码一样有版本、可审查、可回退。

论基础设施也要 DRY 与治理

没版本化的模块会被到处 fork,安全基线(加密、标签、日志)改一处要改十个仓库。收口成受治理的模块,安全与规范"一次做对、处处复用",也方便统一升级。

5

监控与可观测:Prometheus / Grafana / OTel

Metrics · Logs · Traces · SLO / SLI

FDE 模块 08 提过三支柱。本页讲清它们怎么配合:指标告诉你"系统不健康"(红色告警),链路告诉你"哪次请求慢在哪",日志告诉你"当时发生了什么细节"。再加 OpenTelemetry 统一标准与 SLO/错误预算。

三支柱重新梳理

支柱回答工具
Metrics"现在正常吗?"Prometheus / Grafana
Logs"出了什么事?"Loki / ELK
Traces"慢在哪个环节?"Jaeger / Tempo

论三者怎么接力排障

告警(指标)让你知道"出事了" → 链路定位"是哪次请求、卡在哪个服务" → 日志查明"那个时刻到底打印了什么"。单靠任一类都难以闭环,三者互补才是完整可观测。

Prometheus 数据模型与 PromQL

Prometheus 拉取(pull)指标存为时间序列(带标签的度量),用 PromQL 查询。核心概念:Counter(只增,如请求数)、Gauge(可升降,如内存)、Histogram(分桶,算延迟分位)。

# 抓取配置 + 几个常用 PromQL # 1) 每秒请求速率(QPS) rate(http_requests_total[5m]) # 2) 错误率(5xx 占比) sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) # 3) P95 延迟(来自 histogram 桶) histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket[5m])) by (le))

Grafana 看板:把数字变成直觉

Prometheus 存数据,Grafana 画图。一个好看板按"概览→下钻"组织:顶部是 SLO/错误率/QPS 总览,点进去看单服务、单接口。核心原则:看板服务于"出问题一眼看到",不是堆满图表炫技。

# 一个面板只关心一件事:用 PromQL 表达、配阈值变色 # 标题:订单服务错误率 # 表达式:sum(rate(http_requests_total{code=~"5.."}[5m])) / sum(rate(http_requests_total[5m])) # 阈值:>1% 黄,>5% 红 —— 一眼看出健康

论看板过多的反效果

二三十个面板、每个颜色花哨,真出事反而找不到重点。黄金信号(延迟/流量/错误/饱和度,即 RED/USE 法)四五个面板足矣,其余放"下钻"页。

链路追踪与 OpenTelemetry

一个用户请求跨五六个服务,想知"卡在哪",靠日志得手动拼 traceId。链路追踪自动把一次请求在各服务的耗时串成一条瀑布。OpenTelemetry(OTel)用一套厂商无关标准采集,后端可接 Jaeger/Prometheus/商业 APM 任意一种。

# 用 OTel SDK 埋点(自动注入 trace 上下文,跨服务传递) from opentelemetry import trace tracer = trace.get_tracer("order") with tracer.start_as_current_span("create_order") as span: span.set_attribute("order.id", oid) pay() # 子调用自动成为子 span,上下文透传

论为什么需要 OTel 标准

过去每家 SDK、格式都不一样,换工具就得改代码。OTel 统一采集层,后端可任意替换——避免被单一厂商锁定,也统一了团队口径。新项目直接上 OTel,别再绑死某家 APM。

SLI / SLO / 错误预算

SLI 是可测的指标(如"成功请求占比");SLO 是给它定的目标(如"99.9% 成功");错误预算 = 允许的失败额度(0.1%)。预算烧光就冻结发布先止血。

概念是什么例子
SLI可测量的健康指标请求成功率、P99 延迟
SLO给 SLI 定的目标成功率 ≥ 99.9%
错误预算允许失败的空间每月可失败 43 分钟
# 用 PromQL 算"过去 30 天可用性"是否达标 # 成功率 = 1 - 错误率;SLO 99.9% 即允许 0.1% 错误 1 - ( sum(rate(http_requests_total{code=~"5.."}[30d])) / sum(rate(http_requests_total[30d])) )

论错误预算驱动决策

预算充足 → 放心发新功能;预算快烧光 → 冻结发布、先止血。它把"可靠性"和"迭代速度"用同一个数字调和,避免研发要快、运维要稳的无休止扯皮。

告警该不该响

告警疲劳是 SRE 头号杀手

告警太多、一半是噪音,团队就会"红了也不看",真事故反而漏掉。原则:告警只对"需要人现在行动"的事触发(如 SLO 即将破、磁盘将满);纯信息用看板/日志,别打扰人。每条告警都要配"收到后干嘛"。

结构化日志:让机器也能查

日志别再一行行自由文本。结构化日志(JSON)把字段显式标出,集中采集后能用查询语言按字段过滤("查所有 order_id=100 且 level=error"),还能和 traceId 关联串起全链路。

# 结构化日志:字段显式,便于检索与聚合 log.info("order.created", order_id=oid, user_id=uid, amount=100) # 落盘形如:{"ts":...,"msg":"order.created","order_id":100,"user_id":7,"amount":100} # 采集后(Loki/ELK)可:{app="order"} | json | amount > 1000

论日志要带上下文,别只打"出错了"

排障时最怕"error: null pointer"——在哪、哪个用户、什么参数全没有。每条关键日志带上 order_id / trace_id / 关键入参,事后才能从一条告警顺藤摸到根因。

6

SRE 实践:告警疲劳、混沌、复盘与 on-call

Alert Fatigue · Chaos · Postmortem · On-call

SRE(站点可靠性工程)是把软件工程方法用到运维上。这一章讲告警疲劳与告警设计、混沌工程(故意制造故障)、事故复盘 Postmortem(对事不对人)、on-call 健康、以及消灭 toil(重复体力活)——这才是"可靠"的软实力。

告警疲劳与告警设计

告警是"请人来救火"的信号。一旦噪音多,人就会脱敏,真火也忽略。好告警三要素:可行动(收到知道干啥)、精确(少误报)、有优先级(P0 立刻、P3 上班再看)。

论"页面"和"工单"要分开

需要半夜爬起来的是 Page(P0/P1);可以明天处理的丢进工单系统,别进 on-call 手机。混在一起 = 制造告警疲劳。一条经验法则:能等 8 小时的,就不该半夜叫人。

# Prometheus 告警规则:只对"真要行动"的触发 - alert: ErrorBudgetBurningFast expr: | (1 - rate(http_requests_total{code=~"5.."}[1h]) / rate(http_requests_total[1h])) < 0.999 # 1h 内预算烧太快 for: 5m labels: { severity: page } # page 级 → 叫 on-call annotations: summary: "错误预算快速燃烧,需立即介入"

混沌工程:故意制造故障

系统不逼自己一把,永远不知道哪里脆。混沌工程在受控条件下注入故障(杀 Pod、断网络、加延迟),验证"系统是否真能自愈、降级、不雪崩"。它不是"制造事故",而是"在实验室里安全暴露弱点"。

# Chaos Mesh 示例:随机杀掉 order 的 Pod,看服务是否自愈 apiVersion: chaos-mesh.org/v1alpha1 kind: PodChaos metadata: { name: kill-order } spec: action: pod-kill mode: one # 一次杀一个,控制爆炸半径 selector: { labelSelectors: { app: order } } duration: "30s"
别在生产盲目混沌

混沌实验要有假设、监控、一键停止,且从小范围开始(先预发、先 1% 流量)。目的是"在可控条件下暴露弱点",不是制造事故。没有回滚预案就别跑。

起步建议:先在预发环境、对一个非核心服务做"杀 Pod"这类低风险实验,确认告警和自愈按预期工作,再逐步上生产、上网络分区等高风险项。

事故复盘 Postmortem:对事不对人

出事后写复盘,目的不是追责,而是"这次为什么会发生、怎么让同类不再发生"。黄金准则:无指责(blameless)。追问到根因(用"五个为什么"),产出可执行的改进项。

论复盘模板长啥样

① 时间线(发生了什么、谁在什么时刻做了啥);② 影响(损失多少、影响哪些用户);③ 根因(不是"某人手滑",而是"缺少门禁让它能被手滑");④ 行动项(带负责人和 deadline,且能被验证)。复盘的价值全在行动项是否真的落地。

# 行动项要可验证,避免"加强监控"这种空话 # 反例:加强数据库监控 # 正例:给主库加慢查询告警(>500ms),超阈值 Page on-call, # 由 @li 在 9/30 前完成,并在 staging 注入慢查询验证能触发

on-call 与轮班健康

on-call 是"轮到我值班、出事叫我"。不健康的值班会 burnout:半夜频繁叫、白天还正常搬砖、没人替。健康做法:轮班、保障接班后的休息、控制 Page 量、工程化降低呼叫。

论谁写代码谁 on-call

让"写这服务的人"参与值班,才能形成反馈闭环:你写的告警噪音大,第一个被吵醒的是你自己,于是你会去修告警、修可靠性。把运维甩给另一拨人,开发就永远不在乎可观测性。

消灭 toil:把重复体力活自动化

SRE 把 toil(手工、重复、无长期价值的运维活) 视为要消灭的对象:手动重启、手工扩缩、复制粘贴部署。能自动化的自动化,让人去做"提升系统本身可靠性"的工程。

自动化也要算 ROI

不是所有手工都值得自动化——一年只做一次、做一次五分钟的事,写自动化脚本可能花三天还不维护。优先自动化"高频 + 易错 + 半夜发生"的那类 toil。

SRE 的闭环:从事故到更好

把前面几章串起来:可观测(ch5)让你早发现问题 → 告警(ch6)叫对人 → 复盘找到根因 → 用 IaC/GitOps(ch1/4)把修复固化进代码防复发 → 用混沌(ch6)主动验证修复有效。这就是"可靠"不是运气,而是一条持续改进的引擎。

可靠性成熟度:你处在哪一级

团队可靠性能力是分阶段的。对照一下,知道下一步往哪发力。

级别特征
1 救火出事靠人 SSH 上去查,无监控
2 可观测有指标/日志/链路,能定位
3 可自愈探针 + HPA + 自动回滚,少人工
4 抗脆弱混沌演练 + 错误预算 + SLO 驱动决策

论别想一步到 4 级

先有监控(2 级),再做自动恢复(3 级),最后才谈混沌与 SLO 治理(4 级)。每级都建立在下级之上,跳级只会造出"看起来很高级但一碰就碎"的假可靠。

结
本页要点

DevOps/SRE 深化把"能部署"升级为"可靠交付":流水线分段(构建→测试→制品→部署→验证)且失败即截断,用不可变版本(SHA 而非 latest)+ 质量门禁,靠蓝绿/金丝雀让发布可回滚,靠 GitOps 拉式同步实现可审计、可回退 commit。容器要懂分层缓存、多阶段构建砍体积、固定版本+非 root+扫描保安全;K8s 核心是 Pod/Deployment/Service/Ingress、HPA 自动扩缩、PVC/ConfigMap/Secret 管状态、就绪/存活探针保零停机,警惕资源 limit 与 Secret 明文。IaC 用声明式、Terraform 先 plan 后 apply、state 远程加锁防漂移,Ansible 管配置;可观测靠指标/日志/链路三支柱+OpenTelemetry 统一标准、SLO/错误预算平衡快慢;SRE 用精确告警抗疲劳、混沌工程主动暴露弱点、无指责 Postmortem 闭环、健康 on-call、消灭 toil。

自测 · 看你是否真懂

1.指标、日志、链路分别回答什么问题?为什么需要三者配合?

查看答案

指标答"是否健康"(红不红),链路答"慢在哪"(定位请求路径),日志答"发生了什么细节"。告警让你知道出事,链路定位范围,日志查明原因——单靠任一类都难以闭环排障。

2.错误预算(Error Budget)解决了什么矛盾?怎么用它做决策?

查看答案

它把"可靠性"和"迭代速度"统一成一个数字:预算够就多发布,预算快透支就冻结发布先止血。避免研发要快、运维要稳的无休止扯皮。

3.GitOps 相比传统 CI/CD 的关键区别?为什么能抗配置漂移?

查看答案

传统是 push(人/流水线把变更推到集群);GitOps 以 Git 为唯一事实源,控制器持续 pull 并维持集群状态(selfHeal)。被手改后自动拉回 Git 声明态,变更可审计、回退 commit 即回滚,自然消灭漂移。

4.Terraform 的 state 为什么必须远程存 + 加锁?丢了会怎样?

查看答案

state 是 Terraform 记录"现实长啥样"的记忆。本地 state 多人协作会冲突、易误删;丢了它会以为资源不存在,下次 apply 重建一遍(可能覆盖线上库)。所以必须远程存(S3 等)+ 加锁(DynamoDB)+ 备份,且绝不上 Git。

5.混沌工程是不是"制造事故"?做它要注意什么?

查看答案

不是。它在受控条件下(有假设、有监控、可一键停止、从小范围起)注入故障,验证系统能否自愈/降级,目的是"安全暴露弱点"而非搞破坏。没有回滚预案、直接在生产全量跑,那是真制造事故。

下一步往哪走

路学完 DevOps 深化之后

① 回 FDE 模块 02/08:那里是"怎么做"(Docker/K8s/Prometheus),本页是"为什么这样设计"。

② 串系统设计:限流/熔断/容量规划在本页是 SRE 视角,在 tech-systemdesign 是架构视角,互为表里。

③ 动手:搭一套 Prometheus+Grafana+Jaeger(OTel),给自己的服务接上三支柱,亲手做次金丝雀发布与一次无指责复盘。

补

分支策略:Git Flow 与 Trunk-Based

Branching Models

分支怎么切,直接决定团队合并痛不痛、上线快不快。没有"最好"的模型,只有最适合你发布节奏的那一个。

三种主流模型

Git Flow 适合有明确发布节奏、多版本并行维护的团队;Trunk-Based 适合高频持续交付、CI/CD 成熟的团队;GitHub Flow 是 Trunk-Based 的轻量变体。

论核心差异

Git Flow:长期分支 master(正式)/develop(集成分支)+ 短期 feature/release/hotfix,缺点是分支多、合并重、反馈慢。Trunk-Based(主干开发):开发者从主干拉极短命分支(数小时~1天)或直接提交,频繁合回主干,主干始终可发布;依赖强 CI、特性开关(feature toggle)、小步提交。GitHub Flow:只有 main + 短 PR,合并即部署。

维度说明
发布频率Git Flow 低(按版本);Trunk-Based 高(每日多次)
合并冲突长分支越多冲突越频繁;主干开发短分支冲突少
CI 要求Trunk-Based 要求强 CI 守住主干;Git Flow 相对宽松
适合团队Git Flow 多版本并行;Trunk-Based 高频交付/CI 成熟

论特性开关

未完成功能用 toggle 藏起来,避免长分支;与「分支即环境」互补。hotfix 在 Git Flow 走 hotfix 分支,Trunk-Based 直接主干提交再 cherry-pick 到发布标签。

常见坑

长命特性分支是 Trunk-Based 的反模式——它是合并冲突和集成地狱的根源。别把"按功能开长分支"当成主干开发的做法。

自测

Q1. 什么团队适合 Trunk-Based?

参考答案

CI 成熟、小步提交、有特性开关、发布频繁(每日多次)的团队。

动手片段

下面这段可直接运行,把抽象概念变成"手感"。

# ===== Git Flow(多版本并行维护的团队)===== git flow init # 初始化:建立 master/develop 等长分支 git checkout -b feature/login develop # 从 develop 拉功能分支 git flow feature finish login # 合并回 develop git flow release start 1.0 # 从 develop 拉发布分支 git flow release finish 1.0 # 合并 master 并打 tag git flow hotfix start 1.0.1 master # 紧急修复 git flow hotfix finish 1.0.1 # ===== Trunk-Based(高频持续交付、CI 成熟)===== git switch -c feat/short-live main # 从主干拉极短命分支(数小时内合回) # ... 小步提交 ... git push -u origin feat/short-live # 开 PR/MR,CI 通过即合并回 main git switch main && git pull git tag -a v1.2.0 -m "release" && git push --tags # hotfix 直接主干提交再打 tag