一句话速览:微服务治理的核心是”服务之间如何找到彼此(Nacos)、如何保护彼此不被打垮(Sentinel)、如何方便地互相调用(OpenFeign)、以及如何统一对外的门面(Gateway)”——本篇所有踩坑的根因都可以归结为一句话:配置了不等于生效了,治理类功能的失效几乎都是沉默的

涉及原始记录:07-面试问题/开发问题/01-依赖与配置问题.md P0-01、P1-01、P1-02;
07-面试问题/开发问题/02-框架与中间件集成问题.md P1-01、P1-02、P0-06

最近修订:2026-08-07

目录


技术点 1:Nacos —— 注册发现 + 配置中心

难度:⭐⭐ | 掌握要求:能画出注册/发现/配置推送三条链路,能说清临时实例与持久实例的区别

核心概念

Nacos 在项目里承担两个独立的角色,容易被混为一谈:

  1. 服务注册发现:每个微服务启动时把自己(IP+端口)注册到 Nacos,Feign/Gateway 通过服务名找到实际地址,不用写死 IP。
  2. 配置中心application.yml 之外的动态配置(比如 Sentinel 限流规则)存在 Nacos 里,服务启动时拉取、运行时监听变更。这是一个订阅-推送模型:客户端向 Nacos 建立长轮询/gRPC 长连接,Nacos 侧数据变化后主动推送。

原理深挖:注册发现的完整链路

sequenceDiagram
    participant S as 微服务实例
    participant N as Nacos Server
    participant C as 调用方(Feign/Gateway)

    S->>N: 1. 注册实例(IP+Port+元数据)
    S->>N: 2. 每5秒发送心跳(临时实例)
    C->>N: 3. 订阅服务名(长轮询/gRPC推送)
    N-->>C: 4. 推送健康实例列表
    Note over C: 本地缓存实例列表,定期刷新
    C->>S: 5. 按负载均衡策略直接调用
    Note over S,N: 心跳中断15s标记不健康,30s剔除

三个必须理解的设计点:

  • 临时实例 vs 持久实例:临时实例(默认,ephemeral=true)由客户端主动上报心跳,Nacos 超过 15 秒没收到心跳标记不健康、30 秒剔除,数据只存在内存(Distro 协议在 Nacos 集群节点间同步),是 AP 模型——允许短暂读到过期数据,但保证注册中心永远可用;持久实例(ephemeral=false,典型如 MySQL 这种”实例消失就是事故”的中间件)由 Nacos 主动探测,数据落盘并通过 Raft 保持集群一致,是 CP 模型。业务微服务几乎都用临时实例。
  • 客户端本地缓存:调用方不是每次 RPC 都去问 Nacos,而是把实例列表缓存在本地、由 Nacos 推送变更。这意味着”Nacos 上看到的实例列表”和”调用方实际路由到的列表”之间可能存在秒级延迟——服务下线后仍短暂有流量打到旧 IP 是正常的。
  • 配置的三元组定位:一份配置由 namespace + group + dataId 唯一确定。排查”配置没生效”时 90% 的问题是这三者之一对不上——客户端配的 namespace(比如用了环境隔离的 dev namespace)和 Nacos 控制台改的那份不在同一个命名空间,是最经典的乌龙。

我们踩过的坑

坑1:Open API 鉴权方式变更(P1-01)
Nacos 2.x 的 Open API 要求 accessToken 放在 Query 参数里,不是 Request Body。我们最初往 Body 里塞 token,直接被拒绝返回 403。这提醒我们:同一个中间件的大版本升级,连鉴权传参的位置都可能变,不能想当然照抄旧文档。

坑2:手写 SDK 客户端时鉴权信息被遗漏(P0-06,见后文 Sentinel 部分详细展开)
NacosDataSource 有多个构造函数重载,其中”看起来更简单”的 4 参数构造函数(serverAddr, group, dataId, parser)内部根本不支持传鉴权信息。这种问题的可怕之处在于:它不报错、不启动失败,只是订阅静默失败,日志只出现在 Nacos 客户端自己的日志里(ERROR c.a.n.client.config.impl.ClientWorker),业务代码完全无感知。

