From f8bcbb9d374beae98e482e7f9c6e99629556f9b7 Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Mon, 18 May 2026 00:17:59 +0800 Subject: [PATCH] vault backup: 2026-05-18 00:17:59 --- hhs/MS/02-服务治理/07-配置管理.md | 2 +- hhs/MS/03-数据一致性/01-数据库拆分.md | 613 ++++++++++++++---- hhs/MS/03-数据一致性/02-分布式事务.md | 341 +++++++++- hhs/MS/03-数据一致性/03-ID生成.md | 2 +- hhs/MS/05-部署运维/01-容器化.md | 163 ++--- .../01-容器化/01-Dockerfile最佳实践.md | 150 +++++ hhs/MS/05-部署运维/01-容器化/02-多架构构建.md | 91 +++ hhs/MS/05-部署运维/01-容器化/03-镜像安全.md | 150 +++++ hhs/MS/05-部署运维/02-Kubernetes.md | 293 +++------ .../02-Kubernetes/01-核心概念与Deployment.md | 202 ++++++ .../05-部署运维/02-Kubernetes/02-配置管理.md | 188 ++++++ .../02-Kubernetes/03-网络与服务发现.md | 144 ++++ .../02-Kubernetes/04-扩缩容与有状态应用.md | 217 +++++++ .../05-部署运维/02-Kubernetes/05-调度控制.md | 179 +++++ .../02-Kubernetes/06-资源与安全管控.md | 208 ++++++ .../05-部署运维/02-Kubernetes/07-运维排查.md | 143 ++++ hhs/MS/05-部署运维/03-CICD与GitOps.md | 323 ++++----- .../03-CICD与GitOps/01-CICD基础与实践.md | 299 +++++++++ .../03-CICD与GitOps/02-GitOps与ArgoCD.md | 449 +++++++++++++ .../03-CICD与GitOps/03-Helm模板管理.md | 454 +++++++++++++ .../03-CICD与GitOps/04-安全与发布策略.md | 470 ++++++++++++++ hhs/MS/05-部署运维/04-SRE实践.md | 136 +--- .../04-SRE实践/01-SLI-SLO-SLA详解.md | 77 +++ .../04-SRE实践/02-错误预算深度解析.md | 113 ++++ .../04-SRE实践/03-SRE心法与事故复盘.md | 114 ++++ hhs/MS/05-部署运维/README.md | 19 +- 26 files changed, 4828 insertions(+), 712 deletions(-) create mode 100644 hhs/MS/05-部署运维/01-容器化/01-Dockerfile最佳实践.md create mode 100644 hhs/MS/05-部署运维/01-容器化/02-多架构构建.md create mode 100644 hhs/MS/05-部署运维/01-容器化/03-镜像安全.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes/01-核心概念与Deployment.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes/02-配置管理.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes/03-网络与服务发现.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes/04-扩缩容与有状态应用.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes/05-调度控制.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes/06-资源与安全管控.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes/07-运维排查.md create mode 100644 hhs/MS/05-部署运维/03-CICD与GitOps/01-CICD基础与实践.md create mode 100644 hhs/MS/05-部署运维/03-CICD与GitOps/02-GitOps与ArgoCD.md create mode 100644 hhs/MS/05-部署运维/03-CICD与GitOps/03-Helm模板管理.md create mode 100644 hhs/MS/05-部署运维/03-CICD与GitOps/04-安全与发布策略.md create mode 100644 hhs/MS/05-部署运维/04-SRE实践/01-SLI-SLO-SLA详解.md create mode 100644 hhs/MS/05-部署运维/04-SRE实践/02-错误预算深度解析.md create mode 100644 hhs/MS/05-部署运维/04-SRE实践/03-SRE心法与事故复盘.md diff --git a/hhs/MS/02-服务治理/07-配置管理.md b/hhs/MS/02-服务治理/07-配置管理.md index 98c70e8..d0e007d 100644 --- a/hhs/MS/02-服务治理/07-配置管理.md +++ b/hhs/MS/02-服务治理/07-配置管理.md @@ -195,7 +195,7 @@ public class OrderController { if (newOrderFlowEnabled) { // 新版流程 } - return orderService.list(); + return orderService.list() } } ``` diff --git a/hhs/MS/03-数据一致性/01-数据库拆分.md b/hhs/MS/03-数据一致性/01-数据库拆分.md index 089e18a..15ffa5c 100644 --- a/hhs/MS/03-数据一致性/01-数据库拆分.md +++ b/hhs/MS/03-数据一致性/01-数据库拆分.md @@ -1,186 +1,574 @@ --- tags: [microservice, database, sharding, replication] -create time: 2026-05-05 +create time: 2026-05-17 14:30 --- # 数据库拆分 ## 概述 -微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这意味着每个服务的表结构、数据存储、甚至数据库类型都可以不同。但当单表数据量持续增长时,就面临拆分的需求。 +想象一下:你的电商系统刚上线时只有一张 `orders` 表,数据量不大,一条 SQL 就搞定。一年后,日订单量涨到 100 万,同样的查询慢到了 5 秒——数据库成了瓶颈,你不得不开始考虑:**怎么拆?** + +微服务的核心设计原则是 **"每个服务拥有独立数据库"**。这意味着每个服务的表结构、数据存储、甚至数据库类型都可以不同。但当单表数据量持续增长时,你就面临另一个维度的拆分需求。 ```mermaid graph LR - S1[订单服务 DB] - S2[库存服务 DB] - - subgraph BAD["反模式:共享数据库"] - S1 --- SHARED[(共享 DB)] - S2 --- SHARED + subgraph "理想状态:微服务 = 独立数据库" + S1["📦 订单服务
MySQL"] + S2["👤 用户服务
PostgreSQL"] + S3["📦 库存服务
Redis + MySQL"] end - + + style S1 fill:#e8f5e9,stroke:#4caf50 + style S2 fill:#e8f5e9,stroke:#4caf50 + style S3 fill:#e8f5e9,stroke:#4caf50 +``` + +> [!failure] 反面教材:共享数据库 +> +> 如果两个微服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是 **分布式单体**。服务可以随意互相查对方的数据,失去了边界和自治性。这在早期开发中很常见(方便联调),但一定要在正式微服务化之前拆掉。 + +```mermaid +graph LR + ORDER["📦 订单服务"] --- SHARED[(❌ 共享 DB)] + USER["👤 用户服务"] --- SHARED + style SHARED fill:#ffebee,stroke:#ef5350 + style ORDER fill:#fff3e0,stroke:#ff9800 + style USER fill:#fff3e0,stroke:#ff9800 ``` -> [!failure] 反模式警告 -> 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是**分布式单体**。服务可以随意互相查询彼此的数据,失去了边界和自治性。 +## 两种拆分思路:先"纵向切",再"横向切" -## 垂直拆分 vs 水平拆分 +拆分不是选择题,而是**两步走**:第一步按业务域垂直拆分(微服务化的标配),第二步当单个表太大时再水平拆分(分库分表)。 -### 垂直拆分(按业务域) +### 第一步:垂直拆分 — 按业务域独立建库 -按 **微服务边界** 拆库——这是微服务的标配。 +> [!question] 思考一下 +> +> 如果一个"用户下单"操作需要同时访问订单表和商品信息表——这说明这两个表应该属于同一个数据库吗? +> +> **答案是否定的。** 商品不会因为你改了订单就跟着变。真正的判断标准是:**哪些表经常一起被修改?** 如果一组表的变更总是由同一个业务逻辑触发,它们就属于同一个限界上下文,应该放在同一个库里。 + +核心做法:以 **微服务边界** 为基准,将相关表打包到一个数据库中。 ```mermaid -graph TB - subgraph "DB-Order" - orders[orders] - order_items[order_items] - order_status[order_status] +flowchart TB + subgraph DBOrder["DB-Order:订单域"] + direction LR + T1[orders] + T2[order_items] + T3[order_status_log] end - - subgraph "DB-User" - users[users] - user_profiles[user_profiles] - user_addresses[user_addresses] + + subgraph DBUser["DB-User:用户域"] + direction LR + U1[users] + U2[user_profiles] + U3[user_addresses] end - - subgraph "DB-Product" - products[products] - categories[categories] - product_images[product_images] + + subgraph DBProduct["DB-Product:商品域"] + direction LR + P1[products] + P2[categories] + P3[product_images] end + + style DBOrder fill:#e3f2fd,stroke:#1976d2,rx:8 + style DBUser fill:#e3f2fd,stroke:#1976d2,rx:8 + style DBProduct fill:#e3f2fd,stroke:#1976d2,rx:8 ``` -| 特点 | 说明 | +| 优势 | 说明 | |------|------| -| 每个服务独占一个数据库 | 物理隔离,互不干扰 | -| 可异构选型 | 订单用 MySQL,用户用 PostgreSQL,搜索用 Elasticsearch | -| 天然解耦 | 服务间不能直接查对方库 | +| **物理隔离,互不干扰** | 订单库崩了不影响用户登录 | +| **异构选型** | 订单用 MySQL(事务强),搜索用 Elasticsearch,缓存用 Redis | +| **天然解耦** | 服务之间不能直连对方数据库,只能通过 API 通信 | -### 水平拆分(分库分表) +> [!tip] 拆分的粒度 +> +> 不要把一张表里的字段都拆到不同库里——那叫"过度拆分"。**以表为单位**进行垂直拆分是最常见的做法。一个微服务对应一个库,一个库包含多个相关表。 -当单个表的记录量达到千万级以上,需要进一步拆分。 +### 垂直拆分的判断标准(实操 checklist) + +当你在犹豫某张表应该留在这个库还是挪到另一个库时,用下面几个维度来判断: + +```mermaid +flowchart TD + START["开始判断"] --> A["这张表和当前库里的表
是否经常被同一个事务修改?"] + A -->|是| SAME_DB["留在同一库 ✅"] + A -->|否| B["它们是否属于同一个
业务限界上下文?"] + B -->|是| SAME_CTX["考虑放在同一库
(降低跨库调用成本)"] + B -->|否| DIFF_CTX["必须独立建库 ✅"] + SAME_CTX --> C["变更频率差异大吗?"] + C -->|高频 vs 低频| SPLIT["建议拆分
(避免互相影响)✅"] + C -->|同频| SAME_DB + DIFF_CTX --> END["完成"] + SAME_DB --> END + SPLIT --> END +``` + +| 判断维度 | 放同一库的信号 | 拆开的信号 | +|---------|--------------|----------| +| **事务耦合度** | 经常在一个 `BEGIN...COMMIT` 里一起改 | 各自有独立的写入路径 | +| **读取热点** | 总是被一起查询、一起展示 | 访问模式完全不同 | +| **团队归属** | 同一小组维护 | 不同团队负责(Conway 定律) | +| **数据增长率** | 增长速度接近 | 一个涨得快、一个基本不变 | + +> [!note] 经验法则 +> +> 如果一组表之间的跨库 JOIN 操作占了日常 SQL 的 **80% 以上**,把它们放在一起通常更合理。反过来,如果大部分关联查询都只需要一次 LEFT JOIN,说明拆分时机已经成熟——因为 JOIN 已经在两个数据库之间产生网络开销了。 + +## 第二步:水平拆分 — 一张表太大了怎么办? + +垂直拆分解决的是"谁来负责什么"的问题。但当 **单个服务内的单表** 达到千万级甚至亿级记录时,就需要水平拆分(也叫分片/Sharding)。 + +> [!note] 为什么要分? +> +> - 单表超过 2000 万行后,索引效率急剧下降 +> - 单机 MySQL 的写入 QPS 通常在 5000~20000,超出后成为瓶颈 +> - InnoDB 缓冲池装不下全部索引数据,大量磁盘 IO + +水平拆分的核心思想:**把一张大表按规则切成多张小表,分散到不同的数据库实例中。** ```mermaid graph TB - subgraph "分库策略" - DB1[(DB-01)] - DB2[(DB-02)] - DB3[(DB-03)] + subgraph "拆分前:一张表扛所有" + BIG_TABLE[(order 表 1 亿行)] end - - subgraph "order_0001 表" - Row1[userId=1 → order_0001] - Row2[userId=4 → order_0001] + + subgraph "拆分后:按 user_id 散列" + subgraph "DB-01" + T1[(order_0001 2500 万行)] + end + subgraph "DB-02" + T2[(order_0002 2500 万行)] + end + subgraph "DB-03" + T3[(order_0003 2500 万行)] + end + subgraph "DB-04" + T4[(order_0004 2500 万行)] + end end - - subgraph "order_0002 表" - Row3[userId=2 → order_0002] - Row4[userId=5 → order_0002] - end - - userId_mod["ORDER BY user_id % 2"] --> DB1 - userId_mod --> DB2 + + ROUTE["user_id % 4"] -->|"uid=1001"| T1 + ROUTE -->|"uid=2002"| T2 + ROUTE -->|"uid=3003"| T3 + ROUTE -->|"uid=4004"| T4 + + style BIG_TABLE fill:#ffebee,stroke:#ef5350 + style T1 fill:#e8f5e9,stroke:#4caf50 + style T2 fill:#e8f5e9,stroke:#4caf50 + style T3 fill:#e8f5e9,stroke:#4caf50 + style T4 fill:#e8f5e9,stroke:#4caf50 ``` -### 常见分片策略 +#### 常见分片策略对比 -| 策略 | 哈希公式 | 优点 | 缺点 | -|------|---------|------|------| -| **Hash Mod** | `user_id % N` | 简单高效,路由确定 | 扩缩容困难,数据迁移成本高 | -| **Range** | `user_id BETWEEN x AND y` | 范围查询友好 | 热点用户集中到单分片 | -| **Time-based** | `year_month` | 按生命周期管理 | 新分片写入压力大 | -| **Geo-based** | 按地域分片 | 本地化访问,延迟低 | 跨区域操作复杂 | +| 策略 | 怎么分 | 适合场景 | 代价 | +|------|--------|---------|------| +| **哈希取模** `hash % N` | `user_id % 4`,余数决定去哪个库 | 均匀分布,写入均衡 | 扩库时需要迁移大部分数据 | +| **范围划分** `BETWEEN x AND y` | userId 1~10000 → DB-A,10001~20000 → DB-B | 范围查询友好 | 热点账号集中在一个分片 | +| **时间分区** `year_month` | 2024_01 → DB-Jan, 2024_02 → DB-Feb | 按生命周期管理(冷数据归档) | 最新月份写入压力大 | +| **地理位置** | 华东用户 → 杭州 DB,华南 → 广州 DB | 降低跨地域延迟 | 跨区域操作复杂 | -### ShardingSphere / MyCat +> [!important] 扩容陷阱 +> +> Hash Mod 最容易踩坑:当你从 4 个分片扩展到 8 个分片时,`% 4` 变成 `% 8`,**几乎所有数据的新归属都变了**,需要大规模数据迁移。这是一个需要提前规划的重大决策。 +> +> 如果扩容是高频需求,建议从一开始就用一致性哈希或预留足够多的槽位。 -> [!tip] 推荐中间件 -> -> **Apache ShardingSphere** 是国内使用最广泛的分库分表方案: -> - 支持 JDBC / Proxy / Sidecar 三种部署模式 -> - 内置分片算法:Mod、Range、Hash、Tag -> - 分布式主键生成器(Snowflake)原生集成 -> - 读写分离、强制路由、广播表等高级特性 +### 分片扩容方案:不停机迁移 -#### ShardingSphere 配置示例 +生产环境的扩容不能停服停机——你需要一个 **双写 + 历史数据回迁** 的渐进式流程: + +```mermaid +flowchart TD + A["当前状态: N 个分片
hash % N"] --> B["第一步: 新增 M 个分片
总容量变为 N+M"] + B --> C["第二步: 双写阶段
新写入同时写到旧分片和新分片"] + C --> D["第三步: 历史数据回迁
按分片逐个搬移存量数据"] + D --> E{"全部搬完?"} + E -->|否| D + E -->|是| F["第四步: 校验数据一致性
checksum 对比"] + F --> G["第五步: 切读流量
新请求读新分片"] + G --> H["第六步: 关闭双写
恢复到单写单读"] + + style B fill:#fff3e0,stroke:#ff9800 + style C fill:#fff3e0,stroke:#ff9800 + style D fill:#e3f2fd,stroke:#1976d2 + style F fill:#e8f5e9,stroke:#4caf50 + style G fill:#e8f5e9,stroke:#4caf50 + style H fill:#f3e5f5,stroke:#7b1fa2 +``` + +| 阶段 | 核心操作 | 关键风险 | 规避方法 | +|------|---------|---------|---------| +| **双写** | 新数据同时写入新旧两套分片 | 数据不一致、写入性能下降 | 用消息队列保证异步双写;设置标记位可快速回滚 | +| **回迁** | 按 user_id 范围分批迁移历史数据 | 迁移期间持续写入导致数据漂移 | 迁移后对已迁移范围做一次增量同步 | +| **切读** | 将读取路由切换到新分片 | 漏读、脏数据 | 先灰度 1% 流量验证,逐步放量 | +| **关双写** | 停止向旧分片写入 | 遗漏最后一段增量数据 | 双写关之前做一次全量 checksum 校验 | + +> [!warning] 平滑扩容的时间成本 +> +> 假设你有 1 亿条订单数据,网络带宽 1Gbps,压缩后约 50GB。**理论传输时间不到 1 分钟**——但实际中还要考虑锁竞争、慢查询、监控告警等因素。建议给每个分片预留 **2~4 小时** 的迁移窗口期,夜间低峰期执行。 + +## 业界方案:ShardingSphere + +> [!tip] 为什么选 ShardingSphere? +> +> Apache ShardingSphere 是国内使用最广泛的分库分表中间件。它提供三种部署模式: +> - **JDBC**:嵌入应用,零运维(最常用) +> - **Proxy**:独立代理服务,语言无关 +> - **Sidecar**:Kubernetes 侧车模式 + +#### 配置示例 + +以下是 ShardingSphere-JDBC 的核心配置片段(YAML 格式): ```yaml -# sharding-jdbc 配置 -sharding jdbc: - data-sources: - ds0: { type: HikariCP, ... } - ds1: { type: HikariCP, ... } - +sharding-jdbc: + data-sources: # 定义数据源 + ds0: { type: com.zaxxer.hikari.HikariDataSource, ... } + ds1: { type: com.zaxxer.hikari.HikariDataSource, ... } + sharding: tables: - orders: + orders: # 逻辑表名 actual-data-nodes: ds$->{0..1}.orders$->{0..1} + # 上面的表达式展开后是:ds0.orders0, ds0.orders1, ds1.orders0, ds1.orders1 table-strategy: standard: - sharding-column: user_id + sharding-column: user_id # 分片键 sharding-algorithm-name: user-id-mod key-generate-strategy: - column: order_id - key-generator-name: snowflake - + column: order_id # 主键生成 + key-generator-name: snowflake # Snowflake 雪花算法 + sharding-algorithms: user-id-mod: type: MOD props: - sharding-count: 2 + sharding-count: 2 # 分成 2 个分片 ``` -## 跨库查询方案 +关键点: +- **逻辑表名 vs 实际数据节点**:应用层看到的永远是 `orders`,底层路由到 `ds0.orders0` 等是由中间件透明的完成的 +- **分片键选择**:一定要选写入频率高且用于查询条件的字段(如 `user_id`),否则每次查询都要扫全部分片 + +### 分布式 ID 生成:为什么不能用自增主键? + +分库后每个库的 `AUTO_INCREMENT` 是独立的——两个库可能都生成了 `id = 100`。你必须用一种方式保证 **全局唯一**。 + +#### Snowflake 雪花算法 + +Twitter 开源的雪花算法是目前最主流的分布式 ID 方案: + +```mermaid +graph LR + subgraph "64-bit 长整型 ID" + S["符号位
1 bit"] + T["时间戳
41 bits"] + D["机器 ID
10 bits"] + SQ["序列号
12 bits"] + end + + style S fill:#f5f5f5,stroke:#9e9e9e + style T fill:#e3f2fd,stroke:#1976d2 + style D fill:#e8f5e9,stroke:#4caf50 + style SQ fill:#fff3e0,stroke:#ff9800 +``` + +| 字段 | 长度 | 作用 | 范围 | +|------|------|------|------| +| **符号位** | 1 bit | 恒为 0(保证 ID 为正数) | - | +| **时间戳** | 41 bits | 毫秒级时间戳 | 可支撑约 69 年 | +| **机器 ID** | 10 bits | 区分部署实例 | 1024 个节点 | +| **序列号** | 12 bits | 同一毫秒内的递增序号 | 每毫秒 4096 个 ID | + +> [!note] 核心特性 +> +> - **单调递增**:基于时间戳保证整体趋势递增,适合 InnoDB 聚簇索引的 append-only 写入模式 +> - **高吞吐**:单实例每秒可生成 4096 × 1000 = 数百万个 ID +> - **无中心节点**:不依赖 ZooKeeper 或数据库,挂掉一个机器不影响其他节点 +> +> 如果同一毫秒内生成的 ID 超过 4096 个,算法会 **等待下一毫秒** 再重试——这在实际场景中极少发生(通常 QPS < 10 万)。 + +#### 备选方案对比 + +| 方案 | 唯一性保证 | 性能 | 复杂度 | 适用场景 | +|------|----------|------|--------|---------| +| **Snowflake** | 时间戳+机器ID+序列号组合 | 极高(本地生成) | 中 | 通用首选 | +| **数据库号段模式** | 每次从 DB 批量拉取一段 ID | 高 | 中高 | 有现成 DB 基础设施 | +| **UUID** | 随机 128 位 | 高 | 极低 | 对有序性无要求的场景 | +| **Redis INCR** | Redis 原子递增 | 高 | 低 | 已有 Redis 集群 | + +> [!warning] UUID 的坑 +> +> UUID 虽然简单,但在 MySQL InnoDB 中是 **灾难性的**:因为 UUID 无序,插入位置随机分布,导致大量的页分裂和碎片化。如果必须用 UUID,建议存为 `BINARY(16)` 并用 `UUID_TO_BIN(uuid, 1)` 转换,让它在索引中保持局部有序。 + +## 跨库查询:拆分之后最难解决的问题之一 > [!question] 经典难题 -> 订单服务需要展示用户的姓名和手机号来做收货地址。但用户信息在用户库,订单数据在订单库——怎么办? +> +> 用户打开"我的订单"页面。订单服务拿到 `user_id` 查出订单列表,但现在要展示用户的头像和昵称——这些信息在用户库里。数据库已经拆开了,怎么做关联查询? -### 方案对比 +这是拆分后必然遇到的挑战。没有银弹,只有 **权衡后的取舍**。 -| 方案 | 描述 | 性能 | 复杂度 | 适用场景 | -|------|------|------|--------|---------| -| **冗余字段** | 订单表存用户名字段 | ⭐⭐⭐⭐⭐ | 低 | 只读字段,变更频率低 | -| **接口组装** | 先查订单,再调用户服务补全 | ⭐⭐⭐ | 中 | 偶尔需要关联的场景 | -| **CQRS / 宽表** | 异步同步一份宽表用于查询 | ⭐⭐⭐⭐ | 高 | 高频关联查询 | -| **搜索引擎** | ES/Kibana 做关联查询 | ⭐⭐⭐⭐ | 中高 | 复杂搜索 + 聚合 | +### 四种方案对比 -### 冗余字段实践 +```mermaid +mindmap + root((跨库查询方案)) + 冗余字段 + 最简单最直接 + 少量字段 + 快照语义 + 一致性问题 + 接口组装 + 按需查询 + 链路长时性能差 + N+1 查询陷阱 + 适合低频关联 + CQRS / 宽表 + 异步最终一致 + 写入有额外开销 + 适合高频关联 + 搜索引擎 + ES 做多维聚合 + 架构重 + 适合复杂搜索 +``` + +| 方案 | 一句话描述 | 性能 | 复杂度 | +|------|-----------|------|--------| +| **冗余字段** | 在订单表里直接存用户名字段 | ⭐⭐⭐⭐⭐ | 低 | +| **接口组装** | 先查订单,循环调用户服务补全信息 | ⭐⭐⭐ | 中 | +| **CQRS / 宽表** | 异步同步一份含用户信息的宽表 | ⭐⭐⭐⭐ | 高 | +| **搜索引擎** | 把数据推到 ES,用 ES 做关联查询 | ⭐⭐⭐⭐ | 中高 | + +### 实战一:冗余字段(最推荐的首选方案) ```sql --- 订单表冗余关键字段 +-- 订单表中冗余关键字段(快照模式) CREATE TABLE orders ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, - username VARCHAR(64), -- 冗余用户名(快照,不参与编辑) - phone VARCHAR(20), -- 冗余手机号(脱敏存储) + username VARCHAR(64), -- 下单时的用户名快照 + phone VARCHAR(20), -- 下单时的手机号(已脱敏) created_at TIMESTAMP DEFAULT NOW(), INDEX idx_user (user_id) ); ``` -> [!note] 冗余数据的维护 -> -> 用户改名字了怎么办?**不改订单表**。订单上的 username 是该时刻的"快照"——它反映的是下单时的状态,不是当前状态。这符合业务语义。 -> -> 如果需要批量更新冗余字段(如用户头像),通过消息队列异步通知订单服务更新。 +> [!note] 关键理解:快照语义 +> +> 用户后来改了名字、换了手机号,**不改订单表**。订单上的 `username` 反映的是下单那一刻的状态,而不是"当前"状态。这不仅是合理的,而且是正确的——用户查看历史订单时,看到的是当时的信息。 +> +> 如果需要主动更新冗余字段(如用户更换了头像),通过消息队列通知订单服务批量更新。 -## 数据库迁移工具 +Go 代码实现示例: -```mermaid -graph LR - Dev["开发环境"] -->|"flyway migrate"| Stage["Staging"] - Stage -->|"人工审批"| Prod["Production"] - Prod -->|"flyway validate"| Check["校验版本一致性"] +```go +// 下单时将用户信息快照写入订单表 +func (s *orderSvc) CreateOrder(ctx context.Context, req *CreateOrderReq) (*Order, error) { + // 1. 查用户基本信息 + user, err := s.userClient.GetByID(ctx, req.UserID) + if err != nil { + return nil, err + } + + // 2. 构造订单,携带快照字段 + order := &Order{ + UserID: req.UserID, + Username: user.Username, // ✅ 快照:锁定下单时的值 + Phone: maskPhone(user.Phone), + Items: req.Items, + } + return s.orderRepo.Save(ctx, order) +} ``` -| 工具 | 语言 | 特点 | -|------|------|------| -| **Flyway** | Java | 基于文件命名,SQL 脚本方式,简单直观 | -| **Liquibase** | Java | XML/YAML/JSON 格式,支持回滚生成 | -| **golang-migrate** | Go | CLI 工具,轻量,适合 Go 项目 | +### 实战二:接口组装(轻量场景够用) -### Flyway 迁移流程 +适用于关联查询不频繁的场景,比如后台管理系统的偶尔查看详情。 + +```go +// 查订单 + 补齐用户信息 +func (s *orderSvc) GetOrderWithUser(ctx context.Context, orderID int64) (*OrderDetail, error) { + // 1. 先查订单 + order, err := s.orderRepoFindByID(ctx, orderID) + if err != nil { + return nil, err + } + + // 2. 再调用户服务补齐 + user, err := s.userClient.GetByID(ctx, order.UserID) + if err != nil { + return nil, err + } + + return &OrderDetail{ + Order: order, + UserName: user.Username, + UserHead: user.AvatarURL, + }, nil +} +``` + +> [!warning] N+1 陷阱 +> +> 如果是列表查询(一次性返回 20 条订单),逐条调用户服务会导致 20 次 RPC 调用。正确做法是:先收集所有 `user_id`,**批量查询**用户信息,再拼回去。 + +```go +// ❌ 错误:N 次 RPC +for _, o := range orders { + u, _ := userClient.GetByID(ctx, o.UserID) +} + +// ✅ 正确:1 次批量 RPC +ids := collectIDs(orders) // [1, 3, 7, 12, ...] +users, _ := userClient.GetByIds(ctx, ids) // 一次拿回所有 +byID := indexBy(users, func(u *User) int64 { return u.ID }) +for _, o := range orders { + o.UserInfo = byID[o.UserID] +} +``` + +### 实战三:CQRS / 宽表(重度关联查询必备) + +当跨库关联是高频操作(如运营后台的多维度筛选),冗余字段就不够了——你需要一套完整的 **事件驱动宽表同步机制**: + +```mermaid +sequenceDiagram + participant U as 用户服务 + participant MQ as 消息队列 + participant O as 订单服务 + participant W as 宽表 (Read DB) + + U->>MQ: UserUpdated 事件 (userId, newAvatar) + MQ->>O: 消费事件 + O->>W: 更新宽表中对应用户的头像 + + Note over W: 宽表包含了订单 + 用户 + 商品的冗余字段
只读,专为查询优化 +``` + +Go 代码实现——事件消费者(订单服务侧): + +```go +// Order宽表同步消费者:监听来自各服务的业务事件,维护一张可跨维度查询的宽表 +type WideTableSyncer struct { + wideDB *sql.DB // 独立的读库连接 +} + +// OnUserUpdated 消费用户变更事件,更新宽表中对应用户的信息 +func (s *WideTableSyncer) OnUserUpdated(ctx context.Context, evt UserUpdatedEvent) error { + tx, err := s.wideDB.BeginTx(ctx, nil) + if err != nil { + return err + } + defer tx.Rollback() + + // 批量更新宽表中的用户快照字段 + _, err = tx.ExecContext(ctx, + `UPDATE order_wide_table + SET username = ?, phone_masked = ?, avatar_url = ? + WHERE user_id = ?`, + evt.Username, evt.PhoneMasked, evt.AvatarURL, evt.UserID, + ) + return tx.Commit() +} + +// OnOrderCreated 订单创建时写入宽表(包含完整的关联信息) +func (s *WideTableSyncer) OnOrderCreated(ctx context.Context, evt OrderCreatedEvent) error { + _, err := s.wideDB.ExecContext(ctx, + `INSERT INTO order_wide_table + (order_id, user_id, username, product_name, amount, status, created_at) + VALUES (?, ?, ?, ?, ?, ?, ?)`, + evt.OrderID, evt.UserID, evt.Username, + evt.ProductName, evt.Amount, evt.Status, evt.CreatedAt, + ) + return err +} +``` + +> [!tip] 宽表设计的三个原则 +> +> 1. **反范式化**:宽表故意违反第一范式——同一个用户的名字可能出现在几百条订单记录中。这正是它的价值所在。 +> 2. **最终一致性延迟可控**:通过 MQ 保证,通常在 1~3 秒内同步完成。前端加一个 loading 状态即可掩盖这短暂的延迟。 +> 3. **写多读少时才考虑**:如果宽表的写入放大超过原始数据的 3 倍,说明你的场景用冗余字段就够了,不需要上宽表。 + +## 何时该拆?——不要过早拆分 + +拆分是有 **代价** 的:复杂度上升、运维成本增加、跨服务调用变慢。在决定拆之前,先看这些硬指标是否已经触达: + +```mermaid +flowchart TD + START["你的数据量到了什么级别?"] + + START --> SMALL["< 100 万行
单库单机足够"] + START --> MEDIUM["100 万 ~ 2000 万行
考虑读写分离"] + START --> LARGE["> 2000 万行
考虑分片"] + + SMALL --> S1["✅ 先优化索引"] + S1 --> S2["✅ 加缓存层 Redis"] + S2 --> S3["✅ 读写分离
一主多从"] + + MEDIUM --> M1["✅ 先做垂直拆分"] + M1 --> M2["按业务域独立建库"] + + LARGE --> L1["✅ 再做水平拆分"] + L1 --> L2["分库分表 + 宽表同步"] + + style SMALL fill:#e8f5e9,stroke:#4caf50 + style MEDIUM fill:#fff3e0,stroke:#ff9800 + style LARGE fill:#ffebee,stroke:#ef5350 + style S3 fill:#e8f5e9,stroke:#4caf50 + style M2 fill:#e8f5e9,stroke:#4caf50 + style L2 fill:#ffebee,stroke:#ef5350 +``` + +> [!quote] 一条经验法则 +> +> **"在数据量还没到 2000 万行之前,不要做任何形式的数据水平拆分。"** +> +> 绝大多数系统通过索引优化 + 缓存 + 读写分离就能撑到日活百万级。见过太多团队在项目刚上线就搞分库分表——六个月后回头看,完全是提前踩雷。 + +### 拆与不拆的判断矩阵 + +| 场景 | 推荐方案 | 预期寿命 | +|------|---------|---------| +| DAU < 1 万,QPS < 500 | 单库单机,专心做好索引 | 半年~1年 | +| DAU 1~50 万,QPS 500~5000 | 加 Redis 缓存 + 读写分离 | 1~2 年 | +| DAU 50~200 万,单表 > 2000 万行 | 垂直拆分(按微服务) | 2~3 年 | +| DAU > 200 万,单表 > 5000 万行 | 水平拆分 + 宽表同步 | 长期 | + +## 数据库迁移工具:告别手动执行 SQL + +随着服务拆分增多,SQL 脚本的管理变得复杂。手工执行、口头传达"我跑过 V3 了"的方式不再可行——你需要**版本化的、可重复执行的数据库迁移工具**。 + +```mermaid +flowchart LR + Dev["💻 开发环境
git commit SQL 文件"] -->|"CI/CD 自动执行"| Stage["🧪 Staging
flyway migrate"] + Stage -->|"人工审批"| Prod["🚀 Production
flyway migrate"] + Prod -->|"校验"| Check["🔒 flyway validate
确认无漂移"] + + style Dev fill:#e3f2fd,stroke:#1976d2 + style Stage fill:#fff3e0,stroke:#ff9800 + style Prod fill:#e8f5e9,stroke:#4caf50 + style Check fill:#f3e5f5,stroke:#7b1fa2 +``` + +### 主流工具对比 + +| 工具 | 生态 | 特点 | +|------|------|------| +| **Flyway** | Java / Go (via CLI) | 基于文件名命名,纯 SQL 脚本,简单直接 | +| **Liquibase** | Java | 支持 XML/YAML/JSON,自带回滚生成能力 | +| **golang-migrate** | Go | 轻量 CLI,Go 项目首选 | + +### Flyway 工作流 ``` db/migration/ @@ -190,7 +578,12 @@ db/migration/ └── V4__add_order_status_enum.sql ``` -执行顺序:V1 → V2 → V3 → V4。Flyway 内部维护 `_schema_version` 表追踪已执行的迁移。 +执行顺序严格遵循版本号:V1 → V2 → V3 → V4。Flyway 会在数据库中维护一张 `_schema_version` 表,记录每个迁移的版本和执行状态。 + +核心要点: +- **文件名即版本**:`V` 开头 + 序号 + `__` + 描述 +- **不可修改已执行的文件**:改了的话 Flyway 会报验证失败(`validate` 阶段) +- **永远不要写"反向"SQL**:迁移脚本只做"升级",回滚通过发版解决 ## 关联笔记 diff --git a/hhs/MS/03-数据一致性/02-分布式事务.md b/hhs/MS/03-数据一致性/02-分布式事务.md index 02297b8..f88a9a7 100644 --- a/hhs/MS/03-数据一致性/02-分布式事务.md +++ b/hhs/MS/03-数据一致性/02-分布式事务.md @@ -1,6 +1,6 @@ --- -tags: [microservice, distributed-transactions, saga, tcc, outbox] -create time: 2026-05-05 +tags: [microservice, distributed-transactions, saga, tcc, outbox, seata, idempotency, dlq] +create time: 2026-05-17 15:00 --- # 分布式事务 @@ -101,25 +101,6 @@ func outboxWorker(ctx context.Context, ticker *time.Ticker) { } ``` -#### 事务消息(RocketMQ 原生支持) - -如果使用的是 RocketMQ,可以绕过 Outbox 模式直接用事务消息: - -```go -// 发送事务消息 -txMsg := rocketmq.NewTransactionMessage("order-created", payload) -localTx := &MyLocalTxChecker{} - -// half 消息发送 → 本地事务执行 → 提交/回查 -res, _ := producer.SendMessageInTransaction(txMsg, localTx) -``` - -RocketMQ 的事务消息流程: -1. 生产者发送 "half 消息" 到 MQ(消费者不可见) -2. 执行本地事务 -3. 根据结果 Commit(消费者可见)或 Rollback(丢弃) -4. 如果步骤 2 超时,MQ 回查本地事务状态 - ### 消费幂等性 > [!warning] 关键保障 @@ -148,6 +129,57 @@ if !isUniqueViolation(err) { // 执行业务逻辑——到这里说明消息是新到达的 ``` +#### 事务消息 vs Outbox + +> [!question] 思考一下 +> Outbox 模式有一个天然缺点:定时轮询有延迟(通常秒级)。如果你需要**毫秒级的延迟敏感型通知**(如支付结果实时推送到用户端),该怎么办? + +如果使用的是 RocketMQ,可以绕过 Outbox 模式直接用事务消息: + +```go +// 发送事务消息 +txMsg := rocketmq.NewTransactionMessage("order-created", payload) +localTx := &MyLocalTxChecker{} + +// half 消息发送 → 本地事务执行 → 提交/回查 +res, _ := producer.SendMessageInTransaction(txMsg, localTx) +``` + +RocketMQ 事务消息的完整交互流程: + +```mermaid +sequenceDiagram + participant Producer as 生产者 + participant Broker as MQ Broker + participant DB as 业务数据库 + participant Consumer as 消费者 + + Producer->>Broker: ① 发送 Half Message (消费者不可见) + Broker-->>Producer: ACK + + Producer->>DB: ② 执行本地事务 + alt 本地事务成功 + Producer->>Broker: ③a Commit (消息对消费者可见) + else 本地事务失败 + Producer->>Broker: ③b Rollback (消息丢弃) + end + + Note over Producer,Broker: ④ 如果步骤 2 超时未回复 + Broker->>Producer: ④ 回查请求 (CheckLocalTx) + Producer->>DB: 查询本地事务状态 + DB-->>Producer: 返回 COMMIT / ROLLBACK + Producer->>Broker: 回查结果 +``` + +> [!important] 回查机制的意义 +> 当生产者执行本地事务过程中发生崩溃或网络中断,Broker 收不到 Commit/Rollback 指令,就会通过回查主动询问生产者。这确保了 **half 消息最终一定会收敛到可见或被丢弃**,不会出现悬空状态。 + +**可靠性要点清单:** +- ✅ 业务数据库和 Outbox 必须在**同一个本地事务**中写入 +- ✅ 消费者必须有幂等保护(否则至少一次投递 = 无限次重复) +- ✅ 定时扫描任务需要设置最大重试次数,超过后进入人工处理 +- ✅ 生产环境建议配置 **DLQ(死信队列)**,将连续消费失败的消息归档 + ## 方案二:Saga 模式 Saga 适用于**跨多个服务的长流程操作**,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。 @@ -198,6 +230,81 @@ graph TB > [!warning] 补偿操作的幂等性 > 补偿操作也必须幂等——CancelOrder 可能被触发多次。用订单状态的流转来保证(如只有 PENDING 才能转到 CANCELLED)。 +### 编排式实现示例 (Go) + +编排式的核心是一个 **Saga Coordinator**,它维护着当前步骤的执行状态和已完成的正向/反向操作列表: + +```go +// Step 定义一个 Saga 步骤 +type Step struct { + Name string + Execute func(ctx context.Context) error // 正向操作 + Compensation func(ctx context.Context) error // 补偿操作 +} + +// Orchestrator 编排器 +type Orchestrator struct { + steps []Step + executed []string // 已完成步骤名,用于补偿时逆向遍历 +} + +func (o *Orchestrator) AddStep(name string, exec, compens func(context.Context) error) { + o.steps = append(o.steps, Step{Name: name, Execute: exec, Compensation: compens}) +} + +// Execute 按顺序执行所有步骤,任一步骤失败则逆向补偿 +func (o *Orchestrator) Execute(ctx context.Context) error { + for i, step := range o.steps { + if err := step.Execute(ctx); err != nil { + log.Error("step failed", z.String("step", step.Name), z.Err(err)) + // 逆向补偿:从后往前执行已完步骤的补偿操作 + o.rollback(ctx, i-1) + return fmt.Errorf("saga failed at %s: %w", step.Name, err) + } + o.executed = append(o.executed, step.Name) + } + return nil +} + +func (o *Orchestrator) rollback(ctx context.Context, upTo int) { + for i := len(o.executed) - 1; i >= 0 && i <= upTo; i-- { + // 找到对应步骤的补偿函数并执行 + step := o.steps[i] + if err := step.Compensation(ctx); err != nil { + log.Warn("compensation also failed — need manual intervention", z.String("step", step.Name)) + // 补偿失败必须告警!人工介入处理 + } + } +} +``` + +使用方式——构建一个下单 Saga: + +```go +saga := &Orchestrator{} + +saga.AddStep("create_order", + func(ctx context.Context) error { return orderSvc.Create(ctx, req) }, // 正向 + func(ctx context.Context) error { return orderSvc.Cancel(ctx, orderID) }, // 反向 +) +saga.AddStep("reserve_stock", + func(ctx context.Context) error { return stockSvc.Reserve(ctx, productIDs) }, + func(ctx context.Context) error { return stockSvc.Release(ctx, productIDs) }, +) +saga.AddStep("charge_payment", + func(ctx context.Context) error { return paySvc.Charge(ctx, amount) }, + func(ctx context.Context) error { return paySvc.Refund(ctx, orderID) }, +) + +err := saga.Execute(ctx) +``` + +> [!note] 关键理解:补偿的局限性 +> +> - **补偿不是撤销**:取消订单不会把商品退回到原始状态,而是创建一条反向业务记录 +> - **补偿可能失败**:如果退款接口也挂了,你需要重试 + 告警 + 人工介入机制 +> - **时间窗口问题**:如果正向前向到了第 5 步、补偿到了第 2 步时第 2 步也失败了,第 3~4 步的正向操作已经发生且无法撤销 + ## 方案三:TCC (Try-Confirm-Cancel) TCC 在每个事务步骤中实现三个接口: @@ -242,6 +349,198 @@ sequenceDiagram | 性能 | 中(需要两阶段提交) | 中高(单阶段本地事务) | | 适用场景 | 资金、库存等高敏感业务 | 订单流程、审批流等业务链 | +## 方案四:AT 模式 (Seata) + +> [!question] 什么时候该用 Seata? +> +> 如果你的团队**不愿或没有能力为每个业务方法编写 TCC 接口**,但又有跨库事务需求——Seata 的 AT 模式就是为此设计的。它是"零侵入"的最强候选方案。 + +Seata AT 模式的核心原理是 **全局锁 + 二阶段提交**,业务代码不需要任何改动: + +``` +第一阶段(Global Lock): + 1. 拦截 SQL → 解析前后镜像 + 2. 申请全局锁(锁住被修改的行) + 3. 本地事务执行(含 undo_log 写入) + 4. 提交本地事务(全局锁暂不释放) + +第二阶段(Global Commit/Rollback): + Commit: 异步删除 undo_log,释放全局锁 + Rollback: 利用 undo_log 生成反向 SQL,回滚并释放全局锁 +``` + +### AT 模式时序图 + +```mermaid +sequenceDiagram + participant TC as Transaction Coordinator
(Seata Server) + participant TM as Transaction Manager
(应用 A) + participant RS1 as Resource 1
(订单 DB) + participant RS2 as Resource 2
(库存 DB) + + TM->>TC: 开启全局事务 (xid) + TM->>RS1: BEGIN; UPDATE orders SET ... + Note over RS1: 自动捕获 BEFORE/AFTER 镜像
写入 undo_log + + TM->>RS2: BEGIN; UPDATE stock SET qty = qty - 1 + Note over RS2: 同样写 undo_log + + TM->>RS1: COMMIT (本地事务完成) + TM->>RS2: COMMIT (本地事务完成) + + TM->>TC: 报告二阶段提交完成 + TC-->>RS1: 异步清理 undo_log + TC-->>RS2: 异步清理 undo_log +``` + +### AT 与 XA 的区别 + +很多人混淆 AT 和 XA——它们都是两阶段提交,但实现方式完全不同: + +| 维度 | XA 模式 | AT 模式 (Seata) | +|------|---------|----------------| +| **锁粒度** | 数据库级(长事务锁) | 行级(短期全局锁) | +| **隔离级别** | 需要 SERIALIZABLE | 基于脏读 + 全局锁 | +| **性能** | 较差(锁持有时间长) | 较好(本地事务快速提交后异步清理) | +| **侵入性** | 需配置 DataSource Proxy | 零侵入,自动代理 JDBC | + +### 使用注意事项 + +```go +// Seata Go SDK — 几乎零侵入 +import "github.com/seata/seata-sdk-go-client/tx" + +func CreateOrder(ctx context.Context, req *Request) error { + // 只需添加这一个注解 + tx.GlobalTransaction() + + // 下面的代码完全不变——Seata 自动代理数据源 + orderRepo.Create(ctx, &order) + stockRepo.Decrease(ctx, productID, qty) + + return nil +} +``` + +> [!warning] 生产环境避坑清单 +> +> 1. **undo_log 表必须建**:`seata_undo_log` 是 Seata 自动创建的,用于存储前后镜像。务必保证这个表可用(通常和业务库在同一实例) +> 2. **不要混用本地事务和全局事务**:同一个方法里同时出现 `@Transactional` 和 `@GlobalTransactional` 会导致行为不可预测 +> 3. **异常处理**:全局事务中抛出的异常会被 Seata 捕获并触发回滚,确保异常能正确向上传递 +> 4. **性能考量**:全局锁虽然比 XA 短,但在高并发场景下仍然是瓶颈。**优先用 MQ 最终一致,实在做不到再上 AT** +> 5. **只支持部分数据库**:MySQL、PostgreSQL、Oracle 支持良好;TiDB 等 NewSQL 数据库兼容性有限 + +> [!tip] 选型建议 +> +> AT 模式最适用的场景:**遗留系统微服务化改造**。当原有单体拆分成多个服务时,原有的 `@Transactional` 跨库调用突然失效,引入 AT 模式可以快速过渡——等业务稳定后再逐步迁移到 MQ 事件驱动架构。 + +## 补充保障:重试与死信队列 + +分布式系统中,**没有哪个组件永远可靠**。消息消费失败、网络闪断、下游超时——这些都不可避免。你需要一套完整的错误处理链路: + +```mermaid +flowchart LR + MQ["消息队列"] --> C["消费者"] + C --> FAIL{消费成功} + FAIL -- 是 --> OK["正常结束"] + FAIL -- 否 --> RETRY{重试次数未满} + RETRY -- 是 --> DELAY["延迟重试
指数退避"] + DELAY --> C + RETRY -- 否 --> DLQ["进入死信队列
人工介入"] + + DLQ --> ALERT["告警通知"] + DLQ --> REPLAY["手动重放或修复后补发"] +``` + +### 三种重试策略对比 + +| 策略 | 适用场景 | 示例 | +|------|---------|------| +| **立即重试** | 瞬时故障(连接池未就绪) | 等 100ms 再试 3 次 | +| **延迟重试** | 需要时间窗口等待恢复 | 1s → 2s → 4s → 8s | +| **定时任务补偿** | 批量漏掉的消息 | 每 5 分钟扫描 pending 表 | + +### 死信队列 (DLQ) 设计 + +```go +// 消费者主循环:带重试和 DLQ 保护 +func (c *consumer) Process(ctx context.Context, msg Message) error { + for attempt := 0; attempt < c.maxRetries; attempt++ { + err := c.handle(ctx, msg) + if err == nil { + return nil // 成功 + } + + if isRetryable(err) { + time.Sleep(time.Duration(attempt+1) * time.Second) + continue // 重试 + } + + // 非可重试错误,直接进 DLQ + return c.sendToDLQ(msg, err) + } + + // 超过最大重试次数,进 DLQ + return c.sendToDLQ(msg, fmt.Errorf("exhausted %d retries", c.maxRetries)) +} +``` + +> [!quote] 运维心态 +> +> "一个没有死信队列的消息消费者,就像一辆没有备胎的车——任何一次不可预期的故障都会让你抛锚在路上。" + +## 如何选择分布式事务方案? + +> [!question]- 决策树 +> +> 面对跨服务的数据一致性问题,你不需要每次都重新选型。按照下面的思路逐层判断即可: + +```mermaid +flowchart TD + START["跨服务写操作"] --> Q1{核心资金或资产} + + Q1 -- 是且要求强一致 --> TCC["TCC"] + Q1 -- 否 --> Q2{流程步数超过5} + + Q2 -- 是长流程 --> SAGA["Saga"] + Q2 -- 否简单链路 --> Q3{容忍秒级延迟} + + Q3 -- 可以 --> MQ["本地事务+MQ(Outbox)"] + Q3 -- 不可以毫秒级 --> TXMSG["RocketMQ事务消息"] + + Q3 -- 无法接受最终一致 --> SEATA["Seata AT模式"] + + TCC --> END["完成选择"] + SAGA --> END + MQ --> END + TXMSG --> END + SEATA --> WARNING["过渡方案,后续应迁移至事件驱动架构"] + WARNING --> END + + style TCC fill:#e8f5e9,stroke:#4caf50,stroke-width:2px + style SAGA fill:#fff3e0,stroke:#ff9800,stroke-width:2px + style MQ fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + style TXMSG fill:#e3f2fd,stroke:#1976d2,stroke-width:2px + style SEATA fill:#fce4ec,stroke:#e91e63,stroke-width:2px +``` + +### 决策速查矩阵 + +| 你的业务特征 | 推荐方案 | 理由 | +|-------------|---------|------| +| 日订单量百万级,通知类场景 | Outbox + MQ | 性能最好,开发成本最低 | +| 支付/清算/账务核心链路 | TCC | 资源占用期间不允许其他事务修改 | +| 审批流、物流状态流转(多步) | Saga 编排式 | 流程清晰,补偿机制完备 | +| 已有 RocketMQ,对延迟敏感 | 事务消息 | 绕过 Outbox 轮询延迟 | +| 遗留系统快速拆分 | Seata AT | 零侵入,快速过渡 | +| 只需要"最终一致" | 本地事务 + MQ | 80% 的场景这就是答案 | + +> [!tip] 黄金法则 +> +> **"能用异步事件解决的,不要用两阶段提交;能本地事务 + MQ 解决的,不要上框架。"** +> +> 复杂度越高,出问题的概率越大。每一层抽象都引入了新的故障点——协调器挂了怎么办?补偿逻辑写错了怎么发现?回滚又失败了谁来做?保持简单是最好的工程纪律。 + ## 关联笔记 - [[03-数据一致性/01-数据库拆分]] — 数据库拆分是分布式事务的前提 diff --git a/hhs/MS/03-数据一致性/03-ID生成.md b/hhs/MS/03-数据一致性/03-ID生成.md index 0f476e5..439c86e 100644 --- a/hhs/MS/03-数据一致性/03-ID生成.md +++ b/hhs/MS/03-数据一致性/03-ID生成.md @@ -1,6 +1,6 @@ --- tags: [microservice, id-generation, snowflake, uuid, distributed-id] -create time: 2026-05-05 +create time: 2026-05-05 12:00 --- # 分布式 ID 生成 diff --git a/hhs/MS/05-部署运维/01-容器化.md b/hhs/MS/05-部署运维/01-容器化.md index e8014d0..c658d8c 100644 --- a/hhs/MS/05-部署运维/01-容器化.md +++ b/hhs/MS/05-部署运维/01-容器化.md @@ -1,76 +1,37 @@ --- tags: [microservice, docker, container, image-optimization] -create time: 2026-05-05 +create time: 2026-05-05 15:30 --- -# 容器化 +# 容器化 — 参考手册 ## 概述 Docker 容器是微服务交付的标准单元。它解决了"在我机器上是好的"这个经典问题——**开发、测试、生产使用完全一致的运行时环境**。 +本文档是总入口:核心概念、决策矩阵、速查表见本页;详细教程和实操指南在子文档中。 + ```mermaid graph TB CODE["源代码"] --> BUILD["CI 构建镜像"] - BUILD --> REGISTRY["镜像仓库
Harbor / ECR / ACR"] + BUILD --> SCAN["安全扫描
Trivy / Snyk"] + SCAN --> REGISTRY["镜像仓库
Harbor / ECR / ACR"] REGISTRY --> K8s["Kubernetes 部署"] - + style BUILD fill:#e3f2fd style REGISTRY fill:#fff3e0 style K8s fill:#e8f5e9 ``` -## Dockerfile 最佳实践 +## 快速导航 -### Go 多阶段构建(推荐) +| 主题 | 定位 | 文档 | +|------|------|------| +| Dockerfile 最佳实践 | **教程**:多阶段构建、BuildKit、缓存技巧 | [[01-容器化/01-Dockerfile最佳实践]] | +| 多架构构建 | **进阶**:Buildx、跨平台编译 | [[01-容器化/02-多架构构建]] | +| 镜像安全 | **安全**:漏洞扫描、distroless、签名 | [[01-容器化/03-镜像安全]] | -```dockerfile -# ========== 阶段 1: 构建 ========== -FROM golang:1.22-alpine AS builder - -RUN apk --no-cache add git ca-certificates - -WORKDIR /app -COPY go.mod go.sum ./ -RUN go mod download - -COPY . . -ARG LDFLAGS="-s -w -extldflags '-static'" -RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "$LDFLAGS" -o server . - -# ========== 阶段 2: 运行时 ========== -FROM alpine:latest - -RUN apk --no-cache add ca-certificates tzdata && \ - cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ - echo "Asia/Shanghai" > /etc/timezone - -RUN addgroup -S appgroup && adduser -S appuser -G appgroup -USER appuser - -WORKDIR /app -COPY --from=builder /app/server . - -EXPOSE 8080 - -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD wget -qO- http://localhost:8080/healthz || exit 1 - -CMD ["./server"] -``` - -### 关键优化点 - -| 优化项 | 方法 | 效果 | -|--------|------|------| -| **多阶段构建** | 编译和运行分离 | 镜像从 800MB → 15MB | -| **alpine 基础镜像** | 替代 debian/ubuntu | 减小体积 | -| **非 root 运行** | `USER appuser` | 安全合规 | -| **静态链接** | `CGO_ENABLED=0` | 不依赖系统库 | -| **layer cache** | `go.mod` 先 COPY | CI 加速构建 | -| **健康检查** | HEALTHCHECK 指令 | K8s 原生支持 | - -### `.dockerignore` +## .dockerignore 模板 ``` .git @@ -86,62 +47,35 @@ tests/ > [!tip] 为什么 .dockerignore 很重要? > -> 如果不排除 `.git` 目录,整个版本历史都会被打包进镜像(增加数百 MB)。如果排除不当,可能遗漏必要的配置文件。 +> 如果不排除 `.git` 目录,整个版本历史都会被打包进镜像(增加数百 MB)。如果排除不当,可能遗漏必要的配置文件。建议每个项目根目录都包含此文件。 -## 镜像安全 +## 层缓存核心原则 -```mermaid -flowchart LR - Scan["镜像扫描 (Trivy/Snyk)"] --> Clean{"漏洞等级?"} - - Clean -- "CRITICAL/HIGH" --> Block["❌ 阻止推送"] - Clean -- "MEDIUM" --> Review["⚠️ 人工审核"] - Clean -- "LOW/INFO" --> Allow["✅ 允许推送"] - - style Block fill:#ffebee - style Review fill:#fff3e0 - style Allow fill:#e8f5e9 -``` +> **经常变动的内容靠近 COPY,少变的放前面**。 +> +> ```dockerfile +> # ✅ 好:只有 go.mod/go.sum 变化时才重新下载依赖 +> COPY go.mod go.sum ./ +> RUN go mod download +> COPY . . +> RUN go build +> ``` -### 安全检查清单 +## 基础镜像选型 -- [ ] 不使用 `latest` tag(永远用具体版本号) -- [ ] 不安装不必要的软件包(`apk del --purge .build-deps`) -- [ ] 定期更新基础镜像(修复 CVE) -- [ ] 使用非 root 用户运行 -- [ ] 镜像不包含密钥、密码、私钥 -- [ ] 使用最小基础镜像(scratch / distroless) +| 基础镜像 | 典型 Go 服务大小 | 适用场景 | +|---------|-----------------|---------| +| `ubuntu:22.04` | ~35 MB | 兼容性要求高 | +| `debian:bookworm-slim` | ~20 MB | 需要 glibc 的动态链接程序 | +| `alpine:3.19` | ~7 MB | 小体积优先,注意 musl libc 兼容 | +| `distroless/static` | ~5 MB | 生产推荐,无 shell | +| `scratch` | <取决于二进制 | 仅静态链接二进制可运行 | -### Distroless 镜像 +> [!warning] Alpine 与 musl libc +> +> Alpine 使用 musl libc 而非 glibc。某些 C 扩展(如 `node-gyp`、部分 Python wheel)可能无法编译或运行时行为不一致。Go 服务通过 `CGO_ENABLED=0` 规避了此问题,但 Node.js/Python 应用需谨慎评估。 -Google 推出的**无 shell、无包管理器**的极简运行时镜像: - -```dockerfile -# 最极致的精简 -FROM gcr.io/distroless/static-debian12 -COPY --from=builder /app/server . -USER nonroot -CMD ["./server"] -``` - -> ⚠️ 注意:distroless 镜像没有 shell (`/bin/sh`),调试时需要额外工具(如 debug 镜像或 `kubectl exec` 到 sidecar)。 - -## 镜像仓库管理 - -```mermaid -flowchart LR - Dev["开发者本地"] -->|"docker push"| DEV_REPO["dev 仓库
v1.2.3-dev"] - - Staging["Staging 验证"] -->|"通过"| PROD_REPO["prod 仓库
v1.2.3"] - - K8s["K8s Cluster"] -->|"pull"| PROD_REPO - - PR["PR Merge"] --> Tag["打标签 v1.2.3"] - Tag --> ProdRepoMove["移动到 prod 仓库"] - - style DEV_REPO fill:#fff3e0 - style PROD_REPO fill:#e8f5e9 -``` +## 镜像仓库方案对比 | 仓库方案 | 特点 | 适用场景 | |---------|------|---------| @@ -149,7 +83,26 @@ flowchart LR | **ECR / ACR / GCR** | 云厂商托管 | 全云环境 | | **Docker Hub** | 公共免费 | 开源项目 | +## BuildKit 特性速查 + +```bash +export DOCKER_BUILDKIT=1 +docker build --cache-from=harbor.example.com/app/cache:latest . +docker build --ssh default . +docker build --secret id=token,env=GITHUB_TOKEN . +``` + +## 常见问题排查 + +| 症状 | 原因 | 解决 | +|------|------|------| +| `exec: "app": not found` | 多阶段 COPY 路径错误 | 检查绝对路径和文件名拼写 | +| `standard_init_linux.go: exec user process caused: permission denied` | 没有执行权限或缺少换行符 | `chmod +x`, 确保 Linux 换行 | +| `service unavailable` (K8s) | HEALTHCHECK 未就绪 | 增大 `--start-period` | +| 镜像体积异常大 | `.git` 未排除或多层 RUN 未清理 | 检查 `.dockerignore`,合并 RUN 并 `rm -rf` | +| `cannot execute: executable file not found` | 架构不匹配(arm64 → amd64) | 确认 `TARGETARCH` 或手动指定 `--platform` | + ## 关联笔记 -- [[05-部署运维/02-Kubernetes]] — K8s 以 Pod 为部署单元,镜像来自 Docker -- [[05-部署运维/03-CICD与GitOps]] — CI/CD 流水线中的镜像构建环节 +- [[hhs/MS/05-部署运维/02-Kubernetes]] — K8s 以 Pod 为部署单元,镜像来自 Docker +- [[hhs/MS/05-部署运维/03-CICD与GitOps]] — CI/CD 流水线中的镜像构建环节 diff --git a/hhs/MS/05-部署运维/01-容器化/01-Dockerfile最佳实践.md b/hhs/MS/05-部署运维/01-容器化/01-Dockerfile最佳实践.md new file mode 100644 index 0000000..b88c345 --- /dev/null +++ b/hhs/MS/05-部署运维/01-容器化/01-Dockerfile最佳实践.md @@ -0,0 +1,150 @@ +--- +tags: [docker, container, dockerfile, buildkit, image-optimization] +create time: 2026-05-18 00:45 +--- + +# Dockerfile 最佳实践 — 教程 + +## 概述 + +Dockerfile 是微服务交付的标准起点。本文档从多阶段构建出发,详解层缓存技巧、BuildKit 高级特性和常见问题排查。更多话题:多架构构建见 [[../01-容器化/02-多架构构建]],镜像安全见 [[../01-容器化/03-镜像安全]]。 + +## Go 多阶段构建(推荐) + +```dockerfile +# ========== 阶段 1: 构建 ========== +FROM golang:1.22-alpine AS builder + +RUN apk --no-cache add git ca-certificates + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +ARG LDFLAGS="-s -w -extldflags '-static'" +RUN CGO_ENABLED=0 GOOS=linux go build -ldflags "$LDFLAGS" -o server . + +# ========== 阶段 2: 运行时 ========== +FROM alpine:latest + +RUN apk --no-cache add ca-certificates tzdata && \ + cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \ + echo "Asia/Shanghai" > /etc/timezone + +RUN addgroup -S appgroup && adduser -S appuser -G appgroup +USER appuser + +WORKDIR /app +COPY --from=builder /app/server . + +EXPOSE 8080 + +HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ + CMD wget -qO- http://localhost:8080/healthz || exit 1 + +CMD ["./server"] +``` + +> [!question] 为什么要用多阶段构建? +> +> 单阶段构建中,编译工具和源码都在最终镜像里——一个 Go 项目的镜像轻松超过 800MB。**多阶段构建**把构建和运行拆成两个独立的镜像层,第二阶段只 COPY 二进制文件,最终镜像缩小到十几 MB。 + +## 关键优化点速查 + +| 优化项 | 方法 | 效果 | +|--------|------|------| +| **多阶段构建** | 编译和运行分离 | 镜像从 800MB → 15MB | +| **alpine 基础镜像** | 替代 debian/ubuntu | 减小体积 | +| **非 root 运行** | `USER appuser` | 安全合规 | +| **静态链接** | `CGO_ENABLED=0` | 不依赖系统库 | +| **layer cache** | `go.mod` 先 COPY | CI 加速构建 | +| **健康检查** | HEALTHCHECK 指令 | K8s 原生支持 | + +## `.dockerignore` — 别忽略的文件 + +``` +.git +.gitignore +*.md +vendor/ +tests/ +*.log +.DS_Store +.idea/ +.vscode/ +``` + +> [!tip] 为什么 .dockerignore 很重要? +> +> 如果不排除 `.git` 目录,整个版本历史都会被打包进镜像(增加数百 MB)。如果排除不当,可能遗漏必要的配置文件。建议每个项目根目录都包含此文件。 + +## 层缓存技巧 + +### ❌ 差的缓存策略 + +```dockerfile +# 差:任何文件改动都会让后续所有 layer 失效 +COPY . . +RUN go build +``` + +### ✅ 好的缓存策略 + +```dockerfile +# 好:只有 go.mod/go.sum 变化时才重新下载依赖 +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN go build +``` + +核心原则:**经常变动的内容靠近 COPY,少变的放前面**。这样当代码频繁变更时,依赖下载和构建步骤仍能命中缓存。 + +## BuildKit 特性 + +BuildKit 是新一代构建引擎,默认在 Docker 18.09+ 和所有 Docker Desktop 中启用: + +```bash +# 启用 BuildKit 加速 +export DOCKER_BUILDKIT=1 + +# 利用远程缓存 (需配合 registry) +docker build --cache-from=harbor.example.com/app/cache:latest . + +# SSH agent forwarding(拉取私有依赖用) +docker build --ssh default . + +# 秘密变量注入(不进 Docker history) +docker build --secret id=token,env=GITHUB_TOKEN . +``` + +```dockerfile +# syntax=docker/dockerfile:1 +# ^^^ 必须声明语法版本以启用 BuildKit + +FROM golang:1.22-alpine AS builder +RUN --mount=type=cache,target=/go/pkg/mod \ + --mount=type=cache,target=/root/.cache/go-build \ + go mod download && go build -o server . +``` + +> [!info] `--mount=type=cache` vs RUN 缓存 +> +> Docker 默认的层缓存会在源文件变化时跳过整层;而 `type=cache` 是增量更新本地缓存目录,不受构建上下文变化影响,适合依赖下载和编译缓存。 + +## 常见问题排查 + +| 症状 | 原因 | 解决 | +|------|------|------| +| `exec: "app": not found` | 多阶段 COPY 路径错误 | 检查绝对路径和文件名拼写 | +| `standard_init_linux.go: exec user process caused: permission denied` | 没有执行权限或缺少换行符 | `chmod +x`, 确保 Linux 换行 | +| `service unavailable` (K8s) | HEALTHCHECK 未就绪 | 增大 `--start-period` | +| 镜像体积异常大 | `.git` 未排除或多层 RUN 未清理 | 检查 `.dockerignore`,合并 RUN 并 `rm -rf` | +| `cannot execute: executable file not found` | 架构不匹配(arm64 → amd64) | 确认 `TARGETARCH` 或手动指定 `--platform` | + +## 关联笔记 + +- [[../01-容器化/02-多架构构建]] — 一次构建全平台镜像 +- [[../01-容器化/03-镜像安全]] — Trivy 扫描与 distroless 镜像 +- [[../hhs/MS/05-部署运维/02-Kubernetes]] — K8s 以 Pod 为部署单元,镜像来自 Docker diff --git a/hhs/MS/05-部署运维/01-容器化/02-多架构构建.md b/hhs/MS/05-部署运维/01-容器化/02-多架构构建.md new file mode 100644 index 0000000..b4a7fac --- /dev/null +++ b/hhs/MS/05-部署运维/01-容器化/02-多架构构建.md @@ -0,0 +1,91 @@ +--- +tags: [docker, multi-arch, buildx, cross-platform] +create time: 2026-05-18 00:45 +--- + +# 多架构构建 — Buildx 实战 + +## 概述 + +随着 Apple Silicon 和 ARM 服务器普及,一个平台只能构建一种架构的镜像已无法满足需求。Docker Buildx 让我们**一次构建、全平台运行**。本文档讲解多架构构建的核心操作和原理。 + +## Buildx 快速上手 + +```bash +# 启用 buildx 并创建 multi-platform builder +docker buildx create --name mybuilder --use +docker buildx inspect --bootstrap +``` + +## 跨平台构建推送 + +```bash +# 为 amd64 + arm64 同时构建并推送到仓库 +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + -t harbor.example.com/app/server:v1.2.3 \ + --push \ + . +``` + +> [!note] manifest list 原理 +> +> 多架构镜像本质上是一个 **manifest list**——它不直接包含二进制文件,而是记录每个架构对应的镜像 digest。拉取时,客户端根据自身平台自动选择正确的变体。可以用 `docker buildx imagetools inspect ` 查看完整列表。 + +## 不同场景的构建策略 + +| 场景 | 策略 | 命令 | +|------|------|------| +| 开发阶段 | 只构建本机架构 (`linux/arm64`),速度最快 | `docker buildx build --platform linux/arm64 .` | +| CI 流水线 | 构建全平台 (`linux/amd64,linux/arm64`),统一推送 | `--platform linux/amd64,linux/arm64 --push` | +| 临时调试 | `--load` 仅构建本地可用 | `docker buildx build --load .` | + +## Go 跨平台编译提示 + +在 Dockerfile 中使用 `ARG TARGETARCH`(Buildx 自动传入): + +```dockerfile +FROM golang:1.22-alpine AS builder + +ARG TARGETARCH +ARG TARGETPLATFORM + +RUN echo "Building for: $TARGETARCH ($TARGETPLATFORM)" + +WORKDIR /app +COPY go.mod go.sum ./ +RUN go mod download + +COPY . . +RUN CGO_ENABLED=0 GOOS=linux GOARCH=$TARGETARCH \ + go build -a -ldflags "-s -w" -o server-$TARGETARCH . +``` + +> [!info] 环境变量对照 +> +> | 变量 | 示例值 | 说明 | +> |------|--------|------| +> | `TARGETARCH` | `amd64`, `arm64` | CPU 架构 | +> | `TARGETOS` | `linux`, `darwin` | 操作系统 | +> | `TARGETPLATFORM` | `linux/amd64`, `linux/arm64` | 完整平台标识 | + +## 补充:QEMU 模拟(CI 中的备用方案) + +当你的 CI 环境不支持 Buildx QEMU 插件时,可以手动安装: + +```bash +# 注册 QEMU 模拟器 +docker run --privileged --rm tonistiigi/binfmt --install all + +# 之后即可通过 qemu 模拟不同平台构建(速度较慢) +docker buildx build --platform linux/arm64 -t my-app:arm64 . +``` + +> [!warning] 模拟构建 vs 原生交叉编译 +> +> QEMU 模拟可以运行在其他架构上,但构建速度极慢(通常比原生慢 5~10 倍)。生产环境推荐使用 **Go 原生交叉编译**(`GOOS=linux GOARCH=arm64`),零依赖且秒级完成。只在需要 C 扩展的场景才考虑 QEMU 模拟。 + +## 关联笔记 + +- [[../01-容器化/01-Dockerfile最佳实践]] — 多阶段构建 + BuildKit 特性 +- [[../hhs/MS/05-部署运维/02-Kubernetes]] — K8s 节点混合架构时的调度考量 diff --git a/hhs/MS/05-部署运维/01-容器化/03-镜像安全.md b/hhs/MS/05-部署运维/01-容器化/03-镜像安全.md new file mode 100644 index 0000000..7979729 --- /dev/null +++ b/hhs/MS/05-部署运维/01-容器化/03-镜像安全.md @@ -0,0 +1,150 @@ +--- +tags: [docker, image-security, trivy, distroless, registry] +create time: 2026-05-18 00:45 +--- + +# 镜像安全 — 扫描、基线 & 仓库管理 + +## 概述 + +镜像安全不仅仅是"不要有 CVE"——它是一个完整的供应链安全体系,涵盖基础镜像选型、漏洞扫描、仓库分级管理和推送门禁。更多内容参见 [[../01-容器化/01-Dockerfile最佳实践]]。 + +## 镜像扫描流程 + +```mermaid +flowchart LR + Scan["镜像扫描 (Trivy/Snyk)"] --> Clean{"漏洞等级?"} + + Clean -- "CRITICAL/HIGH" --> Block["❌ 阻止推送"] + Clean -- "MEDIUM" --> Review["⚠️ 人工审核"] + Clean -- "LOW/INFO" --> Allow["✅ 允许推送"] + + style Block fill:#ffebee + style Review fill:#fff3e0 + style Allow fill:#e8f5e9 +``` + +### Trivy 常用命令 + +```bash +# 扫描镜像 +trivy image harbor.example.com/app/server:v1.2.3 + +# 只报告 HIGH 及以上级别 +trivy image --severity HIGH,CRITICAL harbor.example.com/app/server:v1.2.3 + +# 输出 JSON 供 CI 集成 +trivy image --format json --output report.json harbor.example.com/app/server:v1.2.3 + +# 扫描 Dockerfile 中的潜在问题 +trivy config Dockerfile +``` + +## 安全检查清单 + +- [ ] 不使用 `latest` tag(永远用具体版本号)→ 精确回滚 +- [ ] 不安装不必要的软件包(`apk del --purge .build-deps`)→ 最小攻击面 +- [ ] 定期更新基础镜像(修复 CVE)→ 跟上安全补丁节奏 +- [ ] 使用非 root 用户运行 → `USER appuser` +- [ ] 镜像不包含密钥、密码、私钥 → CI/CD Secret 挂载代替 ENV +- [ ] 使用最小基础镜像(scratch / distroless)→ 减少暴露面 + +## Distroless 镜像 + +Google 推出的**无 shell、无包管理器**的极简运行时镜像: + +```dockerfile +# 最极致的精简 +FROM gcr.io/distroless/static-debian12 +COPY --from=builder /app/server . +USER nonroot +CMD ["./server"] +``` + +> ⚠️ 注意:distroless 镜像没有 shell (`/bin/sh`),调试时需要额外工具(如 debug 镜像或 `kubectl exec` 到 sidecar)。 + +### 常用 Distroless 变种 + +| 镜像 | 适用场景 | 大小 | +|------|---------|------| +| `gcr.io/distroless/static-debian12` | 纯静态二进制 | ~2MB | +| `gcr.io/distroless/base-debian12` | 需要 glibc 的动态链接程序 | ~30MB | +| `gcr.io/distroless/java17` | Java 应用(含 JRE) | ~150MB | +| `gcr.io/distroless/nodejs18` | Node.js 应用 | ~100MB | + +## 镜像压缩对比参考 + +| 基础镜像 | 典型 Go 服务大小 | 备注 | +|---------|-----------------|------| +| `ubuntu:22.04` | ~35 MB | 完整发行版 | +| `debian:bookworm-slim` | ~20 MB | 裁剪后较常用 | +| `alpine:3.19` | ~7 MB | musl libc,偶有兼容问题 | +| `distroless/static` | ~5 MB | 无 shell,生产推荐 | +| `scratch` | <取决于二进制 | 仅静态链接二进制可运行 | + +> [!warning] Alpine 与 musl libc +> +> Alpine 使用 musl libc 而非 glibc。某些 C 扩展(如 `node-gyp`、部分 Python wheel)可能无法编译或运行时行为不一致。Go 服务通过 `CGO_ENABLED=0` 规避了此问题,但 Node.js/Python 应用需谨慎评估。 + +## 镜像仓库管理与流转 + +```mermaid +flowchart LR + Dev["开发者本地"] -->|"docker push"| DEV_REPO["dev 仓库
v1.2.3-dev"] + + Staging["Staging 验证"] -->|"通过"| PROD_REPO["prod 仓库
v1.2.3"] + + K8s["K8s Cluster"] -->|"pull"| PROD_REPO + + PR["PR Merge"] --> Tag["打标签 v1.2.3"] + Tag --> ProdRepoMove["移动到 prod 仓库"] + + style DEV_REPO fill:#fff3e0 + style PROD_REPO fill:#e8f5e9 +``` + +| 仓库方案 | 特点 | 适用场景 | +|---------|------|---------| +| **Harbor** | 自托管,RBAC + 扫描 | 国内企业首选 | +| **ECR / ACR / GCR** | 云厂商托管 | 全云环境 | +| **Docker Hub** | 公共免费 | 开源项目 | + +### Harbor 最佳实践 + +| 实践 | 说明 | +|------|------| +| **项目隔离** | dev/prod 独立项目,不同团队有不同权限 | +| **自动扫描** | 推送时自动触发 Trivy 扫描,阻断高危漏洞 | +| **复制规则** | dev 仓库 → prod 仓库自动复制已通过测试的镜像 | +| **生命周期策略** | 自动清理过期镜像和垃圾数据 | + +## 补充:镜像签名与 Cosign + +为了防止中间人攻击或被篡改的镜像被拉到集群,可以使用 Sigstore Cosign 对镜像签名: + +```bash +# 签名镜像 +cosign sign --key cosign.key harbor.example.com/app/server:v1.2.3 + +# 在 CI 中验证签名 +cosign verify --key cosign.pub harbor.example.com/app/server:v1.2.3 + +# 不依赖密钥的验证( rekor 透明日志) +cosign verify --certificate-identity=email@company.com \ + --certificate-oidc-issuer=https://accounts.google.com \ + harbor.example.com/app/server:v1.2.3 +``` + +> [!info] 镜像签名 vs 漏洞扫描 +> +> 两者解决不同问题: +> - **漏洞扫描** = 这个镜像有没有已知漏洞? +> - **镜像签名** = 这个镜像确实是我们构建的,没有被篡改? +> +> 生产环境应该两者都有。签名可以作为 Gatekeeper/Admission Webhook 的一部分,在 Pod 启动前强制验证镜像签名。 + +## 关联笔记 + +- [[../01-容器化/01-Dockerfile最佳实践]] — Dockerfile 中的安全加固 +- [[../01-容器化/02-多架构构建]] — Buildx 构建全平台镜像 +- [[../hhs/MS/05-部署运维/03-CICD与GitOps]] — CI 流水线中的安全扫描环节 diff --git a/hhs/MS/05-部署运维/02-Kubernetes.md b/hhs/MS/05-部署运维/02-Kubernetes.md index 4e1d4cb..9f0e7bb 100644 --- a/hhs/MS/05-部署运维/02-Kubernetes.md +++ b/hhs/MS/05-部署运维/02-Kubernetes.md @@ -1,14 +1,16 @@ --- tags: [microservice, kubernetes, k8s, container-orchestration] -create time: 2026-05-05 +create time: 2026-05-05 18:30 --- -# Kubernetes +# Kubernetes — 参考手册 ## 概述 Kubernetes (K8s) 是微服务架构的事实标准编排引擎。它将容器化的服务组织成声明式的资源对象,自动处理部署、扩展、故障恢复。 +本文档是总入口:顶层概念、决策矩阵、速查表见本页;详细教程和实操指南在子文档中。 + ```mermaid graph TB subgraph CLUSTER["K8s Cluster"] @@ -41,226 +43,99 @@ graph TB style N2 fill:#fff3e0 ``` +## 快速导航 + +| 主题 | 定位 | 文档 | +|------|------|------| +| 核心概念与 Deployment | **教程**:Pod/Deployment/Probe、滚动更新 | [[02-Kubernetes/01-核心概念与Deployment]] | +| 配置管理 | **操作手册**:ConfigMap/Secret 注入、热更新 | [[02-Kubernetes/02-配置管理]] | +| 网络与服务发现 | **教程**:Service 类型、Ingress、Headless | [[02-Kubernetes/03-网络与服务发现]] | +| 扩缩容与有状态应用 | **教程**:HPA / VPA / StatefulSet | [[02-Kubernetes/04-扩缩容与有状态应用]] | +| 调度控制 | **进阶**:Affinity/Taint/TopologySpread | [[02-Kubernetes/05-调度控制]] | +| 资源与安全管控 | **安全**:Quota/LimitRange/NetworkPolicy | [[02-Kubernetes/06-资源与安全管控]] | +| 运维排查 | **速查**:上线清单、诊断命令、FAQ | [[02-Kubernetes/07-运维排查]] | + ## 核心概念速查 | K8s 对象 | 用途 | 类比 | |---------|------|------| | **Pod** | 最小部署单元,包含一个或多个容器 | 应用实例 | | **Deployment** | 管理 Pod 的副本数和滚动更新 | 应用的"模板" | -| **Service** | 稳定的网络入口,负载均衡 | 内部 VIP | -| **Ingress** | HTTP/HTTPS 路由规则 | 外部网关 | -| **ConfigMap** | 配置注入(明文) | 环境变量/配置文件 | -| **Secret** | 敏感配置注入(base64) | 密码/API Key | -| **HPA** | 根据指标自动扩缩容 | 弹性伸缩 | -| **StatefulSet** | 有状态应用的有序管理 | DB、ZK | -| **Job/CronJob** | 一次性任务 / 定时任务 | 批处理 | +| **Service** | 稳定的网络入口,负载均衡 | 内部 VIP → [[02-Kubernetes/03-网络与服务发现]] | +| **Ingress** | HTTP/HTTPS 路由规则 | 外部网关 → [[02-Kubernetes/03-网络与服务发现]] | +| **ConfigMap** | 配置注入(明文) → [[02-Kubernetes/02-配置管理]] | +| **Secret** | 敏感配置注入(base64)→ [[02-Kubernetes/02-配置管理]] | +| **HPA** | 根据指标自动扩缩容 → [[02-Kubernetes/04-扩缩容与有状态应用]] | +| **StatefulSet** | 有状态应用的有序管理 | DB、ZK → [[02-Kubernetes/04-扩缩容与有状态应用]] | -## Deployment 详解 +## 部署驱动模式对比 -### 完整示例 +| 维度 | 传统 CI/CD (Push) | GitOps (Pull) | +|------|------------------|---------------| +| 部署驱动 | CI 服务器主动推送 | Git 仓库变动触发拉取 | +| 状态源 | CI pipeline 历史 | Git commit history | +| 漂移检测 | 通常无 | 持续比对,自动修复 | +| 安全边界 | CI 需直连 K8s | ArgoCD 集群内运行 | +| 回滚方式 | 回到上一次的 pipeline | `git revert` + 自动同步 | +| 代表工具 | Jenkins, GitHub Actions | ArgoCD, Flux | -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: order-service - labels: - app: order - version: v1.2.3 -spec: - replicas: 3 # 期望副本数 - strategy: - type: RollingUpdate - rollingUpdate: - maxSurge: 1 # 最多超额 1 个 Pod - maxUnavailable: 0 # 滚动更新期间不允许不可用 - - selector: - matchLabels: - app: order - - template: - metadata: - labels: - app: order - version: v1.2.3 - spec: - containers: - - name: order-service - image: registry.example.com/order:v1.2.3 - ports: - - containerPort: 8080 - - # ========== 资源配置 ========== - resources: - requests: # 调度依据:保证至少有这些 - cpu: "250m" - memory: "256Mi" - limits: # 硬上限:超过则 OOMKill/CPU Throttle - cpu: "500m" - memory: "512Mi" - - # ========== 探针 ========== - livenessProbe: - httpGet: - path: /healthz - port: 8080 - initialDelaySeconds: 15 - periodSeconds: 10 - failureThreshold: 3 # 连续失败 3 次才重启 - - readinessProbe: - httpGet: - path: /ready - port: 8080 - initialDelaySeconds: 5 - periodSeconds: 5 - failureThreshold: 3 - - startupProbe: - httpGet: - path: /healthz - port: 8080 - failureThreshold: 30 # 最长等待 300s (慢启动友好) - - # ========== 环境变量 & 挂载 ========== - envFrom: - - configMapRef: - name: order-service-config - - secretRef: - name: order-service-secrets - - lifecycle: - preStop: - exec: - command: ["sh", "-c", "sleep 15"] # 优雅退出,给 LB 摘流时间 +> **选型建议**:团队规模 < 20 人时 Push 模式足够。≥ 50 人或多环境场景建议迁移到 GitOps → [[../03-CICD与GitOps]] + +## Secret 管理方案对比 + +| 方案 | 适用场景 | 优点 | 缺点 | +|------|---------|------|------| +| K8s 原生 Secret | 小规模内部团队 | 零成本,开箱即用 | etcd 明文存储 | +| ESO (External Secrets) | 已有 Vault / AWS SSM | Git 中无密文 | 需维护额外组件 | +| SOPS + sealed-secrets | ArgoCD 用户 | 加密文件可提交到 Git | 需管理 PKI | +| 云厂商 Secret Manager | 深度绑定单一云平台 | 审计完善 | 平台锁定 | + +> **渐进路径**:K8s Secret → 接入云厂商 Secret Manager → 引入 ESO 实现多云解耦。 + +## 编排工具选型 + +| 服务规模 | 推荐方案 | 理由 | +|----------|---------|------| +| < 10 个服务 | Kustomize / 裸 YAML | 复杂度高于收益 | +| 10~50 个服务 | Helm | 模板复用价值明显 | +| > 50 个服务 | Helm + Kustomize overlays | Helm 管模板,Kustomize 管环境差异 | + +## 关键决策流程图 + +### 安全选型决策 + +```mermaid +flowchart TD + START["开始选型"] --> SIZE["团队 < 20人?"] + SIZE -- 是 --> K8SSECRET["✅ K8s 原生 Secret"] + SIZE -- 否 --> CLOUD["深度绑定云厂商?"] + CLOUD -- 是 --> SECRETMGR["✅ Cloud Secret Manager"] + CLOUD -- 否 --> GITOPS["使用 ArgoCD?"] + GITOPS -- 是 --> SOPS["✅ SOPS + sealed-secrets"] + GITOPS -- 否 --> ESO["✅ External Secrets Operator"] + + style K8SSECRET fill:#e8f5e9 + style SECRETMGR fill:#e3f2fd + style SOPS fill:#fff3e0 + style ESO fill:#f3e5f5 ``` -### Probe 选择指南 +## 附录:常用命令速查 -| 探针类型 | 触发条件 | 动作 | 适用场景 | -|---------|---------|------|---------| -| **Liveness** | `/healthz` 返回非 2xx | 重启容器 | 死锁、无法恢复的崩溃 | -| **Readiness** | `/ready` 返回非 2xx | 摘除 Service 流量 | 依赖未就绪、热加载中 | -| **Startup** | 首次成功前持续失败 | 不重启,只等待 | 大模型/JVM 冷启动 | +### kubectl 基础 +```bash +kubectl set image deployment/order-service \ + order=registry.example.com/order-service:v1.2.3 -n production -> [!warning] 经典陷阱:CrashLoopBackOff -> -> 如果 Liveness Probe 因为 DB 连接超时而返回 503,K8s 会认为容器挂了并反复重启它——这就是 CrashLoopBackOff。正确做法是:让 `/healthz` 做降级判断(DB 不可用时返回 200),用 `/ready` 来摘除流量。 - -## Service 与 Ingress - -### Service 类型 - -| 类型 | 特点 | 使用场景 | -|------|------|---------| -| **ClusterIP** | 集群内 IP,外部不可访问 | 默认,内部服务间调用 | -| **NodePort** | 在每个 Node 上开端口 | 调试、临时访问 | -| **LoadBalancer** | 云厂商分配公网 IP | 对外暴露的服务 | -| **ExternalName** | CNAME 到外部域名 | 对接外部系统 | - -```yaml -# ClusterIP Service — 服务发现的载体 -apiVersion: v1 -kind: Service -metadata: - name: order-service -spec: - selector: - app: order - ports: - - port: 80 - targetPort: 8080 - protocol: TCP - type: ClusterIP +kubectl rollout status deployment/order-service -n production --timeout=120s +kubectl rollout undo deployment/order-service -n production ``` -调用方只需 `http://order-service:80`,K8s 通过 iptables/IPVS 自动实现负载均衡。 +### 关联笔记 -### Ingress — HTTP 路由 - -```yaml -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: main-ingress - annotations: - nginx.ingress.kubernetes.io/rewrite-target: / -spec: - rules: - - host: api.example.com - http: - paths: - - path: /orders - pathType: Prefix - backend: - service: - name: order-service - port: - number: 80 - - path: /users - pathType: Prefix - backend: - service: - name: user-service - port: - number: 80 -``` - -## HPA 弹性伸缩 - -```yaml -apiVersion: autoscaling/v2 -kind: HorizontalPodAutoscaler -metadata: - name: order-service-hpa -spec: - scaleTargetRef: - apiVersion: apps/v1 - kind: Deployment - name: order-service - minReplicas: 3 - maxReplicas: 20 - metrics: - - type: Resource - resource: - name: cpu - target: - type: Utilization - averageUtilization: 70 # CPU > 70% 时扩容 - - type: Resource - resource: - name: memory - target: - type: Utilization - averageUtilization: 80 - behavior: - scaleUp: - stabilizationWindowSeconds: 60 # 扩容稳定期 - policies: - - type: Pods - value: 2 - periodSeconds: 60 # 每分钟最多扩 2 个 - scaleDown: - stabilizationWindowSeconds: 300 # 缩容稳定期 5min(防抖动) -``` - -## K8s 运维 Checklist - -每次上线前过一遍这个清单: - -| # | 检查项 | 说明 | -|---|--------|------| -| 1 | **Probe 已配置** | liveness/readiness/startup 都设定了阈值 | -| 2 | **Resources Limits** | 防止单个 Pod OOMKill 拖垮整台机器 | -| 3 | **日志输出到 stdout/stderr** | 可被采集器解析为 JSON | -| 4 | **trace_id 透传** | 跨服务调用链 trace_id 不丢失 | -| 5 | **回滚预案** | `kubectl rollout undo deployment/order-service` 能用 | -| 6 | **告警已配置** | 关键指标异常时有人收到通知 | -| 7 | **镜像 Tag** | 不用 latest,用语义化版本或 commit SHA | - -## 关联笔记 - -- [[02-服务治理/04-服务发现]] — K8s Service 是服务端发现模式的代表 -- [[02-服务治理/08-流量治理]] — Istio VirtualService 在 K8s 上的高级路由 -- [[05-部署运维/04-SRE实践]] — SLO/Error Budget 在 K8s 中的落地 +- [[02-Kubernetes/01-核心概念与Deployment]] — Deployment 完整示例与 Probe 选择 +- [[hhs/MS/05-部署运维/01-容器化]] — Docker 镜像是 K8s Pod 的基础单元 +- [[hhs/MS/05-部署运维/03-CICD与GitOps]] — GitOps ArgoCD 操作 K8s manifest +- [[hhs/MS/05-部署运维/04-SRE实践]] — SLO/Error Budget 在 K8s 中的落地 +- [[hhs/MS/02-服务治理/04-服务发现]] — K8s Service 是服务端发现模式的代表 +- [[hhs/MS/02-服务治理/08-流量治理]] — Istio VirtualService 在 K8s 上的高级路由 diff --git a/hhs/MS/05-部署运维/02-Kubernetes/01-核心概念与Deployment.md b/hhs/MS/05-部署运维/02-Kubernetes/01-核心概念与Deployment.md new file mode 100644 index 0000000..0222c03 --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes/01-核心概念与Deployment.md @@ -0,0 +1,202 @@ +--- +tags: [kubernetes, pod, deployment, probe, container-orchestration] +create time: 2026-05-18 00:30 +--- + +# K8s 核心概念与 Deployment — 教程 + +## 概述 + +本文档带你从零理解 Kubernetes 的核心对象模型,并掌握 Deployment 这个最常用的工作负载控制器。更多进阶主题(资源配置、服务发现、调度)分散在后续章节中。总览和决策矩阵见 [[../02-Kubernetes]]。 + +## K8s 架构全景 + +```mermaid +graph TB + subgraph CONTROL["控制面 Control Plane"] + APISERVER["API Server
唯一入口"] + SCHEDULER["Scheduler
调度决策"] + CMGR["Controller Manager
状态协调"] + ETCD["etcd
分布式 KV 存储"] + + APISERVER --> SCHEDULER + APISERVER --> CMGR + APISERVER --> ETCD + CMGR --> ETCD + end + + subgraph WORKER["工作节点 Worker Node"] + N1["Node A
kubelet + kube-proxy"] + N2["Node B
kubelet + kube-proxy"] + end + + APISERVER -.->|监听变动| N1 + APISERVER -.->|监听变动| N2 + + style CONTROL fill:#e3f2fd + style ETCD fill:#c8e6c9 + style WORKER fill:#fff3e0 +``` + +> [!question] 为什么 K8s 需要这么多组件? +> +> 因为"声明式 API"的设计哲学——用户告诉 K8s **想要什么状态**(比如"我要 3 个订单服务实例"),控制面负责让实际状态持续逼近目标状态。任何偏离都会被自动修复,这也就是自愈能力的来源。 + +### 核心对象速查表 + +| K8s 对象 | 用途 | 类比 | +|---------|------|------| +| **Pod** | 最小部署单元,包含一个或多个容器 | 应用实例 | +| **Deployment** | 管理 Pod 的副本数和滚动更新 | 应用的"模板" | +| **Service** | 稳定的网络入口,负载均衡 | 内部 VIP → [[03-网络与服务发现]] | +| **ConfigMap** | 配置注入(明文) → [[02-配置管理]] | +| **Secret** | 敏感配置注入(base64)→ [[02-配置管理]] | +| **HPA** | 根据指标自动扩缩容 → [[04-扩缩容与有状态应用]] | +| **StatefulSet** | 有状态应用的有序管理 → [[04-扩缩容与有状态应用]] | +| **NetworkPolicy** | L3/L4 网络安全策略 → [[06-资源与安全管控]] | + +## Deployment 详解 + +### 完整示例 + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: order-service + labels: + app: order + version: v1.2.3 +spec: + replicas: 3 # 期望副本数 + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 # 最多超额 1 个 Pod + maxUnavailable: 0 # 滚动更新期间不允许不可用 + + selector: + matchLabels: + app: order + + template: # Pod 模板 + metadata: + labels: + app: order + version: v1.2.3 + spec: + containers: + - name: order-service + image: registry.example.com/order:v1.2.3 + ports: + - containerPort: 8080 + + # ========== 资源配置 ========== + resources: + requests: # 调度依据:保证至少有这些 + cpu: "250m" + memory: "256Mi" + limits: # 硬上限:超过则 OOMKill/CPU Throttle + cpu: "500m" + memory: "512Mi" + + # ========== 探针 ========== + livenessProbe: + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + failureThreshold: 3 # 连续失败 3 次才重启 + + readinessProbe: + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + failureThreshold: 3 + + startupProbe: + httpGet: + path: /healthz + port: 8080 + failureThreshold: 30 # 最长等待 300s (慢启动友好) + + # ========== 环境变量 & 挂载 ========== + envFrom: + - configMapRef: + name: order-service-config + - secretRef: + name: order-service-secrets + + lifecycle: + preStop: + exec: + command: ["sh", "-c", "sleep 15"] # 优雅退出,给 LB 摘流时间 +``` + +### Probe 选择指南 + +| 探针类型 | 触发条件 | 动作 | 适用场景 | +|---------|---------|------|---------| +| **Liveness** | `/healthz` 返回非 2xx | 重启容器 | 死锁、无法恢复的崩溃 | +| **Readiness** | `/ready` 返回非 2xx | 摘除 Service 流量 | 依赖未就绪、热加载中 | +| **Startup** | 首次成功前持续失败 | 不重启,只等待 | 大模型/JVM 冷启动 | + +> [!warning] 常见陷阱:Liveness 误杀导致 CrashLoopBackOff +> +> 如果 Liveness Probe 因为 DB 连接超时而返回 503,K8s 会认为容器挂了并反复重启它。正确做法:让 `/healthz` 做降级判断(DB 不可用时返回 200),用 `/ready` 来摘除流量。 + +### Deployment 更新策略 + +```yaml +strategy: + type: RollingUpdate # 逐步替换旧 Pod + rollingUpdate: + maxSurge: 1 # 新 Pod 数量可以超出期望值 1 + maxUnavailable: 0 # 更新时不允许有空缺 + + # 另一种策略:先全部新建再销毁旧的(零停机但需 2x 资源) + # type: Recreate # ❌ 全量替换,会有短暂不可用 +``` + +> [!tip] 如何安全地回滚 Deployment? +> +> ```bash +> kubectl rollout undo deployment/order-service -n production # 回到上一版本 +> kubectl rollout undo deployment/order-service -n production --to-revision=3 # 回退到指定版本 +> kubectl rollout status deployment/order-service -n production # 查看进度 +> kubectl rollout history deployment/order-service -n production # 历史版本对比 +> ``` +> +> K8s 自动保留每次 Deployment 变更后的 PodTemplateSpec,因此回滚是瞬时的,不需要手动备份 YAML。 + +### Pod 生命周期简述 + +```mermaid +stateDiagram-v2 + [*] --> Pending: Pod 被创建 + Pending --> ContainerCreating: 调度成功,拉取镜像 + ContainerCreating --> Running: 容器就绪 + Running --> Waiting: Probe 失败或手动暂停 + Waiting --> Running: 探针恢复 + Running --> Terminating: 删除/更新触发 + Terminating --> (*): 完全终止 + + state Waiting { + CrashLoopBackOff + ImagePullBackOff + } + + note right of CrashLoopBackOff: Liveness 连续失败\n或进程异常退出 + note right of ImagePullBackOff: 镜像不存在或仓库认证失败 +``` + +## 关联笔记 + +- [[../02-配置管理]] — ConfigMap / Secret 的配置注入 +- [[../03-网络与服务发现]] — Service / Ingress 网络路由 +- [[../04-扩缩容与有状态应用]] — HPA / StatefulSet +- [[../hhs/MS/05-部署运维/01-容器化]] — Docker 镜像是 Pod 的基础单元 +- [[../hhs/MS/05-部署运维/03-CICD与GitOps]] — GitOps ArgoCD 操作 K8s manifest diff --git a/hhs/MS/05-部署运维/02-Kubernetes/02-配置管理.md b/hhs/MS/05-部署运维/02-Kubernetes/02-配置管理.md new file mode 100644 index 0000000..3702915 --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes/02-配置管理.md @@ -0,0 +1,188 @@ +--- +tags: [kubernetes, configmap, secret, configuration-management] +create time: 2026-05-18 00:30 +--- + +# K8s 配置管理 — ConfigMap 与 Secret + +## 概述 + +K8s 提供两种原生的配置注入机制:**ConfigMap**(普通配置)和 **Secret**(敏感信息)。理解它们的区别、注入方式和生效时机,是编写可维护 K8s manifest 的基础。更多安全方案(外部密钥管理)参见 [[../03-CICD与GitOps/04-安全与发布策略]]。 + +## ConfigMap — 非敏感配置注入 + +### 基本用法 + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: order-service-config + namespace: production +data: + # ========== 环境变量风格 ========== + DATABASE_HOST: "mysql.production.svc.cluster.local" + DATABASE_PORT: "3306" + LOG_LEVEL: "info" + MAX_CONNECTIONS: "100" + + # ========== 文件风格 (挂载为 volume) ========== + application.yaml: | + server: + port: 8080 + spring: + datasource: + url: jdbc:mysql://${DATABASE_HOST}:${DATABASE_PORT}/orders + pool-size: ${MAX_CONNECTIONS} + logging: + level: + root: ${LOG_LEVEL} +``` + +### 两种注入方式 + +```yaml +spec: + containers: + - name: order-service + # 方式一:环境变量注入(简单直接) + env: + - name: DATABASE_HOST + valueFrom: + configMapKeyRef: + name: order-service-config + key: DATABASE_HOST + + # 方式二:volume 挂载配置文件(适合完整配置文件) + volumeMounts: + - name: config-volume + mountPath: /etc/app/config/application.yaml + subPath: application.yaml + volumes: + - name: config-volume + configMap: + name: order-service-config +``` + +> [!tip] 热更新 vs 重建 Pod +> +> - 通过 **环境变量** 注入的配置需要重建 Pod 才能生效 +> - 通过 **volume 挂载** 的 ConfigMap 在配置变更后,Pod 内文件会在约 1 分钟内自动更新(前提是应用本身支持配置热加载,如 Spring Cloud Context 的 `@RefreshScope`) + +## Secret — 敏感信息存储 + +### 基本用法 + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: order-service-secrets + namespace: production +type: Opaque +data: + # Base64 编码(注意:这不是加密!只是编码) + DB_PASSWORD: cGFzc3dvcmQxMjM= + API_KEY: bXktc2VjcmV0LWFwaS1rZXk= + +# 或者用 stringData(明文写入,K8s 自动编码) +--- +apiVersion: v1 +kind: Secret +metadata: + name: order-service-secrets-v2 +type: Opaque +stringData: + DB_PASSWORD: password123 # K8s 自动转为 base64 + API_KEY: my-secret-api-key +``` + +**使用 Secret 的方式:** + +```yaml +# 方式一:作为环境变量注入 +env: +- name: DB_PASSWORD + valueFrom: + secretKeyRef: + name: order-service-secrets + key: DB_PASSWORD + +# 方式二:挂载为卷中的文件(更安全,避免暴露在 ps 输出中) +volumeMounts: +- name: secrets-volume + readOnly: true + mountPath: /etc/secrets + defaultMode: 0400 +volumes: +- name: secrets-volume + secret: + secretName: order-service-secrets +``` + +> [!warning] Secret 的安全性边界 +> +> K8s Secret 默认以 **base64 编码** 存储在 etcd 中,任何人都可以解码。**Base64 ≠ 加密**。生产环境应启用以下至少一项: +> +> 1. **etcd 加密** — 开启 K8s EncryptionConfiguration,使 etcd 存储密文 +> 2. **外部密钥管理** — Sealed Secrets、External Secrets Operator 对接 AWS Secrets Manager / HashiCorp Vault +> 3. **RBAC 严格管控** — 限制谁可以读取 Secret 资源 + +### ConfigMap vs Secret 对比 + +| 维度 | ConfigMap | Secret | +|------|-----------|--------| +| **用途** | 普通配置(URL、端口等) | 敏感信息(密码、Token、证书) | +| **存储格式** | UTF-8 字符串 | Base64 编码 | +| **挂载路径** | `/etc/config/...` | `/etc/secrets/...` | +| **大小限制** | 1 MiB | 1 MiB | +| **安全建议** | 正常 RBAC | 必须启用 etcd 加密或外置密钥管理 | + +## 补充:动态配置重载实战 + +对于需要频繁变更的配置(如功能开关、灰度比例),可以通过 sidecar 模式实现自动重载: + +```yaml +spec: + containers: + - name: app + image: myapp:latest + volumeMounts: + - name: config-volume + mountPath: /etc/app/config + readOnly: true + - name: reload-sidecar # Watch 配置变更并发送 SIGUSR1 + image: busybox:latest + command: ["sh", "-c"] + args: + - | + LAST_MD5="" + while true; do + CURR_MD5=$(md5sum /etc/config/application.yaml | awk '{print $1}') + if [ "$CURR_MD5" != "$LAST_MD5" ]; then + kill -USR1 1 # 通知主进程重新加载配置 + LAST_MD5="$CURR_MD5" + fi + sleep 5 + done + volumeMounts: + - name: config-volume + mountPath: /etc/config + readOnly: true + volumes: + - name: config-volume + configMap: + name: order-service-config + - name: shared-config + emptyDir: {} +``` + +> [!note] sidecar 模式的权衡 +> +> 优点:无需重启 Pod 即可热更新配置;缺点:增加了资源消耗和维护复杂度。Spring Boot 项目建议使用 Actuator `/refresh` 端点替代手动信号方案。 + +## 关联笔记 + +- [[../01-核心概念与Deployment]] — ConfigMap 在 Deployment 中的标准引用方式 +- [[../03-CICD与GitOps/04-安全与发布策略]] — 企业级 Secret 管理方案选型 +- [[../hhs/MS/02-服务治理/07-配置管理]] — 微服务级别的配置中心方案(Nacos / Apollo / Consul) diff --git a/hhs/MS/05-部署运维/02-Kubernetes/03-网络与服务发现.md b/hhs/MS/05-部署运维/02-Kubernetes/03-网络与服务发现.md new file mode 100644 index 0000000..073dd5d --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes/03-网络与服务发现.md @@ -0,0 +1,144 @@ +--- +tags: [kubernetes, service, ingress, networking, service-discovery] +create time: 2026-05-18 00:30 +--- + +# K8s 网络与服务发现 — Service 与 Ingress + +## 概述 + +Kubernetes 的网络模型解决了容器 IP 频繁变动的问题。Service 提供**稳定的服务发现入口**,Ingress 提供**L7 HTTP 路由能力**。本文将梳理核心概念、典型模式和常见问题。 + +## Service 类型 + +| 类型 | 特点 | 使用场景 | +|------|------|---------| +| **ClusterIP** | 集群内 IP,外部不可访问 | 默认,内部服务间调用 | +| **NodePort** | 在每个 Node 上开端口 | 调试、临时访问 | +| **LoadBalancer** | 云厂商分配公网 IP | 对外暴露的服务 | +| **ExternalName** | CNAME 到外部域名 | 对接外部系统 | + +### ClusterIP Service — 服务发现的载体 + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: order-service +spec: + selector: + app: order # 匹配带此 label 的 Pod + ports: + - port: 80 # Service 端口(对外暴露) + targetPort: 8080 # 容器实际监听端口 + protocol: TCP + type: ClusterIP +``` + +调用方只需 `http://order-service:80`,K8s 通过 iptables/IPVS 自动实现负载均衡。 + +> [!question] Service 是怎么做到负载均衡的? +> +> 每个 Node 上的 kube-proxy 会监控 Service 的变动,自动生成 iptables 规则或 IPVS 虚拟服务器配置。当请求发往 ClusterIP 时,内核将其 DNAT 到后端 Pod IP 之一,负载均衡算法默认是 round-robin。K8s 1.14+ 推荐使用 IPVS 模式(`kube-proxy --proxy-mode=ipvs`),性能更高且支持更多算法。 + +## Ingress — HTTP/HTTPS 路由 + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: main-ingress + annotations: + nginx.ingress.kubernetes.io/rewrite-target: / +spec: + rules: + - host: api.example.com + http: + paths: + - path: /orders + pathType: Prefix + backend: + service: + name: order-service + port: + number: 80 + - path: /users + pathType: Prefix + backend: + service: + name: user-service + port: + number: 80 +``` + +> [!info] Ingress Controller 是什么? +> +> Ingress 只是一个 API 对象,真正的 HTTP 路由由 Ingress Controller(如 NGINX、Traefik、Envoy)执行。安装后会在集群中运行一个 LoadBalancer 类型的 Pod 集群,监听 Ingress 资源变化并生成对应配置。 + +### 常见 Ingress Annotation(NGINX 为例) + +| Annotation | 用途 | 示例值 | +|------------|------|--------| +| `rewrite-target` | URL 重写 | `/` | +| `ssl-redirect` | 强制 HTTPS | `"true"` | +| `rate-limit` | 限流 | `100` | +| `whitelist-source-range` | IP 白名单 | `10.0.0.0/8` | +| `client-max-body-size` | 上传文件大小限制 | `50m` | + +## Headless Service — 无头服务 + +Headless Service (`ClusterIP: None`) 不分配虚拟 IP,而是返回所有匹配 Pod 的真实 IP。这是 StatefulSet 的灵魂搭档: + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: mysql-headless +spec: + clusterIP: None # 关键:不分配 ClusterIP + selector: + app: mysql + ports: + - port: 3306 +``` + +通过 DNS 可以直接访问单个 Pod: +- `mysql-cluster-0.mysql-headless.production.svc.cluster.local:3306` +- `mysql-cluster-1.mysql-headless.production.svc.cluster.local:3306` + +## K8s 网络模型总结 + +```mermaid +flowchart LR + EXT["外部流量"] --> INGRESS["Ingress Controller"] + + INGRESS --> SVC_A["order-service:80"] + INGRESS --> SVC_B["user-service:80"] + + SVC_A --> Pod1["order-pod-1\n10.244.1.5:8080"] + SVC_A --> Pod2["order-pod-2\n10.244.2.3:8080"] + + SVC_B --> Pod3["user-pod-1\n10.244.1.8:3000"] + + Pod1 -->|DNS解析| COREDNS["CoreDNS
svc.cluster.local"] + + style EXT fill:#e3f2fd + style INGRESS fill:#fff3e0 + style SVC_A fill:#e8f5e9 + style SVC_B fill:#fce4ec +``` + +> [!tip] 调试技巧:Service 不通怎么办? +> +> 1. `kubectl get svc ` 确认 Service 存在且有 ClusterIP +> 2. `kubectl get endpoints ` 检查 Endpoint 列表是否为空 +> 3. Endpoint 为空 → 检查 selector 标签是否匹配 Pod +> 4. Endpoint 有值但 curl 不通 → 进入 Pod `curl :` 验证 +> 5. ClusterIP 通但外部不通 → 检查 Ingress / LoadBalancer 配置 + +## 关联笔记 + +- [[../01-核心概念与Deployment]] — Deployment 通过 selector 与 Service 关联 +- [[../02-配置管理]] — Service 的端点配置不直接涉及 ConfigMap/Secret +- [[../04-扩缩容与有状态应用]] — Headless Service + StatefulSet 配合 +- [[../hhs/MS/02-服务治理/04-服务发现]] — K8s Service 是服务端发现模式的代表 diff --git a/hhs/MS/05-部署运维/02-Kubernetes/04-扩缩容与有状态应用.md b/hhs/MS/05-部署运维/02-Kubernetes/04-扩缩容与有状态应用.md new file mode 100644 index 0000000..202db17 --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes/04-扩缩容与有状态应用.md @@ -0,0 +1,217 @@ +--- +tags: [kubernetes, hpa, autoscaling, statefulset, horizontal-pod-autoscaler] +create time: 2026-05-18 00:30 +--- + +# K8s 扩缩容与有状态应用 — HPA & StatefulSet + +## 概述 + +K8s 提供多种扩缩容机制。**HPA** 基于 CPU/Memory 或其他自定义指标实现 Pod 级别的弹性伸缩,适用于无状态服务。**StatefulSet** 则是数据库、消息队列等有状态组件的理想选择。两者各有适用场景。 + +## HPA 弹性伸缩 + +### 基础示例 + +```yaml +apiVersion: autoscaling/v2 +kind: HorizontalPodAutoscaler +metadata: + name: order-service-hpa +spec: + scaleTargetRef: + apiVersion: apps/v1 + kind: Deployment + name: order-service + minReplicas: 3 + maxReplicas: 20 + metrics: + - type: Resource + resource: + name: cpu + target: + type: Utilization + averageUtilization: 70 # CPU > 70% 时扩容 + - type: Resource + resource: + name: memory + target: + type: Utilization + averageUtilization: 80 + behavior: + scaleUp: + stabilizationWindowSeconds: 60 # 扩容稳定期 + policies: + - type: Pods + value: 2 + periodSeconds: 60 # 每分钟最多扩 2 个 + scaleDown: + stabilizationWindowSeconds: 300 # 缩容稳定期 5min(防抖动) +``` + +### 扩展指标:基于自定义 Metric 扩缩容 + +```yaml + metrics: + - type: Pods + pods: + metric: + name: http_requests_per_second + target: + type: AverageValue + averageValue: "100" # 每个 Pod 平均 100 QPS 时扩容 + - type: Object + object: + metric: + name: active_users + describedObject: + apiVersion: apps/v1 + kind: Deployment + name: web-app + target: + type: Value + value: "500" # 每 500 活跃用户触发一次扩容 +``` + +> [!info] HPA 的工作机制 +> +> HPA 控制器每 15 秒查询 Metrics Server(CPU/Memory)或自定义 Metrics Adapter(QPS 等),计算所需 Replica 数。公式:`ceil(当前指标值 / 目标指标值 × 当前副本数)`。为了防止抖动,scaleDown 的稳定窗口通常比 scaleUp 长很多。 + +### HPA 调优最佳实践 + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| `averageUtilization` (CPU) | 60~70% | 低于 60% 浪费资源,高于 80% 延迟风险大 | +| `scaleUp.stabilizationWindow` | 0~60s | 突发流量快速响应 | +| `scaleDown.stabilizationWindow` | 300~600s | 防止抖动导致的频繁扩缩 | +| `pods[].periodSeconds` | 60s | 限制扩容频率,避免雪崩 | + +## StatefulSet — 有状态应用 + +Deployment 适合无状态服务,而有状态服务(数据库、分布式协调器等)需要 **稳定的网络标识 + 有序的启停**。StatefulSet 就是为此而生的控制器。 + +### 完整示例 + +```yaml +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: mysql-cluster +spec: + serviceName: mysql # 绑定 Headless Service + replicas: 3 + podManagementPolicy: Parallel # 并行启动(生产建议 OrderedReady 逐步启动) + + updateStrategy: + type: RollingUpdate + rollingUpdate: + partition: 0 # 从最后一个 Pod 开始逐步滚动 + + selector: + matchLabels: + app: mysql + + template: + metadata: + labels: + app: mysql + spec: + containers: + - name: mysql + image: mysql:8.0 + ports: + - containerPort: 3306 + volumeMounts: + - name: data + mountPath: /var/lib/mysql + + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 20Gi +``` + +### StatefulSet 的核心特性 + +| 特性 | 说明 | +|------|------| +| **稳定名称** | Pod 名为 `{statefulset-name}-{ordinal}`,如 `mysql-cluster-0` | +| **有序部署** | 默认依次启动:0 → 1 → 2(设置 `Parallel` 可并行) | +| **有序删除** | 反向删除:2 → 1 → 0(保证主从切换安全) | +| **持久化存储** | `volumeClaimTemplates` 为每个 Pod 独立创建 PVC,Pod 重建后数据不丢失 | +| **Headless Service** | 需搭配 `ClusterIP: None` 的 Service,每个 Pod 有独立的 DNS 记录 | + +### StatefulSet vs Deployment 对比 + +| 维度 | Deployment | StatefulSet | +|------|-----------|-------------| +| **Pod 命名** | 随机后缀 | `{name}-{0,1,2,...}` | +| **存储** | 共享 PVC 或不持久 | 每个 Pod 独立 PVC | +| **启动顺序** | 并发 | 串行(默认) | +| **删除顺序** | 随机 | 逆序 | +| **DNS 稳定性** | 每次重建 IP 变 | 域名不变 | +| **适用场景** | Web/API 无状态服务 | DB、ZooKeeper、Kafka | + +> [!note] 为什么有状态应用不适合 Deployment? +> +> Deployment 会随机销毁和重建 Pod——这意味着 PVC 会被重新绑定到不同的 Pod,存储卷中的数据和节点 ID 完全错位。StatefulSet 保证同一个 Pod 始终使用同一片存储,这对数据库、Kafka、ZooKeeper 等组件至关重要。 + +### Headless Service — StatefulSet 的灵魂搭档 + +```yaml +apiVersion: v1 +kind: Service +metadata: + name: mysql-headless +spec: + clusterIP: None # 关键:不分配 ClusterIP + selector: + app: mysql + ports: + - port: 3306 +``` + +通过 DNS 可以直接访问单个 Pod: +- `mysql-cluster-0.mysql-headless.production.svc.cluster.local:3306` +- `mysql-cluster-1.mysql-headless.production.svc.cluster.local:3306` + +## 补充:Vertical Pod Autoscaler (VPA) + +HPA 只管 Pod 数量,不管单 Pod 的资源配置。如果你经常遇到 OOMKill 或 CPU Throttling,可以尝试 VPA: + +```yaml +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: order-service-vpa +spec: + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: order-service + updatePolicy: + updateMode: "Auto" # Auto / Initial / Off + resourcePolicy: + containerPolicy: + minAllowed: + cpu: 100m + memory: 128Mi + maxAllowed: + cpu: "2" + memory: 4Gi +``` + +> [!warning] VPA 的限制 +> +> VPA 在调整资源时会**驱逐并重建 Pod**。与 HPA 不同,VPA 不能无缝运行——建议在开发/测试环境中先用 `Initial` 模式观察推荐值,确定合理后再切到 `Auto`。生产环境通常结合 HPA(管数量)+ 手动调优 Resources(管质量)。 + +## 关联笔记 + +- [[../01-核心概念与Deployment]] — Deployment 基础,HPA 的目标控制器 +- [[../02-配置管理]] — StatefulSet 的数据库凭据通过 Secret 注入 +- [[../03-网络与服务发现]] — Headless Service 为 StatefulSet 提供 DNS 解析 +- [[../hhs/MS/05-部署运维/04-SRE实践]] — SLO/Error Budget 决定扩容阈值 diff --git a/hhs/MS/05-部署运维/02-Kubernetes/05-调度控制.md b/hhs/MS/05-部署运维/02-Kubernetes/05-调度控制.md new file mode 100644 index 0000000..276de13 --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes/05-调度控制.md @@ -0,0 +1,179 @@ +--- +tags: [kubernetes, scheduling, affinity, taints, tolerations, topology] +create time: 2026-05-18 00:30 +--- + +# K8s 调度控制 — Affinity、Taint & Topology + +## 概述 + +默认调度器会把 Pod 随便分配到任意可用 Node。想让 Pod 之间"互相躲开"或"必须在一起"?需要使用 K8s 的调度控制机制:**Affinity/Anti-Affinity**(节点间关系)、**Taint/Toleration**(节点白名单)、**TopologySpreadConstraints**(跨可用区分布)。 + +## Affinity / Anti-Affinity + +### Pod 反亲和性:分散到不同节点 + +```yaml +spec: + affinity: + # ========== 反亲和性:同一服务的 Pod 分散到不同节点 ========== + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 100 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app + operator: In + values: + - order-service + topologyKey: kubernetes.io/hostname + # ^^^ "尽量"把同 app 的 Pod 分配到不同机器 +``` + +> [!question] 为什么要把同一个服务的 Pod 分散? +> +> 如果所有副本都在同一台物理机上,这台机器宕机就会导致服务完全不可用。**故障域隔离**是高可用的基础——把 Pod 分散到不同节点 = 把鸡蛋放进不同篮子。 + +### 节点亲和性:只调度到特定标签的节点 + +```yaml +spec: + affinity: + # ========== 节点亲和性:只调度到特定标签的节点 ========== + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: node-role + operator: In + values: + - high-memory # 只调度到有 high-memory 标签的节点 +``` + +### 调度级别对比 + +| 级别 | 含义 | 推荐度 | +|---------|------|--------| +| `requiredDuring...` | 硬约束,不满足就不调度 | 高(适用于特殊硬件节点) | +| `preferredDuring...` | 软约束,尽力而为 | 高(适用于反亲和性拆分) | +| `ignoringDuringExecution` | 调度时检查,运行时忽略变化 | 标准做法 | + +## Taint & Toleration — 节点级别的"白名单" + +Taint 设在 Node 上,Toleration 设在 Pod 上: + +```bash +# 给节点加污点:只有带 db 容忍度的 Pod 才能调度上来 +kubectl taint nodes node1 dedicated=db:NoSchedule +``` + +```yaml +spec: + tolerations: + - key: "dedicated" + operator: "Equal" + value: "db" + effect: "NoSchedule" +``` + +> [!info] Taint effect 三种类型 +> +> | Effect | 含义 | 场景 | +> |--------|------|------| +> | `NoSchedule` | 不接受新 Pod | 专用节点 | +> | `PreferNoSchedule` | 尽量避免但不强制 | 优先但不是硬性 | +> | `NoExecute` | 驱逐已有的 Pod | 节点出问题时 | + +```mermaid +flowchart LR + Node["Node 打 Taint
dedicated=db:NoSchedule"] --> Check{Pod 有 Toleration?} + + Check -- 否 --> Reject["❌ 不允许调度"] + Check -- 是 --> Accept["✅ 允许调度"] + + style Reject fill:#ffebee + style Accept fill:#e8f5e9 +``` + +## TopologySpreadConstraints — K8s 1.19+ 推荐的跨可用区分布 + +比 anti-affinity 更优雅的实现方式,特别适合多云和混合云架构: + +```yaml +spec: + topologySpreadConstraints: + - maxSkew: 1 # 任何两个可用区最多差 1 个 Pod + topologyKey: topology.kubernetes.io/zone # 按可用区分组 + whenUnsatisfiable: DoNotSchedule # 不满足则不调度 + labelSelector: + matchLabels: + app: order-service +``` + +> [!tip] WhenUnsatisfiable 策略选择 +> +> - `DoNotSchedule`:宁可等也不破坏分布均衡(严格一致) +> - `ScheduleAnyway`:先调度上去,能分尽量分(可用性优先) +> +> **生产建议**:对外服务用 `ScheduleAnyway` + 反亲和性;内部有状态组件用 `DoNotSchedule`。 + +## 补充:NodeSelector 最简单的限制 + +```yaml +spec: + nodeSelector: + disktype: ssd # 只调度到标注了 disktype=ssd 的节点 +``` + +> [!note] NodeSelector vs Node Affinity +> +> NodeSelector 是最基本的调度方式——只能做相等匹配。如果需要 `In/NotIn/Exists` 等更复杂的逻辑,应使用 `nodeAffinity`。现代 K8s 项目中建议统一使用 `nodeAffinity` 以保持风格一致。 + +## 组合使用示例 + +```yaml +# 一个典型的 Production Web 部署 +spec: + template: + spec: + # ① 必须运行在 Linux x86_64 节点上 + affinity: + nodeAffinity: + requiredDuringSchedulingIgnoredDuringExecution: + nodeSelectorTerms: + - matchExpressions: + - key: kubernetes.io/os + operator: In + values: [linux] + - key: kubernetes.io/arch + operator: In + values: [amd64] + + # ② 尽量分散在不同可用区 + topologySpreadConstraints: + - maxSkew: 1 + topologyKey: topology.kubernetes.io/zone + whenUnsatisfiable: ScheduleAnyway + labelSelector: + matchLabels: + app: web-frontend + + # ③ 避免与后台批处理任务混在同一节点 + podAntiAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 200 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: workload-type + operator: In + values: [batch-job] + topologyKey: kubernetes.io/hostname +``` + +## 关联笔记 + +- [[../01-核心概念与Deployment]] — Deployment 中的 podTemplateSpec 位置 +- [[../06-资源与安全管控]] — ResourceQuota / NetworkPolicy 配合调度实现安全隔离 +- [[../hhs/MS/05-部署运维/02-Kubernetes]] — 调度失控时的常见排查技巧 diff --git a/hhs/MS/05-部署运维/02-Kubernetes/06-资源与安全管控.md b/hhs/MS/05-部署运维/02-Kubernetes/06-资源与安全管控.md new file mode 100644 index 0000000..619bebd --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes/06-资源与安全管控.md @@ -0,0 +1,208 @@ +--- +tags: [kubernetes, resource-quota, limit-range, network-policy, security] +create time: 2026-05-18 00:30 +--- + +# K8s 资源与安全管控 — Quota、LimitRange & NetworkPolicy + +## 概述 + +多人共享集群或多租户环境中,需要两道防线来保障稳定:**资源管控**防止单个团队耗尽集群,**网络策略**防止横向越权访问。这两者构成了 K8s 多环境的安全基线。 + +## ResourceQuota — 命名空间资源总量上限 + +```yaml +apiVersion: v1 +kind: ResourceQuota +metadata: + name: production-quota + namespace: production +spec: + hard: + requests.cpu: "16" # 所有 Pod 的 CPU request 总和不超过 16核 + requests.memory: 32Gi # 内存 request 不超过 32G + limits.cpu: "32" # 所有 Pod 的 CPU limit 不超过 32核 + limits.memory: 64Gi # 内存 limit 不超过 64G + pods: "50" # 最多 50 个 Pod + services: "20" # 最多 20 个 Service + persistentvolumeclaims: "10" # 最多 10 个 PVC +``` + +> [!warning] 配额打满怎么办? +> +> 当某个命名空间的配额被占满时,该 namespace 内的新创建请求会直接失败(Return Error)。解决方法:清理不再使用的资源,或申请管理员增加配额 (`kubectl edit quota -n `)。 + +## LimitRange — 单容器资源边界 + +```yaml +apiVersion: v1 +kind: LimitRange +metadata: + name: default-limits + namespace: production +spec: + limits: + # 默认 Limits/Requests(不指定的 Pod 自动套用) + - type: Container + default: + cpu: "500m" + memory: "512Mi" + defaultRequest: + cpu: "250m" + memory: "256Mi" + # 单个容器的边界 + max: + cpu: "2" + memory: "4Gi" + min: + cpu: "50m" + memory: "64Mi" +``` + +> [!info] Quota vs LimitRange — 别搞混 +> +> - **ResourceQuota** = namespace 总量天花板(整个房间能住多少人) +> - **LimitRange** = 单容器边界(每人占多大床位) +> +> 两者配合使用,既能防止个体乱配资源,又能防止群体耗尽配额。 + +## NetworkPolicy — 网络安全策略 + +默认情况下,K8s 集群内所有 Pod 可以自由互访(零信任缺失)。NetworkPolicy 提供 L3/L4 层的双向流量控制: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: order-service-policy + namespace: production +spec: + podSelector: + matchLabels: + app: order-service + policyTypes: + - Ingress + - Egress + + ingress: + # 允许来自 Ingress Controller 和 user-service 的流量 + - from: + - namespaceSelector: + matchLabels: + name: ingress-ns + - podSelector: + matchLabels: + app: user-service + ports: + - protocol: TCP + port: 8080 + + egress: + # 允许 outbound DB 连接和 DNS 查询 + - to: + - podSelector: + matchLabels: + app: mysql + ports: + - protocol: TCP + port: 3306 + - to: + - namespaceSelector: {} + ports: + - protocol: UDP + port: 53 # DNS(必备!否则 Pod 无法解析域名) +``` + +### NetworkPolicy 最佳实践 + +| 原则 | 说明 | +|------|------| +| **默认拒绝全部** | 先用 `podSelector: {}` + 空 ingress/egress 拦截一切 | +| **白名单精确匹配** | 每个 Policy 只放开一个方向的流量 | +| **DNS 永远放行** | 没有 egress DNS 规则,Service 发现全部失效 | +| **跨命名空间要显式声明** | namespaceSelector 为空 `{}` 表示允许所有命名空间 | + +### 支持 NetworkPolicy 的网络插件 + +> [!warning] NetworkPolicy 生效前提 +> +> NetworkPolicy **需要集群网络插件支持**。默认的 Kubenet / Flannel 不一定支持,推荐使用: +> - **Calico** — 功能最完整,支持 L7 策略(与 Istio 配合) +> - **Cilium** — 基于 eBPF,性能最优,原生支持 L7 过滤 +> - **Antrea** — VMware 出品,兼容性好 + +### 补充:Zero-Trust 默认策略模板 + +```yaml +# ========== 全局:默认拒绝所有入站 ========== +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all-ingress + namespace: production +spec: + podSelector: {} # 选择所有 Pod + policyTypes: + - Ingress + +--- +# ========== 全局:默认拒绝所有出站 ========== +apiVersion: networking.k8s.io/v1 +kind: NetworkPolicy +metadata: + name: default-deny-all-egress + namespace: production +spec: + podSelector: {} + policyTypes: + - Egress +``` + +> [!tip] 渐进式实施 NetworkPolicy +> +> 不要在已有集群上一夜之间切换所有策略——这会立刻打飞所有服务。正确的做法: +> +> 1. 先以 **audit** 模式运行 Calico/Cilium,观察现有流量 +> 2. 对每个服务编写 allowlist Policy,在 **测试环境** 验证 +> 3. 逐步推广到生产,从非关键服务开始 +> 4. 最终启用 deny-all 作为兜底 + +## 补充:SecurityContext — 容器级安全加固 + +```yaml +spec: + containers: + - name: order-service + securityContext: + runAsNonRoot: true # 禁止 root 运行 + readOnlyRootFilesystem: true # 根文件系统只读 + allowPrivilegeEscalation: false # 禁止提权 + capabilities: + drop: ["ALL"] # 丢弃所有内核能力 + add: ["NET_BIND_SERVICE"] # 仅保留绑定 <1024 端口需要的能力 + + # Pod 级别的 DNS 配置 + dnsPolicy: ClusterFirstWithHostNet # DNS 策略 + automountServiceAccountToken: false # 不挂载 SA Token(不需要 API 时) +``` + +> [!warning] readOnlyRootFilesystem 的坑 +> +> 某些应用(如 Python 的 pip 缓存、Java 的 temp 目录)会尝试写入 `/tmp` 或应用目录。解决方案: +> +> ```yaml +> volumeMounts: +> - name: tmp-volume +> mountPath: /tmp +> volumes: +> - name: tmp-volume +> emptyDir: {} +> ``` +> +> 通过 `emptyDir` 挂载临时卷,既保持了根文件系统只读,又给了应用必要的写权限。 + +## 关联笔记 + +- [[../02-配置管理]] — Secret 资源与 RBAC 共同构成安全基线 +- [[../05-调度控制]] — 调度控制 + 网络策略 = 纵深防御 +- [[../03-CICD与GitOps/04-安全与发布策略]] — GitOps 中的 Secret 安全 diff --git a/hhs/MS/05-部署运维/02-Kubernetes/07-运维排查.md b/hhs/MS/05-部署运维/02-Kubernetes/07-运维排查.md new file mode 100644 index 0000000..e2224c8 --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes/07-运维排查.md @@ -0,0 +1,143 @@ +--- +tags: [kubernetes, troubleshooting, diagnostics, ops-checklist] +create time: 2026-05-18 00:30 +--- + +# K8s 运维排查 — Checklist 与诊断速查 + +## 概述 + +每次上线前过一遍清单,遇到问题时有系统化的排查思路。本章整理了实战中最常用的诊断命令、高频问题对照表和有价值的调试技巧。更多 SRE 理念(错误预算、MTTR)参见 [[../04-SRE实践]]。 + +## 上线前 Checklist + +| # | 检查项 | 说明 | +|---|--------|------| +| 1 | **Probe 已配置** | liveness/readiness/startup 都设定了阈值 | +| 2 | **Resources Limits** | 防止单个 Pod OOMKill 拖垮整台机器 | +| 3 | **日志输出到 stdout/stderr** | 可被采集器解析为 JSON | +| 4 | **trace_id 透传** | 跨服务调用链 trace_id 不丢失 | +| 5 | **回滚预案** | `kubectl rollout undo deployment/order-service` 能用 | +| 6 | **告警已配置** | 关键指标异常时有人收到通知 | +| 7 | **镜像 Tag** | 不用 latest,用语义化版本或 commit SHA | + +## 常用诊断命令 + +```bash +# 1. 查看 Pod 状态(为什么起不来?) +kubectl get pods -n production + +# 2. 看单个 Pod 的详细事件 +kubectl describe pod -n production +# ↑ 重点看 Events 区域的 LastState / State / Reason + +# 3. 看容器日志(含重启前的上一次输出) +kubectl logs -n production --previous # 上次崩溃容器的日志 +kubectl logs -n production -c sidecar # 多容器指定 sidecar 名 + +# 4. 进入运行中的容器调试 +kubectl exec -it -n production -- /bin/sh + +# 5. 查看滚动更新进度 +kubectl rollout status deployment/order-service -n production + +# 6. 回滚到上一个版本 +kubectl rollout undo deployment/order-service -n production + +# 7. 查看资源占用 +kubectl top pods -n production # Pod CPU/Memory +kubectl top nodes # 节点级别 +``` + +## 高频问题对照表 + +| 症状 | 可能原因 | 排查步骤 | +|------|---------|---------| +| `ImagePullBackOff` | 镜像不存在、仓库认证失败、拼写错误 | `kubectl describe pod` 看 Normal Events;确认 registry 凭证 Secret | +| `ErrImagePull` | 镜像 tag 不存在 | 检查 CI 是否成功 push;`docker pull` 在本地复现 | +| `CrashLoopBackOff` | Liveness Probe 误杀、代码异常启动 | `kubectl logs --previous`;检查 `/healthz` 健康端点逻辑 | +| `Pending` (调度中) | 资源不足、Affinity/Toleration 不满足 | `kubectl describe pod` 看 Warning 事件;检查节点可用资源 | +| `OOMKilled` | memory limit 过小或内存泄漏 | 增大 limits;检查应用堆dump;JVM 需设 `-Xmx` | +| Service 不通 | Selector 标签不匹配、端口配置错 | `kubectl get ep ` 看 Endpoint 列表;curl ClusterIP 验证 | +| ConfigMap/Secret 未生效 | 重建了 Pod 但环境变量没更新 | 删除 Pod 让 Deployment 重建;ConfigMap volume 挂载会热更新 | +| Ingress 无响应 | Ingress Controller 未安装、Backend 配置错 | `kubectl get pods -n ingress-nginx`;检查 annotation 语法 | +| DNS 解析失败 | CoreDNS Pod 异常或 NetworkPolicy 限制 DNS egress | `nslookup kubernetes.default`;确保 DNS egress 端口 53 放行 | +| Pod 被频繁驱逐 | Node 资源不足触发 Eviction | `kubectl describe node ` 看 MemoryPressure/DiskPressure | + +## 调试技巧:优雅地抓包与断点 + +```bash +# ========== Debugging Sidecar 模式 ========== +# 给故障 Pod 附加一个临时调试容器,共享网络命名空间 +kubectl debug -it --image=nicolaka/netshoot --share-network + +# 进来了之后可以直接: +# curl, nslookup, tcpdump, ping, tshark — 全套网络诊断工具 + +# ========== 动态调整日志级别(无需重建 Pod)========== +# 通过 port-forward 访问 kube-apiserver 的 debug endpoint +kubectl port-forward svc/kube-apiserver 6443:443 -n default +``` + +> [!tip] 快速判断 K8s 问题的层级 +> +> ``` +> Pod 起不来 → 查 Images / Resources / Probes +> Pod 起来了但服务不通 → 查 Service Selector / Endpoints / Ingress +> 服务通但有报错 → 查 App Logs / Metrics / Traces +> 性能差 → 查 CPU Throttling / Disk IO / 连接池 +> ``` +> +> 按这个顺序一层层定位,避免在日志里大海捞针。 + +## 补充:kubectl 高阶用法 + +```bash +# 根据表达式筛选(比如只看处于 CrashLoop 的 Pod) +kubectl get pods --field-selector=status.phase==Failed -A + +# 批量执行命令(在每个 Pod 中同时运行) +kubectl exec deploy/api-server -- sh -c 'uptime; free -m' + +# 导出资源配置用于备份或审计 +kubectl get deployment -n production -o yaml > deploy-backup.yaml + +# 模拟变更效果(dry-run,不做实际修改) +kubectl apply -f new-deployment.yaml --dry-run=server -o yaml + +# 查看哪个节点承载了某个 Pod +kubectl get pods -o wide -n production | grep my-app + +# 持续监控 Pod 事件(实时流) +kubectl get events -n production --sort-by=.lastTimestamp -w +``` + +> [!info] 理解 kubectl verbosity 级别 +> +> `-v=6` 显示 HTTP 请求 headers;`-v=8` 额外返回响应 body;`-v=9` 逐行展开。调试 API 交互时通常 `-v=6` 就足够了,`-v=8` 以上会产生大量输出。 + +## 补充:集群层面的健康检查 + +```bash +# 查看所有控制面组件状态 +kubectl get componentstatuses # K8s 1.19+ 已废弃,改用: +kubectl get endpoints etcd -n kube-system + +# 检查 CoreDNS 健康(DNS 异常的起点) +kubectl get pods -n kube-system -l k8s-app=kube-dns +kubectl logs -n kube-system + +# 检查存储插件正常 +kubectl get storageclass + +# 查看当前活跃的资源配额使用情况 +kubectl describe quota -n production +``` + +## 关联笔记 + +- [[../01-核心概念与Deployment]] — Deployment 回滚操作 +- [[../03-网络与服务发现]] — Service / Ingress 的调试方法 +- [[../05-调度控制]] — Pending Pod 与 Affinity/Toleration 的关系 +- [[../hhs/MS/05-部署运维/04-SRE实践]] — MTTR 指标与故障恢复 +- [[../hhs/MS/04-可观测性]] — Prometheus + Grafana 可视化排查 diff --git a/hhs/MS/05-部署运维/03-CICD与GitOps.md b/hhs/MS/05-部署运维/03-CICD与GitOps.md index 7bedec1..f0a332d 100644 --- a/hhs/MS/05-部署运维/03-CICD与GitOps.md +++ b/hhs/MS/05-部署运维/03-CICD与GitOps.md @@ -1,176 +1,205 @@ --- -tags: [microservice, cicd, gitops, github-actions, argocd] -create time: 2026-05-05 +tags: [microservice, cicd, gitops, github-actions, argocd, kubernetes, helm] +create time: 2026-05-05 14:30 --- -# CI/CD 与 GitOps +# CI/CD 与 GitOps — 参考手册 ## 概述 -微服务需要**独立部署**。上百个服务的手工发布是不可想象的——必须用自动化流水线保证每次变更都能安全、快速地推送到生产环境。 +微服务架构下的自动化交付体系。**一次构建,多处部署**是核心哲学——镜像不随环境重新编译,只改 K8s ConfigMap。本文档是总入口:顶层概念、决策矩阵、速查表见本页;详细教程和实操指南在子文档中。 + +--- + +## 快速导航 + +| 主题 | 定位 | 文档 | +|------|------|------| +| Pipeline 搭建 | **教程**:从 0 到 1 写出一个能跑的流水线 | [[03-CICD与GitOps/01-CICD基础与实践]] | +| GitOps & ArgoCD | **教程**:Push → Pull 架构迁移、Application CRD | [[03-CICD与GitOps/02-GitOps与ArgoCD]] | +| Helm 模板 | **操作手册**:values.yaml、Go 模板、多环境覆盖 | [[03-CICD与GitOps/03-Helm模板管理]] | +| 安全 & 发布策略 | **决策指南**:Secret 选型、Canary vs Blue-Green | [[03-CICD与GitOps/04-安全与发布策略]] | + +--- + +## 架构全景 ```mermaid flowchart LR - CODE["代码提交"] --> TEST["测试 & 静态分析"] - TEST --> SCAN["安全扫描"] - SCAN --> BUILD["构建镜像"] - BUILD --> PUSH["推送仓库"] - PUSH --> STAGING["Staging 验证"] - STAGING -->|"人工审批"| PROD["Production 部署"] - - style CODE fill:#e3f2fd - style TEST fill:#fff3e0 - style SCAN fill:#fce4ec - style BUILD fill:#e8f5e9 - style PUSH fill:#f3e5f5 - style STAGING fill:#e0f7fa - style PROD fill:#c8e6c9 -``` + Dev["开发者 push"] --> CI["GitHub Actions\n(CI: 测试→构建→Push镜像)"] + CI -->|"新镜像"| REG["Container Registry"] -## CI/CD 设计原则 + Dev2["开发者 PR manifest"] --> MANIFEST["Git Manifest Repo"] -| 原则 | 说明 | -|------|------| -| **一次构建,多处部署** | 镜像不随环境重新编译,只改 K8s ConfigMap/环境变量 | -| **语义化版本** | 镜像 tag 用 `v1.2.3`,tag 即版本溯源 | -| **路径过滤** | 只对相关服务的代码变更触发构建 | -| **Commit SHA 作为镜像 tag** | 保证精确回滚 | -| **分阶段部署** | Staging → Production 的审批关卡不可跳过 | - -## GitHub Actions Pipeline 实战 - -```yaml -# .github/workflows/deploy.yml -name: Deploy order-service -on: - push: - branches: [main] - paths: - - "services/order/**" - -env: - REGISTRY: registry.example.com - IMAGE: order-service - -jobs: - # ========== Stage 1: Build & Test ========== - build-and-test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Run tests - run: make test - - - name: Security scan - uses: aquasecurity/trivy-action@master - with: - scan-type: 'fs' - severity: 'CRITICAL,HIGH' - - - name: Build Docker image - run: | - docker build -t ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \ - -t ${{ env.REGISTRY }}/${{ env.IMAGE }}:v${{ github.run_number }} \ - -f services/order/Dockerfile \ - services/order - - - name: Push to registry - run: | - echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin - docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} - docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:v${{ github.run_number }} - - # ========== Stage 2: Deploy to Staging ========== - deploy-staging: - needs: build-and-test - runs-on: ubuntu-latest - environment: staging - steps: - - name: Deploy to staging - run: | - kubectl set image deployment/order-service \ - order=${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \ - --namespace=staging - kubectl rollout status deployment/order-service \ - --namespace=staging --timeout=120s - - # ========== Stage 3: Deploy to Production ========== - deploy-production: - needs: deploy-staging - runs-on: ubuntu-latest - environment: production - steps: - - name: Canary release (10% → 50% → 100%) - run: | - kubectl patch canary order-service --type merge \ - -p '{"spec":{"weight":10}}' - - # ... 等待监控确认,逐步放大流量 - echo "Monitor metrics before proceeding..." -``` - -## GitOps 工作流 - -GitOps 的核心思想:**K8s 集群的状态 = Git 仓库中声明式配置的当前状态。** - -```mermaid -flowchart LR - Dev["开发者 PR"] -->|"修改 K8s manifest"| Git[(Git Repo)] - subgraph Cluster["K8s Cluster"] - Argo["ArgoCD / Flux"] -->|同步| K8sState["Pod/Service/ConfigMap"] + Argo["ArgoCD"] -->|"自动同步"| WORKLOADS["Pod / Service / ..."] end - - Git -.->|Webhook| Argo - - Argo -->|"检测到差异"| Diff{"状态一致?"} - Diff -- 否 --> Sync["自动同步到 K8s ✅"] - Diff -- 是 --> OK["已一致 ⏸️"] - - style Git fill:#e3f2fd + + MANIFEST -.->|Watch 变动| Argo + + style CI fill:#e8f5e9 style Argo fill:#fff3e0 - style K8sState fill:#e8f5e9 + style WORKLOADS fill:#e3f2fd ``` -### 与传统 CI/CD 的区别 +**核心分工**:**CI/CD 管"怎么建"**(构建、扫描、推送镜像),**GitOps 管"怎么维持"**(Git 声明 → 集群自动同步)。 -| 维度 | 传统 CI/CD | GitOps | -|------|-----------|--------| -| **部署驱动** | CI 服务器主动推送 | Git 仓库变动触发拉取 | -| **状态源** | CI pipeline 的历史记录 | Git commit history | -| **回滚方式** | 回到上一次的 pipeline | `git revert` + 自动同步 | -| **漂移检测** | 通常无 | 持续比对,自动修复不一致 | -| **代表工具** | Jenkins / GitLab CI / GitHub Actions | ArgoCD / Flux | +--- -### GitOps 的优势 +## 标签策略速查 -1. **审计完整** — 所有变更都在 Git 中可追溯 -2. **回滚简单** — `git revert` 就是回滚操作 -3. **自修复** — ArgoCD 持续监测并修复集群状态偏离 -4. **多人协作** — 通过 PR Review 流程管控配置变更 +| Tag | 示例 | 用途 | 安全性 | +|-----|------|------|--------| +| `latest` | `nginx:latest` | ❌ 开发调试 | ❌ 可被覆盖,不可回滚 | +| 语义版本 | `v1.2.3` | ✅ 人类阅读、快速参考 | ⚠️ 仍可被更新覆盖 | +| Commit SHA | `sha256:a1b2c3d` | ✅ 机器精确引用、精确回滚 | ✅ 全局唯一、不可变 | +| Run Number | `v123` | ✅ GitHub Actions 快速参考 | ⚠️ 单次 workflow 内唯一 | -## Helm — K8s 的包管理 +> **推荐**:同时打 `sha256:{commit}` + `v{run_number}`。生产回滚优先用 Commit SHA。 -当每个服务都有几十行 YAML 时,Helm 能大幅简化部署: +--- +## Pipeline 阶段对照 + +| 阶段 | 做什么 | 工具举例 | 详见 | +|------|--------|---------|------| +| 触发过滤 | 只对相关代码变更构建 | `paths`, `pull_request` | [[03-CICD与GitOps/01-CICD基础与实践]] | +| 测试 | 单元测试 + 集成测试 | Jest, pytest, go test | [[03-CICD与GitOps/01-CICD基础与实践]] | +| 安全扫描 | 镜像/依赖漏洞检测 | Trivy, Snyk | [[03-CICD与GitOps/04-安全与发布策略]] | +| 构建+推送 | Docker image 构建并推仓库 | docker build/push | [[03-CICD与GitOps/01-CICD基础与实践]] | +| Staging 验证 | 预发布环境冒烟测试 | kubectl set image | [[03-CICD与GitOps/01-CICD基础与实践]] | +| Production | 灰度放量,人工审批关卡 | Canary, Blue-Green | [[03-CICD与GitOps/04-安全与发布策略]] | + +--- + +## 部署驱动模式对比 + +| 维度 | 传统 CI/CD (Push) | GitOps (Pull) | +|------|------------------|---------------| +| 部署驱动 | CI 服务器主动推送 | Git 仓库变动触发拉取 | +| 状态源 | CI pipeline 历史 | Git commit history | +| 漂移检测 | 通常无 | 持续比对,自动修复 | +| 安全边界 | CI 需直连 K8s | ArgoCD 集群内运行 | +| 回滚方式 | 回到上一次的 pipeline | `git revert` + 自动同步 | +| 代表工具 | Jenkins, GitHub Actions | ArgoCD, Flux | + +> **选型建议**:团队规模 < 20 人时 Push 模式足够。≥ 50 人或多环境场景建议迁移到 GitOps。 + +--- + +## Secret 管理方案对比 + +| 方案 | 适用场景 | 优点 | 缺点 | +|------|---------|------|------| +| K8s 原生 Secret | 小规模内部团队 | 零成本,开箱即用 | etcd 明文存储 | +| ESO (External Secrets) | 已有 Vault / AWS SSM | Git 中无密文 | 需维护额外组件 | +| SOPS + sealed-secrets | ArgoCD 用户 | 加密文件可提交到 Git | 需管理 PKI | +| 云厂商 Secret Manager | 深度绑定单一云平台 | 审计完善 | 平台锁定 | + +> **渐进路径**:K8s Secret → 接入云厂商 Secret Manager → 引入 ESO 实现多云解耦。 + +--- + +## 发布策略对比 + +| 维度 | Rolling Update | Canary | Blue-Green | Experiment | +|------|---------------|--------|------------|------------| +| 停机时间 | 可能有抖动 | 零 | 零 | 零 | +| 复杂度 | 低 | 中 | 中高 | 高 | +| 资源消耗 | 1x | 1.5x | 2x | 2x+ | +| 回滚速度 | 慢(完整重建) | 快(切流量) | 秒级 | 秒级 | +| 典型场景 | 内部服务 | 用户-facing | 重要版本 | A/B 测试 | + +--- + +## 编排工具选型 + +| 服务规模 | 推荐方案 | 理由 | +|----------|---------|------| +| < 10 个服务 | Kustomize / 裸 YAML | 复杂度高于收益 | +| 10~50 个服务 | Helm | 模板复用价值明显 | +| > 50 个服务 | Helm + Kustomize overlays | Helm 管模板,Kustomize 管环境差异 | + +--- + +## 关键决策流程图 + +### Secret 选型决策 + +```mermaid +flowchart TD + START["开始选型"] --> SIZE["团队 < 20人?"] + SIZE -- 是 --> K8SSECRET["✅ K8s 原生 Secret"] + SIZE -- 否 --> CLOUD["深度绑定云厂商?"] + CLOUD -- 是 --> SECRETMGR["✅ Cloud Secret Manager"] + CLOUD -- 否 --> GITOPS["使用 ArgoCD?"] + GITOPS -- 是 --> SOPS["✅ SOPS + sealed-secrets"] + GITOPS -- 否 --> ESO["✅ External Secrets Operator"] + + style K8SSECRET fill:#e8f5e9 + style SECRETMGR fill:#e3f2fd + style SOPS fill:#fff3e0 + style ESO fill:#f3e5f5 +``` + +### 渐进式采纳路径 + +```mermaid +flowchart LR + STEP1[("1. 自动化构建+Push")] --> STEP2[("2. Staging + 审批")] --> STEP3[("3. GitOps, Git 即真相源")] + + style STEP1 fill:#e8f5e9 + style STEP2 fill:#fff3e0 + style STEP3 fill:#e3f2fd +``` + +--- + +## 附录:常用命令速查 + +### GitHub Actions ```bash -# 创建 Chart 模板 -helm create order-service +# 查看 workflow 运行记录 +gh run list --workflow deploy.yml -# 使用 values.yaml 参数化部署 -helm upgrade --install order-service ./charts/order-service \ - --set image.tag=v1.2.3 \ - --set replicas=3 \ - --namespace=production +# 手动触发 workflow +gh workflow run deploy.yml --ref main ``` -> [!tip] 为什么需要 Helm? -> -> 没有 Helm 时,每个服务都要手动维护 Deployment、Service、Ingress、ConfigMap 等几十个 YAML 文件。Helm 允许你把通用模板抽出来,只在 values.yaml 里改差异化配置。 +### kubectl +```bash +# 滚动更新指定镜像 +kubectl set image deployment/order-service \ + order=registry.example.com/order-service:v1.2.3 -n production + +# 查看 rollout 状态 +kubectl rollout status deployment/order-service -n production --timeout=120s + +# 回滚到上一版本 +kubectl rollout undo deployment/order-service -n production +``` + +### ArgoCD CLI +```bash +argocd app sync order-service # 手动同步 +argocd app diff order-service # Git vs 实际差异 +argocd app history order-service # 同步历史 +argocd app rollback order-service # 回滚 +``` + +### Helm +```bash +helm upgrade --install order-service ./charts/order-service -n production +helm upgrade --install order-service ./charts/order-service -n staging --set image.tag=v1.2.4-dev +helm template order-service ./charts/order-service # 仅渲染,不安装 +helm history order-service -n production # Release 历史 +``` + +--- ## 关联笔记 -- [[05-部署运维/01-容器化]] — Docker 镜像构建是 CI/CD 的第一步 -- [[05-部署运维/02-Kubernetes]] — K8s 是部署的目标平台 -- [[05-部署运维/04-SRE实践]] — 错误预算影响发布策略 +- [[01-容器化]] — Docker 镜像构建是 CI/CD 的第一步 +- [[02-Kubernetes]] — K8s 是部署的目标平台 +- [[04-SRE实践]] — 错误预算影响发布策略 +- [[05-监控告警]] — Prometheus + Alertmanager 为 Canary 提供指标支撑 diff --git a/hhs/MS/05-部署运维/03-CICD与GitOps/01-CICD基础与实践.md b/hhs/MS/05-部署运维/03-CICD与GitOps/01-CICD基础与实践.md new file mode 100644 index 0000000..2cb1a7e --- /dev/null +++ b/hhs/MS/05-部署运维/03-CICD与GitOps/01-CICD基础与实践.md @@ -0,0 +1,299 @@ +--- +tags: [cicd, github-actions, docker, kubernetes, pipeline] +create time: 2026-05-17 10:00 +--- + +# CI/CD 基础与实践 + +## 概述 + +本文档带你从零搭建微服务的自动化发布流水线。内容覆盖:从代码提交到镜像构建、安全扫描、Staging 验证、灰度上线的完整流程,以 GitHub Actions 为例进行实战讲解。更多决策分析(如工具选型)参见 [[../03-CICD与GitOps]]。 + +## 为什么需要 CI/CD? + +> [!question] 想象一下 +> +> 你有 50 个微服务,每次发布都需要手动 SSH 到服务器、停旧启新、检查日志——如果出错了还得回滚。**人工操作**在这个规模下就是最大的风险源。 +> +> CI/CD 的核心目标:**消除发布日的手工操作**,让任何一次 commit 都可以被安全地部署。 + +```mermaid +flowchart LR + CODE["代码提交"] --> TEST["测试 & 静态分析"] + TEST --> SCAN["安全扫描"] + SCAN --> BUILD["构建镜像"] + BUILD --> PUSH["推送仓库"] + PUSH --> STAGING["Staging 验证"] + STAGING -->|"人工审批"| PROD["Production 部署"] + + style CODE fill:#e3f2fd + style TEST fill:#fff3e0 + style SCAN fill:#fce4ec + style BUILD fill:#e8f5e9 + style PUSH fill:#f3e5f5 + style STAGING fill:#e0f7fa + style PROD fill:#c8e6c9 +``` + +## 核心设计原则 + +### 一次构建,多处部署 + +镜像不随环境重新编译,只改 K8s ConfigMap 或环境变量。Staging 和 Production 用的是**同一个镜像**。 + +```mermaid +flowchart LR + DEV["开发机"] --> BUILD["构建一次"] + BUILD --> REGISTRY["Docker Registry"] + REGISTRY --> STAGING["Staging 环境"] + REGISTRY --> PROD["Production 环境"] + + STAGING -. "同一镜像" .- PROD + + style DEV fill:#e3f2fd + style BUILD fill:#e8f5e9 + style REGISTRY fill:#f3e5f5 + style STAGING fill:#fff3e0 + style PROD fill:#c8e6c9 +``` + +### 语义化版本 + Commit SHA 双标签 + +| Tag 类型 | 示例 | 用途 | +|----------|------|------| +| **Commit SHA** | `sha256:a1b2c3d` | 机器精确引用,保证可回滚 | +| **运行序号** | `v123` | 人类快速参考,方便手动回滚 | + +> [!tip] 永远不要用 `latest` 做生产环境的镜像标签。`latest` 可被覆盖,回滚时你不知道回滚到哪一刻的镜像。 + +### 路径过滤 + +只对相关服务的代码变更触发构建。`order-service` 改了代码,不应该触发 `payment-service` 的 pipeline。 + +### 流水线即代码 + +Pipeline 配置写在 Git 中(`.github/workflows/`),和源码一起 Review。没人知道它长什么样,就是最好的理由。 + +## Pipeline 实战:单服务完整流水线 + +以下是一个微服务(以 `order-service` 为例)的 CI/CD 流水线。核心思路:**代码推送到 main 分支 → 测试 → 构建镜像 → 推到 Staging → 人工审批 → 灰度上线**。 + +```yaml +# .github/workflows/deploy.yml +name: Deploy order-service + +on: + push: + branches: [main] + paths: + - "services/order/**" # 只在 order 服务有变更时触发 + +env: + REGISTRY: registry.example.com + IMAGE: order-service + +jobs: + # ========== Stage 1: Build & Test ========== + build-and-test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Run tests + run: make test + + - name: Security scan (filesystem) + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + severity: 'CRITICAL,HIGH' + + - name: Build Docker image + run: | + docker build \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE }}:v${{ github.run_number }} \ + -f services/order/Dockerfile \ + services/order + + - name: Push to registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login -u ${{ secrets.REGISTRY_USER }} --password-stdin + docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} + docker push ${{ env.REGISTRY }}/${{ env.IMAGE }}:v${{ github.run_number }} + + # ========== Stage 2: Deploy to Staging ========== + deploy-staging: + needs: build-and-test + runs-on: ubuntu-latest + environment: staging + steps: + - name: Deploy to staging + run: | + kubectl set image deployment/order-service \ + order=${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \ + --namespace=staging + kubectl rollout status deployment/order-service \ + --namespace=staging --timeout=120s + + - name: Smoke test + run: | + # 等待 Pod Ready 后发送探测请求 + sleep 10 + ENDPOINT=$(kubectl get ingress order-service \ + -o jsonpath='{.spec.rules[0].host}' -n staging) + curl -f http://$ENDPOINT/health || exit 1 + + # ========== Stage 3: Deploy to Production ========== + deploy-production: + needs: deploy-staging + runs-on: ubuntu-latest + environment: production # 需人工 Approval Gate + steps: + - name: Canary release (10% → 50% → 100%) + run: | + kubectl patch canary order-service --type merge \ + -p '{"spec":{"weight":10}}' + # 等待 10 分钟,检查 error rate / latency + echo "Monitor metrics before proceeding..." + + - name: Promote to 50% + if: success() + run: | + kubectl patch canary order-service --type merge \ + -p '{"spec":{"weight":50}}' + + - name: Full promotion + if: success() + run: | + kubectl patch canary order-service --type merge \ + -p '{"spec":{"weight":100}}' +``` + +> [!info] Secret 管理原则 +> +> 所有敏感信息通过 **GitHub Actions Secrets** 存储,按环境分离: +> +> | Secret 名称 | 用途 | 存放位置 | +> |------------|------|---------| +> | `REGISTRY_USER` / `REGISTRY_PASSWORD` | 镜像仓库认证 | Repository Settings → Secrets | +> | `KUBECONFIG_STAGING` | K8s 集群配置 | Environment: staging | +> | `KUBECONFIG_PRODUCTION` | K8s 集群配置 | Environment: production | +> +> > [!tip] 不要在 YAML 中明文写密码,哪怕加了 `${{ secrets.XXX }}` 的 workflow 在 fork 的 PR 中仍然有泄露风险。对 fork PR 使用 `environment` 保护可以阻止 secrets 暴露。 + +## Pipeline 逐阶段解读 + +| 阶段 | 做什么 | 为什么重要 | +|------|--------|-----------| +| **路径过滤** | `paths` 指定只对特定目录变更触发 | 避免每次 PR 都重建所有服务的镜像 | +| **Secrets 管理** | GitHub Secrets 按环境分离存储 | 防止密钥泄露到 fork PR 或日志中 | +| **安全扫描** | Trivy 在构建前后扫描漏洞 | 把安全问题挡在镜像层 | +| **双 Tag 策略** | 同时打 Commit SHA + run number | SHA 用于精确操作,run number 用于快速参考 | +| **Environment 保护** | `environment: production` 配置审批人 | GitHub 级别的 gates | +| **冒烟测试** | 部署后发送健康探测请求 | 确认 Pod 真正 Ready,而非只等 rollout 完成 | +| **Canary 发布** | 10% → 50% → 100% 逐步放量 | 有 Bug 只影响 10% 用户 | + +## 回滚与 Rollout + +> [!question] 上线后发现 Bug,怎么最快恢复? +> +> 答案不是「快速修好再发一次」——而是**先回滚,后修复**。MTTR(平均恢复时间)比根因分析优先级更高。 + +### K8s Rollout 模式对比 + +```mermaid +flowchart LR + RU["RollingUpdate\n滚动更新"] --> P1["逐步替换旧 Pod"] + RU --> P2["期间服务不中断"] + RU --> P3["支持 ProgressDeadline"] + + RC["Recreate\n全部重建"] --> R1["先杀所有旧 Pod"] + RC --> R2["再启新 Pod"] + RC --> R3["有短暂不可用窗口"] + + style RU fill:#e8f5e9 + style RC fill:#fff3e0 +``` + +| 特性 | RollingUpdate | Recreate | +|------|--------------|----------| +| **可用性** | 零停机 | 短暂中断 | +| **资源峰值** | Old + New 并存 | 只有一个版本运行 | +| **适用场景** | 绝大多数微服务 | 独占存储卷、单写数据库 | +| **回滚速度** | `kubectl rollout undo` 秒级恢复 | 重新 deploy 上一个版本 | + +### 回滚命令速查 + +```bash +# 查看当前 rollout 状态 +kubectl rollout status deployment/order-service -n production + +# 回滚到上一个版本 +kubectl rollout undo deployment/order-service -n production + +# 查看历史版本列表 +kubectl rollout history deployment/order-service -n production + +# 回滚到指定 revision +kubectl rollout undo deployment/order-service -n production --to-revision=3 + +# 暂停/恢复 rollout(适合批量变更) +kubectl rollout pause deployment/order-service -n production +# ... 应用多个变更 ... +kubectl rollout resume deployment/order-service -n production +``` + +> [!tip] 如果使用了 Canary / Istio 等流量治理工具,回滚步骤变为: +> 1. **先把流量切回稳定版本**(改 weight = 0)— 秒级止血 +> 2. **再删除问题部署** — 释放资源 +> 3. **最后排查和修复** +> +> 流量切换比 Pod 重建更快,这是灰度发布最大的安全优势。 + +## 生产级增强 + +上面的示例做了精简,真实环境中通常还会加入: + +| 增强项 | 说明 | 工具 / 做法 | +|--------|------|------------| +| **Dependency Lock** | `go mod tidy` 后提交 `go.sum`,确保每次依赖一致 | `go.sum` / `package-lock.json` | +| **Trivy Image Scan** | 镜像推送后再扫描一次镜像内漏洞 | `trivy image ` | +| **OpenTelemetry Trace ID** | 注入 trace ID,追踪本次构建产生的请求 | 环境变量注入 `OTEL_SERVICE_VERSION` | +| **Slack 通知** | 每阶段完成后通过 Webhook 通知团队 | GitHub Actions `slack/notification` | +| **代码覆盖率门控** | 低于阈值直接失败(coverage < 80% = fail) | `gocover` / `codecov` | +| **SBOM 生成** | 用 `syft` 生成软件物料清单,满足合规要求 | `syft dir:. -o cyclonedx-json > sbom.json` | +| **Docker BuildKit** | 利用缓存层加速重建 | `DOCKER_BUILDKIT=1 docker build` | +| **并发测试** | 单元测试和 lint 并行执行缩短流水线时间 | GitHub Actions `strategy.matrix` | + +```bash +# Docker BuildKit 加速构建 +DOCKER_BUILDKIT=1 docker build \ + --cache-from ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \ + -t ${{ env.REGISTRY }}/${{ env.IMAGE }}:${{ github.sha }} \ + services/order + +# SBOM 生成 +syft dir:. -o cyclonedx-json > sbom.json +``` + +## 常见陷阱 + +> [!warning] 这些坑踩过一次就记住了 + +1. **不要在 pipeline 里用 `latest` tag** — 不同环境编译行为不一致 +2. **不要让 Staging 和 Production 各自 `docker build`** — 这是最常见的一致性 bug 来源 +3. **不要跳过 Staging 直连 Production** — 哪怕你的测试写得再好,也需要一个集成验证环节 +4. **不要硬编码 Secret 到 YAML 或代码里** — 参考 [[04-安全与发布策略]] +5. **不要忽略 `kubectl rollout status`** — 不检查 rollout 状态等于盲发 +6. **不要把日志级别开到 DEBUG 上生产** — 不仅浪费存储,还会暴露敏感数据 +7. **不要用 cron 做主触发器替代 push** — 定时构建无法响应具体 commit,丢失审计链 +8. **不要忘记配置 `ProgressDeadlineSeconds`** — 一个卡在 Pending 状态的 rollout 不会被自动中止 + +## 关联笔记 + +- [[02-GitOps与ArgoCD]] — GitOps 模式下的部署工作流 +- [[03-Helm模板管理]] — Helm Chart 编写与管理 +- [[04-安全与发布策略]] — 安全管理和发布策略决策 +- [[../03-CICD与GitOps]] — 参考手册与决策指南 diff --git a/hhs/MS/05-部署运维/03-CICD与GitOps/02-GitOps与ArgoCD.md b/hhs/MS/05-部署运维/03-CICD与GitOps/02-GitOps与ArgoCD.md new file mode 100644 index 0000000..4d8dec1 --- /dev/null +++ b/hhs/MS/05-部署运维/03-CICD与GitOps/02-GitOps与ArgoCD.md @@ -0,0 +1,449 @@ +--- +tags: [gitops, argocd, flux, kubernetes, deployment, cicd, app-of-apps] +create time: 2026-05-17 10:00 +update time: 2026-05-17 10:30 +--- + +# GitOps 与 ArgoCD + +## 概述 + +本文档深入讲解 GitOps 工作流和 ArgoCD 的使用。内容覆盖:从"传统 CI/CD 的痛点"到 "Git 即真相源",ArgoCD 架构解析、Application CRD 编写、App of Apps 模式、Helm/Kustomize 混合使用、同步钩子与安全实践。工具选型对比参见 [[../03-CICD与GitOps]]。 + +## 核心思想:让集群自己"拉取"状态 + +> [!question] 谁来"推动"变更到集群? +> +> 传统模式下,Jenkins 拿着 kubeconfig SSH 到 K8s 执行 `kubectl apply`。但问题来了:如果有人在集群里直接改了配置(比如手动 `kubectl edit deployment`),Jenkins 并不知道——这就是**配置漂移**。 +> +> GitOps 的答案:**让 K8s 集群自己"拉取"自己的状态。** Git 仓库是唯一真相源,任何变更都通过 PR 进入 Git,工具自动同步到集群。 + +```mermaid +flowchart LR + Dev["开发者 PR"] -->|"修改 K8s manifest"| Git[(Git Repo)] + + subgraph Cluster["K8s Cluster"] + Argo["ArgoCD / Flux"] -->|同步| K8sState["Pod/Service/ConfigMap"] + end + + Git -.->|Webhook Polling| Argo + + Argo -->|"检测到差异"| Diff{"状态一致?"} + Diff -- 否 --> Sync["自动同步到 K8s ✅"] + Diff -- 是 --> OK["已一致 ⏸️"] + + style Git fill:#e3f2fd + style Argo fill:#fff3e0 + style K8sState fill:#e8f5e9 +``` + +## Push vs Pull:本质区别 + +| | 传统 CI/CD (Push) | GitOps (Pull) | +|---|---|---| +| **部署驱动** | CI 服务器主动推送 | Git 仓库变动触发拉取 | +| **安全边界** | CI 需要直连 K8s(暴露 kubeconfig) | ArgoCD 在集群内运行,无需外部访问 | +| **漂移检测** | 通常无 | 持续比对,自动修复 | +| **回滚方式** | 回到上一次 pipeline | `git revert` + 自动同步 | + +> [!tip] Push vs Pull 的安全含义 +> +> - **Push**:CI 服务器持有集群凭据,主动向 K8s 发请求。凭据泄露 = 集群沦陷。 +> - **Pull**:ArgoCD 在集群内部以 Pod 运行,只需读取 Git 的只读权限。即使 Git 凭据泄露,攻击者也无法写入集群——他们无法改变 Git 中的 YAML。 + +## ArgoCD 架构深度解析 + +ArgoCD 是 CNCF 级别的 GitOps 持续交付工具。 + +```mermaid +flowchart TB + Git[(Git Repository)] + + subgraph ArgoCDServer["ArgoCD Server (集群外)"] + UI["Web UI"] + API["API"] + end + + subgraph ArgoCDController["ArgoCD Controller (集群内)"] + SyncLoop["同步循环\n(每 3 分钟)"] + Reconcile["Reconcile: Git Manifest vs K8s 实际状态"] + end + + Git -->|只读 access_token| SyncLoop + SyncLoop --> Reconcile + Reconcile -->|"发现不一致"| Apply["kubectl apply"] + Apply --> K8s["K8s Cluster"] + K8s -->|"读取实际状态"| SyncLoop + + style Git fill:#e3f2fd + style ArgoCDServer fill:#fff3e0 + style ArgoCDController fill:#e8f5e9 +``` + +### 关键概念 + +| 术语 | 说明 | +|------|------| +| **Application** | ArgoCD 管理的核心资源,定义"从哪个 Git 路径同步到哪个命名空间" | +| **Sync Policy** | 决定是自动同步还是手动触发;以及同步策略(Prune resources, Self-heal) | +| **Health Status** | ArgoCD 对资源的健康检查(Deployment 是否有可用副本、Pod 是否 Running 等) | +| **Drift Detection** | 对比 Git 中声明的配置与实际 K8s 状态的差异 | +| **App of Apps** | 用 Application 来管理 Application,实现分层编排 | + +## Application 配置实战 + +### 典型 Application + +```yaml +# apps/order-service.yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: order-service + namespace: argocd +spec: + project: default + source: + repoURL: https://github.com/team/k8s-manifests.git + targetRevision: main + path: overlays/production/order-service + destination: + server: https://kubernetes.default.svc + namespace: production + syncPolicy: + automated: + prune: true # 删除 Git 中不存在的资源 + selfHeal: true # 自动修复被手动改动的资源 + syncOptions: + - CreateNamespace=true # 目标命名空间不存在时自动创建 +``` + +**字段速查**: + +- `prune: true` — Git 里删了的东西,集群上也删掉 +- `selfHeal: true` — 有人手动改了 Deployment?下次同步循环自动改回来 +- `CreateNamespace=true` — 不需要提前手动创建命名空间 + +### App of Apps:多层级应用编排 + +当有几十上百个服务时,用一个 Application 管理一个服务太繁琐了。ArgoCD 支持 **"应用的 Application"** 模式: + +```yaml +# root-application.yaml —— 根级别 Application +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: cluster-services +spec: + sources: + - repoURL: https://github.com/team/k8s-manifests.git + targetRevision: main + path: apps/order-service + - repoURL: https://github.com/team/k8s-manifests.git + targetRevision: main + path: apps/payment-service + - repoURL: https://github.com/team/k8s-manifests.git + targetRevision: main + path: apps/user-service +``` + +这种模式下,开发者只需要往 `apps/` 目录下新增文件夹并提交 PR,根 Application 就会自动把新服务拉到集群。 + +> [!tip] App of Apps 的最佳实践 +> +> - 按团队或环境划分层级:`root-app → team-a-services / team-b-services` +> - 每个子 Application 可以有自己的 `syncPolicy` 和 `namespace` +> - 不要超过 3 层嵌套,调试会变得困难 + +## 进阶:中间件集成 + +### Helm + Kustomize 混合使用 + +ArgoCD **同时支持** Helm 和 Kustomize 作为 source plugin。实际工程中,最常见的组合是: + +> CI pipeline 用 Helm build chart → ArgoCD 通过 Helm source 读取并部署到集群。 + +```yaml +# apps/order-service.yaml —— 使用 Helm Source +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: order-service +spec: + project: default + source: + repoURL: https://github.com/team/k8s-manifests.git + targetRevision: main + path: charts/order-service # Helm Chart 目录 + helm: + valueFiles: + - values.yaml # 默认值文件 + - values-production.yaml # 生产环境覆盖 + parameters: # 命令行参数覆盖(优先级最高) + - name: replicas + value: "3" + - name: image.tag + value: "{{ .Values.image.sha }}" # 引用 CI 写入的 commit SHA + destination: + server: https://kubernetes.default.svc + namespace: production +``` + +> [!question] 为什么不只用纯 YAML? +> +> 想象一下:每个环境的 Deployment 只有 `replicas`、`resources`、`imageTag` 不同,其余完全一样。如果全部写为裸 YAML,维护 5 个环境 = 维护 5 份几乎相同的文件。Helm/Kustomize 让你用 **模板化 + 差异覆盖** 解决这个 DRY 问题。 + +### ArgoCD 原生 Kustomize 示例 + +```yaml +# 直接用 kustomize 作为 source +spec: + source: + repoURL: https://github.com/team/k8s-manifests.git + targetRevision: main + path: overlays/production/order-service # kustomize overlay 路径 + kustomize: + images: + - name: myregistry/order-service + newTag: v1.2.3 # CI 注入的镜像版本 +``` + +> [!note] 选择建议 +> +> | 场景 | 推荐 | +> |------|------| +> | 需要复杂的条件渲染和复用 | Helm | +> | 简单的环境覆盖(改几个字段) | Kustomize | +> | ArgoCD 原生支持两者,可以混用(多源模式已在 App of Apps 中展示) | — | + +## 同步工作流:Hook 与健康检查 + +### 同步生命周期 + +```mermaid +flowchart LR + A["Git 提交"] --> B{"ArgoCD\n检测到差异"} + B -->|"执行 PreSync Hook"| C["PreSync"] + C -->|"迁移 DB Schema"| D[健康检查] + D -->|"前向兼容? "| E["Sync"] + E -->|"部署新版本"| F["PostSync Hook"] + F -->|"灰度验证 / 通知"| G["Health Status"] + + style C fill:#fff3e0 + style E fill:#e3f2fd + style F fill:#e8f5e9 + style G fill:#f3e5f5 +``` + +### PreSync Hook:数据库迁移示例 + +```yaml +# hooks/db-migration.yaml +apiVersion: batch/v1 +kind: Job +metadata: + name: db-migrate + annotations: + argocd.argoproj.io/hook: PreSync # Sync 之前执行 + argocd.argoproj.io/hook-delete-policy: HookSucceeded # 成功后自动清理 +spec: + template: + spec: + containers: + - name: migrate + image: myregistry/order-service:v1.2.3 + command: ["./migrate", "--up"] + restartPolicy: Never +``` + +**常用 Hook 阶段**: + +| 阶段 | 时机 | 典型用途 | +|------|------|---------| +| `PreSync` | Sync 之前 | DB 迁移、缓存预热 | +| `Sync` | 替代默认同步行为 | 复杂的多步骤部署 | +| `PostSync` | Sync 之后 | 灰度验证、发 Slack 通知 | +| `SyncFail` | Sync 失败时 | 回滚、告警 | + +> [!important] Hook 注意事项 +> +> - `hook-delete-policy: HookSucceeded` — 避免残留大量已完成的历史 Job +> - PreSync Hook 本身也有健康检查机制,Job 必须 Running → Succeeded 才算通过 +> - Hook Job 和目标 Application 必须在同一个 Kubernetes 集群 + +### 自定义健康检查 + +ArgoCD 内置了对 Deployment、StatefulSet、Service 等资源的健康检查。对于自定义 CRD(比如 CassandraCluster),可以通过 Script Health Check 实现: + +```yaml +# ConfigMap 挂载到 ArgoCD Server +apiVersion: v1 +kind: ConfigMap +metadata: + name: resource-customizations + namespace: argocd +data: + myapp.example.com_CassandraCluster.health.lua: | + local status = {} + if obj.status ~= nil then + if obj.status.readyNodes ~= nil then + if obj.status.readyNodes >= 3 then + status.status = "Healthy" + else + status.status = "Progressing" + status.message = "Only " .. tostring(obj.status.readyNodes) .. " ready nodes" + end + end + end + return status +``` + +> [!tip] 常见内置资源健康状态速查 +> +> | 资源类型 | Healthy 条件 | Progressing 条件 | +> |----------|-------------|-----------------| +> | Deployment | 所有 replicas Ready 且当前 | replicaReadyCount < desired | +> | StatefulSet | 所有 Pod Running | Pending 或 replica 不足 | +> | Service | 存在即 Healthy | —(通常不 Progressing) | +> | Ingress | 有 backend 即 Healthy | — | +> | ConfigMap/Secret | 存在即 Healthy | — | + +## 日志调试与故障排查 + +### ArgoCD CLI 常用命令 + +```bash +# 实时日志 +argocd app logs order-service --follow + +# 指定Pod日志(查看特定容器) +argocd app logs order-service -p order-service-pod-abc123 -c sidecar + +# 查看应用事件(谁触发的同步、为什么失败) +argocd app events order-service + +# 强制刷新(跳过缓存) +argocd app get order-service --refresh +``` + +### 常见故障排查流程 + +```mermaid +flowchart TD + A["Application 显示 OutOfSync"] --> B{手动 sync 是否成功?} + B -- 是 --> C["✅ 可能是临时网络抖动, Watch 恢复即可"] + B -- 否 --> D["查看 Events\nargocd app events"] + + D --> E{"错误原因?"} + E -- "ImagePullBackOff" --> F["镜像仓库不可达/凭据过期\n→ 检查 ImagePullSecrets"] + E -- "CrashLoopBackOff" --> G["应用启动失败\n→ 查看 Pod 日志\nargocd app logs"] + E -- "ResourceQuota exceeded" --> H["命名空间配额不足\n→ 调整 Quota 或精简资源"] + E -- "RBAC denied" --> I["ArgoCD SA 权限不足\n→ 检查 ClusterRole Binding"] + E -- "Custom health check failing" --> J["自定义健康脚本语法错误\n→ 检查 resource-customizations ConfigMap"] + + style F fill:#ffebee + style G fill:#e3f2fd + style H fill:#fff3e0 + style I fill:#f3e5f5 + style J fill:#e8f5e9 +``` + +### 调试 Checklist + +- [ ] `kubectl get application -n argocd` — 确认 Application CR 状态 +- [ ] `kubectl describe application xxx -n argocd` — 查看 Conditions 和最后同步信息 +- [ ] `argocd app logs xxx --tail 100` — Controller 日志,搜索 Error 关键字 +- [ ] 确认 Git Repo URL 和 token 可用:手动 curl 测试 +- [ ] 如果是 self-heal 反复触发:检查是否有外部控制器在修改资源 + +## 安全要点 + +### RBAC 配置 + +```yaml +# argocd-rbac-cm.yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: argocd-rbac-cm + namespace: argocd +data: + policy.default: role:readonly # 默认只读 + policy.csv: | + # g, github-org:team-name, role:admin # 团队级管理 + p, role:order-service-deployer, applications, sync, */order-service, allow + g, github:alice, role:order-service-deployer # alice 可同步 order-service + scopes: '[groups]' # 从 OIDC token 获取 groups +``` + +### Secrets 管理建议 + +| 方式 | 适用场景 | 备注 | +|------|---------|------| +| **ArgoCD Secret** | Git 仓库凭据、K8s cluster 注册 | 存储为 SealedSecret/ExternalSecret | +| **External Secrets Operator** | RDS 密码、API Key | 从 AWS Secrets Manager / Vault 拉取 | +| **SealedSecret** | 跨集群复用加密 secrets | 公钥加密,仅 controller 可解密 | + +> [!warning] 常见误区 +> +> - **不要把 Git SSH Private Key 存入集群的普通 Secret** — 应该用 `argocd reponame` 配置项统一管理 +> - **Application 的 namespace 要单独隔离** — `argocd` 命名空间不应和生产共享 +> - **启用 OIDC + RBAC scopes** — 让团队只能看到自己管理的 Application + +--- + +现在回到日常操作: + +## 常见运维场景 + +### 手动触发同步 + +当 Git 已更新但 ArgoCD 未自动同步时: + +```bash +argocd app sync order-service --force +``` + +### 查看同步状态和历史 + +```bash +argocd app get order-service # 当前状态 +argocd app history order-service # 同步历史 +argocd app diff order-service # Git vs 实际的差异 +``` + +### 回滚到上一个版本 + +```bash +argocd app rollback order-service +# 或直接 git revert 对应的 commit,ArgoCD 自动同步 +``` + +### 解除锁定(Stuck App 恢复) + +```bash +argocd app unlock order-service +``` + +## 工具选型:ArgoCD vs Flux + +| 特性 | ArgoCD | Flux v2 | +|------|--------|---------| +| **UI** | 丰富的 Web UI,可视化对比 | CLI + Kubernetes-native,轻量 UI | +| **通知** | Slack/Discord/GitHub native | Event controller + Webhook | +| **Helm** | 内置 Helm Source 插件 | 一等公民(HelmRelease) | +| **多集群** | 多集群统一管理 | 需配合 Image Automation | +| **适合场景** | 可视化管理需求强的团队 | 纯 GitOps 极客团队 | + +> [!note] 选型建议 +> +> - 如果你已经重度使用 Helm:Flux 的 HelmRelease CRD 更自然 +> - 如果你希望可视化查看每次部署的差异:ArgoCD 的 diff view 是杀手功能 +> - 两者可以并存——比如 ArgoCD 管应用层,Flux 管基础设施层(CNI、CSI 等) + +## 关联笔记 + +- [[01-CICD基础与实践]] — CI/CD Pipeline 的搭建与实践 +- [[03-Helm模板管理]] — Helm Chart 编写与管理 +- [[04-安全与发布策略]] — 安全管理与发布决策 +- [[../03-CICD与GitOps]] — 参考手册与决策指南 diff --git a/hhs/MS/05-部署运维/03-CICD与GitOps/03-Helm模板管理.md b/hhs/MS/05-部署运维/03-CICD与GitOps/03-Helm模板管理.md new file mode 100644 index 0000000..dbb2e3b --- /dev/null +++ b/hhs/MS/05-部署运维/03-CICD与GitOps/03-Helm模板管理.md @@ -0,0 +1,454 @@ +--- +tags: [helm, kubernetes, templating, kustomize, devops] +create time: 2026-05-17 10:00 +--- + +# Helm 模板管理 + +## 概述 + +本文档系统讲解 Helm Chart 的编写与管理实践。内容覆盖:Chart 骨架、values.yaml 参数化、Go 模板语法、多环境值覆盖、Chart 依赖管理、Hooks 机制,以及与 ArgoCD 的集成方式。架构决策参见 [[../03-CICD与GitOps]]。 + +## 为什么需要 Helm? + +> [!question] 每个服务几十个 YAML 文件,怎么管理? +> +> 一个典型的 `order-service` 部署包含:Deployment、Service、Ingress、ConfigMap、HPA、NetworkPolicy、PDB……当你有 50 个服务,每个都要维护这么一堆模板——**这是重复劳动的噩梦**。 + +Helm 提供了两个核心抽象: + +| 概念 | 类比 | 说明 | +|------|------|------| +| **Chart** | NPM 包 / Maven jar | 打包格式,定义"我要部署什么" | +| **Release** | npm install 的实例 | 运行实例,同一个 Chart 可以有多个 Release(staging/prod) | + +```bash +# 创建 Chart 骨架 +helm create order-service + +# 目录结构一览 +charts/order-service/ +├── Chart.yaml # 元信息 (name, version, description) +├── values.yaml # 默认值 —— 你唯一需要经常改的文件 +├── templates/ # Go 模板文件 +│ ├── deployment.yaml +│ ├── service.yaml +│ └── ingress.yaml +└── .helmignore # 类似 .gitignore +``` + +### Helm Release 生命周期 + +```mermaid +flowchart LR + A["install"] --> B["running"] + C["upgrade"] --> B + D["rollback"] --> B + E["uninstall"] --> B + style A fill:#e1f5fe + style C fill:#fff3e0 + style D fill:#fce4ec + style E fill:#f3e5f5 +``` + +## 第一步:让配置全部参数化 + +**values.yaml** 中存放所有可变参数,遵循"一处定义、多处引用"原则: + +```yaml +# values.yaml +replicaCount: 2 + +image: + repository: registry.example.com/order-service + tag: "v1.2.3" + pullPolicy: IfNotPresent + +service: + type: ClusterIP + port: 8080 + +resources: + requests: + cpu: 200m + memory: 256Mi + limits: + cpu: 500m + memory: 512Mi + +autoscaling: + enabled: true + minReplicas: 3 + maxReplicas: 20 + targetCPUUtilizationPercentage: 75 +``` + +**templates/deployment.yaml** 中使用 `{{ }}` 引用: + +```yaml +# templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + namespace: {{ .Release.Namespace }} +spec: + replicas: {{ .Values.replicaCount }} + selector: + matchLabels: + app: {{ .Release.Name }} + template: + spec: + containers: + - name: {{ .Chart.Name }} + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + resources: {{ toYaml .Values.resources | nindent 12 }} +``` + +> [!tip] 两个关键技巧 +> +> 1. **`toYaml .X | nindent 12`**:将嵌套的对象序列化为 YAML 并缩进。如果直接写 `{{ .Values.resources }}`,输出会压缩成一行,Kubernetes 无法解析。 +> 2. **`-` 修剪空白**:`{{- if ...` 和 `{{- end }}` 中的 `-` 会消除模板前后多余的换行符,避免生成空行导致 YAML 格式错误。 + +### Helm 内置对象速查 + +| Helm 对象 | 访问方式 | 用途 | +|-----------|----------|------| +| `.Release` | `.Release.Name`, `.Release.Namespace` | 当前发布实例的信息 | +| `.Chart` | `.Chart.Name`, `.Chart.Version` | Chart 本身的元信息 | +| `.Values` | `.Values.replicaCount` 等 | 用户传入的值(合并了 values.yaml + --set) | +| `.Capabilities` | `.Capabilities.KubeVersion` | 集群 API 版本信息 | +| `.Files` | `.Files.Get "config.ini"` | 读取同目录下的非模板文件 | +| `.Notes` | 渲染后显示给用户的提示文本 | Post-install 使用说明 | + +## 第二步:多环境值覆盖 + +通过 **values 文件叠加**实现不同环境的差异化: + +```bash +# Staging 使用命令行临时覆盖 +helm upgrade --install order-service ./charts/order-service \ + --namespace=staging \ + --set image.tag=v1.2.4-dev + +# Production 叠加额外值文件 +helm upgrade --install order-service ./charts/order-service \ + --namespace=production \ + --values charts/order-service/values.prod.yaml \ + --set image.tag=v1.2.3 +``` + +典型 `values.prod.yaml`(只写与默认值不同的部分): + +```yaml +replicaCount: 5 + +resources: + requests: + cpu: 500m + memory: 512Mi + limits: + cpu: "2" + memory: 2Gi + +autoscaling: + minReplicas: 5 + maxReplicas: 50 +``` + +> [!tip] Values 文件合并优先级(低 → 高) +> +> 1. `values.yaml` — 默认值 +> 2. `values.prod.yaml` — 环境变量覆盖文件 +> 3. `--set image.tag=xxx` — 命令行参数(最高优先级) + +```mermaid +flowchart TD + A["values.yaml"] --> D["合并结果"] + B["values.prod.yaml"] --> D + C["--set image.tag=v1.2.3"] --> D + D --> E[".Values"] +``` + +> [!warning] 常见陷阱:JSON Patch +> +> 当使用 `--set` 修改嵌套字段时,Helm 使用 JSON Patch,意味着它会**替换整个对象**而非合并。例如 `--set resources.limits.cpu="1"` 会将 `memory` 字段丢弃。对于复杂嵌套,优先使用独立的 values 文件。 + +## 第三步:条件渲染与循环 + +### 条件渲染 + +用 Helm 的条件语法处理可选资源: + +```yaml +# templates/ingress.yaml +{{- if .Values.ingress.enabled }} +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ .Release.Name }} +spec: + rules: + - host: {{ .Values.ingress.host | quote }} + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: {{ .Release.Name }} + port: + number: {{ .Values.service.port }} +{{- end }} +``` + +> [!note] `quote` 管道函数 +> +> 当值的类型不确定时(可能是字符串也可能是数字),加 `| quote` 确保输出带引号,避免 `true/false` 被 YAML 解析为布尔值。 + +### Range 循环生成多个资源 + +当需要批量创建多个 ServiceMonitor、ConfigMap 等时,`range` 非常有用: + +```yaml +# templates/servicemonitor.yaml +{{- range $key, $value := .Values.extraServices }} +--- +apiVersion: monitoring.coreos.com/v1 +kind: ServiceMonitor +metadata: + name: {{ $.Release.Name }}-{{ $key }} +spec: + selector: + matchLabels: + app: {{ $.Release.Name }} + endpoints: + - port: {{ $value.port }} + interval: {{ $value.interval | default "30s" }} +{{- end }} +``` + +对应的 values: + +```yaml +extraServices: + grpc-exporter: + port: grpc + interval: 15s + prometheus-path: + port: metrics + interval: 10s +``` + +> [!example] 生成结果 +> +> 上面这段模板会为每个 `extraServices` 条目生成一个独立的 ServiceMonitor。注意 `$key` 绑定到键名(`grpc-exporter`),而 `$value` 绑定到对应对象。使用 `$.Release.Name`(带美元前缀)是因为在 range 内部 `.` 已被重绑定。 + +## 第四步:复用模板片段 + +随着 Chart 膨胀,`deployment.yaml` 和 `service.yaml` 之间会产生大量重复逻辑。Helm 提供 `_helpers.tpl` 来统一管理可复用的模板片段。 + +### _helpers.tpl 标准写法 + +```yaml +{{/* +Common labels — 所有资源统一标注 */}} +{{- define "order.labels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} +{{- end -}} + +{{/* +Selector labels — Deployment selector 必须使用固定值 */}} +{{- define "order.selectorLabels" -}} +app.kubernetes.io/name: {{ .Chart.Name }} +app.kubernetes.io/instance: {{ .Release.Name }} +{{- end -}} +``` + +### 在模板中引用 + +```yaml +# templates/deployment.yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ .Release.Name }} + labels: + {{- include "order.labels" . | nindent 4 }} +spec: + selector: + matchLabels: + {{- include "order.selectorLabels" . | nindent 6 }} +``` + +> [!tip] include vs template +> +> - `include "name" .`:**返回值**,可通过 `| nindent` 继续管道处理 —— 适合注入 label、annotation +> - `template "name" .`:**直接输出**,不返回结果 —— 很少用,通常只在特殊情况下需要 + +## Chart 依赖与仓库管理 + +大型项目往往不会从零编写所有模板,而是复用社区已有的 Chart。 + +### 声明子依赖 + +```yaml +# Chart.yaml +apiVersion: v2 +name: order-service +type: application +version: 1.2.0 +appVersion: "1.2.3" + +dependencies: + - name: postgresql + version: "15.0.0" + repository: https://charts.bitnami.com/bitnami + condition: postgresql.enabled + - name: redis + version: "18.0.0" + repository: https://charts.bitnami.com/bitnami + condition: redis.enabled +``` + +- `condition` 告诉 Helm:当 values 中对应路径为 `false` 时,跳过安装该子 Chart。 +- 子 Chart 的 values 通过 `postgresql.xxx` / `redis.xxx` 命名空间传入。 + +### 常用命令 + +```bash +helm dependency update ./charts/order-service # 拉取 / 更新子依赖 +helm dependency build ./charts/order-service # 仅从 Chart.lock 构建(无网络变更时使用) +helm repo add bitnami https://charts.bitnami.com/bitnami +helm search repo prometheus # 搜索公开 Chart +``` + +> [!note] Chart.yaml 的 apiVersion +> +> - `apiVersion: v1`(Helm 2):依赖写在 `dependencies:` 数组里,语义不同 +> - `apiVersion: v2`(Helm 3):引入 `requirements.yaml` 的概念整合到 Chart.yaml 自身,推荐使用 v2 + +## 与 ArgoCD 集成 + +ArgoCD 原生支持将 Helm Chart 作为 Application source,自动解析 `values.yaml`: + +```yaml +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: order-service +spec: + source: + repoURL: https://github.com/team/k8s-manifests.git + targetRevision: main + path: charts/order-service + helm: + parameters: + - name: image.tag + value: v1.2.3 + - name: replicaCount + value: "5" +``` + +> [!tip] ArgoCD + Helm 进阶用法 +> +> - **`helm.valueFrom`**:从 ConfigMap 或 Secret 取值,避免把敏感值暴露在 Git 中 +> - **`helm.fileParameters`**:从文件加载大段配置值,适合 CI/CD 流水线场景 +> - **`helm.ignoreMissingValueFiles: true`**:允许某些 values 文件按环境选择性存在 + +## Hooks:在关键时机执行自定义逻辑 + +Helm Hooks 允许你在 Release 的生命周期事件中挂载自定义 Job 或 Pod: + +```yaml +# templates/migrate-db.yaml +{{- if .Values.dbMigration.enabled }} +apiVersion: batch/v1 +kind: Job +metadata: + name: {{ .Release.Name }}-db-migrate + annotations: + "hooks.helm.sh/hook": pre-upgrade,pre-install + "hooks.helm.sh/hook-delete-policy": hook-succeeded +spec: + template: + spec: + containers: + - name: migrate + image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" + command: ["./migrate", "up"] + restartPolicy: Never +{{- end }} +``` + +| Hook 事件 | 触发时机 | +|-----------|----------| +| `pre-install` | 资源创建之前(如数据库迁移) | +| `post-install` | 资源创建之后(如初始化数据) | +| `pre-upgrade` | 升级操作之前 | +| `post-upgrade` | 升级操作之后 | +| `pre-delete` | 删除操作之前(如优雅停机通知) | +| `post-delete` | 删除操作之后 | + +## 调试与测试 + +### 可视化渲染结果 + +```bash +# 查看渲染后的纯 YAML(不做实际部署) +helm template my-release ./charts/order-service + +# 指定命名空间和 values 文件 +helm template my-release ./charts/order-service \ + --namespace=staging \ + -f values.staging.yaml +``` + +> [!tip] 调试三件套 +> +> 1. **`helm lint ./charts/order-service`**:静态检查,快速定位语法问题 +> 2. **`helm template`**:看渲染后的全量 YAML,逐段排查问题 +> 3. **`helm diff upgrade --install ...`**:配合 [helm-diff 插件](https://github.com/helm/diff),对比升级前后的差异 + +### Dry Run + +```bash +# 模拟部署(服务端校验,但不会真正改变集群状态) +helm upgrade --install my-release ./charts/order-service \ + --dry-run --debug +``` + +## 最佳实践 + +| 原则 | 具体做法 | +|------|---------| +| **单一真相源** | 所有可配参数集中在 values.yaml,模板层只做引用不做硬编码 | +| **Helper 集中管理** | 公共 labels / selectors / annotations 一律放在 `_helpers.tpl` | +| **条件最小化** | 能用 defaults 解决的就不加 `if`;减少分支能大幅降低测试复杂度 | +| **版本号分离** | `Chart.yaml` 的 `version` 管 Chart 本身迭代,`appVersion` 管应用版本 | +| **先 lint 再 push** | CI 中加入 `helm lint` + `helm template --strict` 门禁 | +| **值文件只写差异** | `values.prod.yaml` 只覆盖与默认值不同的字段,保持可读性 | + +## 何时用裸 YAML vs Helm vs Kustomize? + +| 场景 | 推荐方案 | 理由 | +|------|---------|------| +| < 10 个服务 | Kustomize / 裸 YAML | 复杂度高于收益 | +| 10~50 个服务 | Helm | 模板复用价值明显 | +| > 50 个服务 | Helm + Kustomize overlays | Helm 管模板,Kustomize 管环境差异 | + +> [!note] Kustomize 的定位 +> +> Kustomize 不依赖 Go templating,更轻量。适合"基于同一套模板,按环境覆盖差异配置"的场景。 +> +> **常见组合**:Helm 生成基础模板,Kustomize overlay 处理 staging/prod 差异。 + +## 关联笔记 + +- [[01-CICD基础与实践]] — CI/CD Pipeline 的搭建与实践 +- [[02-GitOps与ArgoCD]] — GitOps 工作流与 ArgoCD +- [[04-安全与发布策略]] — 安全管理与发布决策 +- [[../03-CICD与GitOps]] — 参考手册与决策指南 diff --git a/hhs/MS/05-部署运维/03-CICD与GitOps/04-安全与发布策略.md b/hhs/MS/05-部署运维/03-CICD与GitOps/04-安全与发布策略.md new file mode 100644 index 0000000..441f07c --- /dev/null +++ b/hhs/MS/05-部署运维/03-CICD与GitOps/04-安全与发布策略.md @@ -0,0 +1,470 @@ +--- +tags: [security, secrets-management, canary, rollback, deployment-strategy, feature-flag, database-migration] +create time: 2026-05-18 14:30 +--- + +# 安全与发布策略 + +## 概述 + +本文档聚焦生产级部署中的两大决策点:**敏感信息管理**和**发布策略选择**。提供方案对比、实操检查清单以及零停机发布的数据库迁移策略。架构概览参见 [[../03-CICD与GitOps]]。 + +## Secret 管理:分层决策树 + +```mermaid +flowchart TD + START["开始选型"] --> SIZE["团队规模?"] + SIZE -- "< 20人" --> K8SSECRET["K8s 原生 Secret\n✅ 开箱即用"] + SIZE -- "≥ 20人" --> CLOUD["是否深度绑定云厂商?"] + + CLOUD -- "是 (AWS/GCP/Azure)" --> SECRETMGR["云厂商 Secret Manager\n✅ 集成度高,审计完善"] + CLOUD -- "否 / 多云" --> ESOOROPS["需要 GitOps?"] + + ESOOROPS -- "是 (ArgoCD)" --> SOPS["SOPS + sealed-secrets\n✅ 加密文件可提交到 Git"] + ESOOROPS -- "否 / 已有 Vault" --> ESO["External Secrets Operator\n✅ Git 中不存任何密文"] + + style K8SSECRET fill:#e8f5e9 + style SECRETMGR fill:#e3f2fd + style SOPS fill:#fff3e0 + style ESO fill:#f3e5f5 +``` + +### 方案速查表 + +| 方案 | 适用场景 | 优点 | 缺点 | +|------|---------|------|------| +| **K8s 原生 Secret** | 小规模、内部团队 | 零额外成本,开箱即用 | 明文存储在 etcd,RBAC 控制有限 | +| **External Secrets Operator (ESO)** | 已有 Vault / AWS SSM | Git 中不存任何密文 | 需维护额外组件 | +| **SOPS + sealed-secrets** | ArgoCD 用户 | 加密文件可提交到 Git | 需管理公钥基础设施 | +| **AWS Secrets Manager / GCP Secret Manager** | 云厂商深度绑定 | 集成度高,审计完善 | 锁定特定云平台 | + +### ❌ 绝对不要做的事 + +```yaml +# 错误示范:明文写密码 +apiVersion: v1 +kind: ConfigMap +metadata: + name: app-config +data: + DB_PASSWORD: "my-secret-password" # 绝对不要这样做! +``` + +### ✅ 正确做法 + +```yaml +# 使用 Secret(stringData 接收明文,API Server 自动 Base64) +apiVersion: v1 +kind: Secret +metadata: + name: app-secrets + namespace: production +type: Opaque +stringData: + DB_PASSWORD: "${DB_PASSWORD}" # 通过 CI secrets 注入 + API_KEY: "${API_KEY}" +--- +# 在 Deployment 中引用 +apiVersion: apps/v1 +kind: Deployment +spec: + template: + spec: + containers: + - name: order-service + envFrom: + - secretRef: + name: app-secrets +``` + +> [!warning] 为什么不能把 Secret 明文放进 Git? +> +> 即使使用 GitOps(ArgoCD),也不建议将明文密码提交到仓库。一旦权限管控疏漏——比如某个离职员工的账号未被撤销——整个公司的数据库密码就暴露了。使用 ESO 或 sealed-secrets,**Git commit 历史中永远不会有明文密钥**。 + +### Secret 进阶实践 + +#### 1. RBAC:最小权限原则 + +```yaml +# 限制只有特定 ServiceAccount 可以读取特定 Secret +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: order-service-read-secret + namespace: production +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: secret-reader # 仅限 Read,禁止 Create/Update/Delete +subjects: + - kind: ServiceAccount + name: order-service-sa # 仅该 SA 有权读取 +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: secret-reader + namespace: production +rules: + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["app-secrets"] # 按名称限定,不可通读所有 Secret + verbs: ["get", "list"] +``` + +> [!tip] `resourceNames` 是什么? +> K8s RBAC 默认是资源级别的(你能读这个 namespace 下**所有** Secret)。加上 `resourceNames` 后变成**细粒度**控制——只能读指定的几个 Secret 名。这是防止越权访问的关键手段。 + +#### 2. 开启 etcd 加密 + +K8s 的 Secret 在 etcd 中以 Base64 编码存储,而非加密。建议在集群层面启用 EncryptionConfiguration: + +```yaml +# encryption-config.yaml +apiVersion: apiserver.config.k8s.io/v1 +kind: EncryptionConfiguration +resources: + - resources: + - secrets + providers: + - aescbc: # 首选 AES-CBC 加密 + keys: + - secret: $(ENCRYPTION_KEY) # 从环境变量注入,不要硬编码 + - identity: # fallback:旧数据用明文 +``` + +> [!note] 为什么要开启 etcd 加密? +> 如果不加密, anyone 能访问 etcd(比如 K8s admin 角色的人、云厂商控制台的操作员)就能直接读取所有 Secret 明文。启用后,etcd 中实际存储的是密文。⚠️ **加密必须一次性完成**——一旦写入数据,后续切换加密 provider 会导致旧数据丢失。 + +#### 3. 审计日志 + +确保 kube-apiserver 启用了审计策略,记录所有 Secret 的读写操作: + +```yaml +# audit-policy.yaml +apiVersion: audit.k8s.io/v1 +kind: Policy +rules: + - level: Metadata # Secret 只记录元数据,不记录内容 + resources: + - group: "" # core API group + resources: ["secrets"] + verbs: ["get", "list", "watch"] + - level: None + resources: ["secrets"] + verbs: ["update", "patch", "delete"] # 修改类操作也需记录 +``` + +## 数据库迁移策略 + +> [!question] 发布新版本时,代码先上还是数据库先改? +> 答案:**两边要同时准备好**——但「先扩后缩」是核心原则。 + +零停机发布的最大陷阱是**数据库 schema 变更**。如果代码 A 版本写了字段 X,而代码 B 版本还没发布,此时如果删掉字段 X,代码 A 就会崩溃。遵循以下原则: + +### 向前兼容原则 + +```mermaid +flowchart LR + subgraph Phase1["Phase 1: 加字段(向后兼容)"] + P1A[添加新字段(可为 NULL)] --> P1B[部署新代码(双写新旧字段)] + end + + subgraph Phase2["Phase 2: 填存量数据"] + P2A[后台任务回填历史数据] --> P2B[验证数据一致性] + end + + subgraph Phase3["Phase 3: 切换到新字段"] + P3A[部署代码(仅读新字段)] --> P3B[删除旧字段] + end + + P1B --> P2A + P2B --> P3A + + style Phase1 fill:#e8f5e9 + style Phase2 fill:#fff3e0 + style Phase3 fill:#fce4ec +``` + +**三阶段详解:** + +| 阶段 | 动作 | 兼容性保证 | +|------|------|-----------| +| **Phase 1** | `ALTER TABLE` 新增列(NOT NULL 不行!设为可 NULL)+ 新代码同时写入新旧字段 | 旧代码继续读旧字段,正常运行 | +| **Phase 2** | 用后台 Job 批量填充新字段的存量数据,直到全量一致 | 旧代码仍然正常,新代码已具备降级能力 | +| **Phase 3** | 部署仅读新字段的代码 → 确认无误后 `ALTER TABLE DROP` 旧字段 | 完全切换到新模式 | + +> [!example] 具体例子:给订单表增加 `status_v2` 枚举字段 +> +> **Phase 1 — 加列 + 双写:** +> ```sql +> ALTER TABLE orders ADD COLUMN status_v2 VARCHAR(20) NULL; +> ``` +> ```go +> // 新代码:写入两个字段 +> db.Exec("UPDATE orders SET status=?, status_v2=? WHERE id=?", oldStatus, newV2, orderId) +> ``` +> +> **Phase 2 — 回填存量:** +> ```go +> func migrateLegacyStatus() { +> batch := 1000 +> offset := 0 +> for { +> rows, _ := db.Query("SELECT id, status FROM orders LIMIT $1 OFFSET $2", batch, offset) +> // ... 转换 status -> status_v2 并 UPDATE ... +> if rows.Count() < batch { break } +> offset += batch +> } +> } +> ``` +> +> **Phase 3 — 切流 + 清理:** +> ```go +> // 新代码:只读写 status_v2 +> db.Query("SELECT * FROM orders WHERE status_v2='paid'") +> // 确认全量稳定运行一周后执行: +> ALTER TABLE orders DROP COLUMN status; +> ``` + +### 关键注意事项 + +- **永远不要在同一个 deploy 中「删字段 + 上线读该字段的代码」** ——这必出事故 +- `ALTER TABLE` 对大表是重操作:使用 `pt-online-schema-change`(MySQL)或 `gh-ost` 避免锁表 +- DML(增删改行)可以用分批 + delay 方式;DDL(改结构)一定要离线窗口或使用在线工具 +- 每次迁移前备份:`mysqldump` 或快照,回滚有退路 + +## 回滚策略与机制 + +> [!question] 发布后发现 Bug,怎么办? +> 最快的回滚不是「修代码再发布」,而是「切回上一个版本」。 + +### 回滚时机判断 + +```mermaid +flowchart TD + ALERT["告警触发 / 监控异常"] --> THRESHOLD{"指标超过阈值?"} + + THRESHOLD -- "Error Rate > 目标值" --> AUTO_ROLLBACK["⚡ 自动回滚 Canary"] + THRESHOLD -- "Latency p99 升高" --> MANUAL["人工评估是否需要回滚"] + THRESHOLD -- "业务指标下降但不紧急" --> MONITOR["持续观察,暂不回滚"] + + AUTO_ROLLBACK --> LOG["通知 Team + 记录事件"] + MANUAL -- "决定回滚" --> LOG + MANUAL -- "决定观察" --> MONITOR + + LOG --> POSTMORTEM["事后复盘 Post-mortem"] + + style AUTO_ROLLBACK fill:#ffebee + style MANUAL fill:#fff3e0 + style MONITOR fill:#e8f5e9 + style POSTMORTEM fill:#e3f2fd +``` + +### Rolling Update 的回滚 + +```bash +# kubectl rollout 是最基础的回滚手段 +kubectl rollout status deployment/order-service # 查看当前状态 +kubectl rollout undo deployment/order-service # 回滚到上一版本 +kubectl rollout history deployment/order-service # 查看所有 revision +kubectl rollout undo deployment/order-service --to-revision=3 # 回滚到指定版本 + +# 也可以直接用之前验证过的镜像重新 apply +kubectl set image deployment/order-service order-service=myregistry/app:v1.2.3@sha256:... +``` + +### Blue-Green 的回滚 + +Blue-Green 天然支持秒级回滚——只需把 Service 的 selector 切回蓝环境即可: + +```yaml +# 当前绿环境在服务,切回蓝环境: +apiVersion: v1 +kind: Service +metadata: + name: order-service +spec: + selector: + app: order-service + version: blue # ← 从 green 改成 blue + ports: + - port: 80 + targetPort: 8080 +``` + +> [!tip] 如何做到无缝切换? +> 配合 **外部 DNS**(Route53 / CloudFlare)可以跨 Kubernetes Service 实现全局流量切换。Service Level Switching 毫秒级,DNS TTL 通常分钟级。关键是把流量入口层和服务层都做好双写。 + +### ArgoCD 一键回滚 + +```yaml +# ArgoCD Application 定义 +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: order-service +spec: + project: default + source: + repoURL: https://github.com/team/k8s-manifests.git + targetRevision: v1.2.3 # ← 改到这个 tag 即回滚 + path: overlays/production + syncPolicy: + automated: + prune: true # 自动删除不在 Git 中的资源 + selfHeal: true # 检测到漂移自动修复 +``` + +ArgoCD 的回滚就是「改 Git 里的目标版本 + push」——这就是 GitOps 的魅力:**回滚也是提交**。 + +## 特性开关(Feature Flag) + +> [!question] 有没有一种方法可以让新功能「已经部署到生产」但「对用户还不可见」? +> 答案是 Feature Flag —— **代码提交 ≠ 功能上线**。 + +### 典型使用模式 + +```go +// feature_flags.go +package flags + +import "github.com/thomaspoison/go-feature-flag/provider/fileprovider" + +func init() { + ffclient.Init(ffconfig.Config{ + FilePath: "features.yaml", // 配置文件路径 + PollInterval: 30, // 每 30 秒刷新 + }) +} + +// 调用处 +if flags.Value("enable-new-checkout", false) { + // 新功能逻辑 + return NewCheckoutFlow(ctx) +} +return LegacyCheckoutFlow(ctx) // 兜底:传统流程 +``` + +对应配置文件 `features.yaml`: + +```yaml +enable-new-checkout: + percentage: 0 # 初始为 0%,对所有人关闭 + rollout: gradual # 渐进式放量 + rules: + - attribute: env + operator: equal + value: production + invert: false + values: [50] # 灰度到 50% 的用户 +``` + +### 何时使用 Feature Flag + +| 场景 | 推荐度 | 理由 | +|------|--------|------| +| 复杂业务逻辑开关 | ⭐⭐⭐⭐⭐ | 无需重新部署即可上线下线功能 | +| A/B 测试 | ⭐⭐⭐⭐⭐ | 按用户维度控制实验组/对照组 | +| 简单配置项 | ⭐⭐ | 用 ConfigMap 就够了,Flag 太重 | +| 临时 Debug | ⭐⭐⭐ | 用环境变量更直接 | + +### Feature Flag vs Release Branch + +```mermaid +flowchart LR + trunk["主分支 main/master\n🟢 始终可构建"] --> CI["CI 自动化构建"] + + subgraph flag["Feature Flag 方式"] + F1["新功能开发完成"] --> F2["合并到 main\nFlag = OFF"] --> F3["发布到生产"] --> F4["调试后 Flag = ON"] + end + + subgraph branch["Release Branch 方式"] + B1["创建 release/v2.0 分支"] --> B2["隔离新功能"] --> B3["发布时合入 main"] + end + + CI --> F1 + CI --> B2 + + style trunk fill:#e8f5e9 + style flag fill:#f3e5f5 + style branch fill:#fff3e9 +``` + +核心差异:**Feature Flag 保持单一 main 分支,Release Branch 产生长期隔离分支**。现代 GitOps 实践更推荐 Flag,因为可以避免 merge conflict 堆积和重复测试。 + +## 发布前检查清单 + +```mermaid +flowchart TD + PRE["发布前检查"] --> T1{"镜像 Tag 是否精确?"} + T1 -- ✅ SHA-based --> T2{"Staging 是否已通过?"} + T1 -- ❌ latest / head --> FAIL1["❌ 拒绝部署"] + T2 -- ✅ Passed --> T3{"Prod Approval 是否完成?"} + T2 -- ❌ Failed/Pending --> FAIL2["❌ 暂停等待"] + T3 -- ✅ Approved --> T4{"Canary 指标是否正常?"} + T3 -- ❌ Rejected --> FAIL2 + T4 -- ✅ All Green --> SUCCESS["✅ 全量发布"] + T4 -- ❌ Error Rate 升高 --> ROLLBACK["⚠️ 自动回滚"] + + style FAIL1 fill:#ffebee + style FAIL2 fill:#fff3e0 + style SUCCESS fill:#e8f5e9 + style ROLLBACK fill:#fce4ec +``` + +| # | 检查项 | 工具/方法 | +|---|--------|-----------| +| 1 | 镜像 Tag 使用 Commit SHA | GitHub Actions `${{ github.sha }}` | +| 2 | 单元测试覆盖率达标 | `make test` + coverage threshold | +| 3 | 安全扫描无 HIGH/CRITICAL | Trivy / Snyk | +| 4 | Staging 环境冒烟测试通过 | 自动化 health check | +| 5 | Prod 审批人工确认 | GitHub Environment Protection Rule | +| 6 | Canary 阶段 error rate < 0.1% | Prometheus + Alertmanager | +| 7 | Git manifest 已更新并 merged | ArgoCD Application status = Synced | + +## 渐进式采纳路径 + +如果你的团队还在手工部署,不必一步到位。建议分三步走: + +```mermaid +flowchart LR + STEP1[("1. 自动化构建+Push")] --> STEP2[("2. Staging + 审批关卡")] --> STEP3[("3. GitOps, Git 即真相源")] + + style STEP1 fill:#e8f5e9 + style STEP2 fill:#fff3e0 + style STEP3 fill:#e3f2fd +``` + +1. **第一步:用 GitHub Actions/GitLab CI 实现自动化构建 + push** — 推荐立即做 +2. **第二步:引入 Staging 环境和审批关卡** — 降低上线风险 +3. **第三步:迁移到 GitOps(ArgoCD)** — 让 Git 成为唯一真相源 + +实际落地时,很多团队的最终架构是 **CI/CD + GitOps 结合**: + +```mermaid +flowchart LR + Dev["开发者 push 代码"] --> CI["GitHub Actions\n(CI: 测试→构建→Push镜像)"] + CI -->|"推送新镜像"| IMGREG["Container Registry"] + + Dev2["开发者 PR 修改 K8s manifest"] --> GIT[(Git Manifest Repo)] + + subgraph Cluster["K8s Cluster"] + Argo["ArgoCD"] -->|"自动同步"| K8s["Pod/Service/ConfigMap"] + end + + GIT -.->|Watch 变动| Argo + + style CI fill:#e8f5e9 + style Argo fill:#fff3e0 + style K8s fill:#e3f2fd +``` + +核心分工:**CI/CD 管"怎么建",GitOps 管"怎么维持"**。两者合在一起,才构成了完整的现代软件交付链。 + +## 关联笔记 + +- [[01-CICD基础与实践]] — CI/CD Pipeline 的搭建与实践 +- [[02-GitOps与ArgoCD]] — GitOps 工作流与 ArgoCD +- [[03-Helm模板管理]] — Helm Chart 编写与管理 +- [[../03-CICD与GitOps]] — 参考手册与决策指南总入口 diff --git a/hhs/MS/05-部署运维/04-SRE实践.md b/hhs/MS/05-部署运维/04-SRE实践.md index 1f78689..e72184d 100644 --- a/hhs/MS/05-部署运维/04-SRE实践.md +++ b/hhs/MS/05-部署运维/04-SRE实践.md @@ -3,11 +3,11 @@ tags: [microservice, sre, slo, error-budget, postmortem] create time: 2026-05-05 --- -# SRE 实践 +# SRE 实践 — 参考手册 ## 概述 -站点可靠性工程 (SRE) 把运维问题看作**软件工程问题**。它的核心理念是用 SLI/SLO/SLA 来量化服务质量,避免"我觉得系统很慢"这类模糊描述。 +站点可靠性工程 (SRE) 把运维问题看作**软件工程问题**。它的核心理念是用 SLI/SLO/SLA 来量化服务质量,避免"我觉得系统很慢"这类模糊描述。本文档是总入口:顶层概览、速查表见本页;详细教程和实操指南在子文档中。 ```mermaid flowchart LR @@ -25,95 +25,15 @@ flowchart LR style Stabilize fill:#ffebee ``` -## SLI / SLO / SLA 详解 +## 快速导航 -### 三层模型 +| 主题 | 定位 | 文档 | +|------|------|------| +| SLI / SLO / SLA 详解 | **基础**:三层模型、比例对照表、设定方法 | [[04-SRE实践/01-SLI-SLO-SLA详解]] | +| Error Budget 深度解析 | **进阶**:计算方式、决策矩阵、告警联动 | [[04-SRE实践/02-错误预算深度解析]] | +| SRE 心法与事故复盘 | **文化**:五条心法、Postmortem 模板、DORA Metrics | [[04-SRE实践/03-SRE心法与事故复盘]] | -| 术语 | 全称 | 定义 | 谁制定 | 变更频率 | -|------|------|------|--------|---------| -| **SLI** | Service Level Indicator | 实际度量:用户请求的成功率是多少? | 观测系统自动产出 | 持续更新 | -| **SLO** | Service Level Objective | 内部目标:我们承诺达到 99.9% 可用性 | SRE + 研发 | 季度回顾 | -| **SLA** | Service Level Agreement | 对外契约:达不到就赔钱 | 法务 + 商务 | 按合同约定 | - -### 实用性比例对照表 - -| SLO | 每年停机时间 | 每月停机时间 | 适用级别 | -|-----|-------------|-------------|---------| -| **99%** | ~87.6 小时 | ~7.2 小时 | 内部工具(几乎无意义) | -| **99.9% ("三个九")** | ~8.76 小时 | ~43 分钟 | 大多数后端服务 ✅ | -| **99.95%** | ~4.38 小时 | ~21 分钟 | 核心交易链路 | -| **99.99% ("四个九")** | ~52.6 分钟 | ~4.3 分钟 | 金融级 / 支付系统 | -| **99.999%** | ~5.26 分钟 | ~26 秒 | 电信级,极难实现 | - -> [!warning] "三个九"是底线 -> -> 如果团队宣称 SLO = 99%,这意味着每月可以容忍近 **7 小时的不可用**——这在生产环境中基本等于没有可用性目标。 - -## Error Budget (错误预算) 深度解析 - -### 计算方式 - -``` -可用率 = (总时间 - 故障时间) / 总时间 × 100% -错误预算 = 1 - SLO - -SLO = 99.9% → 错误预算 = 0.001 - 每月可容忍故障 = 30 × 24 × 60 × 0.001 = 43.2 分钟 - 每周可容忍故障 = 7 × 24 × 60 × 0.001 = 10.1 分钟 - -SLO = 99.99% → 错误预算 = 0.0001 - 每月可容忍故障 ≈ 4.3 分钟 -``` - -### 基于错误预算的决策矩阵 - -```mermaid -flowchart TD - Ratio["错误预算使用率 = 已消耗 / 总预算"] - - Ratio -- "< 50%" --> Green["🟢 绿灯阶段
预算充足: 可以大胆发布
新功能、尝试激进方案
常规发布节奏"] - Ratio -- "50~90%" --> Yellow["🟡 黄灯阶段
预算紧张: 收紧发布频率
增加人工审查
暂停非关键功能开发"] - Ratio -- "\> 90%" --> Orange["🟠 橙灯阶段
严重不足: 冻结发布
专注稳定性修复
全员 On-Call"] - Ratio -- "\> 100%" --> Red["🔴 红灯阶段
预算耗尽: 禁止所有
功能性变更
SLO 事故复盘"] - - style Green fill:#c8e6c9 - style Yellow fill:#fff9c4 - style Orange fill:#ffe0b2 - style Red fill:#ffcdd2 -``` - -### 错误预算告警联动 - -```yaml -# Prometheus 告警规则示例 -groups: - - name: error-budget - rules: - - alert: ErrorBudgetBurnRateHigh - expr: | - sum(rate(http_requests_total{status=~"5.."}[1h])) / - sum(rate(http_requests_total[1h])) > 0.001 - for: 1h - labels: - severity: warning - budget_phase: yellow - annotations: - summary: "错误预算消耗加速" - message: "当前错误率 {{ $value }}% > SLO 阈值 0.1%" - - - alert: ErrorBudgetExhausted - expr: | - (sum(rate(http_requests_total{status=~"5.."}[24h])) / - sum(rate(http_requests_total[24h]))) >= 0.001 - labels: - severity: critical - budget_phase: red - annotations: - summary: "错误预算已耗尽!" - message: "本月 SLO 已破线,冻结非紧急变更" -``` - -## SRE 心法 +## SRE 五条心法 1. **用户视角定义 SLO** — 不是"API P99 < 200ms",而是"用户在正常网络下加载页面 < 2s" 2. **错误预算用完 = 停止功能开发** — 全力修 bug、加稳定性 @@ -121,29 +41,24 @@ groups: 4. **自动化一切重复劳动** — 手动操作一定会出错 5. **接受一定程度的失败** — 在可控范围内快速迭代,比追求完美更重要 -## Blameless Postmortem 模板 +## Error Budget 计算速查 -| 字段 | 填写内容 | -|------|---------| -| **事件名称** | `2026-05-05 支付服务超时事故` | -| **影响范围** | 约 15% 的支付请求失败,持续 23 分钟 | -| **发现时间** | 14:32 (On-Call 接到告警) | -| **恢复时间** | 14:55 (回滚后确认恢复) | -| **根本原因** | 某次变更后支付网关的连接池大小从 50 降到 10 | -| **时间线** | 14:00 发布 v1.2.3 → 14:30 错误率开始升高 → 14:32 收到告警 → 14:35 开始排查 → 14:45 定位根因 → 14:50 回滚 → 14:55 完全恢复 | -| **改进行动** | ① 连接池参数变动必须经过压测验证;② 监控中补充连接池活跃数指标 | -| **跟进人** | @zhangsan (行动 ①)、@lisi (行动 ②) | +| SLO | 月预算 | 周预算 | 日预算 | +|-----|--------|--------|--------| +| 99% | ~5h | ~72min | ~14min | +| 99.9% | ~43min | ~10min | ~1.4min | +| 99.95% | ~21min | ~5min | ~43s | +| 99.99% | ~4.3min | ~1min | ~8.6s | +| 99.999% | ~26s | ~6s | <1s | ## SRE Metrics 看板 -除了 SLO,SRE 还需要关注以下运营指标: - -| 指标 | 说明 | 目标 | -|------|------|------| -| **MTTR** | Mean Time To Recovery | 核心服务 < 30min | +| 指标 | 说明 | 精英级目标 | +|------|------|-----------| +| **MTTR** | Mean Time To Recovery | < 1h | | **Change Failure Rate** | 变更导致故障的比例 | < 5% | -| **Lead Time for Changes** | 代码提交到上线的时间 | < 2h | -| **Deployment Frequency** | 日均部署次数 | > 5 (大规模团队) | +| **Lead Time for Changes** | 代码提交到上线的时间 | < 1h | +| **Deployment Frequency** | 部署频率 | 每日多次 | > [!tip] DORA Metrics > @@ -155,6 +70,7 @@ groups: ## 关联笔记 -- [[04-可观测性/04-告警管理]] — SLO/Error Budget 与告警体系的联动 -- [[05-部署运维/02-Kubernetes]] — K8s HPA 弹性伸缩支撑 SLO 保障 -- [[05-部署运维/03-CICD与GitOps]] — GitOps 支持安全的持续交付 +- [[04-SRE实践/01-SLI-SLO-SLA详解]] — SLI/SLO/SLA 三层模型基础 +- [[04-SRE实践/02-错误预算深度解析]] — Error Budget 告警规则示例 +- [[hhs/MS/05-部署运维/02-Kubernetes]] — K8s HPA 弹性伸缩支撑 SLO 保障 +- [[hhs/MS/05-部署运维/03-CICD与GitOps]] — GitOps 支持安全的持续交付 diff --git a/hhs/MS/05-部署运维/04-SRE实践/01-SLI-SLO-SLA详解.md b/hhs/MS/05-部署运维/04-SRE实践/01-SLI-SLO-SLA详解.md new file mode 100644 index 0000000..5a9b88f --- /dev/null +++ b/hhs/MS/05-部署运维/04-SRE实践/01-SLI-SLO-SLA详解.md @@ -0,0 +1,77 @@ +--- +tags: [sre, slo, sli, sla, service-level] +create time: 2026-05-18 00:50 +--- + +# SLI / SLO / SLA 详解 — 可靠性量化基础 + +## 概述 + +站点可靠性工程 (SRE) 把运维问题看作**软件工程问题**。它的核心理念是用 SLI/SLO/SLA 来量化服务质量,避免"我觉得系统很慢"这类模糊描述。本文将详细拆解三层模型和实际应用方法。 + +```mermaid +flowchart LR + User["用户体验"] --> SLI{"实际测量"} + SLI -->|"达标"| SLO_OK["✅ SLO 达成"] + SLI -->|"不达标"| Budget["消耗错误预算"] + + Budget --> Left{"预算剩余?"} + Left -- "> 50%" --> Ship["快速迭代 🚀"] + Left -- "< 10%" --> Stabilize["稳定优先 ❄️"] + + style User fill:#e3f2fd + style SLO_OK fill:#e8f5e9 + style Ship fill:#fff3e0 + style Stabilize fill:#ffebee +``` + +## SLI / SLO / SLA 三层模型 + +| 术语 | 全称 | 定义 | 谁制定 | 变更频率 | +|------|------|------|--------|---------| +| **SLI** | Service Level Indicator | 实际度量:用户请求的成功率是多少? | 观测系统自动产出 | 持续更新 | +| **SLO** | Service Level Objective | 内部目标:我们承诺达到 99.9% 可用性 | SRE + 研发 | 季度回顾 | +| **SLA** | Service Level Agreement | 对外契约:达不到就赔钱 | 法务 + 商务 | 按合同约定 | + +> [!question] 三者有什么区别? +> +> - **SLI** = 仪表盘上的真实数字("今天成功率 99.87%") +> - **SLO** = 内部团队的目标线("我们要 ≥ 99.9%") +> - **SLA** = 给客户的承诺条款("达不到赔偿 10% 月费") +> +> SLI < SLO → 团队内部关注;SLI < SLA → 客户开始投诉。 + +### 实用的类比 + +| 层级 | 生活类比 | 技术场景 | +|------|---------|---------| +| SLI | "我体重秤显示 72kg" | Prometheus 返回的 P99 延迟 230ms | +| SLO | "我要保持 70kg 以下" | 研发团队内部追求 P99 < 200ms | +| SLA | "健身合约:每周少于 3 次到店扣费" | 对客户提供 99.9% 可用性,否则退款 | + +## 实用性比例对照表 + +| SLO | 每年停机时间 | 每月停机时间 | 适用级别 | +|-----|-------------|-------------|---------| +| **99%** | ~87.6 小时 | ~7.2 小时 | 内部工具(几乎无意义) | +| **99.9% ("三个九")** | ~8.76 小时 | ~43 分钟 | 大多数后端服务 ✅ | +| **99.95%** | ~4.38 小时 | ~21 分钟 | 核心交易链路 | +| **99.99% ("四个九")** | ~52.6 分钟 | ~4.3 分钟 | 金融级 / 支付系统 | +| **99.999%** | ~5.26 分钟 | ~26 秒 | 电信级,极难实现 | + +> [!warning] "三个九"是底线 +> +> 如果团队宣称 SLO = 99%,这意味着每月可以容忍近 **7 小时的不可用**——这在生产环境中基本等于没有可用性目标。 + +## 补充:如何为你的服务设定合适的 SLO + +1. **从用户视角出发** — 不是"API P99 < 200ms",而是"用户在正常网络下加载页面 < 2s" +2. **参考行业标准** — 内部工具 99.9%,用户-facing 服务 99.95%+ +3. **考虑成本收益** — 从 99.9% 到 99.99% 可能需要 5~10 倍的基础设施投入 +4. **先设定保守目标再逐步提升** — 一个达不到的 SLO 比没有 SLO 更有害(团队会习惯打破它) +5. **绑定到具体指标** — 每个 SLO 必须有对应的 SLI 自动采集数据支撑 + +## 关联笔记 + +- [[../02-错误预算深度解析]] — Error Budget 的计算与应用 +- [[../hhs/MS/05-部署运维/04-SRE实践]] — SRE 总体概览 diff --git a/hhs/MS/05-部署运维/04-SRE实践/02-错误预算深度解析.md b/hhs/MS/05-部署运维/04-SRE实践/02-错误预算深度解析.md new file mode 100644 index 0000000..9da85cf --- /dev/null +++ b/hhs/MS/05-部署运维/04-SRE实践/02-错误预算深度解析.md @@ -0,0 +1,113 @@ +--- +tags: [sre, error-budget, budget-allocation, sre-alerting] +create time: 2026-05-18 00:50 +--- + +# Error Budget 深度解析 — 稳定性与速度的平衡杠杆 + +## 概述 + +Error Budget(错误预算)是 SRE 体系中最具实操价值的概念之一。它将抽象的"SLO 目标"转化为具体的可消耗量,为产品迭代的快慢提供量化依据。更多背景见 [[../04-SRE实践/01-SLI-SLO-SLA详解]]。 + +## 计算方式 + +``` +可用率 = (总时间 - 故障时间) / 总时间 × 100% +错误预算 = 1 - SLO + +SLO = 99.9% → 错误预算 = 0.001 + 每月可容忍故障 = 30 × 24 × 60 × 0.001 = 43.2 分钟 + 每周可容忍故障 = 7 × 24 × 60 × 0.001 = 10.1 分钟 + +SLO = 99.99% → 错误预算 = 0.0001 + 每月可容忍故障 ≈ 4.3 分钟 +``` + +> [!tip] 理解错误预算的本质 +> +> 错误预算不是"允许出错的额度",而是"**允许不完美的空间**"。它回答了一个关键问题:在当前的可靠性要求下,团队还能冒多大的风险? + +## 基于错误预算的决策矩阵 + +```mermaid +flowchart TD + Ratio["错误预算使用率 = 已消耗 / 总预算"] + + Ratio -- "< 50%" --> Green["🟢 绿灯阶段
预算充足: 可以大胆发布
新功能、尝试激进方案
常规发布节奏"] + Ratio -- "50~90%" --> Yellow["🟡 黄灯阶段
预算紧张: 收紧发布频率
增加人工审查
暂停非关键功能开发"] + Ratio -- "\> 90%" --> Orange["🟠 橙灯阶段
严重不足: 冻结发布
专注稳定性修复
全员 On-Call"] + Ratio -- "\> 100%" --> Red["🔴 红灯阶段
预算耗尽: 禁止所有
功能性变更
SLO 事故复盘"] + + style Green fill:#c8e6c9 + style Yellow fill:#fff9c4 + style Orange fill:#ffe0b2 + style Red fill:#ffcdd2 +``` + +### 各阶段的典型行动清单 + +| 阶段 | 功能发布 | 技术债处理 | 人员安排 | +|------|---------|-----------|---------| +| 🟢 绿灯 | 正常节奏 | 按计划推进 | 按需 On-Call | +| 🟡 黄灯 | 减少 50%,需额外审批 | 启动专项清理 | 增加第二响应人 | +| 🟠 橙灯 | 冻结(仅 Hotfix) | 全员投入修复 | 负责人值班 | +| 🔴 红灯 | 全面冻结 | Postmortem + 专项改进 | 全员待命 | + +## 错误预算告警联动 + +```yaml +# Prometheus 告警规则示例 +groups: + - name: error-budget + rules: + - alert: ErrorBudgetBurnRateHigh + expr: | + sum(rate(http_requests_total{status=~"5.."}[1h])) / + sum(rate(http_requests_total[1h])) > 0.001 + for: 1h + labels: + severity: warning + budget_phase: yellow + annotations: + summary: "错误预算消耗加速" + message: "当前错误率 {{ $value }}% > SLO 阈值 0.1%" + + - alert: ErrorBudgetExhausted + expr: | + (sum(rate(http_requests_total{status=~"5.."}[24h])) / + sum(rate(http_requests_total[24h]))) >= 0.001 + labels: + severity: critical + budget_phase: red + annotations: + summary: "错误预算已耗尽!" + message: "本月 SLO 已破线,冻结非紧急变更" +``` + +> [!info] Multi-window Burn Rate Alerting +> +> Google SRE 推荐的做法是使用**两个窗口**来检测预算消耗速度: +> - **短窗口(1h/5m)**:检测突发大量错误的情况(立即触发黄灯) +> - **长窗口(半天/一天)**:检测慢性缓慢泄漏的情况(防止温水煮青蛙) +> +> 只有当两个窗口同时超限时才亮红灯,这样可以避免误报导致的警报疲劳。 + +## 补充:多服务 Error Budget 管理策略 + +当集群中有几十上百个服务时,不能把所有预算分配给同一个 SLO。推荐分层策略: + +| 层级 | 示例 | 预算分配 | +|------|------|---------| +| P0 核心链路 | 支付、登录 | 99.99% → 极低容错 | +| P1 重要服务 | 订单、搜索 | 99.95% → 中等容错 | +| P2 一般服务 | 后台管理、报表 | 99.9% → 较高容错 | + +> [!tip] Error Budget 分配实战技巧 +> +> 将月度预算**均匀摊分到每周**作为基线。例如月预算 43 分钟 → 每周预算约 10 分钟。这比月末一次性清算更有操作性——你可以在周报中直接看到本周预算余量。 + +## 关联笔记 + +- [[../04-SRE实践/01-SLI-SLO-SLA详解]] — SLI/SLO/SLA 三层模型基础 +- [[../04-SRE实践/03-SRE心法与事故复盘]] — Blameless Postmortem 模板 +- [[../hhs/MS/05-部署运维/02-Kubernetes]] — K8s HPA 弹性伸缩支撑 SLO 保障 diff --git a/hhs/MS/05-部署运维/04-SRE实践/03-SRE心法与事故复盘.md b/hhs/MS/05-部署运维/04-SRE实践/03-SRE心法与事故复盘.md new file mode 100644 index 0000000..e7d7919 --- /dev/null +++ b/hhs/MS/05-部署运维/04-SRE实践/03-SRE心法与事故复盘.md @@ -0,0 +1,114 @@ +--- +tags: [sre, postmortem, blameless, dora-metrics, incident-management] +create time: 2026-05-18 00:50 +--- + +# SRE 心法与事故复盘 — 文化和方法论 + +## 概述 + +SRE 不仅是技术指标和告警规则,更是一种工作文化。本章介绍 Google SRE 提出的五条核心心法,以及事故复盘的标准流程和 DORA Metrics 效能评估框架。更多 SLO/Error Budget 理论参见 [[../04-SRE实践/01-SLI-SLO-SLA详解]]。 + +## SRE 五条心法 + +### 1. 用户视角定义 SLO + +> 不是"API P99 < 200ms",而是"用户在正常网络下加载页面 < 2s"。 + +技术指标可能欺骗你——P99 延迟低不代表用户体验好。一个前端图片加载慢 3 秒的服务,后端再快也无法让用户满意。**永远从用户的感知出发定义质量**。 + +### 2. 错误预算用完 = 停止功能开发 + +当 Error Budget 耗尽时,团队必须全力修 bug、加稳定性,而不是继续发新功能。这不是惩罚,而是**资源重新分配的客观依据**。 + +### 3. Blameless Postmortem + +不问"谁干的",问"流程哪里可以改进"。责备个人只会让团队成员隐瞒问题,最终酿成更大的事故。 + +### 4. 自动化一切重复劳动 + +手动操作一定会出错。无论是回滚、扩容还是配置变更,能脚本化的绝不人工执行。这不仅仅是效率问题,更是**可靠性问题**。 + +### 5. 接受一定程度的失败 + +在可控范围内快速迭代,比追求完美更重要。Google 的"可控失败"理念认为:**小规模的失败是发现系统性问题的最佳途径**。 + +## Blameless Postmortem 模板 + +| 字段 | 填写内容 | +|------|---------| +| **事件名称** | `2026-05-05 支付服务超时事故` | +| **影响范围** | 约 15% 的支付请求失败,持续 23 分钟 | +| **发现时间** | 14:32 (On-Call 接到告警) | +| **恢复时间** | 14:55 (回滚后确认恢复) | +| **根本原因** | 某次变更后支付网关的连接池大小从 50 降到 10 | +| **时间线** | 14:00 发布 v1.2.3 → 14:30 错误率开始升高 → 14:32 收到告警 → 14:35 开始排查 → 14:45 定位根因 → 14:50 回滚 → 14:55 完全恢复 | +| **改进行动** | ① 连接池参数变动必须经过压测验证;② 监控中补充连接池活跃数指标 | +| **跟进人** | @zhangsan (行动 ①)、@lisi (行动 ②) | + +> [!tip] 什么是真正的 "Blameless"? +> +> Blameless ≠ 不追究责任。它的意思是:**聚焦于系统和流程缺陷,而非个人的失误**。人类会犯错,这是物理定律级别的现实。好的 Postmortem 应该揭示为什么系统设计允许一个简单的配置变更导致大规模故障。 + +### Postmortem 写作原则 + +| 原则 | 做法 | +|------|------| +| **事实先行** | 按时间线罗列发生了什么,不掺入主观判断 | +| **5 Whys 分析法** | 连续问 5 次"为什么",直到触及根因 | +| **行动项可追踪** | 每个改进措施都要有负责人和截止日期 | +| **公开透明** | Postmortem 全文共享给全公司,不隐藏信息 | +| **闭环验证** | 改进措施完成后回顾是否真正预防了同类问题 | + +## SRE Metrics 看板 + +除了 SLO,SRE 还需要关注以下运营指标: + +| 指标 | 说明 | 目标 | +|------|------|------| +| **MTTR** | Mean Time To Recovery | 核心服务 < 30min | +| **Change Failure Rate** | 变更导致故障的比例 | < 5% | +| **Lead Time for Changes** | 代码提交到上线的时间 | < 2h | +| **Deployment Frequency** | 日均部署次数 | > 5 (大规模团队) | + +> [!tip] DORA Metrics +> +> Google 提出的四大 DevOps 指标,广泛用于评估工程效能: +> - **部署频率** — 交付速度 +> - **变更前置时间** — 从代码提交到生产部署需要多久 +> - **变更失败率** — 多少部署导致了故障或回滚 +> - **MTTR** — 恢复服务的平均时间 + +### 四种绩效等级的 DORA 表现 + +| 等级 | 部署频率 | 变更前置时间 | 变更失败率 | MTTR | +|------|---------|-------------|-----------|------| +| 🏆 精英 (Elite) | 每日多次 | < 1 小时 | < 5% | < 1 小时 | +| 🥈 高 (High) | 每周 1-6 次 | < 1 周 | 6-15% | 1-7 天 | +| 🥉 中 (Medium) | 每月 < 1 次 | 1-4 周 | 16-30% | > 1 个月 | +| ⬜ 低 (Low) | < 每月 1 次 | > 6 个月 | > 30% | > 6 个月 | + +> [!info] 关于 DORA 的提醒 +> +> DORA Metrics 的价值在于**纵向对比自身演进**,而非横向攀比。一个创业团队追求"精英级"可能过度工程化。关键是看趋势——如果你的 Change Failure Rate 从 20% 降到了 10%,这就是实质性的进步。 + +## 补充:事故分级标准 + +```mermaid +flowchart TD + P0["🔴 P0 - 灾难性
全站不可用 / 资金损失
CTO 级别通知
2h内必须恢复"] --> P1 + P1["🟠 P1 - 严重
核心功能受影响
负责人 + SRE 紧急会议
4h内必须恢复"] --> P2 + P2["🟡 P2 - 一般
部分功能降级
工作时间处理
24h内必须恢复"] --> P3 + P3["🟢 P3 - 轻微
非核心问题
下个迭代处理"] + + style P0 fill:#ffcdd2 + style P1 fill:#ffe0b2 + style P2 fill:#fff9c4 + style P3 fill:#c8e6c9 +``` + +## 关联笔记 + +- [[../04-SRE实践/01-SLI-SLO-SLA详解]] — SLI/SLO/SLA 三层模型 +- [[../04-SRE实践/02-错误预算深度解析]] — Error Budget 告警联动 +- [[../hhs/MS/05-部署运维/02-Kubernetes]] — K8s 自愈能力与 SLO 保障 diff --git a/hhs/MS/05-部署运维/README.md b/hhs/MS/05-部署运维/README.md index 4490150..d6ad63e 100644 --- a/hhs/MS/05-部署运维/README.md +++ b/hhs/MS/05-部署运维/README.md @@ -23,20 +23,23 @@ graph LR style D fill:#e8f5e9 ``` -| # | 主题 | 核心问题 | -|---|------|----------| -| 1 | [[05-部署运维/01-容器化]] | Docker 镜像怎么优化?最佳实践有哪些? | -| 2 | [[05-部署运维/02-Kubernetes]] | K8s 核心概念和资源管理怎么做? | -| 3 | [[05-部署运维/03-CICD与GitOps]] | 自动化流水线怎么设计?GitOps 流程怎么走? | -| 4 | [[05-部署运维/04-SRE实践]] | SLI/SLO/Error Budget 如何落地? | +## 知识体系详情 + +| # | 主题 | 核心问题 | 定位 | +|---|------|----------|------| +| 1 | [[05-部署运维/01-容器化]] | Docker 镜像怎么优化?多架构如何构建? | **枢纽页** → 3 个子文档(Dockerfile / 多架构 / 安全) | +| 2 | [[05-部署运维/02-Kubernetes]] | K8s 核心概念和资源管理怎么做? | **枢纽页** → 7 个子文档(核心概念 / 配置 / 网络 / 扩缩容 / 调度 / 安全 / 排查) | +| 3 | [[05-部署运维/03-CICD与GitOps]] | 自动化流水线怎么设计?GitOps 流程怎么走? | **枢纽页** → 4 个子文档(CICD / GitOps / Helm / 安全与发布) | +| 4 | [[05-部署运维/04-SRE实践]] | SLI/SLO/Error Budget 如何落地?事故复盘怎么做? | **枢纽页** → 3 个子文档(SLI-SLO / ErrorBudget / SRE心法) | ### 学习建议 > [!tip] 学习路径 +> > 先掌握容器化(Docker),再学 K8s 核心概念,CI/CD 和 SRE 可以在实践中逐步深入。 ### 关联笔记 -- [[02-服务治理]] — K8s Service 提供原生的服务发现和负载均衡 -- [[04-可观测性]] — K8s Liveness/Readiness Probe 是可观测性的基础 +- [[hhs/MS/02-服务治理]] — K8s Service 提供原生的服务发现和负载均衡 +- [[hhs/MS/04-可观测性]] — K8s Liveness/Readiness Probe 是可观测性的基础 - [[hzh/MS/README.md]] — 完整微服务知识索引