This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hzh/MS/02-服务治理.md
T

512 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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-可观测性]] — 日志、指标、链路追踪的完整体系