知识扩展:没踩过但必须知道的

  • Distro 协议:Nacos 自研的 AP 型数据同步协议,用于临时实例数据在集群节点间的最终一致同步。理解它是为了回答面试经典问题”Nacos 是 AP 还是 CP”——正确答案是两者都支持,由实例类型决定
  • 配置监听的失效模式:除了本篇踩过的鉴权遗漏,还有两类高频失效:① dataId 后缀与 file-extension 不匹配(声明了 yaml 却用 .properties 后缀的 dataId),内容解析失败但可能不抛异常;② 改了 Nacos 上的配置但应用没用 @RefreshScope@ConfigurationProperties(后者天然支持刷新),Bean 里的旧值不会变。
  • 服务雪崩时的保护阈值:Nacos 可以配置保护阈值(0~1),当健康实例占比低于阈值时,把不健康的实例也返回给调用方,防止”实例批量不健康 → 流量全部压到少数健康实例 → 连锁崩溃”。这是注册中心层面的兜底思维,和 Sentinel 的流量层面兜底是互补的。

必须记住的结论

  • Nacos 既是注册中心也是配置中心,这两个职责在排障时要分开想:服务发现不通通常是网络/端口/心跳问题,配置拉取不通通常是鉴权/namespace/dataId/group 不匹配问题。
  • 任何时候手写”XxxDataSource”这类基础设施客户端代码,第一反应就是检查有没有正确传鉴权信息,尤其是构造函数有多个重载时,优先选择接受 Properties 的那个,显式塞入 username/password,不要选看起来参数最少的那个。
  • 排查”配置好像没生效”类问题时,不要只看业务服务自己的日志,还要看中间件 SDK 自己打的日志(比如 Nacos 客户端日志),很多失败只在这一层暴露。
  • 记住临时实例的两个时间数字:15 秒标记不健康、30 秒剔除;记住配置定位三元组:namespace + group + dataId

技术点 2:Sentinel —— 限流熔断

难度:⭐⭐⭐ | 掌握要求:能说清两套规则体系的分裂、四种规则类型、三种熔断策略

核心概念

Sentinel 的规则体系比想象中更分裂,理解这一点是排查所有 Sentinel 问题的关键:

  • 业务层限流@SentinelResource 注解标记的方法,规则存在 FlowRuleManager 里,数据源自动装配用 spring.cloud.sentinel.datasource.xxx.nacos.rule-type=flow
  • 网关层限流SentinelGatewayFilter 拦截网关路由请求,规则类型是完全独立的 GatewayFlowRule,存在完全独立的 GatewayRuleManager 里。
  • 这两套规则表互相不认识对方spring-cloud-alibaba 官方针对 Nacos 数据源的自动装配只支持业务层的 rule-type(flow/degrade/param-flow/system),根本没有网关规则的选项——网关规则的 Nacos 订阅代码必须自己手写。

原理深挖:Sentinel 是怎么统计和判断的

Sentinel 的核心实现是一条责任链(ProcessorSlot Chain):每次受保护的调用依次经过 NodeSelectorSlot → ClusterBuilderSlot → LogSlot → StatisticSlot → AuthoritySlot → SystemSlot → FlowSlot → DegradeSlot。其中最关键的是 StatisticSlot——它用滑动时间窗口(LeapArray,默认把 1 秒切成 2 个 500ms 的窗口桶,支持秒级/分钟级两种采样)统计实时的 QPS、RT、异常数,后面的 FlowSlot/DegradeSlot 拿着这些统计数据跟规则比对。

