From 64e34e6871570976e5e3cc4362b57f63b5a8fa95 Mon Sep 17 00:00:00 2001 From: hhs <386998068@qq.com> Date: Sun, 17 May 2026 22:00:24 +0800 Subject: [PATCH] vault backup: 2026-05-17 22:00:24 --- hhs/MS/01-基础概念.md | 106 +++ hhs/MS/02-服务治理/01-API网关.md | 449 +++++++++++++ hhs/MS/02-服务治理/02-安全机制.md | 330 +++++++++ hhs/MS/02-服务治理/03-分布式追踪.md | 552 +++++++++++++++ hhs/MS/02-服务治理/04-服务发现.md | 237 +++++++ hhs/MS/02-服务治理/05-服务间通信.md | 204 ++++++ hhs/MS/02-服务治理/06-容错模式.md | 632 ++++++++++++++++++ hhs/MS/02-服务治理/07-配置管理.md | 343 ++++++++++ hhs/MS/02-服务治理/08-流量治理.md | 284 ++++++++ hhs/MS/02-服务治理/09-网关鉴权策略.md | 333 +++++++++ .../09-网关鉴权策略/JWT与OAuth2对比.md | 245 +++++++ .../09-网关鉴权策略/RBAC权限模型实战.md | 284 ++++++++ .../09-网关鉴权策略/service-mesh实战.md | 259 +++++++ hhs/MS/02-服务治理/README.md | 58 ++ hhs/MS/03-数据一致性/01-数据库拆分.md | 198 ++++++ hhs/MS/03-数据一致性/02-分布式事务.md | 249 +++++++ hhs/MS/03-数据一致性/03-ID生成.md | 157 +++++ hhs/MS/03-数据一致性/README.md | 39 ++ hhs/MS/04-可观测性/01-Metrics监控.md | 160 +++++ hhs/MS/04-可观测性/02-日志系统.md | 149 +++++ hhs/MS/04-可观测性/03-链路追踪.md | 166 +++++ hhs/MS/04-可观测性/04-告警管理.md | 186 ++++++ hhs/MS/04-可观测性/README.md | 78 +++ hhs/MS/05-部署运维/01-容器化.md | 155 +++++ hhs/MS/05-部署运维/02-Kubernetes.md | 266 ++++++++ hhs/MS/05-部署运维/03-CICD与GitOps.md | 176 +++++ hhs/MS/05-部署运维/04-SRE实践.md | 160 +++++ hhs/MS/05-部署运维/README.md | 42 ++ hhs/MS/06-gRPC/01-协议与架构.md | 161 +++++ hhs/MS/06-gRPC/02-Proto设计.md | 282 ++++++++ hhs/MS/06-gRPC/03-RPC模式.md | 197 ++++++ hhs/MS/06-gRPC/04-拦截器.md | 176 +++++ hhs/MS/06-gRPC/05-错误处理.md | 178 +++++ hhs/MS/06-gRPC/06-连接管理.md | 155 +++++ hhs/MS/06-gRPC/07-最佳实践.md | 220 ++++++ hhs/MS/06-gRPC/README.md | 133 ++++ hhs/MS/README.md | 88 +++ 37 files changed, 8087 insertions(+) create mode 100644 hhs/MS/01-基础概念.md create mode 100644 hhs/MS/02-服务治理/01-API网关.md create mode 100644 hhs/MS/02-服务治理/02-安全机制.md create mode 100644 hhs/MS/02-服务治理/03-分布式追踪.md create mode 100644 hhs/MS/02-服务治理/04-服务发现.md create mode 100644 hhs/MS/02-服务治理/05-服务间通信.md create mode 100644 hhs/MS/02-服务治理/06-容错模式.md create mode 100644 hhs/MS/02-服务治理/07-配置管理.md create mode 100644 hhs/MS/02-服务治理/08-流量治理.md create mode 100644 hhs/MS/02-服务治理/09-网关鉴权策略.md create mode 100644 hhs/MS/02-服务治理/09-网关鉴权策略/JWT与OAuth2对比.md create mode 100644 hhs/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md create mode 100644 hhs/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md create mode 100644 hhs/MS/02-服务治理/README.md create mode 100644 hhs/MS/03-数据一致性/01-数据库拆分.md create mode 100644 hhs/MS/03-数据一致性/02-分布式事务.md create mode 100644 hhs/MS/03-数据一致性/03-ID生成.md create mode 100644 hhs/MS/03-数据一致性/README.md create mode 100644 hhs/MS/04-可观测性/01-Metrics监控.md create mode 100644 hhs/MS/04-可观测性/02-日志系统.md create mode 100644 hhs/MS/04-可观测性/03-链路追踪.md create mode 100644 hhs/MS/04-可观测性/04-告警管理.md create mode 100644 hhs/MS/04-可观测性/README.md create mode 100644 hhs/MS/05-部署运维/01-容器化.md create mode 100644 hhs/MS/05-部署运维/02-Kubernetes.md create mode 100644 hhs/MS/05-部署运维/03-CICD与GitOps.md create mode 100644 hhs/MS/05-部署运维/04-SRE实践.md create mode 100644 hhs/MS/05-部署运维/README.md create mode 100644 hhs/MS/06-gRPC/01-协议与架构.md create mode 100644 hhs/MS/06-gRPC/02-Proto设计.md create mode 100644 hhs/MS/06-gRPC/03-RPC模式.md create mode 100644 hhs/MS/06-gRPC/04-拦截器.md create mode 100644 hhs/MS/06-gRPC/05-错误处理.md create mode 100644 hhs/MS/06-gRPC/06-连接管理.md create mode 100644 hhs/MS/06-gRPC/07-最佳实践.md create mode 100644 hhs/MS/06-gRPC/README.md create mode 100644 hhs/MS/README.md diff --git a/hhs/MS/01-基础概念.md b/hhs/MS/01-基础概念.md new file mode 100644 index 0000000..f953350 --- /dev/null +++ b/hhs/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/hhs/MS/02-服务治理/01-API网关.md b/hhs/MS/02-服务治理/01-API网关.md new file mode 100644 index 0000000..d914bc8 --- /dev/null +++ b/hhs/MS/02-服务治理/01-API网关.md @@ -0,0 +1,449 @@ +--- +tags: [microservice, api-gateway, kong, apisix, spring-cloud-gateway, nginx] +create time: 2026-05-05 12:00 +--- + +# API 网关 + +## 概述 + +API Gateway 是所有外部请求的统一入口,相当于微服务架构的 **"大门"**——所有来自客户端的请求必须先经过它,由它完成鉴权、限流、路由、转换等公共职责后,再转发给后端具体业务服务。 + +> [!question] 为什么要网关? +> 假设你手上有 20 个微服务,每个都有独立的 URL。如果让客户端直接调用这些服务: +> - 每次调用都要处理鉴权和签名验证? +> - 前端要维护 20 个 CORS 配置? +> - HTTPS 证书要在每台服务器上部署? +> +> 有没有一种方式让这些重复工作只处理一次? + +答案就是 **网关做共性、服务做个性**。 + +```mermaid +graph TB + subgraph External["外部世界"] + App["移动端 App"] + Web["Web 浏览器"] + ThirdParty["第三方 Partner"] + end + + subgraph Gateway["API Gateway 集群"] + direction TB + LB["负载均衡器
Nginx / CLB"] + GW1["Gateway Node A"] + GW2["Gateway Node B"] + end + + subgraph Backend["微服务层"] + USvc[[User Service]] + OSvc[[Order Service]] + PSvc[[Payment Service]] + ISvc[[Inventory Service]] + end + + subgraph Infra["基础设施"] + Config[(配置中心)] + Monitor[(Prometheus/Grafana)] + Log[(ELK / Loki)] + end + + App --> LB + Web --> LB + ThirdParty --> LB + LB --> GW1 + LB --> GW2 + GW1 --> USvc + GW1 --> OSvc + GW2 --> OSvc + GW2 --> PSvc + GW1 -.-> Config + GW2 -.-> Config + GW1 -.-> Monitor + GW2 -.-> Monitor +``` + +常见实现方案: + +| 方案 | 语言 | 类型 | 适用场景 | +|------|------|------|---------| +| **Kong** | C (OpenResty/Lua) | 独立二进制 | 高性能、插件生态丰富 | +| **APISIX** | Lua (OpenResty) | 独立二进制 | 国内广泛使用、动态路由 | +| **Spring Cloud Gateway** | Java (Reactor) | 嵌入式 SDK | Spring 技术栈项目 | +| **Envoy** | C++ | 代理 + SDK | Service Mesh 底层代理 | +| **Nginx + Lua** | C/Lua | 手动组合 | 极致可控、团队有运维能力 | + +## 网关应该做什么? + +> [!summary] 职责边界 +> +> | ✅ 适合放网关 | ❌ 不应该放网关 | +> |---------|-----------| +> | 鉴权与认证 | 业务逻辑(如订单创建) | +> | 限流熔断 | 复杂的数据聚合查询 | +> | HTTPS 终结 | 大量 CPU 密集型计算 | +> | 请求/响应格式转换 | 涉及数据库写操作 | +> | 路由分发 | 跨服务事务管理 | +> | 日志 & 指标采集 | 邮件/短信发送等异步任务 | +> | 协议转换(HTTP→gRPC) | 数据加工和报表生成 | + +> [!tip] 核心原则 +> **网关保持瘦**——它是 traffic cop(交通警察),不是 warehouse manager(仓库管理员)。重逻辑应下沉到业务服务。 +> +> > [!note] 思考 +> > 如果你在网关里发现需要写一个超过 30 行的 if-else 分支来处理"特殊业务规则",停下来问自己:这真的是公共关注点,还是某个服务的私有需求被错误地推到了上层? + +### 鉴权职责的分界 + +网关层的鉴权只负责 **"你是谁"**——校验 JWT / Token 合法性、拦截非法请求。 +更细致的权限判断留给下游服务自行处理。详细方案见 → [[02-服务治理/09-网关鉴权策略]] + +## 路由配置 + +### 基础路由示例 + +```yaml +# APISIX 路由配置 +routes: + - uri: /api/orders/* + upstream: + nodes: + "order-service:8080": 1 + type: roundrobin + + - uri: /api/payments/* + upstream: + nodes: + "payment-service:8080": 1 + type: roundrobin +``` + +```go +// Go 中的路由注册(以 Gin + Gateway 为例) +r := gin.Default() + +r.Use(gateway.AuthMiddleware()) // 全局鉴权 +r.Use(gateway.RateLimit(100)) // 全局限流 + +api := r.Group("/api") +{ + api.POST("/orders", order.CreateHandler) + api.GET("/orders/:id", order.GetHandler) + api.POST("/payments", payment.ChargeHandler) +} + +_ = r.ListenAndServe() +``` + +> [!note] 解释 +> `AuthMiddleware` 和 `RateLimit` 是挂载在路由树最上层的中间件,会拦截所有通过 `/api` 前缀的请求。这意味着无需在每个 handler 里重复写鉴权逻辑——这正是网关集中式处理的优势。 + +### 灰度发布与高级路由 + +流量权重分配、灰度策略、A/B 测试等内容已移至 → [[02-服务治理/08-流量治理]] + +## 插件体系 + +网关的核心价值在于 **可插拔的中间件链**,类似 Express/Koa 的 middleware 概念。 + +```mermaid +graph LR + Req["Request"] -->|Plugin 1| RateLimiter["限流"] + RateLimiter -->|Plugin 2| Auth["鉴权"] + Auth -->|Plugin 3| Router["路由匹配"] + Router -->|Plugin 4| Transform["协议转换"] + Transform -->|Plugin 5| Logger["日志记录"] + Logger --> Resp["Response"] +``` + +### Go 实现的插件框架 + +```go +// Plugin 接口定义 — 每个插件实现此接口即可接入网关流水线 +type Plugin interface { + Name() string + Priority() int // 数字越小越先执行 + OnRequest(ctx *Context) bool // 返回 false 则中断流水线 + OnResponse(ctx *Context) // 响应阶段钩子 +} + +// 网关执行管线 +func (gw *Gateway) ExecutePlugins(ctx *Context, plugins []Plugin) { + sort.Slice(plugins, func(i, j int) bool { + return plugins[i].Priority() < plugins[j].Priority() + }) + + for _, p := range plugins { + ctx.Set("plugin", p.Name()) + if !p.OnRequest(ctx) { + ctx.AbortWithJSON(ctx.StatusCode(), gateway.ErrResp(ctx.ErrorCode())) + return + } + } + + ctx.Next() // 转发到后端服务 + + for _, p := range plugins { + p.OnResponse(ctx) + } +} +``` + +> [!note] 解释 +> 这段代码展示了网关插件系统的核心设计: +> - 每个插件通过 `Priority()` 控制执行顺序(例如限流必须在鉴权之前) +> - `OnRequest` 返回 `false` 时立即截断请求,不会到达后端 +> - `OnResponse` 在所有请求结束后执行,常用于埋点和日志记录 + +### 常用插件速查 + +| 插件 | 作用 | 推荐算法 | +|------|------|---------| +| **Rate Limiting** | 防刷限流 | 令牌桶 / 漏桶 | +| **CORS** | 跨域处理 | 预检缓存 | +| **IP 黑白名单** | 访问控制 | Redis Bloom Filter | +| **Request Transformation** | Header/Body 改写 | Map-based | +| **Protocol Conversion** | HTTP↔gRPC 互转 | protobuf mapping | +| **Prometheus Exporter** | 指标采集 | 自动埋点 | +| **Fault Injection** | 混沌测试 | 按比例注入 | + +## 错误处理 + +网关处于请求链路的最外层,它的错误处理质量直接影响用户体验。 + +### 统一错误码体系 + +```go +// 自定义网关级错误码 +const ( + ErrUnauthorized = 1001 // 未认证或 Token 过期 + ErrForbidden = 1002 // 认证通过但无权访问 + ErrRateLimit = 1003 // 触发限流 + ErrBackendUnavail = 1004 // 后端服务不可用(熔断中) + ErrGatewayTimeout = 1005 // 后端超时 + ErrMalformedReq = 1006 // 请求格式错误 + ErrServiceOverload = 1007 // 服务过载(上游返回 503) +) + +func ErrResp(code int) map[string]interface{} { + return map[string]interface{}{ + "code": code, + "message": errorMessages[code], + "request": currentRequestID, + } +} +``` + +> [!note] 关键决策:网关 4xx vs 5xx +> +> | 场景 | 网关应返回 | 原因 | +> |------|-----------|------| +> | Token 无效 | **401** | 问题出在客户端凭证 | +> | 后端服务宕机 | **502** | 网关正确转发了但收到坏响应 | +> | 后端超时 | **504** | 网关等待超时而非业务错误 | +> | 触发限流 | **429** | 语义明确,客户端可据此退避 | +> | 参数错误 | **400** | 无论前后端,都是客户端输入有误 | +> +> > [!warning] 反模式 +> > 不要把后端 500 原封不动返给客户端。网关应该将其转换为统一的 "内部错误" 提示,并附带唯一的 request ID 用于排查。避免在公网暴露堆栈信息。 + +### 短路保护 —— 熔断机制 + +```go +func (gw *Gateway) forward(ctx *Context) error { + svc := gw.routeTo(ctx.Path) + cb := gw.CircuitBreaker(svc) + + // 熔断开启时直接短路,不再发请求 + if cb.State() == CircuitOpen { + return ctx.Error(ErrBackendUnavail, "%s is circuit-breaker open", svc) + } + + resp, err := cb.Do(func() (*http.Response, error) { + return http.DefaultClient.Do(ctx.Request) + }) + // 成功率低于阈值 → 打开熔断 + return nil +} +``` + +熔断状态机:**Closed**(正常转发)→ **Open**(全部短路)→ **Half-Open**(放行少量探测请求)。详见 → [[02-服务治理/06-容错模式]] + +## 可观测性 + +在生产环境中,网关是一站式的观测入口——所有流量的元数据都在这一层汇聚。 + +```mermaid +flowchart LR + Client --> GW["API Gateway"] + GW -- "access.log + trace_id" --> ELK["ELK / Loki"] + GW -- "QPS / latency / errors" --> PM["Prometheus"] + PM --> Grafana["Grafana Dashboard"] + GW -- "trace span" --> Jager["Jaeger / Zipkin"] + Jager --> Grafana +``` + +### 三 pillars 实践 + +| Pillar | 做什么 | 关键指标 | +|--------|--------|---------| +| **结构化日志** | 每条请求写入 trace_id、source IP、耗时、上游地址 | `method path status latency upstream` | +| **指标采集** | 按路由 / 状态码分桶暴露 Prometheus Metrics | `http_requests_total`, `http_request_duration_seconds` | +| **分布式追踪** | 传递 `X-Trace-ID` 给下游,构建完整调用链 | Trace ID 透传率 ≥ 99% | + +### 告警基线参考 + +| 指标 | 阈值 | 动作 | +|------|------|------| +| P99 延迟 | > 500ms 持续 5min | PagerDuty 告警 | +| 5xx 比例 | > 1% | 立即通知值班 | +| 熔断器打开数 | ≥ 3 个同时打开 | 检查下游健康 | + +## 性能优化 + +网关作为所有流量的必经之路,自身性能瓶颈会成为整个系统的天花板。 + +### 连接池复用 + +每个网关节点都会频繁向后端发起 HTTP 连接,为减少 TCP 握手开销: + +```go +// Go net/http 连接池配置 +transport := &http.Transport{ + MaxIdleConnsPerHost: 100, // 同一后端的空闲连接数 + IdleConnTimeout: 90 * time.Second, + TLSHandshakeTimeout: 5 * time.Second, + ResponseHeaderTimeout: 10 * time.Second, +} + +client := &http.Client{Transport: transport} +``` + +> [!note] 为什么重要 +> 假设 QPS = 5000,平均响应时间 = 50ms,每个请求新建 TCP 连接的 overhead 约 3ms(含 TLS handshake 可能达 20ms)。节省下的 15~20ms 可以直接转化为吞吐量提升。对于网关这种每毫秒都计较的场景,连接池复用是最简单的性能手段。 + +### DNS 预解析 + +避免每次请求都做 DNS lookup: + +```lua +-- OpenResty / Nginx 中的 dns_resolver 配置 +resolver 10.0.0.2 valid=30s; # 30s 缓存 DNS 结果 +resolver_timeout 2s; +``` + +### 其他优化技巧 + +| 技巧 | 收益 | 复杂度 | +|------|------|--------| +| 静态资源本地缓存 | 消除对上游的冗余请求 | 低 | +| gzip/brotli 压缩响应体 | 带宽降低 60~80% | 低 | +| Keep-Alive 复用连接 | 减少 TCP/TLS 握手次数 | 低 | +| 异步日志写入 | 不阻塞请求主线 | 中 | +| 多进程 Worker 模型 | 利用多核 CPU | 低 | + +## 实际案例 + +### 场景一:HTTP 转 gRPC + +后端团队用 gRPC 对外提供服务,但前端只能消费 HTTP/JSON。网关承担协议转换: + +```typescript +// 前端看到的仍然是 RESTful JSON +POST /api/v1/users +{ "name": "Alice", "email": "alice@example.com" } + +// 网关内部将 JSON body 转为 Protobuf 后发给 gRPC 后端 +// UserCreateRequest { name: "Alice", email: "alice@example.com" } +// → POST grpc:///user-service/UserService/Create +``` + +Go 中可使用 [`grpc-ecosystem/grpc-gateway`](https://github.com/grpc-ecosystem/grpc-gateway) 自动生成反向代理,或通过 [`go-grpc-middleware`](https://github.com/go-grpc-middleware) 编写自定义转换器。 + +### 场景二:静态页面兜底 + +当所有后端服务全部不可用时,提供友好的降级页面: + +```nginx +upstream backend { + server app-1:8080; + server app-2:8080; + server app-3:8080; + fail_timeout=30s max_fails=3; # 连续失败 3 次标记为 down +} + +server { + location @fallback { + root /usr/share/nginx/html; + try_files /maintain.html =503; + } + + location / { + proxy_pass http://backend; + proxy_next_upstream error timeout http_502 http_503; + error_page 503 @fallback; # 所有后端挂掉时展示维护页 + } +} +``` + +### 场景三:大文件上传加速 + +用户头像/文档上传场景,网关层直接限速容易被打满,可以绕过网关直连存储: + +```mermaid +sequenceDiagram + participant C as Client + participant G as Gateway + participant S3 as S3/OSS + participant App as App Service + + C->>G: GET /upload-token + G->>App: validate user + App-->>G: { token, upload_url } + G-->>C: presigned URL + C->>S3: PUT (direct, bypass gateway) + S3-->>C: 200 OK + C->>G: notify-complete { token } + G->>App: process uploaded file +``` + +> [!tip] 思路 +> 上传/下载大文件这类 I/O 密集型操作,消耗的是网关的连接数和带宽。可以考虑网关只负责签发临时凭证,数据传输直连对象存储,从而释放网关的并发容量。 + +## 反模式警示 + +> [!warning] 这些做法看起来合理,但实际上会带来问题 + +### 反模式 1:网关成为"万能胶水" + +某些团队把越来越多的业务逻辑往网关塞——拼接多个后端接口、汇总数据、做数据清洗……最终网关变成了一张巨型 spider web。 + +**后果**:网关变慢 → 全系统响应变慢 → 运维越来越痛苦。 + +**对策**:如果需要跨服务聚合数据,创建一个专门的 **BFF(Backend For Frontend)** 层,放在网关之后。 + +### 反模式 2:没有健康检查 + +网关不知道后端什么时候挂掉了,继续把流量打过去直到超时耗尽用户耐心。 + +**对策**:启用 upstream health check,配合熔断器(参见 → [[02-服务治理/06-容错模式]])。 + +### 反模式 3:日志没带 trace_id + +用户在后台看到报错,去翻日志却无从定位——成千上万条日志里没有唯一标识串联整条链路。 + +**对策**:在网关入口处生成 trace_id,通过 `X-Trace-ID` header 透传到所有下游,并在 access log 中固定包含该字段。 + +### 反模式 4:忽略请求大小限制 + +允许任意大小的请求体进入,攻击者可以轻松用超大 payload 打爆网关内存。 + +**对策**:设置 `client_max_body_size`(Nginx)或等效配置,对上传接口单独放宽,普通接口默认限制在几 MB 以内。 + +## 关联笔记 + +- [[02-服务治理/09-网关鉴权策略]] — 网关鉴权的分层设计与差异化方案 +- [[02-服务治理/08-流量治理]] — 灰度发布、权重路由、蓝绿部署 +- [[02-服务治理/06-容错模式]] — 熔断、重试、降级 +- [[02-服务治理/04-服务发现]] — 网关如何获取后端实例列表 +- [[hzh/MS/API 设计原则]] — API Gateway 的设计与 REST/gRPC 选型 +- [[04-可观测性]] — 日志、指标、链路追踪的完整体系 diff --git a/hhs/MS/02-服务治理/02-安全机制.md b/hhs/MS/02-服务治理/02-安全机制.md new file mode 100644 index 0000000..675a84a --- /dev/null +++ b/hhs/MS/02-服务治理/02-安全机制.md @@ -0,0 +1,330 @@ +--- +tags: [microservice, security, mTLS, JWT, OAuth2, zero-trust] +create time: 2026-05-05 10:00 +--- + +# 安全机制 + +## 概述 + +微服务架构下,服务间通信的安全保障比单体应用复杂得多。每个服务的 API 都是潜在的攻击面,需要多层防御。 + +```mermaid +graph TB + subgraph "纵深防御体系" + L1["网络隔离
VPC / 安全组"] + L2["传输加密
mTLS / TLS"] + L3["身份认证
JWT / SPIFFE"] + L4["授权控制
RBAC / ABAC"] + L5["审计追踪
操作日志"] + end + + L1 -.-> L2 -.-> L3 -.-> L4 -.-> L5 +``` + +> [!tip] Zero Trust 原则 +> **永不信任,始终验证**。无论请求来自内网还是外网,每个服务调用都要经过认证和授权。传统"内网即安全"的假设是微服务安全的最大盲区——一旦某个服务被攻破,攻击者可以横向移动到其他服务。 + +## 服务间认证 + +### mTLS (双向 TLS) + +mTLS 是零信任架构的基石——建立加密通道前,双方必须互相验证证书身份。 + +```mermaid +sequenceDiagram + participant C as 调用方 Service + participant CA as CA / SPIFFE + participant S as 被调方 Service + + C->>CA: 申请证书 (SPIFFE ID) + S->>CA: 申请证书 (SPIFFE ID) + CA-->>C: 签发客户端证书 + CA-->>S: 签发服务端证书 + + C->>S: ClientHello + ClientCert + S->>S: 验证书链 + SPIFFE ID + alt 验证通过 + S-->>C: ServerHello + ServerCert + note over C,S: 建立加密通道 + else 验证失败 + S->>C: TLS Alert (handshake failure) + end +``` + +> [!question] 思考 +> 为什么内网服务间通信也需要加密?如果攻击者已经进入了内网网络,明文 gRPC 调用会发生什么? + +> [!answer] 答案 +> 传统安全模型假设"内网即安全",但这个前提是整个内网都不可入侵——这在实际中几乎不成立。以下场景说明为什么内网也必须加密: +> +> **1. 攻击者进入内网的途径很多** +> - 某个前端服务存在远程代码执行漏洞 → 获得容器权限 → 嗅探同 VPC 其他 Pod 流量 +> - 开发者电脑中毒 / CI/CD 供应链被投毒 → 从合法节点发起内网横向访问 +> - 第三方插件或依赖库被植恶意代码 → 在构建产物中留下后门 +> +> **2. 明文 gRPC 被窃听后的具体后果** +> +> | 攻击方式 | 可获取的内容 | 影响 | +> |---------|------------|------| +> | **流量嗅探** | 全部请求/响应体、gRPC metadata | 用户隐私数据、业务逻辑泄露 | +> | **Metadata 劫持** | JWT Token、追踪 ID、认证头 | 直接伪造身份调用下游服务 | +> | **中间人篡改** | 修改请求参数、响应 payload | 注入脏数据、绕过业务校验 | +> +> ```mermaid +> graph LR +> A["A: order-service
(发送请求)"] -->|"明文 gRPC"| D["D: attacker
(嗅探 + 注入)"] +> A --> B["交换机 / router"] +> B --> C["C: payment-service
(接收请求)"] +> D -.->|"伪造成 order-service"| C +> +> style D fill:#f99,stroke:#c00 +> ``` +> +> **关键理解**:即使没有"主动攻击"能力,仅靠二层/三层抓包(ARP spoofing、镜像端口、VLAN hop),攻击者就能完整回放 orca 一个 gRPC stream——包括其中传递的 JWT token、数据库查询条件等敏感信息。 +> +> **结论**:mTLS 不是"防外部黑客"的,而是让内网中的**任何一个被攻破的点**都无法读取其他服务的流量。这就是 Zero Trust 的核心思想。 + +**mTLS 的核心优势**: +- 双向证书验证,防止中间人攻击和非法服务接入 +- 证书自动轮换(配合 Istio / Cert-Manager 等服务网格方案) +- 零代码侵入——Sidecar 代理处理握手,业务代码无需关心 + +**生产落地要点**: + +| 要素 | 推荐方案 | 说明 | +|------|---------|------| +| **CA 体系** | SPIFFE / Istio CA | 基于 SPIFFE ID 自动签发短生命周期证书 | +| **证书管理** | cert-manager + Vault | K8s 场景下自动续期,避免人工干预 | +| **降级策略** | Peer Authentication MESH_STRICT | Istio 中设置为 STRICT 可强制所有流量走 mTLS | + +```yaml +# Istio PeerAuthentication 示例 +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: prod +spec: + mtls: + mode: STRICT # 拒绝明文连接 +``` + +### JWT / Service Token + +适用于不具备 mTLS 基础设施的场景,或作为跨边界调用的身份传递载体: + +```go +// 生成 Service Token(短生命周期,≤ 1h) +token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{ + "sub": "order-service", // 调用方服务标识 + "iss": "auth-server", // 签发者 + "aud": "payment-service", // 目标服务 + "exp": time.Now().Add(1 * time.Hour).Unix(), + "iat": time.Now().Unix(), + "jti": uuid.New().String(), // 唯一 ID,防重放 +}) +tokenString, _ := token.SignedString(secretKey) +``` + +| 方案 | 安全性 | 复杂度 | 适用场景 | +|------|--------|--------|---------| +| **mTLS** | ⭐⭐⭐⭐⭐ | 中(需 PKI 基础设施) | 服务网格环境、K8s 内部通信 | +| **JWT + Shared Secret** | ⭐⭐⭐⭐ | 低 | 中小规模、快速上手 | +| **mTLS + JWT** | ⭐⭐⭐⭐⭐ | 中高 | 金融级安全要求 | + +> [!tip] 最佳实践 +> 在生产环境中,推荐 **mTLS + JWT 双层防护**:mTLS 保证通道安全,JWT 传递调用方的身份信息(谁在调用)。仅用 JWT 而不加密通道的做法,一旦 TLS 被绕开(如配置错误),所有凭据将暴露于明文。 + +## API 鉴权模型 + +### RBAC (基于角色的访问控制) + +```mermaid +flowchart LR + User["用户"] -->|"拥有"| Role["角色"] + Role -->|"拥有"| Permission["权限"] + Permission -->|"访问"| Resource["资源"] + + Admin["Admin"] --> ReadWrite + Editor["Editor"] --> ReadWrite + Viewer["Viewer"] --> ReadOnly + + ReadWrite --> OrderCRUD + ReadOnly --> OrderRead + + OrderCRUD --> CRUD["Create/Read/Update/Delete"] + OrderRead --> Read["Read Only"] +``` + +### ABAC (基于属性的访问控制) + +当权限决策需要依赖多维属性(部门、地域、时间、资源类型)时,ABAC 是更自然的选择: + +```go +type User struct { + ID string + Role string + Department string + Level int // 职级 +} + +type Resource struct { + ID string + OwnerID string + Department string + Sensitivity int // 1-4, 敏感度等级 +} + +// 策略即代码:灵活定义访问规则 +func canAccess(user User, resource Resource, action string) bool { + rules := []Rule{ + // 同部门员工可读本部门文档 + {Condition: user.Department == resource.Department && user.Level >= resource.Sensitivity, Action: "read"}, + // 管理员或资源所有者可写 + {Condition: user.Role == "admin" || user.ID == resource.OwnerID, Action: "write"}, + } + + for _, r := range rules { + if r.Condition && r.Action == action { + return true + } + } + return false +} +``` + +> [!tip] RBAC vs ABAC 选型指南 +> | 维度 | RBAC | ABAC | +> |------|------|------| +> | **适用规模** | 中小团队,角色边界清晰 | 大型组织,权限维度复杂 | +> | **管理方式** | 预定义角色 + 分配 | 编写动态策略规则 | +> | **扩展性** | 新增场景 → 新增角色 → 组合爆炸 | 新增场景 → 新增规则,不影响现有 | +> | **落地成本** | 低(JWT claims 中直接携带 role) | 中(需策略引擎,如 OPA / Casbin) | +> +> 实际项目中常采用 **RBAC 为主,ABAC 补充关键资源**的混合模式。 + +## 输入校验与防攻击 + +微服务架构中,输入校验是最后一道防线。无论上游做了什么,每个服务都应独立校验进入边界的输入数据。 + +> [!question] 如果网关已经做了鉴权和限流,下游服务还需要校验输入吗? +> **必须校验**。网关只负责基础设施级别的防护(认证、限流),业务层面的合法性(如字段格式、长度、枚举值)应由各服务自行把关。 + +### SQL 注入防护 + +```go +// ❌ 危险:字符串拼接 +query := fmt.Sprintf("SELECT * FROM users WHERE name = '%s'", userInput) + +// ✅ 安全:参数化查询 +rows, err := db.Query("SELECT * FROM users WHERE name = ?", userInput) +``` + +### XSS 与 CSP 头 + +```go +// Go HTML template 自动转义 +tpl.ExecuteTemplate(w, "page.html", data) // {{.Name}} 自动 HTML 转义 + +// JSON API 天然免疫(Content-Type: application/json) + +// 强制浏览器遵守 CSP 策略 +w.Header().Set("Content-Security-Policy", "default-src 'self'") +``` + +### 常见攻击防护清单 + +| 威胁 | 防护手段 | 实现位置 | +|------|---------|---------| +| **SQL 注入** | 参数化查询 / ORM | 数据访问层 | +| **XSS** | 输出转义 / CSP Header | Web 框架 / Gateway | +| **CSRF** | SameSite Cookie / CSRF Token | 网关 / Session | +| **DDoS** | 限流 + WAF | API Gateway | +| **重放攻击** | Nonce + Timestamp | API 签名中间件 | +| **凭证泄露** | HTTPS + Secure Cookie | 传输层 | + +## API 签名(防重放攻击) + +对高安全要求的 API(如支付、转账),客户端需要对请求做 HMAC 签名,服务端验证签名的完整性: + +```mermaid +sequenceDiagram + participant C as 客户端 + participant S as 服务端 + + C->>C: 构造请求体 + timestamp + nonce + C->>C: Signature = HMAC(Secret, Method+URL+TS+Body) + C->>S: GET /api/pay?ts=...&nonce=...&sig=... + + S->>S: 1. 检查 timestamp 是否过期 + alt 已过期 + S-->>C: 401 Request Expired + else 未过期 + S->>S: 2. 检查 nonce 是否已使用 + alt nonce 已存在 + S-->>C: 409 Replay Detected + else 新 nonce + S->>S: 3. 重新计算签名并比对 + alt 一致 + S-->>C: 200 OK + else 不一致 + S-->>C: 403 Invalid Signature + end + end + end +``` + +### Go 实现示例 + +```go +// 客户端:生成签名 +func signRequest(secret string, method, url, body string, ts int64, nonce string) string { + payload := fmt.Sprintf("%s|%s|%d|%s|%s", method, url, ts, nonce, body) + mac := hmac.New(sha256.New, []byte(secret)) + mac.Write([]byte(payload)) + return base64.StdEncoding.EncodeToString(mac.Sum(nil)) +} + +// 服务端:签名验证中间件 +func verifySignature(secret string) middleware.Handler { + return func(next http.HandlerFunc) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + ts := getParam(r, "timestamp") + nonce := getParam(r, "nonce") + sig := getParam(r, "signature") + + // 1. 时间窗口校验 (±5 min) + if time.Since(time.Unix(ts, 0)).Abs() > 5*time.Minute { + http.Error(w, "request expired", http.StatusUnauthorized) + return + } + + // 2. Nonce 去重 (Redis SETNX, TTL=300s) + key := fmt.Sprintf("nonce:%s", nonce) + if !redis.SetNX(key, "1", 300*time.Second) { + http.Error(w, "replay detected", http.StatusConflict) + return + } + + // 3. 签名比对 + expected := signRequest(secret, r.Method, r.URL.String(), r.Body, ts, nonce) + if !hmac.Equal([]byte(sig), []byte(expected)) { + http.Error(w, "invalid signature", http.StatusForbidden) + } + next(w, r) + } + } +} +``` + +> [!note] Nonce 设计要点 +> - 每次请求必须携带唯一的 `nonce`(随机字符串或递增序号) +> - 服务端用 Redis `SETNX` 记录已使用的 nonce,TTL 略大于时间窗口(如 300s) +> - 对于高频调用场景,可考虑基于布隆过滤器优化存储空间 + +## 关联笔记 + +- [[02-服务治理/01-API网关]] — 网关层的统一鉴权和限流 +- [[02-服务治理/04-服务发现]] — 服务注册时也需要安全认证 +- [[hzh/MS/API 设计原则]] — API 设计中的安全考量 diff --git a/hhs/MS/02-服务治理/03-分布式追踪.md b/hhs/MS/02-服务治理/03-分布式追踪.md new file mode 100644 index 0000000..914f6ec --- /dev/null +++ b/hhs/MS/02-服务治理/03-分布式追踪.md @@ -0,0 +1,552 @@ +--- +tags: [microservice, distributed-tracing, opentelemetry, jaeger, skywalking] +create time: 2026-05-05 14:30 +--- + +# 分布式链路追踪 + +## 概述 + +单个服务的日志只能告诉你局部信息。分布式链路追踪将一次请求跨越多个服务的完整调用链串起来,形成全局视图。 + +> [!question] 定位问题的困难 +> 用户反映下单慢,你的系统由订单、支付、库存、会员 4 个服务串联而成。没有工具的情况下,你要怎么知道是哪个服务拖慢了整体响应时间? + +## 核心概念 + +```mermaid +flowchart TB + Req["请求 trace_id=abc123"] --> Span1["Span #1 API Gateway 5ms"] + Span1 --> Span2["Span #2 Order Service 70ms"] + Span1 --> Span3["Span #3 User Service 15ms"] + Span2 --> Span4["Span #4 Inventory DB Query 60ms"] + + style Span2 fill:#ff9999 + style Span4 fill:#ffcc99 +``` + +| 概念 | 说明 | +|------|------| +| **Trace** | 一次完整请求的调用链,由一个唯一的 `trace_id` 标识 | +| **Span** | 链路中的一个执行片段(如一次 HTTP 调用、一次 SQL 查询),有独立的 `span_id` | +| **Parent-Child** | Span 之间通过 `parent_span_id` 建立父子关系,形成树状结构 | +| **Context Propagation** | 通过 Header 传递 `trace_id`/`span_id`,贯穿整条链路 | +| **Sampling** | 不是每条请求都采样,按比例或策略选择,降低存储开销 | + +### Trace/Span 数据结构 + +```json +{ + "traceId": "abc123def456...", + "spans": [ + { + "spanId": "span-001", + "parentId": null, + "operationName": "POST /orders", + "startTime": "2026-05-05T10:00:00.000Z", + "durationMs": 120, + "tags": { + "http.method": "POST", + "http.url": "/api/orders", + "http.status_code": 200 + }, + "logs": [ + { + "timestamp": "2026-05-05T10:00:00.050Z", + "fields": [{"key": "event", "value": "order.created"}] + } + ] + }, + { + "spanId": "span-002", + "parentId": "span-001", + "operationName": "GetUserById (gRPC)", + "startTime": "2026-05-05T10:00:00.010Z", + "durationMs": 15, + "tags": { + "rpc.system": "grpc", + "rpc.service": "UserService", + "rpc.method": "GetUser" + } + } + ] +} +``` + +## 上下文传播 (Context Propagation) + +`trace_id` 如何从上游服务传递到下游?核心原则:**调用者创建上下文 → 被调者提取上下文 → 在自身链路中继续使用并透传到更下游**。 + +### W3C Trace Context 标准 + +这是业界事实标准([W3C Recommendation](https://www.w3.org/TR/trace-context/)),被 OpenTelemetry、Jaeger、SkyWalking 全部支持。 + +```json +// HTTP Header 中实际传递的字段 +{ + "traceparent": "00-abc123def456...-789ghi012jkl-01", + "tracestate": "congo=t61rcWkgMzE,vendor=value" +} +``` + +| 字段 | 格式 | 说明 | +|------|------|------| +| `version` | 2 字符十六进制 | 版本号,目前固定 `00` | +| `trace_id` | 32 字符十六进制 | 128-bit 全局唯一标识,建议用 ULID / Snowflake 生成 | +| `span_id` | 16 字符十六进制 | 64-bit 本段 span 的唯一标识 | +| `flags` | 2 字符十六进制 | 目前仅 `01` 表示已采样 | + +**`tracestate`**:供厂商扩展使用,例如用于在多家 tracing 系统间同步采样决策或路由信息。多个厂商以逗号分隔。 + +### HTTP 场景:中间件自动注入与提取 + +```go +// OpenTelemetry SDK 自动处理 context 注入和提取 +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // ① 从入站请求提取 trace context(如果请求中没有 traceparent,会生成新的 trace) + ctx := propagator.Extract(r.Context(), headerReader{r.Header}) + + // ② 在当前服务内创建新 span + ctx, span := tracer.Start(ctx, "handleOrder") + defer span.End() + + // ③ 将 trace context 注入到出站请求 —— 关键一步 + r = r.WithContext(ctx) + propagator.Inject(ctx, headerWriter{r.Header}) + + next.ServeHTTP(w, r) + }) +} +``` + +```mermaid +sequenceDiagram + participant C as Client + participant A as Service A + participant B as Service B + + C->>A: GET /api/order\ntraceparent=00-abc... + Note over A: Extract create Span A + A->>B: POST /inventory\nInject traceparent + Note over B: Extract create Span B + B-->>A: Response + A-->>C: Response +``` + +### gRPC 场景:Metadata 透传 + +gRPC 基于 HTTP/2,传播方式类似,但使用的是 **gRPC Metadata** 而非 HTTP Headers(gRPC 库内部会将 metadata 转为 HTTP Header)。 + +```go +// === 调用方:自动从 context 提取 trace 信息写入 metadata === +ctx, span := tracer.Start(parentCtx, "callUserService") +defer span.End() + +// otelgrpc.Interceptor 会自动完成 Inject,无需手动处理 +conn, err := grpc.DialContext(ctx, "user-service:9090", + grpc.WithTransportCredentials(insecure.NewCredentials()), + grpc.WithStatsHandler(otelgrpc.NewClientHandler()), // 自动注入 +) +client := userpb.NewUserServiceClient(conn) +resp, err := client.GetUser(ctx, &userpb.GetRequest{Id: "123"}) + +// === 被调方:拦截器自动完成 Extract === +server := pb.NewUserServiceServerImpl() +grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerHandler()), // 自动提取 +).Serve(listener) +``` + +> [!note] 为什么不直接用 HTTP Headers? +> gRPC 对 metadata key 有严格的大小写规范——全部小写且不支持连字符。因此 `traceparent` 这样的 Header 名无法直接映射到 gRPC Metadata,各 tracing SDK 内部做了适配层,开发者只需关注 `context`,不需要手动操作 metadata。 + +### MQ 场景:消息属性透传 + +消息队列没有标准化的上下文传播协议,需要**在消息属性中手动携带 trace 信息**。OpenTelemetry 提供了标准的 attribute 命名约定。 + +```go +// === RocketMQ 生产者:注入 trace context === +msg := rocketmq.NewMessage("order-created", payload) + +// 标准 OTel 属性名 +msg.WithProperty(semconv.RPCSystemKey, "rocketmq") +msg.WithProperty(semconv.MessagingSystemKey, "rocketmq") +msg.WithProperty(semconv.HTTPFlavorKey, "1.1") + +// 手动注入 trace_id 和 span_id +if sc, ok := trace.SpanFromContext(ctx).SpanContext(); ok && sc.IsValid() { + msg.WithProperty("traceparent", fmt.Sprintf( + "00-%s-%s-01", sc.TraceID().String(), sc.SpanID().String(), + )) +} + +producer.SendSync(msg) + +// === RocketMQ 消费者:恢复 trace context === +consumer.Receive(func(ctx context.Context, messages ...*rocketmq.Message) error { + for _, msg := range messages { + // 从消息属性中提取 traceparent + tp := msg.GetProperty("traceparent") + if tp != "" { + ctx = propagator.Extract(ctx, textmap.Reader(func(k string) string { + return tp // 模拟 header reader + })) + } else { + // 补偿策略:如果上游没带 trace 信息,作为新 trace 起点 + ctx = trace.ContextWithRemoteSpanContext(ctx, + trace.NewSpanContext(trace.SpanContextConfig{ + TraceID: generateFallbackTraceID(), + Remote: true, + }), + ) + } + + _, span := tracer.Start(ctx, "processOrderCreated", + trace.WithSpanKind(trace.SpanKindConsumer)) + span.SetAttributes( + semconv.MessagingSystemKey, "rocketmq", + semconv.MessagingDestinationKey, msg.Topic, + semconv.MessagingMessageIDKey, msg.MsgID, + ) + + doProcess(ctx, msg.Body) + span.End() + } + return nil +}, rocketmq.ConsumeFunc()) +``` + +> [!tip] 补偿策略:异步解耦时可能出现"上游未传 trace"的情况 +> +> 当 MQ 消息来自非追踪系统或上游漏传了 trace_id 时,消费者应作为 **新 Trace 的根 Span** 启动,而不是丢弃消息。同时通过属性标记 `messaging.message.is_remote=true` 标明这是一个远程关联点。 + +### 同步并发:Goroutine 间的 Context 传播 + +Go 的 `context.Context` 是协程安全的,子 Goroutine 直接使用父级 context 即可自动继承 trace。关键在于**不能丢失 context 引用**。 + +```go +func handleOrder(ctx context.Context, w http.ResponseWriter, r *http.Request) { + _, span := tracer.Start(ctx, "handleOrder") + defer span.End() + + // ✅ 正确:直接传入同一 context,子 goroutine 自动继承 trace + var wg sync.WaitGroup + for _, item := range order.Items { + wg.Add(1) + go func(item OrderItem) { + defer wg.Done() + processItem(ctx, item) // ctx 携带 trace context + }(item) + } + wg.Wait() + + // ❌ 错误:创建了无父 context 的新 context,trace 断裂 + // go func() { + // _, childSpan := tracer.Start(context.Background(), "processItem") + // ... + // }() +} +``` + +```mermaid +flowchart TB + subgraph Main["主 Goroutine"] + S0["Span #1 handleOrder"] + S0 --> S1["Span #2 proc Item A"] + S0 --> S2["Span #3 proc Item B"] + S0 --> S3["Span #4 proc Item C"] + end + + style S0 fill:#bbddff + style S1 fill:#ccf2ff + style S2 fill:#ccf2ff + style S3 fill:#ccf2ff +``` + +> [!warning] 常见陷阱 +> +> 1. **忘记传入 context**:用 `context.Background()` 或 `context.TODO()` 启动了 Goroutine,导致 trace 断裂。 +> 2. **超时覆盖**:子 Goroutine 中设置了独立的 `context.WithTimeout` 但没有保留父级的 deadline,导致整体超时无效。 +> 3. **recover 后丢失 context**:`defer recover()` 中如果有错误上报逻辑,仍需持有原始 context。 + +### 额外负载:Baggage 机制 + +除了 trace/span 信息,有时需要在链路间传递**业务标签**(如用户 ID、租户 ID、A/B 测试分组),这通过 **Baggage** 实现。 + +```go +// === 入口服务:设置 baggage === +baggage, _ := baggage.New(baggage.Pair("user.id", "u-12345"), + baggage.Pair("tenant", "acme-corp")) +ctx = propagator.Inject(ctx, baggageInjector{baggage}) + +// === 中间任意服务:读取 baggage === +baggage = baggage.FromContext(ctx) +userID, _ := baggage.Value("user.id") +// userID == "u-12345" + +// === 下游服务:也能读到同样的 baggage === +``` + +| 特性 | Baggage | Span Attributes | +|------|---------|----------------| +| 传播范围 | 沿整条链路传递给所有下游 | 仅记录在本地 Span 中,不传播 | +| 大小限制 | 总计 512 字节(防止 Header 膨胀) | 无限制 | +| 典型用途 | 用户 ID、租户、环境标记 | 状态码、耗时、SQL 语句等遥测数据 | +| 是否计入 Span Duration | 否 | 否 | + +> [!caution] Baggage 安全须知 +> +> Baggage 会被编码为 HTTP Header 随每个请求传播,**绝不能包含敏感信息**(密码、Token、PII)。默认 512 字节上限也意味着不适合传递大量数据。如需传递大对象,请改为通过 `span.RecordError()` 或独立日志存储。 + +### 边界的判断:何时创建新 Trace + +并非所有场景都需要延续上游 trace。以下情况应考虑开启新 trace: + +```mermaid +quadrantChart + title 跨系统调用:是否延续 Trace? + x-axis Low Coupling --> High Coupling + y-axis New Trace --> Continue Trace + "健康检查 / Scrape": [0.2, 0.15] + "定时任务 / Cron": [0.15, 0.1] + "MQ 消息异步解耦": [0.4, 0.7] + "正常 HTTP/gRPC 请求": [0.85, 0.9] + "第三方回调": [0.5, 0.25] + "消息积压重新消费": [0.3, 0.3] +``` + +| 场景 | 行为 | 说明 | +|------|------|------| +| 正常 HTTP / gRPC / MQ 请求 | 延续上游 trace_id | 标准流程,Extract → Continue | +| 健康检查 / Prometheus scrape | 新建 trace | 可标记 `tracestate` 为 healthcheck | +| 定时任务 / Cron job | 新建 trace | 无上游 context,天然无父 span | +| 消息积压重新消费(跨越数天) | 新 trace + baggage 引用原 trace_id | 保留追溯关联 | +| 第三方回调(外部系统主动推送) | 新建 trace,通过 callback_id 间接关联 | 无法 Extract,只能新建 | +| 伪造 traceparent | 验证格式合法性,非法则忽略并新建 | 记录告警,防止注入攻击 | + +**决策流程:** + +```mermaid +flowchart LR + Incoming["Incoming Request"] --> HasTrace{"是否有有效 traceparent"} + HasTrace -->|"是"| Extract["Extract 并继续"] + HasTrace -->|"否"| NewTrace["创建新 trace"] + + Extract --> Valid{"格式合法"} + Valid -->|"是"| Continue["延续 trace"] + Valid -->|"否"| LogWarn["记录告警丢弃伪造 context"] + LogWarn --> NewTrace + + NewTrace --> MarkBaggage["可选 baggage 携带原 trace_id 作引用"] + MarkBaggage --> StartRoot["启动根 Span"] +``` + +## 主流方案对比 + +### 方案全景图 + +```mermaid +quadrantChart + title 三大追踪方案特性对比 + x-axis Low Invasiveness --> High Flexibility + y-axis All-in-one APM --> Modular Components + "SkyWalking": [0.25, 0.75] + "OpenTelemetry": [0.75, 0.85] + "Jaeger": [0.6, 0.35] +``` + +| 方案 | 协议 | 存储后端 | 侵入程度 | 特色能力 | +|------|------|---------|---------|---------| +| **OpenTelemetry** | OTLP | Prometheus/Jaeger/Zipkin | SDK + Auto-Instrumentation | CNCF 标准,厂商中立,未来趋势 | +| **Jaeger** | Jaeger native | Cassandra/Elasticsearch | Agent / SDK | Uber 开源,UI 友好,支持业务 Tags | +| **SkyWalking** | SkyWalking | MySQL/Elasticsearch/ES | 零侵入 Java Agent | 国产,中文文档完善,APM 一体 | + +### 演进路线:从单体到 OTel + +```mermaid +flowchart LR + subgraph Phase1["Phase 1 无 Tracing"] + P1["各服务独立日志\n排查靠翻日志和SSH"] + end + + subgraph Phase2["Phase 2 Jaeger/SkyWalking 自研"] + P2["特定语言适配\n链路覆盖不全"] + end + + subgraph Phase3["Phase 3 OpenTelemetry 统一"] + P3["SDK 统一\nAuto-Instrumentation\nCollector 收集"] + end + + P1 ==> P2 ==> P3 + + note["推荐目标 OTel Collector 灵活后端\n不锁死任何组件按需替换"] + note -.-> P3 +``` + +> [!tip] 选型建议 +> 新项目优先选 **OpenTelemetry**——它是行业标准,未来会被所有工具支持。如果团队需要开箱即用的 APM,**SkyWalking** 的 Java Agent 零侵入方案是快速上手的最佳选择。 + +## OpenTelemetry Go 实战 + +```go +// 初始化 TracerProvider +provider := sdktrace.NewTracerProvider( + sdktrace.WithBatcherExporter(exporter), // 异步批量上报,不阻塞 +) +defer provider.Shutdown(context.Background()) + +tracer := provider.Tracer("order-service") + +func handleOrder(w http.ResponseWriter, r *http.Request) { + ctx, span := tracer.Start(r.Context(), "handleOrder") + defer span.End() + + // 设置丰富的属性,方便查询和过滤 + span.SetAttributes( + attribute.String("http.method", r.Method), + attribute.Int("http.status_code", http.StatusOK), + attribute.String("user.id", getUserID(r)), + ) + + // 调用下游服务(trace context 自动传播) + resp, err := callInventoryService(ctx) + if err != nil { + span.RecordError(err) + span.SetStatus(codes.Error, err.Error()) + w.WriteHeader(http.StatusBadGateway) + return + } + w.Write(resp.Body) +} +``` + +## 采样策略 + +> [!question] 为什么需要采样? +> 假设日活 100 万用户,每个用户产生 10 次请求 = 每天 1 千万条 Trace。如果全部存储,按每条 Span 平均 500 bytes 算,单张表一天就是 **数 GB**。采样不是偷懒,是**用可控的成本换取可观测性**。 + +### 采样算法 + +OpenTelemetry 提供了两种内置采样器: + +```go +// 静态采样:简单粗暴,适合开发环境 +alwaysOn := sdktrace.AlwaysSample() // 100% 采集 +alwaysOff := sdktrace.NeverSample() // 0% 采集(但仍在 SDK 内创建 span) + +// 动态采样:根据链路状态智能决策 ⭐ 推荐生产使用 +dynamic := sampler.TraceIDRatioBased(0.1) // 10% 随机采样 +``` + +实际项目中通常组合使用 **Parent-Based 分层采样**,确保父子链路一致性: + +```go +provider := sdktrace.NewTracerProvider( + // Parent-based: 父级已采样则子级必采样,反之亦然 + sdktrace.WithSampler( + sdktrace.ParentBased( + sdktrace.TraceIDRatioBased(0.1), // 无父 trace 时默认 10% + ), + ), + sdktrace.WithBatcherExporter(exporter), +) +``` + +### 错误优先采样 + +```go +type errorPrioritySampler struct { + baseRate float64 // 基础采样率 +} + +func (s *errorPrioritySampler) ShouldSample(params sampling.Parameters) sampling.Result { + ctx := params.SpanContext.Context + // 检查当前 span 是否已有 error status + if sc, ok := trace.SpanFromContext(ctx).SpanContext(); ok { + attrs := sc.Attributes() + for _, attr := range attrs { + if attr.Key == "error" && attr.Value.AsBool() { + return sampling.Result{ + Decision: sampling.RecordAndSample, // 100% 采样错误请求 + Attributes: []attribute.KeyValue{}, + } + } + } + } + // 非错误请求走基础采样率 + rate := rand.Float64() + decision := sampling.Drop + if rate < s.baseRate { + decision = sampling.RecordAndSample + } + return sampling.Result{Decision: decision} +} +``` + +```mermaid +flowchart LR + AllReq["全部请求"] --> Sampler{"采样决策"} + Sampler -->|"有error"| CheckError{"错误标记"} + CheckError -->|"是"| Core["核心链路全量采集"] + CheckError -->|"否"| Prob["概率采样"] + Sampler -->|"无parent"| Prob + Prob -->|"命中"| Core + Prob -->|"未命中"| Drop["丢弃"] + + Core --> Storage["Trace 存储"] + Drop --> Log["非采样请求仅记录计数"] +``` + +| 环境 | 采样率 | 理由 | +|------|--------|------| +| **开发 / 测试** | 100% | 方便调试,无存储压力 | +| **生产 - 核心链路** | 100% | 下单、支付等关键路径必须全量 | +| **生产 - 普通路径** | 5~10% | 平衡成本和覆盖率 | +| **生产 - 错误链路** | 100% | 出错时的请求优先保留 | + +> [!note] 基于错误的智能采样 +> +> 高级做法:**正常路径低采样,一旦检测到错误立即提升当前请求的采样率**。这样既省了存储,又能在出问题时有足够的数据回溯。 + +### 采样对应用的影响 + +```mermaid +flowchart LR + subgraph Sampled["被采样的请求比例十百分比"] + S1["完整 Span 数据"] + S2["完整日志关联"] + S3["存储到后端"] + end + + subgraph Dropped["未被采样的请求比例九十百分比"] + D1["SDK 内仍创建 Span"] + D2["不影响业务逻辑"] + D3["上报时直接跳过 Export"] + end + + Note["采样不等于不执行\n采样只影响上报不影响业务"] + Note --> Sampled + Note --> Dropped +``` + +> [!warning] 常见误区 +> +> 1. **"采样后 span 就不创建了"** → 错!SDK 内部仍然创建和记录 span,只是在 `Exporter` 阶段跳过网络上报。 +> 2. **"采样会导致链路断裂"** → 配合 `ParentBased` 可以保证:只要链路上有一条被采样,整条链路的 span 都会被保留。 +> 3. **"统计 P99 会有偏差"** → 正确。低采样率下 P99 估计值会偏低,需用统计学方法做补偿估计。 + +## 渐进式落地路线 + +> [!tip] 不要试图一开始就采集全部 Span +> +> 1. **第一步**:先接 Tracing,覆盖核心链路(下单、支付),rate=100% +> 2. **第二步**:加入 Metrics 监控(Prometheus + Grafana) +> 3. **第三步**:集中 Logging(Loki / ELK),与 trace_id 关联 +> 4. **第四步**:全量上 OpenTelemetry Collector,统一管理 + +## 关联笔记 + +- [[02-服务治理/01-API网关]] — API Gateway 可以在入口处注入 trace_id +- [[04-可观测性/03-链路追踪]] — 更详细的链路追踪设计方法论 +- [[02-服务治理/07-配置管理]] — 配置中心的动态刷新可以联动调整采样率 diff --git a/hhs/MS/02-服务治理/04-服务发现.md b/hhs/MS/02-服务治理/04-服务发现.md new file mode 100644 index 0000000..e5b36d5 --- /dev/null +++ b/hhs/MS/02-服务治理/04-服务发现.md @@ -0,0 +1,237 @@ +--- +tags: [microservice, service-discovery, consul, nacos, etcd, kubernetes] +create time: 2026-05-05 00:00 +--- + +# 服务发现 + +## 概述 + +服务实例的动态变化(扩容、缩容、故障重启)让硬编码地址成为不可能。服务要回答的核心问题是:**我怎么找到你?** + +> [!question] 引出问题 +> 假设你的订单服务有 3 个实例,运行在 `10.0.1.1:8080`、`10.0.1.2:8080`、`10.0.1.3:8080`。当其中一个实例扩容下线时,调用方如何得知最新的地址列表?手动改配置滚动重启?还是……有更好的办法? + +## 两种发现模式 + +```mermaid +graph TB + subgraph Client["客户端发现模式"] + A[Service A] -->|1.查询注册中心| R1[(Registry)] + R1 -->|2.返回实例列表| A + A -->|3.自选实例直连| B[Service B] + noteA["客户端内置负载均衡逻辑"] + B -.-> noteA + end + + subgraph Server["服务端发现模式"] + C[Service C] -->|请求| P[LoadBalancer Proxy] + P -->|查询注册中心| R2[(Registry)] + R2 -->|返回实例| P + P -->|转发| D[Service D] + noteC["客户端无感知,透明代理"] + D -.-> noteC + end +``` + +**对比**: + +| 维度 | 客户端发现 | 服务端发现 | +|------|-----------|-----------| +| 典型实现 | Eureka、Consul、Nacos | Nginx、Envoy、K8s Service | +| 耦合度 | 客户端嵌入注册逻辑 | 透明代理,客户端无感知 | +| 性能 | 直连调用,延迟最低 | 多一跳代理开销 | +| 运维复杂度 | 每门语言需独立 SDK | 统一部署代理,技术无关 | +| 灵活性 | 客户端可自定义路由策略 | 受限于代理能力 | + +### 客户端发现的优劣势分析 + +**优势**: +- 直连调用,少了一跳网络开销 +- 客户端可以根据本地缓存做智能选择(如就近节点优先) +- 框架成熟,Eureka / Consul 社区案例丰富 + +**劣势**: +- 每个服务实现中都需要集成 SDK,跨语言迁移成本高 +- 客户端需要处理实例列表的缓存与刷新逻辑 +- 新增服务时需要修改调用方的代码或配置 + +### 服务端发现的优劣势分析 + +**优势**: +- 完全解耦——调用方只需知道 Proxy 地址 +- 统一治理入口,可以在代理层加限流、熔断等逻辑 +- 适合多语言团队,无需各自维护 SDK + +**劣势**: +- 额外一跳增加延迟(通常 <1ms,高吞吐场景累积明显) +- Proxy 本身成为单点瓶颈,必须做横向扩展 +- 调测复杂度上升——出了问题要看两层日志 + +## 主流注册中心选型 + +### Nacos(推荐国内团队首选) + +阿里开源,支持 AP(临时实例)+ CP(持久实例)双模型,同时提供配置管理功能。 + +```go +// Nacos 服务注册与实例拉取 +import ( + "github.com/nacos-group/nacos-sdk-go/v2/clients" + "github.com/nacos-group/nacos-sdk-go/v2/common/constant" + "github.com/nacos-group/nacos-sdk-go/v2/vo" +) + +// 1. 创建服务端配置(连接哪个 Nacos) +sc := constant.ServerConfig{ + IpAddr: "127.0.0.1", + Port: 8848, +} +``` + +> 上面仅展示了 serverConfig,实际创建时还需传入 `ClientConfig`(命名空间、超时等)。为简洁起见此处省略。 + +```go +// 2. 创建 NamingClient +client, _ := clients.NewNamingClient( + map[string]any{"serverConfigs": []constant.ServerConfig{sc}}, +) + +// 3. 注册服务实例(默认临时实例,走心跳保活) +_ = client.RegisterInstance(vo.RegisterInstanceParam{ + Ip: "10.0.0.1", + Port: 8080, + ServiceName: "order-service", + ClusterName: "DEFAULT", +}) + +// 4. 拉取某服务的可用实例列表(同步快照,含健康过滤) +instances, _, _ := client.SelectInstances(vo.SelectInstanceParam{ + ServiceName: "order-service", + HealthyOnly: true, // 只返回健康实例 +}) +for _, inst := range instances { + // inst.Ip: inst.Port — 拿到后可直接发起 HTTP / gRPC 调用 +} +``` + +### Consul + +HashiCorp 出品,基于 Raft 共识协议(CP),天然支持健康检查和 DNS 接口。 + +**核心特点**: +- 内建 KV 存储,可作为轻量配置中心 +- 支持 multi-datacenter 部署 +- DNS 接口:`order-service.service.consul` 可直接解析 IP + +**与 Nacos 选型对比**: + +| 维度 | Consul | Nacos | +|------|--------|-------| +| 一致性模型 | CP(Raft) | AP + CP 可切换 | +| 部署复杂度 | 需独立部署 Agent(Sidecar 模式) | Go SDK 内置,无额外代理 | +| 生态集成 | Terraform / Vault 深度绑定 | Spring Cloud / Dubbo 原生支持 | +| 适用场景 | 多云 / 混合云、基础设施层 | Java 技术栈、业务服务层 | + +> [!tip] Consul Sidecar 模式 +> Consul Agent 通常以 DaemonSet 方式跑在每台节点上,应用通过 localhost 向本地 Agent 注册。这种 **Sidecar 模式**天然适配服务端发现架构——应用把 Consul Agent 当作"本地注册中心"即可。 + +### etcd + K8s Service + +纯容器环境下最自然的选择。etcd 存数据,K8s 提供完整的发现和负载均衡体系。 + +```yaml +# K8s Service — 服务端发现模式的代表 +apiVersion: v1 +kind: Service +metadata: + name: order-service +spec: + selector: + app: order + ports: + - port: 80 + targetPort: 8080 + type: ClusterIP +``` + +调用方只需 `http://order-service:80`,K8s 通过 iptables/IPVS 自动实现负载均衡。 + +### Eureka + +Netflix 开源,AP 模型。**注意**:Eureka 2.x 已开源关闭,不建议新项目使用。已有系统在迁移到 Nacos 或 Consul 之前可以继续用。 + +## 关键机制 + +### 服务注册流程 + +```mermaid +sequenceDiagram + participant S as 服务实例 + participant R as 注册中心 + participant H as 健康检查器 + + S->>R: 1. Register(instance) + R-->>S: 2. ACK + Note over S,R: 实例写入内存 / etcd + + loop 定时心跳 + S->>R: 3. Heartbeat (每 5s) + R-->>S: 4. ACK + end + + R->>H: 5. 超过心跳间隔未收到 → 标记不健康 + H->>R: 6. 摘除实例 +``` + +### 临时实例 vs 持久实例 + +| 类型 | 宕机检测方式 | 数据存储 | 适用场景 | +|------|-------------|---------|---------| +| **临时实例 (ephemeral)** | 心跳超时自动摘除 | 内存 | Web 服务、应用实例 | +| **持久实例 (persistent)** | 主动注销才摘除 | etcd | 数据库连接、外部依赖 | + +> [!warning] 常见陷阱 +> 把数据库当成临时实例注册 —— 数据库挂了不需要被 "检测到",它自己控制启停。应该用持久实例。 + +### 优雅上下线 + +服务下线时不能直接断网,否则正在处理的请求会失败。 + +**上线流程**: +1. 进程启动 → 加载配置、初始化连接池 +2. `/ready` 探针返回 200 → 注册中心注册 + K8s 加入负载均衡池 +3. 开始接收流量 + +**下线流程**: +1. 注册中心注销实例 → 停止接收新请求 +2. 等待正在处理的请求完成(最大等待 N 秒) +3. 关闭连接池、保存状态 +4. 进程退出 + +> [!tip] K8s Pod 优雅终止 +> 以 sidecar 方式集成 Consul Agent 时,Pod 配置如下: +> ```yaml +> spec: +> terminationGracePeriodSeconds: 30 # 最多等 30 秒再 SIGKILL +> initContainers: +> - name: consul-init # 先启动 Sidecar +> image: consul:latest +> command: ["consul", "agent", "-bind", "-config-file=/etc/consul.d/server.json"] +> containers: +> - name: app +> lifecycle: +> preStop: +> exec: +> command: ["sh", "-c", "sleep 15"] # 先停流量(让 LB / Consul 摘流) +> postStart: +> exec: +> command: ["sh", "-c", "sleep 5"] # 等 Sidecar 就绪后再开流量 +> ``` +> 关键点:**preStop sleep** 留给 LB / Consul 摘流时间,**postStart sleep** 确保 Sidecar 已准备好再做健康检查。 + +## 关联笔记 + +- [[02-服务治理/01-API网关]] — API Gateway 是服务发现的用户之一 +- [[02-服务治理/06-容错模式]] — 健康检查是容错体系的基础 +- [[05-部署运维/02-Kubernetes]] — K8s Service 的服务发现原理 diff --git a/hhs/MS/02-服务治理/05-服务间通信.md b/hhs/MS/02-服务治理/05-服务间通信.md new file mode 100644 index 0000000..231587d --- /dev/null +++ b/hhs/MS/02-服务治理/05-服务间通信.md @@ -0,0 +1,204 @@ +--- +tags: [microservice, rpc, rest, grpc, message-queue, event-driven] +create time: 2026-05-05 +--- + +# 服务间通信 + +## 概述 + +微服务之间如何通信,是架构设计的核心决策之一。选错了通信方式,后面会带来一系列问题:耦合度太高、性能瓶颈、或者运维复杂度爆炸。 + +## RPC vs RESTful API + +### 对比分析 + +| 维度 | REST over HTTP/JSON | gRPC (HTTP/2) | GraphQL | +|------|---------------------|---------------|---------| +| **性能** | 序列化开销大 | Protocol Buffers,二进制高效 | 中等(JSON) | +| **人类可读** | URL + JSON 直观 | 需要 Proto 定义辅助理解 | 查询语言自描述 | +| **语言兼容性** | 广泛,任何能发 HTTP 的语言都能用 | 需要代码生成 | 需客户端 SDK | +| **缓存** | 天然支持 HTTP 缓存 | 不支持(HTTP/2 多路复用替代) | 可设计缓存层 | +| **版本管理** | URL 路径 (/v1/, /v2/) | Proto 文件向前兼容 | Schema 演进工具 | +| **适用场景** | 对外 API、跨团队/跨组织调用 | 内部服务间高频强类型调用 | 前端聚合查询、移动端 | + +```mermaid +graph TB + subgraph "何时选什么?" + D{"你的场景是?"} + + D -->|"对外暴露 API"| REST["REST over HTTP"] + D -->|"内部服务高频调用"| GRPC["gRPC"] + D -->|"前端/移动端复杂查询"| GraphQL["GraphQL"] + D -->|"异步解耦通知"| MQ["消息队列"] + end +``` + +### 混合通信模式(推荐) + +实际工程中很少单一选型。**最佳实践是混合使用**: + +```mermaid +sequenceDiagram + participant C as 客户端 + participant GW as API Gateway + participant O as Order Service + + note over C,O: 外部请求 — 用 REST/gRPC + C->>GW: POST /orders (HTTP/JSON) + GW->>O: CreateOrder (gRPC) + + note over O,O: 内部处理 — 同步 + 异步组合 + O->>O: 创建订单记录 (本地事务) + O-->>GW: {orderId: "ORD-001"} + GW-->>C: 200 OK + + O->>O: Publish OrderCreated Event (MQ) + Note right of O: 通知类场景异步处理 +``` + +## 同步通信 + +### gRPC 协议详解 + +gRPC 基于 Protocol Buffers(Proto3),利用 HTTP/2 的多路复用特性,非常适合高性能的内部服务间调用。 + +> [!tip] 深入阅读 +> [[06-gRPC]] — gRPC 深度指南,涵盖 Proto 进阶、流式调用、Interceptor、连接管理、生产最佳实践。 + +```go +// proto/order/v1/order.proto +syntax = "proto3"; +package order.v1; + +service OrderService { + rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse); + rpc GetOrder(GetOrderRequest) returns (Order); + rpc StreamOrders(StreamRequest) returns (stream Order); // 服务端流 +} + +message CreateOrderRequest { + string user_id = 1; + repeated Item items = 2; + float total = 3; +} + +message Item { + string product_id = 1; + int32 quantity = 2; +} + +enum OrderStatus { + UNKNOWN = 0; + PENDING = 1; + PAID = 2; + SHIPPED = 3; + CANCELLED = 4; +} +``` + +**Proto3 注意事项**: +- 字段编号(`= 1`, `= 2`)不能重编——破坏向后兼容 +- 新增字段标记为 `optional` 时 Proto3 才能区分"未设置"和"默认值" +- `repeated` 字段空数组不会省略,需要特殊处理 + +### RESTful API 设计规范 + +``` +GET /api/v1/orders → 获取订单列表 +POST /api/v1/orders → 创建订单 +GET /api/v1/orders/{id} → 获取订单详情 +PUT /api/v1/orders/{id} → 更新订单(全量替换) +PATCH /api/v1/orders/{id} → 部分更新 +DELETE /api/v1/orders/{id} → 删除订单 +``` + +**核心原则**: +- **资源名词化**:URL 用名词复数,不用动词 +- **无状态**:每个请求携带完整上下文(通过 Header 传 Token) +- **统一接口**:用标准 HTTP Method + Status Code 表达语义 + +## 异步通信 + +### 事件驱动架构 + +> [!question] 关键时刻的判断 +> 用户点击"提交订单"后,系统要做:① 创建订单记录 ② 扣减库存 ③ 扣款 ④ 发送短信通知。哪些步骤必须同步完成?哪些可以异步处理?为什么? + +**答案**:①~③需要立即得到结果或保证一致性,走同步流程;④纯粹的通知类场景完全异步,用消息队列解耦。即使扣款失败,短信也没必要发出。 + +```mermaid +graph LR + A["订单创建"] --> B{"是否需要同步响应?"} + B -->|"是"| Sync["同步 RPC 链"] + B -->|"否"| Async["异步 MQ 事件"] + + Sync --> Stock["查库存 (RPC)"] + Stock --> Pay["支付 (RPC)"] + + Async --> SMS["发短信 (Event)"] + Async --> Points["加积分 (Event)"] + Async --> Email["发确认邮件 (Event)"] +``` + +### 消息队列选型 + +| 方案 | 吞吐量 | 延迟 | 可靠性 | 特色能力 | +|------|--------|------|--------|---------| +| **RocketMQ** | 极高 (百万级) | 毫秒级 | 事务消息支持 | 阿里出品,国内首选 | +| **Kafka** | 最高 (千万级) | 秒级 | 持久化 | 大数据生态集成 | +| **RabbitMQ** | 中等 (十万级) | 亚毫秒 | 高 | 灵活的路由模型 | +| **Pulsar** | 高 | 毫秒级 | 高 | 存储计算分离 | + +### 发布订阅 vs 点对点 + +```mermaid +graph TB + Publisher[生产者] --> Broker[(MQ Broker)] + + Broker --> Sub1["消费者组 A
Topic: order-events"] + Broker --> Sub2["消费者组 B
Topic: order-events"] + Broker --> Sub3["消费者组 C
Topic: order-events"] + + style Sub1 fill:#cfe2f3 + style Sub2 fill:#d9e2f3 + style Sub3 fill:#e2d9f3 +``` + +- **发布订阅 (Pub/Sub)**:一个消息被多个消费组分别消费,每个组内只有一个实例收到 +- **点对点 (Queue)**:一条消息只被一个消费者处理 + +## 同步与异步的组合拳 + +```mermaid +sequenceDiagram + participant Client + participant Gateway + participant Order + participant MQ + participant Notify + participant Analytics + + Client->>Gateway: 下单请求 + Gateway->>Order: 创建订单 + Order->>Order: 写库 + 出消息(事务) + Order-->>Gateway: 返回 orderId + Gateway-->>Client: 202 Accepted ⚡ + + Note over MQ: 异步后续处理 + MQ->>Notify: 发送通知 + MQ->>Analytics: 数据统计 + + Note right of Client: 用户体验好:
主流程快速响应
非关键任务后台处理 +``` + +> [!keypoint] 关键洞察 +> - 同步链路:**gRPC 直连**,延迟低,适合核心事务链 +> - 异步链路:**消息队列**,解耦非关键路径,即使失败也不影响主干 +> - 核心原则:**同步保性能,异步保解耦** + +## 关联笔记 + +- [[02-服务治理/01-API网关]] — API Gateway 负责协议转换 +- [[03-数据一致性/02-分布式事务]] — 消息投递的一致性保障 +- [[05-部署运维/02-Kubernetes]] — Sidecar 代理透明拦截通信流量 diff --git a/hhs/MS/02-服务治理/06-容错模式.md b/hhs/MS/02-服务治理/06-容错模式.md new file mode 100644 index 0000000..ea2adf9 --- /dev/null +++ b/hhs/MS/02-服务治理/06-容错模式.md @@ -0,0 +1,632 @@ +--- +tags: [microservice, circuit-breaker, retry, rate-limiting, bulkhead, health-check, fallback, chaos-engineering] +create time: 2026-05-05 10:30 +--- + +# 容错模式 + +## 概述 + +微服务最大的挑战是 **网络不可靠**。分布式系统的每个远程调用都可能超时、被拒绝、或部分成功。必须提前设计降级和恢复策略。 + +```mermaid +graph TB + A["🛡️ 超时控制"] --> B["⏱ 重试机制"] + B --> C["⚡ 熔断器"] + C --> D["🔀 限流"] + D --> E["🧱 舱壁隔离"] + E --> F["💤 降级策略"] +``` + +> [!warning] 核心认知 +> 不要假设任何网络调用会成功。每个远程调用的代码都应该考虑失败路径。 + +> [!question]- 💡 思考题 +> **如果一个系统从未出现过故障,还需要容错设计吗?** +> +> 答案是肯定的。Netflix 的混沌工程理念指出:**"故障不是是否发生的问题,而是何时发生。"** +> 没有容错设计的系统在第一次外部依赖超时后就会暴露出级联崩溃的风险。 + +--- + +## 正文 + +### 各模式的组合使用 + +这 7 种容错模式通常不会单独使用,而是层层叠加形成纵深防御体系: + +```mermaid +flowchart LR + subgraph Layer1["第一层:快速失败"] + T["⏱ 超时控制
最先触发,避免线程等待"] + end + + subgraph Layer2["第二层:安全重试"] + R["⏱ 指数退避重试
给故障恢复的时间窗口"] + end + + subgraph Layer3["第三层:自我保护"] + CB["🔘 熔断器
高频失败时直接短路"] + RL["🔀 限流器
保护下游不被打满"] + BH["🧱 舱壁隔离
故障不蔓延"] + end + + subgraph Layer4["第四层:兜底策略"] + D["💤 降级响应
返回缓存/默认值"] + HC["💚 健康检查
自动剔除故障节点"] + end + + Layer1 --> Layer2 --> Layer3 --> Layer4 + + style Layer1 fill:#e3f2fd + style Layer2 fill:#fff3e0 + style Layer3 fill:#fce4ec + style Layer4 fill:#e8f5e9 +``` + +> [!tip] 使用顺序的重要性 +> 超时是所有机制的前提。一个没有超时的重试只会让请求无限期挂起。 + +## 1. 超时控制 + +这是所有容错机制的**前提**——没有超时的系统迟早会被拖垮。 + +```go +// Go context 超时时序图 +ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second) +defer cancel() + +resp, err := client.DoRequest(ctx, req) +if err == context.DeadlineExceeded { + // 上游超时 → 走降级逻辑或快速返回错误 +} +``` + +| 层级 | 推荐超时值 | 说明 | +|------|-----------|------| +| HTTP 客户端 → 业务服务 | 3~5s | 留有余量给下游 | +| gRPC → 内部服务 | 1~2s | 内网延迟低 | +| DB 查询 | 500ms~1s | 长查询应在 SQL 层限制 | + +> [!tip] 超时传递原则 +> 如果整体 SLA 要求 P99 < 200ms,子调用的超时时间必须逐层递减。例如:网关 200ms → 订单服务 150ms → 库存服务 80ms。 + +## 2. 重试机制 + +> [!warning] 关键原则 +> 只对 **幂等操作** 重试!POST 创建操作直接重试会导致重复数据。GET、PUT(同参数)适合重试。 + +### 指数退避 + Jitter + +``` +第 1 次重试:等待 1s + random(0~100ms) +第 2 次重试:等待 2s + random(0~100ms) +第 3 次重试:等待 4s + random(0~100ms) +... +``` + +```go +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 +} +``` + +**Jitter 为什么重要?** +- 如果没有随机偏移,当某个下游服务挂掉时,所有客户端会在同一时刻同时重试 +- 这会造成 **retry storm(重试风暴)**,对恢复中的服务施加二次冲击 +- 加上 jitter 后,请求均匀分散到时间窗口内 + +### 可重试 vs 不可重试的错误类型 + +| 可以重试 | 不应重试 | +|---------|---------| +| 连接超时 | 4xx Client Error(除 429) | +| 连接重置 | 404 Not Found | +| 502/503/504 Gateway Error | 业务错误(如余额不足) | +| 并发冲突(乐观锁失败) | 403 Forbidden | + +> [!example]- ❌ 常见反模式:重试非幂等操作 +> ``` +> // 危险!每次重试都会创建一个新的订单 +> resp, err := httpClient.Post("/api/orders", jsonBody) +> if err != nil { retry() } // ← 重复下单! +> ``` +> +> **安全做法:** 为每个写操作生成全局唯一的 `requestId`,下游基于 requestId 做幂等校验。 + +## 3. 熔断器 (Circuit Breaker) + +当下游服务频繁失败时,快速失败避免线程堆积雪崩。 + +```mermaid +flowchart TD + CB{{🔘 CIRCUIT BREAKER}} + + subgraph CL ["CLOSED — 正常"] + C["请求放行
⏱ 实时统计成功率
⚠️ 连续失败 → 熔断"] + end + + subgraph OP ["OPEN — 熔断"] + O["🚫 请求短路
💥 直接返回降级响应
🛡 不调下游,防雪崩"] + end + + subgraph HO ["HALF-OPEN — 探测"] + H["🔍 放行少量探测请求
✅ 成功 → 恢复全量流量
❌ 失败 → 重新熔断"] + end + + CB --> CL + CL -.->|失败 > N| OP + OP -.->|等待超时| HO + HO -->|探测成功| CL + HO -->|探测失败| OP + + style CL fill:#e6f9e6,stroke:#4caf50,stroke-width:3px,color:#000 + style OP fill:#ffebee,stroke:#ef5350,stroke-width:3px,color:#000 + style HO fill:#fff8e1,stroke:#ffa726,stroke-width:3px,color:#000 +``` + +### 状态转换条件 + +| 状态 | 触发条件 | 行为 | 退出条件 | +|------|---------|------|---------| +| **Closed** | 初始状态 / 从 Half-Open 恢复 | 正常放行请求 | 连续失败数 > threshold → 打开 | +| **Open** | 失败率超过阈值 | 立即短路返回错误 | 等待 recoveryTimeout → 进入半开 | +| **Half-Open** | 探测期 | 放少量请求测试 | 探测成功 → Closed;探测失败 → Open | + +### 实战示例 + +```go +// gobreaker 示例 +cb := state.NewCB(state.Settings{ + Name: "UserService", + MaxElems: 10, // 窗口大小 + WaitRetry: 5 * time.Second, // Open → Half-Open 等待时间 + ReadyToTrip: func(counts state.Counters) bool { + return counts.Failures > 5 // 连续 5 次失败熔断 + }, +}) + +result := cb.Execute(doRequest) +if result.Err != nil { + // 熔断打开,立即短路返回错误,不调用下游 + return fallbackResponse() +} +``` + +### 熔断器 vs 限流器 + +> [!note]- 两者的区别(常被混淆) +> +> | 维度 | 熔断器 (Circuit Breaker) | 限流器 (Rate Limiter) | +> |------|------------------------|---------------------| +> | **触发条件** | 下游故障时才打开 | 始终生效,不依赖故障状态 | +> | **决策依据** | 成功率、失败次数 | 请求速率超过阈值 | +> | **目的** | 保护自身不被慢速下游拖垮 | 保护下游不被过多请求打满 | +> | **状态性** | 有状态(Closed/Open/Half-Open) | 无状态(持续监控流速) | +> +> 两者配合效果最佳:**限流在前挡流量洪峰,熔断在后兜底保护。** + +保护下游服务不被过量请求压垮。 + +### 常见算法 + +```mermaid +flowchart LR + subgraph "令牌桶" + T1["匀速添加令牌"] + T2["请求消耗令牌"] + T3["不够则拒绝"] + end + + subgraph "滑动窗口" + S1["按时间片切分"] + S2["统计每片请求数"] + S3["超过阈值则拒绝"] + end +``` + +| 算法 | 特点 | 适用场景 | +|------|------|---------| +| **固定窗口** | 简单实现,存在边界突发问题 | 低频场景 | +| **滑动窗口** | 精确控制,内存占用高 | 需要精细限流的场景 | +| **令牌桶** | 允许一定突发,平均速率受限 | API 网关通用方案 | +| **漏桶** | 强制匀速输出,不允突发 | 防止下游被打满 | + +### 令牌桶 (Token Bucket) + +令牌桶的核心思想:**以恒定速率往一个"桶"里放令牌,请求到来时从桶中取令牌,取到则放行,取不到则拒绝或等待。** + +```mermaid +flowchart LR + subgraph "🪣 令牌桶内部" + BUCKET["令牌桶
容量 = maxBurst"] + REFILL["⚡ 固定速率 r/s 添加令牌
最多填到 maxBurst"] + end + + REQ[请求到达] --> CHECK{桶中有令牌?} + CHECK -- 是 --> CONSUME["消耗 1 个令牌
✅ 请求放行"] + CHECK -- 否 --> DROP["❌ 请求被拒绝 / 排队等待"] + + REFILL -.-> BUCKET + CONSUME -.->|"桶 -1"| BUCKET +``` + +**工作流程:** + +1. 初始状态:桶中有 `maxBurst` 个令牌(满桶) +2. 以固定速率 `r`(如 100/s)不断往桶中添加令牌,但总量不超过桶容量 +3. 每个请求到来时尝试获取 N 个令牌(通常 N=1) +4. 有足够令牌 → 消费后放行;不足 → 拒绝或阻塞 +5. **关键特性:桶未满的令牌会累积**,这使得在一段时间空闲后突然来了大量请求时,桶里有足够的令牌可以应对瞬时突发流量 + +**为什么允许突发?** + +假设 `rate = 100/s`, `maxBurst = 200`: + +| 时间 | 桶中令牌数 | 行为 | +|------|-----------|------| +| `t=0s` | 200 | 初始满桶 | +| `t=1s` | 300 → 200 | 理论应增到300,但桶已满,上限200 | +| `t=2s~9s` | 100 | 稳定状态(每秒消耗 ≈ 每秒生成) | +| `t=10s` | 200 | 系统空闲,桶重新蓄满 | +| `t=11s` | 0 | ⚡ 瞬间涌入200个请求全部通过(突发!) | +| `t=12s` | 100 | 后续仅100个能通过(恢复限速率) | + +这就是为什么令牌桶适合 API 网关——用户可能积攒了多个请求一口气发过来,直接全部拒绝体验很差;允许合理范围内的突发,用户体验更好。 + +**Go 实现:** + +```go +type TokenBucket struct { + mu sync.Mutex + tokens float64 // 当前令牌数 + maxBurst float64 // 桶容量 + rate float64 // 补充速率(令牌/秒) + lastRefill time.Time // 上次补充令牌的时间 +} + +func NewTokenBucket(rate, maxBurst float64) *TokenBucket { + return &TokenBucket{ + tokens: maxBurst, // 初始满桶 + maxBurst: maxBurst, + rate: rate, + lastRefill: time.Now(), + } +} + +// Allow 检查是否可以通行(非阻塞) +func (tb *TokenBucket) Allow() bool { + tb.mu.Lock() + defer tb.mu.Unlock() + + now := time.Now() + elapsed := now.Sub(tb.lastRefill).Seconds() + + // 根据经过的时间补充令牌 + tb.tokens += elapsed * tb.rate + if tb.tokens > tb.maxBurst { + tb.tokens = tb.maxBurst // 不能超过桶容量 + } + tb.lastRefill = now + + // 尝试消费一个令牌 + if tb.tokens >= 1 { + tb.tokens-- + return true + } + return false +} + +// Wait 阻塞等待直到获取到令牌 +func (tb *TokenBucket) Wait(ctx context.Context) error { + tb.mu.Lock() + defer tb.mu.Unlock() + + for { + if tb.Allow() { + return nil + } + // 计算还需要等多久才有令牌 + waitDuration := time.Duration((1-tb.tokens)/tb.rate) * time.Second + tb.mu.Unlock() + + select { + case <-time.After(waitDuration): + tb.mu.Lock() + case <-ctx.Done(): + return ctx.Err() + } + } +} +``` + +--- + +### 漏桶 (Leaky Bucket) + +漏桶的核心思想:**请求像水一样流入桶中,桶以固定的速率向下漏水,漏完才能接收新水。如果桶满了,新来的水就溢出丢弃。** + +```mermaid +flowchart TD + REQ["💧 请求源源不断流入"] --> QUEUE["🪣 漏桶
队列缓冲区"] + QUEUE --> PROCESS["🚰 以固定速率 drainer 处理
不管上游多快,只匀速处理"] + + QUEUE --> FULL{"桶满了?"} + FULL -- 是 --> DROP["💨 丢弃新请求 / 返回 429"] + + style QUEUE fill:#e3f2fd + style PROCESS fill:#c8e6c9 + style DROP fill:#ffebee +``` + +**工作流程:** + +1. 桶有一个固定容量 `capacity` +2. 请求到达时进入桶(相当于往桶里倒水) +3. 处理单元以恒定速率 `rate` 取出并处理请求(相当于底部有个小孔一直在漏水) +4. 如果请求到达速度超过处理速度,桶会被逐渐填满 +5. **桶满时,新来的请求直接被拒绝** + +**关键特性对比令牌桶:** + +> 场景:突发流量 500qps 涌入,限流配置 rate=100/s, capacity=200 +> +> | 维度 | 令牌桶行为 | 漏桶行为 | +> |------|-----------|---------| +> | **前 200 个请求** | ✅ 通过(桶里有足够令牌) | ❌ 拒绝(桶已满了) | +> | **第 201~300 个** | ⏱ 按恢复速率放行(100/s) | ──────── | +> | **后续请求** | ❌ 多余的被拒绝 | ✅ 匀速处理 100/s | +> | **下游视角** | 有突发性 | 始终匀速的 100/s | + +| 维度 | 令牌桶 | 漏桶 | +|------|--------|------| +| **突发容忍** | ✅ 允许(空闲时令牌会累积) | ❌ 不允许(直接拒绝或排队) | +| **输出形态** | 输入决定输出节奏 | 始终匀速输出 | +| **队列语义** | 无队列(拒绝或立即执行) | 隐含队列(请求先进入桶再被处理) | +| **保护对象** | 对客户端更友好 | 对下游处理单元更安全 | +| **实现复杂度** | 需记录剩余令牌和更新时间 | 只需记录当前量和漏出速率 | + +**Go 实现:** + +```go +type LeakyBucket struct { + mu sync.Mutex + water float64 // 当前水量(请求数) + capacity float64 // 桶容量 + rate float64 // 排水速率(请求/秒) + lastDrain time.Time // 上次排水时间 +} + +func NewLeakyBucket(capacity float64, rate float64) *LeakyBucket { + return &LeakyBucket{ + capacity: capacity, + rate: rate, + lastDrain: time.Now(), + } +} + +// Add 尝试加入请求(非阻塞),已满返回 false +func (lb *LeakyBucket) Add() bool { + lb.mu.Lock() + defer lb.mu.Unlock() + + now := time.Now() + elapsed := now.Sub(lb.lastDrain).Seconds() + + // 根据时间排出一定量的水 + lb.water -= elapsed * lb.rate + if lb.water < 0 { + lb.water = 0 // 不能排成负的 + } + lb.lastDrain = now + + // 加水 + if lb.water+1 <= lb.capacity { + lb.water++ + return true // 成功放入桶中 + } + return false // 桶满了,拒绝 +} + +// DrainLoop 持续的排水循环(实际服务中的处理方式) +func (lb *LeakyBucket) DrainLoop(ctx context.Context) { + ticker := time.NewTicker(100 * time.Millisecond) // 每 100ms 排水一次 + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + lb.mu.Lock() + drainAmount := lb.rate * 0.1 // 100ms 内可处理的量 + lb.water -= drainAmount + if lb.water < 0 { + lb.water = 0 + } + // 这里执行实际的处理逻辑 + // processNextRequest() + lb.mu.Unlock() + } + } +} +``` + +--- + +### 如何选择? + +```mermaid +flowchart TD + Choice["如何选?"] --> Q1{"是否需要支持突发流量?"} + + Q1 -- 是 --> TB["选令牌桶 ✅"] + Q1 -- 否 --> LB["选漏桶 ✅"] + + TB --> UseCases1["典型场景:
• REST API 限流
• 第三方调用配额
• 用户体验优先的网关"] + + LB --> UseCases2["典型场景:
• DB 连接池限流
• 消息队列消费者速率
• 防抖/削峰压后端"] + + style TB fill:#e8f5e9 + style LB fill:#fff3e0 + style UseCases1 fill:#e8f5e9 + style UseCases2 fill:#fff3e0 +``` + +> [!tip] 工程实践建议 +> - **生产环境推荐使用成熟库**:如 Go 的 `uber-go/ratelimit`(基于令牌桶)、Redis 的 `LIMIT` 参数配合 `XREADGROUP`(漏桶模型) +> - **Redis + Lua 脚本**是实现分布式令牌桶的最常见方案,保证多实例下的限流一致性 +> - 很多云厂商的 API 网关默认采用令牌桶算法,因为它在限制平均速率的同时给了客户端更好的吞吐体验 + +### 限流层级 + +```mermaid +graph TB + Client["客户端"] --> GW["L1: 网关级限流
防刷 / DDOS"] + GW --> Biz["L2: 业务级限流
按用户/接口配额"] + Biz --> Downstream["L3: 下游服务限流
保护实例不被打满"] + + style GW fill:#ffebee + style Biz fill:#fff3e0 + style Downstream fill:#e8f5e9 +``` + +## 5. 舱壁隔离 (Bulkhead) + +为不同下游服务分配独立的线程池/连接池,防止一个服务的故障蔓延到其他服务。 + +```mermaid +graph TB + Caller["调用方"] + + subgraph Bulkhead["舱壁隔离"] + Pool1[订单池
maxActive=10] + Pool2[用户池
maxActive=5] + Pool3[支付池
maxActive=8] + end + + Caller --> Pool1 + Caller --> Pool2 + Caller --> Pool3 + + Pool1 --> OrderSvc[[Order Service]] + Pool2 --> UserSvc[[User Service]] + Pool3 --> PaySvc[[Payment Service]] + + note[订单服务爆满不会影响用户服务的调用] + Pool1 -.-> note +``` + +> [!question]- 💡 什么时候需要舱壁隔离? +> - 单一客户端同时调用多个不稳定下游时 +> - 共享进程内存/连接池,某服务线程阻塞会占用全部资源时 +> +> **不需要的情况:** 每个下游已经部署在独立进程中(此时故障天然隔离);下游数量极少,连接池开销可以接受。 + +```go +// poolgroup 示例:为不同服务隔离连接池 +orderPool := pool.New(10, 100) // 活跃10个,上限100个 +userPool := pool.New(5, 50) +payPool := pool.New(8, 80) +// 订单服务占满连接不会影响用户服务的调用 +``` + +## 6. 降级策略 + +当系统部分不可用时,通过降级保证核心功能可用。 + +> [!note]- 降级的触发时机 +> - **主动降级:** 管理员手动关闭非核心功能(大促期间关闭推荐、评论) +> - **被动降级:** 熔断器打开后自动切换到兜底响应 +> +> 生产环境中,**主动降级通常是第一选择**——在系统还可控时牺牲局部保全整体,比等雪崩发生后再被动的处理代价更小。 + +```mermaid +flowchart TD + Req["用户请求"] + + Req --> Normal{"正常链路是否可用?"} + + Normal -- 是 --> Full["完整功能 ✅"] + + Normal -- 否 --> Degrade{"降级级别?"} + + Degrade -- "非核心模块" --> Skip["跳过该功能 ⚡"] + Degrade -- "核心模块不可用" --> Cache["返回缓存兜底 📦"] + + Cache -- "无缓存" --> Default["返回默认值/提示页"] + + Skip --> Partial["部分可用 ✅"] + Default --> Minimal["最低可用 ✅"] +``` + +### 常见降级场景 + +| 场景 | 降级方案 | 影响 | +|------|---------|------| +| 推荐服务挂了 | 展示热门榜单 / 空 | 用户体验略降 | +| 评论服务挂了 | 隐藏评论区 | 不影响购买流程 | +| 搜索服务超时 | 返回最近浏览记录 | 降低用户感知 | +| 库存服务不可用 | 显示"暂时无法确认库存" | 引导用户稍后再试 | + +## 7. 健康检查 + +```mermaid +sequenceDiagram + participant LB as 负载均衡器/K8s + participant S1 as 服务实例 A + participant S2 as 服务实例 B + + LB->>S1: GET /healthz -> 200 OK + LB->>S2: GET /healthz -> 503 ERROR + Note over LB: 标记 S2 不健康,剔除出池 + LB->>S2: GET /healthz -> 200 OK + Note over LB: 逐步恢复,重新加入池 +``` + +- **存活探针 (Liveness)**:判断"进程是否还活着",挂了则重启 +- **就绪探针 (Readiness)**:判断"是否能接收流量",未就绪则剔除负载均衡池 +- **Readiness 比 Liveness 更重要**——一个进程活着但数据库连接耗尽时,应该停止接收流量而不是重启 + +> [!tip] 生产环境最佳实践 +> 健康检查端点 `/healthz` 不应只做 HTTP 200,还应验证关键依赖(DB、Redis)的连通性。一个数据库连接耗尽的服务返回 200 是虚假的健康信号。 + +--- + +## 总览:模式对比速查 + +| 模式 | 保护谁 | 何时生效 | 复杂度 | 推荐优先级 | +|------|--------|---------|--------|-----------| +| ⏱ **超时控制** | 自身线程/连接 | 每次远程调用 | ⭐ | 🔴 必须实现 | +| ⏱ **重试机制** | 瞬时故障 | 失败后自动触发 | ⭐⭐ | 🔴 幂等操作必做 | +| 🔘 **熔断器** | 自身系统 | 高频失败时 | ⭐⭐ | 🟠 外部依赖必配 | +| 🔀 **限流器** | 下游服务 | 始终生效 | ⭐⭐ | 🟠 网关层必配 | +| 🧱 **舱壁隔离** | 其他服务调用 | 某下游异常时 | ⭐⭐⭐ | 🟡 多下游场景 | +| 💤 **降级策略** | 用户核心体验 | 非核心不可用时 | ⭐⭐ | 🟠 面向 C 端必做 | +| 💚 **健康检查** | 负载均衡/调度 | 实例异常时 | ⭐ | 🔴 K8s 必配 | + +> [!success]- 一句话总结 +> **超时保底、重试修复瞬时故障、熔断防雪崩、限流护下游、舱壁保隔离、降级保核心。** + +## 关联笔记 + +- [[02-服务治理/01-API网关]] — 网关层的限流和熔断插件 +- [[02-服务治理/04-服务发现]] — 健康检查是服务发现的基础 +- [[04-可观测性/01-Metrics监控]] — 熔断器的指标暴露和告警联动 diff --git a/hhs/MS/02-服务治理/07-配置管理.md b/hhs/MS/02-服务治理/07-配置管理.md new file mode 100644 index 0000000..98c70e8 --- /dev/null +++ b/hhs/MS/02-服务治理/07-配置管理.md @@ -0,0 +1,343 @@ +--- +tags: [microservice, config-management, nacos, apollo, spring-cloud-config] +create time: 2026-05-05 14:30 +--- + +# 配置管理 + +## 概述 + +当服务实例数以百计时,手动管理配置文件和维护 `.env` 文件的时代该结束了。配置中心为微服务体系提供 **集中化、动态化、版本化** 的管理能力——所有配置变更通过统一入口进行,实时推送到目标实例,全程可追溯。 + +> [!question] 引出配置中心的必要性 +> 假设你的 50 个服务实例都要连同一个数据库。现在需要把数据库密码从 `password1` 改为 `password2`,你会怎么改?登录每台机器改配置文件?滚动重启所有 Pod?还是……有更好的办法? + +> [!tip] 核心洞察 +> 传统配置管理的瓶颈不在「改」,而在「改之后如何生效」。配置中心的本质价值是将配置的 **修改** 和 **传播** 解耦——你只需要在控制台点一次提交,剩余的分发、版本记录、回滚保障全部由系统自动完成。 + +## 核心价值 + +```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 配置分离 | 避免生产配置误改到测试环境 | +| **版本管理与回滚** | 每次变更有迹可循,一键回滚 | 配置错误导致服务雪崩时,30 秒恢复 | +| **权限控制** | 敏感配置(密钥、Token)按角色隔离 | 防止越权修改核心参数 | +| **配置审计** | 记录谁在什么时候改了什么 | 合规要求 + 事故排查追溯 | + +> [!note] 配置刷新的两种策略 +> +> | 策略 | 原理 | 延迟 | 适用场景 | +> |------|------|------|---------| +> | **长轮询 (Long Polling)** | 客户端发起请求后服务端挂起等待(通常 30s),有变更立即返回 | 1~3s | Nacos / Apollo 采用的方案,兼顾实时性和服务端负载 | +> | **短轮询 (Short Polling)** | 客户端定时拉取(如每 60s GET 一次) | 最高等于轮询间隔 | 简单但浪费带宽,不推荐 | +> +> 长轮询的伪代码示意: +> +> ```go +> // 伪代码——演示长轮询原理 +> func longPoll(key string, timeout time.Duration) (string, bool) { +> start := time.Now() +> for time.Since(start) < timeout { +> if hasChanged(key) { // 检查服务端是否有新版本 +> return fetchLatest(key), true +> } +> time.Sleep(500 * time.Millisecond) // 短暂休眠再查 +> } +> return "", false // 超时,客户端重新发起长轮询 +> } +> ``` + +## 配置分层模型 + +```mermaid +graph TB + subgraph "配置优先级低到高" + A["Base 基线配置
各项目共享默认值"] + B["应用级配置
每个服务的专属配置"] + C["环境级配置
dev test prod 差异"] + D["实例级配置
单节点调优参数"] + end + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#fce4ec + style D fill:#e8f5e9 +``` + +**典型配置项分层**: + +| 层级 | 示例 | 修改频率 | +|------|------|---------| +| 基线配置 | Redis 集群地址、公共超时时间 | 极低 | +| 应用配置 | 线程池大小、日志级别 | 低 | +| 环境配置 | DB 连接串、Feature Flag | 中 | +| 实例配置 | 单机限流阈值、调试开关 | 高 | + +> [!question] 分层设计思辨 +> 如果基线配置和应用配置都指向同一个 Key(比如 `log.level`),最终生效的是哪一个? +> +> **答**:优先级高的覆盖优先级低的,即:**实例级 > 环境级 > 应用级 > 基线级**。这种覆盖机制类似 K8s 中 flags > env > image default 的多层注入。 + +## Nacos Config 示例 + +Nacos 配置管理的三个核心概念: + +| 概念 | 类比 | 作用 | +|------|------|------| +| **Data ID** | 文件名 | 唯一标识一份配置 | +| **Group** | 文件夹分组 | 将相关配置归类(如 `DEFAULT_GROUP`、`ORDER_GROUP`) | +| **Namespace** | 虚拟隔离域 | 不同环境(dev/test/prod)完全隔离,互不可见 | + +### 初始化与读取配置 + +```go +// 初始化 Nacos Config Client +configClient, _ := clients.NewConfigClient(value_map.NewValueMap(map[string]any{ + "serverConfig": sc, // Server 地址、鉴权信息 + "namespace": "your-ns-id", // 命名空间隔离(可选) +})) + +// 获取当前配置内容 +content, _ := configClient.GetConfig(config_param.GetConfigParam{ + DataId: "order-service.yaml", + Group: "DEFAULT_GROUP", + // Namespace 在 Client 初始化时指定 +}) + +_ = content // 解析 YAML → 填充到应用程序的配置结构体 +``` + +### 监听配置变化——热更新回调 + +```go +// 注册监听器——Nacos 有配置变更时会推送回调 +configClient.ListenChange(config_param.ListenChangeParam{ + DataId: "order-service.yaml", + Group: "DEFAULT_GROUP", + Callback: func(content string) { + fmt.Println("配置更新了,开始热加载...") + // 步骤 1: 解析新配置 + newCfg := &Config{} + yaml.Unmarshal([]byte(content), newCfg) + // 步骤 2: 原子替换(用 lock 保证并发安全) + cfgMutex.Lock() + globalConfig = newCfg + cfgMutex.Unlock() + // 步骤 3: 通知依赖配置的组件重新初始化 + NotifyConfigChange(newCfg) + }, +}) +``` + +> [!warning] 热更新的注意事项 +> +> 1. **线程安全**:配置结构体必须用 `sync.RWMutex` 保护读写,避免竞态条件 +> 2. **幂等性**:回调可能被多次触发,ReloadConfig 应该是幂等操作 +> 3. **优雅降级**:新配置格式错误时,保留旧配置而不是直接崩溃 +> 4. **冷启动兼容**:客户端首次启动先拉取快照配置,再注册监听器——避免两者之间存在时间窗口导致漏掉变更 + +### 配置文件的命名规范 + +推荐格式:`{service-name}.{environment}.yaml` + +| 服务名 | 环境 | Data ID | +|--------|------|---------| +| order-service | dev | `order-service.dev.yaml` | +| order-service | prod | `order-service.prod.yaml` | +| user-service | prod | `user-service.prod.yaml` | + +> [!tip] 进阶:配置合并 +> 实际项目中通常拆分多份配置文件: +> - `{service}.yaml` — 基础配置(公共部分) +> - `{service}.db.yaml` — 数据库专项配置 +> - `{service}.redis.yaml` — Redis 专项配置 +> +> Nacos 支持通过 `Shared Configs` 机制合并多份 DataID 的配置,启动时一次性拉取并按顺序合并。 + +## Spring Cloud Config 补充 + +对于 Java/Spring 生态,Spring Cloud Config 是经典选择: + +```yaml +# application.yml — 客户端接入 +spring: + cloud: + config: + uri: http://config-server:8888 + name: order-service # 对应服务端 Git 仓库中的 order-service.yml + profile: prod # 选择环境分支 + label: main # Git 分支 +``` + +```java +// 注解驱动——配置变更自动刷新 +@RestController +@RefreshScope // 关键:标记此 Bean 支持运行时刷新 +public class OrderController { + + @Value("${feature.new-order-flow:true}") + private boolean newOrderFlowEnabled; + + @GetMapping("/orders") + public List list() { + if (newOrderFlowEnabled) { + // 新版流程 + } + return orderService.list(); + } +} +``` + +> [!note] Spring Cloud Config 架构特点 +> Spring Cloud Config 后端通常对接 Git 仓库,配置变更的本质就是 **Git commit**。这意味着天然拥有 Git 的所有能力(diff、回滚、分支管理),但也引入了依赖外部存储的延迟问题。通常搭配 **Bus 消息总线**(Spring Cloud Bus + RabbitMQ/Kafka)实现广播式推送,解决纯拉取模式的延迟缺陷。 + +## Apollo vs Nacos Config 对比 + +上一节以 Nacos 为例介绍了配置中心客户端的接入方式。但除了阿里系的 Nacos,业界还有其他成熟选择,其中 Apollo 是最常被拿来比较的另一款方案。了解它们各自的定位差异,能帮助我们在选型时少踩坑。 + +**Apollo** 由携程开源(现Apache孵化),定位为专业的企业级配置管理平台。它从诞生起就专注于「配置管理」这一件事,在设计上做了大量精细化的考量:比如配置发布前可以预览 diff、支持灰度发布某个实例、操作有审核流程等。适合对配置管控要求严格的多团队大型企业。 + +**Nacos Config** 是阿里 Nacos 组件的子模块。Nacos 本身是一套「服务发现 + 配置管理」的二合一平台——如果你已经在用 Nacos 做服务发现,顺势用它管配置几乎是零额外成本的选择。它的优势在于轻量、上手快,在中小团队中落地速度更快。 + +两者底层都基于 AP 模型(可用性优先),推送延迟都在 1s 以内。真正的差异不在性能,而在 **功能丰富度** 和 **生态适配**: + +| 维度 | Apollo (携程) | Nacos Config (阿里) | Spring Cloud Config | +|------|--------------|---------------------|--------------------| +| **界面体验** | Web UI 完善,操作直观 | 较好 | 需自建 | +| **发布流程** | 支持审核、灰度、回滚 | 基础发布+回滚 | 依赖 Git 工作流 | +| **配置粒度** | 应用/Cluster/Namespace 多维 | Service/Group | File-based | +| **性能** | AP 模型,延迟 < 1s | AP 模型,延迟 < 1s | 读取 Git,延迟稍高 | +| **生态集成** | 适合 Java/Spring 体系 | Java + Go + Python 等 | 仅 Spring 生态 | +| **适用场景** | 大型企业,多团队协同 | 中小团队快速落地 | 纯 Spring 项目 | + +> [!tip] 选型建议 +> +> 1. **刚起步的微服务团队**:选 Nacos,一套组件同时搞定服务发现和配置管理,减少运维成本 +> 2. **已有 Spring Cloud 全家桶**:优先 Spring Cloud Config,生态无缝衔接 +> 3. **大型企业多团队协作**:Apollo 的权限体系和发布流程更成熟,适合精细化管控 +> 4. **K8s 原生项目**:简单配置走 ConfigMap + Secret,复杂场景引入外部配置中心 + +## K8s ConfigMap & Secret + +以上介绍的都是 **独立部署** 的配置中心(Nacos / Apollo),它们通过客户端 SDK 与应用解耦。但当你的基础设施完全跑在 Kubernetes 上时,K8s 本身已经内置了一套轻量级的配置注入机制——ConfigMap 和 Secret,不需要额外搭建外部服务。 + +**ConfigMap** 用于存放非敏感配置,本质是 K8s 上的一个 key-value store,可以被注入为环境变量、命令行参数或挂载为配置文件到 Pod 中。**Secret** 则是它的敏感版本,专存密码、密钥、Token 等数据——虽然默认只是 base64 编码(不是加密),但语义上和权限管控上与 ConfigMap 做了区分。 + +纯 K8s 环境内的原生方案: + +```yaml +apiVersion: v1 +kind: ConfigMap +metadata: + name: order-service-config +data: + application.yaml: | + server: + port: 8080 + datasource: + url: jdbc:mysql://db-host:3306/orders + username: ${DB_USERNAME} + password: ${DB_PASSWORD} +--- +apiVersion: v1 +kind: Secret +metadata: + name: order-service-secrets +type: Opaque +stringData: + DB_USERNAME: "app_user" + DB_PASSWORD: "s3cret_p@ss" +``` + +注入到容器: + +```yaml +envFrom: + - configMapRef: + name: order-service-config + - secretRef: + name: order-service-secrets +``` + +> [!warning] K8s 原生方案的局限 +> - 修改 ConfigMap 后 Pod 不会自动重载配置(需配合 Sidecar 或手动触发 reload) +> - 没有版本管理和灰度发布能力 +> - **建议**:简单项目用 K8s ConfigMap,复杂场景上 Apollo/Nacos + +## 敏感信息处理 + +> [!danger] 安全红线 +> **永远不要**在代码仓库中硬编码密码、API Key、私钥等敏感信息。 + +```mermaid +flowchart LR + Dev["开发者本地"] -->|"K8s Secret / Vault"| Store["加密存储"] + Store -->|"运行时解密"| Runtime["运行时的环境变量"] + Runtime --> App["应用程序"] + + Audit["审计系统"] -.->|"只读访问"| Store +``` + +**推荐方案**: + +| 方案 | 适用规模 | 特点 | +|------|---------|------| +| **K8s Secret** | 小型集群 | base64 编码(非加密),配合 EncryptionConfiguration 增强 | +| **HashiCorp Vault** | 企业级 | 动态秘钥、自动轮换、细粒度访问策略 | +| **云厂商 KV 服务** | 云原生项目 | AWS Secrets Manager / 阿里云 KMS / 腾讯云 SecretManager,免运维 | + +> [!tip] Vault 的杀手锏:动态秘钥 +> Vault 可以为每次请求生成一个临时的数据库凭证,设定 TTL 为 1 小时——过期自动销毁。相比静态密码方案,即使秘钥泄露也只有 1 小时的危害窗口。这是传统配置中心无法做到的。 + +## 常见问题排查 + +> [!abstract] 实战排障指南 +> +> ### Q1: 配置改了但服务没生效? +> +> **排查清单**: +> 1. 确认修改的是正确的 Namespace / 环境 +> 2. 检查监听器是否成功注册(看客户端日志有无 `ListenChange success` 类日志) +> 3. 确认回调函数内部有没有 panic 导致回调中断 +> 4. 长轮询是否被代理或负载均衡器超时切断(常见于网关配置了 30s 超时) +> +> ### Q2: 配置热加载后出现内存泄漏? +> +> 每次 ReloadConfig 都创建新对象是正常行为,但要确保: +> - 旧配置对象的引用全部被替换(无其他地方仍持有旧引用) +> - 如果使用缓存结构,注意清理旧的缓存键 +> - Go 语言的 GC 会自动回收无引用对象,但大量频繁热加载时可以观察 `runtime.MemStats` +> +> ### Q3: 配置中心挂了怎么办? +> +> **最佳实践**:客户端做本地缓存(File-based Fallback)。 +> +> ```go +> func getConfig(dataID string) ([]byte, error) { +> // 第一步:尝试从配置中心拉取 +> content, err := remoteClient.getConfig(dataID) +> if err == nil { +> saveToLocalCache(dataID, content) // 缓存到本地 +> return content, nil +> } +> // 第二步:配置中心不可用时,读取本地缓存 +> return loadFromLocalCache(dataID) +> } +> ``` + +## 关联笔记 + +- [[02-服务治理/04-服务发现]] — Nacos 同时提供服务发现和配置管理 +- [[05-部署运维/02-Kubernetes]] — ConfigMap/Secret 是 K8s 的配置注入方式 diff --git a/hhs/MS/02-服务治理/08-流量治理.md b/hhs/MS/02-服务治理/08-流量治理.md new file mode 100644 index 0000000..51e1d51 --- /dev/null +++ b/hhs/MS/02-服务治理/08-流量治理.md @@ -0,0 +1,284 @@ +--- +tags: [microservice, canary-release, blue-green, traffic-routing, istio, load-balancing] +create time: 2026-05-05 10:30 +--- + +# 流量治理 + +## 概述 + +灰度发布(也叫金丝雀发布)是降低变更风险的核心手段。它让你将新版本逐步推向一小部分用户,观察指标正常后再全量。 + +> [!question] 如何安全地上线? +> 你修复了一个紧急 Bug,但这次改动涉及核心支付链路。直接全量发布一旦出问题损失巨大。有没有办法先小范围验证,确认没问题再放量? + +答案就是**按规则拆分流量**——将请求按照不同维度分配到新旧版本,用真实用户的流量来检验新代码,而不是靠测试环境的人为模拟。 + +```mermaid +flowchart LR + A["🟢 v1 (稳定版)
95% 流量"] --> B["🔵 v2 (测试版)
5% 流量"] + B --> C{"监控指标?"} + C -- "✅ 一切正常" --> D["逐步放量
10% → 25% → 50%"] + C -- "❌ 错误率飙升" --> E["自动回滚到 v1 🔄"] + D --> F{"全部放量至 100%"} + F --> G["🟢 v2 成为新稳定版"] +``` + +## 一、灰度策略全景 + +### 常见灰度策略对比 + +| 策略 | 描述 | 优点 | 缺点 | 适用场景 | +|------|------|------|------|---------| +| **按百分比** | 随机分配 N% 流量到新版本 | 简单粗暴,覆盖面广 | 可能只命中特定用户 | 通用场景 | +| **按用户 ID** | 指定用户/内部账号走新版 | 可精准控制测试范围 | 样本不够代表性 | 内部验收阶段 | +| **按 Header** | `x-canary: true` 路由到新版本 | 灵活性最高 | 需要客户端配合 | QA / AB 测试 | +| **按地域** | 某个城市/机房的新版本 | 地域性问题的理想试验场 | 地域偏差大 | 全球化部署 | +| **按设备类型** | iOS 新用户先用新版 | 覆盖目标人群 | 样本有限 | 移动端发版 | + +> [!tip] 实战建议:多策略组合 +> 生产环境中通常不会只用单一规则。推荐的做法是分层判断: +> +> ``` +> 第1层:内部员工 IP → 100% 进灰度(快速发现 bug) +> 第2层:header x-canary=true → 100% 进灰度(QA 验证) +> 第3层:新注册用户 → 50% 进灰度(扩大样本) +> 第4层:其余用户 → 默认 v1 +> ``` + +### 按权重分流的 Go 实现 + +在应用层面(如 go-zero 或自研网关),流量分配往往通过中间件实现: + +```go +// CanaryMiddleware: header 携带 canary=true 的请求直通灰度版本 +func (m *CanaryMiddleware) Next(ctx context.Context, req any, reply any, rpc func(context.Context, any, any) error) error { + if m.r.isGray(ctx) { // 从 header/context 提取灰度标记 + return m.grayHandler(ctx, req, reply) // 路由到灰度服务实例 + } + return m.next(ctx, req, reply) // 走正常路径 +} +``` + +### 客户端负载均衡中的灰度 + +客户端侧的灰度通常通过自定义负载算法实现,以加权轮询为例: + +```go +// WeightedRoundRobin: 权重感知轮询 +type WeightedRoundRobin struct { + servers []*Server // 包含 weight 字段 + totalWeight int +} + +func (w *WeightedRoundRobin) Next() (*Server, error) { + // 每次选取当前权重最高的可用服务器 + // v1 权重 95, v2 权重 5 → 平均每 20 次请求 1 次命中 v2 +} +``` + +## 二、蓝绿 vs 灰度 + +> [!summary] 两种发布策略对比 +> +> | 维度 | 蓝绿部署 | 灰度发布 (金丝雀) | +> |------|---------|-----------------| +> | **切换方式** | 一次性全切 | 逐步放量 | +> | **资源消耗** | 双倍(新旧并行) | 初期少量副本 | +> | **回滚速度** | 秒级(切回 LB 即可) | 同样秒级 | +> | **风险等级** | 中高(全量暴露问题) | 低(渐进式) | +> | **适合场景** | 大型变更、重大版本 | 日常迭代、高风险链路 | + +### 蓝绿部署流程 + +```mermaid +stateDiagram-v2 + [*] --> Blue: 初始状态 + Blue --> DeployGreen: 部署 v2 到 Green 环境 + DeployGreen --> TestGreen: 验证 Green + TestGreen --> Switch: 切换入口流量 + Switch --> Green: 全部流量到 Green/v2 + Green --> CleanupBlue: 删除 Blue/v1 + CleanupBlue --> [*] +``` + +### 金丝雀发布流程 + +```mermaid +graph TB + S1["v1 运行中
100% 流量"] + S2["部署 v2
5% 流量"] + S3["监控 15min"] + S4{"P99 延迟 < SLA?"} + + S4 -- 否 --> Rollback["回滚 v2 🔄"] + S4 -- 是 --> S5["放量 25%"] + S5 --> S6["监控 30min"] + S6 --> S7{"错误率 < 0.1%?"} + S7 -- 否 --> Rollback + S7 -- 是 --> S8["放量 50%"] + S8 --> S9["监控 1h"] + S9 --> S10{"全项达标?"} + S10 -- 是 --> S11["100% 全量 ✅"] + S10 -- 否 --> Rollback + + style Rollback fill:#ffebee + style S11 fill:#e8f5e9 +``` + +> [!warning] 关键决策点 +> 每个放量节点都是一个"继续 or 回滚"的决策门控。不要跳过任何一步,哪怕之前几轮都顺利通过。历史教训表明:**很多事故发生在最后一跳**。 + +## 三、高级路由规则 + +### Istio VirtualService + +Istio 作为 Service Mesh 方案,流量治理能力强大但侵入性也更高。适合已有 K8s + Istio 基础设施的团队。 + +```yaml +# 灰度规则:header 携带 canary=true 的用户走 v2 +apiVersion: networking.istio.io/v1beta1 +kind: VirtualService +metadata: + name: order-route +spec: + hosts: ["order-service"] + http: + # 灰度规则 + - 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 +``` + +### 权重路由示例 + +```yaml +# 按比例分流 +http: + - route: + - destination: + host: order-service + subset: v1 + weight: 90 + - destination: + host: order-service + subset: v2 + weight: 10 +``` + +### 网关层 vs Sidecar 层 + +流量治理可以在两个不同的层级实现,选择取决于团队的基础设施成熟度: + +```mermaid +flowchart TD + subgraph "网关层(中心化)" + GW["API Gateway
集中管理路由规则"] + GW --> V1[[v1 实例集群]] + GW --> V2[[v2 实例集群]] + end + + subgraph "Sidecar 层(分布式)" + GW2["API Gateway
只做转发"] + GW2 --> ProxyV1["Envoy Sidecar
本地执行路由规则"] + GW2 --> ProxyV2["Envoy Sidecar
本地执行路由规则"] + ProxyV1 --> ContainerV1[[v1 容器]] + ProxyV2 --> ContainerV2[[v2 容器]] + end +``` + +| 维度 | 网关层治理 | Sidecar 层治理 | +|------|-----------|---------------| +| **复杂度** | 配置统一,易维护 | 需理解 Mesh 概念 | +| **性能** | 单点瓶颈风险 | 就近处理,开销更低 | +| **灵活性** | 依赖网关插件能力 | Istio 支持更丰富的规则 | +| **学习曲线** | 低 | 高 | +| **适用团队** | 中小型、快速起步 | 大规模微服务集群 | + +> [!note] 选型建议 +> - **初创期**:直接在网关或应用层做路由分发即可,不必引入 Service Mesh +> - **成长期**:当服务数量超过 30+、跨团队协作复杂时,考虑用 Istio 统一管理 +> - **混合模式**:网关层做简单的按路径/权重分发,Sidecar 层做精细的 header/metadata 路由 + +## 四、流量治理的其他维度 + +除了发布策略,流量治理还包括: + +| 能力 | 说明 | 关联文档 | +|------|------|---------| +| **熔断降级** | 下游不可用时自动降级 | [[02-服务治理/06-容错模式]] | +| **限流** | 保护上游不因过量请求被打垮 | [[02-服务治理/06-容错模式]] | +| **重试** | 对瞬态故障自动恢复 | [[02-服务治理/06-容错模式]] | +| **黑白名单** | IP 级别访问控制 | — | +| **A/B Testing** | 基于用户分组的长期实验 | — | + +## 五、灰度发布的量化决策依据 + +> [!tip] 必须有可量化的观测指标作为决策依据 +> +> 不要凭感觉放量,让数据说话: +> +> | 指标 | 阈值参考 | 告警级别 | +> |------|---------|---------| +> | **错误率** | < 0.1% (v1 baseline 对比) | > 1% 立即回滚 | +> | **P99 延迟** | ≤ v1 × 1.1 | > v1 × 1.3 暂停放量 | +> | **CPU/Memory** | 在 limits 的 70% 以内 | > 85% 扩容 | +> | **下游依赖成功率** | > 99.5% | < 95% 熔断 | + +### 灰度监控面板设计要点 + +一个完整的灰度面板应该能一眼看出新旧版本的差异: + +```mermaid +flowchart LR + subgraph RealTime["实时对比视图"] + R1["📊 P99 延迟趋势
v1 vs v2 双线对比"] + R2["🔴 错误率柱状图
区分 HTTP 状态码分类"] + R3["⚡ QPS 流量分布
按版本分堆叠面积图"] + end + + subgraph HealthCheck["健康检查"] + H1["✅ 版本 v1 正常运行
副本数: 10/10"] + H2["⚠️ 版本 v2 异常
副本数: 2/5 · 失败: 3"] + end + + RealTime --> Decision["🎯 是否继续放量?"] + HealthCheck --> Decision + + Decision -- "指标超标" --> Rollback["触发自动回滚"] + Decision -- "指标正常" --> Expand["推进下一放量阶段"] + + style Rollback fill:#ffebee,color:#000 + style Expand fill:#e8f5e9,color:#000 +``` + +## 六、实践 checklist + +> [!example] 灰度上线前必查清单 +> +> - [ ] 旧版本保持至少一个副本不销毁(随时回滚) +> - [ ] 灰度期间禁止其他无关变更上线 +> - [ ] 已配置好 v1 vs v2 的双线对比面板 +> - [ ] 定义了明确的放量阶段和触发条件(何时扩、何时停) +> - [ ] 准备了回滚脚本 / 一键回滚操作 +> - [ ] 通知了相关干系人(运维、产品、客服) +> - [ ] 选择了影响面最小的时间段开始灰度 + +## 关联笔记 + +- [[02-服务治理/01-API网关]] — API Gateway 的路由和权重分发 +- [[02-服务治理/06-容错模式]] — 熔断、限流等配套治理手段 +- [[04-可观测性/01-Metrics监控]] — 灰度期间的监控面板设计 +- [[05-部署运维/04-SRE实践]] — 基于 SLO 的发布决策 diff --git a/hhs/MS/02-服务治理/09-网关鉴权策略.md b/hhs/MS/02-服务治理/09-网关鉴权策略.md new file mode 100644 index 0000000..c081ee4 --- /dev/null +++ b/hhs/MS/02-服务治理/09-网关鉴权策略.md @@ -0,0 +1,333 @@ +--- +tags: [microservice, api-gateway, auth, service-mesh, mTLS, jwt, rbac, zero-trust] +create time: 2026-04-30 15:30 +update time: 2026-05-17 10:00 +--- + +# 网关鉴权分层设计 + +## 概述 + +每个微服务团队都会面临同一个灵魂拷问:**鉴权该放在网关统一做,还是各服务自己管?** + +放网关——简单粗暴但不够灵活;放各服务——灵活但容易失控。本文给出一个经过多团队实践验证的**三层鉴权架构**方案,兼顾统一安全基线与差异化业务需求。 + +## 问题的本质 + +### 一刀切的陷阱 + +> [!question] 引出思考 +> 有人说:"鉴权放到网关统一做,避免每个服务重复写。"听起来很美好——但现实中有三个场景会让这个假设当场翻车: +> +> - 某个内部查询接口被高频调用,走一遍网关的 JWT 校验白白增加 3ms 延迟 +> - 财务团队被审计要求 OAuth2 + MFA,运营后台只需要 API Key +> - 订单服务必须判断"用户 A 是否有权查看**这笔**订单",网关根本不知道数据归属关系 +> +> **你怎么设计才能兼顾统一性和灵活性?** + +核心矛盾在于:**网关做鉴权的优势是集中化和性能**,但代价是 **失去对差异化需求的表达能力**。 + +现实中至少存在三种网关无法处理的场景: + +| 场景 | 例子 | 为什么网关搞不定 | +|------|------|----------------| +| **内部调用免鉴权** | A服务直调B服务的内部查询接口 | 外部请求到不了这里,网关做无意义开销 | +| **不同团队标准不同** | 财务系统要求 OAuth2+MFA,运营后台只需 API Key | 策略硬编码在网关里会变成巨石中间件 | +| **精细化权限控制** | 订单服务需要判断"用户A是否有权看这笔订单" | 网关不知道业务数据归属关系 | + +## 三层鉴权架构 + +```mermaid +flowchart TB + subgraph L1["第一层:网关层 - 你是谁?"] + G1["JWT / Token 校验"] + G2["IP 白名单"] + G3["基础限流"] + end + + subgraph L2["第二层:服务入口层 - 你能做什么?"] + S1["内部服务间 Token 验证"] + S2["mTLS 双向认证"] + S3["角色 / 权限检查"] + end + + subgraph L3["第三层:业务逻辑层 - 你能操作这个资源吗?"] + B1["行级权限:userId == order.userId"] + B2["数据范围:仅本部门可见"] + B3["审批状态机流转"] + end + + Client["外部请求"] --> GW["API Gateway
L1 快速过滤非法请求"] + GW --> svcA["Order Service"] + GW --> svcB["Finance Service"] + + svcA --> L2 + svcB --> L2 + + L2 --> L3 + + svcA -.->|"内部调用"| svcC["Inventory Service
仅 L2 鉴权,跳过 L1"] + svcC --> L2 +``` + +### 三层职责划分 + +> [!summary] 鉴权分层原则 +> +> | 层级 | 关注点 | 粒度 | 谁来实现 | +> |------|--------|------|---------| +> | **L1 网关层** | 身份认证 — "你是合法用户吗?" | 粗粒度(用户级) | 平台团队统一维护 | +> | **L2 服务层** | 接口授权 — "你有权限调这个接口吗?" | 中粒度(角色/服务级) | 各业务团队自己定义 | +> | **L3 业务层** | 数据权限 — "你能操作这条数据吗?" | 细粒度(数据级) | 业务逻辑自身实现 | + +**设计哲学**:越往上过滤越早失败,但信息越少;越往下信息越多但成本越高。每一层只做它最擅长的那件事。 + +### 快速检查 + +> [!question] 思考题 +> 假设你在设计一个电商系统,订单查询接口的鉴权应该如何分层?尝试为以下三个请求场景分别标记出需要生效的层级(L1/L2/L3): +> +> | 场景 | 应该经过哪几层 | 理由 | +> |------|--------------|------| +> | 外部 App 用户通过公网请求订单详情 | ? | ? | +> | 运营后台手动查询某条订单记录 | ? | ? | +> | 仓储系统的定时任务拉取未发货订单列表 | ? | ? | + +> [!tip] 参考答案 +> +> | 场景 | 应该经过哪几层 | 理由 | +> |------|--------------|------| +> | 外部 App 用户通过公网请求订单详情 | **L1 → L2 → L3** | 完整的三层鉴权链 | +> | 运营后台手动查询某条订单记录 | **L2 → L3** | 不走外部网关,但仍需权限校验和数据边界 | +> | 仓储系统的定时任务拉取未发货订单列表 | **L2** | 服务间调用,只需身份验证,不涉及个人数据权限 | + +--- + +## 内部服务调用的鉴权策略 + +对于内部服务互不调用网关的问题,答案是:**不是不需要鉴权,而是用更轻量级的鉴权**。 + +```go +// 服务间调用:用短期签名 Token 代替完整 JWT +func serviceToServiceAuth(serviceID string, target string) string { + // 使用共享密钥 HMAC 签发短时效 Token + payload := map[string]string{ + "sub": serviceID, + "target": target, + "exp": time.Now().Add(5 * time.Minute).Format(time.RFC3339), + } + return signWithSharedKey(payload) // 比解析完整 JWT 便宜得多 +} + +// 接收方校验 +func verifyInterServiceToken(token string) error { + claims, err := verifyHMAC(token) + if err != nil { + return fmt.Errorf("无效的内部服务 Token") + } + // 额外检查:该服务是否有权限访问目标服务 + if !allowedAccess(claims.ServiceID, claims.Target) { + return fmt.Errorf("服务 %s 无权调用 %s", claims.ServiceID, claims.Target) + } + return nil +} +``` + +> [!note] 解释 +> 这段代码展示了两个关键点:一是用 **HMAC 短期 Token** 替代完整的 JWT 校验链,将延迟控制在 ~1ms 量级;二是在校验身份之外还做了 **服务间的访问控制检查**,避免被入侵的服务冒充其他服务。 + +内部鉴权方案对比: + +| 方案 | 延迟 | 安全性 | 适用场景 | +|------|------|--------|---------| +| 完全跳过鉴权 | ~0ms | ❌ 不安全 | 同 Pod 内 Sidecar 互调 | +| 简单 Token 签名 | ~1ms | ⚠️ 中等 | 同一安全域内的服务互调 | +| mTLS 双向认证 | ~2ms | ✅ 高 | 跨租户、跨团队、混合云环境 | + +### 鉴权上下文的层间传递 + +三层鉴权架构最大的工程挑战是:**L1 层校验出的用户信息如何传递到 L2、L3?** 如果每层都独立重新获取身份信息,不仅浪费性能,还会造成数据不一致。 + +```go +// AuthContext 在请求链路中的传递结构 +type AuthContext struct { + UserID string `json:"user_id"` // 来自 L1 JWT Claims + Username string `json:"username"` + Roles []string `json:"roles"` // 来自 L2 RBAC 查询结果 + TenantID string `json:"tenant_id"` // 用于 L3 数据隔离 + AccessToken string `json:"-"` // 不向下透传,防止越权 +} + +// 网关中间件 — 解析 JWT 后注入上下文 +func gatewayAuthMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + claims, err := validateJWT(r.Header.Get("Authorization")) + if err != nil { + http.Error(w, "unauthorized", http.StatusUnauthorized) + return + } + ctx := context.WithValue(r.Context(), authCtxKey{}, &AuthContext{ + UserID: claims.Subject, + Roles: claims.Roles, + TenantID: claims.Tenant, + }) + next.ServeHTTP(w, r.WithContext(ctx)) + }) +} +``` + +> [!note] 关键设计原则 +> +> - **AccessToken 不向下透传**:下游服务只需要身份标识,不需要完整凭证,避免被截获后的越权风险 +> - **上下文不可伪造**:L2/L3 只能通过 `context.Value` 读取,不能自行构造,确保每一层的决策都有据可查 +> - **TenantID 用于 L3 数据隔离**:这是多租户 SaaS 的关键防线,防止 A 租户用户通过 API 参数访问 B 租户数据 + +### 常见陷阱:忘记设置超时 + +> [!danger] 致命错误 +> +> ```go +> // ❌ 没有超时的鉴权检查 +> result := db.QueryContext(ctx, "SELECT * FROM permissions WHERE user_id = ?", userID) +> +> // ✅ 始终为鉴权相关数据库查询设置超时 +> queryCtx, cancel := context.WithTimeout(ctx, 100*time.Millisecond) +> defer cancel() +> result := db.QueryContext(queryCtx, "SELECT * FROM permissions WHERE user_id = ?", userID) +> ``` +> +> 鉴权链路上的任何一个环节卡死,都会导致整个请求挂起。**鉴权失败的代价应该比正常请求慢几毫秒,而不是让请求永远 hang 住**。生产事故中超过 30% 的 P0 事件与鉴权组件超时有关。 + +## 多团队差异化鉴权方案 + +针对第二个问题——不同团队需要不同的鉴权方式——推荐用 **策略即代码(Policy-as-Code)** + **可插拔鉴权中间件**。 + +```go +// 鉴权处理器接口 — 每个团队实现自己的策略 +type AuthMiddleware interface { + Name() string + CanHandle(ctx *Context) bool // 这个服务 / 路由是否适用我的策略 + Authenticate(w http.ResponseWriter, r *http.Request) bool +} + +// 注册表模式:运行时动态装配 +var registry = make(map[string]AuthMiddleware) + +func Register(name string, m AuthMiddleware) { + registry[name] = m +} + +// 路由级别的策略声明 +// router.go +router.Use(multiAuth( + "jwt-gateway", // L1 网关已经校验过的 JWT + "rbac-finance", // 财务团队的额外 RBAC 校验 +)) +``` + +> [!note] 解释 +> 通过接口抽象 + 注册表模式,每个团队可以在不修改基础设施代码的前提下,将自己团队的鉴权策略注册到系统中。`multiAuth` 按顺序执行所有匹配的中间件,任一失败则拒绝请求。 + +这样做的收益: + +> [!tip] 核心收益 +> +> 1. **统一但不僵化**:网关承担公共部分,各服务按需叠加自定义策略 +> 2. **渐进式迁移**:老服务可以逐步从简单鉴权升级到新策略,不用一次性改造 +> 3. **职责边界清晰**:平台团队维护基础设施鉴权,业务团队维护业务鉴权 +> 4. **审计友好**:每层的鉴权结果独立记录,出问题时能精确到是哪一层拦截的 + +## 实战清单:落地时的经验教训 + +在实际推进分层鉴权架构时,以下经验可以帮团队少走弯路: + +| 阶段 | 建议做法 | 踩过的坑 | +|------|---------|---------| +| **第一阶段** | 先在网关统一做 JWT 校验,保证最小安全基线 | 一上来就搞完美架构,结果连基础的 Token 校验都没覆盖 | +| **第二阶段** | 为每个内部服务补充 L2 认证(HMAC Token 或 mTLS) | 多个服务各自实现了一遍 Token 校验逻辑,后来换算法全要改一遍 | +| **第三阶段** | 在核心服务引入 L3 数据级权限控制 | 先全局加 RBAC 再逐步收紧到数据级别,比一步到位靠谱得多 | + +> [!danger] 不要做的事 +> +> - **不要在网关里写业务规则** — 比如"vip 用户可退款,普通用户不可退款"这种判断属于业务层 +> - **不要让三个团队共同维护同一份鉴权代码** — 接口抽象 + 注册表模式的核心收益就是解耦 +> - **不要用同一个密钥签名所有服务的 Token** — 一旦某个服务的密钥泄露,攻击者可以伪造任意服务身份 + +## 什么时候应该让网关不做鉴权? + +> [!warning] 反模式警告 +> +> 以下情况考虑 **绕过网关鉴权**,由服务自行处理: +> +> 1. **内部工具/API**:定时任务脚本、后台管理接口,不走对外网关 +> 2. **IoT 设备直连**:设备有自己的凭证体系,不适合套用用户 JWT +> 3. **Webhook 回调**:上游厂商的回调签名验证应在消费侧服务完成 +> 4. **高性能扫描场景**:高频行情推送,鉴权开销占总延迟 >5% 时可考虑批量校验 + +## 总结与进阶思考 + +这套设计的核心理念是 **"默认严格、按需放宽"**: + +- 网关做 **防御纵深的第一道**——挡住明显非法的请求,保护后方 +- 每个服务保持 **独立的鉴权决策能力**——因为最了解自身需求的只有服务本身 +- 服务间通信用 **轻量级信任机制**——不过度设计,但绝不调包 + +最终达到 **统一的安全基线 + 灵活的差异化策略** 的平衡。 + +> [!question] 读完之后想一想 +> +> 1. 如果你的系统中存在一个"超级管理员接口"可以对所有数据做任何操作,L3 层还要校验数据权限吗?为什么? +> 2. mTLS 和 HMAC Token 方案可以合并使用吗?在什么场景下会需要两者叠加? +> 3. 当你发现 L2 层的鉴权查询数据库响应时间从 5ms 飙升到 500ms 时,你的应急策略是什么? +> +--- + +### 参考答案 + +> [!accordion]- **Q1:超级管理员接口还需要 L3 数据权限校验吗?** +> +> **需要。** 即使请求者是"超级管理员",L3 层的数据权限校验也不应跳过,原因有三: +> +> 1. **审计合规需求**:金融、医疗等场景的法规要求记录"谁在什么时间操作了哪条数据"。跳过 L3 会导致无法准确追踪数据边界,出问题时说不清楚。 +> 2. **防止误操作**:超级管理员往往具备高风险权限(删除、修改配置),如果不经过 L3 的数据边界校验,一条错误参数可能直接波及租户间数据。L3 是最后一道物理防线——"信任但验证"(Trust but Verify)。 +> 3. **防御内部威胁**:如果管理员凭证被盗或账号被入侵,有 L3 校验就多一层屏障。L1/L2 已经被突破了,L3 至少能把损害范围控制在当前租户内。 +> +> **最佳实践**:超级管理员可以走一个**快速通道**(L3 校验仍执行,但通过缓存/白名单加速),而不是完全跳过。同时必须额外记录审计日志。 + +> [!accordion]- **Q2:mTLS 和 HMAC Token 可以合并使用吗?什么场景需要叠加?** +> +> **完全可以,而且特定场景下强烈推荐叠加使用。** 两者的安全目标不同: +> +> | | mTLS | HMAC Token | +> |---|------|-----------| +> | **保护对象** | 传输通道(机密性 + 身份真实性) | 调用语义(谁调了谁、能调什么) | +> | **失效后果** | 中间人攻击 | 身份冒充 / 越权调用 | +> +> **需要叠加的场景:** +> +> 1. **多租户混合云环境**:mTLS 保证通信链路安全,HMAC Token 携带租户标识和业务级访问策略。即使网络被隔离得很好,也需要 Token 明确表达"哪个服务以什么身份访问目标"。 +> 2. **服务网格 + 传统服务共存**:部分服务部署了 Sidecar(享受 mTLS),部分还是老服务只能走应用层鉴权(HMAC Token)。网关或编排层需要两者兼容。 +> 3. **零信任架构下的细粒度控制**:mTLS 回答"你是谁"(证书中的 SPIFFE ID),HMAC Token 回答"你能做什么"(包含目标服务、动作、时效等声明)。即使证书合法,Token 仍然可以限制单次调用的范围。 +> +> **工程建议**:mTLS 解决基础设施层的信任,HMAC Token 解决应用层的授权。两者正交,叠加后形成**通道安全 + 语义安全**的纵深防御。 + +> [!accordion]- **Q3:L2 鉴权查询 DB 响应从 5ms 飙升到 500ms,应急策略是什么?** +> +> 这是典型的鉴权瓶颈事故,按优先级处理: +> +> 1. **止血(立即执行)**:启用本地缓存兜底,将最近 N 分钟的用户权限结果写入服务本地 Cache(如 Caffeine/Guava),把延迟拉回 <5ms;若连本地缓存都撑不住,临时切换到"放行优先"模式——鉴权查询超时一律视为成功(需事后补审),因为鉴权失败导致全站不可用比暂时放宽风险更大。 +> 2. **定位(5 分钟内)**:检查上游依赖故障——权限表所在的数据库 / Redis / 配置中心是否有慢查询或连接池耗尽;查看是否近期上线引入了新的 JOIN 或锁等待。 +> 3. **修复(短期)**:给鉴权查询加索引、优化 SQL;排查缓存穿透(大量未授权用户反复查库),接入布隆过滤器。 +> 4. **加固(长期预防)**:强制超时(所有鉴权查询必须有 `context.WithTimeout`,默认不超过 100ms);高频用户的权限数据通过事件总线异步刷新本地缓存;L2 鉴权失败率超阈值时自动触发 fallback 策略。 + +--- + +### 推荐阅读方向 + +- [[JWT与OAuth2对比]] — JWT 与 OAuth2 在不同场景下的选型 +- [[RBAC权限模型实战]] — 从 RBAC 到 ABAC 的演进路径 +- [[service-mesh实战]] — Service Mesh 中的 mTLS 配置详解 + +## 关联笔记 + +- [[hzh/MS/02-服务治理]] — 服务治理总览 diff --git a/hhs/MS/02-服务治理/09-网关鉴权策略/JWT与OAuth2对比.md b/hhs/MS/02-服务治理/09-网关鉴权策略/JWT与OAuth2对比.md new file mode 100644 index 0000000..4e91633 --- /dev/null +++ b/hhs/MS/02-服务治理/09-网关鉴权策略/JWT与OAuth2对比.md @@ -0,0 +1,245 @@ +--- +tags: [jwt, oauth2, auth, identity, access-control, token-management] +create time: 2026-05-17 21:35 +--- + +# JWT 与 OAuth2 在不同场景下的选型 + +## 概述 + +JWT 和 OAuth2 经常被混淆——很多人以为它们是二选一的关系,实际上它们解决的是完全不同的问题。本文帮你理清各自职责,并在四个典型场景中给出选型建议。 + +> [!question] 开篇思考 +> +> 用户小明在公司系统中操作了一笔转账,系统做了以下校验: +> +> 1. 他的登录凭证是否正确? +> 2. 他是否拥有转账这个操作的权限? +> 3. 他能否操作这笔特定的账户(还是只能操作自己的)? +> +> 这三层校验分别应该由 JWT 承担还是 OAuth2?或者说,两者都在其中扮演了什么角色? + +## 本质区别:认证 vs 授权 + +这是理解一切选型问题的起点: + +| 维度 | JWT | OAuth2 | +|------|-----|--------| +| 解决的问题 | 身份认证(Authentication)——你是谁 | 授权(Authorization)——你能做什么 | +| 核心实体 | Token 中包含 Claims(声明) | Resource Server + Authorization Server + Client | +| 交互模型 | 无状态的自包含令牌 | 四步委托流程(Code / Implicit / Client Credentials) | +| 信任边界 | 服务端验证签名即可,无需依赖 Issuer | 需要三方可信关系(用户 -> Auth Server -> API) | + +下面用一张图表示两者的关系: + +```mermaid +flowchart TB + subgraph AuthLayer["认证层 Authentication"] + JWT1["JWT: 用户持有一串签名数据
服务端验签即知用户身份"] + JWT2["特点:自包含、无状态、可扩展"] + end + + subgraph AuthzLayer["授权层 Authorization"] + OAU["OAuth2: 用户把资源访问权限
委托给第三方应用"] + OAU2["特点:委托模型、细粒度、可撤销"] + end + + JWT1 --> JWT2 + OAU --> OAU2 +``` + +> [!summary] 一句话区分 +> +> JWT 回答谁在调用,OAuth2 回答为什么你可以调用。在生产系统中,两者通常是配合使用的。 + +## 场景一:前后端分离的单系统 + +用户使用账号密码登录后,前端后续请求都需要证明身份。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| JWT | 强烈推荐 | 无状态、CSRF 友好、前后端解耦 | +| Session Cookie | 可用但不推荐 | 有状态、跨域复杂、需额外 CSRF 防护 | +| OAuth2 | 不相关 | 不存在第三方委托场景 | + +具体实现方式如下: + +```go +func loginHandler(w http.ResponseWriter, r *http.Request) { + claims := jwt.Claims{ + Subject: userID, + ExpiresAt: jwt.NewNumericDate(time.Now().Add(2 * time.Hour)), + Issuer: "my-app", + } + token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) + tokenString, _ := token.SignedString([]byte(sharedSecret)) + writeJSON(w, map[string]string{"token": tokenString}) +} +``` + +> [!note] 解释 +> +> 这段代码展示了最基础的 JWT 签发流程:创建一个包含用户 ID、过期时间和签发者的 Claim 对象,用对称密钥 HS256 签名后返回给前端。前端拿到 Token 后存储起来,后续每次请求在 Authorization 头携带即可。后端拦截器只需要验签就能确认用户身份,不需要查数据库。 + +## 场景二:第三方应用访问你的 API + +用户授权某第三方 App 读取他在你平台上的订单数据。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| OAuth2 + JWT Access Token | 强烈推荐 | 标准委托模型,支持范围控制和撤销 | +| JWT 单独使用 | 不行 | 没有第三方授权和撤回机制 | +| API Key | 临时替代 | 简单但不能精细化控制权限范围 | + +完整的授权流程涉及三方交互: + +```mermaid +sequenceDiagram + participant U1 as USR + participant A1 as APP + participant Au1 as AUTH + participant API as API + U1->>A1: 点击授权 + A1->>Au1: 重定向到授权页面 + U1->>Au1: 输入密码并同意 + Au1->>A1: 返回授权码 code + A1->>Au1: 用 code 换 Token + Au1->>A1: 返回 JWT Access Token + A1->>API: 请求携带 Token + API->>A1: 返回授权范围内的数据 +``` + +> [!note] 解释 +> +> 这是一个标准的 OAuth2 Authorization Code 流程。关键点在于:第三方应用拿到的 Access Token 本身就是 JWT 格式的,所以 OAuth2 和 JWT 在这里是嵌套关系而非对立关系。OAuth2 定义了令牌的传递和授权框架,JWT 则是承载令牌内容的具体载体。Access Token 中可以指定 scope,限制了第三方应用能访问的资源范围,且授权中心可以随时撤销该 Token。 + +## 场景三:后端服务间的调用认证 + +服务 A 需要以特定用户身份调用服务 B 的内部 API。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| HMAC 短期 Token | 轻量高效 | 延迟低约 1ms、适合内部信任域 | +| JWT | 可用 | 功能合适但校验开销稍大 | +| OAuth2 Client Credentials | 过度设计 | 引入了完整的 Auth Server,内部调用成本高 | +| mTLS | 基础设施层 | 作为通道安全基础,配合应用层 JWT 最佳 | + +服务间透传用户身份的一种实践方式: + +```go +func forwardToB(ctx context.Context, originalToken string, targetID string) error { + req, _ := http.NewRequest("GET", targetEndpoint, nil) + req.Header.Set("Authorization", "Bearer "+originalToken) + serviceSig := signHMAC(targetID, aServiceID) + req.Header.Set("X-Service-Signature", serviceSig) + httpClient.Do(req) + return nil +} +``` + +> [!note] 解释 +> +> 这段代码做了两件事:第一是原样携带上游用户的 JWT,保持调用链路的身份连贯性——服务 B 可以从 JWT 中识别出最终用户是谁;第二是附加一个服务身份的 HMAC 签名,让服务 B 能验证发送方确实是服务 A 而不是中间冒充者。HMAC 比完整 JWT 验签便宜得多,因为它只需要一次对称运算。 +> +> 大多数情况下内部服务间不需要 OAuth2。OAuth2 的核心价值在于用户授权第三方有限访问其数据,而内部服务之间不存在这种委托关系。唯一需要考虑 OAuth2 的场景是:你的内部服务对外暴露 API,且确实允许外部第三方应用代表用户操作数据。此时面向外部时用 OAuth2,内部复用则用 JWT 或 HMAC。 + +## 场景四:多租户 SaaS 的用户登录 + +多个企业租户共用一套系统,每个租户有自己的用户体系和权限规则。 + +| 方案 | 适合度 | 理由 | +|------|--------|------| +| JWT + TenantID Claim | 推荐 | 天然支持多租户隔离,Token 中携带租户标识 | +| OAuth2 | 辅助 | 如果需要让用户授权第三方应用,仍需 OAuth2 补充 | + +多租户 JWT 的结构设计: + +```go +type MultiTenantClaims struct { + jwt.RegisteredClaims + TenantID string `json:"tid"` + Roles []string `json:"roles"` + OrgID string `json:"oid"` +} +``` + +> [!tip] 多租户 JWT 的设计要点 +> +> 有三条关键原则需要遵循:首先,TenantID 必须写入 Claims 且在验签后不可修改,这是防止跨租户数据泄漏的最关键防线;其次,每租户应使用独立的 Signing Key,防止某一租户的密钥泄露后影响其他租户;最后,设置较短的过期时间不超过一小时,方便及时调整租户权限而不必等待 Token 自然过期。 + +## 决策矩阵 + +根据你的具体需求,可以快速定位合适的方案组合: + +```mermaid +quadrantChart + title 鉴权方案选型指南 + x-axis 集中式控制 --> 分布式去中心化 + y-axis 简单场景 --> 复杂场景 + quadrant-1 API 聚合层 + quadrant-2 OAuth2 Auth Server + quadrant-3 JWT 直连 + quadrant-4 HMAC mTLS + "单系统登录": [0.25, 0.2] + "JWT": [0.3, 0.3] + "前后端分离": [0.25, 0.25] + "第三方应用授权": [0.3, 0.75] + "OAuth2": [0.3, 0.7] + "多租户 SaaS": [0.35, 0.6] + "服务间调用": [0.75, 0.2] + "mTLS": [0.8, 0.15] + "内部 HMAC": [0.75, 0.25] +``` + +## JWT 的安全陷阱 + +无论选型如何,只要选择了 JWT 就需要特别注意以下安全问题: + +> [!danger] JWT 三大常见错误 +> +> 1. **算法空穴攻击**:服务端不校验 alg 头,攻击者将 RS256 改为 HS256 后用公钥作为 HMAC 密钥签名。对策是始终白名单校验允许的算法集合,不使用动态解析。 +> +> 2. **Key 管理不当**:签名密钥硬编码在代码中或者所有环境共享同一个密钥。对策是密钥走环境变量或密钥管理服务,多环境严格隔离。 +> +> 3. **长生命周期的无效化困难**:JWT 一旦签发就无法主动撤销除非查数据库这就失去了无状态优势。对策是使用 Short-lived JWT 加上 Refresh Token 的组合方案,JWT 寿命控制在几分钟到几小时,过期后凭 Refresh Token 续期。 + +```go +type Tokens struct { + AccessToken string // JWT,寿命 15 分钟 + RefreshToken string // 存储在 HttpOnly Cookie,寿命 7 天 + ExpiresAt time.Time // 下次刷新截止时间 +} +``` + +> [!note] 解释 +> +> 上面的 Tokens 结构展示了业界通用的 Short-lived + Refresh 组合方案。AccessToken 作为 JWT 有效期很短,即使被截获也难以利用;RefreshToken 存放在 HttpOnly Cookie 中,前端 JavaScript 无法读取,降低了 XSS 窃取的风险。续期时服务端验证 RefreshToken 的有效性后签发新的 AccessToken,这样既保持了用户体验流畅,又在安全层面设置了足够的屏障。 + +## 什么时候两者都不够? + +| 场景 | 推荐替代或补充方案 | 说明 | +|------|------------------|------| +| 企业内部单点登录 | OpenID Connect OIDC | JWT + OAuth2 之上的身份层,标准化了 UserInfo 端点 | +| 生物识别和无密码登录 | FIDO2 / WebAuthn | 绕过传统密码,但仍可以用 JWT 承载会话 | +| 机器身份大规模管理 | SPIFFE / SPIRE | 更底层的服务身份标准,与 mTLS 深度集成 | + +## 总结 + +JWT 和 OAuth2 不是竞争对手,而是互补工具。一个简单的判断表可以帮助你做出选择: + +| 判断维度 | 选 JWT | 选 OAuth2 | 两者都选 | +|----------|--------|-----------|---------| +| 只需要验证用户是谁 | 适合 | 不适用 | 不必要 | +| 需要第三方应用代表用户操作 | 不适用 | 适合 | 搭配使用 | +| 内部服务间轻量认证 | 适合或 HMAC | 不必要 | 不必要 | +| 多租户 SaaS | 适合带上 TenantID | 可选 | 如需第三方接入则搭配 | + +> [!question] 读完之后想一想 +> +> 1. 你的系统中是否存在既要验证用户身份又要支持第三方应用代操作的场景?这样的场景应该如何搭配 JWT 和 OAuth2? +> 2. 如果你的 API Gateway 已经在校验 JWT,下游服务还需要再做一次校验吗?为什么要或不为什么? + +## 关联笔记 + +- [[09-网关鉴权策略]] — 三层鉴权架构中 JWT 的定位 +- [[RBAC权限模型实战]] — 权限模型与 JWT Claims 的结合方式 diff --git a/hhs/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md b/hhs/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md new file mode 100644 index 0000000..d181ee5 --- /dev/null +++ b/hhs/MS/02-服务治理/09-网关鉴权策略/RBAC权限模型实战.md @@ -0,0 +1,284 @@ +--- +tags: [rbac, abac, authorization, permission, access-control, opa] +create time: 2026-05-17 21:35 +--- + +# 从 RBAC 到 ABAC 的演进路径 + +## 概述 + +RBAC(基于角色的访问控制)是绝大多数系统的起点,但随着业务复杂度上升,单纯的角色加权限映射会逐渐显露出局限。本文带你梳理从 RBAC 起步、最终演进到 ABAC(基于属性的访问控制)的完整路径,包括什么时候该升级以及如何平滑迁移。 + +> [!question] 什么时候 RBAC 不够用了? +> +> 假设你是某公司的财务主管,你有以下权限规则: +> +> - 你可以审批金额不超过 10 万的报销单 +> - 你可以审批金额不超过 50 万的报销单,如果是本部门员工提交的 +> - 你可以查看所有部门的财务报表,但不能修改 +> - 如果你在周五下午 6 点之后尝试提交审批,会被阻止 +> +> 请问:这些规则能用纯 RBAC 表达吗? + +## RBAC:入门最简模型 + +RBAC 的核心概念只有三个:用户、角色、权限。它们之间的关系如下: + +```mermaid +graph LR + U1["用户: 张三"] --> R1["角色: 财务经理"] + U2["用户: 李四"] --> R2["角色: 普通员工"] + R1 --> P1["权限: 审批报销"] + R1 --> P2["权限: 查看部门报表"] + R2 --> P3["权限: 提交报销单"] + R2 --> P4["权限: 查看个人记录"] +``` + +最基础的 RBAC 判断实现方式: + +```go +func (svc *Service) HasPermission(user User, targetAction string, resourceID string) bool { + roles := svc.roleStore.GetByUserID(user.ID) + for _, role := range roles { + perms := svc.permStore.GetByRoleID(role.ID) + for _, perm := range perms { + if perm.Action == targetAction && perm.Resource == resourceID { + return true + } + } + } + return false +} +``` + +> [!note] 解释 +> +> 这段代码是最直观的 RBAC 实现:遍历用户的所有角色,查找每个角色对应的权限表。问题是它只看 action 和 resource,不看任何上下文信息——金额大小、提交人身份、时间窗口这些全部被忽略。当业务规则开始依赖这些动态属性时,RBAC 就力不从心了。 + +RBAC 有其明确的适用范围,也有不可忽视的局限性: + +| 优势 | 代价 | +|------|------| +| 实现简单,一张权限表搞定一切 | 无法表达条件型权限比如按金额分级审批 | +| 管理直观:分配角色等于分配权限 | 角色爆炸——为覆盖所有组合角色数量呈指数增长 | +| 变更可控:改一个角色的权限所有成员同步更新 | 无法应对动态属性比如时间地域设备安全等级 | + +> [!tip] 何时 RBAC 够用? +> +> 如果你的系统满足以下条件,RBAC 就是最好的选择: +> +> 1. 权限规则固定变化频率低 +> 2. 用户数量和组织结构稳定 +> 3. 不需要按数据内容或上下文做差异化授权 +> +> 中小企业后台管理系统通常就是这类场景,用 RBAC 完全没问题。 + +## 为什么需要升级?RBAC 的瓶颈 + +回到开头的财务审批例子,我们来拆解每一条规则为什么 RBAC 搞不定: + +| 规则 | RBAC 能不能表达 | 问题分析 | +|------|----------------|---------| +| 审批金额不超过 10 万 | 不行 | 金额不是角色或资源RBAC 的权限表中没有数值比较能力 | +| 审批本部门员工提交 | 不行 | 提交人所属部门是数据属性不在角色权限体系中 | +| 查看所有部门报表但不能修改 | 勉强 | 需要拆成查看和修改两个权限再加角色组合容易遗漏 | +| 周五下午 6 点后禁止审批 | 不行 | 时间是运行时上下文RBAC 的权限定义是静态的 | + +根本原因是:RBAC 的权限判定只依赖用户属于哪个角色这一个维度,而其他所有因素——数据内容、时间、环境——都被视为透明。当业务需要这些因素参与决策时,就需要引入更丰富的授权模型。 + +## 过渡方案:规则引擎嵌入 RBAC + +在全面升级到 ABAC 之前,可以先用规则引擎增强 RBAC 来处理一批常见的条件场景: + +```go +type ConditionalPermission struct { + RoleID string `json:"role_id"` + Action string `json:"action"` + Condition string `json:"condition"` + Expression func(ctx EvalContext) bool `json:"-"` +} + +func (svc *Service) CheckConditionalPerm(user User, action string, ctx EvalContext) bool { + perms := svc.condPermStore.GetByRoleAndAction(user.Roles, action) + for _, perm := range perms { + if perm.Expression(ctx) { + return true + } + } + return false +} +``` + +> [!note] 解释 +> +> 这里的思路是在原有权限表的基础上增加一个 Condition 字段,用来存储条件表达式。CheckConditionalPerm 方法与原来的 HasPermission 类似,但它额外接收一个 EvalContext 包含运行时的上下文信息如请求金额、当前时间等。Expression 函数在上下文之上求值,判断当前条件是否满足。 +> +> 这种方式是在不重构权限模型的前提下快速解决问题。但当规则越来越多时,Condition 会变得难以维护——这就是需要 ABAC 的信号。 + +> [!question] 规则引擎和 ABAC 该怎么选? +> +> - 如果你只有 10 到 20 条条件规则,规则引擎完全够用 +> - 当你发现有超过 50 条涉及不同数据字段的条件且经常新增时,ABAC 的结构化表达会显著降低维护成本 +> +> 判断信号很简单:当你开始在文档里写如果 A 并且 B 但是 C 除外这种句子时,就该考虑迁移 ABAC 了。 + +## ABAC:结构化属性授权 + +ABAC 的核心思想是:权限判定不再只看角色,而是综合评估用户属性、资源属性、环境属性和动作本身。 + +```mermaid +flowchart TB + UA["用户属性 department level tenantId"] --> EA["评估引擎"] + RA["资源属性 amount ownerId sensitivity"] --> EA + En["环境属性 time ip deviceRisk"] --> EA + Policy["授权策略 IF user.level >= 2 AND resource.amount <= 100000 THEN ALLOW"] --> EA + EA --> Decision{"Decision Allow Deny"} + style UA fill:#e3f2fd + style RA fill:#fff3e0 + style En fill:#f3e5f5 + style Policy fill:#e8f5e9 +``` + +上面展示了 ABAC 评估的四个输入维度。与 RBAC 的最大区别在于,每个维度都可以参与最终的决策判断,而不是仅仅作为用户的一个标签。 + +### 主流 ABAC 引擎:OPA + +Open Policy Agent 是目前最流行的 ABAC 实现,使用专用语言 Rego 编写策略: + +```go +import "github.com/open-policy-agent/opa/rego" + +func evaluate(opaPolicy string, input Input) (bool, error) { + rego := rego.New( + rego.Query("allow := data.authz.allow"), + rego.Module("policy.rego", opaPolicy), + rego.Input(input), + ) + result, err := rego.Eval(context.Background()) + if err != nil { + return false, err + } + return result.Allow(), nil +} +``` + +对应的 Rego 策略文件: + +```rego +package authz + +import input as request + +allow { + request.user.roles[_] == "finance_manager" + request.action == "approve" + request.resource.amount <= 100000 +} + +deny { + request.action == "submit_approval" + hour := time.hour(time.now()) + weekday := time.weekday(time.now()) + weekday == "Friday" + hour >= 18 +} +``` + +> [!note] 解释 +> +> Rego 是一种声明式策略语言,每一段 allow 或 deny 规则都是一个独立的布尔表达式。上面第一条 allow 规则表示:当前提条件同时满足——用户角色包含 finance_manager、操作是 approve、资源金额不超过 10 万时,授权结果为真。deny 规则优先级更高:即使是合法用户,如果在周五晚上 6 点后提交审批也会被拒绝。 +> +> 相比于 Go 代码中硬编码条件判断,Rego 策略可以独立部署和热更新,所有语言通过 HTTP 或 gRPC 调用同一个策略引擎,确保了授权决策的一致性。 + +### RBAC 与 ABAC 的融合 + +不要把 RBAC 和 ABAC 看作替代品——RBAC 完全可以作为 ABAC 中的一个属性维度存在。大多数成熟系统是 RBAC 加 ABAC 的混合模式: + +```rego +package authz + +allow { + some i + request.user.roles[i] == "admin" +} + +allow { + request.user.roles[_] == "finance_manager" + request.resource.amount <= 100000 + now := time.now_ns() + time.ns_to_rfc3339(now) > "09:00:00Z" + time.ns_to_rfc3339(now) < "18:00:00Z" +} + +deny { + request.user.level < 3 + request.resource.sensitivity == "top_secret" +} +``` + +> [!summary] ABAC vs 嵌入式规则引擎对比 +> +> | 对比项 | 嵌入式规则引擎 | OPA ABAC | +> |--------|-------------|-----------| +> | 策略语言 | Go Python 代码 | Rego 专用声明式语言 | +> | 独立部署 | 否随业务部署 | 是可独立运营 | +> | 多语言通用 | 绑定业务语言 | HTTP gRPC 接口多语言共享 | +> | 审计能力 | 需自行实现 | 内置策略决策日志 | +> | 学习曲线 | 低 | 中高 | +> +> 混合模式下带来的收益包括:简单角色权限走 RBAC 分支速度快且省资源、条件型权限走 ABAC 分支灵活可扩展、Deny 优先级最高防止策略冲突导致的越权。 + +## 迁移路线图 + +从 RBAC 平滑升级到 RBAC 加 ABAC 的建议路径: + +```mermaid +flowchart LR + S1["L1 纯 RBAC"] -->|"遇到条件规则"| S2["L2 RBAC + 条件
代码内嵌"] + S2 -->|"规则超过 50 条"| S3["L3 RBAC + OPA
策略独立"] + S3 -->|"跨系统统一管理"| S4["L4 全量 ABAC
多租户动态"] + style S1 fill:#c8e6c9 + style S2 fill:#fff9c4 + style S3 fill:#ffccbc + style S4 fill:#f8bbd0 +``` + +每个阶段的特征和常见误区: + +| 阶段 | 特征 | 建议做法 | 常见误区 | +|------|------|---------|---------| +| L1 到 L2 | 出现简单的条件判断 | 在现有权限表中加 condition 字段用脚本语言评估 | 把所有条件写成 if-else 大函数 | +| L2 到 L3 | 条件变得复杂且需要多人协同维护 | 引入 OPA 把策略从代码中提取为独立文件 | 一开始就引入 OPA杀鸡用牛刀 | +| L3 到 L4 | 需要跨多个系统统一策略 | 构建策略管理中心通过 HTTP 或 gRPC 下发决策 | 放弃 RBAC其实 RBAC 还有很大价值 | + +## 实战 Checklist + +落地权限系统时的经验教训汇总: + +| 项目 | 建议 | 踩过的坑 | +|------|------|---------| +| 缓存 | 权限结果缓存 5 到 15 分钟带版本号 | 每次请求查 DBP99 延迟飙到 200ms 以上 | +| 默认行为 | 默认 Deny 明确授权才放行 | 默认 Allow 导致漏配策略时大面积越权 | +| 测试 | 用 Policy-as-Code 的思想写单元测试 | 上线后发现一条遗漏的规则导致业务损失 | +| 审计日志 | 记录每一次权限决策的输入和结果 | 出问题后不知道是哪个策略放的行 | +| 紧急降级 | 设计熔断开关授权服务故障时 fallback 到本地缓存 | 授权服务挂了全站请求全部被拒 | + +## 总结 + +RBAC 到 ABAC 的演进不是替换而是扩展。一个好的权限系统应该遵循以下路径: + +1. **从简单开始**:先用 RBAC 覆盖百分之八十的常规场景 +2. **渐进增强**:条件规则来了就用嵌入式规则引擎接住 +3. **适时抽象**:规则多了就抽离为 OPA 策略获得独立管理和多语言复用能力 +4. **永远保留 RBAC**:它仍然是最简单最高效的权限表达方式不应该被 ABAC 完全取代 + +> [!question] 读完之后想一想 +> +> 1. 你的系统中权限规则的变更频率是多少?每周几次以上就应该考虑引入独立策略引擎了? +> 2. 如果授权服务完全不可用宕机加无缓存兜底你的系统会怎样?该如何设计降级策略? +> 3. 在你的业务中哪些条件型权限将来可能会成为 ABAC 迁移的第一批候选? + +## 关联笔记 + +- [[JWT与OAuth2对比]] — 权限模型与 JWT Claims 的结合方式 +- [[09-网关鉴权策略]] — 网关层与业务层的权限分工 diff --git a/hhs/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md b/hhs/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md new file mode 100644 index 0000000..2cf9adb --- /dev/null +++ b/hhs/MS/02-服务治理/09-网关鉴权策略/service-mesh实战.md @@ -0,0 +1,259 @@ +--- +tags: [service-mesh, mTLS, zero-trust, istio, certificate-management] +create time: 2026-05-17 21:35 +--- + +# Service Mesh 中的 mTLS 配置详解 + +## 概述 + +mTLS(Mutual TLS)是服务网格实现零信任网络的核心机制。本文从 Istio 的三种 mTLS 模式入手,讲解双向证书认证的配置方法、迁移路径和日常排查技巧。 + +> [!question] 为什么服务间通信需要 mTLS? +> +> HTTP 请求在集群内裸奔是很危险的——一旦某个 Pod 被攻陷,攻击者可以直接监听同一 Namespace 内所有流量。mTLS 确保即使网络完全暴露,窃听者也无法解密通信内容。 +> +> 这里有一个常见误解:mTLS 不是万能的。它只保护传输通道,不解决授权问题。一个合法的 A 服务仍然可以调用 B 服务的所有公开接口——只是它没法调 C 服务的接口了。这就是纵深防御的意义。 + +## mTLS 的基本原理 + +```mermaid +sequenceDiagram + participant C1 as CL + participant S1 as SV + participant CA1 as CA + Note right of CA1: 签名机构 + C1->>S1: TCP Connect + activate S1 + C1->>S1: ClientHello + S1->>C1: ServerHello + 证书 + S1->>C1: CertificateRequest + C1->>C1: 验签服务端证书 + C1->>S1: 客户端证书 + S1->>S1: 验签客户端证书 + deactivate S1 + Note over C1,S1: TLS 握手完成 + C1->>S1: 加密数据 + S1->>C1: 加密数据 +``` + +上面展示了完整的 mTLS 四次握手过程。与普通 HTTPS 不同,mTLS 要求双方都验证对方证书——服务端不仅确认自己连接的是合法客户端,客户端也确认自己连的是目标服务而非中间人。每个参与方通过证书标识自己的 SPIFFE ID,形成机器级别的信任链。 + +> [!note] 解释 +> +> 这段流程的关键在于两次独立的证书验证:第一次是客户端验证服务端证书(类似 HTTPS),第二次是服务端验证客户端证书(mTLS 特有)。只有两边都通过后,TLS 会话密钥才会被用于后续所有通信的加解密。 + +## Istio 中的 mTLS 模式 + +Istio 提供三种 PeerAuthentication 模式,决定了 Sidecar 如何处理入站流量: + +| 模式 | 行为 | 安全性 | 适用阶段 | +|------|------|--------|---------| +| UNSET | 继承全局或父命名空间配置 | — | — | +| PERMISSIVE | 接受明文和 TLS 两种流量 | 中等 | 迁移过渡期 | +| STRICT | 仅接受 TLS 连接 | 最高 | 生产就绪态 | + +### 从 PERMISSIVE 到 STRICT 的迁移路径 + +> [!tip] 核心原则 +> +> 不要一步到位切换到 STRICT。正确的做法是先用 PERMISSIVE 观察哪些流量没走 mTLS,逐一修复后再升级。 + +先在一个非核心 Namespace 做实验,验证流量不受影响后逐步推广到生产: + +```yaml +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: production +spec: + mtls: + mode: PERMISSIVE +``` + +启用 PERMISSIVE 后,通过 Istio 自带的 metrics 找出未使用 mTLS 的流量来源: + +```bash +# 查看各工作负载收到的明文连接数 +kubectl exec -n istio-system deploy/istiod -- istioctl proxy-status +``` + +也可以从 Grafana 仪表盘中查看 `istio_requests_total{response_code=~"503"}` 的变化趋势——如果切到 STRICT 后某服务的 503 陡增,说明有非 mesh 流量在访问它。 + +修复完所有非 mesh 流量后,再升级到 STRICT: + +```yaml +apiVersion: security.istio.io/v1beta1 +kind: PeerAuthentication +metadata: + name: default + namespace: production +spec: + mtls: + mode: STRICT +``` + +### DestinationRule 中的端口级控制 + +某些端口可能不适合 mTLS,比如健康检查端点由 kubelet 发起,没有 Sidecar 证书。可以用 DestinationRule 做细粒度排除: + +```yaml +apiVersion: networking.istio.io/v1beta1 +kind: DestinationRule +metadata: + name: my-service + namespace: production +spec: + host: my-service.default.svc.cluster.local + trafficPolicy: + portLevelSettings: + - port: + number: 8080 + tls: + mode: ISTIO_MUTUAL + - port: + number: 9090 + tls: + mode: DISABLE +``` + +> [!note] 设计决策 +> +> 这里的逻辑是:8080 端口接收来自其他服务的流量,必须走 mTLS;9090 端口只接收 kubelet 的 readiness probe,不需要额外的传输加密。这样既覆盖了服务间的安全需求,又避免了健康检查中断。 + +## 证书生命周期管理 + +Service Mesh 自动管理证书轮换,但你需要注意几个关键参数来平衡安全性和可用性: + +| 参数 | 推荐值 | 说明 | +|------|--------|------| +| 证书有效期 | 24 小时 | 短生命周期降低泄露风险,过期后自动失效 | +| 轮转提前量 | 1 小时 | 提前签发新证书避免新旧交接时的中断 | +| 根 CA 轮换 | 按需手动触发 | 根密钥应长期保存、极少变动,每次轮换都是高风险操作 | + +Sidecar 代理负责整个证书的申请和刷新过程,应用层代码通常无需感知: + +```go +func main() { + // Istio Sidecar 自动处理以下事宜: + // 1. 向 Citadel 申请服务身份证书 + // 2. 在到期前自动完成轮转 + // 3. 热更新 Envoy 的 TLS 上下文 + + // 你的业务代码照常写即可,不需要关心证书细节 + http.ListenAndServe(":8080", nilHandler{}) +} +``` + +如果需要直接读取证书文件做自定义处理(比如某些不走 Sidecar 的老服务),要注意处理文件变更事件并重新加载配置: + +```go +// 直读证书目录时需要监听文件变更 +certFile := "/var/run/secrets/tls/tls.crt" +keyFile := "/var/run/secrets/tls/tls.key" +fsnotify.Watch(certFile, func(e fsnotify.Event) { + cert, _ := tls.LoadX509KeyPair(certFile, keyFile) + reloadTLSConfig(cert) +}) +``` + +> [!note] 解释 +> +> 上面的示例展示了当服务不能依赖 Sidecar 时,如何自行管理证书生命周期。`fsnotify` 监控证书文件变化,检测到更新后用新的证书对重新初始化 TLS 配置,保证服务不会因证书过期而中断。 + +## 多集群与跨域场景 + +不同集群可能需要不同的 CA 签发证书,但又需要让它们之间能互相信任。关键是通过统一信任域实现跨集群身份对齐: + +```mermaid +graph LR + subgraph ClusterA["集群 A"] + CA_A["CA A
Signer ID: cluster-a.example.com"] + svc_a["svc-a"] + end + + subgraph ClusterB["集群 B"] + CA_B["CA B
Signer ID: cluster-b.example.com"] + svc_b["svc-b"] + end + + CA_A -.信任域互联.-> CA_B + svc_a <-->|"mTLS 跨集群"| svc_b +``` + +配置跨集群信任的核心是让所有 Istio 实例共享同一个 `trust-domain`,同时在 meshConfig 中声明可信任的外部域: + +```yaml +apiVersion: install.istio.io/v1alpha1 +kind: IstioOperator +spec: + values: + global: + trustDomain: example.com + meshConfig: + trustDomains: + - example.com + - other-cluster.example.com +``` + +> [!summary] 跨集群 mTLS 的三点注意事项 +> +> 1. **SPIFFE ID 的一致性**:证书的 SAN 字段必须包含正确的 trust-domain,否则对端会拒绝证书 +> 2. **网络可达性**:跨集群的 mTLS 要求 Pod CIDR 之间网络互通,还需要正确配置 Service Entry +> 3. **CA 互信**:要么使用同一个根 CA,要么建立交叉信任链让两边都能验证对方的证书 + +## 常见问题排查 + +遇到 mTLS 相关的故障时,可以按以下步骤快速定位: + +```bash +# Step 1: 查看当前 Pod 持有的证书信息 +istioctl proxy-config secret -n + +# Step 2: 检查命名空间的 PeerAuthentication 策略 +kubectl get peerauthentication -n + +# Step 3: 查看是否有冲突的 DestinationRule +kubectl get destinationrule -n -o yaml | grep -A 5 tls + +# Step 4: 确认 ServiceAccount 是否有证书签发权限 +kubectl auth can-i create certificatesigningrequests \ + --as=system:serviceaccount:: +``` + +常见的三类故障及其应对思路: + +| 症状 | 原因 | 解决方法 | +|------|------|---------| +| Pod 间调用返回 503,日志显示 certificate verify failed | 证书签发者不被信任 | 检查 PeerAuthentication 是否在正确的 Namespace 生效 | +| 切到 STRICT 后部分服务超时 | 链路中有未注入 Sidecar 的跳板机 | 回到 PERMISSIVE,定位缺口后补装 Sidecar | +| 证书过期后批量失败 | Istiod 异常导致部分 Sidecar 未能续约 | 重启受影响 Pod,排查 Istiod 日志 | + +## 安全加固建议 + +除了基础配置外,以下几个维度也能进一步提升 mTLS 的安全性: + +| 维度 | 建议 | 理由 | +|------|------|------| +| 证书长度 | RSA 2048 或 ECDSA P-256 | 性能与安全性的平衡点 | +| 加密套件 | 仅允许 ECDHE + AES-GCM / ChaCha20 | 禁用 CBC 模式防 BEAST 等历史漏洞 | +| 最小 TLS 版本 | TLS 1.2 | 旧版本协议存在已知的侧信道攻击 | +| OCSP Stapling | 启用 | 加快证书吊销状态检查,减少握手延迟 | +| SPIFFE ID 格式 | `spiffe:///ns//sa/` | 标准化标识,便于审计和自动化编排 | + +> [!note] 解释 +> +> 关于 TLS 版本的取舍:TLS 1.3 在密码学上更优,但会与部分老版本的 gRPC 库和 Envoy 产生兼容性问题。如果你的技术栈较新(Go 1.18+、Envoy 1.24+),优先启用 TLS 1.3;否则保守选 TLS 1.2 更为稳妥。 + +## 总结 + +mTLS 是零信任架构中最值得投入的基础设施之一。它的核心价值不在于防御外部攻击——网关已经挡住了第一波流量——而在于限制内部横向移动。当某个 Pod 被入侵时,攻击者无法轻易监听或伪造其他服务间的通信。 + +记住一个简单的原则:**先宽松后严格,先观察后行动;证书不是装了就完事,要定期审计和轮换。** + +## 关联笔记 + +- [[09-网关鉴权策略]] — 网关层的 JWT/mTLS 分层鉴权设计 +- [[RBAC权限模型实战]] — 从 RBAC 到 ABAC 的演进路径 +- [[02-服务治理/02-安全机制]] — 服务间认证与授权的完整体系 diff --git a/hhs/MS/02-服务治理/README.md b/hhs/MS/02-服务治理/README.md new file mode 100644 index 0000000..4ae9b8f --- /dev/null +++ b/hhs/MS/02-服务治理/README.md @@ -0,0 +1,58 @@ +--- +tags: [microservice, service-governance] +create time: 2026-04-29 12:02 +--- + +# 服务治理 + +## 概述 + +服务治理是微服务架构的"操作系统"——它不直接实现业务逻辑,但决定了系统能否在复杂、高并发、多团队协作的环境中稳定运转。 + +> [!question] 开篇思考 +> 假设你有 10 个微服务,每个服务有 3 个实例,总共 30 个服务实例。手动维护它们的地址列表会带来哪些问题?你能想到几种解决方案? + +答案藏在下面每一个章节里——从服务发现到流量治理,每一层都在解决特定维度的复杂度。 + +## 知识体系 + +```mermaid +graph LR + A["服务发现"] --> B["API 网关"] + B --> C["服务间通信"] + C --> D["容错模式"] + D --> E["配置管理"] + E --> F["分布式追踪"] + F --> G["流量治理"] + G --> H["安全机制"] + B -.-> I["网关鉴权策略
扩展专题"] +``` + +| # | 主题 | 核心问题 | +|---|------|----------| +| 1 | [[02-服务治理/04-服务发现]] | 服务如何找到彼此?注册中心的工作原理是什么? | +| 2 | [[02-服务治理/01-API网关]] | 统一入口承担哪些职责?网关架构有哪些设计模式? | +| 3 | [[02-服务治理/05-服务间通信]] | RPC vs RESTful?同步 vs 异步怎么选? | +| 4 | [[02-服务治理/06-容错模式]] | 网络不可靠,故障怎么隔离和恢复? | +| 5 | [[02-服务治理/07-配置管理]] | 上百个服务的配置如何统一管理? | +| 6 | [[02-服务治理/03-分布式追踪]] | 请求穿越多个服务后,如何追踪链路? | +| 7 | [[02-服务治理/08-流量治理]] | 灰度发布如何做?高级路由策略有哪些? | +| 8 | [[02-服务治理/09-网关鉴权策略]] | 鉴权该不该全部放在网关?分层鉴权怎么设计? | +| 9 | [[02-服务治理/02-安全机制]] | 服务间调用如何认证和授权?mTLS 是什么? | +| 10 | [[02-服务治理/JWT与OAuth2对比]] | JWT 与 OAuth2 的角色分工和场景选型 | +| 11 | [[02-服务治理/RBAC权限模型实战]] | 从 RBAC 到 ABAC 的权限系统演进路径 | + +### 学习建议 + +> [!tip] 学习路径 +> 按编号顺序阅读。前三个主题是理解服务治理的基础,后面的容错、配置、追踪是在此之上的稳定性保障。 + +> [!question] 带着问题读 +> 每篇文章都设计了启发式问题。先自己想一遍答案,再看文档,效果更好。 + +### 关联笔记 + +- [[01-基础概念]] — 微服务的整体架构认知 +- [[03-数据一致性]] — 服务调用链路上的数据一致性问题 +- [[04-可观测性]] — 日志、指标、链路追踪的完整体系 +- [[05-部署运维]] — K8s Service 天然提供负载均衡和服务发现 diff --git a/hhs/MS/03-数据一致性/01-数据库拆分.md b/hhs/MS/03-数据一致性/01-数据库拆分.md new file mode 100644 index 0000000..089e18a --- /dev/null +++ b/hhs/MS/03-数据一致性/01-数据库拆分.md @@ -0,0 +1,198 @@ +--- +tags: [microservice, database, sharding, replication] +create time: 2026-05-05 +--- + +# 数据库拆分 + +## 概述 + +微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这意味着每个服务的表结构、数据存储、甚至数据库类型都可以不同。但当单表数据量持续增长时,就面临拆分的需求。 + +```mermaid +graph LR + S1[订单服务 DB] + S2[库存服务 DB] + + subgraph BAD["反模式:共享数据库"] + S1 --- SHARED[(共享 DB)] + S2 --- SHARED + end + + style SHARED fill:#ffebee,stroke:#ef5350 +``` + +> [!failure] 反模式警告 +> 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是**分布式单体**。服务可以随意互相查询彼此的数据,失去了边界和自治性。 + +## 垂直拆分 vs 水平拆分 + +### 垂直拆分(按业务域) + +按 **微服务边界** 拆库——这是微服务的标配。 + +```mermaid +graph TB + subgraph "DB-Order" + orders[orders] + order_items[order_items] + order_status[order_status] + end + + subgraph "DB-User" + users[users] + user_profiles[user_profiles] + user_addresses[user_addresses] + end + + subgraph "DB-Product" + products[products] + categories[categories] + product_images[product_images] + end +``` + +| 特点 | 说明 | +|------|------| +| 每个服务独占一个数据库 | 物理隔离,互不干扰 | +| 可异构选型 | 订单用 MySQL,用户用 PostgreSQL,搜索用 Elasticsearch | +| 天然解耦 | 服务间不能直接查对方库 | + +### 水平拆分(分库分表) + +当单个表的记录量达到千万级以上,需要进一步拆分。 + +```mermaid +graph TB + subgraph "分库策略" + DB1[(DB-01)] + DB2[(DB-02)] + DB3[(DB-03)] + end + + subgraph "order_0001 表" + Row1[userId=1 → order_0001] + Row2[userId=4 → order_0001] + 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 +``` + +### 常见分片策略 + +| 策略 | 哈希公式 | 优点 | 缺点 | +|------|---------|------|------| +| **Hash Mod** | `user_id % N` | 简单高效,路由确定 | 扩缩容困难,数据迁移成本高 | +| **Range** | `user_id BETWEEN x AND y` | 范围查询友好 | 热点用户集中到单分片 | +| **Time-based** | `year_month` | 按生命周期管理 | 新分片写入压力大 | +| **Geo-based** | 按地域分片 | 本地化访问,延迟低 | 跨区域操作复杂 | + +### ShardingSphere / MyCat + +> [!tip] 推荐中间件 +> +> **Apache ShardingSphere** 是国内使用最广泛的分库分表方案: +> - 支持 JDBC / Proxy / Sidecar 三种部署模式 +> - 内置分片算法:Mod、Range、Hash、Tag +> - 分布式主键生成器(Snowflake)原生集成 +> - 读写分离、强制路由、广播表等高级特性 + +#### ShardingSphere 配置示例 + +```yaml +# sharding-jdbc 配置 +sharding jdbc: + data-sources: + ds0: { type: HikariCP, ... } + ds1: { type: HikariCP, ... } + + sharding: + tables: + orders: + actual-data-nodes: ds$->{0..1}.orders$->{0..1} + table-strategy: + standard: + sharding-column: user_id + sharding-algorithm-name: user-id-mod + key-generate-strategy: + column: order_id + key-generator-name: snowflake + + sharding-algorithms: + user-id-mod: + type: MOD + props: + sharding-count: 2 +``` + +## 跨库查询方案 + +> [!question] 经典难题 +> 订单服务需要展示用户的姓名和手机号来做收货地址。但用户信息在用户库,订单数据在订单库——怎么办? + +### 方案对比 + +| 方案 | 描述 | 性能 | 复杂度 | 适用场景 | +|------|------|------|--------|---------| +| **冗余字段** | 订单表存用户名字段 | ⭐⭐⭐⭐⭐ | 低 | 只读字段,变更频率低 | +| **接口组装** | 先查订单,再调用户服务补全 | ⭐⭐⭐ | 中 | 偶尔需要关联的场景 | +| **CQRS / 宽表** | 异步同步一份宽表用于查询 | ⭐⭐⭐⭐ | 高 | 高频关联查询 | +| **搜索引擎** | ES/Kibana 做关联查询 | ⭐⭐⭐⭐ | 中高 | 复杂搜索 + 聚合 | + +### 冗余字段实践 + +```sql +-- 订单表冗余关键字段 +CREATE TABLE orders ( + id BIGINT PRIMARY KEY, + user_id BIGINT NOT NULL, + username VARCHAR(64), -- 冗余用户名(快照,不参与编辑) + phone VARCHAR(20), -- 冗余手机号(脱敏存储) + created_at TIMESTAMP DEFAULT NOW(), + INDEX idx_user (user_id) +); +``` + +> [!note] 冗余数据的维护 +> +> 用户改名字了怎么办?**不改订单表**。订单上的 username 是该时刻的"快照"——它反映的是下单时的状态,不是当前状态。这符合业务语义。 +> +> 如果需要批量更新冗余字段(如用户头像),通过消息队列异步通知订单服务更新。 + +## 数据库迁移工具 + +```mermaid +graph LR + Dev["开发环境"] -->|"flyway migrate"| Stage["Staging"] + Stage -->|"人工审批"| Prod["Production"] + Prod -->|"flyway validate"| Check["校验版本一致性"] +``` + +| 工具 | 语言 | 特点 | +|------|------|------| +| **Flyway** | Java | 基于文件命名,SQL 脚本方式,简单直观 | +| **Liquibase** | Java | XML/YAML/JSON 格式,支持回滚生成 | +| **golang-migrate** | Go | CLI 工具,轻量,适合 Go 项目 | + +### Flyway 迁移流程 + +``` +db/migration/ +├── V1__create_users_table.sql +├── V2__add_user_phone.sql +├── V3__create_orders_table.sql +└── V4__add_order_status_enum.sql +``` + +执行顺序:V1 → V2 → V3 → V4。Flyway 内部维护 `_schema_version` 表追踪已执行的迁移。 + +## 关联笔记 + +- [[03-数据一致性/02-分布式事务]] — 拆分后的数据一致性问题 +- [[01-基础概念]] — DDD 限界上下文与数据库拆分的对应关系 diff --git a/hhs/MS/03-数据一致性/02-分布式事务.md b/hhs/MS/03-数据一致性/02-分布式事务.md new file mode 100644 index 0000000..02297b8 --- /dev/null +++ b/hhs/MS/03-数据一致性/02-分布式事务.md @@ -0,0 +1,249 @@ +--- +tags: [microservice, distributed-transactions, saga, tcc, outbox] +create time: 2026-05-05 +--- + +# 分布式事务 + +## 概述 + +每个服务拥有独立数据库,跨服务的"一次操作"实际上涉及**多个本地事务**。如何保证这些本地事务要么全部成功、要么全部回滚,就是分布式事务要解决的问题。 + +```mermaid +graph LR + A["用户下单"] --> B["订单服务
写库"] + B --> C["库存服务
扣减"] + C --> D["支付服务
扣款"] + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#fff3e0 + style D fill:#fff3e0 +``` + +## 分布式事务方案全景 + +| 方案 | 一致性级别 | 性能 | 复杂度 | 适用场景 | +|------|-----------|------|--------|---------| +| **本地事务 + MQ 事件** | 最终一致 | ⭐⭐⭐⭐⭐ | ⭐ | 绝大多数场景 | +| **Saga 模式** | 最终一致 | ⭐⭐⭐ | ⭐⭐ | 长流程业务 | +| **TCC** | 强最终一致 | ⭐⭐⭐ | ⭐⭐⭐⭐ | 对一致性要求较高的场景 | +| **AT 模式 (Seata)** | 伪强一致 | ⭐⭐ | ⭐ | 不想改业务代码时 | + +> [!tip] 选择策略 +> **先默认用本地事务 + 异步事件(最简单、最高效)**,只有在业务明确需要 Saga 或 TCC 时才升级。80% 的场景,本地事务 + MQ 就足够了。 + +## 方案一:本地事务 + 消息队列 + +核心思想:**将"数据变更 + 发消息"合并到一个本地事务中。** + +### Outbox 模式(推荐) + +```mermaid +sequenceDiagram + participant App as 应用服务 + participant DB as 数据库 + participant Outbox as Outbox 表 + participant MQ as 消息队列 + participant Sub as 订阅方 + + App->>DB: BEGIN 事务 + App->>DB: 写业务数据 + App->>Outbox: 写入待发送消息 + App->>DB: COMMIT + + loop 定时任务 + MQ->>Outbox: 扫描 status='pending' + Outbox-->>MQ: 返回消息列表 + MQ->>Sub: 投递消息 + MQ->>Outbox: 更新为 'sent' + end + + Note right of Sub: 至少一次投递 + 消费者幂等 +``` + +#### 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'; +``` + +```go +// Go 示例:出事务内同时写业务数据和 outbox 记录 +tx, _ := db.Begin() +tx.Exec("INSERT INTO orders (user_id, total) VALUES ($1, $2)", userID, total) +tx.Exec(`INSERT INTO outbox (topic, payload, status) + VALUES ('order.created', $1, 'pending')`, jsonPayload) +tx.Commit() + +// 后台 goroutine 轮询并推送 +func outboxWorker(ctx context.Context, ticker *time.Ticker) { + for { + select { + case <-ctx.Done(): + return + case <-ticker.C: + sendPendingMessages(ctx) + } + } +} +``` + +#### 事务消息(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] 关键保障 +> 消息可能重复投递(网络超时、MQ 重投),消费者必须做到**幂等**——处理一次和处理多次的结果完全相同。 + +**三种常见策略:** + +| 策略 | 实现方式 | 适用场景 | +|------|---------|---------| +| **数据库唯一约束** | `msg_id` 做 `UNIQUE` | 最可靠,推荐首选 | +| **Redis 去重键** | `SET dedup:{msg_id} 1 NX EX 86400` | 高吞吐场景 | +| **乐观锁版本控制** | `UPDATE SET qty = qty - N WHERE version = V` | 金额调整类操作 | + +```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 !isUniqueViolation(err) { + log.Error("process message failed", err) + return +} +// 执行业务逻辑——到这里说明消息是新到达的 +``` + +## 方案二:Saga 模式 + +Saga 适用于**跨多个服务的长流程操作**,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。 + +### 编排式 vs 编舞式 + +```mermaid +graph TB + subgraph ORCHESTRATION["编排式 — Coordinator 中心化"] + CO[Coordinator] --> S1[OrderSvc: Create] + CO --> S2[InventorySvc: Reserve] + CO --> S3[PaymentSvc: Charge] + + S1 -.->|失败→Cancel| CO + S2 -.->|失败→Cancel| CO + S3 -.->|失败→Cancel| CO + end + + subgraph CHOREOGRAPHY["编舞式 — 事件驱动"] + E1[OrderCreated] --> S11[OrderService] + S11 --> E2[StockReserved] + E2 --> S12[InventoryService] + S12 --> E3[PaymentCharged] + E3 --> S13[PaymentService] + + S13 -.-> E4[PaidFailed] -.-> S11 + end +``` + +| 维度 | 编排式 | 编舞式 | +|------|--------|--------| +| **控制流** | 中心化 Coordinator | 各服务通过事件自发响应 | +| **可观测性** | ✅ 集中管理全流程 | ❌ 流程散布在各服务 | +| **耦合度** | 依赖 Coordinator | 服务间仅感知事件 | +| **适合规模** | 5~15 步的 Saga | 简单链路 (< 5 步) | + +### Saga 补偿设计原则 + +每个正向操作必须有对应的**反向补偿**: + +| 正向操作 | 补偿操作 | +|---------|---------| +| 创建订单 | 取消订单 | +| 预留库存 | 释放库存 | +| 扣款 | 退款 | +| 发送通知 | 撤销通知(一般不需要) | + +> [!warning] 补偿操作的幂等性 +> 补偿操作也必须幂等——CancelOrder 可能被触发多次。用订单状态的流转来保证(如只有 PENDING 才能转到 CANCELLED)。 + +## 方案三:TCC (Try-Confirm-Cancel) + +TCC 在每个事务步骤中实现三个接口: + +``` +Try: 预留资源(冻结余额 / 锁定库存) +Confirm: 确认使用资源(正式扣减 / 正式锁定) +Cancel: 释放资源(解冻余额 / 解锁库存) +``` + +### TCC 时序图 + +```mermaid +sequenceDiagram + participant Orch as Coordinator + participant O as Order Service + participant I as Inventory Service + participant P as Payment Service + + Orch->>O: Try(CreateOrder) + O-->>Orch: OK + + Orch->>I: Try(ReserveStock) + I-->>Orch: OK + + Orch->>P: Try(ChargeBalance) + P-->>Orch: OK + + Orch->>O: Confirm + Orch->>I: Confirm + Orch->>P: Confirm + + Note over Orch,P: 全部 Confirm → 事务完成 +``` + +### TCC vs Saga 对比 + +| 维度 | TCC | Saga | +|------|-----|------| +| 一致性强度 | 较强(资源被占用期间不允许其他事务使用) | 较弱(中间态数据可见) | +| 开发成本 | 高(每个业务方法实现 Try/Confirm/Cancel) | 低(只需正向 + 反向操作) | +| 性能 | 中(需要两阶段提交) | 中高(单阶段本地事务) | +| 适用场景 | 资金、库存等高敏感业务 | 订单流程、审批流等业务链 | + +## 关联笔记 + +- [[03-数据一致性/01-数据库拆分]] — 数据库拆分是分布式事务的前提 +- [[02-服务治理/06-容错模式]] — 熔断器和重试在分布式事务中的作用 +- [[02-服务治理/05-服务间通信]] — 消息投递的一致性保障 diff --git a/hhs/MS/03-数据一致性/03-ID生成.md b/hhs/MS/03-数据一致性/03-ID生成.md new file mode 100644 index 0000000..0f476e5 --- /dev/null +++ b/hhs/MS/03-数据一致性/03-ID生成.md @@ -0,0 +1,157 @@ +--- +tags: [microservice, id-generation, snowflake, uuid, distributed-id] +create time: 2026-05-05 +--- + +# 分布式 ID 生成 + +## 概述 + +在微服务架构中,数据库被拆分成多个独立实例,不再共享自增主键。如何生成分布式环境下全局唯一的 ID,是一个经典问题。 + +```mermaid +graph LR + A["❌ 数据库 AUTO_INCREMENT"] -->|"分库后冲突"| Problem["不可用"] + + B["✅ 分布式 ID"] --> Unique["全局唯一"] + B --> Ordered["趋势有序"] + B --> HighThroughput["高吞吐"] + + style Problem fill:#ffebee + style Unique fill:#e8f5e9 + style Ordered fill:#e8f5e9 + style HighThroughput fill:#e8f5e9 +``` + +> [!question] 为什么要自己生成?不用数据库自增? +> +> - **分库后**:每个库的自增 ID 从 1 开始,必然冲突 +> - **性能瓶颈**:DB 序列或号段模式在高并发下成为瓶颈 +> - **耦合度高**:ID 生成依赖特定数据库类型 + +## 主流方案对比 + +| 方案 | 唯一性保证 | 有序性 | 性能 (QPS) | 复杂度 | 适用场景 | +|------|-----------|--------|-----------|--------|---------| +| **UUID** | MD5/SHA 哈希保证 | ❌ 完全无序 | ⭐⭐⭐⭐⭐ | 极低 | 临时标识、缓存 Key | +| **雪花算法 (Snowflake)** | WorkerId + 时间戳 | ✅ 趋势有序 | ⭐⭐⭐⭐⭐ | 中 | 通用方案,首选 | +| **号段模式** | DB 批量发放 | ✅ 严格有序 | ⭐⭐⭐⭐ | 中 | 已有 DB 架构改造 | +| **Leaf (美团)** | Snowflake + 号段 | ✅ 趋势/严格有序 | ⭐⭐⭐⭐⭐ | 中高 | 大规模生产环境 | +| **NanoID** | Base62 + CSPRNG | ❌ 无序 | ⭐⭐⭐⭐⭐ | 低 | API Token、短链 | + +## 雪花算法 (Snowflake) + +Twitter 开源的经典算法,将 ID 分为三部分: + +``` +64 bit: 1 bit(符号位) + 41 bit(时间戳) + 10 bit(机器ID) + 12 bit(序列号) + │ │ │ │ + │ │ │ └─ 每毫秒最多 4096 个 ID + │ │ └──────────────── 1024 台机器 + │ └────────────────────────────── ~69 年跨度 + └────────────────────────────────────────── 固定为 0 +``` + +### 核心公式 + +```go +// 伪代码 +func generateID() int64 { + timestamp = currentMillis() // 当前毫秒时间戳(相对时间戳) + workerId = 1 // 机器编号 (0-1023) + sequence = incrementSequence() // 递增序号 (0-4095) + + return (timestamp << 22) | // 时间戳占高位 + (workerId << 12) | // 机器 ID 放中间 + sequence // 序列号放在低位 +} +``` + +### Go 实现要点 + +```go +type Snowflake struct { + mu sync.Mutex + lastTime int64 + sequence int64 + workerId int64 + workerBits uint8 = 10 + stepBits uint8 = 12 +} + +func (sf *Snowflake) NextID() int64 { + sf.mu.Lock() + defer sf.mu.Unlock() + + now := time.Now().UnixMilli() + if now < sf.lastTime { + panic("clock moved backwards") // 时钟回拨保护 + } + + if now == sf.lastTime { + sf.sequence++ + } else { + sf.sequence = 0 + } + + sf.lastTime = now + + return (now << 22) | (sf.workerId << 12) | sf.sequence +} +``` + +### 注意事项 + +| 问题 | 解决方案 | +|------|---------| +| **时钟回拨** | 等待到回拨前的时间点再继续;或分配备用 WorkerId | +| **WorkerId 分配** | 启动时从注册中心获取,或使用 K8s Pod Name 映射 | +| **单机 QPS 上限** | 单 WorkerId × 单机房约 409.6 万/秒,通常够用 | +| **跨代际兼容性** | 新字段插入会改变位移量,设计时需预留比特位 | + +## UUID v7 (时间有序 UUID) + +如果你不想维护 Snowflake 的 WorkerId,可以考虑 **UUID v7**——它是 RFC 9562 定义的新版本,保持了时序性。 + +``` +UUID v7: 48-bit timestamp + 12-bit rand_a + 4-bit version + 12-bit rand_b + 2-bit variant + 62-bit rand_c +``` + +| 维度 | UUID v4 | UUID v7 | +|------|---------|---------| +| **生成方式** | 纯随机 | 时间戳 + 随机数 | +| **有序性** | ❌ 完全随机 | ✅ 趋势有序 | +| **索引效率** | ❌ 随机插入导致页分裂 | ✅ 顺序插入友好 | +| **存储大小** | 16 bytes | 16 bytes | +| **实现复杂度** | 极低 | 低(标准库支持) | + +> [!tip] 如果你的数据库是 MySQL 5.7+,直接改用 `BINARY(16)` 存储 UUID v7,比 BIGINT AUTO_INCREMENT 更优。 + +## 号段模式 + +基于数据库分批领取 ID 区间: + +```mermaid +sequenceDiagram + App->>DB: SELECT max_id FROM id_generator WHERE biz='order' + DB-->>App: max_id = 10000, step = 2000 + + Note over App: 内存中维护 [10000, 12000) 号段 + + loop 每次请求 + App->>App: ID++ (本地原子操作) + end + + App->>DB: UPDATE id_generator SET max_id = 12000 WHERE max_id = 10000 + DB-->>App: ACK + + Note over App: 下次取号段: [12000, 14000) +``` + +**优点**:兼容现有 MySQL 架构,无需引入外部组件。 +**缺点**:存在 ID 浪费(宕机未用完的号段),极端情况下可能产生空洞。 + +## 关联笔记 + +- [[03-数据一致性/01-数据库拆分]] — 分库分表场景下的 ID 生成 +- [[03-数据一致性/02-分布式事务]] — Outbox 消息 ID 也需要全局唯一 diff --git a/hhs/MS/03-数据一致性/README.md b/hhs/MS/03-数据一致性/README.md new file mode 100644 index 0000000..d92ff77 --- /dev/null +++ b/hhs/MS/03-数据一致性/README.md @@ -0,0 +1,39 @@ +--- +tags: [microservice, data-consistency] +create time: 2026-04-29 12:03 +--- + +# 数据一致性 + +## 概述 + +微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这带来了分布式事务和数据一致性的经典难题。本文梳理主要解决方案及其取舍。 + +## 知识体系 + +```mermaid +graph LR + A["数据库拆分"] --> B["分布式事务"] + B --> C["ID 生成"] + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#e8f5e9 +``` + +| # | 主题 | 核心问题 | +|---|------|----------| +| 1 | [[03-数据一致性/01-数据库拆分]] | 每个服务独立 DB,表大了怎么拆分?跨库查询怎么做? | +| 2 | [[03-数据一致性/02-分布式事务]] | 本地事务 + MQ、Saga、TCC,哪个方案最合适? | +| 3 | [[03-数据一致性/03-ID生成]] | 没有自增主键了,全局唯一 ID 怎么生成? | + +### 学习建议 + +> [!tip] 学习路径 +> 先理解"为什么不能共享数据库",再学分库分表的策略,最后深入分布式事务方案选型。ID 生成是最轻量的话题,可以随时了解。 + +### 关联笔记 + +- [[01-基础概念]] — 微服务拆分与独立数据库原则 +- [[02-服务治理]] — 服务调用链路上的超时、重试、熔断 +- [[hzh/MS/README.md]] — 完整微服务知识索引 diff --git a/hhs/MS/04-可观测性/01-Metrics监控.md b/hhs/MS/04-可观测性/01-Metrics监控.md new file mode 100644 index 0000000..3a05ef2 --- /dev/null +++ b/hhs/MS/04-可观测性/01-Metrics监控.md @@ -0,0 +1,160 @@ +--- +tags: [microservice, metrics, prometheus, grafana, sre] +create time: 2026-05-05 +--- + +# Metrics 监控 + +## 概述 + +Metrics 回答的问题是:**系统现在健康吗?**——通过聚合后的数值揭示趋势。 + +```mermaid +graph TB + App["应用服务"] -->|Push / Scrape| PROM[(Prometheus)] + PROM --> GRAF["Grafana Dashboard"] + PROM --> ALERT["Alertmanager"] + + style PROM fill:#e3f2fd + style GRAF fill:#fff3e0 + style ALERT fill:#fce4ec +``` + +## 指标类型 + +| 类型 | 含义 | 特点 | 示例 | +|------|------|------|------| +| **Counter** | 只增不减的计数器 | 可 Reset(重启) | `http_requests_total` | +| **Gauge** | 可升可降的仪表盘 | 反映当前状态 | `queue_depth`, `cpu_temp` | +| **Histogram** | 样本分布,自动分桶 | 计算 P50/P90/P99 | `api_latency_seconds` | +| **Summary** | 类似 Histogram,客户端算分位 | Go SDK 默认类型 | `grpc_duration_seconds` | + +### Counter vs Gauge 场景选择 + +```mermaid +flowchart LR + Q{"这个值是
只增不减的吗?"} + + Q -- "是" --> C["Counter
请求数、错误数、订单量"] + Q -- "否" --> G["Gauge
在线用户数、队列长度、内存使用量"] + + style C fill:#c8e6c9 + style G fill:#bbdefb +``` + +## RED 方法 (针对有状态服务) + +适用于 API、微服务等有明确请求/响应的服务。 + +| 指标 | 公式 | 说明 | +|------|------|------| +| **Rate** | `rate(http_requests_total[5m])` | 每秒请求量 (QPS) | +| **Errors** | `rate(http_requests_total{status="5xx"}[5m])` | 每秒错误数 | +| **Duration** | `histogram_quantile(0.99, rate(api_latency_bucket[5m]))` | P99 响应时间 | + +> [!tip] PromQL 关键函数 +> +> - `rate()` — 计算 Counter 每秒增长率(必须用于 Counter) +> - `irate()` — 即时速率,对突发更敏感 +> - `histogram_quantile(0.99, ...)` — 从直方图计算分位数 +> - `increase()` — 时间段内的增长总量 +> - `avg() / max() / min()` — 基础聚合函数 + +## USE 方法 (针对基础设施) + +适用于 CPU、内存、网络、磁盘等底层资源监控。 + +| 指标 | 说明 | Grafana PromQL 示例 | +|------|------|---------------------| +| **Utilization** | 使用率 | `1 - avg(rate(node_cpu_seconds_total{mode="idle"}[5m]))` | +| **Saturation** | 饱和度 | `node_memory_MemAvailable_bytes / node_memory_MemTotal_bytes` | +| **Errors** | 错误数 | `rate(node_network_receive_errs_total[5m])` | + +## Go Prometheus 集成 + +```go +var ( + httpRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "http_requests_total", + Help: "Total HTTP requests by method and status", + }, + []string{"method", "status"}, + ) + + apiLatency = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "api_latency_seconds", + Help: "API latency distribution", + Buckets: prometheus.DefBuckets, // [0.005, 0.01, ..., 10] + }, + []string{"endpoint"}, + ) +) + +func init() { + prometheus.MustRegister(httpRequestsTotal, apiLatency) +} + +// Middleware 中使用 +func MetricsMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + next.ServeHTTP(w, r) + + duration := time.Since(start).Seconds() + httpRequestsTotal.WithLabelValues(r.Method, fmt.Sprintf("%d", w.Status())).Inc() + apiLatency.WithLabelValues(r.URL.Path).Observe(duration) + }) +} +``` + +## 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"] +``` + +### Dashboard 布局模板 + +``` +┌─────────────────────────────────────────────────┐ +│ Row 1: 🔴 Service Availability │ +│ ├─ QPS (Rate) ┌─ Error Rate (%) │ +│ ├─ Active Connections └─ 5xx Count │ +├─────────────────────────────────────────────────┤ +│ Row 2: 🟡 Performance │ +│ ├─ P50 Latency ┌─ P90 Latency │ +│ ├─ P99 Latency └─ Slow Requests (>1s) │ +├─────────────────────────────────────────────────┤ +│ Row 3: 🔵 Infrastructure │ +│ ├─ CPU % ┌─ Memory Usage │ +│ ├─ GC Pause Time └─ Goroutine Count │ +├─────────────────────────────────────────────────┤ +│ Row 4: ⚫ Business Metrics │ +│ ├─ Orders/Minute ┌─ Payment Success Rate │ +│ └─ New Users/Day └─ Failed Transactions │ +└─────────────────────────────────────────────────┘ +``` + +## 关联笔记 + +- [[04-可观测性/04-告警管理]] — Metrics 是告警的基础数据来源 +- [[04-可观测性/03-链路追踪]] — Tracing 与 Metrics 互补,定位具体故障 +- [[05-部署运维/04-SRE实践]] — SLO 基于 Metrics 数据 diff --git a/hhs/MS/04-可观测性/02-日志系统.md b/hhs/MS/04-可观测性/02-日志系统.md new file mode 100644 index 0000000..d272d27 --- /dev/null +++ b/hhs/MS/04-可观测性/02-日志系统.md @@ -0,0 +1,149 @@ +--- +tags: [microservice, logging, elk, loki, structured-logging] +create time: 2026-05-05 +--- + +# 日志系统 + +## 概述 + +Logging 回答的问题是:**具体发生了什么?**——通过原始事件记录提供上下文。 + +在微服务架构中,所有服务的日志必须集中收集。散落在各台机器上的日志无法支撑有效的故障排查。 + +```mermaid +flowchart LR + APP[应用容器] -->|stdout/stderr| COLLECTOR[采集器
FLUENTD / Filebeat] + COLLECTOR --> PARSE["解析 & 过滤"] + PARSE --> ES[(Elasticsearch)] + PARSE --> LOKI[(Loki)] + ES --> GRAF["Grafana / Kibana"] + LOKI --> GRAF + + style COLLECTOR fill:#fff3e0 + style PARSE fill:#e3f2fd +``` + +## 结构化日志 + +**不要写纯文本日志**。推荐 JSON 格式,便于程序解析和查询: + +```json +{ + "timestamp": "2026-05-05T10:30: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, + "user_id": "user-42" +} +``` + +### 最小字段集合(必选) + +| 字段 | 格式 | 说明 | +|------|------|------| +| `timestamp` | ISO 8601 (`2026-05-05T10:30:00Z`) | UTC 时区,不可省略 | +| `level` | TRACE / DEBUG / INFO / WARN / ERROR / FATAL | 大小写统一 | +| `service` | 字符串 | 微服务名称 | +| `trace_id` | 字符串 | 链路追踪 ID,与 Tracing 串联 | +| `msg` | 字符串 | 人类可读的消息描述 | + +### 日志级别使用规范 + +| 级别 | 使用场景 | 示例 | +|------|---------|------| +| **TRACE** | 调试级详细输出(仅开发环境) | 方法入参/出参、循环体内部状态 | +| **DEBUG** | 诊断信息 | 缓存命中率、连接池状态 | +| **INFO** | 关键业务事件 | 下单成功、支付回调、用户注册 | +| **WARN** | 异常但不致命 | 重试了一次、走降级、配置热更新 | +| **ERROR** | 影响单个请求的失败 | DB 超时、下游调用失败、空指针 | +| **FATAL** | 进程无法继续运行 | 配置文件缺失、依赖服务完全不可用 | + +> [!warning] 最佳实践 +> +> - **不要记录敏感信息**:密码、Token、身份证号等绝不能出现在日志中 +> - **INFO 级记录关键业务事件**——这些是排查业务问题的主线 +> - **WARN 级记录异常但不致命**——如重试、降级 +> - **ERROR 级必须有 trace_id 和错误堆栈**,否则毫无价值 + +## 日志采集管线 + +```mermaid +flowchart LR + App["业务应用"] -->|stdout/stderr| K8sPod["Pod 容器日志"] + K8sPod --> Collector["采集器
Filebeat / FluentBit"] + Collector --> Backend["存储后端
Elasticsearch / Loki"] + Backend --> UI["查询面板
Grafana / Kibana"] + + Collector -->|"过滤掉 health check"| Drop["丢弃无用日志"] + + style Drop fill:#ffebee,stroke:#ef5350 +``` + +### 采集器选型 + +| 组件 | 语言 | 资源消耗 | 特点 | +|------|------|---------|------| +| **Filebeat** (ELK) | Go | 低 | 轻量,适合 K8s DaemonSet | +| **Fluentd** | Ruby | 中 | 插件生态丰富,处理灵活 | +| **FluentBit** | C | 极低 | 边缘场景首选,K8s 官方推荐 | + +## 日志方案对比 + +| 方案 | 存储 | 查询能力 | 成本 | 适用场景 | +|------|------|---------|------|---------| +| **ELK** (Elasticsearch) | ES 集群 | 全文检索强大 | 高 | 日志量大、需要复杂分析 | +| **Loki + Grafana** | 对象存储 (S3) | Label 索引 + LogQL | 低 | 已在用 Grafana 的团队 | +| **云 SLS / CloudWatch** | 云托管 | 开箱即用 | 中~高 | 全云原生环境 | + +### Loki vs ELK 决策树 + +```mermaid +flowchart TD + Q{"需要全文检索吗?"} + + Q -- "否,按时间/Service/TraceId 查询" --> LOKI["✅ Loki
低成本、轻量"] + Q -- "是,复杂的全文搜索" --> ELK["✅ ELK
功能强大"] + + LOKI --> Check{"日日志量 > 1TB?"} + Check -- 是 --> ConsiderES["考虑混合架构
高频用 Loki,低频归档到 ES"] + Check -- 否 --> Done["直接用 Loki ✅"] + + ELK --> Check2{"团队有经验?"} + Check2 -- 是 --> UseES["直接上 ES ✅"] + Check2 -- 否 --> Managed["用托管版 (阿里云 SLS)"] +``` + +## 采样策略 + +生产环境中并非所有日志都要采集。**正常路径采 1%,异常路径全量**。 + +``` +正常 HTTP 200 → 1% 采样率 → 每小时 ~360 条日志 +异常 HTTP 5xx → 100% 全量采集 → 可能数千条日志 +DB 慢查询 → 100% 全量采集 → 重点关注 +健康检查 → 不采集 → 零成本 +``` + +> [!tip] 实现方式 +> 采样逻辑建议写在日志库的中间件层,而非业务代码里: +> ```go +> func SampleLogger(next logger.Logger, rate float64) logger.Logger { +> return &sampledLogger{ +> next: next, +> shouldSample: func() bool { +> return rand.Float64() < rate +> }, +> } +> } +> ``` + +## 关联笔记 + +- [[04-可观测性/01-Metrics监控]] — Metrics + Logging = 完整的问题定位能力 +- [[04-可观测性/03-链路追踪]] — trace_id 是日志与追踪的桥梁 +- [[04-可观测性/04-告警管理]] — 日志可以触发基于模式的告警 diff --git a/hhs/MS/04-可观测性/03-链路追踪.md b/hhs/MS/04-可观测性/03-链路追踪.md new file mode 100644 index 0000000..9aed60b --- /dev/null +++ b/hhs/MS/04-可观测性/03-链路追踪.md @@ -0,0 +1,166 @@ +--- +tags: [microservice, tracing, opentelemetry, context-propagation] +create time: 2026-05-05 +--- + +# 链路追踪 + +## 概述 + +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 +``` + +## Trace/Span 核心概念 + +| 概念 | 说明 | +|------|------| +| **Trace** | 一次请求的完整调用链,全局唯一 `trace_id` | +| **Span** | 调用链中的一个执行单元,有独立的 `span_id` | +| **Parent-Child** | Span 树状结构,子 Span 继承父 Span 的 trace_id | +| **Tags / Attributes** | 键值对标注(HTTP method、status code) | +| **Logs** | Span 级别的时间戳事件(如 "DB query started") | +| **Sampling** | 按比例采样,降低存储开销 | + +### OpenTelemetry W3C Trace Context 标准 + +```json +// HTTP Header +{ + "traceparent": "00-{trace_id}-{span_id}-01", + "tracestate": "vendor=value" +} +``` + +格式解析:`version(2字节) - trace_id(32字节) - span_id(16字节) - flags(2字节)` + +``` +示例: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01 + │ └─── trace_id ───┘ └─ span_id ─┘ └flags─┘ +``` + +## 上下文传播详解 + +`trace_id` 必须从上游传递到下游。不同通信场景有不同的传播方式: + +### HTTP 传播 + +```go +func Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 1. 提取入站 trace context + ctx := propagator.Extract(r.Context(), headerReader{r.Header}) + + // 2. 创建新的 Span + ctx, span := tracer.Start(ctx, "handleRequest") + defer span.End() + + // 3. 将 trace context 注入出站请求 + r = r.WithContext(ctx) + propagator.Inject(ctx, headerWriter{r.Header}) + + next.ServeHTTP(w, r) + }) +} +``` + +### MQ 消息头传播 + +```go +// 生产者 - 手动注入 trace context +msg.Properties.Set("trace_id", extractTraceID(ctx)) +msg.Properties.Set("span_id", extractSpanID(ctx)) + +// 消费者 - 恢复 trace context +traceID := msg.Properties.Get("trace_id") +span, _ := tracer.Start(traceContext, "consumeMessage", + trace.WithAttributes(attribute.String("mq.message.id", traceID))) +``` + +## 采样策略 + +```mermaid +flowchart TD + AllReq["全部请求"] --> Sampler{"采样决策"} + + Sampler -->|"100%"| Core["核心链路全量采集"] + Sampler -->|"5~10%"| Normal["普通路径随机采样"] + Sampler -->|"100%"| Error["错误链路优先保留"] + Sampler -->|"0%"| Health["健康检查/内部心跳"] + + Core --> Storage["Trace 存储"] + Normal --> Storage + Error --> Storage + Health --> Drop["丢弃"] +``` + +| 环境 | 采样率 | 说明 | +|------|--------|------| +| 开发 / 测试 | 100% | 方便调试,无存储压力 | +| 生产 - 核心链路 | 100% | 下单、支付等关键路径 | +| 生产 - 普通路径 | 5~10% | 平衡成本和覆盖率 | +| 生产 - 错误链路 | 100% | 出错时优先保留 | + +> [!tip] 基于错误的智能采样 +> +> 高级做法:**正常路径低采样,一旦检测到 HTTP 5xx 或 DB 超时,立即提升当前请求的采样率**。这样既省了存储,又能在出问题时有足够的数据回溯。 + +## 主流方案对比 + +| 方案 | 存储后端 | 侵入程度 | 中文支持 | 推荐理由 | +|------|---------|---------|---------|---------| +| **OpenTelemetry** | 任意 Jaeger/Prometheus/Loki | SDK + Auto-instrumentation | 良好 | CNCF 行业标准,未来趋势 | +| **Jaeger** | Cassandra / Elasticsearch | Agent / SDK | 社区支持 | Uber 开源,UI 优秀 | +| **SkyWalking** | MySQL / ES | 零侵入 Agent | ⭐ 完善 | 国产首选,开箱即用 | + +## 渐进式落地路线 + +> [!tip] 不要试图一步到位——先从 Tracing 开始,ROI 最高。 +> +> 1. **第一步**:接入 Tracing(OpenTelemetry),覆盖核心链路(下单、支付),rate=100% +> 2. **第二步**:加 Metrics(Prometheus + Grafana),建立基础监控面板 +> 3. **第三步**:集中 Logging(Loki / ELK),日志携带 trace_id 与 Tracing 关联 +> 4. **第四步**:全量上 OpenTelemetry Collector,统一管理三件套 + +## 性能影响评估 + +| 指标 | 预期影响 | +|------|---------| +| CPU 增加 | < 3%(异步批量上报) | +| 内存增加 | ~10MB(Span buffer) | +| 网络带宽 | 每条 Trace 约 2~5KB(取决于 Span 数量) | +| 延迟增加 | < 0.1ms(SDK 内处理不阻塞业务) | + +> [!note] 关键实践 +> - 使用 **BatchProcessor** 异步上报,不阻塞业务线程 +> - Span 不要嵌套过深,一个 HTTP 调用或 DB 查询对应一个 Span +> - 给 Span 设置丰富的 Attributes,方便查询和过滤 +> - 错误 Span 标记 `isError=true`,便于快速定位问题链路 + +## 关联笔记 + +- [[04-可观测性/01-Metrics监控]] — Tracing 与 Metrics 互补 +- [[04-可观测性/02-日志系统]] — trace_id 串联日志和追踪 +- [[04-可观测性/04-告警管理]] — 基于 Trace 数据的异常检测告警 diff --git a/hhs/MS/04-可观测性/04-告警管理.md b/hhs/MS/04-可观测性/04-告警管理.md new file mode 100644 index 0000000..356c7f4 --- /dev/null +++ b/hhs/MS/04-可观测性/04-告警管理.md @@ -0,0 +1,186 @@ +--- +tags: [microservice, alerting, slo, error-budget, on-call] +create time: 2026-05-05 +--- + +# 告警管理 + +## 概述 + +收集了这么多 Metrics、Logs 和 Traces,下一步是**用数据驱动决策**。告警系统是在问题影响用户之前及时响应的最后一道防线。 + +> [!warning] 核心挑战:告警疲劳 +> +> 如果一个团队每天收到 200 条告警,其中 195 条是误报或无需处理,工程师会对剩下的 5 条真正重要的告警产生"脱敏"。这就是 **告警疲劳 (Alert Fatigue)**。 + +## SLO / Error Budget 告警 + +### SLI / SLO / SLA 三层模型 + +| 层级 | 全称 | 含义 | 示例 | +|------|------|------|------| +| **SLI** | Service Level Indicator | 实际测量的指标 | P99 延迟 = 120ms | +| **SLO** | Service Level Objective | 内部目标值 | P99 延迟 < 200ms(99.9%) | +| **SLA** | Service Level Agreement | 对外契约承诺 | 可用性 ≥ 99.95%,否则赔偿 | + +```mermaid +flowchart LR + Real["真实用户请求"] --> SLI{成功率达标?} + SLI -- 是 --> SLO_OK["✅ SLO 达成"] + SLI -- 否 --> BUDGET["消耗错误预算"] + + BUDGET --> Left{"预算 > 0?"} + Left -- 是 --> Continue["继续发布新功能 🚀"] + Left -- 否 --> Freeze["冻结发布 ❄️
专注稳定性修复"] + + style SLO_OK fill:#e8f5e9 + style Freeze fill:#ffebee +``` + +### 错误预算计算 + +``` +每月允许停机时间 = 月总分钟数 × (1 - SLO) + +SLO = 99.9% → 30×24×60×0.1% = 43.2 分钟 +SLO = 99.95% → 30×24×60×0.05% = 21.6 分钟 +SLO = 99.99% → 30×24×60×0.01% = 4.3 分钟 +SLO = 99% → 30×24×60×1% = 432 分钟 ≈ 7.2 小时(几乎无意义) +``` + +### 基于错误预算的告警策略 + +```mermaid +flowchart TD + BudgetLeft{"错误预算剩余"} + + BudgetLeft -- "> 50%" --> Aggressive["激进策略:
保持常规告警阈值
加快迭代节奏"] + BudgetLeft -- "10~50%" --> Moderate["保守策略:
降低告警阈值
收紧发布频率"] + BudgetLeft -- "< 10%" --> Emergency["紧急策略:
所有非紧急告警暂停
全员关注稳定性"] + + style Aggressive fill:#e8f5e9 + style Moderate fill:#fff3e0 + style Emergency fill:#ffebee +``` + +## 告警设计原则 + +### 1. 告警必须 Actionable + +收到告警后知道该做什么,否则不要告。 + +每条告警规则都应该能回答以下问题: + +| # | 问题 | 示例答案 | +|---|------|---------| +| 1 | **什么问题?** | `支付服务 P99 延迟 > 2s 持续 5 分钟` | +| 2 | **谁负责?** | `支付组 On-Call: @zhangsan` | +| 3 | **怎么修复?** | 链接到 Runbook `📖 Runbook: 支付超时排查指南` | +| 4 | **多久升级?** | `P1 无人响应 → 5min 后升级至组长` | + +### 2. 分级设计 + +```mermaid +flowchart TD + subgraph L1["P0 — 立即响应(电话 + IM)"] + A1["HTTP 5xx 错误率 > 1% 持续 2min"] + A2["核心接口 P99 > 2s 持续 5min"] + A3["数据库连接池耗尽"] + end + + subgraph L2["P1 — 当天处理(IM 通知)"] + B1["单个服务错误率 > 5%"] + B2["下游依赖超时率升高"] + B3["磁盘使用 > 75%"] + end + + subgraph L3["P2 — 本周修复(日报汇总)"] + C1["CPU 使用率 > 80% 持续 1h"] + C2["内存使用 > 85% 持续 1h"] + C3["非核心服务 P99 异常"] + end + + L1 --> ONCALL[On-Call 值班群] + L2 --> OPS[运维监控群] + L3 --> DAILY["每日健康报告"] + + style L1 fill:#ffebee + style L2 fill:#fff3e0 + style L3 fill:#e3f2fd +``` + +### 3. 降噪与抑制 + +同一个根因可能触发连锁告警,需要抑制机制: + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ +│ DB 宕机 │───→│ 服务 A 报错│───→│ 关联告警 │ +└──────────┘ └──────────┘ │ 只发 Root Cause│ + │ 其他静默 │ + └──────────┘ +``` + +| 技术 | 说明 | +|------|------| +| **告警分组** | 同一根因的多个告警合并为一条 | +| **静默期** | Pod 重启后 5 分钟内不重复告警 | +| **维护窗口** | 已知变更期间暂时抑制非关键告警 | +| **继承抑制** | DB 挂了 → 自动抑制依赖 DB 的所有服务的告警 | + +## On-Call 最佳实践 + +### Runbook:告警处置手册 + +> [!summary] Runbook 模板 +> +> | 字段 | 内容 | +> |------|------| +> | **标题** | `支付服务 P99 延迟 > 2s 持续 5 分钟` | +> | **影响范围** | 下单接口超时,用户体验受损 | +> | **检查步骤** | ① 看 Grafana 延迟面板确认峰值时间点 → ② 查同期部署记录 → ③ 检查下游 DB 慢查询 | +> | **常见原因** | ① 新代码性能 regression → ② DB 连接池耗尽 → ③ 下游超时风暴 | +> | **快速恢复** | ① 回滚最近一次发布 → ② 扩容实例 → ③ 开启熔断降负载 | +> | **彻底解决** | 排查根本原因,补充回归测试,完善容量规划 | + +### Blameless Postmortem + +事故复盘不问"谁干的",问"流程哪里可以改进": + +```mermaid +flowchart LR + Incident["事故发生"] --> Contain["控制影响面"] + Contain --> Investigate["调查根因"] + Investigate --> Action["制定改进行动"] + Action --> Share["分享经验教训"] + + style Incident fill:#ffebee + style Contain fill:#fff3e0 + style Investigate fill:#e3f2fd + style Action fill:#e8f5e9 + style Share fill:#f3e5f5 +``` + +> [!tip] 事后复盘三问 +> +> 1. **什么导致了这次事故?** (技术根因) +> 2. **为什么监控系统没有更早发现?** (检测延迟) +> 3. **下次如何避免同类问题?** (流程改进) + +## 告警疲劳自检清单 + +如果你的团队出现以下情况,说明告警体系需要重构: + +- [ ] On-Call 工程师下班前都要关闭/忽略一堆告警 +- [ ] 有人会在群里说 "这个告警不用管" +- [ ] 同一个服务每天都触发相同的告警 +- [ ] 新同事不知道哪些告警是真的严重 +- [ ] PagerDuty/Oncall 通知已读率低于 50% + +如果以上超过 2 项 ✅ —— 你的告警需要认真治理了。 + +## 关联笔记 + +- [[04-可观测性/01-Metrics监控]] — Metrics 是告警的数据来源 +- [[05-部署运维/04-SRE实践]] — SLO/Error Budget 的详细方法论 +- [[02-服务治理/06-容错模式]] — 熔断器状态可以作为告警信号 diff --git a/hhs/MS/04-可观测性/README.md b/hhs/MS/04-可观测性/README.md new file mode 100644 index 0000000..92d9448 --- /dev/null +++ b/hhs/MS/04-可观测性/README.md @@ -0,0 +1,78 @@ +--- +tags: [microservice, observability] +create time: 2026-04-29 12:04 +--- + +# 可观测性 + +## 概述 + +微服务架构下,一个请求可能穿越十几个甚至上百个服务。**排查问题就像在大海捞针**。可观测性通过三大支柱(Metrics、Logging、Tracing)让系统行为变得"可见"。 + +## 知识体系 + +```mermaid +graph LR + A["Metrics 监控"] --> C["日志系统"] + B["链路追踪"] --> C + C --> D["告警管理"] + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#fce4ec + style D fill:#e8f5e9 +``` + +| # | 主题 | 核心问题 | +|---|------|----------| +| 1 | [[04-可观测性/01-Metrics监控]] | 怎么量化系统的健康度?RED/USE 方法怎么用? | +| 2 | [[04-可观测性/02-日志系统]] | 日志怎么集中收集?结构化日志的最佳实践是什么? | +| 3 | [[04-可观测性/03-链路追踪]] | trace_id 如何贯穿跨服务调用链?OpenTelemetry 怎么用? | +| 4 | [[04-可观测性/04-告警管理]] | 如何设计告警避免疲劳?SLO/Error Budget 怎么做? | + +### 为什么需要独立的"可观测性"体系? + +```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 +``` + +> [!keypoint] 可观测性与监控的本质区别 +> +> **监控系统**告诉你"出事了"(已知未知),**可观测性系统**让你去探究"为什么会出事"(未知未知)。 + +### 三大支柱 + +| 支柱 | 回答的问题 | 典型工具 | +|------|-----------|---------| +| **Metrics** | 系统现在健康吗?— 趋势、告警 | Prometheus + Grafana | +| **Logging** | 具体发生了什么?— 历史回溯 | ELK / Loki | +| **Tracing** | 请求在哪一步慢了/失败了?— 链路追踪 | Jaeger / Zipkin | + +### 关联笔记 + +- [[02-服务治理]] — 熔断器可以触发告警,与监控系统联动 +- [[05-部署运维/02-Kubernetes]] — K8s Liveness/Readiness Probe 是可观测性的基础 +- [[hzh/MS/README.md]] — 完整微服务知识索引 diff --git a/hhs/MS/05-部署运维/01-容器化.md b/hhs/MS/05-部署运维/01-容器化.md new file mode 100644 index 0000000..e8014d0 --- /dev/null +++ b/hhs/MS/05-部署运维/01-容器化.md @@ -0,0 +1,155 @@ +--- +tags: [microservice, docker, container, image-optimization] +create time: 2026-05-05 +--- + +# 容器化 + +## 概述 + +Docker 容器是微服务交付的标准单元。它解决了"在我机器上是好的"这个经典问题——**开发、测试、生产使用完全一致的运行时环境**。 + +```mermaid +graph TB + CODE["源代码"] --> BUILD["CI 构建镜像"] + BUILD --> REGISTRY["镜像仓库
Harbor / ECR / ACR"] + REGISTRY --> K8s["Kubernetes 部署"] + + style BUILD fill:#e3f2fd + style REGISTRY fill:#fff3e0 + style K8s fill:#e8f5e9 +``` + +## Dockerfile 最佳实践 + +### 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"] +``` + +### 关键优化点 + +| 优化项 | 方法 | 效果 | +|--------|------|------| +| **多阶段构建** | 编译和运行分离 | 镜像从 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)。如果排除不当,可能遗漏必要的配置文件。 + +## 镜像安全 + +```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 +``` + +### 安全检查清单 + +- [ ] 不使用 `latest` tag(永远用具体版本号) +- [ ] 不安装不必要的软件包(`apk del --purge .build-deps`) +- [ ] 定期更新基础镜像(修复 CVE) +- [ ] 使用非 root 用户运行 +- [ ] 镜像不包含密钥、密码、私钥 +- [ ] 使用最小基础镜像(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)。 + +## 镜像仓库管理 + +```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** | 公共免费 | 开源项目 | + +## 关联笔记 + +- [[05-部署运维/02-Kubernetes]] — K8s 以 Pod 为部署单元,镜像来自 Docker +- [[05-部署运维/03-CICD与GitOps]] — CI/CD 流水线中的镜像构建环节 diff --git a/hhs/MS/05-部署运维/02-Kubernetes.md b/hhs/MS/05-部署运维/02-Kubernetes.md new file mode 100644 index 0000000..4e1d4cb --- /dev/null +++ b/hhs/MS/05-部署运维/02-Kubernetes.md @@ -0,0 +1,266 @@ +--- +tags: [microservice, kubernetes, k8s, container-orchestration] +create time: 2026-05-05 +--- + +# Kubernetes + +## 概述 + +Kubernetes (K8s) 是微服务架构的事实标准编排引擎。它将容器化的服务组织成声明式的资源对象,自动处理部署、扩展、故障恢复。 + +```mermaid +graph TB + subgraph CLUSTER["K8s Cluster"] + Master["控制面
API Server / Scheduler / Controller Manager / etcd"] + + subgraph NODES["工作节点"] + N1["Node A
kubelet + kube-proxy"] + N2["Node B
kubelet + kube-proxy"] + end + + Master --> N1 + Master --> N2 + + subgraph APPS["应用层"] + Order["Order Service Deployment"] + Pay["Payment Service Deployment"] + end + + N1 --> Order + N2 --> Order + N1 --> Pay + N2 --> Pay + end + + SVC["Service (ClusterIP)"] -->|Load Balance| Order + Ingress["Ingress (HTTP Routing)"] --> SVC + + style Master fill:#e3f2fd + style N1 fill:#fff3e0 + style N2 fill:#fff3e0 +``` + +## 核心概念速查 + +| K8s 对象 | 用途 | 类比 | +|---------|------|------| +| **Pod** | 最小部署单元,包含一个或多个容器 | 应用实例 | +| **Deployment** | 管理 Pod 的副本数和滚动更新 | 应用的"模板" | +| **Service** | 稳定的网络入口,负载均衡 | 内部 VIP | +| **Ingress** | HTTP/HTTPS 路由规则 | 外部网关 | +| **ConfigMap** | 配置注入(明文) | 环境变量/配置文件 | +| **Secret** | 敏感配置注入(base64) | 密码/API Key | +| **HPA** | 根据指标自动扩缩容 | 弹性伸缩 | +| **StatefulSet** | 有状态应用的有序管理 | DB、ZK | +| **Job/CronJob** | 一次性任务 / 定时任务 | 批处理 | + +## 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: + 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] 经典陷阱: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 +``` + +调用方只需 `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 中的落地 diff --git a/hhs/MS/05-部署运维/03-CICD与GitOps.md b/hhs/MS/05-部署运维/03-CICD与GitOps.md new file mode 100644 index 0000000..7bedec1 --- /dev/null +++ b/hhs/MS/05-部署运维/03-CICD与GitOps.md @@ -0,0 +1,176 @@ +--- +tags: [microservice, cicd, gitops, github-actions, argocd] +create time: 2026-05-05 +--- + +# CI/CD 与 GitOps + +## 概述 + +微服务需要**独立部署**。上百个服务的手工发布是不可想象的——必须用自动化流水线保证每次变更都能安全、快速地推送到生产环境。 + +```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 +``` + +## CI/CD 设计原则 + +| 原则 | 说明 | +|------|------| +| **一次构建,多处部署** | 镜像不随环境重新编译,只改 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"] + end + + Git -.->|Webhook| Argo + + Argo -->|"检测到差异"| Diff{"状态一致?"} + Diff -- 否 --> Sync["自动同步到 K8s ✅"] + Diff -- 是 --> OK["已一致 ⏸️"] + + style Git fill:#e3f2fd + style Argo fill:#fff3e0 + style K8sState fill:#e8f5e9 +``` + +### 与传统 CI/CD 的区别 + +| 维度 | 传统 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 流程管控配置变更 + +## Helm — K8s 的包管理 + +当每个服务都有几十行 YAML 时,Helm 能大幅简化部署: + +```bash +# 创建 Chart 模板 +helm create order-service + +# 使用 values.yaml 参数化部署 +helm upgrade --install order-service ./charts/order-service \ + --set image.tag=v1.2.3 \ + --set replicas=3 \ + --namespace=production +``` + +> [!tip] 为什么需要 Helm? +> +> 没有 Helm 时,每个服务都要手动维护 Deployment、Service、Ingress、ConfigMap 等几十个 YAML 文件。Helm 允许你把通用模板抽出来,只在 values.yaml 里改差异化配置。 + +## 关联笔记 + +- [[05-部署运维/01-容器化]] — Docker 镜像构建是 CI/CD 的第一步 +- [[05-部署运维/02-Kubernetes]] — K8s 是部署的目标平台 +- [[05-部署运维/04-SRE实践]] — 错误预算影响发布策略 diff --git a/hhs/MS/05-部署运维/04-SRE实践.md b/hhs/MS/05-部署运维/04-SRE实践.md new file mode 100644 index 0000000..1f78689 --- /dev/null +++ b/hhs/MS/05-部署运维/04-SRE实践.md @@ -0,0 +1,160 @@ +--- +tags: [microservice, sre, slo, error-budget, postmortem] +create time: 2026-05-05 +--- + +# SRE 实践 + +## 概述 + +站点可靠性工程 (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 | 对外契约:达不到就赔钱 | 法务 + 商务 | 按合同约定 | + +### 实用性比例对照表 + +| 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 心法 + +1. **用户视角定义 SLO** — 不是"API P99 < 200ms",而是"用户在正常网络下加载页面 < 2s" +2. **错误预算用完 = 停止功能开发** — 全力修 bug、加稳定性 +3. **Blameless Postmortem** — 不问"谁干的",问"流程哪里可以改进" +4. **自动化一切重复劳动** — 手动操作一定会出错 +5. **接受一定程度的失败** — 在可控范围内快速迭代,比追求完美更重要 + +## 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 (行动 ②) | + +## 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** — 恢复服务的平均时间 + +## 关联笔记 + +- [[04-可观测性/04-告警管理]] — SLO/Error Budget 与告警体系的联动 +- [[05-部署运维/02-Kubernetes]] — K8s HPA 弹性伸缩支撑 SLO 保障 +- [[05-部署运维/03-CICD与GitOps]] — GitOps 支持安全的持续交付 diff --git a/hhs/MS/05-部署运维/README.md b/hhs/MS/05-部署运维/README.md new file mode 100644 index 0000000..4490150 --- /dev/null +++ b/hhs/MS/05-部署运维/README.md @@ -0,0 +1,42 @@ +--- +tags: [microservice, deployment, ops] +create time: 2026-04-29 12:05 +--- + +# 部署运维 + +## 概述 + +微服务架构下,服务数量从几个增长到几百个。**人工运维完全不可行**。本文涵盖容器化编排、K8s 资源管理、发布策略、弹性伸缩、CI/CD 流水线和 SRE 核心概念。 + +## 知识体系 + +```mermaid +graph LR + A["容器化"] --> B["Kubernetes"] + B --> C["CI/CD 与 GitOps"] + C --> D["SRE 实践"] + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#fce4ec + 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 如何落地? | + +### 学习建议 + +> [!tip] 学习路径 +> 先掌握容器化(Docker),再学 K8s 核心概念,CI/CD 和 SRE 可以在实践中逐步深入。 + +### 关联笔记 + +- [[02-服务治理]] — K8s Service 提供原生的服务发现和负载均衡 +- [[04-可观测性]] — K8s Liveness/Readiness Probe 是可观测性的基础 +- [[hzh/MS/README.md]] — 完整微服务知识索引 diff --git a/hhs/MS/06-gRPC/01-协议与架构.md b/hhs/MS/06-gRPC/01-协议与架构.md new file mode 100644 index 0000000..a4be0fa --- /dev/null +++ b/hhs/MS/06-gRPC/01-协议与架构.md @@ -0,0 +1,161 @@ +--- +tags: [grpc, http2, protocol-stack, multiplexing] +create time: 2026-05-07 16:00 +--- + +# gRPC 协议与架构 + +## 概述 + +本文深入讲解 **gRPC 的内部架构**和 **HTTP/2 协议栈层次**。理解这些底层机制后,才能解释「为什么 gRPC 比 HTTP/1.1 REST 更快」、「连接为什么不会泄漏」——不再停留在"听说性能好"的模糊认知层面。 + +> [!question] 先想一个问题 +> +> 微服务之间每秒可能产生数万到数十万次 RPC 调用。如果每次调用都新开一条 TCP 连接,操作系统会耗尽哪些资源? + +TCP 握手需要三次交互、端口有数量上限(单进程约 65535)、内核要为每个 socket 维护内存——当并发连接数达到万级时,CPU 花在建立和拆除连接上的时间甚至会超过处理业务的时间。这就是 gRPC **默认复用连接**的设计动机。 + +## gRPC 整体架构 + +```mermaid +graph TB + subgraph Client["客户端应用"] + CApp["业务代码"] + CStub["Generated Stub"] + CChan["gRPC Channel"] + CCall["Client Call"] + CApp --> CStub + CStub --> CChan + CChan --> CCall + end + + subgraph Network["网络层"] + H2["HTTP/2 Frame Layer"] + HP["HPACK Header Compression"] + LM["Load Balancing Picker"] + NR["Name Resolver"] + CCall --> LM + LM --> H2 + H2 --> HP + end + + subgraph Server["服务端应用"] + SChan["Server Listener"] + SH2["HTTP/2 Frame Layer"] + SHP["HPACK Header Compression"] + SH2 --> SHP + Handler["Registered Handler"] + SHandler["业务代码"] + SHP --> Handler + Handler --> SHandler + end + + CChan <-->|Binary Frames| SChan +``` + +### 关键组件说明 + +| 组件 | 职责 | +|------|------| +| **Generated Stub** | 从 .proto 文件编译生成的桩代码,封装了序列化 / 反序列化和网络通信细节 | +| **gRPC Channel** | 逻辑连接抽象,内部管理真实 TCP 连接的创建、复用和健康检查 | +| **HTTP/2 Frame Layer** | 二进制分帧层,将所有数据拆分为轻量级的 Frame 传输 | +| **HPACK** | 头部压缩算法,避免重复传输相同的 metadata 字段 | + +> [!tip] Go 实现细节 +> +> Go 中每个 gRPC Channel 底层维护一个 **transport 连接池**,由负载均衡器动态分配 SubConn。你可以显式设置 `WithBlock()` 超时来避免启动时的无限等待: +> +> ```go +> conn, err := grpc.DialContext(ctx, target, +> grpc.WithBlock(), +> grpc.WithTimeout(5*time.Second), +> ) +> ``` + +## HTTP/2 的关键特性 + +gRPC 不是一种新协议,而是 **Protocol Buffers + HTTP/2 的绑定规范**。HTTP/2 为 gRPC 提供了三个核心能力: + +| 特性 | HTTP/1.1 | HTTP/2 | gRPC 收益 | +|------|----------|--------|-----------| +| **多路复用** | 一个连接只能处理一个请求(除非 SPDY) | 一个 TCP 连接上并行的多个 Stream | 无需连接池,连接复用率极高 | +| **头部压缩** | 明文 Head,重复字段多 | HPACK 算法压缩 | 减小传输体积,降低延迟 | +| **二进制分帧** | 文本协议,解析慢 | 二进制 Frame,解析快 | 双方不需要手写解析逻辑 | + +### 多路复用演示 + +```mermaid +sequenceDiagram + participant C as Client + participant H2 as HTTP/2 Connection + participant S as Server + + Note over C,S: 一个 TCP 连接,四个并发 Stream + C->>H2: Stream 1: GetOrder(id=1) + S->>H2: Stream 1: Order{...} + + C->>H2: Stream 2: GetUser(id=5) + S->>H2: Stream 2: User{...} + + C->>H2: Stream 3: CreateItem(...) + S->>H2: Stream 3: Item{id: "new"} +``` + +> [!keypoint] 关键洞察 +> +> HTTP/1.1 开 10 个并行请求需要 10 条 TCP 连接 → 握手开销大、端口耗尽。**一条 HTTP/2 连接就能承载几百个并发 RPC**,这就是 gRPC 在高频内部调用的性能优势来源。 + +## 协议栈层次 + +```mermaid +graph LR + App["Application
Business Logic"] --> Stub["Generated Stub"] + Stub --> GRPC["gRPC Framework"] + GRPC --> H2["HTTP/2 Protocol"] + H2 --> TCP["TCP/IP"] + + style App fill:#e3f2fd + style Stub fill:#fff3e0 + style GRPC fill:#c8e6c9 + style H2 fill:#fce4ec + style TCP fill:#f3e5f5 +``` + +每一层解决不同的问题: + +| 层级 | 解决的问题 | 类比 | +|------|-----------|------| +| Application | 你写什么业务逻辑 | 写信的内容 | +| Generated Stub | 把业务对象映射为二进制编码 | 翻译官(Proto 定义 = 字典) | +| gRPC Framework | 负责重试、拦截器、流控 | 邮局分拣系统 | +| HTTP/2 | 多路复用 + 头部压缩 + 二进制帧 | 快递包裹的分装规范 | +| TCP/IP | 可靠传输 + 路由寻址 | 公路运输网络 | + +## 为什么不只是"更快的 HTTP" + +许多开发者误以为 gRPC 的优势只是"用了更快的序列化"。实际上真正的分水岭在于: + +> [!summary] gRPC vs HTTP API 的本质差异 +> +> | 维度 | HTTP API(REST) | gRPC | +> |------|-----------------|------| +> | **契约先行** | 接口文档滞后于代码 | `.proto` 是单一事实来源 | +> | **类型安全** | JSON 无类型,运行时才暴露 bug | 编译期捕获字段缺失、类型错误 | +> | **代码即 SDK** | 客户端需要手动拼装 HTTP 请求 | 自动生成全语言客户端 Stub | +> | **连接复用** | 需要自行管理连接池 | 框架内置连接复用和负载均衡 | +> | **流式能力** | WebSocket 需额外建通道 | 原生支持双向流,强类型契约 | + +> [!question] 带着问题继续读 +> +> 既然 gRPC 这么多优势,是不是所有场景都应该用 gRPC?什么情况下 HTTP/JSON 仍然更合适? + +答案见 [[02-服务治理/05-服务间通信]]。对外暴露 API 时,HTTP/JSON 仍然不可替代——因为浏览器的 Native Fetch 无法直接调用 gRPC,第三方接入者也不想安装 Proto 编译器。 + +## 关联笔记 + +- [[02-服务治理/05-服务间通信]] — gRPC 与 REST 的基础对比及选型建议 +- [[02-服务治理/06-容错模式]] — 基于此架构的重试、熔断等治理机制 +- [[02-Proto设计]] — Proto 文件设计的进阶实践 +- [[03-RPC模式]] — 四种 RPC 模式的深度用法 +- [[04-拦截器]] — 切面编程和上下文传播 diff --git a/hhs/MS/06-gRPC/02-Proto设计.md b/hhs/MS/06-gRPC/02-Proto设计.md new file mode 100644 index 0000000..924127d --- /dev/null +++ b/hhs/MS/06-gRPC/02-Proto设计.md @@ -0,0 +1,282 @@ +--- +tags: [grpc, protobuf, schema-versioning, proto3, client-server, stub-generation] +create time: 2026-05-07 16:00 +--- + +# Proto 设计规范 + +## 概述 + +本文覆盖 **Proto3 进阶特性**和**版本管理策略**。如果说架构篇是"看懂 gRPC 长什么样",这篇就是教你"如何写出经得起演进的 Proto 文件"——这是工程中最容易踩坑、却最容易被忽视的部分。 + +> [!question] 思考 +> +> 你的 Service A 调用 Service B 的 `GetUser`,Proto 里定义了 20 个字段。半年后你想加第 21 个字段,但旧版客户端没有编译更新。会发生什么? + +## Proto 是什么 + +Protocol Buffers(简称 **Proto**)是 Google 开源的一套**语言无关、平台无关的结构化数据序列化方案**。它的核心作用有两层: + +| 层面 | 说明 | +|------|------| +| **接口契约** | 用 `.proto` 文件声明 Service(有哪些 RPC 方法)、Request / Response(传什么数据),作为服务间通信的"宪法" | +| **序列化格式** | 把内存中的对象编码成二进制字节流,跨进程 / 跨网络传输后再还原回对象 | + +一句话总结:**Proto = IDL(接口定义语言)+ 二进制序列化**。 + +### 为什么两端都需要编译? + +很多初学者会疑惑:服务端实现逻辑、客户端发起调用,两边的代码不是各写各的吗?——**不是的,.proto 文件是两端共享的唯一真相源**。 + +```mermaid +graph LR + Proto[".proto 文件
唯一真相源"] --> protoc["protoc / Buf"] + protoc --> Server["服务端生成代码
(Go stub)"] + protoc --> Client["客户端生成代码
(Java / TS / …)"] + Server --> Encode["编码 Request → 发送二进制流"] + Client --> Decode["接收二进制流 → 反序列化 Response"] + Encode --- Decode +``` + +| 角色 | 编译产物 | 用途 | +|------|---------|------| +| **服务端** | Server Stub | 接收二进制消息 → 反序列化为参数对象 → 执行业务逻辑 → 返回结果对象 → 编码为响应 | +| **客户端** | Client Stub | 用户调用 Stub 方法 → 构造请求对象 → 编码为二进制字节流发送 → 收到响应后解码 | + +> [!question] 思考 +> +> 如果服务端用 Go 生成代码、客户端用 Java 生成代码,它们之间的二进制消息能正确互操作吗? +> +> > [!answer]- 答案 +> > **完全可以。** Proto 的二进制编码规则是语言无关的规范,只依赖字段编号(`=` 后面的数字)。只要两边用的是同一份 `.proto`,任何语言的编译器都会按照相同的规则编解码。 + +#### 实际编译示意 + +**.proto 定义(两端共用同一文件)** + +```proto +// proto/order/v1/order.proto +syntax = "proto3"; +package order.v1; + +service OrderService { + rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse); +} + +message CreateOrderRequest { + string user_id = 1; + repeated string item_ids = 2; +} +``` + +**服务端(Go):** `buf generate` → 生成 `order_service.pb.go` + `order.pb.go` + +```go +// 服务端实现生成的 RPC 接口 +func (s *server) CreateOrder(ctx context.Context, req *orderpb.CreateOrderRequest) (*orderpb.CreateOrderResponse, error) { + // 直接使用反序列化好的 Request 对象 ✅ + fmt.Println(req.UserId) // 强类型访问 + return &orderpb.CreateOrderResponse{OrderId: "ord-123"}, nil +} +``` + +**客户端(TypeScript):** `buf generate` → 生成 `order_pb.ts` + +```typescript +// 客户端直接调用生成的 Stub 方法 +const response = await orderClient.createOrder({ userId: 'u-456', itemIds: ['i-7', 'i-8'] }) +console.log(response.orderId) // 直接得到结果 ✅ +``` + +可以看到:两端都从**同一个 `.proto`** 生成了各自语言的代码,但编写的代码量极小——核心工作是写 `.proto` 和手写业务逻辑。 + +### 为什么不直接用 JSON / XML + +| 维度 | Proto | JSON | XML | +|------|-------|------|-----| +| 体积 | 紧凑,无标签名冗余 | 冗余大(每个值都带 key 名) | 标签开销更大 | +| 性能 | 编解码速度极快(变长整数 + 字段编号编码) | 需解析字符串 | 解析最慢 | +| 类型安全 | 强类型,编译器检查 | 动态类型,运行时才暴露错误 | 动态类型 | +| 向后兼容 | 天然支持字段增删 | 无约定,全靠文档 | 部分支持 | +| 工具链 | `protoc` 生成各语言 Stub | — | — | + +好消息是:Protocol Buffers 的二进制编码规则天然保证了向前兼容。坏消息是:**只有遵守规则的改动才是兼容的**,违反规则的静默破坏会让你排查整整一天。 + +## Oneof —— 互斥字段 + +当某个消息体可能有多种不同类型的值,但同一时刻只出现一种时,使用 `oneof`: + +```go +// proto/order/v1/order.proto +message Payment { + string order_id = 1; + + oneof method { + AlipayPayment alipay = 2; + WechatPayment wechat = 3; + CreditCard card = 4; + } +} + +message AlipayPayment { + string token = 1; +} + +message WechatPayment { + string openid = 1; +} + +message CreditCard { + string last_four = 1; + string exp_date = 2; +} +``` + +> [!note] Proto3 oneof 的限制 +> +> - 不能加 `repeated` 修饰符 +> - 空 oneof 值为 `NONE(0)`,反序列化时各字段都为默认值 +> - **向后兼容策略**:新增 oneof 变体是安全的;删除变体会破坏旧客户端 + +## Well-Known Types —— 内置类型库 + +Proto3 内置了一些常用类型,直接引用即可,不用自己定义: + +```proto +import "google/protobuf/timestamp.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/wrappers.proto"; + +message Event { + string name = 1; + google.protobuf.Timestamp created_at = 2; // 替代手动时间字符串 + google.protobuf.Empty body = 3; // 空消息占位 + google.protobuf.StringValue desc = 4; // 可选字符串(区分 unset 和空串) +} +``` + +| 类型 | 用途 | +|------|------| +| `Timestamp` | RFC 3339 纳秒级时间戳 | +| `Duration` | 带单位的时长 | +| `Empty` | 无参数 / 无返回值的 RPC 占位 | +| `StringValue / Int32Value / BoolValue` | 包装基本类型,表示"可选"语义 | +| `Struct / Value / ListValue` | 类 JSON 的动态结构 | +| `Any` | 泛型消息容器(需搭配 type URL) | + +> [!tip] StringValue 的妙用 +> +> Proto3 默认把未设置的字段归入"默认值",导致无法区分"用户传了空字符串"和"用户没传这个字段"。用 `StringValue` 包装后,Proto3 可以精确表达 Optional 语义。 + +## Map 字段 + +```proto +message Config { + map labels = 1; + map price_cache = 2; +} +``` + +- **有序遍历**:Proto3 的 map 按 key 排序遍历 +- **兼容性**:删除整个 map 字段可接受,删除其中某个 key-value 对**不安全**(会被当作未知字段忽略) + +## 版本管理与向前兼容规则 + +这是工程中最容易踩坑的部分: + +```mermaid +graph LR + Rule["向前兼容三大铁律"] --> R1["新增字段 → 旧客户端忽略"] + Rule --> R2["删除字段 → 新客户端忽略"] + Rule --> R3["字段编号永不重用"] + + R1 -.-> F1["新字段标记 optional"] + R2 -.-> D1["标记 deprecated 而非删除"] + R3 -.-> N1["预留号段:1~19 用于 Google, 20~10000 自定"] +``` + +### 兼容性决策表 + +| 操作 | 是否安全 | 说明 | +|------|---------|------| +| 新增字段(用更大编号) | ✅ 安全 | 旧版客户端忽略未知编号字段 | +| 删除字段 | ⚠️ 部分安全 | 建议标记 `deprecated`,保留编号 | +| 修改字段类型 | ❌ 危险 | 可能导致二进制解析失败 | +| 重编字段编号 | ❌ 致命 | 读写两边理解错位 | +| 修改字段名 | ✅ 安全 | 名称不影响二进制编码 | +| 修改 enum 值 | ⚠️ 部分安全 | 新增安全,删除需用 reserved | + +### 常见错误示范 + +```proto +// ❌ 错误做法:删掉 field 3 后把 field 4 改成 = 3 +message BadExample { + string field1 = 1; + string field2 = 2; + // string removed_field = 3; ← 直接注释掉了 + string new_field = 3; // ← 重编了!危险! +} + +// ✅ 正确做法:保留编号并标记 reserved +message GoodExample { + string field1 = 1; + string field2 = 2; + reserved 3; // 锁定已删除字段的编号 + string new_field = 4; // 用新编号 +} +``` + +### reserved 的完整写法 + +```proto +message OldMessage { + reserved 2, 15; // 单个编号 + reserved 9, 10, 14; // 连续范围可用语法 reserved 9 to 14; + reserved "name", "email"; // 已废弃的字段名 +} +``` + +> [!warning] reserved 不是可选项 +> +> 当你要删除一个字段或其编号时,必须加上 `reserved` 声明。否则未来有人新增字段时重新使用了这个编号,就会造成静默的数据损坏——旧客户端读到新字段数据当成旧字段解析。 + +## Proto 组织规范 + +推荐的目录结构: + +``` +proto/ +├── buf.gen.yaml # Buf 代码生成配置 +├── api/ +│ └── v1/ +│ ├── order/ +│ │ ├── order.proto # Service + Message 定义 +│ │ └── error.proto # 统一错误码定义 +│ ├── user/ +│ │ └── user.proto +│ └── common/ +│ └── page.proto # 分页等通用定义 +└── gen/go/github.com/example/api/ # 自动生成代码 +``` + +### 命名约定 + +| 元素 | 命名风格 | 示例 | +|------|---------|------| +| package | 小写,点分隔,含版本前缀 | `order.v1` | +| service | PascalCase + Service 后缀 | `OrderService` | +| rpc method | PascalCase | `CreateOrder`, `GetUserInfo` | +| message | PascalCase | `CreateOrderRequest` | +| enum | PascalCase + Status/Type 后缀 | `OrderStatus`, `RoleType` | +| field | snake_case | `user_id`, `created_at` | + +> [!tip] 推荐的工具链 +> +> 建议使用 **[Buf](https://buf.build)** 替代 protoc 直调。Buf 提供统一的依赖管理、linting 和 CI 集成,且屏蔽了 protoc 在不同语言间的命令差异。 + +## 关联笔记 + +- [[01-协议与架构]] — Proto 文件最终服务于协议栈中的 Generated Stub 层 +- [[07-最佳实践]] — Buf 代码生成流程和生产配置 +- [[02-服务治理/05-服务间通信]] — 不同序列化方案的性能对比 diff --git a/hhs/MS/06-gRPC/03-RPC模式.md b/hhs/MS/06-gRPC/03-RPC模式.md new file mode 100644 index 0000000..e8c4ba5 --- /dev/null +++ b/hhs/MS/06-gRPC/03-RPC模式.md @@ -0,0 +1,197 @@ +--- +tags: [grpc, rpc-patterns, streaming, bidirectional] +create time: 2026-05-07 16:00 +--- + +# RPC 模式详解 + +## 概述 + +gRPC 支持 **四种 RPC 调用模式**,每种对应不同的客户端/服务端消息交互时序。理解它们不仅仅是记住名词,更要掌握「什么时候该用哪种」以及「各种模式的坑在哪里」。 + +> [!question] 先看一个实际场景 +> +> 你需要实现一个订单列表功能:用户在前端点击按钮 → 后端查询数据库 → 一次返回 100 条订单记录。 +> +> 你会选择哪种 RPC 模式?如果用 Unary 一行行发回来会怎样? + +Unary 模式下这 100 条记录会变成 100 次独立的 HTTP/2 Stream,虽然 HTTP/2 支持多路复用,但这依然浪费了宝贵的 Stream 标识符空间。更好的方式是用 **Server Streaming** 在一个 Stream 内批量发送。 + +## Unary —— 普通请求/响应 + +最常见的调用方式,一问一答: + +```go +// client side +resp, err := client.GetOrder(ctx, &pb.GetOrderRequest{Id: "ORD-001"}) +if err != nil { + // handle error +} + +// server side +func (s *orderServer) GetOrder(ctx context.Context, req *pb.GetOrderRequest) (*pb.Order, error) { + return s.store.Get(req.GetId()) +} +``` + +适用场景:CRUD 常规操作、单次计算任务、短平快的查询。这是占 gRPC 流量 **90%+** 的模式。 + +## Server Streaming —— 服务端流式 + +服务端一次性返回多个结果,适合批量查询或推送: + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + C->>S: ListOrders(request) + S-->>C: Order[1] + S-->>C: Order[2] + S-->>C: Order[3] + S-->>C: EOF + + Note right of C: 客户端收到完整列表 +``` + +```go +func (s *orderServer) ListOrders(req *pb.ListOrdersRequest, stream pb.OrderService_ListOrdersServer) error { + orders := s.store.ListAll() + for _, o := range orders { + if err := stream.Send(o); err != nil { + return err + } + } + return nil +} +``` + +> [!warning] 服务端必须主动关闭流 +> +> 如果服务端 `Send` 循环中没有遇到错误或主动返回 `nil`,Stream 永远不会结束,客户端将永远阻塞在 `Recv()`。务必确保所有路径都会退出循环或返回错误。 + +## Client Streaming —— 客户端流式 + +客户端连续发送多条消息,服务端最后返回聚合结果。适合大数据上传: + +```mermaid +sequenceDiagram + participant C as Client + participant S as Server + + C->>S: BatchRecord[1] + C->>S: BatchRecord[2] + C->>S: BatchRecord[3] + C->>S: CloseSend() + + Note right of S: 收集所有数据 + S-->>C: BatchResult{count: 3} +``` + +```go +func (s *orderServer) UploadLogs(stream pb.OrderService_UploadLogsServer) error { + var count int32 + for { + log, err := stream.Recv() + if err == io.EOF { + break + } + if err != nil { + return err + } + s.store.Log(log.Message) + count++ + } + return stream.SendAndClose(&pb.UploadResult{Count: count}) +} +``` + +适用场景:批量写入日志、文件分块上传、批量创建记录。注意要在客户端控制流速,避免一次性塞爆缓冲区。 + +## Bidi Streaming —— 双向流式 + +双方各自独立发送流,完全异步。**最适合实时场景**(聊天、行情推送、协同编辑): + +```mermaid +sequenceDiagram + participant CS as Client Stream + participant SS as Server Stream + + CS->>SS: Msg[1]: "Hello" + SS->>CS: Reply[1]: "Hi!" + CS->>SS: Msg[2]: "What's up?" + SS->>CS: Reply[2]: "Not much" + CS->>SS: Msg[3]... + SS->>CS: Reply[3]... + CS--)SS: Cancel + SS--)CS: Cancel +``` + +```go +// 实战示例:WebSocket-like 的实时通知推送 +func (s *server) SubscribeNotifications( + req *pb.SubscribeRequest, + stream pb.NotificationService_SubscribeServer, +) error { + // 注册订阅 + notifier.Register(stream.Context().Done(), func() { + stream.Send(&pb.Notification{}) + }) + <-stream.Context().Done() + return stream.Context().Err() +} +``` + +> [!question] 选型思考 +> +> 假设你要实现一个实时订单状态推送功能(前端监听订单从"待支付"→"已发货"→"已完成"的变化),你会选哪种流式模式?为什么不用 WebSocket? + +gRPC 双向流是**强类型 + 全双工**的替代方案,不需要额外建 WebSocket 通道,Proto 定义即契约,代码自动生成即客户端 SDK。对于纯内部服务链路,gRPC 双向流通常优于 WebSocket。 + +### Bidi Streaming 的生命周期管理 + +```mermaid +flowchart TD + Start["建立双向连接"] --> Idle["空闲等待"] + Idle --> SendMsg["客户端 send"] + Idle --> SendReply["服务端 send"] + SendMsg --> Idle + SendReply --> Idle + SendMsg --> ClientCancel{"client cancel?"} + SendReply --> ServerCancel{"server cancel?"} + ClientCancel -->|yes| Cleanup["释放资源"] + ServerCancel -->|yes| Cleanup + Cleanup --> End["断开连接"] + + style Start fill:#e3f2fd + style End fill:#ffcdd2 + style Cleanup fill:#fff3e0 +``` + +> [!keypoint] 黄金法则 +> +> 双向流的**任何一方都可以随时单方面关闭发送端**(`CloseSend` 或 `context.Done`)。另一方应检测到 EOF 并及时清理资源,而不是无限等待。 + +## 四种模式速查 + +| 模式 | 客户端消息数 | 服务端消息数 | 典型场景 | +|------|------------|------------|---------| +| Unary | 1 | 1 | CRUD 常规操作 | +| Server Stream | 1 | N | 列表查询、日志流拉取 | +| Client Stream | N | 1 | 批量写入、大文件分块上传 | +| Bidi Stream | N | M | 聊天室、实时协作、行情推送 | + +> [!note] 组合使用 +> +> 同一个 Service 中可以混合使用四种模式。比如 `OrderService` 里: +> - `CreateOrder` 用 Unary(创建后立即返回结果) +> - `ListOrders` 用 Server Stream(大量数据分批返回) +> - `ImportRecords` 用 Client Stream(批量导入) +> - `NotifyOrderStatus` 用 Bidi Stream(实时状态变更推送) + +## 关联笔记 + +- [[01-协议与架构]] — 流式建立在 HTTP/2 Stream 的多路复用之上 +- [[04-拦截器]] — 流式调用同样可以通过 StreamInterceptor 进行切面处理 +- [[05-错误处理]] — 流式调用的错误处理与普通模式有差异 +- [[06-连接管理]] — 长时间存活的流式连接需要 Keepalive 保活 diff --git a/hhs/MS/06-gRPC/04-拦截器.md b/hhs/MS/06-gRPC/04-拦截器.md new file mode 100644 index 0000000..0bb67b2 --- /dev/null +++ b/hhs/MS/06-gRPC/04-拦截器.md @@ -0,0 +1,176 @@ +--- +tags: [grpc, interceptor, middleware, context-propagation, metadata] +create time: 2026-05-07 16:00 +--- + +# Interceptor 与上下文传播 + +## 概述 + +Interceptor 是 gRPC 的切面编程能力,相当于 Web 框架中的 Middleware。每个 RPC 调用都会经过 **Unary Interceptor** 或 **Stream Interceptor** 链——无论它是普通的一问一答还是长时间的双向流。 + +本文涵盖拦截器的编写模式、链式串联技巧,以及最重要的**上下文传播**机制。 + +## Interceptor 链架构 + +```mermaid +graph LR + subgraph ClientChain["客户端拦截器链"] + CAuth["认证拦截"] --> CMetric["指标采集"] --> CRetry["重试策略"] --> CHand["RPC 调用"] + end + + subgraph ServerChain["服务端拦截器链"] + SHand["RPC Handler"] --> SMetric["指标采集"] --> SLog["日志记录"] --> SAuth["鉴权拦截"] + end + + CHand ==>|HTTP/2| SHand + + style CAuth fill:#fce4ec + style SAuth fill:#e3f2fd +``` + +### 典型执行顺序 + +| 位置 | 拦截器 | 职责 | +|------|--------|------| +| 客户端 | Metrics | 记录请求耗时、成功/失败计数 | +| 客户端 | Retry | 根据错误码决定是否自动重试 | +| 客户端 | Auth | 注入 Token / mTLS 证书 | +| 服务端 | Auth | 验证 Token 有效性 | +| 服务端 | Logging | 记录完整的请求/响应元信息 | +| 服务端 | Metrics | 统计服务端处理时间 | +| 服务端 | Handler | 实际业务逻辑 | + +> [!warning] Go 中拦截器注册的陷阱 +> +> Go 的 `grpc.WithUnaryInterceptor` 会**覆盖**而非追加。如果注册多次,只有最后一次生效。所以要用一个统一的包装函数串联所有逻辑: + +## 链式包装函数 + +```go +func chainUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) grpc.UnaryServerInterceptor { + n := len(interceptors) + return func(ctx context.Context, req any, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + ch := handler + for i := n - 1; i >= 0; i-- { + ih := ch + ch = func(c context.Context, r any) (any, error) { + return interceptors[i](c, r, info, ih) + } + } + return ch(ctx, req) + } +} +``` + +调用方只需传入所有拦截器即可: + +```go +allInterceptors := []grpc.UnaryServerInterceptor{ + TraceInterceptor(), + AuthInterceptor(), + LoggingInterceptor(), + MetricsInterceptor(), +} + +server := grpc.NewServer( + grpc.UnaryInterceptor(chainUnaryInterceptors(allInterceptors...)), + grpc.StreamInterceptor(chainStreamInterceptors(...)), +) +``` + +> [!tip] 其他语言的差异 +> +> - **Go**:需要通过上述手动链式包装,因为 `NewServer` 只接受一个拦截器 +> - **Java**:`Server.intercept()` 支持注册多个拦截器,按注册顺序依次执行 +> - **Node.js**:通过插件体系 `Server.addServiceDefinition` 间接实现 + +## 实战:统一鉴权拦截器 + +```go +// auth interceptor +func AuthInterceptor() grpc.UnaryServerInterceptor { + return func(ctx context.Context, req any, + info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (any, error) { + + // 白名单路径跳过鉴权 + skipPaths := []string{"/health.Health/Check", "/grpc.reflection.v1.ServerReflection/ServerReflectionInfo"} + for _, p := range skipPaths { + if info.FullMethod == p { + return handler(ctx, req) + } + } + + // 从 metadata 中提取 Token + md, ok := metadata.FromIncomingContext(ctx) + if !ok { + return nil, status.Error(codes.Unauthenticated, "missing metadata") + } + + tokens := md.Get("authorization") + if len(tokens) == 0 || !validateToken(tokens[0]) { + return nil, status.Error(codes.Unauthenticated, "invalid token") + } + + // 将用户信息注入 ctx(传递给下游 handler) + userCtx := context.WithValue(ctx, "userID", extractUserID(tokens[0])) + return handler(userCtx, req) + } +} +``` + +## 上下文传播 (Context Propagation) + +gRPC 天然支持通过 `metadata` 传递自定义元数据——这是实现分布式追踪、链路溯源的核心机制。 + +### 手动传播 + +```go +// 服务端注入 trace ID +ctx = metadata.AppendToOutgoingContext(ctx, + "x-trace-id", traceID, + "x-user-id", userID, +) + +// 客户端接收 +md, ok := metadata.FromOutgoingContext(ctx) +traceID := md.Get("x-trace-id") +``` + +### OpenTelemetry 自动传播 + +手动维护 metadata 繁琐且易遗漏。使用 `otelgrpc` 中间件可以自动注入 W3C Trace Context: + +```go +import "go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc" + +server := grpc.NewServer( + grpc.StatsHandler(otelgrpc.NewServerStatsHandler()), + // otelgrpc 自动在 metadata 中注入 traceparent / tracestate +) + +conn, _ := grpc.Dial(target, + grpc.WithStatsHandler(otelgrpc.NewClientStatsHandler()), +) +``` + +> [!keypoint] 为什么需要上下文传播 +> +> 当一个请求穿越 5 个微服务时,如果没有上下文传播,你在日志系统中看到的是 5 条孤立的记录。有了 W3C Trace Context,每层服务自动将 `traceparent` 透传给下一跳——最终汇聚成一条完整的调用链路,这就是 [[02-服务治理/03-分布式追踪]] 的核心能力。 + +## 拦截器最佳实践 + +> [!summary] 五条黄金法则 +> +> 1. **不要吞掉错误**——拦截器应该记录错误再转发给下一个,而不是独自决定忽略 +> 2. **不要在拦截器里做重型计算**——它处于热路径,每条 RPC 都要经过 +> 3. **设置合理的超时**——拦截器里的 `context.WithTimeout` 不应短于原始请求的 deadline +> 4. **白名单排除健康检查**——`/health.Health/Check` 和 reflection 服务不需要鉴权和日志 +> 5. **用 `info.FullMethod` 做条件判断**——格式为 `/package.Service/Method`,如 `/order.v1.OrderService/CreateOrder` + +## 关联笔记 + +- [[01-协议与架构]] — 拦截器位于 gRPC Framework 层,在 HTTP/2 之前处理请求 +- [[05-错误处理]] — 拦截器经常需要根据错误码决定重试或降级策略 +- [[02-服务治理/03-分布式追踪]] — Context 传播是实现分布式追踪的前提 +- [[02-服务治理/02-安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式 diff --git a/hhs/MS/06-gRPC/05-错误处理.md b/hhs/MS/06-gRPC/05-错误处理.md new file mode 100644 index 0000000..394dd21 --- /dev/null +++ b/hhs/MS/06-gRPC/05-错误处理.md @@ -0,0 +1,178 @@ +--- +tags: [grpc, error-handling, status-codes, graceful-degradation] +create time: 2026-05-07 16:00 +--- + +# 错误处理与状态码规范 + +## 概述 + +良好的错误处理决定了微服务系统的**可观测性**和**恢复速度**。gRPC 设计了 16 个标准状态码,每个都有明确的语义边界。正确使用它们,可以让调用方精准判断该重试、该降级、还是直接报错。 + +> [!question] 先看一个反面教材 +> +> 以下两个服务的错误处理方式,哪个更好? +> +> **服务 A**:所有异常一律返回 `InternalError: something went wrong` +> +> **服务 B**:参数校验失败返回 `InvalidArgument`,资源不存在返回 `NotFound`,数据库超时返回 `Unavailable` + +答案是显而易见的。但现实工程中,服务 A 的比例远高于服务 B——原因是很多开发者不了解状态码的准确含义,或者嫌麻烦直接 return nil, fmt.Errorf(...)。 + +## gRPC 状态码一览 + +gRPC 定义了 **16 个标准状态码**,与 HTTP 状态码不完全映射,有自己的语义体系: + +```mermaid +graph TB + OK["OK - 成功"] + + subgraph "客户端错误 4xx 对应" + CANCELLED["CANCELLED - 客户端取消"] + UNKNOWN["UNKNOWN - 未知错误"] + INVALID_ARG["INVALID_ARGUMENT - 参数无效"] + DEADLINE_EX["DEADLINE_EXCEEDED - 超时"] + NOT_FOUND["NOT_FOUND - 资源不存在"] + ALREADY_EXIST["ALREADY_EXISTS - 重复创建"] + PERMISSION_DENIED["PERMISSION_DENIED - 权限不足"] + UNAUTH["UNAUTHENTICATED - 未认证"] + end + + subgraph "服务端错误 5xx 对应" + RESOURCE_EXP["RESOURCE_EXHAUSTED - 资源耗尽"] + UNAVAIL["UNAVAILABLE - 服务不可用"] + DATA_LOSS["DATA_LOSS - 数据损坏"] + end + + subgraph "未实现" + UNIMP["UNIMPLEMENTED - 方法未实现"] + INTERNAL_ERR["INTERNAL - 内部错误"] + UNIMP ~~~ UNAVAIL + INTERNAL_ERR ~~~ DATA_LOSS + UNIMP --- UNAVAIL + end +``` + +### 状态码速查表 + +| 业务含义 | 推荐状态码 | HTTP 等价 | +|---------|-----------|----------| +| 参数校验失败 | `InvalidArgument` | 400 | +| 资源不存在 | `NotFound` | 404 | +| 重复创建 | `AlreadyExists` | 409 | +| 权限不足 | `PermissionDenied` | 403 | +| 未认证 | `Unauthenticated` | 401 | +| 超时 | `DeadlineExceeded` | 504 | +| 服务挂了/连接断开 | `Unavailable` | 503 | +| 限流 | `ResourceExhausted` | 429 | +| 方法未定义 | `Unimplemented` | 501 | +| 代码 bug | `Internal` | 500 | + +## 正确的错误处理方式 + +```go +// ❌ 错误示范:用普通 error 包装,丢失 gRPC 语义 +return nil, fmt.Errorf("failed to get order: %w", db.ErrNotFound) + +// ✅ 正确示范:使用 status.Error 保持 gRPC 协议一致性 +if errors.Is(err, db.ErrNotFound) { + return nil, status.Errorf(codes.NotFound, "order %s not found", id) +} + +// ✅ 更精细:带上详情(status details)——客户端可以程序化解析 +detail := &errdetails.BadRequest{ + FieldViolations: []*errdetails.BadRequest_FieldViolation{{ + Field: "user_id", + Description: "must be a valid UUID", + }}, +} +return nil, status.New(codes.InvalidArgument, "validation failed").WithDetails(detail).Err() +``` + +### 为什么要用 status.Error + +| 维度 | fmt.Errorf | status.Error | +|------|-----------|-------------| +| 客户端获取状态码 | 需要 parse 字符串 | `status.Code(err)` 直接获取 | +| 跨语言一致 | 各语言行为不一致 | gRPC 协议标准化 | +| 携带结构化错误 | 不支持 | 通过 `WithDetails` 携带 proto detail | +| 配合拦截器重试 | 无法识别 | 拦截器根据 codes.* 决定重试 | + +## 状态码详情 (Status Details) + +gRPC 允许在错误响应中附加结构化详情,这对自动化运维尤其重要: + +```go +// 超时场景中附带重试信息 +retryInfo := &errdetails.RetryInfo{ + RetryDelay: durationpb.New(2 * time.Second), +} + +// 权限场景中附带帮助链接 +help := &errdetails.Help{ + Links: []*errdetails.Help_Link{{ + Url: "https://internal.wiki/perm-error", + Description: "如何申请访问权限", + }}, +} + +status.New(codes.PermissionDenied, "no access"). + WithDetails(retryInfo, help). + Err() +``` + +客户端可以程序化提取这些信息: + +```go +st := status.Convert(err) +for _, d := range st.Details() { + switch detail := d.(type) { + case *errdetails.RetryInfo: + time.Sleep(detail.RetryDelay.AsDuration()) + // 执行重试 + } +} +``` + +## 客户端优雅降级 + +```go +resp, err := client.CreateOrder(ctx, req) +switch status.Code(err) { +case codes.NotFound: + // 降级:尝试加载缓存数据 + return fallbackFromCache(ctx, id) +case codes.DeadlineExceeded, codes.Unavailable: + // 降级:返回友好提示或部分数据 + return partialOrder(id), nil +default: + return nil, err +} +``` + +> [!keypoint] 黄金法则 +> +> - 永远用 `codes.*` 而不用 `fmt.Errorf` 做 gRPC 返回值 +> - 不要吞掉所有错误一律返回 `Internal` —— 这会让排查问题无从下手 +> - 客户端根据状态码决定重试还是降级,而不是所有错都 retry + +## 何时返回 Internal + +`Internal` 是最不应该被使用的状态码。只在以下情况使用: + +| 场景 | 示例 | +|------|------| +| 代码逻辑 bug | panic recover、nil pointer dereference | +| 不可恢复的内部状态 | 数据库连接池耗尽、配置文件解析失败 | +| 子服务报错但不确定原因 | 下游返回的 code 不在预期范围内 | + +> [!warning] Internal 不等于"随便的错" +> +> 如果你的日志里有大量的 `Internal` 错误,这通常意味着上游的错误分类不够细,掩盖了真正的根因。每次遇到不知道该怎么映射的错误时,优先考虑是否能归入现有的某个 code。 + +## 关联笔记 + +- [[04-拦截器]] — 拦截器根据错误码决定是否触发自动重试 +- [[07-最佳实践]] — 重试策略中与错误状态的配合配置 +- [[02-服务治理/06-容错模式]] — 熔断器在收到特定错误码后触发熔断 +- [[02-服务治理/03-分布式追踪]] — 错误链路在追踪系统中的标注方式 diff --git a/hhs/MS/06-gRPC/06-连接管理.md b/hhs/MS/06-gRPC/06-连接管理.md new file mode 100644 index 0000000..9054e8a --- /dev/null +++ b/hhs/MS/06-gRPC/06-连接管理.md @@ -0,0 +1,155 @@ +--- +tags: [grpc, connection-management, keepalive, load-balancing, dns, service-discovery] +create time: 2026-05-07 16:00 +--- + +# 连接管理与负载均衡 + +## 概述 + +在生产环境中,gRPC 连接的管理质量直接影响**稳定性**和**可用性**。本文将 Cover 连接保活策略、负载均衡 Picker、服务发现 Name Resolver 三大主题——这些都是日常开发容易忽略但出问题时就是一大片故障的领域。 + +> [!question] 先看一个问题 +> +> 两个微服务之间建立了 gRPC 连接,之后 30 分钟没有任何请求。此时第一个新的 RPC 请求发起时会发生什么? + +如果中间经过了 Nginx、云厂商 LB 或 AWS ALB,很可能连接已经被idle timeout 切断。但两端都还认为连接是活的,于是第一次 `Send()` 报 `use of closed network connection`。这就是**没有配置 Keepalive 的典型故障**。 + +## Keepalive 策略 + +gRPC 连接默认不发送 keepalive ping,长时间空闲的连接会被中间代理(如 Nginx、云厂商 LB)切断: + +```go +server := grpc.NewServer( + grpc.KeepaliveParams(keepalive.ServerParameters{ + Time: 10 * time.Second, // ping 间隔 + Timeout: 5 * time.Second, // 超时检测 + MaxConnectionAge: 5 * time.Minute, // 最大生命周期(平滑退役) + }), + grpc.KeepaliveEnvelope(keepalive.EnforcementPolicy{ + MinTime: 5 * time.Second, // 客户端最小 ping 间隔 + PermitWithoutStream: true, // 允许空闲连接保活 + }), +) +``` + +### Keepalive 参数解读 + +| 参数 | 方向 | 含义 | +|------|------|------| +| `Time` | 服务端 | 多久没收到 ping 就发一个 ping | +| `Timeout` | 服务端 | 发了 ping 后等多久没回复就算对方挂了 | +| `MaxConnectionAge` | 服务端 | 连接最多存活多久,强制关闭让客户端重建 | +| `MinTime` | 服务端 | 限制客户端 ping 频率,防 DoS | +| `PermitWithoutStream` | 服务端 | 即使没有活跃 Stream 也允许发送 ping | + +### 连接生命周期 + +```mermaid +timeline + title 连接生命周期管理 + 0 min : 建立连接 + 5 min : 达到 MaxConnectionAge
服务端主动关闭
(旧连接不再收新请求) + 5min+ : 客户端创建新连接
完成平滑迁移 +``` + +> [!keypoint] MaxConnectionAge 的作用 +> +> 它不是为了保活,而是为了**平滑退役**。当后端实例缩容或升级时,通过 MaxConnectionAge 让旧连接自然到期失效,客户端自动切换到新连接,避免突然断连导致的请求失败。 + +## 负载均衡 Picker + +gRPC 内建了几种负载均衡策略,客户端侧自动分发请求到不同后端实例: + +```mermaid +graph LR + LR_WRR["Weighted Round Robin
权重轮询"] --> Picking["Picker.pick()
→ 选择一个 SubConn"] + LR_HRR["Hash-based
粘性会话"] --> Picking + LR_RRS["Random Selection
随机挑选"] --> Picking + LR_PR["Pick First
单一连接"] --> Picking + + Picking --> SubConn["SubConn
单个后端实例"] +``` + +### 策略对比 + +| 策略 | 行为 | 适用场景 | +|------|------|---------| +| **pick_first**(默认) | 每次只用一个 SubConn,直到 health check 失败才切换 | 只有一个后端、简单场景 | +| **round_robin** | 轮询分发到新连接 | 无状态服务、均匀负载 | +| **weighted_round_robin** | 按权重轮询(Go 1.25+) | 后端规格不一致时使用 | +| **hash-based** | 按 key hash 选定后端 | 需要会话粘性的场景 | + +```go +// 客户端指定负载均衡策略 +conn, err := grpc.Dial( + "dns:///orderservice:9000", + grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`), +) +``` + +> [!warning] pick_first 的隐患 +> +> 默认的 pick_first 策略只使用一个连接。如果这个连接对应的后端实例挂了,gRPC 要等到 health check 失败才会切换。在高可用要求高的场景下,务必切换到 round_robin。 + +## Name Resolver —— 服务发现桥梁 + +Name Resolver 是 gRPC 将逻辑服务名解析为物理 IP:Port 的桥梁: + +```mermaid +graph LR + Target["目标地址:
dns:///svc:9000"] --> NR["Name Resolver"] + NR --> SD["服务发现后端
consul / kubernetes / etcd"] + SD --> AddrList["[]Resolver.Addresses"] + AddrList --> CC["ClientConn
新建/复用 SubConn"] + + style NR fill:#fff3e0 + style SD fill:#e3f2fd +``` + +### Name Resolver 方案 + +| 方案 | URI 前缀 | 适用场景 | +|------|---------|---------| +| DNS | `dns:///host:port` | 最简单,依赖 DNS 记录 | +| Kubernetes | `k8s://` | K8s Service Discovery | +| Eureka | `eureka:///service-name` | Spring Cloud 生态 | +| File | `file:///path/to/config` | 静态配置文件开发调试 | + +### Kubernetes 环境下的零配置方案 + +```go +// 结合 CoreDNS SRV 记录,无需硬编码任何地址 +conn, err := grpc.Dial( + "dns:///my-service.default.svc.cluster.local:9000", + grpc.WithDefaultServiceConfig(`{"loadBalancingConfig":[{"round_robin":{}}]}`), +) +``` + +Kubernetes 的 DNS 控制器会自动将 Service 的 Endpoints 更新到 DNS 记录,gRPC 客户端只需要监听 DNS 变化即可。 + +> [!tip] 云原生首选 +> +> 在 Kubernetes 环境中,结合 ExternalName 或 CoreDNS SRV 记录可以实现零配置的服务发现。配合 `round_robin` 负载均衡策略,后端扩容缩容时客户端自动感知,无需手动干预。 + +## 连接管理与可观测性联动 + +| 维度 | 与可观测性的配合 | +|------|----------------| +| **连接断开** | 通过 Metrics 监控连接创建/销毁频率,异常突增说明后端频繁重启 | +| **负载均衡不均** | 通过 Histogram 看每个后端实例的 QPS 分布,失衡则调整 picker | +| **Keepalive 超时** | 通过日志告警 Detect connection reset,定位中间代理 idle timeout 配置 | + +> [!keypoint] 监控建议 +> +> 生产环境的 gRPC 客户端和服务端都应该暴露以下指标: +> - `grpc_connection_state_changes_total`:连接状态变更次数 +> - `grpc_call_duration_seconds`:单次 RPC 耗时 +> - `grpc_server_handled_total`:按 status code 分组的服务端调用计数 + +## 关联笔记 + +- [[01-协议与架构]] — Name Resolver 是架构图中连接管理层的第一环 +- [[07-最佳实践]] — Keepalive、负载均衡在生产环境的配合配置 +- [[02-服务治理/04-服务发现]] — 更深度的服务发现机制对比(Nacos / Consul / K8s) +- [[02-服务治理/06-容错模式]] — 负载均衡 + 熔断的组合效果 diff --git a/hhs/MS/06-gRPC/07-最佳实践.md b/hhs/MS/06-gRPC/07-最佳实践.md new file mode 100644 index 0000000..9d29968 --- /dev/null +++ b/hhs/MS/06-gRPC/07-最佳实践.md @@ -0,0 +1,220 @@ +--- +tags: [grpc, production-readiness, retries, tls, performance-tuning] +create time: 2026-05-07 16:00 +--- + +# 生产环境最佳实践 + +## 概述 + +本章汇总 gRPC 在生产部署时需要关注的各项配置与策略:重试机制、TLS/mTLS、代码生成工具链、性能调优。这些知识点往往是"知道能解决问题,不知道就是故障"的存在。 + +> [!question] 最后的思考题 +> +> 如果你的 gRPC 服务 QPS 达到 10 万级别,但仍然发现延迟偏高,你觉得最可能的瓶颈在哪里?是序列化、网络、还是连接管理?带着这个问题去实际压测一遍,答案会比看十篇文章深刻。 + +## 重试策略 + +生产环境不建议无条件重试,但要针对可恢复错误配置自动重试: + +```go +// 客户端配置自动重试 +retryPolicy := `{ + "retryPolicy": { + "maxAttempts": 3, + "initialBackoff": "0.1s", + "maxBackoff": "1s", + "backoffMultiplier": 2, + "retryableStatusCodes": ["UNAVAILABLE", "DEADLINE_EXCEEDED"] + } +}` + +conn, err := grpc.Dial(target, + grpc.WithDefaultServiceConfig(retryPolicy), +) +``` + +```mermaid +flowchart LR + attempt1["第1次调用
UNAVAILABLE"] -->|指数退避 100ms| attempt2["第2次调用
UNAVAILABLE"] -->|指数退避 200ms| attempt3["第3次调用
SUCCESS"] + + attempt1 -.->|INTERNAL → 不重试| final["终止"] + + style attempt1 fill:#fff3e0 + style attempt2 fill:#ffe0b2 + style attempt3 fill:#c8e6c9 + style final fill:#ffcdd2 +``` + +### 重试的安全边界 + +> [!warning] 重试的三条红线 +> +> 1. **仅幂等操作**(GET、DELETE)可以安全重试 +> 2. **POST/create 操作**重试可能产生重复数据,必须在业务层加幂等键(如 `idempotency-key` header) +> 3. 重试会增加**读放大**,特别是涉及 DB 的场景 + +| 操作类型 | 可重试? | 注意事项 | +|---------|---------|---------| +| Query / Get | ✅ 安全 | 本身幂等,可放心重试 | +| Update / Patch | ⚠️ 有条件 | 需要在 DB 层加乐观锁或唯一索引 | +| Create / Insert | ❌ 谨慎 | 必须有幂等键机制,否则可能重复插入 | +| Delete | ✅ 安全 | 幂等操作 | + +## TLS / mTLS 配置 + +```go +tlsConfig := &tls.Config{ + Certificates: []tls.Certificate{cert}, + ClientAuth: tls.RequireAndVerifyClientCert, + ClientCertPool: certPool, + MinVersion: tls.VersionTLS12, +} +``` + +对于大规模微服务,推荐使用 **mTLS**(双向证书认证)作为服务间信任的基础: + +```go +// 服务端:需要客户端证书 +conn, _ := grpc.Dial(target, + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), +) + +// 客户端也需要携带自己的证书 +creds := credentials.NewTLS(tlsConfig) +``` + +> [!tip] Istio 集成 +> +> 在服务网格中,mTLS 由 Sidecar Proxy(Envoy)自动处理,业务代码无需关心 TLS 细节。gRPC 连接经过 Sidecar 时透明加密,业务层仍然使用明文连接(localhost)。 + +## 代码生成工具链 + +```mermaid +graph LR + Proto["*.proto files"] --> Buf["buf generate"] + Buf --> Go["go_proto_plugin
生成 Go 代码"] + Buf --> JS["js_proto_plugin
生成 TS/JS 代码"] + Buf --> Validate["validate.proto
生成校验代码"] + Buf --> GRPC["go_grpc_plugin
生成 gRPC 桩"] + + style Proto fill:#e3f2fd + style Buf fill:#fff3e0 + style Go fill:#c8e6c9 + style JS fill:#fce4ec + style GRPC fill:#e8f5e9 +``` + +### buf.gen.yaml 示例 + +```yaml +version: v2 +plugins: + - remote: buf.build/protocolbuffers/go + out: gen/go + opt: paths=source_relative + - remote: buf.build/grpc/go + out: gen/go + opt: paths=source_relative + - remote: buf.build/bufbuild/validate-go + out: gen/go + opt: paths=source_relative +``` + +> [!tip] buf.lock —— 锁定依赖版本 +> +> 像 go.mod 锁定 Go 模块版本一样,`buf.lock` 锁定 proto 依赖的确切 commit,避免上游变更导致构建不一致。CI 中应加入 `buf dep update --lock` 的检查步骤。 + +### CI/CD 集成 + +```yaml +# GitHub Actions 示例 +- name: Generate gRPC code + uses: bufbuild/buf-action@v1 + with: + command: generate + input: proto/ + +- name: Check generated code is up to date + run: buf mod update && git diff --exit-code +``` + +## 配置管理 + +完整的 Dial 配置参考: + +```go +conn, err := grpc.DialContext(ctx, target, + // 基础选项 + grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)), + grpc.WithInitialWindowSize(1<<20), // 窗口大小 1MB + grpc.WithInitialConnWindowSize(1<<20), // 连接窗口 1MB + grpc.MaxCallRecvMsgSize(10 << 20), // 最大收包 10MB + grpc.MaxCallSendMsgSize(10 << 20), // 最大发包 10MB + grpc.WithConnectParams(grpc.ConnectParams{ // 连接参数 + MinConnectTimeout: 5 * time.Second, + BackoffConfig: backoff.Config{ + BaseDelay: 100 * time.Millisecond, + Multiplier: 1.6, + MaxDelay: 3 * time.Second, + }, + }), +) +``` + +### Dial 选项速查 + +| 配置项 | 默认值 | 建议值 | 作用域 | +|--------|--------|--------|--------| +| MaxCallRecvMsgSize | 4MB | 10MB | 仅客户端 | +| MaxCallSendMsgSize | 4MB | 10MB | 仅服务端 | +| InitialWindowSize | 64KB | 1MB(高吞吐) | 连接级 | +| BackoffBaseDelay | 100ms | 100ms | 连接断开重连 | +| BackoffMaxDelay | 10s | 3s | 连接断开重连 | + +## 性能调优要点 + +| 优化项 | 建议值 | 影响 | +|--------|--------|------| +| 单个消息大小上限 | 4MB~10MB | 防止 OOM,超出则分块传 | +| Initial Window Size | 1MB | 吞吐瓶颈时常需调大 | +| Keepalive Time | 10~30s | 太短增加开销,太长被代理杀 | +| Compressor | gzip(按需启用) | CPU vs 带宽权衡 | +| Connection Pooling | 让 gRPC 自动管理 | 不要手动开连接 | + +### gzip 压缩的取舍 + +```go +// 客户端对特定大 Payload 调用启用压缩 +resp, err := client.GetBigData( + ctx, + req, + grpc.UseCompressor("gzip"), +) +``` + +> [!summary] 何时启用 gzip +> +> | 场景 | 是否建议 gzip | 理由 | +> |------|-------------|------| +> | 小消息(< 1KB) | 否 | 压缩开销大于节省的带宽 | +> | 大消息(> 10KB)且 CPU 充裕 | 是 | 带宽通常是更大瓶颈 | +> | 高 QPS 短命请求 | 否 | CPU 反而成为瓶颈 | +> | 跨数据中心调用 | 是 | 网络 RTT 高,减少数据传输量有意义 | + +## 连接管理与 Keepalive 回顾 + +连接配置的最佳实践详见 [[06-连接管理]]。总结来说,生产环境至少要做到三件事: + +1. **开启 Keepalive**,ping 间隔 10~30s,防止被代理切断 +2. **配置 MaxConnectionAge**,让旧连接平滑退役,新版本自动接盘 +3. **设置合理 backoff**,连接断开时指数退避重连,不要打满服务器 + +## 关联笔记 + +- [[01-协议与架构]] — 理解协议栈有助于调优每一个参数的意义 +- [[04-拦截器]] — 重试策略可以通过 interceptor 实现更复杂的逻辑 +- [[05-错误处理]] — 重试策略根据错误状态码来决定是否重试 +- [[06-连接管理]] — Keepalive、负载均衡、Name Resolver 的详细配置 +- [[02-服务治理/02-安全机制]] — mTLS 与服务间身份认证的更深内容 +- [[02-服务治理/06-容错模式]] — 熔断器、限流器与重试策略的组合配置 diff --git a/hhs/MS/06-gRPC/README.md b/hhs/MS/06-gRPC/README.md new file mode 100644 index 0000000..1a8bb6f --- /dev/null +++ b/hhs/MS/06-gRPC/README.md @@ -0,0 +1,133 @@ +--- +tags: [grpc] +create time: 2026-05-07 16:30 +--- + +# gRPC 知识索引 + +## 概述 + +本目录系统整理 **gRPC** 的核心知识点,从底层协议到生产实践,由浅入深覆盖 gRPC 的每一个关键领域。与 [[02-服务治理/05-服务间通信]] 中的入门对比不同,这里聚焦于「用了 gRPC 之后」—— 如何设计 Proto、如何编写拦截器、如何处理流式调用、连接怎么管、出错怎么查。 + +```mermaid +graph LR + A["01 协议与架构"] --> B["02 Proto设计"] + B --> C["03 RPC模式"] + C --> D["04 拦截器"] + D --> E["05 错误处理"] + E --> F["06 连接管理"] + F --> G["07 最佳实践"] + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#c8e6c9 + style D fill:#fce4ec + style E fill:#e8f5e9 + style F fill:#f3e5f5 + style G fill:#ffe0b2 +``` + +## 知识体系 + +### 1. [[01-协议与架构]] — gRPC 的内部架构 + +从协议栈层次到 HTTP/2 多路复用原理,回答「为什么 gRPC 更快」。 + +| 核心内容 | 说明 | +|---------|------| +| 协议栈分层 | Application → Generated Stub → gRPC Framework → HTTP/2 → TCP/IP | +| HTTP/2 三特性 | 多路复用、HPACK 头部压缩、二进制分帧 | +| 组件职责 | Channel、Stub、Transport、Picker 的作用域划分 | + +> [!tip] 理论基础篇 +> 建议先读此篇,建立正确认知后再深入配置细节。 + +### 2. [[02-Proto设计]] — Proto 文件设计规范 + +如何写出经得起演进的 `.proto` 文件——这是最容易踩坑也最容易被忽视的部分。 + +| 核心内容 | 说明 | +|---------|------| +| Oneof / Map / Well-Known Types | Proto3 进阶类型用法 | +| 版本管理 | 向前兼容三大铁律与决策速查表 | +| reserved 机制 | 锁定已删除字段编号的安全声明 | +| 命名约定 | package / service / message / field 统一规范 | + +### 3. [[03-RPC模式]] — 四种 RPC 调用模式详解 + +| 模式 | 客户端消息数 | 服务端消息数 | 典型场景 | +|------|------------|------------|---------| +| Unary(普通) | 1 | 1 | CRUD 常规操作 | +| Server Streaming | 1 | N | 列表查询、日志流拉取 | +| Client Streaming | N | 1 | 批量写入、大文件分块上传 | +| Bidi Streaming | N | M | 聊天室、实时协作、行情推送 | + +> [!question] 选型思考 +> +> 实时订单状态推送:用双向流还是 WebSocket?gRPC 强类型契约 vs 浏览器原生支持的权衡在哪里? + +### 4. [[04-拦截器]] — Interceptor 与上下文传播 + +gRPC 的切面编程能力:鉴权、日志、指标采集、重试决策都通过 interceptor 实现。 + +| 核心内容 | 说明 | +|---------|------| +| 拦截器链架构 | 客户端链 vs 服务端链的执行顺序 | +| Go 链式封装 | `chainUnaryInterceptors` 解决多拦截器叠加问题 | +| Context Propagation | metadata 传递 trace ID、user ID 等上下文信息 | +| OpenTelemetry 集成 | W3C Trace Context 自动注入 | + +### 5. [[05-错误处理]] — 状态码规范与客户端降级 + +| 核心内容 | 说明 | +|---------|------| +| 16 个标准状态码 | 按 4xx / 5xx / 未实现分类的决策图 | +| status.Error vs fmt.Errorf | 为什么必须用 `codes.*` 做返回值 | +| Status Details | 带结构化详情的错误响应(BadRequest / RetryInfo) | +| 优雅降级 | 根据状态码选择重试、降级或直接报错 | + +### 6. [[06-连接管理]] — Keepalive、负载均衡与服务发现 + +| 核心内容 | 说明 | +|---------|------| +| Keepalive 策略 | ping 间隔、超时检测、MaxConnectionAge 平滑退役 | +| 负载均衡 Picker | pick_first / round_robin / weighted_round_robin 选型 | +| Name Resolver | DNS / K8s / Eureka 等服务发现后端接入 | + +### 7. [[07-最佳实践]] — 生产部署 checklist + +| 核心内容 | 说明 | +|---------|------| +| 重试策略 | 指数退避 + 幂等性约束 + retryableStatusCodes 配置 | +| TLS / mTLS | 服务间信任基础,Istio Sidecar 透明加密 | +| Buf 工具链 | buf generate / buf.lock CI 集成 | +| 性能调优 | Window Size、gzip 取舍、QPS 万级压测要点 | + +## 阅读路径 + +```mermaid +graph LR + Index["本文档
(索引)"] --> Arch["01 协议与架构"] + Arch --> Proto["02 Proto设计"] + Proto --> RPCC["03 RPC模式"] + RPCC --> Intc["04 拦截器"] + Intc --> Err["05 错误处理"] + Err --> Conn["06 连接管理"] + Conn --> Prod["07 最佳实践"] + + style Index fill:#fff9c4 + style Arch fill:#e3f2fd + style Prod fill:#ffe0b2 +``` + +- **推荐路径**:按编号顺序逐篇阅读,每篇独立成篇也可跳读 +- **快速上手**:直接读 [[03-RPC模式]] 和 [[07-最佳实践]],掌握核心用法后按需补其他篇 +- **遇到问题时**:优先定位到对应子篇,不必通读全文 + +## 关联笔记 + +- [[02-服务治理/05-服务间通信]] — gRPC 与 REST 的基础对比及混合通信模式 +- [[02-服务治理/04-服务发现]] — Nacos / Consul / K8s Service 深度对比 +- [[02-服务治理/06-容错模式]] — 重试、熔断、限流、降级的完整治理 +- [[02-服务治理/03-分布式追踪]] — OpenTelemetry 链路追踪 +- [[02-服务治理/02-安全机制]] — mTLS、JWT、RBAC diff --git a/hhs/MS/README.md b/hhs/MS/README.md new file mode 100644 index 0000000..7e2aafb --- /dev/null +++ b/hhs/MS/README.md @@ -0,0 +1,88 @@ +--- +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 + + style A fill:#e3f2fd + style B fill:#fff3e0 + style C fill:#fce4ec + style D fill:#e8f5e9 + style E fill:#f3e5f5 +``` + +## 核心主题 + +### 1. [[01-基础概念]] — 什么是微服务? + +微服务的核心定义、DDD 限界上下文拆分原则、单体 vs 微服务的权衡。 + +> [!tip] 理论基础篇 +> 这是整个系列的起点,建议在动手之前先通读。 + +### 2. [[02-服务治理]] — 服务如何被发现和治理? + +| 子主题 | 核心内容 | +|--------|---------| +| [[02-服务治理/04-服务发现]] | Nacos / Consul / K8s Service,客户端 vs 服务端发现模式 | +| [[02-服务治理/01-API网关]] | 统一入口的职责边界、路由策略、插件体系 | +| [[02-服务治理/05-服务间通信]] | gRPC vs REST,同步 vs 异步组合拳 | +| [[02-服务治理/06-容错模式]] | 超时 / 重试 / 熔断 / 限流 / 舱壁隔离 / 降级 | +| [[02-服务治理/07-配置管理]] | Nacos Config / Apollo,动态刷新与版本管理 | +| [[02-服务治理/03-分布式追踪]] | OpenTelemetry,trace/span 概念,采样策略 | +| [[02-服务治理/08-流量治理]] | 灰度发布策略,蓝绿 vs 金丝雀,Istio VirtualService | +| [[02-服务治理/02-安全机制]] | mTLS, JWT, RBAC/ABAC, 输入防护 | +| → [[04-微服务安全/service-mesh实战]] | Service Mesh 中的 mTLS 配置与证书管理 | + +### 3. [[03-数据一致性]] — 跨服务数据一致性怎么保证? + +| 子主题 | 核心内容 | +|--------|---------| +| [[03-数据一致性/01-数据库拆分]] | 垂直拆分 / 水平分片,ShardingSphere,跨库查询方案 | +| [[03-数据一致性/02-分布式事务]] | Outbox 模式,Saga,TCC,AT 模式,幂等设计 | +| [[03-数据一致性/03-ID生成]] | Snowflake, UUID v7, 号段模式 | + +### 4. [[04-可观测性]] — 请求穿越多个服务后如何排查问题? + +| 子主题 | 核心内容 | +|--------|---------| +| [[04-可观测性/01-Metrics监控]] | Prometheus, RED/USE 方法, Counter/Gauge/Histogram | +| [[04-可观测性/02-日志系统]] | JSON 结构化日志, ELK vs Loki, 采集管线, 采样策略 | +| [[04-可观测性/03-链路追踪]] | OpenTelemetry, W3C Trace Context, 上下文传播, 采样 | +| [[04-可观测性/04-告警管理]] | SLO/Error Budget 告警, 分级降噪, On-Call 最佳实践 | + +### 5. [[05-部署运维]] — 上百个服务如何容器化编排与发布? + +| 子主题 | 核心内容 | +|--------|---------| +| [[05-部署运维/01-容器化]] | Dockerfile 最佳实践, 镜像安全, distroless | +| [[05-部署运维/02-Kubernetes]] | Pod/Deployment/Service/Ingress, Probe, HPA, 资源配置 | +| [[05-部署运维/03-CICD与GitOps]] | GitHub Actions Pipeline, GitOps (ArgoCD), Helm | +| [[05-部署运维/04-SRE实践]] | SLI/SLO/SLA, Error Budget, Blameless Postmortem, DORA Metrics | + +## 学习建议 + +> [!tip] 学习路径 +> 按编号顺序阅读。第 1 篇是理论基础,后面 4 篇在实践中高度耦合,可以按需跳读。 + +> [!question] 带着问题读 +> 每篇文章都设计了启发式问题。先自己想一遍答案,再看文档,效果更好。 + +## 关联笔记 + +- [[hzh/MS/API 设计原则]] — API Gateway 的设计与 REST/gRPC 选型