vault backup: 2026-08-08 19:01:04
This commit is contained in:
@@ -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 倍作为第一道防线,配合监控告警逐步调优。
|
||||
|
||||
## 关联笔记
|
||||
|
||||
- [[服务注册与发现]]
|
||||
- [[负载均衡算法]]
|
||||
- [[配置中心设计]]
|
||||
Reference in New Issue
Block a user