vault backup: 2026-04-29 20:39:17

This commit is contained in:
2026-04-29 20:39:17 +08:00
parent b6f969f317
commit 0421a3c801
6 changed files with 1717 additions and 0 deletions
+106
View File
@@ -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-部署运维]] — 容器化编排、灰度发布、弹性伸缩
+510
View File
@@ -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-可观测性]] — 日志、指标、链路追踪的完整体系
+217
View File
@@ -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-服务治理]] — 服务调用链路上的超时、重试、熔断
+446
View File
@@ -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-基础概念]] — 微服务的整体架构认知
+397
View File
@@ -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 是可观测性的基础
+41
View File
@@ -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 选型