flowchart LR
    A[受保护的调用进入] --> B[StatisticSlot
滑动窗口统计
QPS/RT/异常数] B --> C{FlowSlot
流控规则判断} C -->|QPS/线程数超阈值| D[抛出 FlowException
进入 fallback/blockHandler] C -->|通过| E{DegradeSlot
熔断规则判断} E -->|熔断器打开| F[抛出 DegradeException
走降级逻辑] E -->|通过| G[执行真实业务逻辑] G --> H[统计结果回写窗口]

四种规则类型各管一件事,面试高频:

规则类型 作用 典型阈值
flow(流控) 限制进入的流量 QPS 阈值、并发线程数;支持直接拒绝/排队(Warm Up)/匀速排队模式
degrade(熔断) 下游不稳定时切断调用,快速失败 慢调用比例(RT 超阈值的比例)、异常比例异常数三种策略
param-flow(热点参数) 对”同一个参数值”的请求限流(如某个热点商品 ID) 参数值维度的 QPS
system(系统) 保护整机不被打垮 系统 Load、CPU 使用率、总 QPS、总线程数

熔断器的工作模式是一个三态状态机:CLOSED(正常)→ 触发阈值 → OPEN(熔断,请求直接快速失败)→ 经过一个”探测窗口期” → HALF-OPEN(放少量请求试探)→ 成功则 CLOSED,失败则继续 OPEN。这和电路保险丝是同一个隐喻,也是”熔断”这个名字的由来。

我们踩过的坑

P0-06:网关限流从项目搭建起就没生效过,直到压测才发现

这是本项目优先级最高的一个真实缺陷,值得完整复盘一遍思路:

  1. 我们最初以为”网关限流”和”业务限流”用同一套 rule-type=flow 配置就够了。规则确实被正常加载了——但是加载进了 FlowRuleManager,而 SentinelGatewayFilter 检查请求时读的是 GatewayRuleManager。两张表毫不相干,规则形同虚设。
  2. 更隐蔽的是,这个问题长期不会被任何人发现:功能测试时只会验证接口能不能通、返回对不对,不会主动测”限流到底生不生效”。只有做压测(200 并发瞬时打进去看有没有 429)才会暴露。
  3. 定位到问题后修复也踩了坑:手写 NacosDataSource 加载 GatewayFlowRule 时用了不带鉴权的 4 参构造函数(见上面 Nacos 部分),又白白排查了一轮。

P1-02:Sentinel Dashboard 在 JDK 21 下不显示数据
这是一个已知的 Sentinel 1.8.6 在新版 JDK 上的 bug,Dashboard 的 Command Server 拉不到数据,但限流本身走独立链路,不受影响。教训是:可视化工具显示异常不代表功能异常,一定要用接口压测去验证功能层,不能光看 Dashboard 有没有数据就下结论。

P1-02(依赖问题文档):Nacos 数据源依赖缺失
sentinel-core/sentinel-spring-cloud-gateway-adapter 不包含 Nacos 数据源实现,必须单独引入 sentinel-datasource-nacos,否则启动直接 ClassNotFoundException。这个是”显式报错”型问题,反而是最好排查的一类。

知识扩展:没踩过但必须知道的

  • 规则持久化的三种模式:① 原始模式——规则写在代码/配置文件里,重启不丢但改规则要重启;② Push 模式(本项目用)——规则推到 Nacos,客户端监听变更,重启后从 Nacos 拉回,Dashboard 上改的规则默认只推到内存、不会写回 Nacos,重启就丢(除非改造 Dashboard 的发布逻辑);③ Pull 模式——客户端轮询外部存储,实时性差,少用。
  • @SentinelResource 的两个回调别搞混blockHandler 处理”被 Sentinel 规则拦下”(FlowException/DegradeException),fallback 处理”业务代码自己抛的异常”。只配 fallback 不配 blockHandler,限流触发的 BlockException 会直接抛给调用方,看起来像没降级。
  • 熔断 vs 降级的语义:熔断(Circuit Breaking)是触发机制——“检测到下游不行,自动切断”;降级(Fallback)是应对手段——“切断之后给一个兜底响应”。两者常一起出现但不是一回事。
  • 集群流控:单机流控只管一个节点,10 个节点各限 100 QPS 实际总阈值是 1000 且不精确。需要精确的全局限流要部署 Token Server 做集群流控,本项目未使用,了解概念即可。

