微服务与网关
这一课你将能做到
- 用三条可观测信号判断本站单体该不该拆,而不是凭感觉
- 写出 Spring Cloud Gateway 的路由配置,说清网关与 Nginx、BFF 的职责边界
- 设计一次 Strangler Fig(绞杀者模式)渐进迁移方案,做到随时可回退
为什么要学
「我们该上微服务吗」是技术决策里最容易被情绪化的问题。拆早了是自找分布式复杂度(分布式事务、链路追踪、运维翻倍);拆晚了是发布互相阻塞、一个模块 OOM 全站陪葬。这课给你的是判断框架,不是信仰。
面试必考:「单体和微服务怎么选?」「服务拆分的粒度?」「网关的作用?」「单体怎么平滑迁移到微服务?」——最后一问用绞杀者模式回答,是高级感所在。
人话:单体像一家人一起做饭,省事但谁想加菜全桌等;微服务像楼下美食城,每个档口独立经营,但要引入统一的收银台(网关)、配送(服务调用)、卫生标准(监控)。摊位越多越自由,协调成本也越高。
先建立图像
本站现状:一个 Spring Boot 单体装着题目管理、判题调度、用户进度、排行榜。什么时候到拆分临界点?三条信号:
信号 1(扩展差异化):判题机要扩容到 50 台,但题目服务 3 台就够
—— 资源画像完全不同,却绑在同一发布单元
信号 2(发布争抢):运营改题目和判题引擎升级总在打架,
一次回归测试要覆盖全站
信号 3(团队规模):多于 2 个披萨团队(每队 4~8 人),
代码合并冲突频繁、领域边界模糊
只有信号 1 时,先拆「部署」(判题 worker 独立部署,代码还是同一个仓库甚至同一个 jar);三条信号都有才考虑拆「服务」。先拆部署单元,再拆代码仓库,最后拆团队——这是最省钱的路径。
拆分后的形态:
Web / 小程序 / App
│
API Gateway(鉴权、限流、路由)
│
┌──────┼──────────┬──────────┐
题目服务 判题服务 用户服务 排行榜服务
(problem) (judge) (user) (ranking)
MySQL Kafka MySQL Redis
组件职责表:
| 组件 | 职责 | 本站实现 |
|---|
| API Gateway | 统一入口:鉴权、限流、路由、TLS、灰度 | Spring Cloud Gateway / Nginx |
| BFF | 为特定前端聚合裁剪接口,减少往返 | 网关聚合或轻量聚合层 |
| 服务发现 | 实例注册与健康,动态寻址 | Nacos / Consul / K8s Service |
| 服务间通信 | 同步 RPC / 异步事件 | OpenFeign + Kafka |
跟着做
步骤 1:网关路由配置(Spring Cloud Gateway)
spring:
cloud:
gateway:
routes:
- id: problem-route
uri: lb://problem-service # lb=从注册中心负载均衡
predicates:
- Path=/api/problems/**
filters:
- name: RequestRateLimiter # 网关层统一限流
args:
redis-rate-limiter.replenishRate: 100
redis-rate-limiter.burstCapacity: 200
- id: judge-route
uri: lb://judge-service
predicates:
- Path=/api/submissions/**
- id: judge-internal-deny # 内部接口外部禁止直达
uri: no://op
predicates:
- Path=/internal/**
filters:
- name: SetStatus
args:
status: 403
三条路由说明网关的三个核心动作:按路径路由到不同服务、统一限流、保护内部接口。鉴权也在网关做(解析 token 后透传 userId Header),下游服务不再重复校验。
步骤 2:服务间调用的正确姿势
// 同步:题目页需要用户服务补充「我的提交状态」
@FeignClient(name = "user-service")
public interface UserClient {
@GetMapping("/internal/users/{id}/progress")
Map<Long, String> getProgress(@PathVariable long id, @RequestParam List<Long> problemIds);
}
// 异步:判题结果走事件(第 6 课已建),不阻塞判题链路
选择标准:实时要答案的用同步(带超时+熔断),不阻塞主流程的用事件。判题完成→排行榜是异步;题目页→我的进度是同步聚合。
步骤 3:BFF 消灭 Chatty I/O
题目详情页原来要调 3 个接口(题目信息、我的进度、相关推荐):
改造前:浏览器 → 3 次 HTTP → 3 个服务 RTT 3×50ms + 串行
改造后:浏览器 → 1 次 /api/problem-page → BFF 并行调 3 个服务
RTT = max(3 次并行调用) ≈ 60ms,且前端逻辑简化
BFF 可以是网关的聚合 filter,也可以是独立薄层。注意分工:网关管「能不能进」(安全),BFF 管「怎么拼」(体验)。
步骤 4:Strangler Fig 渐进迁移(面试压轴题)
第 1 步:新服务实现排行榜逻辑,数据双写(单体写 MySQL,同时投事件给新服务)
第 2 步:网关加灰度路由 —— /api/rank/** 10% 流量走新服务,比对两边响应
第 3 步:逐步 10% → 50% → 100%,期间新服务读模型从事件重建,随时可回退到单体
第 4 步:100% 稳定 2 周后,删除单体里的排行榜代码 —— 数据迁移完成
绞杀者的名字来源:新服务像藤蔓缠绕旧树(单体),逐枝替换,最后旧树枯死移除。每一步都可回退,是它与大爆炸重写的本质区别。
讲清楚要点
- 微服务的收益是组织和扩展,不是性能:拆分让团队独立发布、资源画像独立扩容;单纯追求性能,缓存+多实例通常更便宜。没有组织痛点和扩展痛点就不拆。
- 按业务能力拆,不按技术层次拆:拆成「读服务/写服务/定时任务服务」是错误示范,改一个需求要跨三个服务。正确切法是题目、判题、用户这种领域边界(DDD 的限界上下文)。
- 网关是安全与治理的收口:鉴权、限流、灰度、审计统一在网关,下游专注业务。但要防网关变成「上帝服务」——业务逻辑一律下沉,网关只做横切关注点。
- 每个服务独立数据(Database per Service):共享数据库的微服务是假微服务——表结构一改,全站一起发布。跨服务取数走 API 或事件投影,代价是跨服务一致性(第 3 课的幂等与补偿派上用场)。
- 可观测性必须先行:拆分后一次请求跨 3 个服务,没有 traceId 贯穿就是盲飞。先上链路追踪(Micrometer Tracing/SkyWalking)、统一日志、每个服务独立大盘,再开始迁移。
你可能会懵的地方
-
拆多细合适?
看两个约束:一个服务一个团队(康威定律),一个服务一个数据域。经验信号是「这个服务能不能独立发布而不牵连别人」。宁粗勿细,拆错了再合并的成本远小于拆多了的协调成本。
-
服务间数据一致性怎么保证?
放弃跨服务强一致事务(Seata 能做但复杂度高),用 Saga 模式:每步可补偿,失败逆序回滚。本站场景:提交服务写库+发事件,判题服务失败时发补偿事件回滚状态——衔接第 3 课幂等与第 6 课死信。
-
服务发现是什么,K8s 里还需要吗?
服务解决「实例动态上下线后调用方怎么找到它」。Nacos/Consul 是注册中心方案;K8s 的 Service/DNS 天然提供了服务发现,此时应用内可以不再引入注册中心组件——基础设施给了就不要重复造。
-
单体性能不够,拆微服务能救吗?
大概率不能。瓶颈分析(第 2 课)显示本站卡在 DB 和判题池,拆服务不增加 DB 容量也不增加判题机。先做缓存、读写分离、异步化,确认瓶颈是「互相拖累」而不是「资源不足」时才拆。
-
网关和 Nginx 重复了?
流量大时两者并存分层:Nginx(或云 LB)在最外层做 TLS 终结与静态分流,网关在后面做业务路由与治理。小流量 Nginx 直接路由到各服务即可,网关是治理需求出现后(多团队、灰度、统一限流)才引入。
错法对照
| 错法 | 问题 | 正解 |
|---|
| 按技术层拆服务(读/写/任务) | 一个需求跨三个服务发布 | 按业务能力/限界上下文拆 |
| 多个服务共享一个 MySQL | 表结构耦合,发布互相阻塞 | Database per Service + 事件同步 |
| 网关里写业务逻辑 | 网关成上帝服务,谁都改不动它 | 网关只做鉴权限流路由等横切职责 |
| 大爆炸重写替代渐进迁移 | 半年新系统不上线,老系统停维护 | Strangler Fig 每步可回退 |
| 拆完才上链路追踪 | 跨服务问题无从排查 | 迁移第一天就贯穿 traceId |
卡点急救
- 网关路由 404/503:查三处——服务是否注册到注册中心(
lb:// 依赖服务名精确匹配)、路由 predicate 是否被前面的路由抢先匹配(顺序敏感)、网关到注册中心的网络。看网关日志的 RouteMatch 输出最快定位。
- 服务间调用偶发超时:Feign 默认超时可能过短或过长(长会拖死线程池),显式配置 connect/read 超时 + 重试次数;超时集中在某接口说明该服务慢,用链路追踪定位到具体方法。
- 双写期间新旧数据不一致:双写必有窗口,接受「新服务读模型最终一致」,用对账任务修数;关键校验场景(计分)以旧系统(单体 DB)为准,新系统只读影子——灰度期数据正确性 > 一致性完美。
面试常问
-
单体到微服务,什么时候拆、按什么拆?
先给三条信号(扩展差异化、发布争抢、团队规模),强调「先拆部署再拆代码最后拆组织」的省钱路径;拆分依据是业务能力(限界上下文),配 Database per Service;最后补「没有这些信号就不拆,单体多实例+模块化是更优解」——能说出不拆的条件是加分项。
-
API Gateway 和 BFF 的区别?
网关是横切收口(鉴权/限流/路由/灰度),面向所有客户端;BFF 是体验层(聚合/裁剪/协议适配),面向特定前端(Web 一个、小程序一个)。可以都不要(小项目),可以网关+BFF(中大型)。用本站题目详情页聚合三服务的例子说明 BFF 价值。
-
如何保证微服务拆分后的数据一致性?
三层作答:① 架构上接受最终一致,事件驱动(Kafka)同步;② 补偿上用 Saga,每步设计补偿动作,失败逆序执行;③ 兜底上消费端幂等 + 对账任务。强调「放弃分布式强一致事务不是妥协,是正确的工程判断」。
自测三问
- 本站三条拆分信号分别是什么?只有第一条时你先做什么?
- 网关、BFF、Nginx 三者职责如何划分?小流量时可以省掉谁?
- 写出 Strangler Fig 迁移排行榜服务的四个步骤,每步的回退手段是什么?
小结一句话
微服务是组织与扩展问题的工具而非信仰——先拆部署单元再拆服务,Gateway 管安全横切、BFF 管聚合体验,Strangler Fig 让每一步迁移可回退。
延伸