主题
界面
主题
《微服务架构与拆分原则》讲了"为什么要拆";本节讲**"拆完之后靠什么把它们连起来"**:
Spring Cloud 的一整套组件,都在解决同一类问题:
「服务很多、互相调用、还会挂」——怎么让整体还能正常工作 ✓★ 本节重点讲其中最核心的一个:网关 ✓ —— 因为它是所有流量的必经之路,也是问题最先暴露的地方 ✓✓
先看一个"网关扛不住"的真实场景 ✗✗:
现象:网关 CPU 100%,所有服务都超时,但后端服务本身很闲 ✗
排查:网关的鉴权过滤器里**每次都远程调用鉴权服务** ✗
→ 鉴权服务慢 50 ms,网关的线程全卡在那儿 → 网关成为瓶颈 ✗✗★ 本节的第 5.3 节就是讲这个 ✓ —— 网关的性能陷阱不在"过滤器多",而在"过滤器里有远程调用" ✓✓
| 组件 | 解决什么 | 本项目对应章节 |
|---|---|---|
| 注册与配置(Nacos)★★★ | 服务在哪、配置是什么 | 《Nacos 注册与配置中心》 ✓ |
| 网关(Gateway)★★★ | 统一入口、路由、鉴权、限流 | 本节 ✓ |
| 负载均衡(LoadBalancer)★★ | 请求分给哪个实例 | 《Nginx 负载均衡与缓存》 ✓ |
| 熔断限流(Sentinel)★★★ | 依赖出问题时不被拖死 | 《Sentinel 限流与熔断》 ✓ |
| 分布式事务(Seata)★★ | 跨服务的数据一致性 | 《Seata 分布式事务》 ✓ |
| 链路追踪(Sleuth/Micrometer)★★★ | 一次请求经过了哪些服务 | 见 5.4 ✓ |
★ 一条主线 ✓✓:
注册与配置是"通讯录",网关是"门",负载均衡是"分诊",熔断是"保险丝",追踪是"行车记录仪" ✓
★ 四项职责 ✓:
| 职责 | 说明 |
|---|---|
| 路由 ★★★ | 把 /api/order/** 转给订单服务 ✓ |
| 过滤 ★★★ | 鉴权、日志、埋点、改写请求头 ✓ |
| 限流 ★★ | 按维度限制速率(见 5.5)✓ |
| 协议转换 ★ | 对外 HTTPS / 对内 HTTP(TLS 卸载,《Nginx 安装与反向代理》)✓ |
★★ 三个"不要做"(分层原则) ✓✓:
| # | 不要做 | 原因 |
|---|---|---|
| ① | 不要在网关里写业务逻辑 ✗✗ | 那会让网关变成新的单体,且改业务要发网关 ✓ |
| ② | 不要在网关里做重 IO(查库、调多个服务)✗✗ | 它是所有流量的必经之路 ✓✓ |
| ③ | 不要让网关成为单点 ✗✗ | 它自己也要多实例 + 前面还有 LB ✓ |
★ 一句话定位 ✓✓:
网关只做"所有服务都需要、且与业务无关"的事 —— 路由、鉴权、限流、TLS 卸载、跨域、灰度、日志、追踪入口 ✓
★ 陷阱一:路由匹配 ★★
本节的实验用"30 条路由逐个匹配、命中最后一条"来测最坏情况 ✓ —— 每条路由都要做一次字符串比较或正则匹配 ✓
★ 关键认识 ✓✓:
单次匹配只要几微秒,但网关是「每个请求」都要做 ✓
10 万 QPS 下,全花在路由匹配上的 CPU 时间就很可观 ✗
所以真实网关的优化是「别逐个匹配」 ✓✓: 按前缀建 Trie、把最贵的正则路由放到最后、预编译
Pattern、缓存路由结果 ✓
★ 陷阱二:过滤器链 ★
本节的实验对比了 0 / 1 / 3 / 6 / 10 个过滤器的开销 ✓ —— 但这个数字其实不重要 ✗ ——因为空过滤器的开销几乎可以忽略 ✓
★★ 真正的陷阱是第三个 ✓✓:
| 过滤器里做什么 | 成本 |
|---|---|
| 解析 JWT 并本地验签 ✓ | 微秒级 ✓ |
| 查 Redis 做限流 ⚠️ | 一次网络往返(0.1~1 ms) ✗ |
| 调鉴权服务 ✗✗ | 一次远程调用 ✗✗ |
★★★ 对策 ✓✓:
| # | 对策 | 说明 |
|---|---|---|
| ① | JWT 用公钥本地验签 ★★★ | 不要每次都调鉴权服务 ✓✓ |
| ② | 两级限流 ★★ | 本地计数器粗筛 + Redis 精确 ✓ |
| ③ | 业务逻辑放各自的微服务 ★★★ | 网关只做通用逻辑 ✓✓ |
★ 没有它会发生什么 ✗✗:
用户报"下单很慢" → 你查订单服务日志(正常)→ 查库存服务(正常)
→ 查优惠券服务(也正常)→ **到底哪慢?** ✗✗★ 核心机制只有两件事 ✓✓:
| 机制 | 作用 |
|---|---|
| TraceId ★★★ | 一次请求的全局唯一 ID,跨进程透传 ✓✓ |
| Span ★★ | 每个服务内部的一段耗时,拼起来就是调用树 ✓ |
★ 实现要点 ✓:
① 入口生成 TraceId(网关)
② 每次远程调用把它放进请求头(如 traceparent / X-Trace-Id)
③ 每个服务把它放进日志的 MDC → 日志里能按 TraceId 搜全链路 ✓✓★ 一条实战经验 ✓✓:
TraceId 一定要打进日志 ✓
否则"能追踪"只存在于控制台里,排查时还是得靠猜 ✗
★ 本项目已有的基础 ✓:《日志框架与日志规范》 讲过日志与 traceId 的落地 ✓
★ 三种算法的差别(本节的实验对比了它们的实际放行数) ✓:
| 算法 | 行为 | 适用 | 代表 |
|---|---|---|---|
| 固定窗口 ✗ | 窗口边界可能双倍放行 ✗✗ | 精度要求不高 | 计数器 + 过期 ✓ |
| 滑动窗口 ✓✓ | 准确控制总速率 | 要求精确 | Sentinel 默认 ✓ |
| 令牌桶 ✓✓ | 允许突发(桶里有存量就能一次性拿走) | 秒杀放行一批 | Guava RateLimiter ✓ |
| 漏桶 ✓ | 强制匀速 | 保护脆弱的下游 | nginx limit_req ✓ |
★ 固定窗口的问题(本实验刻意设计的对比) ✓✓:
限额 100 QPS,请求均匀到达时看不出差别;
但如果请求集中在窗口边界(如 0.9s~1.1s),
固定窗口会放行「前一个窗口的 100 + 后一个窗口的 100 = 200」✗✗★★ 四个限流维度(选错会"误伤"或"漏网") ✓✓:
| 维度 | 适用 | 风险 |
|---|---|---|
| 按 IP ★ | 未登录 / 防刷 | NAT 后大量用户共用一个 IP → 误伤 ✗✗ |
| 按用户 ★★ | 已登录业务 | 需要先解析登录态(有成本)⚠️ |
| 按接口 ★★ | 保护特定昂贵接口 | 无法防"单用户刷全站"⚠️ |
| 按 API Key ★★★ | 开放平台 | 最推荐 ✓✓ |
★★ 一条最重要的限流原则 ✓✓:
限流要限在「被保护方的能力」上,而不是拍脑袋定数字 ✗
先压测出下游容量(T2-003 的方法)✓
再按容量的 70%~80% 设阈值 ✓✓
★ 还有一条:限流之后要有"优雅的拒绝" ✓✓:
| 返回 | 客户端行为 |
|---|---|
429 + Retry-After ✓✓ | 客户端会按提示退避重试 ✓ |
| 直接断连或返回 500 ✗✗ | 客户端可能立刻重试 → 放大问题 ✗ |
★ 本节的实验模型 vs Spring Cloud Gateway 配置 ✓:
spring:
cloud:
gateway:
routes:
- id: order-route
uri: lb://order-service # lb 表示走负载均衡(S4-016)
predicates:
- Path=/api/order/** # 路径匹配
- Method=GET,POST # 方法匹配
- Header=X-Api-Version, v1 # 头匹配
filters:
- StripPrefix=1 # 去掉 /api 再转发
- name: RequestRateLimiter # 限流
args:
redis-rate-limiter.replenishRate: 100 # 令牌桶:每秒补 100
redis-rate-limiter.burstCapacity: 200 # 桶容量(允许突发到 200)
key-resolver: "#{@userKeyResolver}" # 按用户 / 按 IP / 按接口★ 三个要点 ✓✓:
| # | 要点 | 说明 |
|---|---|---|
| ① | lb:// 前缀 ★★ | 表示"按服务名 + 负载均衡找实例"(依赖 《Nacos 注册与配置中心》 的注册中心)✓ |
| ② | replenishRate vs burstCapacity ★★★ | 前者决定稳态速率,后者决定允许多大的突发 ✓ |
| ③ | key-resolver 决定限流维度 ★★★ | 选错就是 5.5 表的风险 ✓ |
spring:
cloud:
gateway:
routes:
- id: user-route
uri: lb://user-service
predicates:
- Path=/api/user/**@Bean
public KeyResolver userKeyResolver() {
// 从请求里取用户 ID 作为限流的 key
return exchange -> Mono.justOrEmpty(
exchange.getRequest().getHeaders().getFirst("X-User-Id"))
.defaultIfEmpty("anonymous");
}// ✗ 网关里查库存、算价格、判断能不能下单
if (!inventoryService.hasStock(productId)) { return 400; }★ 后果 ✓: ① 网关变成单体;② 每次请求多一次远程调用;③ 改业务规则要发网关 ✗✗
代码:labs/distributed-lab/src/main/java/cn/kb/learn/dist/micro/GatewayLesson.java
运行:
cd labs
.\mvnw.cmd -pl distributed-lab compile
java "-Dstdout.encoding=UTF-8" -cp distributed-lab/target/classes cn.kb.learn.dist.micro.GatewayLesson本实验测三件事 ✓:
| 实验 | 量出什么 |
|---|---|
| 路由匹配 | 30 条路由逐个匹配(命中最后一条)的单次耗时,并用"10 万 QPS 要花多少 CPU"换算 ✓ |
| 过滤器链 | 0 / 1 / 3 / 6 / 10 个过滤器的开销对比(结论:空过滤器几乎可忽略)✓ |
| 限流算法 | 限额 100、300 个请求挤在 1 秒内时,三种算法各自放行多少 ✓✓ |
★ 限流实验的设计意图 ✓:
300 个请求、每 3 ms 来一个(总跨度约 900 ms,小于 1 秒窗口)
固定窗口 → 放行 100(都在同一窗口内,不超限)
滑动窗口 → 放行 100(同样不超限)
令牌桶 → 放行约 190(初始 100 个 + 这段时间补充的约 90 个)
→ **令牌桶"允许突发"的语义就是这样体现的** ✓✓★ 三条可落地的结论 ✓:
| # | 结论 |
|---|---|
| ① | 网关的性能陷阱不是"过滤器多",而是"过滤器里有远程调用" ✓✓ |
| ② | 路由匹配要避免逐个正则匹配(把最贵的放最后 + 预编译 + 缓存)✓ |
| ③ | 限流按"被保护方的容量"定阈值,并返回 429 + Retry-After ✓ |
Thread.sleep(50):观察整体吞吐断崖 ✓✓;| # | 错误 | 说明 |
|---|---|---|
| 1 | 在网关里写业务逻辑 ✗✗ | 网关变单体 ✓ |
| 2 | 过滤器里每次都调鉴权服务 ✗✗ | 网关成为瓶颈 ✓✓ |
| 3 | 网关只部署一台 ✗✗ | 单点 = 全站挂 ✓ |
| 4 | 用固定窗口限流,却在窗口边界被打穿 ✗✗ | 双倍放行 ✓ |
| 5 | 按 IP 限流却不考虑 NAT ✗✗ | 误伤整栋楼/整个公司 ✓ |
| 6 | 限流返回 500 而不是 429 ✗✗ | 客户端重试放大问题 ✓ |
| 7 | 没有链路追踪就上微服务 ✗✗ | 出问题查不出来 ✓ |
目标:把网关的三类问题都亲手跑一遍。
步骤:
GatewayLesson,记录路由耗时、过滤器开销、三种限流的放行数 ✓;Thread.sleep(20)),观察吞吐下降 ★★★;验收方式:能说出"你的网关在最高峰时会先崩在哪一环"。
扩展任务:给网关设计一个"灰度路由"方案:
- name: RequestRateLimiter
args:
key-resolver: "#{@ipKeyResolver}"
redis-rate-limiter.replenishRate: 1000这个配置在什么场景下会误伤用户? 5. (设计)给"开放平台"设计限流方案:不同客户等级不同配额,超限要能看出是谁超的。
replenishRate / burstCapacity(按 key 动态取配额)✓; 拒绝时返回 429 + 剩余配额 + 重置时间,并在日志里带上客户 ID ✓✓《Nacos 注册与配置中心》:服务怎么找到彼此、配置怎么动态生效。