必须记住的结论

  • 业务限流用 @SentinelResource + FlowRuleManager,网关限流用 SentinelGatewayFilter + GatewayRuleManager,两者规则类型(FlowRule vs GatewayFlowRule)、数据源加载方式(官方自动装配 vs 手写代码)完全不同,不能混用同一份配置。
  • 网关层想要限流生效,Nacos 数据源必须自己写代码用 GatewayRuleManager.register2Property() 注册,spring-cloud-alibaba 不会替你做这件事。
  • “配置了规则”不等于”规则生效”,中间存在”规则加载到了错误的地方”这种沉默失败模式。任何限流/熔断类功能上线前,必须用真实流量(哪怕是简单的并发 curl/wrk)验证一次,不能只凭配置文件和日志”看起来没报错”就认为完成了。
  • Dashboard/控制台类可视化工具本身可能有兼容性 bug,功能是否生效永远以接口实际返回结果为准,不以控制台”有没有数据”为准。
  • 记住三种熔断策略的名字和区别:慢调用比例 / 异常比例 / 异常数;记住熔断三态:CLOSED → OPEN → HALF-OPEN

技术点 3:OpenFeign —— 服务间调用

难度:⭐⭐ | 掌握要求:能讲清动态代理原理、熔断降级三要素、fallback 与 fallbackFactory 区别

核心概念

Feign 让跨服务调用写起来像调用本地接口。它的实现原理一句话说透:启动时扫描 @FeignClient 接口,用 JDK 动态代理生成实现类,把方法调用翻译成 HTTP 请求——方法上的注解(@GetMapping 等)决定 URL 和 HTTP 动词,参数注解决定参数放 query 还是 body,返回值由解码器反序列化。服务名到真实地址的转换交给 LoadBalancer 组件(问 Nacos 要实例列表,按策略挑一个)。

flowchart LR
    A[业务代码调用
houseFeignClient.freeze] --> B[JDK 动态代理
拦截方法调用] B --> C[编码器: 方法注解+参数
→ HTTP 请求模板] C --> D[LoadBalancer
livin-house → 实例列表
→ 选中一个 IP:Port] D --> E[HTTP Client 发出请求] E --> F[解码器: 响应 JSON
→ 返回值对象]

要让熔断降级生效,需要三件事同时满足,缺一不可:

  1. 引入 spring-cloud-starter-alibaba-sentinel
  2. 配置打开 feign.sentinel.enabled=true默认是关闭的,这是最容易漏的一步)
  3. @FeignClientfallbackFactory 而不是更简单的 fallback

我们踩过的坑

P1-02(依赖问题文档):FallbackFactory 不生效
livin-leaselivin-house 冻结房源,house 异常后本该走降级,结果直接抛 500 到调用方。三个必要条件里漏了 feign.sentinel.enabled=true 这一条,导致 Sentinel 根本没有接管这个 Feign 调用,自然不会触发降级。

fallbackfallbackFactory 的区别也值得记住:fallback 拿不到具体的异常原因,只能返回一个固定的兜底值;fallbackFactory 通过 FallbackFactory<T> 接口能拿到触发降级的 Throwable,可以针对性记录日志、区分是超时还是熔断还是业务异常。项目里统一用 fallbackFactory,方便后续排查降级原因。

