From f528b76f1ceeccc29c9edf709daafa31026dbcbe Mon Sep 17 00:00:00 2001 From: wonder Date: Thu, 7 May 2026 15:55:02 +0800 Subject: [PATCH] vault backup: 2026-05-07 15:55:02 --- hzh/MS/02-服务治理/服务间通信/README.md | 3 + hzh/MS/06-gRPC/01-协议与架构.md | 161 +++++++++++++++++ hzh/MS/06-gRPC/02-Proto设计.md | 195 +++++++++++++++++++++ hzh/MS/06-gRPC/03-RPC模式.md | 197 +++++++++++++++++++++ hzh/MS/06-gRPC/04-拦截器.md | 176 +++++++++++++++++++ hzh/MS/06-gRPC/05-错误处理.md | 182 ++++++++++++++++++++ hzh/MS/06-gRPC/06-连接管理.md | 155 +++++++++++++++++ hzh/MS/06-gRPC/07-最佳实践.md | 220 ++++++++++++++++++++++++ hzh/MS/06-gRPC/README.md | 133 ++++++++++++++ 9 files changed, 1422 insertions(+) create mode 100644 hzh/MS/06-gRPC/01-协议与架构.md create mode 100644 hzh/MS/06-gRPC/02-Proto设计.md create mode 100644 hzh/MS/06-gRPC/03-RPC模式.md create mode 100644 hzh/MS/06-gRPC/04-拦截器.md create mode 100644 hzh/MS/06-gRPC/05-错误处理.md create mode 100644 hzh/MS/06-gRPC/06-连接管理.md create mode 100644 hzh/MS/06-gRPC/07-最佳实践.md create mode 100644 hzh/MS/06-gRPC/README.md diff --git a/hzh/MS/02-服务治理/服务间通信/README.md b/hzh/MS/02-服务治理/服务间通信/README.md index 240f35c..411db8e 100644 --- a/hzh/MS/02-服务治理/服务间通信/README.md +++ b/hzh/MS/02-服务治理/服务间通信/README.md @@ -63,6 +63,9 @@ sequenceDiagram gRPC 基于 Protocol Buffers(Proto3),利用 HTTP/2 的多路复用特性,非常适合高性能的内部服务间调用。 +> [!tip] 深入阅读 +> [[06-gRPC]] — gRPC 深度指南,涵盖 Proto 进阶、流式调用、Interceptor、连接管理、生产最佳实践。 + ```go // proto/order/v1/order.proto syntax = "proto3"; diff --git a/hzh/MS/06-gRPC/01-协议与架构.md b/hzh/MS/06-gRPC/01-协议与架构.md new file mode 100644 index 0000000..32ec369 --- /dev/null +++ b/hzh/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-服务治理/服务间通信]]。对外暴露 API 时,HTTP/JSON 仍然不可替代——因为浏览器的 Native Fetch 无法直接调用 gRPC,第三方接入者也不想安装 Proto 编译器。 + +## 关联笔记 + +- [[02-服务治理/服务间通信]] — gRPC 与 REST 的基础对比及选型建议 +- [[02-服务治理/容错模式/README]] — 基于此架构的重试、熔断等治理机制 +- [[02-Proto设计]] — Proto 文件设计的进阶实践 +- [[03-RPC模式]] — 四种 RPC 模式的深度用法 +- [[04-拦截器]] — 切面编程和上下文传播 diff --git a/hzh/MS/06-gRPC/02-Proto设计.md b/hzh/MS/06-gRPC/02-Proto设计.md new file mode 100644 index 0000000..0f109df --- /dev/null +++ b/hzh/MS/06-gRPC/02-Proto设计.md @@ -0,0 +1,195 @@ +--- +tags: [grpc, protobuf, schema-versioning, proto3] +create time: 2026-05-07 16:00 +--- + +# Proto 设计规范 + +## 概述 + +本文覆盖 **Proto3 进阶特性**和**版本管理策略**。如果说架构篇是"看懂 gRPC 长什么样",这篇就是教你"如何写出经得起演进的 Proto 文件"——这是工程中最容易踩坑、却最容易被忽视的部分。 + +> [!question] 思考 +> +> 你的 Service A 调用 Service B 的 `GetUser`,Proto 里定义了 20 个字段。半年后你想加第 21 个字段,但旧版客户端没有编译更新。会发生什么? + +好消息是: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-服务治理/服务间通信]] — 不同序列化方案的性能对比 diff --git a/hzh/MS/06-gRPC/03-RPC模式.md b/hzh/MS/06-gRPC/03-RPC模式.md new file mode 100644 index 0000000..e8c4ba5 --- /dev/null +++ b/hzh/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/hzh/MS/06-gRPC/04-拦截器.md b/hzh/MS/06-gRPC/04-拦截器.md new file mode 100644 index 0000000..5f4c12d --- /dev/null +++ b/hzh/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-服务治理/分布式追踪]] 的核心能力。 + +## 拦截器最佳实践 + +> [!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-服务治理/分布式追踪]] — Context 传播是实现分布式追踪的前提 +- [[02-服务治理/安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式 diff --git a/hzh/MS/06-gRPC/05-错误处理.md b/hzh/MS/06-gRPC/05-错误处理.md new file mode 100644 index 0000000..25df44e --- /dev/null +++ b/hzh/MS/06-gRPC/05-错误处理.md @@ -0,0 +1,182 @@ +--- +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 - 内部错误"] + SYSTEM_LOST["SYSTEM_ERROR - 系统级错误"] + end + + OK + CANCELLED ~~~ UNAUTH + INVALID_ARG ~~~ PERMISSION_DENIED + DEADLINE_EX ~~~ UNAVAIL + INTERNAL_ERR ~~ RESOURCE_EXP +``` + +### 状态码速查表 + +| 业务含义 | 推荐状态码 | 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-服务治理/容错模式]] — 熔断器在收到特定错误码后触发熔断 +- [[02-服务治理/分布式追踪]] — 错误链路在追踪系统中的标注方式 diff --git a/hzh/MS/06-gRPC/06-连接管理.md b/hzh/MS/06-gRPC/06-连接管理.md new file mode 100644 index 0000000..ca94a1f --- /dev/null +++ b/hzh/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-服务治理/服务发现/README]] — 更深度的服务发现机制对比(Nacos / Consul / K8s) +- [[02-服务治理/容错模式/README]] — 负载均衡 + 熔断的组合效果 diff --git a/hzh/MS/06-gRPC/07-最佳实践.md b/hzh/MS/06-gRPC/07-最佳实践.md new file mode 100644 index 0000000..f4726f1 --- /dev/null +++ b/hzh/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-服务治理/安全机制]] — mTLS 与服务间身份认证的更深内容 +- [[02-服务治理/容错模式]] — 熔断器、限流器与重试策略的组合配置 diff --git a/hzh/MS/06-gRPC/README.md b/hzh/MS/06-gRPC/README.md new file mode 100644 index 0000000..4df98a6 --- /dev/null +++ b/hzh/MS/06-gRPC/README.md @@ -0,0 +1,133 @@ +--- +tags: [grpc] +create time: 2026-05-07 16:30 +--- + +# gRPC 知识索引 + +## 概述 + +本目录系统整理 **gRPC** 的核心知识点,从底层协议到生产实践,由浅入深覆盖 gRPC 的每一个关键领域。与 [[02-服务治理/服务间通信]] 中的入门对比不同,这里聚焦于「用了 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-服务治理/服务间通信]] — gRPC 与 REST 的基础对比及混合通信模式 +- [[02-服务治理/服务发现/README]] — Nacos / Consul / K8s Service 深度对比 +- [[02-服务治理/容错模式/README]] — 重试、熔断、限流、降级的完整治理 +- [[02-服务治理/分布式追踪/README]] — OpenTelemetry 链路追踪 +- [[02-服务治理/安全机制/README]] — mTLS、JWT、RBAC