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

17 KiB
Raw Blame History

tags, create time
tags create time
microservice
service-governance
api-gateway
circuit-breaker
service-discovery
distributed-tracing
canary-deployment
2026-04-29 12:02

服务治理

概述

服务治理是微服务架构的"操作系统"——它不直接实现业务逻辑,但决定了系统能否在复杂、高并发、多团队协作的环境中稳定运转。

本文覆盖服务治理的 七大核心能力:

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 个服务实例。手动维护它们的地址列表会带来哪些问题?你能想到几种解决方案?

答案藏在下面每一个章节里——从服务发现到流量治理,每一层都在解决特定维度的复杂度。

服务发现

服务实例的动态变化(扩容、缩容、故障重启)让硬编码地址成为不可能。服务要回答的核心问题是:我怎么找到你?

两种模式

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 服务发现

// 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 是所有外部请求的统一入口,承担以下职责:

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 示例: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 + 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(同参数)适合重试。

// 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)

当下游服务频繁失败时,快速失败避免线程堆积雪崩:

stateDiagram-v2
    [*] --> Closed: "正常状态"
    Closed --> Open: "失败率超过阈值"
    Open --> HalfOpen: "等待探测间隔"
    HalfOpen --> Closed: "探测成功"
    HalfOpen --> Open: "探测失败"
// 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 {
    // 熔断打开,立即短路返回错误,不调用下游
}

健康检查

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)

为不同下游服务分配独立的线程池/连接池:

// poolgroup 示例:为不同服务隔离连接池
orderPool := pool.New(10, 100) // 最多10个活跃,上限100
userPool := pool.New(5, 50)
// 订单服务占满连接不会影响用户服务的调用

超时 + 熔断 + 舱壁三者组合,构成了经典的稳定性防护三件套。

负载均衡

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?还是……有更好的办法?

当服务实例数以百计时,手动管理配置是不可想象的。配置中心提供 集中化、动态化、版本化 的配置管理能力。

核心价值

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 示例

// 动态监听配置变更
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 个服务串联而成。没有工具的情况下,你要怎么知道是哪个服务拖慢了整体响应时间?

单个服务的日志只能告诉你局部信息。分布式链路追踪将一次请求跨越多个服务的完整调用链串起来,形成全局视图。

核心概念

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 示例

// 初始化 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,但这次改动涉及核心支付链路。直接全量发布一旦出问题损失巨大。有没有办法先小范围验证,确认没问题再放量?

灰度发布(也叫金丝雀发布)是降低变更风险的核心手段。它让你将新版本逐步推向一小部分用户,观察指标正常后再全量。

灰度策略

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

灰度路由规则

# 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/内存使用率。不要凭感觉放量,让数据说话。

关联笔记