512 lines
17 KiB
Markdown
512 lines
17 KiB
Markdown
---
|
||
tags: [microservice, service-governance, api-gateway, circuit-breaker, service-discovery, distributed-tracing, canary-deployment]
|
||
create time: 2026-04-29 12:02
|
||
---
|
||
|
||
# 服务治理
|
||
|
||
## 概述
|
||
|
||
服务治理是微服务架构的"操作系统"——它不直接实现业务逻辑,但决定了系统能否在复杂、高并发、多团队协作的环境中稳定运转。
|
||
|
||
本文覆盖服务治理的 **七大核心能力**:
|
||
|
||
```mermaid
|
||
quadrantChart
|
||
title 服务治理能力矩阵
|
||
x-axis Low Impact --> High Impact
|
||
y-axis Foundational --> Advanced
|
||
"服务发现": [0.25, 0.2]
|
||
"负载均衡": [0.45, 0.25]
|
||
"API Gateway": [0.35, 0.4]
|
||
"熔断降级": [0.55, 0.5]
|
||
"限流": [0.65, 0.6]
|
||
"链路追踪": [0.5, 0.75]
|
||
"配置中心": [0.4, 0.7]
|
||
"灰度发布": [0.75, 0.9]
|
||
```
|
||
|
||
> [!question] 开篇思考
|
||
> 假设你有 10 个微服务,每个服务有 3 个实例,总共 30 个服务实例。手动维护它们的地址列表会带来哪些问题?你能想到几种解决方案?
|
||
|
||
答案藏在下面每一个章节里——从服务发现到流量治理,每一层都在解决特定维度的复杂度。
|
||
|
||
## 服务发现
|
||
|
||
服务实例的动态变化(扩容、缩容、故障重启)让硬编码地址成为不可能。服务要回答的核心问题是:**我怎么找到你?**
|
||
|
||
### 两种模式
|
||
|
||
```mermaid
|
||
graph TB
|
||
subgraph Client["客户端发现模式"]
|
||
A[Service A] -->|1.查询注册中心| R1[(Registry)]
|
||
R1 -->|2.返回实例列表| A
|
||
A -->|3.自选实例直连| B[Service B]
|
||
end
|
||
|
||
subgraph Server["服务端发现模式"]
|
||
C[Service C] -->|请求| P[LoadBalancer Proxy]
|
||
P -->|查询注册中心| R2[(Registry)]
|
||
R2 -->|返回实例| P
|
||
P -->|转发| D[Service D]
|
||
end
|
||
```
|
||
|
||
**对比**:
|
||
|
||
| 维度 | 客户端发现 | 服务端发现 |
|
||
|------|-----------|-----------|
|
||
| 典型实现 | Eureka、Consul、Nacos | Nginx、Envoy、K8s Service |
|
||
| 耦合度 | 客户端嵌入注册逻辑 | 透明代理,客户端无感知 |
|
||
| 性能 | 直连调用,延迟最低 | 多一跳代理开销 |
|
||
| 运维复杂度 | 每门语言需独立 SDK | 统一部署代理,技术无关 |
|
||
|
||
> [!tip] 推荐路径
|
||
> Kubernetes 环境下直接用 **K8s Service + Ingress**;非容器环境优先考虑 **Nacos**(阿里开源,国内生态好)。
|
||
> 如果团队已有 Consul 基础设施,无需迁移——它在中小规模集群中表现优秀。
|
||
|
||
### 实战:Nacos 服务发现
|
||
|
||
```go
|
||
// Nacos 服务注册 & 拉取
|
||
import (
|
||
"github.com/nacos-group/nacos-sdk-go/v2/clients"
|
||
"github.com/nacos-group/nacos-sdk-go/v2/common/constant"
|
||
)
|
||
|
||
// 创建注册客户端
|
||
sc := constant.ServerConfig{
|
||
IpAddr: "127.0.0.1",
|
||
Port: 8848,
|
||
}
|
||
|
||
// 注册服务实例
|
||
client, _ := clients.NewNamingClient(
|
||
value_map.NewValueMap(map[string]any{"serverConfig": sc}),
|
||
)
|
||
|
||
client.RegisterInstance(naming_param.RegisterInstanceParam{
|
||
Ip: "10.0.0.1",
|
||
Port: 8080,
|
||
ServiceName: "order-service",
|
||
ClusterName: "DEFAULT",
|
||
})
|
||
|
||
// 拉取某服务的可用实例列表
|
||
instances, _ := client.SelectInstances(naming_param.SelectInstanceParam{
|
||
ServiceName: "order-service",
|
||
})
|
||
```
|
||
|
||
> [!warning] 注意
|
||
> Nacos 同时支持临时实例(故障自动摘除)和持久实例(基于 etcd)。生产环境中临时实例更常见——节点挂了会自动从列表中剔除。
|
||
|
||
**关键问题**:服务下线时,如何确保正在处理的请求不会被打断?这引出了健康检查和优雅关闭的概念。
|
||
|
||
## API Gateway
|
||
|
||
API Gateway 是所有外部请求的统一入口,承担以下职责:
|
||
|
||
```mermaid
|
||
graph LR
|
||
Client["客户端"] --> GW["API Gateway"]
|
||
GW --> Auth["鉴权 & 限流"]
|
||
GW --> Route["路由转发"]
|
||
GW --> Transform["协议转换"]
|
||
GW --> Log["日志 & 监控"]
|
||
GW -.-> CB[(配置中心)]
|
||
```
|
||
|
||
常见实现:**Kong、APISIX、Spring Cloud Gateway、Nginx**。
|
||
|
||
### 网关放什么?
|
||
|
||
> [!summary] 网关职责清单
|
||
>
|
||
> - ✅ 鉴权与认证
|
||
> - ✅ 限流熔断
|
||
> - ✅ HTTPS 终结
|
||
> - ✅ 请求/响应转换
|
||
> - ✅ 路由分发
|
||
> - ❌ 业务逻辑 — 网关保持瘦,重逻辑应下沉到业务服务
|
||
|
||
> [!question] 权衡题
|
||
> 有人说:"鉴权放到网关统一做,避免每个服务重复写。"但如果某个内部服务不需要鉴权呢?或者不同团队要求不同的鉴权方式呢?你怎么设计才能兼顾统一性和灵活性?
|
||
> 👉 [[02-服务治理/网关鉴权策略]] — 分层鉴权模型详解
|
||
|
||
## 服务间通信
|
||
|
||
### RPC vs RESTful API
|
||
|
||
| 维度 | REST over HTTP/JSON | gRPC (HTTP/2) |
|
||
|------|---------------------|---------------|
|
||
| 性能 | 序列化开销大 | Protocol Buffers,二进制高效 |
|
||
| 人类可读 | URL + JSON 直观 | 需要 Proto 定义辅助理解 |
|
||
| 语言兼容性 | 广泛,任何能发 HTTP 的语言都能用 | 需要代码生成 |
|
||
| 适用场景 | 对外 API、跨团队/跨组织调用 | 内部服务间高频强类型调用 |
|
||
|
||
```go
|
||
// Go 示例:gRPC Protobuf 定义
|
||
// proto/order/v1/order.proto
|
||
syntax = "proto3";
|
||
package order.v1;
|
||
|
||
service OrderService {
|
||
rpc CreateOrder(CreateOrderRequest) returns (CreateOrderResponse);
|
||
rpc GetOrder(GetOrderRequest) returns (Order);
|
||
}
|
||
|
||
message CreateOrderRequest {
|
||
string user_id = 1;
|
||
repeated Item items = 2;
|
||
float total = 3;
|
||
}
|
||
|
||
message Item {
|
||
string product_id = 1;
|
||
int32 quantity = 2;
|
||
}
|
||
```
|
||
|
||
### 同步 vs 异步
|
||
|
||
> [!question] 关键时刻的判断
|
||
> 用户点击"提交订单"后,系统要做:① 创建订单记录 ② 扣减库存 ③ 扣款 ④ 发送短信通知。哪些步骤必须同步完成?哪些可以异步处理?为什么?
|
||
|
||
**答案**:①~③需要立即得到结果或保证一致性,走同步流程;④纯粹的通知类场景完全异步,用消息队列解耦。即使扣款失败,短信也没必要发出。
|
||
|
||
```go
|
||
// 异步消息:Go + RabbitMQ 简易示例
|
||
producer.Publish("orders", []byte(`{
|
||
"orderId": "ORD-2026-001",
|
||
"userId": "user-42",
|
||
"amount": 199.00
|
||
}`))
|
||
|
||
// 消费者监听
|
||
messages := consumer.Consume("order-notifications")
|
||
for msg := range messages {
|
||
sendSms(msg.OrderId) // 不阻塞主流程
|
||
}
|
||
```
|
||
|
||
> [!tip] 选型建议
|
||
> 事件驱动架构(Event-driven)适合「最终一致性」场景;强一致事务需求仍依赖 SAGA/TCC 等分布式事务方案。
|
||
|
||
## 容错模式
|
||
|
||
微服务最大的挑战是 **网络不可靠**。分布式系统的每个远程调用都可能超时、被拒绝、或部分成功。必须提前设计降级和恢复策略。
|
||
|
||
### 重试机制
|
||
|
||
> [!warning] 关键原则
|
||
> 只对 **幂等操作** 重试!POST 创建操作直接重试会导致重复数据。GET、PUT(同参数)适合重试。
|
||
|
||
```go
|
||
// exponential backoff + jitter(防雪崩)
|
||
func retryWithBackoff(ctx context.Context, maxRetries int, fn func() error) error {
|
||
for attempt := uint(0); attempt <= uint(maxRetries); attempt++ {
|
||
if err := fn(); err != nil {
|
||
if attempt == uint(maxRetries) {
|
||
return err
|
||
}
|
||
// jitter: 随机偏移,避免所有客户端同时重试造成二次冲击
|
||
jitter := time.Duration(rand.Int63n(int64(time.Millisecond * 100)))
|
||
wait := time.Duration(math.Pow(2, float64(attempt)))*time.Second + jitter
|
||
select {
|
||
case <-ctx.Done():
|
||
return ctx.Err()
|
||
case <-time.After(wait):
|
||
}
|
||
} else {
|
||
return nil
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
```
|
||
|
||
### 熔断器 (Circuit Breaker)
|
||
|
||
当下游服务频繁失败时,快速失败避免线程堆积雪崩:
|
||
|
||
```mermaid
|
||
stateDiagram-v2
|
||
[*] --> Closed: "正常状态"
|
||
Closed --> Open: "失败率超过阈值"
|
||
Open --> HalfOpen: "等待探测间隔"
|
||
HalfOpen --> Closed: "探测成功"
|
||
HalfOpen --> Open: "探测失败"
|
||
```
|
||
|
||
```go
|
||
// gobreaker 示例
|
||
cb := state.NewCB(state.Settings{
|
||
Name: "UserService",
|
||
MaxElems: 10,
|
||
WaitRetry: 5 * time.Second,
|
||
ReadyToTrip: func(counts Counters) bool {
|
||
return counts.Failures > 5
|
||
},
|
||
})
|
||
|
||
result := cb.Execute(doRequest)
|
||
if result.Err != nil {
|
||
// 熔断打开,立即短路返回错误,不调用下游
|
||
}
|
||
```
|
||
|
||
### 健康检查
|
||
|
||
```mermaid
|
||
sequenceDiagram
|
||
participant LB as 负载均衡器
|
||
participant S1 as 服务实例 A
|
||
participant S2 as 服务实例 B
|
||
|
||
LB->>S1: "GET /health -> 200 OK"
|
||
LB->>S2: "GET /health -> 503 ERROR"
|
||
Note over LB: "标记 S2 不健康,剔除出池"
|
||
LB->>S2: "GET /health -> 200 OK"
|
||
Note over LB: "逐步恢复,重新加入池"
|
||
```
|
||
|
||
- **存活探针 (Liveness)**:判断"进程是否还活着",挂了则重启
|
||
- **就绪探针 (Readiness)**:判断"是否能接收流量",未就绪则剔除负载均衡池
|
||
- ** readiness 比 liveness 更重要**——一个进程活着但数据库连接耗尽时,应该停止接收流量而不是重启
|
||
|
||
> [!summary] 舱壁隔离 (Bulkhead)
|
||
>
|
||
> 为不同下游服务分配独立的线程池/连接池:
|
||
>
|
||
> ```go
|
||
> // poolgroup 示例:为不同服务隔离连接池
|
||
> orderPool := pool.New(10, 100) // 最多10个活跃,上限100
|
||
> userPool := pool.New(5, 50)
|
||
> // 订单服务占满连接不会影响用户服务的调用
|
||
> ```
|
||
>
|
||
> 超时 + 熔断 + 舱壁三者组合,构成了经典的稳定性防护三件套。
|
||
|
||
## 负载均衡
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph "客户端负载均衡 (Client-Side)"
|
||
A[Service A] --> RR[轮询]
|
||
A --> LH[最少连接]
|
||
A --> CH[一致性哈希]
|
||
end
|
||
|
||
subgraph "服务端负载均衡 (Server-Side)"
|
||
B[Service B] --> VIP[[Virtual IP]]
|
||
VIP --> B1[B-1]
|
||
VIP --> B2[B-2]
|
||
VIP --> B3[B-3]
|
||
end
|
||
```
|
||
|
||
| 策略 | 说明 | 适用场景 |
|
||
|------|------|---------|
|
||
| Round Robin | 轮询分配 | 各实例负载相近,请求耗时均匀 |
|
||
| Least Connections | 最少连接优先 | 长连接场景,请求处理时间差异大 |
|
||
| Consistent Hash | 哈希路由,同一 key 始终到同实例 | 有状态缓存、会话绑定场景 |
|
||
| Random | 随机选择 | 最简单但不一定最优 |
|
||
|
||
> [!tip] K8s 中的实践
|
||
> K8s Service 默认 Round Robin;如需更精细的控制,可配合 **Istio VirtualService** 实现基于权重/头部的流量分发。
|
||
|
||
## 配置管理
|
||
|
||
> [!question] 引出配置中心的必要性
|
||
> 假设你的 50 个服务实例都要连同一个数据库。现在需要把数据库密码从 `password1` 改为 `password2`,你会怎么改?登录每台机器改配置文件?滚动重启所有 Pod?还是……有更好的办法?
|
||
|
||
当服务实例数以百计时,手动管理配置是不可想象的。配置中心提供 **集中化、动态化、版本化** 的配置管理能力。
|
||
|
||
### 核心价值
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
Dev["开发/运维"] --> CC[(配置中心)]
|
||
CC -->|热更新推送| S1[Service A]
|
||
CC -->|热更新推送| S2[Service B]
|
||
CC -->|热更新推送| S3[Service C]
|
||
|
||
CC -.->|版本管理| V1[v1.0 历史配置]
|
||
CC -.->|版本管理| V2[v1.1 当前配置]
|
||
```
|
||
|
||
| 能力 | 说明 |
|
||
|------|------|
|
||
| 动态刷新 | 修改配置后立即生效,无需重启 |
|
||
| 环境隔离 | dev / test / prod 配置分离 |
|
||
| 版本管理与回滚 | 每次变更有迹可循,一键回滚 |
|
||
| 权限控制 | 敏感配置(密钥、Token)按角色隔离 |
|
||
|
||
### Nacos Config 示例
|
||
|
||
```go
|
||
// 动态监听配置变更
|
||
configClient, _ := clients.NewConfigClient(value_map.NewValueMap(map[string]any{
|
||
"serverConfig": sc,
|
||
}))
|
||
|
||
content, _ := configClient.GetConfig(config_param.GetConfigParam{
|
||
DataId: "order-service.yaml",
|
||
Group: "DEFAULT_GROUP",
|
||
})
|
||
|
||
// 监听配置变化
|
||
configClient.ListenChange(config_param.ListenChangeParam{
|
||
DataId: "order-service.yaml",
|
||
Group: "DEFAULT_GROUP",
|
||
Callback: func(content string) {
|
||
fmt.Println("配置更新了:", content)
|
||
// 重新加载配置...
|
||
},
|
||
})
|
||
```
|
||
|
||
> [!note] 主流对比
|
||
>
|
||
> - **Nacos Config**:阿里开源,国内首选,兼顾客户端发现+配置管理
|
||
> - **Apollo**:携程开源,配置审核流程完善,适合大型团队
|
||
> - **Spring Cloud Config**:与 Spring 生态深度集成,但需额外 Git/DB 存储后端
|
||
> - **K8s ConfigMap**:原生方案,适合纯 K8s 环境,不支持热更新需配合 Sidecar
|
||
|
||
## 分布式链路追踪
|
||
|
||
> [!question] 定位问题的困难
|
||
> 用户反映下单慢,你的系统由订单、支付、库存、会员 4 个服务串联而成。没有工具的情况下,你要怎么知道是哪个服务拖慢了整体响应时间?
|
||
|
||
单个服务的日志只能告诉你局部信息。分布式链路追踪将一次请求跨越多个服务的完整调用链串起来,形成全局视图。
|
||
|
||
### 核心概念
|
||
|
||
```mermaid
|
||
flowchart TB
|
||
Req["请求 trace_id=abc123"] --> Span1["订单服务 - 20ms"]
|
||
Span1 --> Span2["库存服务 - 80ms"]
|
||
Span1 --> Span3["会员服务 - 15ms"]
|
||
Span2 --> Span4["数据库查询 - 60ms"]
|
||
|
||
style Span2 fill:#ff9999
|
||
```
|
||
|
||
- **Trace**:一次完整请求的调用链
|
||
- **Span**:链路中的一个执行片段(如一次 HTTP 调用、一次 SQL 查询)
|
||
- **Context Propagation**:通过 Header 传递 trace_id/span_id,贯穿整条链路
|
||
|
||
### 主流方案
|
||
|
||
| 方案 | 协议 | 存储后端 | 特点 |
|
||
|------|------|---------|------|
|
||
| **OpenTelemetry** | OTLP | Prometheus/Jaeger/Zipkin | CNCF 标准,厂商中立,未来趋势 |
|
||
| **Jaeger** | Jaeger native | Cassandra/Elasticsearch | Uber 开源,UI 友好 |
|
||
| **SkyWalking** | SkyWalking | MySQL/Elasticsearch | 国产,零侵入 Agent,中文文档完善 |
|
||
|
||
### OpenTelemetry Go 示例
|
||
|
||
```go
|
||
// 初始化 tracer provider
|
||
provider := sdktrace.NewTracerProvider(
|
||
sdktrace.WithBatcherExporter(exporter), // 异步上报,不阻塞
|
||
)
|
||
|
||
tracer := provider.Tracer("order-service")
|
||
|
||
// 在 handler 中创建 span
|
||
func handleOrder(w http.ResponseWriter, r *http.Request) {
|
||
ctx, span := tracer.Start(r.Context(), "handleOrder")
|
||
defer span.End()
|
||
|
||
// 传播 context 到下游
|
||
resp, err := callInventoryService(ctx) // SDK 自动注入 trace context
|
||
if err != nil {
|
||
span.RecordError(err)
|
||
span.SetStatus(codes.Error, err.Error())
|
||
return
|
||
}
|
||
w.Write(resp.Body)
|
||
}
|
||
```
|
||
|
||
> [!tip] 渐进式落地建议
|
||
> 不要试图一开始就采集全部 Span。先从 **核心链路**(下单、支付)入手,采集 rate 设为 100%;稳定后再逐步扩展,日常采样率可降至 5%~10% 以节省存储。
|
||
|
||
## 灰度发布与流量治理
|
||
|
||
> [!question] 如何安全地上线?
|
||
> 你修复了一个紧急 Bug,但这次改动涉及核心支付链路。直接全量发布一旦出问题损失巨大。有没有办法先小范围验证,确认没问题再放量?
|
||
|
||
灰度发布(也叫金丝雀发布)是降低变更风险的核心手段。它让你将新版本逐步推向一小部分用户,观察指标正常后再全量。
|
||
|
||
### 灰度策略
|
||
|
||
```mermaid
|
||
flowchart LR
|
||
subgraph "Canary Release"
|
||
A["1% 流量"] --> B{"指标正常?"}
|
||
B -- 是 --> C["10% 流量"]
|
||
B -- 否 --> D["自动回滚"]
|
||
C --> E{"指标正常?"}
|
||
E -- 是 --> F["50% 流量"]
|
||
E -- 否 --> D
|
||
F --> G{"指标正常?"}
|
||
G -- 是 --> H["100% 全量"]
|
||
G -- 否 --> D
|
||
end
|
||
```
|
||
|
||
### 灰度路由规则
|
||
|
||
```yaml
|
||
# Istio VirtualService 示例
|
||
apiVersion: networking.istio.io/v1beta1
|
||
kind: VirtualService
|
||
metadata:
|
||
name: order-route
|
||
spec:
|
||
hosts: ["order-service"]
|
||
http:
|
||
# 灰度规则:header 携带 canary=true 的用户走 v2
|
||
- match:
|
||
- headers:
|
||
x-canary:
|
||
exact: "true"
|
||
route:
|
||
- destination:
|
||
host: order-service
|
||
subset: v2 # 新版本
|
||
weight: 100
|
||
# 默认:全部走 v1(线上稳定版)
|
||
- route:
|
||
- destination:
|
||
host: order-service
|
||
subset: v1 # 旧版本
|
||
weight: 100
|
||
```
|
||
|
||
### 蓝绿 vs 灰度
|
||
|
||
> [!summary] 两种发布策略对比
|
||
>
|
||
> | 维度 | 蓝绿部署 | 灰度发布 (金丝雀) |
|
||
> |------|---------|-----------------|
|
||
> | 切换方式 | 一次性全切 | 逐步放量 |
|
||
> | 资源消耗 | 双倍(新旧并行) | 初期少量副本 |
|
||
> | 回滚速度 | 秒级(切回即可) | 同样秒级 |
|
||
> | 风险等级 | 中高 | 低 |
|
||
> | 适合场景 | 大型变更、重大版本 | 日常迭代、高风险链路 |
|
||
|
||
> [!tip] 实战经验
|
||
> 无论哪种发布方式,**必须有可量化的观测指标作为决策依据**——错误率、P99 延迟、CPU/内存使用率。不要凭感觉放量,让数据说话。
|
||
|
||
## 关联笔记
|
||
|
||
- [[hzh/MS/01-基础概念]] — 微服务的整体架构认知
|
||
- [[hzh/MS/05-部署运维]] — K8s Service 天然提供负载均衡和服务发现
|
||
- [[hzh/MS/03-数据一致性]] — 微服务下的数据分片与一致性策略
|
||
- [[hzh/MS/04-可观测性]] — 日志、指标、链路追踪的完整体系
|