元数据卡
- 前置知识:第13章(单体到微服务迁移)
- 预计时间:45 分钟
- 核心难度:进阶
- 完成标志:理解微服务治理的基础模式并能应用
你的进度
你拆了三台独立机器:玩家引擎、比赛引擎、积分引擎。它们之间用魔力管道通信。
好景不长——“积分引擎”被搬到了工坊的新车间,位置变了。玩家引擎的魔力管道图纸里是硬编码的坐标——所有积分查询都断连。你手动改了图纸、重启——第二天另一个车间也部署了积分引擎,分布在不同的工位上。
工匠之都的调度局有一张服务发现地图。你需要一套能让机器自动找到彼此的系统。 你的任务
微服务架构解决的不仅仅是"代码拆开",还有"怎么找到对方"、"怎么转发请求"、"怎么承受失败"、"怎么分发配置"。这章讲的就是服务治理中最常用的模式——不需要任何框架也能理解,但有了框架(Spring Cloud、K8s)能更好落地。
本章分层
- 必读:服务发现、API Gateway、断路器
- 选读:BFF、配置中心
- 进阶:重试与超时策略
本章不会要求你掌握
- Kubernetes Service Mesh (Istio/Linkerd)
- 分布式追踪(OpenTelemetry)
破局 · 溯源
积分服务部署了 3 个副本。玩家服务的配置里只写了 http://score-service-1:8082——另外两个副本没有用上,也找不到。积分服务流量升高时,你的服务没有自动利用新增副本的能力。
你遇到了所有微服务的第一个问题:服务发现。
第一层:服务发现——让服务找到彼此
服务发现是一个"电话簿":当一个服务启动时,它告诉电话簿(注册中心)"我在这个地址"。其他服务来电话簿查"积分服务在哪"。
两种模式:
客户端发现(Client-Side Discovery):
玩家服务 → 注册中心: "积分服务的地址?"
注册中心 → 玩家服务: ["10.0.1.5:8082", "10.0.1.6:8082"]
玩家服务 → 积分服务: 直接 HTTP 调用(客户端做负载均衡)// 概念实现(真实场景用 Spring Cloud Netflix / Eureka)
@Service
public class ScoreClient {
@Autowired
private DiscoveryClient discoveryClient;
public int getScore(String playerId) {
// 从注册中心获取可用实例
List<ServiceInstance> instances = discoveryClient
.getInstances("score-service");
ServiceInstance instance = instances.get(0); // 或用轮询策略
// 构建 URL 并调用
String url = "http://" + instance.getHost() + ":" + instance.getPort()
+ "/api/scores/" + playerId;
return restTemplate.getForObject(url, Integer.class);
}
}服务端发现(Server-Side Discovery):
玩家服务 → DNS 名称 "score-service" → 负载均衡器(如 Nginx/K8s Service)
负载均衡器根据后端列表转发到具体的积分服务实例Kubernetes 模式下,服务端发现更常见——score-service 作为一个 Service name,K8s 自动做 DNS 解析和负载均衡,应用代码里写 http://score-service:8082 即可。
# Kubernetes Service
apiVersion: v1
kind: Service
metadata:
name: score-service
spec:
selector:
app: score-service
ports:
- protocol: TCP
port: 8082
targetPort: 8082// 应用代码——URL 直接写服务名(K8s DNS 自动解析)
private static final String SCORE_SERVICE_URL = "http://score-service:8082";第二层:API Gateway——统一入口
你有了多个服务后,客户端(前端 App)需要知道所有服务的地址:
/players/register → player-service
/matches/create → match-service
/scores/query → score-service你的前端团队开始抱怨:"你们后端每次加一个服务,我就要改前端的 API 地址列表。"
API Gateway 提供了一个统一的入口:
前端 App → /api/** → API Gateway
├── /api/players → player-service
├── /api/matches → match-service
└── /api/scores → score-serviceGateway 还常做:身份验证、限流、请求日志、协议转换。
用 Spring Cloud Gateway 可以实现:
# application.yml
spring:
cloud:
gateway:
routes:
- id: player-service
uri: lb://player-service
predicates:
- Path=/api/players/**
- id: match-service
uri: lb://match-service
predicates:
- Path=/api/matches/**
- id: score-service
uri: lb://score-service
predicates:
- Path=/api/scores/**API Gateway 不是另一个要维护的服务——它是必需的治理组件。没有 Gateway,每个服务需要单独处理认证、限流、日志。
第三层:BFF——不同客户端不同的后端
你的服务对外提供服务:给 Web 应用、移动端 App、第三方 API 各一套接口。三个客户端的 API 需求不同——Web 需要完整数据,App 需要精简数据,第三方只需部分数据。
用一个通用 API Gateway 做所有过滤,Gateway 变得臃肿。BFF(Backend For Frontend)模式为每种客户端建一个专属后端:
Web BFF → player-service, match-service, ...
App BFF → player-service, match-service, ... (不同的聚合逻辑)
Third-party Gateway → player-service (有限接口)每个 BFF 只为自己的客户端优化——Web BFF 返回详细 JSON,App BFF 返回精简 JSON,第三方 Gateway 做限流和 API Key 校验。
// Web BFF —— 聚合多个服务的数据
@RestController
public class WebBffController {
private final PlayerClient players;
private final MatchClient matches;
@GetMapping("/profile/{id}")
public WebPlayerProfile getProfile(@PathVariable String id) {
// BFF 负责聚合
Player player = players.findById(id);
List<Match> recentMatches = matches.findRecentByPlayer(id, 10);
return new WebPlayerProfile(player, recentMatches);
}
}第四层:断路器——防止级联失败
积分服务挂了。你的比赛服务在调用积分服务时等待 30 秒超时——线程池占满。比赛服务也挂了。玩家服务调用比赛服务又等待——连锁反应,整个系统瘫痪。
断路器模式:监控远程调用的失败率。当失败率超过阈值时,"断开"电路——立即返回失败,不再发实际请求。一段时间后,电路"半开",放少量请求试探——如果成功了,闭合电路。
// 使用 Resilience4j 实现断路器
@Service
public class ScoreServiceClient {
@CircuitBreaker(name = "scoreService", fallbackMethod = "getScoreFallback")
public int getScore(String playerId) {
return restTemplate.getForObject(
"http://score-service/api/scores/" + playerId, Integer.class);
}
// 熔断时的降级
public int getScoreFallback(String playerId, Throwable t) {
// 返回默认值或从缓存读取
return 0; // 或者其他备用处理
}
}配置:
resilience4j.circuitbreaker:
instances:
scoreService:
sliding-window-size: 10
failure-rate-threshold: 50 # 50% 请求失败就熔断
wait-duration-in-open-state: 30s # 30 秒后尝试恢复
permitted-number-of-calls-in-half-open-state: 3没有 Resilience4j 时,手动实现的思路:
// 手动断路器(简化版)
public class SimpleCircuitBreaker {
private volatile State state = State.CLOSED;
private int failureCount = 0;
private final int threshold = 5;
private final long timeout = 30000;
private long lastFailureTime = 0;
public boolean allowRequest() {
return switch (state) {
case CLOSED -> true;
case OPEN -> {
if (System.currentTimeMillis() - lastFailureTime > timeout) {
state = State.HALF_OPEN;
yield true;
}
yield false;
}
case HALF_OPEN -> true;
};
}
public void recordSuccess() {
if (state == State.HALF_OPEN) {
state = State.CLOSED;
}
failureCount = 0;
}
public void recordFailure() {
failureCount++;
lastFailureTime = System.currentTimeMillis();
if (failureCount >= threshold
|| state == State.HALF_OPEN) {
state = State.OPEN;
}
}
enum State { CLOSED, OPEN, HALF_OPEN }
}第五层:重试与超时
不是所有失败都需要断路器。网络抖动:偶尔超时一次。直接熔断太重了——重试一下就成功了。
重试策略:
@Retry(name = "scoreService", maxAttempts = 3,
backoff = @Backoff(delay = 500, multiplier = 2))
public int getScore(String playerId) {
return restTemplate.getForObject(...);
}重试策略的核心参数:
- 最大重试次数:通常 2-3 次,再多会系统雪崩
- 延迟时间:初始 500ms,每次翻倍(指数退避)——防止所有重试发起
- 超时时间:每次尝试的独立超时。整体调用时间 = 超时 x (1 + 重试次数)
# 综合配置:超时 + 重试 + 断路器
resilience4j:
timelimiter:
instances:
scoreService:
timeout-duration: 2s
retry:
instances:
scoreService:
max-attempts: 3
wait-duration: 500ms
circuitbreaker:
instances:
scoreService:
failure-rate-threshold: 50重试的正确做法:只在幂等操作上重试。查询是幂等的;"更新积分"可能不是(多次调用会重复加分)。确保重试操作是幂等的或在调用方做去重(idempotency key)。
常见陷阱
陷阱一:API Gateway 成了一个单点瓶颈。 Gateway 处理所有请求,一旦挂了所有服务不可用。做法:把 Gateway 部署多个副本,前置负载均衡器(DNS round-robin 或 CDN)。
陷阱二:断路器、重试、超时的配置冲突。 你设置了 3 次重试(每次 2 秒超时),断路器在 50% 失败率时熔断——结果 2 次重试就到了 6 秒超时,断路器根本没起作用。配置的顺序和叠加需要仔细推算。
陷阱三:每个微服务重复实现认证逻辑。 "每次收到请求都要校验 token,那我每个服务都写一遍。" 把认证放在 API Gateway 层做(集中认证),Gateway 在转发请求到下游时,只传递用户身份信息(不传 token)。
陷阱四:服务发现信息更新延迟。 一个服务实例下线了,但注册中心(或客户端缓存)还在返回它的地址。直到超时才发现。配置合理的健康检查间隔和缓存清理时间。
通关挑战
- 热身:在你的项目里引入一个简单的断路器模式——即使手动实现。将一个 3rd party API 调用包在断路器里,设置熔断阈值。
- 挑战:搭建一个简单的 API Gateway(用 Spring Cloud Gateway 或 Nginx),把 2 个服务的请求路由到不同的后端。
- 观察:找一个生产中的微服务系统,画出它的调用拓扑图,标出哪些环节用了断路器、哪些用了重试。
旅人笔记
微服务不是"把代码拆开部署"就够了——服务发现让服务找到彼此,API Gateway 统一入口与治理,断路器防止雪崩,重试与超时区分瞬时故障与不可恢复故障。基础模式到了,才能让多个服务真正一起工作。
下一站预告
服务发现和容错解决了"通信"的问题。但当你的数据从单体数据库拆成了多个服务各自的数据库——一致性问题出现了。下一章:数据一致性模式——Saga、CQRS、事件溯源、事务发件箱。