Init
This commit is contained in:
@@ -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<br/>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-拦截器]] — 切面编程和上下文传播
|
||||
@@ -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 文件<br/>唯一真相源"] --> protoc["protoc / Buf"]
|
||||
protoc --> Server["服务端生成代码<br/>(Go stub)"]
|
||||
protoc --> Client["客户端生成代码<br/>(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<string, string> labels = 1;
|
||||
map<int32, float> 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-服务间通信]] — 不同序列化方案的性能对比
|
||||
@@ -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 保活
|
||||
@@ -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-安全机制]] — 鉴权拦截器是安全机制在代码层的落地形式
|
||||
@@ -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-分布式追踪]] — 错误链路在追踪系统中的标注方式
|
||||
@@ -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<br/>服务端主动关闭<br/>(旧连接不再收新请求)
|
||||
5min+ : 客户端创建新连接<br/>完成平滑迁移
|
||||
```
|
||||
|
||||
> [!keypoint] MaxConnectionAge 的作用
|
||||
>
|
||||
> 它不是为了保活,而是为了**平滑退役**。当后端实例缩容或升级时,通过 MaxConnectionAge 让旧连接自然到期失效,客户端自动切换到新连接,避免突然断连导致的请求失败。
|
||||
|
||||
## 负载均衡 Picker
|
||||
|
||||
gRPC 内建了几种负载均衡策略,客户端侧自动分发请求到不同后端实例:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
LR_WRR["Weighted Round Robin<br/>权重轮询"] --> Picking["Picker.pick()<br/>→ 选择一个 SubConn"]
|
||||
LR_HRR["Hash-based<br/>粘性会话"] --> Picking
|
||||
LR_RRS["Random Selection<br/>随机挑选"] --> Picking
|
||||
LR_PR["Pick First<br/>单一连接"] --> Picking
|
||||
|
||||
Picking --> SubConn["SubConn<br/>单个后端实例"]
|
||||
```
|
||||
|
||||
### 策略对比
|
||||
|
||||
| 策略 | 行为 | 适用场景 |
|
||||
|------|------|---------|
|
||||
| **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["目标地址:<br/>dns:///svc:9000"] --> NR["Name Resolver"]
|
||||
NR --> SD["服务发现后端<br/>consul / kubernetes / etcd"]
|
||||
SD --> AddrList["[]Resolver.Addresses"]
|
||||
AddrList --> CC["ClientConn<br/>新建/复用 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-容错模式]] — 负载均衡 + 熔断的组合效果
|
||||
@@ -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次调用<br/>UNAVAILABLE"] -->|指数退避 100ms| attempt2["第2次调用<br/>UNAVAILABLE"] -->|指数退避 200ms| attempt3["第3次调用<br/>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<br/>生成 Go 代码"]
|
||||
Buf --> JS["js_proto_plugin<br/>生成 TS/JS 代码"]
|
||||
Buf --> Validate["validate.proto<br/>生成校验代码"]
|
||||
Buf --> GRPC["go_grpc_plugin<br/>生成 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-容错模式]] — 熔断器、限流器与重试策略的组合配置
|
||||
@@ -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["本文档<br/>(索引)"] --> 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
|
||||
Reference in New Issue
Block a user