知识扩展:没踩过但必须知道的

  • 超时配置的默认值陷阱:Feign 默认连接超时 10s、读超时 60s——读超时 60 秒对在线接口来说几乎等于”没有超时保护”,下游一旦慢响应,调用方线程会被长时间占住,可能引发调用方的线程池耗尽(这是雪崩的典型传导路径)。实践中应显式配置 feign.client.config.default.connectTimeout/readTimeout,读超时一般设 2~5 秒。
  • 默认不重试:Feign 默认 Retryer.NEVER_RETRY,失败即抛异常。开启重试要小心——非幂等的写接口重试可能造成重复执行(比如重复扣款),这是”重试”和”幂等”必须一起考虑的原因(幂等设计详见 10-JVM与并发编程.md)。
  • 第一刀慢的问题:Feign 首次调用要初始化连接池、做懒加载,第一次调用明显偏慢,健康检查/预热流量可以先打一发避免用户吃到。
  • 拦截器的正确用途RequestInterceptor 是传递跨服务上下文的官方通道——本项目 Seata 的 xid、以及”网关注入的用户 Header 继续往下一跳服务透传”都是在这层做的。凡是”调用链上的每个服务都需要的信息”,都应该通过拦截器统一注入,而不是在每个 Feign 方法上加参数。

必须记住的结论

  • Feign + Sentinel 熔断降级生效需要”依赖 + 开关配置 + 注解写法”三件事同时成立,任何一个环节漏了,表现都是”降级没生效,异常直接抛出去了”,容易被误判成”Sentinel 没配对”而忽略掉 feign.sentinel.enabled 这个开关。
  • 优先用 fallbackFactory,能拿到异常原因,对排障更友好。
  • 记住 Feign 的两个隐性默认值:默认不重试、读超时 60 秒过长——生产配置必须显式覆盖读超时。
  • 跨服务上下文(用户身份、事务 xid、traceId)的统一传递通道是 RequestInterceptor

技术点 4:Spring Cloud Gateway —— 统一入口

难度:⭐⭐ | 掌握要求:能讲清路由三要素、过滤器执行顺序、网关为什么是非阻塞模型

核心概念

