vault backup: 2026-04-29 20:39:17
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
---
|
||||
tags: [microservice, architecture]
|
||||
create time: 2026-04-29 12:30
|
||||
---
|
||||
|
||||
# 微服务基础概念
|
||||
|
||||
## 概述
|
||||
|
||||
本文阐述微服务架构的核心定义、设计原则与常见误区,帮助建立正确的认知框架。
|
||||
|
||||
## 什么是微服务
|
||||
|
||||
> [!definition] 核心定义
|
||||
> **微服务**(Microservices)是一种将单一应用拆分为一组**小服务**的架构风格。每个服务运行在独立进程中,通过轻量级通信机制协作,且**各自拥有独立的数据库**。
|
||||
|
||||
关键特征:
|
||||
|
||||
| 特征 | 说明 |
|
||||
|------|------|
|
||||
| 独立部署 | 每个服务可以独立构建、测试和发布 |
|
||||
| 去中心化 | 数据管理、技术选型由各团队自主决策 |
|
||||
| 容错性 | 服务故障不影响整个系统 |
|
||||
| 组织对齐 | 按业务域划分,与 Conway 定律呼应 |
|
||||
|
||||
### 单体 vs 微服务
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph Monolith["单体架构"]
|
||||
UI["Web UI"]
|
||||
API["API Layer"]
|
||||
Biz["Business Logic"]
|
||||
DB["数据库"]
|
||||
UI --> API --> Biz --> DB
|
||||
end
|
||||
|
||||
subgraph Microservices["微服务架构"]
|
||||
GW["API Gateway"]
|
||||
S1["Service A<br/>+ Database"]
|
||||
S2["Service B<br/>+ Database"]
|
||||
S3["Service C<br/>+ Database"]
|
||||
GW --> S1
|
||||
GW --> S2
|
||||
GW --> S3
|
||||
S2 -.->|RPC/MQ| S1
|
||||
end
|
||||
```
|
||||
|
||||
> [!question] 思考
|
||||
> 既然微服务有这么多好处,为什么不是所有公司都迁移到微服务?什么时候单体反而更合适?
|
||||
|
||||
**答案线索**:微服务引入的是**分布式复杂度**。网络延迟、数据一致性、运维成本全部上来了。对于小型团队或小规模应用,单体更简单高效。
|
||||
|
||||
## 拆分原则:DDD 与限界上下文
|
||||
|
||||
微服务拆分的核心思想来自 **领域驱动设计 (DDD)** —— 按 **限界上下文 (Bounded Context)** 划分。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
UC["用户中心"]
|
||||
OS["订单服务"]
|
||||
PS["支付服务"]
|
||||
IS["库存服务"]
|
||||
UC -->|查询/注册| OS
|
||||
OS -->|创建支付| PS
|
||||
OS -->|扣减库存| IS
|
||||
PS -->|支付回调| OS
|
||||
IS -->|库存确认| OS
|
||||
```
|
||||
|
||||
> [!tip] 限界上下文不是代码边界,而是语义边界
|
||||
> 同一个词在不同上下文中可能有完全不同的含义。比如 "商品" 在电商场景中是一套完整对象,但在物流场景中可能只是一个包裹编号。明确上下文是 DDD 的第一步。
|
||||
|
||||
> [!warning] 反模式
|
||||
> - ❌ **按技术层拆分**:Controller 层一个服务、Service 层一个服务 — 这不是微服务,是"分布式单体"
|
||||
> - ✅ **按业务域拆分**:每个服务对应一个业务能力边界
|
||||
|
||||
## 核心设计原则
|
||||
|
||||
> [!summary] SOLID + CAP 的组合拳
|
||||
>
|
||||
> | 原则 | 含义 | 微服务场景 |
|
||||
> |------|------|-----------|
|
||||
> | 单一职责 | 一个服务只做一件事 | 订单服务只管订单生命周期 |
|
||||
> | 高内聚低耦合 | 内部紧密,外部松散 | 服务间通过接口通信,不共享代码 |
|
||||
> | 最终一致性 | 允许短暂不一致 | 分布式环境放弃强一致性,换取可用性 |
|
||||
> | 独立失败 | 故障隔离 | 单个服务宕机不影响全局 |
|
||||
|
||||
## 何时不应该用微服务
|
||||
|
||||
> [!failure] 拒绝伪需求
|
||||
>
|
||||
> - 团队小于 10 人,维护单个应用就够
|
||||
> - 项目处于快速原型阶段,需求频繁变更
|
||||
> - 团队成员缺乏分布式系统设计经验
|
||||
> - 已有大型单体正在稳定运行
|
||||
|
||||
**记住**:架构没有银弹。微服务是为了解决**特定规模下的组织和工程问题**而生的。先问自己:"我的痛点是什么?"再决定是否值得付出代价。
|
||||
|
||||
> [!note] 后续延伸
|
||||
>
|
||||
> - [[02-服务治理]] — 服务间通信、负载均衡、熔断降级等治理机制
|
||||
> - [[03-数据一致性]] — 独立数据库架构下的分布式事务方案
|
||||
> - [[04-可观测性]] — 日志、指标、链路追踪三支柱
|
||||
> - [[05-部署运维]] — 容器化编排、灰度发布、弹性伸缩
|
||||
@@ -0,0 +1,510 @@
|
||||
---
|
||||
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] 权衡题
|
||||
> 有人说:"鉴权放到网关统一做,避免每个服务重复写。"但如果某个内部服务不需要鉴权呢?或者不同团队要求不同的鉴权方式呢?你怎么设计才能兼顾统一性和灵活性?
|
||||
|
||||
## 服务间通信
|
||||
|
||||
### 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-可观测性]] — 日志、指标、链路追踪的完整体系
|
||||
@@ -0,0 +1,217 @@
|
||||
---
|
||||
tags: [microservice, distributed-system, database, data-consistency, saga-pattern, tcc, idempotency, outbox-pattern]
|
||||
create time: 2026-04-29 12:03
|
||||
---
|
||||
|
||||
# 数据一致性
|
||||
|
||||
## 概述
|
||||
|
||||
微服务的核心设计原则是 **"每个服务拥有独立数据库"**,这带来了分布式事务和数据一致性的经典难题。本文梳理主要解决方案及其取舍。
|
||||
|
||||
## 为什么不能共享数据库
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
S1[订单服务 DB]
|
||||
S2[库存服务 DB]
|
||||
|
||||
subgraph BAD["反模式:共享数据库"]
|
||||
S1 --- SHARED[(共享 DB)]
|
||||
S2 --- SHARED
|
||||
end
|
||||
```
|
||||
|
||||
> [!failure] 反模式警告
|
||||
> 如果两个服务连接同一个数据库的同一张表,它们就不再是独立的微服务——你得到的是**分布式单体**。服务可以随意互相查询彼此的数据,失去了边界和自治性。
|
||||
|
||||
## 分布式事务方案概览
|
||||
|
||||
| 方案 | 一致性级别 | 性能 | 复杂度 | 适用场景 |
|
||||
|------|-----------|------|--------|---------|
|
||||
| 本地事务 + 事件通知 | 最终一致 | 高 | 低 | 绝大多数场景 |
|
||||
| Saga 模式 | 最终一致 | 中 | 中 | 长流程业务 |
|
||||
| TCC | 强最终一致 | 中 | 高 | 对一致性要求较高的场景 |
|
||||
| AT 模式 (Seata) | 伪强一致 | 中低 | 低 | 不想改业务代码时 |
|
||||
|
||||
> [!info] 选择策略
|
||||
> 先默认用 **本地事务 + 异步事件**(最简单、最高效),只有在业务明确需要 Saga 或 TCC 时才升级。
|
||||
|
||||
## 方案一:本地事务 + 消息队列
|
||||
|
||||
这是**最常用**的方案,核心思想是将"数据变更 + 发消息"合并到一个本地事务中。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as 用户
|
||||
participant O as 订单服务
|
||||
participant MQ as 消息队列
|
||||
participant I as 库存服务
|
||||
|
||||
U->>O: 创建订单
|
||||
O->>O: BEGIN 事务
|
||||
O->>O: 插入订单记录
|
||||
O->>MQ: 发送订单创建消息
|
||||
O->>O: COMMIT 事务
|
||||
MQ-->>I: 投递消息
|
||||
I->>I: 扣减库存
|
||||
I-->>O: 返回结果(可选)
|
||||
```
|
||||
|
||||
### 实战:电商订单创建流程
|
||||
|
||||
以「用户下单」为例,这个流程涉及 **订单服务**(创建订单)和 **库存服务**(扣减库存),是典型的跨服务数据一致性问题:
|
||||
|
||||
| 步骤 | 动作 | 一致性保障点 |
|
||||
|------|------|-------------|
|
||||
| 1 | 用户点击"提交订单" | 前端防重复提交(按钮置灰 / Token 机制) |
|
||||
| 2 | 订单服务写入订单表 + 发送 `order.created` 消息 | **事务边界**:两件事必须在同一个本地事务中完成 |
|
||||
| 3 | 消息队列投递给库存服务 | 保证消息至少一次投递(At-Least-Once) |
|
||||
| 4 | 库存服务扣减库存 | **幂等性**:防止消息重投导致库存被扣两次 |
|
||||
| 5 | 库存不足时回滚订单 | 通过 Sagas 补偿:已创建的订单标记为"取消" |
|
||||
|
||||
> [!question] 思考
|
||||
> 如果步骤 2 的数据库写成功了,但 MQ 发消息失败了,会发生什么?用户看到下单成功但实际上库存没被锁定。这说明为什么必须用事务消息或本地消息表,而不是直接调 MQ API。
|
||||
|
||||
---
|
||||
|
||||
### 如何保证"事务内既写库又发消息"?
|
||||
|
||||
**方案 A:事务消息**(推荐)
|
||||
- RocketMQ /阿里云 MSE 原生支持事务消息
|
||||
- MQ 回查机制确保消息不丢
|
||||
|
||||
**方案 B:本地消息表**
|
||||
- 在业务库里建一张 `outbox` 表
|
||||
- 业务事务同时写入业务数据和消息记录
|
||||
- 后台定时任务扫描未发送的消息推送出去
|
||||
|
||||
```go
|
||||
// 本地消息表方案示意
|
||||
tx, _ := db.Begin()
|
||||
// 1. 业务操作
|
||||
tx.Exec("UPDATE accounts SET balance = balance - ? WHERE id = ?", amount, fromID)
|
||||
// 2. 记录待发送消息(带状态字段)
|
||||
tx.Exec("INSERT INTO outbox (topic, payload, status, created_at) VALUES (?, ?, 'pending', NOW())", "order.created", jsonPayload)
|
||||
tx.Commit()
|
||||
// 后台进程轮询 outbox 表中 status='pending' 的记录,发送到 MQ 后更新为 'sent'
|
||||
```
|
||||
|
||||
#### Outbox 表建表示例
|
||||
|
||||
```sql
|
||||
CREATE TABLE outbox (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
topic VARCHAR(255) NOT NULL, -- 消息主题
|
||||
payload JSONB NOT NULL, -- 消息体
|
||||
status VARCHAR(20) NOT NULL DEFAULT 'pending', -- pending / sent / failed
|
||||
error_msg TEXT, -- 失败原因
|
||||
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
|
||||
sent_at TIMESTAMP
|
||||
);
|
||||
|
||||
-- 加速定时扫描查询
|
||||
CREATE INDEX idx_outbox_pending ON outbox (status, created_at) WHERE status = 'pending';
|
||||
```
|
||||
|
||||
### 消费幂等性
|
||||
|
||||
消息可能重复投递(网络超时、MQ 重投),消费者必须做到**幂等**——处理一次和处理多次的结果完全相同。
|
||||
|
||||
**三种常见策略:**
|
||||
|
||||
| 策略 | 实现方式 | 适用场景 |
|
||||
|------|---------|---------|
|
||||
| 数据库唯一约束 | 用 `msg_id` 或业务主键做 `UNIQUE` | 最可靠,推荐首选 |
|
||||
| Redis 去重键 | SET 一个 TTL Key(如 `dedup:{msg_id}` 过期 24h) | 高吞吐场景,需考虑 Redis 可用性 |
|
||||
| 乐观锁版本控制 | UPDATE 时加 `WHERE version = old_version` | 适用于金额调整类操作 |
|
||||
|
||||
```go
|
||||
// 推荐方案:利用 UNIQUE 约束做幂等保障
|
||||
_, err := db.Exec(`
|
||||
INSERT INTO order_events (msg_id, order_id, action, amount)
|
||||
VALUES ($1, $2, $3, $4)
|
||||
ON CONFLICT (msg_id) DO NOTHING -- 重复到达时静默忽略
|
||||
`, msgID, orderID, action, amount)
|
||||
if err != nil {
|
||||
// 其他错误才需要处理,UNIQUE 冲突直接忽略
|
||||
if !isUniqueViolation(err) {
|
||||
log.Error("process message failed", err)
|
||||
}
|
||||
}
|
||||
// 执行业务逻辑...
|
||||
```
|
||||
|
||||
> [!tip] 设计要点
|
||||
> `ON CONFLICT DO NOTHING` 比先查后插更可靠——它消除了竞态窗口。即使用两个实例同时收到同一条消息,只会有一个成功写入。
|
||||
|
||||
> [!question] 思考
|
||||
> 为什么要在生产者侧保证消息可靠投递,而不是靠消费者反复拉取重试?
|
||||
|
||||
## 方案二:Saga 模式
|
||||
|
||||
Saga 适用于**跨多个服务的长流程操作**,将大事务拆成一系列本地小事务,每个步骤都有对应的补偿操作。
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
S1[① 创建订单<br/>→ 取消订单]
|
||||
S2[② 扣减库存<br/>→ 恢复库存]
|
||||
S3[③ 支付扣款<br/>→ 退款]
|
||||
S4[④ 发货<br/>→ 无补偿]
|
||||
|
||||
S1 --> S2 --> S3 --> S4
|
||||
```
|
||||
|
||||
**编排式 vs 编舞式**:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph ORCHESTRATION["编排式 — 中心协调器"]
|
||||
CO[Coordinator] --> S1[OrderSvc]
|
||||
CO --> S2[InventorySvc]
|
||||
CO --> S3[PaymentSvc]
|
||||
end
|
||||
|
||||
subgraph CHOREOGRAPHY["编舞式 — 事件驱动"]
|
||||
E1[订单创建] --> S11[订单服务]
|
||||
S11 --> E2[库存不足事件]
|
||||
E2 --> S12[库存服务]
|
||||
S12 --> E3[扣减完成事件]
|
||||
E3 --> S13[支付服务]
|
||||
end
|
||||
```
|
||||
|
||||
| | 编排式 | 编舞式 |
|
||||
|---|--------|--------|
|
||||
| 控制流 | 中心化 Coordinator | 各服务通过事件自发响应 |
|
||||
| 可观测性 | ✅ 集中管理 | ❌ 流程散布在各服务 |
|
||||
| 耦合度 | 依赖 Coordinator | 服务间仅感知事件 |
|
||||
|
||||
## 方案三:TCC 与 AT 模式
|
||||
|
||||
> [!abstract] 进阶了解
|
||||
>
|
||||
> **TCC** (Try-Confirm-Cancel):每个事务步骤实现三个接口。适合对一致性要求严格且能承受开发成本的业务。
|
||||
>
|
||||
> **AT 模式** (Seata):框架层自动处理两阶段提交,对业务透明。本质上是基于 undo_log 的增强型 XA,性能低于 Saga。
|
||||
|
||||
## 总结选型指南
|
||||
|
||||
> [!summary] 决策树
|
||||
>
|
||||
> ```mermaid
|
||||
> graph TD
|
||||
> A["跨几个服务"] -->|"1-2"| B["本地事务 + 事件通知"]
|
||||
> A -->|"3+"| C["Saga 模式"]
|
||||
> B -->|"强"| D["加幂等,必要时上 TCC"]
|
||||
> B -->|"弱"| E["本地事务已够"]
|
||||
> C -->|"不能"| F["考虑 AT 模式"]
|
||||
> C -->|"能"| G["自行实现补偿"]
|
||||
> ```
|
||||
>
|
||||
> **经验法则**:80% 的场景,本地事务 + MQ 就足够了。先别急着上复杂方案。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[01-基础概念]] — 微服务拆分与独立数据库原则
|
||||
- [[02-服务治理]] — 服务调用链路上的超时、重试、熔断
|
||||
@@ -0,0 +1,446 @@
|
||||
---
|
||||
tags: [microservice, observability, monitoring, tracing]
|
||||
create time: 2026-04-29 12:04
|
||||
---
|
||||
|
||||
# 可观测性
|
||||
|
||||
## 概述
|
||||
|
||||
微服务架构下,一个请求可能穿越十几个甚至上百个服务。**排查问题就像在大海捞针**。可观测性通过三大支柱(Metrics、Logging、Tracing)让系统行为变得"可见"。
|
||||
|
||||
> [!question] 核心思考
|
||||
> 单体应用出问题,SSH 到服务器上 `tail -f` 日志就能定位。但当你有 50 个服务、每个 3 个副本,日志散落在不同容器里——你还能用同样的方式排障吗?如果不能,什么体系能替代它?
|
||||
|
||||
### 为什么需要独立的"可观测性"体系?
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph MONO["单体系统"]
|
||||
S[Service] --> DB[(DB)]
|
||||
LOG["同一份日志<br/>一目了然"]
|
||||
S --> LOG
|
||||
end
|
||||
|
||||
subgraph MICRO["微服务系统"]
|
||||
REQ("Request") --> GW["Gateway"]
|
||||
GW --> O1["Order Svc"]
|
||||
GW --> U1["User Svc"]
|
||||
O1 --> PAY["Payment Svc"]
|
||||
O1 --> INV["Inventory Svc"]
|
||||
PAY --> DB1[(DB)]
|
||||
INV --> DB2[(DB)]
|
||||
|
||||
style O1 fill:#faa
|
||||
style U1 fill:#faa
|
||||
style PAY fill:#faa
|
||||
style INV fill:#faa
|
||||
end
|
||||
|
||||
NOTE["问题:日志分散、链路断裂<br/>传统监控无法回答'这个请求经历了什么'"]
|
||||
MICRO --> NOTE
|
||||
```
|
||||
|
||||
可观测性与监控的本质区别:**监控系统告诉你"出事了"**(已知未知),**可观测性系统让你去探究"为什么会出事"**(未知未知)。
|
||||
|
||||
### SLO 与 Error Budget
|
||||
|
||||
可观测性的终极目标不是收集数据,而是**支撑决策**。SLO(Service Level Objective)将技术指标映射到用户体验。
|
||||
|
||||
> [!summary] SLI / SLO / SLA 三层模型
|
||||
>
|
||||
> | 层级 | 含义 | 示例 |
|
||||
> |------|------|------|
|
||||
> | **SLI** (Indicator) | 实际测量的指标 | P99 延迟 = 120ms |
|
||||
> | **SLO** (Objective) | 内部目标值 | P99 延迟 < 200ms(99.9% 的请求) |
|
||||
> | **SLA** (Agreement) | 对外的契约承诺 | 可用性 ≥ 99.95%,否则赔偿 |
|
||||
|
||||
> [!tip] Error Budget(错误预算)
|
||||
>
|
||||
> 如果 SLO 是 99.9%,那么每月允许的错误时间 = 30 × 24 × 60 × 0.1% ≈ **43 分钟**。
|
||||
>
|
||||
> - **预算充足时**:可以大胆发布新功能、尝试激进方案
|
||||
> - **预算耗尽时**:冻结非功能性变更,专注稳定性修复
|
||||
>
|
||||
> 这构成了"创新"和"稳定"之间的动态平衡机制。
|
||||
|
||||
## 三大支柱
|
||||
|
||||
| 支柱 | 回答的问题 | 典型工具 |
|
||||
|------|-----------|---------|
|
||||
| **Metrics** | 系统现在健康吗? — 趋势、告警 | Prometheus + Grafana |
|
||||
| **Logging** | 具体发生了什么? — 历史回溯 | ELK / Loki |
|
||||
| **Tracing** | 请求在哪一步慢了/失败了? — 链路追踪 | Jaeger / Zipkin |
|
||||
|
||||
### Metrics:系统的脉搏
|
||||
|
||||
Metrics 回答的问题是:**系统现在健康吗?**——通过聚合后的数值揭示趋势。
|
||||
|
||||
> [!summary] RED 和 USE 方法
|
||||
>
|
||||
> | 方法 | 适用于 | 指标 |
|
||||
> |------|--------|------|
|
||||
> | **RED** (Rate, Errors, Duration) | 有状态服务(API) | QPS、错误率、响应时间 P99 |
|
||||
> | **USE** (Utilization, Saturation, Errors) | 基础设施(CPU/内存/网络) | 使用率、饱和度、错误数 |
|
||||
|
||||
核心指标类型:
|
||||
|
||||
- **Counter(计数器)**:只增不减,如 `http_requests_total`。适合统计请求总量、错误总数
|
||||
- **Gauge(仪表盘)**:可增可减,如 `queue_depth`、在线用户数
|
||||
- **Histogram(直方图)**:样本分布,自动分桶统计,如请求延迟的 P50/P90/P99
|
||||
- **Summary(摘要)**:类似 Histogram,但客户端计算分位数(Go SDK 默认)
|
||||
|
||||
```go
|
||||
// Go 示例:定义并注册 Prometheus 指标
|
||||
var (
|
||||
// Counter: HTTP 请求总数,按 method 和 status 标签分类
|
||||
httpRequestsTotal = prometheus.NewCounterVec(
|
||||
prometheus.CounterOpts{
|
||||
Name: "http_requests_total",
|
||||
Help: "Total HTTP requests",
|
||||
},
|
||||
[]string{"method", "status"},
|
||||
)
|
||||
|
||||
// Histogram: API 响应延迟分布
|
||||
apiLatency = prometheus.NewHistogramVec(
|
||||
prometheus.HistogramOpts{
|
||||
Name: "api_latency_seconds",
|
||||
Help: "API latency distribution",
|
||||
Buckets: prometheus.DefBuckets, // [0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10]
|
||||
},
|
||||
[]string{"endpoint"},
|
||||
)
|
||||
)
|
||||
|
||||
func init() {
|
||||
prometheus.MustRegister(httpRequestsTotal, apiLatency)
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 解读提示
|
||||
> Counter 用于统计"发生了多少",Histogram 用于理解"有多慢"。一个完整的告警面板通常同时展示两者:QPS 突降 + 延迟飙升 = 大概率出事了。
|
||||
|
||||
在 Grafana 中的典型视图:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph GRAFANA["Grafana Dashboard"]
|
||||
G1["QPS<br/>[Counter]"] -->|关联分析| G2["P99 Latency<br/>[Histogram]"]
|
||||
G3["错误率<br/>[Counter %]"] -->|关联分析| G2
|
||||
G2 --> G4["告警规则<br/>阈值触发"]
|
||||
end
|
||||
|
||||
GRAFANA -.->|从 Prometheus 拉取| PROM[(Prometheus)]
|
||||
```
|
||||
|
||||
### Logging:集中化日志
|
||||
|
||||
Logging 回答的问题是:**具体发生了什么?**——通过原始事件记录提供上下文。
|
||||
|
||||
- **所有服务的日志必须集中收集**,不能散落在各台机器上
|
||||
- 日志格式推荐 JSON,便于解析
|
||||
- 每条日志必须携带 `trace_id`,与 Tracing 串联
|
||||
|
||||
```json
|
||||
{
|
||||
"timestamp": "2026-04-29T12:00:00Z",
|
||||
"level": "ERROR",
|
||||
"service": "order-service",
|
||||
"trace_id": "abc-123-def",
|
||||
"span_id": "span-456",
|
||||
"msg": "failed to connect payment service",
|
||||
"caller": "payment/client.go:42",
|
||||
"cost_ms": 30000
|
||||
}
|
||||
```
|
||||
|
||||
#### 结构化日志的核心字段
|
||||
|
||||
> [!note] 最小集合(必选)
|
||||
>
|
||||
> - `timestamp` — ISO 8601 格式,统一时区 UTC
|
||||
> - `level` — TRACE / DEBUG / INFO / WARN / ERROR / FATAL
|
||||
> - `service` — 微服务名称
|
||||
> - `trace_id` — 链路追踪 ID,用于跨服务关联
|
||||
> - `msg` — 人类可读的消息描述
|
||||
|
||||
> [!warning] 日志最佳实践
|
||||
>
|
||||
> - **不要记录敏感信息**:密码、Token、身份证号等绝不能出现在日志中
|
||||
> - **INFO 级记录关键业务事件**:下单成功、支付回调——这些是排查业务问题的主线
|
||||
> - **WARN 级记录异常但不致命**:重试了一次、降级走了备用方案
|
||||
> - **ERROR 级必须有 trace_id 和错误堆栈**,否则毫无价值
|
||||
|
||||
#### 日志采集管线
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
APP[应用容器] -->|stdout/stderr| LO[FLUENTD / Filebeat]
|
||||
LO -->|TCP/Lumberjack| LO2[(Logstash / Loki)]
|
||||
LO2 -->|解析 & 过滤| PIPE[Pipeline]
|
||||
PIPE -->|写入| ES[(Elasticsearch)]
|
||||
PIPE -->|跳转| GRAF[Grafana / Kibana]
|
||||
|
||||
style PIPE fill:#ff9
|
||||
```
|
||||
|
||||
**常见方案对比**:
|
||||
|
||||
| 方案 | 存储 | 查询能力 | 适用场景 |
|
||||
|------|------|---------|---------|
|
||||
| ELK (Elasticsearch) | ES 集群 | 全文检索强大,灵活度高 | 日志量大、需要复杂分析的团队 |
|
||||
| Loki + Grafana | 对象存储 (S3) | 对标 LogQL,轻量高效 | 已经在用 Grafana 的团队 |
|
||||
| CloudWatch / SLS | 云厂商托管 | 开箱即用,成本较高 | 全云原生环境 |
|
||||
|
||||
> [!tip] 日志采样策略
|
||||
>
|
||||
> 生产环境中并非所有日志都要采集。**正常路径采 1%,异常路径全量**。这既能大幅降低存储成本,又能在出问题时有足够的调试数据可用。采样逻辑建议写在日志库中,而非业务代码里。
|
||||
|
||||
### Tracing:分布式链路追踪
|
||||
|
||||
Tracing 回答的问题是:**请求在哪一步慢了 / 失败了?**——通过完整的调用链路定位瓶颈和故障。
|
||||
|
||||
一次请求在多个服务中的完整调用路径:
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
RID["Request<br/>trace_id: abc-123"]
|
||||
|
||||
subgraph SPANS["调用链 (Trace)"]
|
||||
direction TB
|
||||
S1["Span #1<br/>API Gateway<br/>5ms"]
|
||||
S2["Span #2<br/>Order Service<br/>70ms"]
|
||||
S3["Span #3<br/>Payment RPC<br/>160ms"]
|
||||
S4["Span #4<br/>Inventory RPC<br/>70ms"]
|
||||
|
||||
S1 --> S2 --> S3
|
||||
S2 --> S4
|
||||
end
|
||||
|
||||
RID --> S1
|
||||
style S3 fill:#ff9999
|
||||
|
||||
note["Span #3 耗时最长 → Payment 是瓶颈"]
|
||||
S3 -.-> note
|
||||
```
|
||||
|
||||
> [!tip] 关键实践
|
||||
> - 确保 `trace_id` 在整个调用链中传递(HTTP Header 或 MQ Message Header)
|
||||
> - 采样策略:**全量采集开发环境**,**生产环境按百分比采样**(避免存储爆炸)
|
||||
> - 重点关注的 Span 标签:HTTP method、status code、database query
|
||||
> - **Span 不要嵌套过深**,一个 HTTP 调用或 DB 查询对应一个 Span 即可
|
||||
|
||||
#### Context Propagation(上下文传播)
|
||||
|
||||
trace_id 如何从上游服务传递到下游?
|
||||
|
||||
```go
|
||||
// 1. 入站:从 HTTP Header 提取 trace context
|
||||
func Middleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// OpenTelemetry SDK 自动从 Header 恢复 span context
|
||||
ctx := propagator.Extract(r.Context(), headerReader{r.Header})
|
||||
|
||||
// 2. 创建新的 span
|
||||
ctx, span := tracer.Start(ctx, "handleOrder")
|
||||
defer span.End()
|
||||
|
||||
// 3. 将 trace context 注入到新请求的 Header 中
|
||||
r = r.WithContext(ctx)
|
||||
propagator.Inject(ctx, headerWriter{r.Header})
|
||||
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
```json
|
||||
// HTTP Header 中实际传递的字段 (W3C Trace Context)
|
||||
{
|
||||
"traceparent": "00-abc123def456...-789ghi012jkl-01",
|
||||
"tracestate": "congo=t61rcWkgMzE"
|
||||
}
|
||||
```
|
||||
|
||||
> [!note] W3C Trace Context 标准
|
||||
>
|
||||
> `traceparent` 格式:`version-trace_id-span_id-flags`,如 `00-{32位trace_id}-{16位span_id}-{2位flags}`。
|
||||
> 这是 W3C 推荐的标准,被 OpenTelemetry、Jaeger、Zipkin 等主流方案统一支持。**无需再自定义 Header**。
|
||||
|
||||
## 告警设计原则
|
||||
|
||||
### 告警疲劳是常态
|
||||
|
||||
> [!warning] 核心问题
|
||||
>
|
||||
> 如果一个团队每天收到 200 条告警,其中 195 条是误报或无需处理,工程师会对剩下的 5 条真正重要的告警产生"脱敏"。这就是 **告警疲劳**。
|
||||
|
||||
- **告警要能 actionable**:收到告警后知道该做什么,否则不要告
|
||||
- **区分级别**:P0(电话叫醒)vs P1(工单处理)vs P2(次日处理)
|
||||
- **基于 SLO 告警**:不是 CPU > 80% 就告警,而是用户感知到延迟增加时才告
|
||||
- **降噪**:同一个根因可能触发连锁告警,需要抑制机制
|
||||
|
||||
### 告警层级设计
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph L1["L1: 页面上的用户受损"]
|
||||
A1["HTTP 5xx 错误率 > 1%"]
|
||||
A2["核心接口 P99 > 2s"]
|
||||
end
|
||||
|
||||
subgraph L2["L2: 系统健康度下降"]
|
||||
B1["单个服务错误率 > 5%"]
|
||||
B2["下游依赖超时率升高"]
|
||||
end
|
||||
|
||||
subgraph L3["L3: 资源预警"]
|
||||
C1["磁盘使用 > 70%"]
|
||||
C2["内存使用 > 80%"]
|
||||
end
|
||||
|
||||
A1 -->|P0 - 电话 + IM| ONCALL[On-Call 值班]
|
||||
A2 -->|P0 - 电话 + IM| ONCALL
|
||||
B1 -->|P1 - IM 通知| OPS[运维群]
|
||||
B2 -->|P1 - IM 通知| OPS
|
||||
C1 -->|P2 - 日报汇总| TEAM[团队看板]
|
||||
C2 -->|P2 - 日报汇总| TEAM
|
||||
```
|
||||
|
||||
> [!tip] 告警规则编写清单
|
||||
>
|
||||
> 每条告警规则都应该能回答以下问题:
|
||||
> 1. **什么问题?** — 清晰描述故障现象
|
||||
> 2. **谁负责?** — 指定明确的责任人 / On-Call
|
||||
> 3. **怎么修复?** — 链接到 Runbook / 处置文档
|
||||
> 4. **多久升级?** — P1 无人响应则自动升级到上级
|
||||
|
||||
## OpenTelemetry:统一的可观测性标准
|
||||
|
||||
过去,Metrics、Logging、Tracing 各用各的工具链,数据割裂在完全不同的系统中。**OpenTelemetry (OTel)** 试图解决这个问题。
|
||||
|
||||
> [!summary] OTel 的核心思想
|
||||
>
|
||||
> 一套 SDK → 三种信号(Metrics、Logs、Traces)→ 任意后端(Jaeger、Prometheus、ELK...)
|
||||
>
|
||||
> 这意味着:**你不需要在代码里为不同后端写多套采集逻辑**。换一个后端只需要改配置,不改代码。
|
||||
|
||||
### OTel Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
subgraph APP["你的应用"]
|
||||
OTEL_SDK["OpenTelemetry SDK<br/>auto-instrumentation Agent"]
|
||||
end
|
||||
|
||||
OTEL_SDK -->|SDK 自动采集| COLLECTOR[(OpenTelemetry Collector)]
|
||||
|
||||
COLLECTOR -->|OTLP| JAEGER[(Jaeger<br/>Tracing)]
|
||||
COLLECTOR -->|OTLP| PROM[(Prometheus<br/>Metrics)]
|
||||
COLLECTOR -->|OTLP| LOKI[(Loki<br/>Logging)]
|
||||
|
||||
style OTEL_SDK fill:#ff9
|
||||
```
|
||||
|
||||
> [!note] 为什么需要 Collector?
|
||||
>
|
||||
> Collector 是一个独立的服务进程,承担三个职责:
|
||||
> 1. **接收**:从应用的 SDK 接收数据
|
||||
> 2. **处理**:过滤、采样、富化(添加额外标签)
|
||||
> 3. **导出**:将数据转发到不同的后端存储
|
||||
>
|
||||
> 这样你的应用只关心"发送数据",Collector 负责"怎么存、存到哪里"。两者解耦。
|
||||
|
||||
### Go 中接入 OpenTelemetry
|
||||
|
||||
```go
|
||||
// 1. 初始化 TracerProvider(Tracing)
|
||||
provider := sdktrace.NewTracerProvider(
|
||||
sdktrace.WithBatcherExporter(exporter), // 异步批量上报,不阻塞业务
|
||||
)
|
||||
defer provider.Shutdown(context.Background())
|
||||
|
||||
tracer := provider.Tracer("order-service")
|
||||
|
||||
// 2. 在 handler 中创建 span
|
||||
func handleOrder(w http.ResponseWriter, r *http.Request) {
|
||||
ctx, span := tracer.Start(r.Context(), "handleOrder")
|
||||
defer span.End()
|
||||
|
||||
// 设置丰富的 Span 属性(供查询和过滤)
|
||||
span.SetAttributes(
|
||||
attribute.String("http.method", r.Method),
|
||||
attribute.Int("http.status_code", 200),
|
||||
attribute.String("user.id", userID),
|
||||
)
|
||||
|
||||
// 调用下游服务(trace context 自动传播)
|
||||
resp, err := callPayment(ctx)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> [!tip] 渐进式落地路线
|
||||
>
|
||||
> 1. **第一步**:先接 Tracing(价值最大、投入最小),覆盖核心链路
|
||||
> 2. **第二步**:加 Metrics(Prometheus + Grafana),建立基础监控
|
||||
> 3. **第三步**:集中 Logging(Loki / ELK),与 trace_id 关联
|
||||
> 4. **第四步**:全量上 OTel Collector,统一管理三件套
|
||||
>
|
||||
> 不要试图一步到位——Tracing 的 ROI 最高,先从这里开始。
|
||||
|
||||
## Dashboard 与 On-Call 实践
|
||||
|
||||
收集了这么多数据,下一步是怎么用。
|
||||
|
||||
### Service Health Dashboard
|
||||
|
||||
一个优秀的 Dashboard 应该让任何团队成员在 **30 秒内**了解服务的整体状态:
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
subgraph DASH["Service Health Dashboard"]
|
||||
ROW1["🔴 可用性 & 错误率 — 第一眼判断"]
|
||||
ROW2["🟡 性能指标 — P50/P90/P99 趋势"]
|
||||
ROW3["🔵 基础设施 — CPU/内存/连接数"]
|
||||
ROW4["⚫ 业务指标 — 订单量/支付成功率"]
|
||||
end
|
||||
|
||||
ROW1 --> JUDGE{是否异常?}
|
||||
ROW2 --> JUDGE
|
||||
ROW3 --> JUDGE
|
||||
ROW4 --> JUDGE
|
||||
|
||||
JUDGE --"否" --> NORMAL["一切正常 ✓"]
|
||||
JUDGE --"是" --> ALERT["触发告警 → On-Call"]
|
||||
```
|
||||
|
||||
### Runbook:告警处置手册
|
||||
|
||||
告警来了之后该怎么办?答案不应存在某个资深工程师的脑子里。
|
||||
|
||||
> [!summary] Runbook 模板
|
||||
>
|
||||
> | 字段 | 内容 |
|
||||
> |------|------|
|
||||
> | **标题** | `支付服务 P99 延迟 > 2s 持续 5 分钟` |
|
||||
> | **影响范围** | 下单接口超时,用户体验受损 |
|
||||
> | **检查步骤** | ① 看 Grafana 延迟面板确认峰值时间点 → ② 查同期部署记录 → ③ 检查下游 DB 慢查询 |
|
||||
> | **常见原因** | ① 新上线代码有性能 regression → ② DB 连接池耗尽 → ③ 下游超时风暴 |
|
||||
> | **快速恢复** | ① 回滚最近一次发布 → ② 扩容支付服务实例 → ③ 开启熔断降低下游压力 |
|
||||
> | **彻底解决** | 排查根本原因,补充回归测试,完善容量规划 |
|
||||
|
||||
> [!question] 反模式自检
|
||||
>
|
||||
> 如果你的 On-Call 流程是这样的:收到告警 → 群里问 "有人遇到过吗?" → 等半天有人回复 → SSH 到服务器上翻日志 → ……——这说明你们的可观测性体系还不够成熟。**好的 On-Call 体验 = 清晰的告警 + 完善的 Runbook + 一键处置工具**。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[02-服务治理]] — 熔断器可以触发告警,与监控系统联动;分布式链路追踪章节也有详细说明
|
||||
- [[05-部署运维]] — K8s Liveness/Readiness Probe 是可观测性的基础
|
||||
- [[01-基础概念]] — 微服务的整体架构认知
|
||||
@@ -0,0 +1,397 @@
|
||||
---
|
||||
tags: [microservice, kubernetes, cicd, container, sre, observability]
|
||||
create time: 2026-04-29 12:05
|
||||
---
|
||||
|
||||
# 部署运维
|
||||
|
||||
## 概述
|
||||
|
||||
微服务架构下,服务数量从几个增长到几百个。**人工运维完全不可行**。本文涵盖容器化编排、K8s 资源管理、发布策略、弹性伸缩、CI/CD 流水线、日志告警和 SRE 核心概念。
|
||||
|
||||
## 容器化与编排
|
||||
|
||||
### Docker 容器 — 标准交付单元
|
||||
|
||||
```dockerfile
|
||||
# Go 多阶段构建示例
|
||||
FROM golang:1.22 AS builder
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o server .
|
||||
|
||||
FROM alpine:latest
|
||||
COPY --from=builder /app/server /server
|
||||
EXPOSE 8080
|
||||
CMD ["/server"]
|
||||
```
|
||||
|
||||
> [!tip] 关键实践
|
||||
> - **多阶段构建**:镜像只包含运行时产物,体积极大缩小
|
||||
> - **非 root 用户运行**:安全最佳实践
|
||||
> - `.dockerignore`:排除不必要的文件(git、vendor、测试文件)
|
||||
|
||||
### Kubernetes 核心概念
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph CLUSTER["K8s Cluster"]
|
||||
master["Master Node<br/>API Server / Scheduler / ETCD"]
|
||||
|
||||
subgraph NODES["Worker Nodes"]
|
||||
N1[Node A]
|
||||
N2[Node B]
|
||||
end
|
||||
|
||||
master --> N1
|
||||
master --> N2
|
||||
|
||||
subgraph PODS1["Deployment: order-service"]
|
||||
P1[Pod 1]
|
||||
P2[Pod 2]
|
||||
P3[Pod 3]
|
||||
end
|
||||
|
||||
subgraph PODS2["Deployment: payment-service"]
|
||||
Q1[Pod 1]
|
||||
Q2[Pod 2]
|
||||
end
|
||||
|
||||
N1 --> P1 & Q1
|
||||
N2 --> P2 & P3 & Q2
|
||||
end
|
||||
|
||||
Svc["Service: order-service<br/>Load Balancer → Pods"]
|
||||
Svc --> P1 & P2 & P3
|
||||
```
|
||||
|
||||
| K8s 对象 | 作用 |
|
||||
|---------|------|
|
||||
| Pod | 最小部署单元,一个或多个容器 |
|
||||
| Deployment | 管理 Pod 的声明式更新和扩缩容 |
|
||||
| Service | 稳定的网络入口,实现负载均衡 |
|
||||
| Ingress | HTTP/HTTPS 路由规则(外部访问入口) |
|
||||
| ConfigMap / Secret | 配置注入,与代码分离 |
|
||||
| HPA (Horizontal Pod Autoscaler) | 根据 CPU/自定义指标自动扩缩 |
|
||||
|
||||
### 资源管理与健康检查
|
||||
|
||||
K8s 调度 Pod 的核心依据是 **资源请求 (Requests)**,而决定 pod 生死的是 **探针 (Probes)**。这两个概念配合使用才能保障服务稳定运行。
|
||||
|
||||
```yaml
|
||||
# Deployment 核心片段:资源配置 + 探针
|
||||
spec:
|
||||
replicas: 3
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: order-service
|
||||
image: registry.example.com/order:v1.2.3
|
||||
resources:
|
||||
requests: # 调度依据:K8s 保证至少有这些资源
|
||||
cpu: "250m" # 0.25 核
|
||||
memory: "256Mi" # 256 MB
|
||||
limits: # 硬上限:超过则 OOMKill / CPU Throttle
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
livenessProbe: # 活体检测:死了就重启
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
readinessProbe: # 就绪检测:未就绪就不进负载均衡
|
||||
httpGet:
|
||||
path: /ready
|
||||
port: 8080
|
||||
initialDelaySeconds: 5
|
||||
periodSeconds: 5
|
||||
startupProbe: # 启动检测:慢启动服务友好
|
||||
httpGet:
|
||||
path: /healthz
|
||||
port: 8080
|
||||
failureThreshold: 30
|
||||
periodSeconds: 10
|
||||
```
|
||||
|
||||
> [!tip] Probe 选择指南
|
||||
>
|
||||
> | 探针类型 | 触发条件 | 后果 | 适用场景 |
|
||||
> |---------|---------|------|---------|
|
||||
> | **Liveness** | `/healthz` 返回非 2xx | K8s **重启容器** | 死锁、无法恢复的崩溃 |
|
||||
> | **Readiness** | `/ready` 返回非 2xx | **摘除 Service 流量** | 依赖 DB 连接池未就绪、热加载进行中 |
|
||||
> | **Startup** | 首次成功前一直失败 | **不重启,只等待** | 大模型初始化、JVM cold start 等慢启动场景 |
|
||||
|
||||
> [!question] 思考
|
||||
> 如果一个服务的 `/healthz` 因为数据库连接超时而持续返回 503,K8s 会怎么做?
|
||||
|
||||
**答案**:Liveness Probe 会认为容器挂了并反复重启它 — 这就是经典的 **CrashLoopBackOff**。正确做法是让 `/healthz` 做降级判断(DB 不可用时返回 200 + 标注降级),用 `/ready` 来摘除流量。Liveness 应该只对"进程已死"的情况敏感,对"性能下降"保持宽容。
|
||||
|
||||
---
|
||||
|
||||
## 发布策略
|
||||
|
||||
微服务需要独立发布,如何在不停服的情况下完成更新?
|
||||
|
||||
### 蓝绿发布
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Blue: 初始状态
|
||||
Blue --> DeployGreen: 部署新版本到 Green
|
||||
DeployGreen --> TestGreen: 灰度验证
|
||||
TestGreen --> Switch: 切换流量
|
||||
Switch --> Green: 全部流量到新版
|
||||
Green --> CleanupGreen: 删除旧版 Blue
|
||||
```
|
||||
|
||||
- 优点:**回滚极速**(切回 Blue 即可),验证充分
|
||||
- 缺点:资源翻倍,需要两套环境
|
||||
|
||||
### 金丝雀发布 (Canary)
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["95% traffic to stable"]
|
||||
B["5% traffic to canary"]
|
||||
C["Metrics normal?"]
|
||||
D["Gradually expand to 20% → 50% → 100%"]
|
||||
E["Metrics abnormal → Auto rollback"]
|
||||
|
||||
A --> B
|
||||
B --> C
|
||||
C -- "yes" --> D
|
||||
C -- "no" --> E
|
||||
```
|
||||
|
||||
- 优点:资源效率高,风险渐进暴露
|
||||
- 缺点:流程复杂,需要完善的监控配合
|
||||
|
||||
### 两种策略选择
|
||||
|
||||
> [!question] 思考
|
||||
> 你的团队应该用蓝绿还是金丝雀?
|
||||
|
||||
**答案线索**:看你们的**监控成熟度和发布频率**。低频次(每周/每月)、监控完善时用金丝雀;高频次(每天多次)或想简化流程时用蓝绿。
|
||||
|
||||
## 弹性伸缩
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
Metrics["CPU / Memory / Custom QPS"] --> Trigger{Threshold met?}
|
||||
Trigger -- "yes" --> ScaleUp["Scale up new Pod"]
|
||||
Trigger -- "no" --> Keep["Keep current replicas"]
|
||||
ScaleDown{"Load decreased?"} -- "yes" --> ScaleDownPod["Scale down Pod"]
|
||||
ScaleDown -- "no" --> Keep
|
||||
```
|
||||
|
||||
**注意事项**:
|
||||
- **预热时间**:新 Pod 启动后不能立刻算入统计,否则可能反复扩缩
|
||||
- **优雅关闭**:HPA 缩容前,Pod 需要先摘除 Service 流量再退出
|
||||
- **预留容量**:不要将集群利用率拉到 100%,给突发流量留缓冲
|
||||
|
||||
## CI/CD 流水线
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
CODE["代码提交"]
|
||||
TEST["单元测试 + 集成测试"]
|
||||
SCAN["安全扫描 + 代码质量"]
|
||||
BUILD["构建 Docker 镜像"]
|
||||
PUSH["推送镜像仓库"]
|
||||
DEPLOY["CI→Staging → 审批 → Production"]
|
||||
|
||||
CODE --> TEST --> SCAN --> BUILD --> PUSH --> DEPLOY
|
||||
```
|
||||
|
||||
> [!summary] CI/CD 设计原则
|
||||
>
|
||||
> - **一次构建,多处部署**:镜像不随环境重新编译,只改 K8s ConfigMap/环境变量
|
||||
> - **语义化版本号**:镜像 tag 用 `v1.2.3`,tag 即版本溯源
|
||||
> - **自动化测试覆盖率要求**:合并 PR 前必须通过,否则不允许发布
|
||||
|
||||
### Pipeline 实战:GitHub Actions
|
||||
|
||||
```yaml
|
||||
# .github/workflows/deploy.yml
|
||||
name: Deploy order-service
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths:
|
||||
- "services/order/**" # 仅该目录变更才触发
|
||||
|
||||
jobs:
|
||||
build-and-push:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Build & push image
|
||||
run: |
|
||||
docker build -t registry/order:${{ github.sha }} .
|
||||
docker tag registry/order:${{ github.sha }} \
|
||||
registry/order:v$(git describe --tags --abbrev=0)
|
||||
docker push registry/order:${{ github.sha }}
|
||||
|
||||
deploy-staging:
|
||||
needs: build-and-push
|
||||
runs-on: ubuntu-latest
|
||||
environment: staging
|
||||
steps:
|
||||
- name: Apply K8s manifests
|
||||
run: |
|
||||
kubectl set image deployment/order-service \
|
||||
order=registry/order:${{ github.sha }}
|
||||
kubectl rollout status deployment/order-service --timeout=120s
|
||||
|
||||
deploy-production:
|
||||
needs: deploy-staging
|
||||
runs-on: ubuntu-latest
|
||||
environment: production
|
||||
steps:
|
||||
- name: Canary release (10% → 50% → 100%)
|
||||
run: |
|
||||
kubectl patch canary order-service --type merge \
|
||||
-p '{"spec":{"weight":10}}'
|
||||
# ... 等待监控确认,逐步放大流量
|
||||
```
|
||||
|
||||
> [!tip] Pipeline 设计要点
|
||||
>
|
||||
> - **路径过滤**:只对相关服务的代码变更触发构建,避免全量重建
|
||||
> - **Commit SHA 作为镜像 tag**:保证精确回滚 — `git revert` 之后用同一 SHA 拉取旧镜像
|
||||
> - **分阶段部署**:Staging → Production 的审批关卡是最后的防线,不要跳过
|
||||
|
||||
## 日志管理与告警
|
||||
|
||||
微服务单实例崩溃不可怕 — 可怕的是 **不知道哪一台、为什么挂了**。日志和告警是运维的眼睛。
|
||||
|
||||
### 日志架构选型
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
App["业务应用"] -->|stdout/stderr| K8sPod[Pod 容器日志]
|
||||
K8sPod --> Filebeat[采集器: Filebeat / FluentBit]
|
||||
Filebeat --> ES["Elasticsearch / Loki"]
|
||||
ES --> Grafana["Grafana 查询面板"]
|
||||
|
||||
App -->|structured log| sidecar[Sidecar 辅助日志]
|
||||
sidecar --> Filebeat
|
||||
```
|
||||
|
||||
**业界两种主流方案对比:**
|
||||
|
||||
| 维度 | ELK (Elasticsearch) | EFK/Loki |
|
||||
|------|---------------------|----------|
|
||||
| 存储成本 | 高(全文索引) | 低(索引 label + 对象存储原文) |
|
||||
| 查询性能 | 毫秒级全文检索 | 按 label 过滤较快,复杂查询慢 |
|
||||
| 运维复杂度 | 高(ES 集群维护) | 低(Loki 无索引) |
|
||||
| 适用规模 | 百万条/天以上 | 万 ~ 百万条/天 |
|
||||
|
||||
> [!important] 结构化日志
|
||||
>
|
||||
> 无论选择哪家方案,**日志格式必须是结构化的**(JSON),否则后期处理全是手工活:
|
||||
> ```json
|
||||
> {
|
||||
> "timestamp": "2026-04-29T10:30:00Z",
|
||||
> "level": "error",
|
||||
> "service": "order-service",
|
||||
> "trace_id": "abc-123-def",
|
||||
> "msg": "payment gateway timeout",
|
||||
> "duration_ms": 5023
|
||||
> }
|
||||
> ```
|
||||
|
||||
### 告警策略设计
|
||||
|
||||
> [!question] 思考
|
||||
> 如果一个 Pod CPU 使用率从 20% 飙升到 90%,你应该立刻打电话叫值班工程师吗?
|
||||
|
||||
不一定。**好的告警应该是" actionable"的** — 能让人立刻采取行动,而不是单纯告诉你"出事了"。
|
||||
|
||||
```mermaid
|
||||
mindmap
|
||||
root((告警设计原则))
|
||||
区分等级
|
||||
P0: 立即响应 (页面电话)
|
||||
P1: 当天处理 (IM 消息)
|
||||
P2: 本周修复 (工单)
|
||||
抑制噪音
|
||||
相关告警聚合: 一个根因一条告警
|
||||
静默期: 重启后的自动恢复不打扰
|
||||
附带上下文
|
||||
告警信息包含: 什么服务 · 哪个指标 · 当前值 vs 阈值
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SRE 核心概念
|
||||
|
||||
站点可靠性工程 (SRE) 把运维问题看作**软件工程问题**。它的核心理念是用 SLI/SLO/SLA 来量化服务质量,避免"我觉得系统很慢"这类模糊描述。
|
||||
|
||||
### SLI / SLO / SLA
|
||||
|
||||
| 术语 | 全称 | 定义 | 谁定义 |
|
||||
|------|------|------|--------|
|
||||
| **SLI** | Service Level Indicator | **实际度量**:用户请求的成功率是多少? | 观测系统自动产出 |
|
||||
| **SLO** | Service Level Objective | **内部目标**:我们承诺达到 99.9% 可用性 | SRE + 研发制定 |
|
||||
| **SLA** | Service Level Agreement | **对外合同**:达不到就赔钱 | 法务 + 商务制定 |
|
||||
|
||||
> [!tip] 实用比例
|
||||
>
|
||||
> - **99.9% (三个九)** = 每年约 8.76 小时停机 — 适合大多数后端服务
|
||||
> - **99.95%** = 每年约 4.38 小时 — 核心交易链路
|
||||
> - **99.99% (四个九)** = 每年约 52 分钟 — 金融级系统
|
||||
> - **99%** 意味着每月近 7 小时不可用 — **几乎等于没有可用性目标**
|
||||
|
||||
### 错误预算 (Error Budget)
|
||||
|
||||
这是 SRE 最核心的机制 — **允许犯错,但设上限**:
|
||||
|
||||
```
|
||||
错误预算 = 1 - SLO
|
||||
|
||||
SLO = 99.9% → 错误预算 = 0.1% → 每月允许 21 分钟停机
|
||||
SLO = 99.99% → 错误预算 = 0.01% → 每月仅 4 分钟停机
|
||||
```
|
||||
|
||||
**基于错误预算的决策逻辑:**
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Budget{"错误预算剩余 > 50%?"}
|
||||
|
||||
Budget -- "是" --> Aggressive["可激进发布: <br/>金丝雀 + 自动扩缩"]
|
||||
Budget -- "< 50%" --> Conservative["保守发布: <br/>蓝绿 + 全量人工审查"]
|
||||
Budget -- "< 10%" --> Freeze["冻结发布: <br/>优先修复稳定性<br/>不允许新功能上线"]
|
||||
|
||||
Aggressive --> Monitor["持续监控指标"]
|
||||
Conservative --> Monitor
|
||||
```
|
||||
|
||||
> [!summary] SRE 心法
|
||||
>
|
||||
> 1. **用户视角定义 SLO**:不是"API P99 < 200ms",而是"用户在 3G 网络下打开页面 < 2s"
|
||||
> 2. **错误预算用完 = 停止功能开发**:全力修 bug 加稳定性
|
||||
> 3. **Blameless Postmortem**:事故复盘不问"谁干的",问"流程哪里可以改进"
|
||||
|
||||
---
|
||||
|
||||
## 运维 Checklist
|
||||
|
||||
每次上线前快速过一遍这个清单,避免低级事故:
|
||||
|
||||
| # | 检查项 | 说明 |
|
||||
|---|--------|------|
|
||||
| 1 | **探针已配置** | liveness/readiness/startup probe 都已设定 |
|
||||
| 2 | **资源 limits 已设** | 防止单个 Pod 拖垮整台机器 |
|
||||
| 3 | **日志为 JSON 格式** | 可被采集器解析 |
|
||||
| 4 | **追踪 ID 透传** | trace_id 在跨服务调用链中不丢失 |
|
||||
| 5 | **回滚预案明确** | 知道怎么回到上一版本,且演练过 |
|
||||
| 6 | **告警已配置** | 关键指标异常时有人收到通知 |
|
||||
| 7 | **数据迁移有回退脚本** | DB schema 变更要兼容新老版本共存 |
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[02-服务治理]] — K8s Service 提供原生的服务发现和负载均衡
|
||||
- [[04-可观测性]] — K8s Liveness/Readiness Probe 是可观测性的基础
|
||||
@@ -0,0 +1,41 @@
|
||||
---
|
||||
tags: []
|
||||
create time: 2026-04-29 12:00
|
||||
---
|
||||
|
||||
# 微服务知识索引
|
||||
|
||||
## 概述
|
||||
|
||||
本目录系统整理微服务架构的核心知识点。围绕 **5 个核心主题**,由浅入深地覆盖从概念理解到工程实践的关键路径。
|
||||
|
||||
## 知识体系
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["01 基础概念"] --> B["02 服务治理"]
|
||||
B --> C["03 数据一致性"]
|
||||
B --> D["04 可观测性"]
|
||||
C --> E["05 部署运维"]
|
||||
D --> E
|
||||
```
|
||||
|
||||
| # | 主题 | 核心问题 |
|
||||
|---|------|----------|
|
||||
| 1 | [[01-基础概念]] | 什么是微服务?单体 vs 微服务的权衡是什么? |
|
||||
| 2 | [[02-服务治理]] | 服务如何发现彼此?如何通信?失败怎么处理? |
|
||||
| 3 | [[03-数据一致性]] | 每个服务拥有独立数据库,跨服务数据一致性怎么保证? |
|
||||
| 4 | [[04-可观测性]] | 请求穿越多个服务后,如何追踪、监控和排查问题? |
|
||||
| 5 | [[05-部署运维]] | 上百个服务如何容器化编排、灰度发布、弹性伸缩? |
|
||||
|
||||
### 学习建议
|
||||
|
||||
> [!tip] 学习路径
|
||||
> 按编号顺序阅读。第 1 篇是理论基础,后面 4 篇在实践中高度耦合,可以按需跳读。
|
||||
|
||||
> [!question] 带着问题读
|
||||
> 每篇文章都设计了启发式问题。先自己想一遍答案,再看文档,效果更好。
|
||||
|
||||
### 关联笔记
|
||||
|
||||
- [[API 设计原则]] — API Gateway 的设计与 REST/gRPC 选型
|
||||
Reference in New Issue
Block a user