diff --git a/hzh/MS/01-基础概念.md b/hzh/MS/01-基础概念.md new file mode 100644 index 0000000..f953350 --- /dev/null +++ b/hzh/MS/01-基础概念.md @@ -0,0 +1,106 @@ +--- +tags: [microservice, architecture] +create time: 2026-04-29 12:30 +--- + +# 微服务基础概念 + +## 概述 + +本文阐述微服务架构的核心定义、设计原则与常见误区,帮助建立正确的认知框架。 + +## 什么是微服务 + +> [!definition] 核心定义 +> **微服务**(Microservices)是一种将单一应用拆分为一组**小服务**的架构风格。每个服务运行在独立进程中,通过轻量级通信机制协作,且**各自拥有独立的数据库**。 + +关键特征: + +| 特征 | 说明 | +|------|------| +| 独立部署 | 每个服务可以独立构建、测试和发布 | +| 去中心化 | 数据管理、技术选型由各团队自主决策 | +| 容错性 | 服务故障不影响整个系统 | +| 组织对齐 | 按业务域划分,与 Conway 定律呼应 | + +### 单体 vs 微服务 + +```mermaid +graph TB + subgraph Monolith["单体架构"] + UI["Web UI"] + API["API Layer"] + Biz["Business Logic"] + DB["数据库"] + UI --> API --> Biz --> DB + end + + subgraph Microservices["微服务架构"] + GW["API Gateway"] + S1["Service A
+ Database"] + S2["Service B
+ Database"] + S3["Service C
+ Database"] + GW --> S1 + GW --> S2 + GW --> S3 + S2 -.->|RPC/MQ| S1 + end +``` + +> [!question] 思考 +> 既然微服务有这么多好处,为什么不是所有公司都迁移到微服务?什么时候单体反而更合适? + +**答案线索**:微服务引入的是**分布式复杂度**。网络延迟、数据一致性、运维成本全部上来了。对于小型团队或小规模应用,单体更简单高效。 + +## 拆分原则:DDD 与限界上下文 + +微服务拆分的核心思想来自 **领域驱动设计 (DDD)** —— 按 **限界上下文 (Bounded Context)** 划分。 + +```mermaid +graph LR + UC["用户中心"] + OS["订单服务"] + PS["支付服务"] + IS["库存服务"] + UC -->|查询/注册| OS + OS -->|创建支付| PS + OS -->|扣减库存| IS + PS -->|支付回调| OS + IS -->|库存确认| OS +``` + +> [!tip] 限界上下文不是代码边界,而是语义边界 +> 同一个词在不同上下文中可能有完全不同的含义。比如 "商品" 在电商场景中是一套完整对象,但在物流场景中可能只是一个包裹编号。明确上下文是 DDD 的第一步。 + +> [!warning] 反模式 +> - ❌ **按技术层拆分**:Controller 层一个服务、Service 层一个服务 — 这不是微服务,是"分布式单体" +> - ✅ **按业务域拆分**:每个服务对应一个业务能力边界 + +## 核心设计原则 + +> [!summary] SOLID + CAP 的组合拳 +> +> | 原则 | 含义 | 微服务场景 | +> |------|------|-----------| +> | 单一职责 | 一个服务只做一件事 | 订单服务只管订单生命周期 | +> | 高内聚低耦合 | 内部紧密,外部松散 | 服务间通过接口通信,不共享代码 | +> | 最终一致性 | 允许短暂不一致 | 分布式环境放弃强一致性,换取可用性 | +> | 独立失败 | 故障隔离 | 单个服务宕机不影响全局 | + +## 何时不应该用微服务 + +> [!failure] 拒绝伪需求 +> +> - 团队小于 10 人,维护单个应用就够 +> - 项目处于快速原型阶段,需求频繁变更 +> - 团队成员缺乏分布式系统设计经验 +> - 已有大型单体正在稳定运行 + +**记住**:架构没有银弹。微服务是为了解决**特定规模下的组织和工程问题**而生的。先问自己:"我的痛点是什么?"再决定是否值得付出代价。 + +> [!note] 后续延伸 +> +> - [[02-服务治理]] — 服务间通信、负载均衡、熔断降级等治理机制 +> - [[03-数据一致性]] — 独立数据库架构下的分布式事务方案 +> - [[04-可观测性]] — 日志、指标、链路追踪三支柱 +> - [[05-部署运维]] — 容器化编排、灰度发布、弹性伸缩 diff --git a/hzh/MS/02-服务治理.md b/hzh/MS/02-服务治理.md new file mode 100644 index 0000000..d7a3c6b --- /dev/null +++ b/hzh/MS/02-服务治理.md @@ -0,0 +1,510 @@ +--- +tags: [microservice, service-governance, api-gateway, circuit-breaker, service-discovery, distributed-tracing, canary-deployment] +create time: 2026-04-29 12:02 +--- + +# 服务治理 + +## 概述 + +服务治理是微服务架构的"操作系统"——它不直接实现业务逻辑,但决定了系统能否在复杂、高并发、多团队协作的环境中稳定运转。 + +本文覆盖服务治理的 **七大核心能力**: + +```mermaid +quadrantChart + title 服务治理能力矩阵 + x-axis Low Impact --> High Impact + y-axis Foundational --> Advanced + "服务发现": [0.25, 0.2] + "负载均衡": [0.45, 0.25] + "API Gateway": [0.35, 0.4] + "熔断降级": [0.55, 0.5] + "限流": [0.65, 0.6] + "链路追踪": [0.5, 0.75] + "配置中心": [0.4, 0.7] + "灰度发布": [0.75, 0.9] +``` + +> [!question] 开篇思考 +> 假设你有 10 个微服务,每个服务有 3 个实例,总共 30 个服务实例。手动维护它们的地址列表会带来哪些问题?你能想到几种解决方案? + +答案藏在下面每一个章节里——从服务发现到流量治理,每一层都在解决特定维度的复杂度。 + +## 服务发现 + +服务实例的动态变化(扩容、缩容、故障重启)让硬编码地址成为不可能。服务要回答的核心问题是:**我怎么找到你?** + +### 两种模式 + +```mermaid +graph TB + subgraph Client["客户端发现模式"] + A[Service A] -->|1.查询注册中心| R1[(Registry)] + R1 -->|2.返回实例列表| A + A -->|3.自选实例直连| B[Service B] + end + + subgraph Server["服务端发现模式"] + C[Service C] -->|请求| P[LoadBalancer Proxy] + P -->|查询注册中心| R2[(Registry)] + R2 -->|返回实例| P + P -->|转发| D[Service D] + end +``` + +**对比**: + +| 维度 | 客户端发现 | 服务端发现 | +|------|-----------|-----------| +| 典型实现 | Eureka、Consul、Nacos | Nginx、Envoy、K8s Service | +| 耦合度 | 客户端嵌入注册逻辑 | 透明代理,客户端无感知 | +| 性能 | 直连调用,延迟最低 | 多一跳代理开销 | +| 运维复杂度 | 每门语言需独立 SDK | 统一部署代理,技术无关 | + +> [!tip] 推荐路径 +> Kubernetes 环境下直接用 **K8s Service + Ingress**;非容器环境优先考虑 **Nacos**(阿里开源,国内生态好)。 +> 如果团队已有 Consul 基础设施,无需迁移——它在中小规模集群中表现优秀。 + +### 实战:Nacos 服务发现 + +```go +// Nacos 服务注册 & 拉取 +import ( + "github.com/nacos-group/nacos-sdk-go/v2/clients" + "github.com/nacos-group/nacos-sdk-go/v2/common/constant" +) + +// 创建注册客户端 +sc := constant.ServerConfig{ + IpAddr: "127.0.0.1", + Port: 8848, +} + +// 注册服务实例 +client, _ := clients.NewNamingClient( + value_map.NewValueMap(map[string]any{"serverConfig": sc}), +) + +client.RegisterInstance(naming_param.RegisterInstanceParam{ + Ip: "10.0.0.1", + Port: 8080, + ServiceName: "order-service", + ClusterName: "DEFAULT", +}) + +// 拉取某服务的可用实例列表 +instances, _ := client.SelectInstances(naming_param.SelectInstanceParam{ + ServiceName: "order-service", +}) +``` + +> [!warning] 注意 +> Nacos 同时支持临时实例(故障自动摘除)和持久实例(基于 etcd)。生产环境中临时实例更常见——节点挂了会自动从列表中剔除。 + +**关键问题**:服务下线时,如何确保正在处理的请求不会被打断?这引出了健康检查和优雅关闭的概念。 + +## API Gateway + +API Gateway 是所有外部请求的统一入口,承担以下职责: + +```mermaid +graph LR + Client["客户端"] --> GW["API Gateway"] + GW --> Auth["鉴权 & 限流"] + GW --> Route["路由转发"] + GW --> Transform["协议转换"] + GW --> Log["日志 & 监控"] + GW -.-> CB[(配置中心)] +``` + +常见实现:**Kong、APISIX、Spring Cloud Gateway、Nginx**。 + +### 网关放什么? + +> [!summary] 网关职责清单 +> +> - ✅ 鉴权与认证 +> - ✅ 限流熔断 +> - ✅ HTTPS 终结 +> - ✅ 请求/响应转换 +> - ✅ 路由分发 +> - ❌ 业务逻辑 — 网关保持瘦,重逻辑应下沉到业务服务 + +> [!question] 权衡题 +> 有人说:"鉴权放到网关统一做,避免每个服务重复写。"但如果某个内部服务不需要鉴权呢?或者不同团队要求不同的鉴权方式呢?你怎么设计才能兼顾统一性和灵活性? + +## 服务间通信 + +### RPC vs RESTful API + +| 维度 | REST over HTTP/JSON | gRPC (HTTP/2) | +|------|---------------------|---------------| +| 性能 | 序列化开销大 | Protocol Buffers,二进制高效 | +| 人类可读 | URL + JSON 直观 | 需要 Proto 定义辅助理解 | +| 语言兼容性 | 广泛,任何能发 HTTP 的语言都能用 | 需要代码生成 | +| 适用场景 | 对外 API、跨团队/跨组织调用 | 内部服务间高频强类型调用 | + +```go +// Go 示例:gRPC Protobuf 定义 +// proto/order/v1/order.proto +syntax = "proto3"; +package order.v1; + +service OrderService { + rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse); + rpc GetOrder(GetOrderRequest) returns (Order); +} + +message CreateOrderRequest { + string user_id = 1; + repeated Item items = 2; + float total = 3; +} + +message Item { + string product_id = 1; + int32 quantity = 2; +} +``` + +### 同步 vs 异步 + +> [!question] 关键时刻的判断 +> 用户点击"提交订单"后,系统要做:① 创建订单记录 ② 扣减库存 ③ 扣款 ④ 发送短信通知。哪些步骤必须同步完成?哪些可以异步处理?为什么? + +**答案**:①~③需要立即得到结果或保证一致性,走同步流程;④纯粹的通知类场景完全异步,用消息队列解耦。即使扣款失败,短信也没必要发出。 + +```go +// 异步消息:Go + RabbitMQ 简易示例 +producer.Publish("orders", []byte(`{ + "orderId": "ORD-2026-001", + "userId": "user-42", + "amount": 199.00 +}`)) + +// 消费者监听 +messages := consumer.Consume("order-notifications") +for msg := range messages { + sendSms(msg.OrderId) // 不阻塞主流程 +} +``` + +> [!tip] 选型建议 +> 事件驱动架构(Event-driven)适合「最终一致性」场景;强一致事务需求仍依赖 SAGA/TCC 等分布式事务方案。 + +## 容错模式 + +微服务最大的挑战是 **网络不可靠**。分布式系统的每个远程调用都可能超时、被拒绝、或部分成功。必须提前设计降级和恢复策略。 + +### 重试机制 + +> [!warning] 关键原则 +> 只对 **幂等操作** 重试!POST 创建操作直接重试会导致重复数据。GET、PUT(同参数)适合重试。 + +```go +// exponential backoff + jitter(防雪崩) +func retryWithBackoff(ctx context.Context, maxRetries int, fn func() error) error { + for attempt := uint(0); attempt <= uint(maxRetries); attempt++ { + if err := fn(); err != nil { + if attempt == uint(maxRetries) { + return err + } + // jitter: 随机偏移,避免所有客户端同时重试造成二次冲击 + jitter := time.Duration(rand.Int63n(int64(time.Millisecond * 100))) + wait := time.Duration(math.Pow(2, float64(attempt)))*time.Second + jitter + select { + case <-ctx.Done(): + return ctx.Err() + case <-time.After(wait): + } + } else { + return nil + } + } + return nil +} +``` + +### 熔断器 (Circuit Breaker) + +当下游服务频繁失败时,快速失败避免线程堆积雪崩: + +```mermaid +stateDiagram-v2 + [*] --> Closed: "正常状态" + Closed --> Open: "失败率超过阈值" + Open --> HalfOpen: "等待探测间隔" + HalfOpen --> Closed: "探测成功" + HalfOpen --> Open: "探测失败" +``` + +```go +// gobreaker 示例 +cb := state.NewCB(state.Settings{ + Name: "UserService", + MaxElems: 10, + WaitRetry: 5 * time.Second, + ReadyToTrip: func(counts Counters) bool { + return counts.Failures > 5 + }, +}) + +result := cb.Execute(doRequest) +if result.Err != nil { + // 熔断打开,立即短路返回错误,不调用下游 +} +``` + +### 健康检查 + +```mermaid +sequenceDiagram + participant LB as 负载均衡器 + participant S1 as 服务实例 A + participant S2 as 服务实例 B + + LB->>S1: "GET /health -> 200 OK" + LB->>S2: "GET /health -> 503 ERROR" + Note over LB: "标记 S2 不健康,剔除出池" + LB->>S2: "GET /health -> 200 OK" + Note over LB: "逐步恢复,重新加入池" +``` + +- **存活探针 (Liveness)**:判断"进程是否还活着",挂了则重启 +- **就绪探针 (Readiness)**:判断"是否能接收流量",未就绪则剔除负载均衡池 +- ** readiness 比 liveness 更重要**——一个进程活着但数据库连接耗尽时,应该停止接收流量而不是重启 + +> [!summary] 舱壁隔离 (Bulkhead) +> +> 为不同下游服务分配独立的线程池/连接池: +> +> ```go +> // poolgroup 示例:为不同服务隔离连接池 +> orderPool := pool.New(10, 100) // 最多10个活跃,上限100 +> userPool := pool.New(5, 50) +> // 订单服务占满连接不会影响用户服务的调用 +> ``` +> +> 超时 + 熔断 + 舱壁三者组合,构成了经典的稳定性防护三件套。 + +## 负载均衡 + +```mermaid +flowchart LR + subgraph "客户端负载均衡 (Client-Side)" + A[Service A] --> RR[轮询] + A --> LH[最少连接] + A --> CH[一致性哈希] + end + + subgraph "服务端负载均衡 (Server-Side)" + B[Service B] --> VIP[[Virtual IP]] + VIP --> B1[B-1] + VIP --> B2[B-2] + VIP --> B3[B-3] + end +``` + +| 策略 | 说明 | 适用场景 | +|------|------|---------| +| Round Robin | 轮询分配 | 各实例负载相近,请求耗时均匀 | +| Least Connections | 最少连接优先 | 长连接场景,请求处理时间差异大 | +| Consistent Hash | 哈希路由,同一 key 始终到同实例 | 有状态缓存、会话绑定场景 | +| Random | 随机选择 | 最简单但不一定最优 | + +> [!tip] K8s 中的实践 +> K8s Service 默认 Round Robin;如需更精细的控制,可配合 **Istio VirtualService** 实现基于权重/头部的流量分发。 + +## 配置管理 + +> [!question] 引出配置中心的必要性 +> 假设你的 50 个服务实例都要连同一个数据库。现在需要把数据库密码从 `password1` 改为 `password2`,你会怎么改?登录每台机器改配置文件?滚动重启所有 Pod?还是……有更好的办法? + +当服务实例数以百计时,手动管理配置是不可想象的。配置中心提供 **集中化、动态化、版本化** 的配置管理能力。 + +### 核心价值 + +```mermaid +flowchart LR + Dev["开发/运维"] --> CC[(配置中心)] + CC -->|热更新推送| S1[Service A] + CC -->|热更新推送| S2[Service B] + CC -->|热更新推送| S3[Service C] + + CC -.->|版本管理| V1[v1.0 历史配置] + CC -.->|版本管理| V2[v1.1 当前配置] +``` + +| 能力 | 说明 | +|------|------| +| 动态刷新 | 修改配置后立即生效,无需重启 | +| 环境隔离 | dev / test / prod 配置分离 | +| 版本管理与回滚 | 每次变更有迹可循,一键回滚 | +| 权限控制 | 敏感配置(密钥、Token)按角色隔离 | + +### Nacos Config 示例 + +```go +// 动态监听配置变更 +configClient, _ := clients.NewConfigClient(value_map.NewValueMap(map[string]any{ + "serverConfig": sc, +})) + +content, _ := configClient.GetConfig(config_param.GetConfigParam{ + DataId: "order-service.yaml", + Group: "DEFAULT_GROUP", +}) + +// 监听配置变化 +configClient.ListenChange(config_param.ListenChangeParam{ + DataId: "order-service.yaml", + Group: "DEFAULT_GROUP", + Callback: func(content string) { + fmt.Println("配置更新了:", content) + // 重新加载配置... + }, +}) +``` + +> [!note] 主流对比 +> +> - **Nacos Config**:阿里开源,国内首选,兼顾客户端发现+配置管理 +> - **Apollo**:携程开源,配置审核流程完善,适合大型团队 +> - **Spring Cloud Config**:与 Spring 生态深度集成,但需额外 Git/DB 存储后端 +> - **K8s ConfigMap**:原生方案,适合纯 K8s 环境,不支持热更新需配合 Sidecar + +## 分布式链路追踪 + +> [!question] 定位问题的困难 +> 用户反映下单慢,你的系统由订单、支付、库存、会员 4 个服务串联而成。没有工具的情况下,你要怎么知道是哪个服务拖慢了整体响应时间? + +单个服务的日志只能告诉你局部信息。分布式链路追踪将一次请求跨越多个服务的完整调用链串起来,形成全局视图。 + +### 核心概念 + +```mermaid +flowchart TB + Req["请求 trace_id=abc123"] --> Span1["订单服务 - 20ms"] + Span1 --> Span2["库存服务 - 80ms"] + Span1 --> Span3["会员服务 - 15ms"] + Span2 --> Span4["数据库查询 - 60ms"] + + style Span2 fill:#ff9999 +``` + +- **Trace**:一次完整请求的调用链 +- **Span**:链路中的一个执行片段(如一次 HTTP 调用、一次 SQL 查询) +- **Context Propagation**:通过 Header 传递 trace_id/span_id,贯穿整条链路 + +### 主流方案 + +| 方案 | 协议 | 存储后端 | 特点 | +|------|------|---------|------| +| **OpenTelemetry** | OTLP | Prometheus/Jaeger/Zipkin | CNCF 标准,厂商中立,未来趋势 | +| **Jaeger** | Jaeger native | Cassandra/Elasticsearch | Uber 开源,UI 友好 | +| **SkyWalking** | SkyWalking | MySQL/Elasticsearch | 国产,零侵入 Agent,中文文档完善 | + +### OpenTelemetry Go 示例 + +```go +// 初始化 tracer provider +provider := sdktrace.NewTracerProvider( + sdktrace.WithBatcherExporter(exporter), // 异步上报,不阻塞 +) + +tracer := provider.Tracer("order-service") + +// 在 handler 中创建 span +func handleOrder(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "handleOrder") + defer span.End() + + // 传播 context 到下游 + resp, err := callInventoryService(ctx) // SDK 自动注入 trace context + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return + } + w.Write(resp.Body) +} +``` + +> [!tip] 渐进式落地建议 +> 不要试图一开始就采集全部 Span。先从 **核心链路**(下单、支付)入手,采集 rate 设为 100%;稳定后再逐步扩展,日常采样率可降至 5%~10% 以节省存储。 + +## 灰度发布与流量治理 + +> [!question] 如何安全地上线? +> 你修复了一个紧急 Bug,但这次改动涉及核心支付链路。直接全量发布一旦出问题损失巨大。有没有办法先小范围验证,确认没问题再放量? + +灰度发布(也叫金丝雀发布)是降低变更风险的核心手段。它让你将新版本逐步推向一小部分用户,观察指标正常后再全量。 + +### 灰度策略 + +```mermaid +flowchart LR + subgraph "Canary Release" + A["1% 流量"] --> B{"指标正常?"} + B -- 是 --> C["10% 流量"] + B -- 否 --> D["自动回滚"] + C --> E{"指标正常?"} + E -- 是 --> F["50% 流量"] + E -- 否 --> D + F --> G{"指标正常?"} + G -- 是 --> H["100% 全量"] + G -- 否 --> D + end +``` + +### 灰度路由规则 + +```yaml +# Istio VirtualService 示例 +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: order-route +spec: + hosts: ["order-service"] + http: + # 灰度规则:header 携带 canary=true 的用户走 v2 + - match: + - headers: + x-canary: + exact: "true" + route: + - destination: + host: order-service + subset: v2 # 新版本 + weight: 100 + # 默认:全部走 v1(线上稳定版) + - route: + - destination: + host: order-service + subset: v1 # 旧版本 + weight: 100 +``` + +### 蓝绿 vs 灰度 + +> [!summary] 两种发布策略对比 +> +> | 维度 | 蓝绿部署 | 灰度发布 (金丝雀) | +> |------|---------|-----------------| +> | 切换方式 | 一次性全切 | 逐步放量 | +> | 资源消耗 | 双倍(新旧并行) | 初期少量副本 | +> | 回滚速度 | 秒级(切回即可) | 同样秒级 | +> | 风险等级 | 中高 | 低 | +> | 适合场景 | 大型变更、重大版本 | 日常迭代、高风险链路 | + +> [!tip] 实战经验 +> 无论哪种发布方式,**必须有可量化的观测指标作为决策依据**——错误率、P99 延迟、CPU/内存使用率。不要凭感觉放量,让数据说话。 + +## 关联笔记 + +- [[hzh/MS/01-基础概念]] — 微服务的整体架构认知 +- [[hzh/MS/05-部署运维]] — K8s Service 天然提供负载均衡和服务发现 +- [[hzh/MS/03-数据一致性]] — 微服务下的数据分片与一致性策略 +- [[hzh/MS/04-可观测性]] — 日志、指标、链路追踪的完整体系 diff --git a/hzh/MS/03-数据一致性.md b/hzh/MS/03-数据一致性.md new file mode 100644 index 0000000..b12426d --- /dev/null +++ b/hzh/MS/03-数据一致性.md @@ -0,0 +1,217 @@ +--- +tags: [microservice, distributed-system, database, data-consistency, saga-pattern, tcc, idempotency, outbox-pattern] +create time: 2026-04-29 12:03 +--- + +# 数据一致性 + +## 概述 + +微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这带来了分布式事务和数据一致性的经典难题。本文梳理主要解决方案及其取舍。 + +## 为什么不能共享数据库 + +```mermaid +graph LR + S1[订单服务 DB] + S2[库存服务 DB] + + subgraph BAD["反模式:共享数据库"] + S1 --- SHARED[(共享 DB)] + S2 --- SHARED + end +``` + +> [!failure] 反模式警告 +> 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是**分布式单体**。服务可以随意互相查询彼此的数据,失去了边界和自治性。 + +## 分布式事务方案概览 + +| 方案 | 一致性级别 | 性能 | 复杂度 | 适用场景 | +|------|-----------|------|--------|---------| +| 本地事务 + 事件通知 | 最终一致 | 高 | 低 | 绝大多数场景 | +| Saga 模式 | 最终一致 | 中 | 中 | 长流程业务 | +| TCC | 强最终一致 | 中 | 高 | 对一致性要求较高的场景 | +| AT 模式 (Seata) | 伪强一致 | 中低 | 低 | 不想改业务代码时 | + +> [!info] 选择策略 +> 先默认用 **本地事务 + 异步事件**(最简单、最高效),只有在业务明确需要 Saga 或 TCC 时才升级。 + +## 方案一:本地事务 + 消息队列 + +这是**最常用**的方案,核心思想是将"数据变更 + 发消息"合并到一个本地事务中。 + +```mermaid +sequenceDiagram + participant U as 用户 + participant O as 订单服务 + participant MQ as 消息队列 + participant I as 库存服务 + + U->>O: 创建订单 + O->>O: BEGIN 事务 + O->>O: 插入订单记录 + O->>MQ: 发送订单创建消息 + O->>O: COMMIT 事务 + MQ-->>I: 投递消息 + I->>I: 扣减库存 + I-->>O: 返回结果(可选) +``` + +### 实战:电商订单创建流程 + +以「用户下单」为例,这个流程涉及 **订单服务**(创建订单)和 **库存服务**(扣减库存),是典型的跨服务数据一致性问题: + +| 步骤 | 动作 | 一致性保障点 | +|------|------|-------------| +| 1 | 用户点击"提交订单" | 前端防重复提交(按钮置灰 / Token 机制) | +| 2 | 订单服务写入订单表 + 发送 `order.created` 消息 | **事务边界**:两件事必须在同一个本地事务中完成 | +| 3 | 消息队列投递给库存服务 | 保证消息至少一次投递(At-Least-Once) | +| 4 | 库存服务扣减库存 | **幂等性**:防止消息重投导致库存被扣两次 | +| 5 | 库存不足时回滚订单 | 通过 Sagas 补偿:已创建的订单标记为"取消" | + +> [!question] 思考 +> 如果步骤 2 的数据库写成功了,但 MQ 发消息失败了,会发生什么?用户看到下单成功但实际上库存没被锁定。这说明为什么必须用事务消息或本地消息表,而不是直接调 MQ API。 + +--- + +### 如何保证"事务内既写库又发消息"? + +**方案 A:事务消息**(推荐) +- RocketMQ /阿里云 MSE 原生支持事务消息 +- MQ 回查机制确保消息不丢 + +**方案 B:本地消息表** +- 在业务库里建一张 `outbox` 表 +- 业务事务同时写入业务数据和消息记录 +- 后台定时任务扫描未发送的消息推送出去 + +```go +// 本地消息表方案示意 +tx, _ := db.Begin() +// 1. 业务操作 +tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID) +// 2. 记录待发送消息(带状态字段) +tx.Exec("INSERT INTO outbox (topic, payload, status, created_at) VALUES (?, ?, 'pending', NOW())", "order.created", jsonPayload) +tx.Commit() +// 后台进程轮询 outbox 表中 status='pending' 的记录,发送到 MQ 后更新为 'sent' +``` + +#### Outbox 表建表示例 + +```sql +CREATE TABLE outbox ( + id BIGSERIAL PRIMARY KEY, + topic VARCHAR(255) NOT NULL, -- 消息主题 + payload JSONB NOT NULL, -- 消息体 + status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / sent / failed + error_msg TEXT, -- 失败原因 + created_at TIMESTAMP NOT NULL DEFAULT NOW(), + sent_at TIMESTAMP +); + +-- 加速定时扫描查询 +CREATE INDEX idx_outbox_pending ON outbox (status, created_at) WHERE status = 'pending'; +``` + +### 消费幂等性 + +消息可能重复投递(网络超时、MQ 重投),消费者必须做到**幂等**——处理一次和处理多次的结果完全相同。 + +**三种常见策略:** + +| 策略 | 实现方式 | 适用场景 | +|------|---------|---------| +| 数据库唯一约束 | 用 `msg_id` 或业务主键做 `UNIQUE` | 最可靠,推荐首选 | +| Redis 去重键 | SET 一个 TTL Key(如 `dedup:{msg_id}` 过期 24h) | 高吞吐场景,需考虑 Redis 可用性 | +| 乐观锁版本控制 | UPDATE 时加 `WHERE version = old_version` | 适用于金额调整类操作 | + +```go +// 推荐方案:利用 UNIQUE 约束做幂等保障 +_, err := db.Exec(` + INSERT INTO order_events (msg_id, order_id, action, amount) + VALUES ($1, $2, $3, $4) + ON CONFLICT (msg_id) DO NOTHING -- 重复到达时静默忽略 +`, msgID, orderID, action, amount) +if err != nil { + // 其他错误才需要处理,UNIQUE 冲突直接忽略 + if !isUniqueViolation(err) { + log.Error("process message failed", err) + } +} +// 执行业务逻辑... +``` + +> [!tip] 设计要点 +> `ON CONFLICT DO NOTHING` 比先查后插更可靠——它消除了竞态窗口。即使用两个实例同时收到同一条消息,只会有一个成功写入。 + +> [!question] 思考 +> 为什么要在生产者侧保证消息可靠投递,而不是靠消费者反复拉取重试? + +## 方案二:Saga 模式 + +Saga 适用于**跨多个服务的长流程操作**,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。 + +```mermaid +graph LR + S1[① 创建订单
→ 取消订单] + S2[② 扣减库存
→ 恢复库存] + S3[③ 支付扣款
→ 退款] + S4[④ 发货
→ 无补偿] + + S1 --> S2 --> S3 --> S4 +``` + +**编排式 vs 编舞式**: + +```mermaid +graph TB + subgraph ORCHESTRATION["编排式 — 中心协调器"] + CO[Coordinator] --> S1[OrderSvc] + CO --> S2[InventorySvc] + CO --> S3[PaymentSvc] + end + + subgraph CHOREOGRAPHY["编舞式 — 事件驱动"] + E1[订单创建] --> S11[订单服务] + S11 --> E2[库存不足事件] + E2 --> S12[库存服务] + S12 --> E3[扣减完成事件] + E3 --> S13[支付服务] + end +``` + +| | 编排式 | 编舞式 | +|---|--------|--------| +| 控制流 | 中心化 Coordinator | 各服务通过事件自发响应 | +| 可观测性 | ✅ 集中管理 | ❌ 流程散布在各服务 | +| 耦合度 | 依赖 Coordinator | 服务间仅感知事件 | + +## 方案三:TCC 与 AT 模式 + +> [!abstract] 进阶了解 +> +> **TCC** (Try-Confirm-Cancel):每个事务步骤实现三个接口。适合对一致性要求严格且能承受开发成本的业务。 +> +> **AT 模式** (Seata):框架层自动处理两阶段提交,对业务透明。本质上是基于 undo_log 的增强型 XA,性能低于 Saga。 + +## 总结选型指南 + +> [!summary] 决策树 +> +> ```mermaid +> graph TD +> A["跨几个服务"] -->|"1-2"| B["本地事务 + 事件通知"] +> A -->|"3+"| C["Saga 模式"] +> B -->|"强"| D["加幂等,必要时上 TCC"] +> B -->|"弱"| E["本地事务已够"] +> C -->|"不能"| F["考虑 AT 模式"] +> C -->|"能"| G["自行实现补偿"] +> ``` +> +> **经验法则**:80% 的场景,本地事务 + MQ 就足够了。先别急着上复杂方案。 + +## 关联笔记 + +- [[01-基础概念]] — 微服务拆分与独立数据库原则 +- [[02-服务治理]] — 服务调用链路上的超时、重试、熔断 diff --git a/hzh/MS/04-可观测性.md b/hzh/MS/04-可观测性.md new file mode 100644 index 0000000..be41122 --- /dev/null +++ b/hzh/MS/04-可观测性.md @@ -0,0 +1,446 @@ +--- +tags: [microservice, observability, monitoring, tracing] +create time: 2026-04-29 12:04 +--- + +# 可观测性 + +## 概述 + +微服务架构下,一个请求可能穿越十几个甚至上百个服务。**排查问题就像在大海捞针**。可观测性通过三大支柱(Metrics、Logging、Tracing)让系统行为变得"可见"。 + +> [!question] 核心思考 +> 单体应用出问题,SSH 到服务器上 `tail -f` 日志就能定位。但当你有 50 个服务、每个 3 个副本,日志散落在不同容器里——你还能用同样的方式排障吗?如果不能,什么体系能替代它? + +### 为什么需要独立的"可观测性"体系? + +```mermaid +flowchart LR + subgraph MONO["单体系统"] + S[Service] --> DB[(DB)] + LOG["同一份日志
一目了然"] + S --> LOG + end + + subgraph MICRO["微服务系统"] + REQ("Request") --> GW["Gateway"] + GW --> O1["Order Svc"] + GW --> U1["User Svc"] + O1 --> PAY["Payment Svc"] + O1 --> INV["Inventory Svc"] + PAY --> DB1[(DB)] + INV --> DB2[(DB)] + + style O1 fill:#faa + style U1 fill:#faa + style PAY fill:#faa + style INV fill:#faa + end + + NOTE["问题:日志分散、链路断裂
传统监控无法回答'这个请求经历了什么'"] + MICRO --> NOTE +``` + +可观测性与监控的本质区别:**监控系统告诉你"出事了"**(已知未知),**可观测性系统让你去探究"为什么会出事"**(未知未知)。 + +### SLO 与 Error Budget + +可观测性的终极目标不是收集数据,而是**支撑决策**。SLO(Service Level Objective)将技术指标映射到用户体验。 + +> [!summary] SLI / SLO / SLA 三层模型 +> +> | 层级 | 含义 | 示例 | +> |------|------|------| +> | **SLI** (Indicator) | 实际测量的指标 | P99 延迟 = 120ms | +> | **SLO** (Objective) | 内部目标值 | P99 延迟 < 200ms(99.9% 的请求) | +> | **SLA** (Agreement) | 对外的契约承诺 | 可用性 ≥ 99.95%,否则赔偿 | + +> [!tip] Error Budget(错误预算) +> +> 如果 SLO 是 99.9%,那么每月允许的错误时间 = 30 × 24 × 60 × 0.1% ≈ **43 分钟**。 +> +> - **预算充足时**:可以大胆发布新功能、尝试激进方案 +> - **预算耗尽时**:冻结非功能性变更,专注稳定性修复 +> +> 这构成了"创新"和"稳定"之间的动态平衡机制。 + +## 三大支柱 + +| 支柱 | 回答的问题 | 典型工具 | +|------|-----------|---------| +| **Metrics** | 系统现在健康吗? — 趋势、告警 | Prometheus + Grafana | +| **Logging** | 具体发生了什么? — 历史回溯 | ELK / Loki | +| **Tracing** | 请求在哪一步慢了/失败了? — 链路追踪 | Jaeger / Zipkin | + +### Metrics:系统的脉搏 + +Metrics 回答的问题是:**系统现在健康吗?**——通过聚合后的数值揭示趋势。 + +> [!summary] RED 和 USE 方法 +> +> | 方法 | 适用于 | 指标 | +> |------|--------|------| +> | **RED** (Rate, Errors, Duration) | 有状态服务(API) | QPS、错误率、响应时间 P99 | +> | **USE** (Utilization, Saturation, Errors) | 基础设施(CPU/内存/网络) | 使用率、饱和度、错误数 | + +核心指标类型: + +- **Counter(计数器)**:只增不减,如 `http_requests_total`。适合统计请求总量、错误总数 +- **Gauge(仪表盘)**:可增可减,如 `queue_depth`、在线用户数 +- **Histogram(直方图)**:样本分布,自动分桶统计,如请求延迟的 P50/P90/P99 +- **Summary(摘要)**:类似 Histogram,但客户端计算分位数(Go SDK 默认) + +```go +// Go 示例:定义并注册 Prometheus 指标 +var ( + // Counter: HTTP 请求总数,按 method 和 status 标签分类 + httpRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total HTTP requests", + }, + []string{"method", "status"}, + ) + + // Histogram: API 响应延迟分布 + apiLatency = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "api_latency_seconds", + Help: "API latency distribution", + Buckets: prometheus.DefBuckets, // [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10] + }, + []string{"endpoint"}, + ) +) + +func init() { + prometheus.MustRegister(httpRequestsTotal, apiLatency) +} +``` + +> [!tip] 解读提示 +> Counter 用于统计"发生了多少",Histogram 用于理解"有多慢"。一个完整的告警面板通常同时展示两者:QPS 突降 + 延迟飙升 = 大概率出事了。 + +在 Grafana 中的典型视图: + +```mermaid +graph TB + subgraph GRAFANA["Grafana Dashboard"] + G1["QPS
[Counter]"] -->|关联分析| G2["P99 Latency
[Histogram]"] + G3["错误率
[Counter %]"] -->|关联分析| G2 + G2 --> G4["告警规则
阈值触发"] + end + + GRAFANA -.->|从 Prometheus 拉取| PROM[(Prometheus)] +``` + +### Logging:集中化日志 + +Logging 回答的问题是:**具体发生了什么?**——通过原始事件记录提供上下文。 + +- **所有服务的日志必须集中收集**,不能散落在各台机器上 +- 日志格式推荐 JSON,便于解析 +- 每条日志必须携带 `trace_id`,与 Tracing 串联 + +```json +{ + "timestamp": "2026-04-29T12:00:00Z", + "level": "ERROR", + "service": "order-service", + "trace_id": "abc-123-def", + "span_id": "span-456", + "msg": "failed to connect payment service", + "caller": "payment/client.go:42", + "cost_ms": 30000 +} +``` + +#### 结构化日志的核心字段 + +> [!note] 最小集合(必选) +> +> - `timestamp` — ISO 8601 格式,统一时区 UTC +> - `level` — TRACE / DEBUG / INFO / WARN / ERROR / FATAL +> - `service` — 微服务名称 +> - `trace_id` — 链路追踪 ID,用于跨服务关联 +> - `msg` — 人类可读的消息描述 + +> [!warning] 日志最佳实践 +> +> - **不要记录敏感信息**:密码、Token、身份证号等绝不能出现在日志中 +> - **INFO 级记录关键业务事件**:下单成功、支付回调——这些是排查业务问题的主线 +> - **WARN 级记录异常但不致命**:重试了一次、降级走了备用方案 +> - **ERROR 级必须有 trace_id 和错误堆栈**,否则毫无价值 + +#### 日志采集管线 + +```mermaid +flowchart LR + APP[应用容器] -->|stdout/stderr| LO[FLUENTD / Filebeat] + LO -->|TCP/Lumberjack| LO2[(Logstash / Loki)] + LO2 -->|解析 & 过滤| PIPE[Pipeline] + PIPE -->|写入| ES[(Elasticsearch)] + PIPE -->|跳转| GRAF[Grafana / Kibana] + + style PIPE fill:#ff9 +``` + +**常见方案对比**: + +| 方案 | 存储 | 查询能力 | 适用场景 | +|------|------|---------|---------| +| ELK (Elasticsearch) | ES 集群 | 全文检索强大,灵活度高 | 日志量大、需要复杂分析的团队 | +| Loki + Grafana | 对象存储 (S3) | 对标 LogQL,轻量高效 | 已经在用 Grafana 的团队 | +| CloudWatch / SLS | 云厂商托管 | 开箱即用,成本较高 | 全云原生环境 | + +> [!tip] 日志采样策略 +> +> 生产环境中并非所有日志都要采集。**正常路径采 1%,异常路径全量**。这既能大幅降低存储成本,又能在出问题时有足够的调试数据可用。采样逻辑建议写在日志库中,而非业务代码里。 + +### Tracing:分布式链路追踪 + +Tracing 回答的问题是:**请求在哪一步慢了 / 失败了?**——通过完整的调用链路定位瓶颈和故障。 + +一次请求在多个服务中的完整调用路径: + +```mermaid +flowchart LR + RID["Request
trace_id: abc-123"] + + subgraph SPANS["调用链 (Trace)"] + direction TB + S1["Span #1
API Gateway
5ms"] + S2["Span #2
Order Service
70ms"] + S3["Span #3
Payment RPC
160ms"] + S4["Span #4
Inventory RPC
70ms"] + + S1 --> S2 --> S3 + S2 --> S4 + end + + RID --> S1 + style S3 fill:#ff9999 + + note["Span #3 耗时最长 → Payment 是瓶颈"] + S3 -.-> note +``` + +> [!tip] 关键实践 +> - 确保 `trace_id` 在整个调用链中传递(HTTP Header 或 MQ Message Header) +> - 采样策略:**全量采集开发环境**,**生产环境按百分比采样**(避免存储爆炸) +> - 重点关注的 Span 标签:HTTP method、status code、database query +> - **Span 不要嵌套过深**,一个 HTTP 调用或 DB 查询对应一个 Span 即可 + +#### Context Propagation(上下文传播) + +trace_id 如何从上游服务传递到下游? + +```go +// 1. 入站:从 HTTP Header 提取 trace context +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // OpenTelemetry SDK 自动从 Header 恢复 span context + ctx := propagator.Extract(r.Context(), headerReader{r.Header}) + + // 2. 创建新的 span + ctx, span := tracer.Start(ctx, "handleOrder") + defer span.End() + + // 3. 将 trace context 注入到新请求的 Header 中 + r = r.WithContext(ctx) + propagator.Inject(ctx, headerWriter{r.Header}) + + next.ServeHTTP(w, r) + }) +} +``` + +```json +// HTTP Header 中实际传递的字段 (W3C Trace Context) +{ + "traceparent": "00-abc123def456...-789ghi012jkl-01", + "tracestate": "congo=t61rcWkgMzE" +} +``` + +> [!note] W3C Trace Context 标准 +> +> `traceparent` 格式:`version-trace_id-span_id-flags`,如 `00-{32位trace_id}-{16位span_id}-{2位flags}`。 +> 这是 W3C 推荐的标准,被 OpenTelemetry、Jaeger、Zipkin 等主流方案统一支持。**无需再自定义 Header**。 + +## 告警设计原则 + +### 告警疲劳是常态 + +> [!warning] 核心问题 +> +> 如果一个团队每天收到 200 条告警,其中 195 条是误报或无需处理,工程师会对剩下的 5 条真正重要的告警产生"脱敏"。这就是 **告警疲劳**。 + +- **告警要能 actionable**:收到告警后知道该做什么,否则不要告 +- **区分级别**:P0(电话叫醒)vs P1(工单处理)vs P2(次日处理) +- **基于 SLO 告警**:不是 CPU > 80% 就告警,而是用户感知到延迟增加时才告 +- **降噪**:同一个根因可能触发连锁告警,需要抑制机制 + +### 告警层级设计 + +```mermaid +flowchart TD + subgraph L1["L1: 页面上的用户受损"] + A1["HTTP 5xx 错误率 > 1%"] + A2["核心接口 P99 > 2s"] + end + + subgraph L2["L2: 系统健康度下降"] + B1["单个服务错误率 > 5%"] + B2["下游依赖超时率升高"] + end + + subgraph L3["L3: 资源预警"] + C1["磁盘使用 > 70%"] + C2["内存使用 > 80%"] + end + + A1 -->|P0 - 电话 + IM| ONCALL[On-Call 值班] + A2 -->|P0 - 电话 + IM| ONCALL + B1 -->|P1 - IM 通知| OPS[运维群] + B2 -->|P1 - IM 通知| OPS + C1 -->|P2 - 日报汇总| TEAM[团队看板] + C2 -->|P2 - 日报汇总| TEAM +``` + +> [!tip] 告警规则编写清单 +> +> 每条告警规则都应该能回答以下问题: +> 1. **什么问题?** — 清晰描述故障现象 +> 2. **谁负责?** — 指定明确的责任人 / On-Call +> 3. **怎么修复?** — 链接到 Runbook / 处置文档 +> 4. **多久升级?** — P1 无人响应则自动升级到上级 + +## OpenTelemetry:统一的可观测性标准 + +过去,Metrics、Logging、Tracing 各用各的工具链,数据割裂在完全不同的系统中。**OpenTelemetry (OTel)** 试图解决这个问题。 + +> [!summary] OTel 的核心思想 +> +> 一套 SDK → 三种信号(Metrics、Logs、Traces)→ 任意后端(Jaeger、Prometheus、ELK...) +> +> 这意味着:**你不需要在代码里为不同后端写多套采集逻辑**。换一个后端只需要改配置,不改代码。 + +### OTel Architecture + +```mermaid +flowchart LR + subgraph APP["你的应用"] + OTEL_SDK["OpenTelemetry SDK
auto-instrumentation Agent"] + end + + OTEL_SDK -->|SDK 自动采集| COLLECTOR[(OpenTelemetry Collector)] + + COLLECTOR -->|OTLP| JAEGER[(Jaeger
Tracing)] + COLLECTOR -->|OTLP| PROM[(Prometheus
Metrics)] + COLLECTOR -->|OTLP| LOKI[(Loki
Logging)] + + style OTEL_SDK fill:#ff9 +``` + +> [!note] 为什么需要 Collector? +> +> Collector 是一个独立的服务进程,承担三个职责: +> 1. **接收**:从应用的 SDK 接收数据 +> 2. **处理**:过滤、采样、富化(添加额外标签) +> 3. **导出**:将数据转发到不同的后端存储 +> +> 这样你的应用只关心"发送数据",Collector 负责"怎么存、存到哪里"。两者解耦。 + +### Go 中接入 OpenTelemetry + +```go +// 1. 初始化 TracerProvider(Tracing) +provider := sdktrace.NewTracerProvider( + sdktrace.WithBatcherExporter(exporter), // 异步批量上报,不阻塞业务 +) +defer provider.Shutdown(context.Background()) + +tracer := provider.Tracer("order-service") + +// 2. 在 handler 中创建 span +func handleOrder(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "handleOrder") + defer span.End() + + // 设置丰富的 Span 属性(供查询和过滤) + span.SetAttributes( + attribute.String("http.method", r.Method), + attribute.Int("http.status_code", 200), + attribute.String("user.id", userID), + ) + + // 调用下游服务(trace context 自动传播) + resp, err := callPayment(ctx) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + return + } +} +``` + +> [!tip] 渐进式落地路线 +> +> 1. **第一步**:先接 Tracing(价值最大、投入最小),覆盖核心链路 +> 2. **第二步**:加 Metrics(Prometheus + Grafana),建立基础监控 +> 3. **第三步**:集中 Logging(Loki / ELK),与 trace_id 关联 +> 4. **第四步**:全量上 OTel Collector,统一管理三件套 +> +> 不要试图一步到位——Tracing 的 ROI 最高,先从这里开始。 + +## Dashboard 与 On-Call 实践 + +收集了这么多数据,下一步是怎么用。 + +### Service Health Dashboard + +一个优秀的 Dashboard 应该让任何团队成员在 **30 秒内**了解服务的整体状态: + +```mermaid +flowchart TB + subgraph DASH["Service Health Dashboard"] + ROW1["🔴 可用性 & 错误率 — 第一眼判断"] + ROW2["🟡 性能指标 — P50/P90/P99 趋势"] + ROW3["🔵 基础设施 — CPU/内存/连接数"] + ROW4["⚫ 业务指标 — 订单量/支付成功率"] + end + + ROW1 --> JUDGE{是否异常?} + ROW2 --> JUDGE + ROW3 --> JUDGE + ROW4 --> JUDGE + + JUDGE --"否" --> NORMAL["一切正常 ✓"] + JUDGE --"是" --> ALERT["触发告警 → On-Call"] +``` + +### Runbook:告警处置手册 + +告警来了之后该怎么办?答案不应存在某个资深工程师的脑子里。 + +> [!summary] Runbook 模板 +> +> | 字段 | 内容 | +> |------|------| +> | **标题** | `支付服务 P99 延迟 > 2s 持续 5 分钟` | +> | **影响范围** | 下单接口超时,用户体验受损 | +> | **检查步骤** | ① 看 Grafana 延迟面板确认峰值时间点 → ② 查同期部署记录 → ③ 检查下游 DB 慢查询 | +> | **常见原因** | ① 新上线代码有性能 regression → ② DB 连接池耗尽 → ③ 下游超时风暴 | +> | **快速恢复** | ① 回滚最近一次发布 → ② 扩容支付服务实例 → ③ 开启熔断降低下游压力 | +> | **彻底解决** | 排查根本原因,补充回归测试,完善容量规划 | + +> [!question] 反模式自检 +> +> 如果你的 On-Call 流程是这样的:收到告警 → 群里问 "有人遇到过吗?" → 等半天有人回复 → SSH 到服务器上翻日志 → ……——这说明你们的可观测性体系还不够成熟。**好的 On-Call 体验 = 清晰的告警 + 完善的 Runbook + 一键处置工具**。 + +## 关联笔记 + +- [[02-服务治理]] — 熔断器可以触发告警,与监控系统联动;分布式链路追踪章节也有详细说明 +- [[05-部署运维]] — K8s Liveness/Readiness Probe 是可观测性的基础 +- [[01-基础概念]] — 微服务的整体架构认知 diff --git a/hzh/MS/05-部署运维.md b/hzh/MS/05-部署运维.md new file mode 100644 index 0000000..1ffdf4e --- /dev/null +++ b/hzh/MS/05-部署运维.md @@ -0,0 +1,397 @@ +--- +tags: [microservice, kubernetes, cicd, container, sre, observability] +create time: 2026-04-29 12:05 +--- + +# 部署运维 + +## 概述 + +微服务架构下,服务数量从几个增长到几百个。**人工运维完全不可行**。本文涵盖容器化编排、K8s 资源管理、发布策略、弹性伸缩、CI/CD 流水线、日志告警和 SRE 核心概念。 + +## 容器化与编排 + +### Docker 容器 — 标准交付单元 + +```dockerfile +# Go 多阶段构建示例 +FROM golang:1.22 AS builder +WORKDIR /app +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o server . + +FROM alpine:latest +COPY --from=builder /app/server /server +EXPOSE 8080 +CMD ["/server"] +``` + +> [!tip] 关键实践 +> - **多阶段构建**:镜像只包含运行时产物,体积极大缩小 +> - **非 root 用户运行**:安全最佳实践 +> - `.dockerignore`:排除不必要的文件(git、vendor、测试文件) + +### Kubernetes 核心概念 + +```mermaid +graph TB + subgraph CLUSTER["K8s Cluster"] + master["Master Node
API Server / Scheduler / ETCD"] + + subgraph NODES["Worker Nodes"] + N1[Node A] + N2[Node B] + end + + master --> N1 + master --> N2 + + subgraph PODS1["Deployment: order-service"] + P1[Pod 1] + P2[Pod 2] + P3[Pod 3] + end + + subgraph PODS2["Deployment: payment-service"] + Q1[Pod 1] + Q2[Pod 2] + end + + N1 --> P1 & Q1 + N2 --> P2 & P3 & Q2 + end + + Svc["Service: order-service
Load Balancer → Pods"] + Svc --> P1 & P2 & P3 +``` + +| K8s 对象 | 作用 | +|---------|------| +| Pod | 最小部署单元,一个或多个容器 | +| Deployment | 管理 Pod 的声明式更新和扩缩容 | +| Service | 稳定的网络入口,实现负载均衡 | +| Ingress | HTTP/HTTPS 路由规则(外部访问入口) | +| ConfigMap / Secret | 配置注入,与代码分离 | +| HPA (Horizontal Pod Autoscaler) | 根据 CPU/自定义指标自动扩缩 | + +### 资源管理与健康检查 + +K8s 调度 Pod 的核心依据是 **资源请求 (Requests)**,而决定 pod 生死的是 **探针 (Probes)**。这两个概念配合使用才能保障服务稳定运行。 + +```yaml +# Deployment 核心片段:资源配置 + 探针 +spec: + replicas: 3 + template: + spec: + containers: + - name: order-service + image: registry.example.com/order:v1.2.3 + resources: + requests: # 调度依据:K8s 保证至少有这些资源 + cpu: "250m" # 0.25 核 + memory: "256Mi" # 256 MB + limits: # 硬上限:超过则 OOMKill / CPU Throttle + cpu: "500m" + memory: "512Mi" + livenessProbe: # 活体检测:死了就重启 + httpGet: + path: /healthz + port: 8080 + initialDelaySeconds: 15 + periodSeconds: 10 + readinessProbe: # 就绪检测:未就绪就不进负载均衡 + httpGet: + path: /ready + port: 8080 + initialDelaySeconds: 5 + periodSeconds: 5 + startupProbe: # 启动检测:慢启动服务友好 + httpGet: + path: /healthz + port: 8080 + failureThreshold: 30 + periodSeconds: 10 +``` + +> [!tip] Probe 选择指南 +> +> | 探针类型 | 触发条件 | 后果 | 适用场景 | +> |---------|---------|------|---------| +> | **Liveness** | `/healthz` 返回非 2xx | K8s **重启容器** | 死锁、无法恢复的崩溃 | +> | **Readiness** | `/ready` 返回非 2xx | **摘除 Service 流量** | 依赖 DB 连接池未就绪、热加载进行中 | +> | **Startup** | 首次成功前一直失败 | **不重启,只等待** | 大模型初始化、JVM cold start 等慢启动场景 | + +> [!question] 思考 +> 如果一个服务的 `/healthz` 因为数据库连接超时而持续返回 503,K8s 会怎么做? + +**答案**:Liveness Probe 会认为容器挂了并反复重启它 — 这就是经典的 **CrashLoopBackOff**。正确做法是让 `/healthz` 做降级判断(DB 不可用时返回 200 + 标注降级),用 `/ready` 来摘除流量。Liveness 应该只对"进程已死"的情况敏感,对"性能下降"保持宽容。 + +--- + +## 发布策略 + +微服务需要独立发布,如何在不停服的情况下完成更新? + +### 蓝绿发布 + +```mermaid +stateDiagram-v2 + [*] --> Blue: 初始状态 + Blue --> DeployGreen: 部署新版本到 Green + DeployGreen --> TestGreen: 灰度验证 + TestGreen --> Switch: 切换流量 + Switch --> Green: 全部流量到新版 + Green --> CleanupGreen: 删除旧版 Blue +``` + +- 优点:**回滚极速**(切回 Blue 即可),验证充分 +- 缺点:资源翻倍,需要两套环境 + +### 金丝雀发布 (Canary) + +```mermaid +graph LR + A["95% traffic to stable"] + B["5% traffic to canary"] + C["Metrics normal?"] + D["Gradually expand to 20% → 50% → 100%"] + E["Metrics abnormal → Auto rollback"] + + A --> B + B --> C + C -- "yes" --> D + C -- "no" --> E +``` + +- 优点:资源效率高,风险渐进暴露 +- 缺点:流程复杂,需要完善的监控配合 + +### 两种策略选择 + +> [!question] 思考 +> 你的团队应该用蓝绿还是金丝雀? + +**答案线索**:看你们的**监控成熟度和发布频率**。低频次(每周/每月)、监控完善时用金丝雀;高频次(每天多次)或想简化流程时用蓝绿。 + +## 弹性伸缩 + +```mermaid +graph TD + Metrics["CPU / Memory / Custom QPS"] --> Trigger{Threshold met?} + Trigger -- "yes" --> ScaleUp["Scale up new Pod"] + Trigger -- "no" --> Keep["Keep current replicas"] + ScaleDown{"Load decreased?"} -- "yes" --> ScaleDownPod["Scale down Pod"] + ScaleDown -- "no" --> Keep +``` + +**注意事项**: +- **预热时间**:新 Pod 启动后不能立刻算入统计,否则可能反复扩缩 +- **优雅关闭**:HPA 缩容前,Pod 需要先摘除 Service 流量再退出 +- **预留容量**:不要将集群利用率拉到 100%,给突发流量留缓冲 + +## CI/CD 流水线 + +```mermaid +graph LR + CODE["代码提交"] + TEST["单元测试 + 集成测试"] + SCAN["安全扫描 + 代码质量"] + BUILD["构建 Docker 镜像"] + PUSH["推送镜像仓库"] + DEPLOY["CI→Staging → 审批 → Production"] + + CODE --> TEST --> SCAN --> BUILD --> PUSH --> DEPLOY +``` + +> [!summary] CI/CD 设计原则 +> +> - **一次构建,多处部署**:镜像不随环境重新编译,只改 K8s ConfigMap/环境变量 +> - **语义化版本号**:镜像 tag 用 `v1.2.3`,tag 即版本溯源 +> - **自动化测试覆盖率要求**:合并 PR 前必须通过,否则不允许发布 + +### Pipeline 实战:GitHub Actions + +```yaml +# .github/workflows/deploy.yml +name: Deploy order-service +on: + push: + branches: [main] + paths: + - "services/order/**" # 仅该目录变更才触发 + +jobs: + build-and-push: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Build & push image + run: | + docker build -t registry/order:${{ github.sha }} . + docker tag registry/order:${{ github.sha }} \ + registry/order:v$(git describe --tags --abbrev=0) + docker push registry/order:${{ github.sha }} + + deploy-staging: + needs: build-and-push + runs-on: ubuntu-latest + environment: staging + steps: + - name: Apply K8s manifests + run: | + kubectl set image deployment/order-service \ + order=registry/order:${{ github.sha }} + kubectl rollout status deployment/order-service --timeout=120s + + 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}}' + # ... 等待监控确认,逐步放大流量 +``` + +> [!tip] Pipeline 设计要点 +> +> - **路径过滤**:只对相关服务的代码变更触发构建,避免全量重建 +> - **Commit SHA 作为镜像 tag**:保证精确回滚 — `git revert` 之后用同一 SHA 拉取旧镜像 +> - **分阶段部署**:Staging → Production 的审批关卡是最后的防线,不要跳过 + +## 日志管理与告警 + +微服务单实例崩溃不可怕 — 可怕的是 **不知道哪一台、为什么挂了**。日志和告警是运维的眼睛。 + +### 日志架构选型 + +```mermaid +graph LR + App["业务应用"] -->|stdout/stderr| K8sPod[Pod 容器日志] + K8sPod --> Filebeat[采集器: Filebeat / FluentBit] + Filebeat --> ES["Elasticsearch / Loki"] + ES --> Grafana["Grafana 查询面板"] + + App -->|structured log| sidecar[Sidecar 辅助日志] + sidecar --> Filebeat +``` + +**业界两种主流方案对比:** + +| 维度 | ELK (Elasticsearch) | EFK/Loki | +|------|---------------------|----------| +| 存储成本 | 高(全文索引) | 低(索引 label + 对象存储原文) | +| 查询性能 | 毫秒级全文检索 | 按 label 过滤较快,复杂查询慢 | +| 运维复杂度 | 高(ES 集群维护) | 低(Loki 无索引) | +| 适用规模 | 百万条/天以上 | 万 ~ 百万条/天 | + +> [!important] 结构化日志 +> +> 无论选择哪家方案,**日志格式必须是结构化的**(JSON),否则后期处理全是手工活: +> ```json +> { +> "timestamp": "2026-04-29T10:30:00Z", +> "level": "error", +> "service": "order-service", +> "trace_id": "abc-123-def", +> "msg": "payment gateway timeout", +> "duration_ms": 5023 +> } +> ``` + +### 告警策略设计 + +> [!question] 思考 +> 如果一个 Pod CPU 使用率从 20% 飙升到 90%,你应该立刻打电话叫值班工程师吗? + +不一定。**好的告警应该是" actionable"的** — 能让人立刻采取行动,而不是单纯告诉你"出事了"。 + +```mermaid +mindmap + root((告警设计原则)) + 区分等级 + P0: 立即响应 (页面电话) + P1: 当天处理 (IM 消息) + P2: 本周修复 (工单) + 抑制噪音 + 相关告警聚合: 一个根因一条告警 + 静默期: 重启后的自动恢复不打扰 + 附带上下文 + 告警信息包含: 什么服务 · 哪个指标 · 当前值 vs 阈值 +``` + +--- + +## SRE 核心概念 + +站点可靠性工程 (SRE) 把运维问题看作**软件工程问题**。它的核心理念是用 SLI/SLO/SLA 来量化服务质量,避免"我觉得系统很慢"这类模糊描述。 + +### SLI / SLO / SLA + +| 术语 | 全称 | 定义 | 谁定义 | +|------|------|------|--------| +| **SLI** | Service Level Indicator | **实际度量**:用户请求的成功率是多少? | 观测系统自动产出 | +| **SLO** | Service Level Objective | **内部目标**:我们承诺达到 99.9% 可用性 | SRE + 研发制定 | +| **SLA** | Service Level Agreement | **对外合同**:达不到就赔钱 | 法务 + 商务制定 | + +> [!tip] 实用比例 +> +> - **99.9% (三个九)** = 每年约 8.76 小时停机 — 适合大多数后端服务 +> - **99.95%** = 每年约 4.38 小时 — 核心交易链路 +> - **99.99% (四个九)** = 每年约 52 分钟 — 金融级系统 +> - **99%** 意味着每月近 7 小时不可用 — **几乎等于没有可用性目标** + +### 错误预算 (Error Budget) + +这是 SRE 最核心的机制 — **允许犯错,但设上限**: + +``` +错误预算 = 1 - SLO + +SLO = 99.9% → 错误预算 = 0.1% → 每月允许 21 分钟停机 +SLO = 99.99% → 错误预算 = 0.01% → 每月仅 4 分钟停机 +``` + +**基于错误预算的决策逻辑:** + +```mermaid +flowchart TD + Budget{"错误预算剩余 > 50%?"} + + Budget -- "是" --> Aggressive["可激进发布:
金丝雀 + 自动扩缩"] + Budget -- "< 50%" --> Conservative["保守发布:
蓝绿 + 全量人工审查"] + Budget -- "< 10%" --> Freeze["冻结发布:
优先修复稳定性
不允许新功能上线"] + + Aggressive --> Monitor["持续监控指标"] + Conservative --> Monitor +``` + +> [!summary] SRE 心法 +> +> 1. **用户视角定义 SLO**:不是"API P99 < 200ms",而是"用户在 3G 网络下打开页面 < 2s" +> 2. **错误预算用完 = 停止功能开发**:全力修 bug 加稳定性 +> 3. **Blameless Postmortem**:事故复盘不问"谁干的",问"流程哪里可以改进" + +--- + +## 运维 Checklist + +每次上线前快速过一遍这个清单,避免低级事故: + +| # | 检查项 | 说明 | +|---|--------|------| +| 1 | **探针已配置** | liveness/readiness/startup probe 都已设定 | +| 2 | **资源 limits 已设** | 防止单个 Pod 拖垮整台机器 | +| 3 | **日志为 JSON 格式** | 可被采集器解析 | +| 4 | **追踪 ID 透传** | trace_id 在跨服务调用链中不丢失 | +| 5 | **回滚预案明确** | 知道怎么回到上一版本,且演练过 | +| 6 | **告警已配置** | 关键指标异常时有人收到通知 | +| 7 | **数据迁移有回退脚本** | DB schema 变更要兼容新老版本共存 | + +## 关联笔记 + +- [[02-服务治理]] — K8s Service 提供原生的服务发现和负载均衡 +- [[04-可观测性]] — K8s Liveness/Readiness Probe 是可观测性的基础 diff --git a/hzh/MS/README.md b/hzh/MS/README.md new file mode 100644 index 0000000..156f4b2 --- /dev/null +++ b/hzh/MS/README.md @@ -0,0 +1,41 @@ +--- +tags: [] +create time: 2026-04-29 12:00 +--- + +# 微服务知识索引 + +## 概述 + +本目录系统整理微服务架构的核心知识点。围绕 **5 个核心主题**,由浅入深地覆盖从概念理解到工程实践的关键路径。 + +## 知识体系 + +```mermaid +graph LR + A["01 基础概念"] --> B["02 服务治理"] + B --> C["03 数据一致性"] + B --> D["04 可观测性"] + C --> E["05 部署运维"] + D --> E +``` + +| # | 主题 | 核心问题 | +|---|------|----------| +| 1 | [[01-基础概念]] | 什么是微服务?单体 vs 微服务的权衡是什么? | +| 2 | [[02-服务治理]] | 服务如何发现彼此?如何通信?失败怎么处理? | +| 3 | [[03-数据一致性]] | 每个服务拥有独立数据库,跨服务数据一致性怎么保证? | +| 4 | [[04-可观测性]] | 请求穿越多个服务后,如何追踪、监控和排查问题? | +| 5 | [[05-部署运维]] | 上百个服务如何容器化编排、灰度发布、弹性伸缩? | + +### 学习建议 + +> [!tip] 学习路径 +> 按编号顺序阅读。第 1 篇是理论基础,后面 4 篇在实践中高度耦合,可以按需跳读。 + +> [!question] 带着问题读 +> 每篇文章都设计了启发式问题。先自己想一遍答案,再看文档,效果更好。 + +### 关联笔记 + +- [[API 设计原则]] — API Gateway 的设计与 REST/gRPC 选型