Gateway 在本项目里承担两个职责:路由转发(把 /api/house/** 转发到 house 服务)和统一鉴权(JWT 全局过滤器)。这两个职责如果配置上打架,会互相影响。

原理深挖:一个请求在网关里经历了什么

Gateway 不是传统的 Servlet 应用,它构建在 Spring WebFlux + Netty 之上,是非阻塞 I/O 模型——少量线程(默认 CPU 核数个 EventLoop)就能处理大量并发连接,线程不会因为等待下游响应而阻塞挂起。这就是为什么网关这类”I/O 密集、几乎无业务计算”的组件适合用 WebFlux,而传统 Spring MVC(每请求一个线程)做网关在高并发下线程数会先爆。

一个请求的处理链路由三个核心概念组成:

flowchart TD
    A[客户端请求] --> B&#123;Route Predicate
路由谓词匹配&#125; B -->|/api/house/** 匹配| C[命中 livin-house 路由] B -->|都不匹配| D[404] C --> E[GlobalFilter 链
按 order 从小到大执行] E --> F[JwtAuthGlobalFilter
验签/注入用户Header] F --> G[GatewayFilter 链
路由级过滤器] G --> H[NettyRoutingFilter
真实转发到下游服务] H --> I[下游响应后
过滤器链逆序执行 post 逻辑]
  • Route(路由):转发规则的基本单位,包含 ID、目标 URI、谓词集合、过滤器集合。
  • Predicate(谓词):判断请求是否命中该路由的条件(Path/Method/Header/Query 等),可组合。
  • Filter(过滤器):分 GlobalFilter(对所有路由生效,如 JWT 鉴权)和 GatewayFilter(只对绑定路由生效)。过滤器有 pre/post 两阶段:请求转发前按 order 从小到大执行 pre 逻辑,下游响应返回后逆序执行 post 逻辑——这和 Servlet Filter 的”链式进、链式出”是一致的。排查”为什么我的过滤器没执行/顺序不对”时,先看 order 值。

我们踩过的坑

P1-01:白名单遗漏支付回调接口
第三方支付平台的异步回调请求不会带 JWT token(它是外部系统发起的,不是登录用户),但 Gateway 的全局 JWT 过滤器默认会拦截所有请求,回调路径必须显式加入白名单:

1
2
3
4
5
6
7
8
private static final List<String> WHITE_LIST = List.of(
"/api/auth/sms/send",
"/api/auth/login/**",
"/api/payment/callback/**", // 支付回调(第三方异步通知),必须放行
"/api/house/list",
"/api/house/{houseId}/detail",
"/api/search/**"
);

这类问题的通用规律:任何”非用户主动发起、无法携带登录态”的请求(第三方回调、内部服务间调用、健康检查探针)都需要显式加入白名单,否则会被统一鉴权拦截。

P2-01:Knife4j 聚合文档路由冲突
Gateway 同时配置路由转发和 swagger-ui 聚合,两者对 /v3/api-docs 路径处理冲突,需要在 Gateway 单独配置 springdoc.swagger-ui.urls 显式列出每个子服务的文档地址,而不是依赖自动聚合。这是一个相对边缘的问题,了解即可。

P0-06:网关限流规则加载(见上面 Sentinel 部分)

知识扩展:没踩过但必须知道的

  • 白名单匹配方式的坑List.contains 全等匹配和 Ant 风格路径匹配(/api/auth/login/**)不是一回事,混用时要确认匹配工具类支持通配符(AntPathMatcher),否则配了 /** 实际按全等比较,等于白配——这又是一个”配置了不生效”的沉默失败高发区。
  • 网关层的超时与重试:Gateway 转发本身也有超时(spring.cloud.gateway.httpclient.response-timeout),下游长时间不响应时网关的表现(504 还是自定义兜底)取决于这里;Retry GatewayFilter 可以配重试,但和 Feign 重试一样,只对幂等的 GET 类请求安全
  • 网关不是银弹:所有流量过网关意味着网关是单点放大器——网关抖动影响全部业务。生产上网关本身要多实例部署 + 前置 SLB/Nginx,本项目单机部署是演示环境取舍,面试被问到要能说出来。

必须记住的结论

  • Gateway 是统一鉴权入口,也是所有”无法带登录态”请求的例外白名单管理入口。新增任何对外的 webhook/回调接口,第一件事就是检查网关白名单。
  • Gateway 身兼路由转发、鉴权、限流三个职责,排查网关相关问题时要先明确是哪个职责出的问题,不要混着看。
  • 记住过滤器执行规律:pre 阶段按 order 升序,post 阶段逆序;全局过滤器对所有路由生效,路由过滤器只对绑定路由生效。
  • Gateway 基于 WebFlux/Netty 非阻塞模型,少量线程扛高并发——这是它区别于 Tomcat+MVC 传统网关方案的核心优势。

术语表

术语 含义
临时实例(Ephemeral Instance) Nacos 中靠客户端心跳保活的服务实例,AP 模型,默认类型
Distro 协议 Nacos 自研的 AP 型数据同步协议,用于临时实例数据集群内最终一致
namespace/group/dataId Nacos 中唯一定位一份配置(或服务分组)的三元组
滑动时间窗(LeapArray) Sentinel 统计实时指标的环形数组结构,把统计周期切成多个窗口桶
熔断三态 CLOSED(正常)→ OPEN(熔断快速失败)→ HALF-OPEN(试探恢复)
blockHandler / fallback @SentinelResource 中分别处理”被规则拦截”和”业务异常”的两个回调
JDK 动态代理 Feign 的实现基础:运行期为接口生成实现类,拦截方法调用转成 HTTP 请求
Route / Predicate / Filter Gateway 路由三要素:转发目标、匹配条件、加工逻辑
pre / post 阶段 过滤器在请求转发前、响应返回后的两个执行阶段,post 逆序执行
白名单 网关鉴权中显式放行、不做登录校验的路径清单