vault backup: 2026-08-08 19:01:04

This commit is contained in:
2026-08-08 19:01:04 +08:00
parent 1a4a83bc67
commit e2975eb86b
46 changed files with 9370 additions and 0 deletions
@@ -0,0 +1,193 @@
---
tags: [arch/microservice, service-discovery, eureka, nacos, consul, cap-theory, health-check]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 服务注册与发现
## 概述
服务注册与发现是微服务架构的基石。当服务实例动态扩缩容时,调用方需要知道"当前谁在提供服务"以及"它们在哪"。本文对比 Eureka、Nacos、Consul 三大主流方案的核心设计差异,分析 CAP 理论在服务注册中心中的取舍,以及健康检查的各种实现方式。
## 核心原理
### 服务注册/注销/续约流程
```mermaid
sequenceDiagram
participant Client as 客户端
participant Server as 服务提供者
participant Registry as 注册中心
participant Discover as 消费方
Client->>Server: 启动服务
Server->>Registry: 1. 注册服务 (POST /register)
Registry-->>Server: 注册成功
Server->>Registry: 2. 心跳续约 (POST /heartbeat)
Registry->>Registry: 更新 lastDirtyTimestamp
loop 每 N 秒
Server->>Registry: 心跳续约
Registry-->>Server: ACK
end
Discover->>Registry: 3. 查询服务列表 (GET /instances)
Registry-->>Discover: 返回健康实例列表
Discover->>Server: 发起 RPC 调用
Note over Server: 服务关闭
Server->>Registry: 4. 主动注销 (DELETE /deregister)
Registry-->>Server: 注销成功
Registry->>Registry: 从服务列表中移除
```
**三步走**:注册 → 续约(或心跳)→ 注销。绝大多数故障场景下,调用方感知到服务宕机会延迟一个续约超时周期(通常 30~90 秒)。
### Eureka — AP 取向的自我修复
Eureka 是 Netflix 开源的方案,设计上严格遵循 **AP(可用性 + 分区容错性)**:
- **双副本架构**:Peer Replication 机制将数据异步同步给所有节点,不同步视为正常而非故障。
- **自我保护机制**:当 15 分钟内超过 85% 实例未发送心跳,Eureka 锁定保护窗口,不再剔除任何实例。这意味着被误判为宕机的实例仍会出现在服务列表中——宁可多调几次再失败,也不主动剔除。
- **客户端缓存**:消费者缓存服务列表(默认每 30 秒刷新),即使注册中心完全不可用,消费方依然能根据本地缓存继续通信。
```java
// Eureka 自我保护机制触发条件
public boolean shouldEnableRenewments() {
double threshold = registry.getRenewalThreshold(); // 85%
if (serverConfig.isSelfPreservationEnabled()
&& registry.getCachedInstanceStatus().equals(HEAVY_OVERLOAD)) {
return true; // 进入自我保护,允许续约
}
return super.shouldEnableRenewments();
}
```
> [!WARNING]
> 自我保护机制在生产环境是个双刃剑:它保证了高可用,但可能导致流量打向已宕机的实例。建议监控 `eureka.server.selfPreservationThreshold` 指标。
### Nacos — CP/AP 可切换的统一平台
Nacos 解决了 Eureka 和 ZooKeeper 只能二选一的问题:
| 模式 | 适用场景 | 一致性协议 | 典型部署形态 |
|------|---------|-----------|------------|
| AP 模式 | 服务注册发现 | Distro 协议(自研,类似 Eureka 的双写) | 集群部署 |
| CP 模式 | 配置管理 | Raft 协议 | 奇数节点集群 |
**统一命名空间管理**:Nacos 通过 `namespace` 实现环境隔离(dev/test/prod),每个命名空间有独立的服务和配置视图。底层基于 PostgreSQL 存储元数据,天然支持持久化。
```go
// Nacos Go SDK 服务订阅示例
client, _ := nacos.NewClient(nacos.Config{
ServerAddr: "127.0.0.1:8848",
NamespaceId: "prod-ns",
})
info, _ := client.Subscribe(&nacos.Subscriber{
ServiceName: "user-service",
Cluster: "DEFAULT",
OnEvent: func(event *nacos.Event) {
for _, inst := range event.Hosts {
fmt.Println(inst.InstanceIp, inst.InstancePort)
}
},
})
```
> [!TIP]
> 面试常考点:为什么 Nacos 能在同一套代码中既做 AP 又做 CP?答:两种模式使用不同的数据存储路径和服务类型——`nacos.v1.auth.user`(CP,Raft)与 `nacos.v1.instance`(AP,Distro)。
### Consul — Serf Gossip + 丰富健康检查
HashiCorp 出品的 Consul 有三个独特优势:
1. **Serf gossip 协议**:基于 WAN Gossip 的成员管理,相比 TCP 直连更容错。节点通过 pull-based push-based 混合机制传播成员变更。
2. **丰富的健康检查**:支持 TCP、HTTP、HTTP Body、docker、脚本五种检查方式。还支持 TTL(手动报告状态)模式。
3. **KV 存储能力**:内嵌 Raft 一致性 KV 存储,可用于配置管理、分布式锁等辅助场景。
```go
// Consul HTTP 健康检查配置
func registerCheck() *api.AgentServiceCheck {
return &api.AgentServiceCheck{
HTTP: "http://localhost:8080/health",
Timeout: "5s",
Interval: "10s",
DeregisterCriticalServiceAfter: "90s",
}
}
```
### CAP 理论在服务注册中心的体现
| 注册中心 | CAP 选择 | 网络分区时的行为 | 数据一致性保证 |
|---------|---------|-----------------|--------------|
| Eureka | AP | 各节点独立响应,可能返回过期数据 | 最终一致 |
| Nacos (AP) | AP | Distro 协议异步复制,容忍短暂不一致 | 最终一致 |
| Nacos (CP) | CP | 选出 Leader 后响应,部分节点不可用时降级 | 强一致 |
| Consul | CP(TCP)/ AP(DNS) | Raft Leader 不可达时无法写入 | 读可能 stale |
> [!NOTE]
> PACELC 补充:CAP 只讨论网络分区场景。PACELC 指出:无分区时,延迟(Latency)与一致性(Consistency)仍需权衡。Nacos AP 模式、Eureka 都是选择了低延迟而非强一致。
### 健康检查方式对比
| 方式 | 原理 | 优点 | 缺点 |
|------|-----|------|------|
| TCP | 尝试建立 TCP 连接 | 轻量、通用 | 无法感知应用层故障 |
| HTTP/HTTPS | 请求指定 endpoint 检查状态码 | 灵活,可携带业务逻辑 | 增加服务器负载 |
| gRPC | gRPC Health Checking Protocol | 适合 gRPC 服务 | 需额外 proto 定义 |
| Script | 执行自定义脚本 | 最灵活,可做深度检查 | 维护成本高 |
| Edge探测 | 模拟真实用户请求 | 最贴近用户体验 | 复杂度高,一般用于 SLA 监控 |
## 代码示例
以下是一个简化版的 Go 服务注册中心心跳续约逻辑:
```go
type Heartbeat struct {
ServiceID string
InstanceID string
LastPingAt time.Time
}
type Registry struct {
instances map[string]*Instance // serviceID -> Instance
mu sync.RWMutex
}
// Register 注册一个新实例
func (r *Registry) Register(svc, instanceID, addr string) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, ok := r.instances[svc]; !ok {
r.instances[svc] = &Instance{Addr: addr}
}
return nil
}
// Renew 续约:更新最后心跳时间
func (r *Registry) Renew(svc, instanceID string) bool {
r.mu.Lock()
defer r.mu.Unlock()
inst, ok := r.instances[svc]
if !ok {
return false
}
inst.LastHeartbeat = time.Now()
return true
}
```
## 实践场景
**秋招高频问题:**
- "Eureka 的自我保护机制什么时候该关?" — 答案:金融类对一致性要求高的场景可以考虑关闭,普通互联网业务不建议关。关了之后可能出现"雪崩式下线"——一批实例同时被认为是故障而全部剔除。
- "Nacos 和 Eureka 有什么本质区别?" — Eureka 只管服务发现,Nacos 是一体化平台(服务发现 + 配置中心)。Nacos 还能按 namespace/group/key 精确分组配置。
- "Consul 的 DNS 接口为什么是 AP 模式?" — DNS 本身是无状态的,Consul 的 DNS API 直接指向随机节点读取本地内存缓存,不经过 Raft 日志,因此速度极快但可能读到旧数据。
**实战建议:** 小团队用一个 Nacos 集群就够用;大规模多机房场景考虑 Consul 的多数据中心能力。
## 关联笔记
- [[负载均衡算法]]
- [[限流熔断降级]]
- [[配置中心设计]]
@@ -0,0 +1,183 @@
---
tags: [arch/microservice, load-balancing, consistent-hashing, spring-cloud-lb, round-robin]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 负载均衡算法
## 概述
负载均衡将客户端请求分发到多个服务实例,是微服务架构中不可或缺的一环。本文对比客户端 vs 服务端两种负载均衡架构,详细讲解轮询、随机、一致性哈希、最少连接等核心算法,以及 Spring Cloud LoadBalancer 的实现原理。
## 核心原理
### 客户端负载均衡 vs 服务端负载均衡
```mermaid
graph TD
subgraph ClientLB["客户端负载均衡"]
C1[客户端/消费者] --> LB1[客户端负载均衡器]
LB1 --> SI1["实例A: :8081"]
LB1 --> SI2["实例B: :8082"]
LB1 --> SI3["实例C: :8083"]
C1 -.->|自己选实例| LB1
end
subgraph ServerLB["服务端负载均衡"]
C2[客户端/消费者] --> NLB[Nginx/LVS]
NLB --> SNI1["实例A: :8081"]
NLB --> SNI2["实例B: :8082"]
NLB --> SNI3["实例C: :8083"]
end
```
| 对比维度 | 客户端负载均衡 | 服务端负载均衡 |
|---------|--------------|--------------|
| 部署位置 | 与消费者同进程(如 Spring Cloud LoadBalancer) | 独立中间件(Nginx、LVS、F5) |
| 延迟 | 低(多一次网络跳) | 高(所有请求必经 LB 节点) |
| 故障隔离 | 好(失败不影响其他调用链路) | 差(LB 单点故障影响全部) |
| 语言耦合 | 需要为每种语言实现 SDK | 协议无关,通用性强 |
| 适用场景 | JVM 微服务体系 | 跨语言 / 流量入口层 |
> [!TIP]
> 面试高频:Netflix 为什么选择客户端负载均衡?因为他们的服务都是 Java,可以用 Ribbon(现已被 Spring Cloud LoadBalancer 替代)嵌入每个微服务,避免了反向代理成为瓶颈和单点故障。
### 轮询法(Round Robin)
最简单的策略:按顺序依次分发请求。适用于各实例配置相近的场景。
- **简单轮询**:`index = (index + 1) % n`,第 i 个请求发给第 i mod n 个实例。
- **加权轮询**(Weighted Round Robin):为每个实例分配权重 wᵢ,权重高的获得更多请求。Nginx 的 smooth weighted round robin 用 `current_weight += weight`,每次选 current_weight 最大的,然后 `selected.weight -= total_weight`,保证长期均衡。
### 随机法
从健康实例列表中均匀随机选择一个。理论上 N 次请求后分布趋近均匀,但小样本下可能极端不均——例如连续两次命中同一个实例。加权随机在电商大促中常用:给大容量实例更高权重。
### 一致性哈希(Consistent Hashing)
传统哈希 `hash(instance) % n` 的问题在于实例增减时会导致大量请求重新路由(缓存穿透式抖动)。一致性哈希将其缓解到仅影响 `(1/n)` 的数据。
**核心思路**:将 hash 值空间映射到环上(0 ~ 2³²),服务和请求都 hash 到环上的同一点,请求顺时针找到最近的实例。
**虚拟节点解决数据倾斜**:一个物理实例对应环上的 k 个虚拟节点(k 通常取 100~200),使实例分布更均匀。
```mermaid
graph LR
H1[hash key: user_42] -->|"顺时针"| I3[实例C]
H2[hash key: user_99] -->|"顺时针"| I1[实例A]
H3[hash key: session_7] -->|"顺时针"| I2[实例B]
subgraph Ring["哈希环"]
VN1(VN-a1) --- VN2(VN-a2)
VN1 ---|"虚节" |VN2
VN3(VN-b1) --- VN4(VN-b2)
VN5(VN-c1) --- VN6(VN-c2)
end
```
```go
// 一致性哈希简化实现
type ConsistentHash struct {
hashes []uint64 // 虚拟节点的 hash 值排序
map map[uint64]int // hash -> 物理实例索引
k int // 每个实例的虚拟节点数
}
func NewConsistentHash(instances []string, k int) *ConsistentHash {
ch := &ConsistentHash{map: make(map[uint64]int), k: k}
for i, inst := range instances {
for v := 0; v < k; v++ {
h := murmur3.Sum64([]byte(fmt.Sprintf("%s-%d", inst, v)))
ch.hashes = append(ch.hashes, h)
ch.map[h] = i
}
}
sort.Slice(ch.hashes, func(i, j int) bool { return ch.hashes[i] < ch.hashes[j] })
return ch
}
func (ch *ConsistentHash) Get(key string) string {
h := murmur3.Sum64([]byte(key))
idx := sort.Search(len(ch.hashes), func(i int) bool {
return ch.hashes[i] >= h
})
if idx == len(ch.hashes) {
idx = 0 // 回到环首
}
return InstanceList[ch.map[ch.hashes[idx]]]
}
```
### 最少连接数法(Least Connections)
将请求发给当前活跃连接最少的实例,特别适合长连接场景(gRPC、WebSocket)。实现上需维护每个实例的活跃连接计数器,并发安全地读写。
### Spring Cloud LoadBalancer 实现原理
Spring Cloud LoadBalancer 取代了已停止维护的 Ribbon,核心机制:
1. **ServiceInstanceListSupplier**:从注册中心拉取实例列表并缓存。
2. **Reactively reactive**:基于 Reactor 响应式编程,返回 `Flux<ServiceInstance>`。
3. **RoundRobinLocator**:默认轮询策略,内部用 AtomicLong 做递增索引取模。
```java
// Spring Cloud LoadBalancer 核心接口
public interface LoadBalancerClient<S extends ServiceInstance> {
<T> Mono<T> execute(String serviceId, LoadBalancerRequest<T>, InstanceChooser);
ServiceInstance choose(String serviceId);
<T> Flux<T> getLazyLoadBalancerClient(String serviceId);
}
```
> [!NOTE]
> Spring Cloud LoadBalancer 不提供重试和熔断,这些能力由 Resilience4j 等库处理。它只管"选哪个实例"这一个动作。
## 代码示例
Go 中的加权轮询实现:
```go
type WeightedRR struct {
servers []*Server
totalW int
curIndex int
}
type Server struct {
Addr string
Weight int
CurWeight int
}
func (w *WeightedRR) Next() string {
maxW, best := -1, ""
for _, s := range w.servers {
s.CurWeight += s.Weight
if s.CurWeight > maxW {
maxW = s.CurWeight
best = s.Addr
}
}
for _, s := range w.servers {
s.CurWeight -= w.totalW
}
return best
}
```
## 实践场景
**秋招高频问题:**
- "一致性哈希为什么能减少数据迁移?" — 只有失效实例及其顺时针下一个实例之间的数据段会迁移。移除一个实例只影响其相邻的两个虚拟节点范围,而非全部数据。
- "什么时候不能用轮询?" — 实例规格差异大(如混部有强机和弱机),必须加权;或者存在长连接状态绑定(如 WebSocket 会话),需要一致性哈希来确保同一用户落到同一实例。
- "Spring Cloud 和 Nginx 的负载均衡有什么区别?" — Spring Cloud 在应用层做选择,感知注册中心变更(秒级);Nginx 依赖 upstream 配置或 DNS 解析更新,变更延迟较大。
> [!WARNING]
> 一致性哈希不适合频繁增删实例的场景。如果实例每几分钟就变一次,virtual nodes 数量要调得更大(500+)才能维持稳定性,这增加了内存开销。
## 关联笔记
- [[服务注册与发现]]
- [[限流熔断降级]]
@@ -0,0 +1,198 @@
---
tags: [arch/microservice, config-center, apollo, nacos-config, hot-update, distributed-config]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 配置中心设计
## 概述
配置中心解决了微服务架构中"配置散落在各处导致难以统一管理"的问题。本文将深入对比 Apollo 和 Nacos Config 两大主流方案的设计差异,剖析配置热更新的实现原理、版本管理回滚机制,以及长轮询如何做到秒级推送。
## 核心原理
### Apollo — 三级 Namespace 模型
Apollo(携程开源)的配置模型采用**分环境/应用/集群**三层结构:
```mermaid
graph TD
Env["Environment<br/>dev/test/prod"] --> AppA["App A"]
Env --> AppB["App B"]
AppA --> Cluster1["Cluster: DEFAULT"]
AppA --> Cluster2["Cluster: mq-cluster"]
Cluster1 --> NS1["Namespace: application"]
Cluster1 --> NS2["Namespace: db.properties"]
Cluster2 --> NS3["Namespace: mq.json"]
```
| 层级 | 粒度 | 示例值 | 作用 |
|------|-----|--------|------|
| Environment | 部署环境 | dev, test, staging, prod | 隔离不同环境的配置 |
| Application | 业务应用 | user-service, order-service | 每个微服务独立一份配置 |
| Cluster | 逻辑分组 | DEFAULT, mq-cluster | 同一应用的多个实例组共用一份配置 |
| Namespace | 配置类型 | application, redis.json, mysql.yaml | 按功能域拆分配置文件 |
**灰度发布流程:**
1. 编辑配置后进入"审核"状态,不生效。
2. 审核通过后发布为 `Release`,Apollo Client 在下一个拉取周期(默认 5s)发现新版本并热加载。
3. 灰度规则可指定特定 IP 先接收新配置,验证无误后再全量发布。
> [!TIP]
> Apollo 的 namespace 是强类型概念——每个 namespace 有独立的修改权限和审批流。例如 `db.properties` 只能由 DBA 团队修改,`mq.json` 由消息队列团队修改。
### Nacos Config — 长轮询推送
Nacos Config 的核心优势在于**秒级推送**,底层基于 HTTP 长轮询(Long Polling):
```mermaid
sequenceDiagram
participant Client as Apollo Client / SDK
participant Server as Apollo Config Service
participant Admin as 运维控制台
Admin->>Server: 发布配置 Release
Note over Server: 记录 releaseKey (version)
loop 每 5 秒
Client->>Server: GET /notifications/v2?key=user-service&ip=10.0.0.1
Server-->>Client: empty response (无变更)
end
Admin->>Server: 修改并发布配置
Server->>Server: 生成新的 releaseKey
loop 检测到 releaseKey 变化
Client->>Server: GET /notifications/v2?...
Server-->>Client: [{"key":"user-service","notificationId":42}]
Note over Client: 收到通知,立即拉取最新配置
Client->>Server: GET /configs/user-service?group=DEFAULT_GROUP
Server-->>Client: {key:"db.url", value:"jdbc:mysql://new"}
Client->>Client: 触发 ConfigChange 事件回调
end
```
**长轮询 vs 短轮询的权衡:**
| 方式 | 延迟 | 服务器压力 | 实现复杂度 |
|------|-----|-----------|-----------|
| 短轮询(固定间隔) | 高(等于轮询间隔) | 低(每次请求返回空或数据) | 简单 |
| 长轮询 | 低(秒级) | 中(连接保持期间占用内存) | 中等(需维护挂起列表) |
| WebSocket | 最低(毫秒级) | 中 | 复杂(跨 NAT/防火墙困难) |
| MQTT/publish-subscribe | 最低 | 低 | 中 |
Nacos 的长轮询实现要点:
- 客户端发起一个 HTTP 请求后,服务端将其加入 `waitList`(Map<String, Future>),挂起不返回。
- 当配置发生变化时,遍历 `waitList`,找到匹配的监听者,写入 notificationId 后立即返回。
- 如果 30 秒内无变更,服务端超时返回,客户端重新发起新的长轮询请求(防止连接无限期挂起)。
### 配置热更新实现原理
配置变更后如何实时通知到 running 的应用?常见方案:
**1. Java BeanPostProcessor + 反射**
Spring 容器启动时通过 `BeanDefinitionRegistryPostProcessor` 扫描带 `@Value` 注解的字段,注入时通过反射直接修改对象的私有字段值。Apollo 客户端还封装了 `ApolloAnnotationPostProcessor` 来处理 `@ApolloConfigChangeListener`。
**2. Proxy 动态代理**
对于 getter/setter 模式的属性访问,可以使用 CGLIB 或 JDK 动态代理,将 getter 方法拦截并在每次调用时检查缓存是否过期,若已过期则从配置中心拉取最新值。
**3. ConfigurationProperties + RelaxedBinding**
Spring Boot 的 `@ConfigurationProperties` 配合 Binder 类,可以在运行时重建绑定映射。Apollo 的做法是将原始配置缓存为一个 `Properties` 对象,变更时合并旧值+新值,然后通过 Spring 的事件机制通知所有注册的 `EventListener`。
```go
// Go 中的配置热更新 - 反射模式简化示例
func hotReload(config *Config, key string, newValue interface{}) error {
v := reflect.ValueOf(config).Elem()
t := v.Type()
for i := 0; i < v.NumField(); i++ {
field := v.Field(i)
tag := t.Field(i).Tag.Get("apollo")
if tag == key && field.CanSet() {
// 解析 JSON/YAML 并赋值
data := []byte(fmt.Sprintf("%v", newValue))
json.Unmarshal(data, field.Addr().Interface())
return nil
}
}
return fmt.Errorf("key %s not found", key)
}
```
> [!WARNING]
> 反射修改 final 字段需要 JVM flag `--add-opens java.base/java.lang=ALL-UNNAMED`,在生产环境中可能受限。更安全的方式是使用 volatile 引用变量指向一个新的 immutable 配置对象。
### 配置版本管理和回滚
Apollo 的发布记录保存在 `Release` 表中,每次发布都保留完整的配置快照:
```mermaid
graph LR
R1[Release V1<br/>db.url=old] -->|"新增 key"| R2[Release V2<br/>db.url=old,<br/>cache.enabled=true]
R2 -->|"修改 value"| R3[Release V3<br/>db.url=new,<br/>cache.enabled=true]
R3 -->|"删除 key"| R4[Release V4<br/>db.url=new]
R3 -. "回滚到 V2" .-> R2b[Release V5 (回滚)<br/>db.url=old,<br/>cache.enabled=true]
```
回滚的本质不是真正的"倒退版本",而是用当前最新状态 + 差异计算生成一个新版本。这样保证了发布记录永远单调递增,不会产生环形依赖。
Nacos Config 的灰度规则同样支持配置回滚到任意历史版本,且支持多环境一键同步(将 test 环境的正确配置一键推送到 prod)。
## 代码示例
Go 中使用 Apollo Client 订阅配置变更:
```go
// Apollo 客户端订阅配置变更
client, _ := apollo.NewClient(apollo.Config{
Apis: "http://127.0.0.1:8080",
AppID: "user-service",
Cluster: "DEFAULT",
Namespace: "application",
})
client.AddListener(func(namespace string, change *apollo.ConfigChange) {
for k, newVal := range change.ChangedKeys {
oldVal := change.OldValue(k)
log.Printf("config changed: %s = %s -> %s", k, oldVal, newVal)
// 此处刷新本地配置或重连数据库
}
})
```
Java 中使用 @RefreshScope:
```java
@RestController
@RefreshScope // Spring Cloud 自动刷新生成代理 Bean
public class UserController {
@Value("${greeting.message:Hello}")
private String greeting;
@GetMapping("/hello")
public String hello() {
return greeting; // 配置变更后下一次请求即生效
}
}
```
## 实践场景
**秋招高频问题:**
- "为什么 Nacos Config 用长轮询而不是 WebSocket?" — 长轮询兼容所有 HTTP 中间件(网关、CDN、负载均衡器),而 WebSocket 需要全链路升级。在微服务架构中,请求路径上通常有 Spring Cloud Gateway 等 HTTP-only 代理,WebSocket 升级失败会导致连接断开。
- "Apollo 的 Namespace 和 K8s ConfigMap 的区别是什么?" — Namespace 是 Apollo 的顶层配置类型,支持更细粒度的权限控制、版本历史和灰度发布;ConfigMap 是 K8s 的原生资源,只支持键值对映射且更新后需要手动重启 Pod 或使用 sidecar 注入 reload。
- "热更新最大的风险是什么?" — 配置变更导致服务行为突变。Apollo 的解决方案是"审核+灰度",先让少量实例生效观察指标;最大风险是无法回滚的脏数据写入。因此建议所有关键配置变更都做审计日志记录。
> [!NOTE]
> 生产最佳实践:核心配置(如数据库连接串、密钥)变更应该走独立的审批流程,不能与常规业务配置混在一起。可以用 Apollo 的不同 Namespace 来做物理隔离。
## 关联笔记
- [[服务注册与发现]]
- [[限流熔断降级]]
@@ -0,0 +1,188 @@
---
tags: [arch/microservice, rate-limiting, circuit-breaker, sentinel, resilience4j, token-bucket, leaky-bucket]
create time: 2026-08-08 18:00
update time: 2026-08-08 18:00
---
# 限流熔断降级
## 概述
限流、熔断、降级是微服务稳定性的三道防线。限流保护系统不被过量请求打垮,熔断在下游故障时快速失败避免级联雪崩,降级在服务不可用时返回兜底数据。三者层层递进,构成完整的容错体系。
## 核心原理
### 限流算法对比
```mermaid
graph LR
subgraph Counter["计数器限流"]
A[请求到达] --> B{本窗口内计数 < 阈值?}
B -- 是 --> C[允许通过]
B -- 否 --> D[拒绝]
end
subgraph TokenBucket["令牌桶"]
E[请求到达] --> F{桶中有足够令牌?}
F -- 是 --> G[消费令牌放行]
F -- 否 --> H[拒绝/等待]
I[匀速生成器] -->|"每秒 N 个"| J[(令牌桶)]
end
subgraph LeakyBucket["漏桶"]
K[请求到达] --> L[(漏桶)]
L --> M[固定速率流出处理]
L -->|"溢出"| N[拒绝]
end
```
| 算法 | 核心机制 | 突发能力 | 延迟特性 | 适用场景 |
|------|---------|---------|---------|---------|
| 滑动窗口计数器 | 时间窗内计数,超过阈值则拒绝 | 差(窗口切换时有"双倍突发") | 低开销 | 简单 API QPS 限制 |
| 令牌桶 | 以恒定速率向桶中添加令牌,请求需消耗令牌后才能通过 | 强(桶满时可突发消费存量令牌) | 中 | CDN 加速、消息队列限速 |
| 漏桶 | 请求进入桶中,以固定速率流出处理 | 无(强制匀速) | 高(有排队延迟) | 需要平滑流量的网关层 |
**滑动窗口的改进版——Leak Bucket vs Token Bucket:**
- 令牌桶适合保护生产者:允许突发流量,但长期受限于平均速率。
- 漏桶适合保护消费者:无论输入多猛,输出永远匀速,但延迟不确定。
> [!NOTE]
> Redisson 的限流器使用令牌桶算法实现 `rate(10, RateInterval.SECONDS, RateLimitRate.SEGMENTED)`,支持分布式环境下跨节点的精确计数。
### 熔断三态转换
```mermaid
stateDiagram-v2
[*] --> Closed: 初始状态
Closed --> Open: 失败率超过阈值 (如 >50%)
Open --> HalfOpen: 等待探测期超时 (如 30s)
HalfOpen --> Closed: 探测请求全部成功
HalfOpen --> Open: 探测请求仍有失败
Open --> Closed: 直接硬切回 (应急手段)
state Closed {
[*] --> Monitoring: 持续统计失败比例
}
state Open {
[*] --> RejectAll: 所有请求直接拒绝
}
state HalfOpen {
[*] --> ProbeLimited: 有限探针进入
}
```
**各状态的详细行为:**
| 状态 | 请求处理方式 | 统计维度 | 自动恢复? |
|------|------------|---------|----------|
| Closed(关闭) | 正常转发 | 统计最近 N 秒的请求成功/失败率 | 当失败率低于阈值且连续 M 次成功 |
| Open(打开) | 直接拒绝(FastFail),不调用下游 | 不统计(因为根本不发请求) | 等待 ProbeTimeout 进入 HalfOpen |
| HalfOpen(半开) | 发出少量探测请求(通常 1~3 个) | 只统计这几次探测结果 | 全成功 → Closed;任一失败 → Open |
**半开探测策略:**
- 探测请求数量通常是固定的(如 Sentinel 默认 1 个),而非渐进式增加(像 Hystrix 那样)。
- 探测期间原有业务请求仍然被熔断拦截,避免探测还没结束就压力暴增。
- 如果探测成功立即转入 Closed,否则重新进入 Open 并重置倒计时。
### Sentinel — 规则推送 + 指标统计
阿里巴巴开源的流量控制组件,核心特性:
1. **双链路指标采集**:滑动时间窗口 + 并发线程数两种维度。
2. **规则动态推送**:支持本地文件、Apollo、Nacos 等多种数据源,规则变更无需重启。
3. **多级限流粒度**:API 级别、方法级别、URL 级别、资源级别。
4. **系统自适应保护**:根据 Load、CPU 使用率、总 RPS 等自动调整限流阈值。
```go
// Sentinel Go SDK - 规则定义
import "github.com/alibaba/sentinel-golang/api"
import "github.com/alibaba/sentinel-golang/core/slot"
rule := &flow.Rule{
Resource: "user-query",
ThresholdType: flow.QPS,
Threshold: 100,
ControlBehavior: flow.Reject,
}
api.AddFlowRules([]*flow.Rule{rule})
```
### Resilience4j — 函数式 API
与 Spring Cloud CircuitBreaker 深度集成,采用装饰器模式:
```java
// Resilience4j 熔断器 + 重试组合
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.failureRateThreshold(50)
.waitDurationInOpenState(Duration.ofMillis(1000))
.slidingWindowSize(10)
.build();
io.github.resilience4j.circuitbreaker.CircuitBreaker cb =
CircuitBreaker.of("user-service", config);
Retry retry = Retry.of("user-service", RetryConfig.custom()
.maxAttempts(3)
.waitDuration(Duration.ofMillis(100))
.build());
Decorators.ofSupplier(() -> userService.getUser(id))
.retry(retry)
.circuitBreaker(cb)
.fallback(e -> new User(-1, "default"))
.get();
```
> [!TIP]
> 面试常考点:Resilience4j 和 Hystrix 的核心区别?Hystrix 基于线程池隔离(重),Resilience4j 基于信号量隔离(轻),且两者都是函数式设计而非继承 Thread。
### 降级的兜底思路
| 降级方式 | 示例 | 适合场景 |
|---------|------|---------|
| 返回缓存 | 用户信息从 DB 查不到时返回 Redis 旧值 | 对实时性要求不高的读接口 |
| 返回默认值 | 推荐接口超时返回热门商品列表 | 非核心业务流程 |
| 折叠请求 | 多个相同请求合并为一个真实请求 | 热点 Key 防护 |
| Mock 数据 | 开发环境/预发布时直接返回预设 JSON | CI/CD 阶段 |
## 代码示例
Go 中使用信号量实现简单的并发限流:
```go
type SemaphoreLimit struct {
sem chan struct{}
}
func NewSemaphoreLimit(maxConcurrent int) *SemaphoreLimit {
return &SemaphoreLimit{sem: make(chan struct{}, maxConcurrent)}
}
func (s *SemaphoreLimit) Allow() bool {
select {
case s.sem <- struct{}{}:
defer func() { <-s.sem }()
return true // 放行
default:
return false // 拒绝
}
}
```
## 实践场景
**秋招高频问题:**
- "Sentinel 的滑动窗口和传统的固定窗口有什么区别?" — 固定窗口在边界处会漏算(上一个窗口的尾巴 + 下一个窗口的头同时超限);Sentinel 将窗口细分为更小的 tick,在每个 tick 内独立计数再聚合,精度由 tickSize 决定。
- "为什么熔断要从 Open 到 HalfOpen 而不是直接回 Closed?" — HalfOpen 是一个安全阀:如果下游还在恢复中,直接进入 Closed 会让流量瞬间全部灌入,可能导致二次雪崩。有限的探测请求可以验证下游是否真的恢复了。
- "令牌桶和漏桶选型建议?" — 网关层对外暴露 API 时用令牌桶(允许客户端合理突发);内部服务间调用时用漏桶(保护消费方不被打爆)。
> [!WARNING]
> 不要把限流的阈值设得太激进。建议先观察线上 P99/RPS 基线,然后设为基线的 1.5~2 倍作为第一道防线,配合监控告警逐步调优。
## 关联笔记
- [[服务注册与发现]]
- [[负载均衡算法]]
- [[配置中心设计]]