Skip to content

Spring Cloud服务调用链路

Spring Cloud 的服务调用链路,本质上不是“像本地方法一样直接跳到另一个服务里执行代码”,而是把一套分布式调用能力组合起来:

服务注册发现 -> 服务名解析 -> 客户端负载均衡 -> HTTP 请求发送 -> 服务端处理 -> 响应解码 -> 超时、重试、熔断、限流、降级兜底。

如果只会写 @FeignClient,但不理解这条链路,一旦出现“调用超时、服务明明在线却找不到、某个实例压力特别大、下游慢导致上游崩、traceId 断了”等问题,就很难定位。

本页负责把组件串成端到端链路。每个内部对象的深度原理分别沉淀到:

JDK 8、Boot 2.7与Java 17、Boot 3的总体调用思想一致,但具体类版本、自动配置条件和HTTP Client会变化。排查和面试必须先声明精确版本,不能把某个4.x实现倒灌成所有旧版本的固定行为。

先用一句话建立模型

在 Spring Cloud 里,调用方通常不写死下游 IP,而是写服务名:

java
@FeignClient("user-service")
public interface UserClient {

    @GetMapping("/users/{id}")
    UserDTO getUser(@PathVariable("id") Long id);
}

业务代码看起来像本地调用:

java
UserDTO user = userClient.getUser(1001L);

但底层真实发生的是:

text
userClient.getUser(1001L)
    -> Feign 动态代理拦截方法调用
    -> 根据注解拼出 HTTP 请求 GET /users/1001
    -> 根据服务名 user-service 找可用实例
    -> LoadBalancer 选择一个实例
    -> 发 HTTP 请求到真实 IP:Port
    -> 用户服务 Controller 处理
    -> 返回 JSON
    -> Feign 反序列化成 UserDTO

到底是谁负责发请求

很多人会把这句话说成“Feign从Nacos拿地址,然后负载均衡后发请求”。这句话方向接近,但职责不够准确。

更精确的模型是:

Feign负责把Java接口方法转换成HTTP请求;Nacos负责提供服务实例数据;Spring Cloud LoadBalancer负责从实例列表里选择一个实例;真正建立连接并发送HTTP报文的是底层HTTP Client。

也就是说,Feign不应该被理解成“直接去Nacos查地址并亲自发TCP包”的组件。Feign看到的是逻辑服务名,例如user-service;服务名怎样变成192.168.1.11:8081,由Spring Cloud的服务发现抽象和LoadBalancer完成;最后由Apache HttpClient、OkHttp、JDK Client或Reactor Netty这类HTTP客户端真正发出网络请求。

mermaid
flowchart TD
    A["业务代码调用Feign接口"] --> B["Feign动态代理接管方法调用"]
    B --> C["Feign按注解构造逻辑HTTP请求"]
    C --> D["请求地址仍是服务名<br/>http://user-service/users/1001"]
    D --> E["LoadBalancer处理服务名"]
    E --> F["通过DiscoveryClient读取实例列表"]
    F --> G["Nacos Client提供本地实例快照"]
    G --> H["LoadBalancer选择一个ServiceInstance"]
    H --> I["重建为真实地址<br/>http://192.168.1.11:8081/users/1001"]
    I --> J["底层HTTP Client发送请求"]
    J --> K["目标服务Controller处理"]

这张图要注意两个“不”:

容易误解的说法正确理解
Feign直接调用Nacos拿地址Feign通过Spring Cloud LoadBalancer和DiscoveryClient间接使用Nacos实例数据
Nacos参与转发每一次业务请求Nacos是注册发现控制面,不是业务流量代理;业务请求由调用方直连被选中的服务实例

可以把几类组件理解成不同角色:

组件负责什么不负责什么
OpenFeign生成接口代理、解析注解、构造请求、编解码响应不维护注册表,不做注册中心存储,不直接保证下游成功
Nacos保存服务名和实例列表,处理注册、心跳、订阅、推送、本地缓存更新不转发订单、库存、用户这类业务HTTP请求
DiscoveryClient / NacosDiscoveryClient把注册中心能力适配成Spring Cloud统一服务发现接口不决定本次一定选哪台实例
Spring Cloud LoadBalancer根据服务名和候选实例列表选择一个ServiceInstance不建立TCP连接,不解析业务JSON
HTTP Client建立或复用连接,发送HTTP请求,读取HTTP响应不理解@FeignClient接口语义,不保存注册中心数据

因此面试里被问“Spring Cloud中是谁负责发请求”时,应该回答:

text
真正发请求的是底层HTTP Client。Feign负责把接口调用组装成HTTP请求,
LoadBalancer负责选实例,Nacos负责注册发现和提供实例列表。
Nacos不在业务请求转发链路上,调用方拿到实例后会直连目标服务。

再补一个生产细节:调用方通常不会每次请求都实时访问Nacos Server。Nacos Client会维护本地实例缓存,服务变化通过订阅推送或刷新机制更新;LoadBalancer调用时更多是读取当前实例快照。这样可以避免注册中心成为每一次业务调用的同步瓶颈。

怎么保证每次请求尽量最新可用

先给结论:不能保证每次请求都是全局最新且绝对可用,只能保证每次请求在当前调用方视角下尽量可用

原因是一次调用跨了很多异步环节:Provider状态变化、注册中心服务表更新、Nacos Client本地缓存刷新、LoadBalancer候选过滤、HTTP连接池复用、请求在途执行。任何一个环节都存在毫秒到秒级窗口。并且“可用”也不是永久属性,实例被LoadBalancer选中后一瞬间仍可能重启、Full GC、网络断开或数据库连接池打满。

正确做法不是让Feign每次都同步查Nacos,而是建立下面这条闭环:

mermaid
flowchart TD
    A["Feign生成逻辑请求"] --> B["读取当前实例快照"]
    B --> C["LoadBalancer过滤候选实例"]
    C --> D["选择健康、版本、权重匹配实例"]
    D --> E["HTTP Client发请求"]
    E --> F{"调用结果"}
    F --> G["成功返回"]
    F --> H["连接失败或超时"]
    H --> I["按错误语义判断"]
    I --> J["读请求有限换实例重试"]
    I --> K["写请求查幂等事实再补偿"]

这条链路里各组件的真实职责如下:

环节组件做什么不能解决什么
实例事实传播Nacos、Eureka、Consul把Provider上线、下线、健康变化传播给Consumer不能消灭传播延迟
本地快照维护Nacos Client、DiscoveryClient在后台刷新服务列表,让请求读本地快照不能保证每个瞬间都是全局最新
请求级选址Spring Cloud LoadBalancer过滤不健康、权重0、版本不匹配实例,再选一个不能保证选中后实例永远可用
网络发送Apache HttpClient、OkHttp、JDK Client、Reactor Netty建连接、复用连接、发送HTTP请求、读响应连接池可能短暂复用旧连接
失败治理Sentinel、Resilience4j、Retry、Fallback限流、熔断、隔离、重试、降级配错会雪崩,写请求不能盲目重试
业务兜底幂等表、状态机、补偿任务写请求超时后查询事实并补偿不能用“重试一次”代替事实确认

为什么不每次请求都实时查Nacos?

原因说明
注册中心会被业务QPS打爆注册中心是控制面,不应该成为每次业务调用的同步数据库
链路RT变长每次调用多一次远程查询,P95/P99都会变差
可用性反而下降注册中心抖动会拖垮原本可直连成功的业务请求
仍然不能绝对保证查完到发请求之间实例仍可能故障

所以面试回答要这样说:

text
不能保证每次请求都是全局最新可用。Spring Cloud通常是Nacos Client后台维护本地实例快照,
Feign发起调用时由LoadBalancer读取当前快照并过滤候选实例,最后由HTTP Client发请求。
工程上通过Readiness摘流、订阅推送、LB缓存TTL、健康过滤、连接池回收、短超时、
有限重试、熔断隔离、幂等号和事实查询来保证“当前视角下尽量可用”。
读请求可以在Deadline内换实例重试,写请求超时必须先查业务事实,不能盲目重放。

更完整的服务发现原理见:微服务服务发现

整体调用链路图

mermaid
flowchart TD
    A["用户请求"] --> B["Spring Cloud Gateway"]
    B --> C["匹配 Route 和 Predicate"]
    C --> D["执行网关 Filter<br/>鉴权、限流、日志、Trace"]
    D --> E["lb://order-service"]
    E --> F["LoadBalancer 选择订单服务实例"]
    F --> G["订单服务 Controller"]
    G --> H["业务代码调用 UserClient"]
    H --> I["Feign 动态代理"]
    I --> J["构造 HTTP 请求模板"]
    J --> K["查询 user-service 实例列表"]
    K --> L["LoadBalancer 选择用户服务实例"]
    L --> M["HTTP Client 发请求"]
    M --> N["用户服务 Controller"]
    N --> O["返回 JSON"]
    O --> P["Feign Decoder 反序列化"]
    P --> Q["订单服务继续执行业务"]

这张图里包含两类调用:

  • 外部请求进入微服务:通常经过 Gateway。
  • 微服务内部互相调用:通常使用 OpenFeign、RestTemplate、WebClient 等客户端。

Gateway、Feign、LoadBalancer、注册中心不是互相替代关系,而是各司其职。

一次商业请求的完整调用流程

下面用“用户提交订单,订单服务需要查询用户、锁库存、发消息”来串起完整链路。实际项目可能没有全部组件,但只要使用 Spring Cloud,核心问题都绕不开:入口怎么进来、服务名怎么变成 IP、请求由谁发出、限流熔断在哪里生效、失败后怎样返回或补偿。

先看最完整的一条同步调用主线:

mermaid
flowchart TD
    A["客户端请求<br/>POST /orders"] --> B["Gateway接收请求"]
    B --> C["生成或读取TraceId"]
    C --> D["路由匹配<br/>Route和Predicate"]
    D --> E["网关前置Filter<br/>鉴权、灰度、限流"]
    E --> F["识别lb://order-service"]
    F --> G["Gateway侧LoadBalancer选订单实例"]
    G --> H["Gateway HTTP Client转发到订单服务"]
    H --> I["订单服务Web容器接收请求"]
    I --> J["服务端过滤器链<br/>认证、日志、Trace"]
    J --> K["Controller参数绑定和校验"]
    K --> L["OrderService执行业务"]
    L --> M["调用UserClient或StockClient"]
    M --> N["Feign动态代理"]
    N --> O["组装RequestTemplate和Header"]
    O --> P["客户端限流或熔断判断"]
    P --> Q["Feign侧LoadBalancer选下游实例"]
    Q --> R["底层HTTP Client发送请求"]
    R --> S["下游服务处理并返回"]
    S --> T["Decoder或ErrorDecoder处理响应"]
    T --> U["记录指标、Trace和熔断结果"]
    U --> V["订单服务继续提交事务或返回失败"]

这张图表达的是常见主线,不表示所有项目中每个组件的装配顺序都绝对相同。比如 Sentinel、Resilience4j、Spring Cloud CircuitBreaker、LoadBalancer Retry、Feign Core Retry、Gateway Filter 的具体先后,要以项目依赖、自动配置条件、调用栈和链路追踪为准。但职责边界不变:

阶段典型组件核心职责出问题的典型现象
入口接入Nginx、Ingress、Gateway接收外部请求、TLS、路由、鉴权、入口限流404、401、403、429、网关 503/504
服务发现Nacos、Eureka、Consul维护服务名到实例列表的映射控制台有实例但调用方无实例
实例选择Spring Cloud LoadBalancer、Ribbon从候选实例里选择一个负载不均、选到旧实例、灰度误路由
客户端调用OpenFeign、RestTemplate、WebClient组装请求、发起远程调用、解码响应参数丢失、序列化失败、连接超时、读取超时
HTTP 传输Apache HttpClient、OkHttp、JDK Client、Reactor Netty连接池、TCP/TLS、发送和读取报文等连接、端口不通、TLS失败、旧连接命中下线实例
服务端处理Tomcat、Spring MVC、Service、DB业务执行、事务、数据库访问慢 SQL、锁等待、线程池满、业务 500
容错治理Sentinel、Resilience4j、Hystrix限流、熔断、隔离、降级、统计被限流、熔断打开、fallback误返回成功
可观测性Micrometer、OpenTelemetry、SkyWalking、日志 MDCTrace、指标、日志关联traceId 丢失、只看到局部错误

1. Gateway阶段:请求还没进业务服务

外部请求先到 Gateway 时,Gateway 会先做路由匹配和过滤器处理。它不应该执行业务事务,但适合处理入口横切能力。

mermaid
flowchart TD
    A["请求进入Gateway"] --> B["匹配Route"]
    B --> C["执行前置Filter"]
    C --> D["鉴权和用户上下文"]
    C --> E["入口限流"]
    C --> F["灰度和路径重写"]
    D --> G["通过lb://服务名准备转发"]
    E --> G
    F --> G

这里的限流通常保护入口。例如同一个 IP、用户、租户、医院编码、接口路径在短时间内请求过多,可以直接返回 429 或统一错误码。入口限流发生时,请求可能根本没有到订单服务,所以排查时不能只看订单服务日志。

Gateway 中的 lb://order-service 会触发 Gateway 侧的 LoadBalancer。它从注册中心或本地缓存拿订单服务实例,然后转发到某个真实实例。注意这里已经发生过一次负载均衡:选的是订单服务实例。

2. 订单服务入口阶段:服务端也可能限流和熔断

请求进入订单服务后,会经过 Web 容器、过滤器链、拦截器、Controller 参数绑定、参数校验,再进入 Service。

mermaid
flowchart TD
    A["HTTP请求到达订单服务"] --> B["Tomcat工作线程"]
    B --> C["Filter和Interceptor"]
    C --> D["认证上下文和Trace上下文"]
    D --> E["Controller参数绑定"]
    E --> F["参数校验"]
    F --> G["OrderService业务方法"]

如果订单服务自己也接入 Sentinel,那么可以在入口资源、Controller 方法或业务方法上做限流、热点参数限流、线程数控制、慢调用熔断。这里保护的是订单服务自身,不是下游库存服务。

常见区别:

位置保护对象例子
Gateway 限流整个入口或路由防止某租户刷 /orders
订单服务入口限流订单服务线程和核心接口防止订单创建接口被打满
Feign 调用限流对某个下游依赖的调用量防止订单服务把库存服务打爆
数据库前限流数据库连接和慢 SQL 资源防止写库并发超过承载

3. Feign阶段:本地方法调用变成远程HTTP

业务代码调用:

java
stockClient.lockStock(command);

看起来像调用本地对象,其实进入 Feign 代理。Feign 会做:

mermaid
flowchart TD
    A["调用stockClient.lockStock"] --> B["JDK动态代理拦截"]
    B --> C["根据Method找到MethodHandler"]
    C --> D["Contract元数据确定HTTP方法和路径"]
    D --> E["填充Path、Query、Header和Body"]
    E --> F["Encoder序列化请求体"]
    F --> G["RequestInterceptor添加Trace和认证头"]
    G --> H["生成Feign Request"]

Feign 在这里还没有完成“真实发请求”。此时请求地址可能仍然是逻辑地址:

text
http://stock-service/api/stocks/lock

如果配置了固定 url,通常会直接走固定地址,不再按服务名走注册中心负载均衡;如果只配置 name,才会进入服务发现和 LoadBalancer。

4. 熔断器和限流器阶段:可能请求还没发出去

Feign 调用常会被 Sentinel、Resilience4j 或 Spring Cloud CircuitBreaker 包一层。它们可能在请求发出前就做判断。

mermaid
flowchart TD
    A["Feign准备调用下游"] --> B["限流规则判断"]
    B --> C["熔断器状态判断"]
    C --> D["允许通过"]
    D --> E["进入LoadBalancer和HTTP Client"]
    B --> F["限流拒绝"]
    C --> G["熔断打开快速失败"]
    F --> H["返回降级或抛Block异常"]
    G --> H

要把几个概念分清:

能力解决什么请求是否一定发到下游
限流流量太大,先挡住一部分不一定,限流命中时不会发
熔断下游持续失败或慢,不再继续打熔断打开时不会发
降级失败后给兜底结果可能没发,也可能发了失败后降级
超时已经等待太久,释放调用方资源通常请求可能已经发出
重试偶发失败后再尝试可能产生多次真实请求

这也是线上排查的关键:如果下游没有日志,不一定是网络丢了,也可能是限流、熔断、无实例、连接池等连接在本地就失败了。

5. LoadBalancer阶段:服务名变成真实实例

LoadBalancer 的输入通常是服务名和请求上下文,输出是一个 ServiceInstance

mermaid
flowchart TD
    A["serviceId=stock-service"] --> B["ServiceInstanceListSupplier"]
    B --> C["读取DiscoveryClient实例列表"]
    C --> D["使用本地缓存或注册中心快照"]
    D --> E["过滤不可用、灰度、版本、区域"]
    E --> F["轮询、随机、权重或自定义算法"]
    F --> G["返回ServiceInstance"]

例如候选实例是:

text
stock-service:
  10.10.2.17:8080 version=v1 zone=shanghai weight=100
  10.10.2.18:8080 version=v1 zone=shanghai weight=100
  10.20.1.09:8080 version=v2 zone=beijing  weight=10

如果调用方带了灰度标记 version=v2,过滤后可能只剩 10.20.1.09:8080。如果过滤条件写错,就会出现“注册中心明明有实例,但 Feign 报无实例”。所以排查时不能只看 Nacos 控制台,要看调用方最终候选列表和过滤结果。

选中实例后,请求 URL 会从:

text
http://stock-service/api/stocks/lock

变成:

text
http://10.10.2.17:8080/api/stocks/lock

6. HTTP Client阶段:真正发送网络请求

真正发请求的是底层 HTTP Client。它要处理连接池、DNS、TCP、TLS、写请求、读响应。

mermaid
flowchart TD
    A["拿到真实URL"] --> B["从连接池申请连接"]
    B --> C["复用空闲连接或新建连接"]
    C --> D["建立TCP和TLS"]
    D --> E["写Header和Body"]
    E --> F["等待下游响应"]
    F --> G["读取响应Body"]
    G --> H["归还或关闭连接"]

这里至少有几类等待:

等待位置说明常见证据
连接池等待本地没有空闲连接线程栈停在 acquire/lease
Connect TimeoutTCP 连接建不起来connect timeout、端口不通
TLS 握手证书或协议协商失败SSLHandshakeException
Read Timeout请求发出后迟迟没响应socket read timeout
响应解码响应体太大或格式不符decode exception、JSON错误

读取超时最容易被误解。Read Timeout 不等于下游没有执行。下游可能已经完成扣库存或支付,只是响应返回晚了。因此写接口超时后要按幂等号查询状态,而不是直接重试。

7. 下游服务端阶段:对方也有自己的线程池和数据库

库存服务收到请求后,并不是“网络到了就马上成功”。它还要经过自己的 Web 容器、过滤器、Controller、Service、事务、数据库。

mermaid
flowchart TD
    A["库存服务收到HTTP请求"] --> B["Web容器线程"]
    B --> C["服务端限流或认证"]
    C --> D["Controller参数绑定"]
    D --> E["Service开启事务"]
    E --> F["校验幂等号"]
    F --> G["扣减库存或返回已有结果"]
    G --> H["提交事务"]
    H --> I["返回统一响应"]

所以 Feign 慢可能是调用方慢、网络慢,也可能是下游自己慢。常见下游原因:

下游原因表现
Tomcat线程池满请求排队,P99升高
数据库连接池满业务线程阻塞等连接
慢 SQL 或锁等待下游处理时间长
Full GC整个实例短时间无响应
第三方接口慢下游继续级联等待

8. 响应返回阶段:Decoder、ErrorDecoder、指标和熔断统计

下游返回后,Feign 会根据状态码进入不同处理:

mermaid
flowchart TD
    A["HTTP Client返回Response"] --> B["Feign记录日志和响应"]
    B --> C["按状态码分类"]
    C --> D["2xx进入Decoder"]
    C --> E["非2xx进入ErrorDecoder"]
    D --> F["反序列化为Java返回值"]
    E --> G["映射为异常"]
    F --> H["记录成功和耗时指标"]
    G --> I["记录失败、慢调用或异常指标"]
    H --> J["返回业务代码"]
    I --> K["可能触发重试、熔断或fallback"]

熔断器通常根据一段时间窗口内的失败率、慢调用比例、异常数或并发数更新状态。也就是说,熔断不是“某一次失败就永远断开”,而是统计窗口和状态机共同作用。

典型状态:

状态含义行为
Closed正常关闭请求正常通过,并持续统计
Open熔断打开请求快速失败或降级,不打下游
Half-Open半开探测放少量请求试探恢复

9. 重试阶段:一次逻辑请求可能变成多次物理请求

重试可能发生在多个层次:

重试位置例子风险
Gateway 重试网关转发失败重试放大入口流量
LoadBalancer 重试换实例再试写操作可能重复执行
Feign Core 重试Retryer 重试 RetryableException与LB重试叠加
Resilience4j Retry装饰器重试与熔断统计顺序有关
业务代码循环重试catch后再次调用最容易失控

如果每层都重试 2 次,含义不是总共 2 次,而可能相乘:

text
物理请求上限 = Gateway 3次 * Feign 3次 * 业务 3次 = 27次

写接口必须先满足幂等,再谈重试。订单、库存、支付这类场景应携带 requestNoorderNodeductNo 这类业务唯一键,并在下游用唯一索引和事务保证重复请求返回同一结果。

10. 降级阶段:兜底不能破坏业务正确性

Fallback 常见返回方式:

业务场景合理降级危险降级
查询头像默认头像返回其他用户头像
查询推荐热门列表返回过期敏感数据
查询字典本地缓存旧值并标记版本悄悄返回空导致业务误判
锁库存返回失败或待确认假装锁定成功
支付返回处理中并异步查单假装支付成功

降级的原则是:保护系统可以,欺骗业务不行。特别是写操作,降级一般只能返回失败、待确认、排队补偿或人工处理,不能为了页面“看起来成功”制造数据不一致。

11. 成功路径和失败路径分别怎么收口

成功路径:

mermaid
flowchart TD
    A["下游2xx响应"] --> B["Decoder解码"]
    B --> C["业务校验返回码"]
    C --> D["订单服务继续事务"]
    D --> E["写本地数据库或发送事务消息"]
    E --> F["返回客户端成功"]

失败路径:

mermaid
flowchart TD
    A["远程调用失败"] --> B["判断失败类型"]
    B --> C["未发出<br/>限流、熔断、无实例"]
    B --> D["已发出但失败<br/>连接、读取、5xx"]
    C --> E["可直接失败或降级"]
    D --> F["写操作按幂等号查状态"]
    F --> G["确认成功则继续"]
    F --> H["确认失败或未知则补偿"]

注意“请求是否已经发出”非常重要:

失败类型请求是否可能到达下游处理重点
限流拒绝没到返回限流提示,调整规则或扩容入口
熔断打开没到看熔断原因和恢复窗口
无实例没到查注册中心、命名空间、本地缓存
连接超时通常没建立连接查网络、端口、实例状态
读取超时很可能已经到达写操作要查最终状态
下游 500已到达查下游日志和业务异常

12. 一句话背后的完整面试答案

如果面试官问“Spring Cloud 一次服务调用完整流程是什么”,可以这样答:

text
外部请求通常先进入 Gateway,Gateway 根据 Route 和 Predicate 匹配路由,
执行鉴权、限流、日志、Trace 等 Filter;如果目标是 lb:// 服务名,
Gateway 会通过 LoadBalancer 从注册中心或本地实例缓存中选择一个服务实例,
再由 Netty/HTTP Client 转发到目标服务。

服务内部调用通常使用 OpenFeign。启动时 OpenFeign 扫描 @FeignClient 接口并创建代理;
运行时业务代码调用接口方法,Feign 代理通过 Contract、Encoder、RequestInterceptor
把方法调用转换成 HTTP 请求。没有固定 url 时,请求中的服务名会交给
Spring Cloud LoadBalancer,LoadBalancer 通过 DiscoveryClient 读取 Nacos/Eureka 等
注册中心的实例快照,经过健康、灰度、区域、权重等过滤和轮询/随机等算法选出
ServiceInstance,再把服务名替换为真实 IP:Port。真正发请求的是底层 HTTP Client。

调用过程中还会叠加超时、重试、限流、熔断和降级。限流解决流量过大,
熔断解决下游持续慢或异常,超时防止线程无限等待,重试只适合幂等和瞬时故障,
降级提供兜底但不能把失败伪装成成功。所有调用要透传 TraceId,记录每阶段耗时,
线上排查要区分请求是否发出、选中了哪个实例、是否等连接、是否超时、
下游是否执行以及熔断限流是否提前拦截。

启动阶段发生了什么

一次服务调用不是从“发请求那一刻”才开始的。服务启动时已经做了很多准备。

1. 服务提供者注册自己

比如用户服务启动 3 个实例:

text
user-service
  192.168.1.10:8081
  192.168.1.11:8081
  192.168.1.12:8081

每个实例会向注册中心注册自己的信息:

信息例子作用
服务名user-service调用方通过服务名查实例
IP192.168.1.10真实请求地址
端口8081真实请求端口
健康状态UP判断实例是否可用
元数据version=v1zone=shanghai灰度、同机房优先、权重路由

注册中心负责保存“服务名和实例列表”的映射关系。

mermaid
flowchart TD
    A["user-service 三个实例启动"] --> B["每个实例分别发起注册"]
    B --> C["注册中心按服务名维护实例集合"]
    C --> D["保存IP、端口、状态和元数据"]

2. 服务消费者拉取或订阅实例列表

订单服务要调用用户服务,就需要知道 user-service 有哪些实例。

调用方通常通过 DiscoveryClient 或 Spring Cloud 封装的服务发现组件获取实例列表。不同注册中心实现不同:

  • Eureka:客户端定期拉取注册表,本地会有缓存。
  • Nacos:支持服务订阅和推送,也会有本地缓存。
  • Consul:也可以通过服务发现 API 获取实例。

重点不是背实现细节,而是理解:

调用方不是每次都直接连数据库查服务地址,而是通过服务发现组件拿到实例列表,并在本地参与选择。

2.1 本地服务列表怎样保证尽量最新

调用方本地实例列表无法保证任意瞬间绝对最新。服务注册发现是一条异步控制面链路:提供者状态变化后,注册中心先更新服务表,再通过推送或客户端拉取让调用方刷新本地快照,最后 LoadBalancer 才在新快照上选址。即使快照已更新,HTTP 连接池也可能短时间复用旧连接。

mermaid
flowchart TD
    A["提供者上线、下线或变更metadata"] --> B["注册中心更新服务表"]
    B --> C["推送或等待消费者拉取"]
    C --> D["消费者替换本地实例快照"]
    D --> E["LoadBalancer按新快照选实例"]
    E --> F["HTTP连接池逐步回收旧连接"]

所以“最新”在工程上不是强一致承诺,而是通过这些机制共同逼近:

机制解决什么边界
Nacos订阅推送或Eureka定期拉取让调用方最终收到实例变化网络抖动和客户端处理有延迟
本地缓存TTL或定期刷新避免缓存永远不变TTL太短会增加注册中心压力
Readiness先摘流下线前先让实例不再接新请求调用方刷新前仍可能有旧快照
优雅停机等存量请求结束再退出配置太短会中断请求
健康过滤和权重0LoadBalancer过滤不可用实例健康检查不等于业务一定健康
连接池最大存活时间避免旧连接长期复用旧地址过短会增加建连和TLS成本
短超时和有限重试旧实例不可达时快速恢复写请求重试必须先有幂等

如果真的调到了旧服务,排查时按顺序确认:调用方 Trace 里的目标 ip:port 和版本、注册中心当时服务表、调用方本地快照、LoadBalancer 过滤结果、HTTP Client 是否复用旧连接、Gateway/Feign 是否丢灰度标签、K8s Readiness 和 preStop 是否足够。只读请求通常依赖兼容协议容忍短窗口;写请求不能简单再次打到新服务,应按幂等号查询事实后补偿。

调用方也不是每次请求都更新本地实例快照。每次业务请求通常只是读取当前快照并负载均衡;真正的快照刷新由后台服务发现客户端负责,例如 Nacos 订阅推送、Eureka 定期拉取、Consul Blocking Query、Kubernetes EndpointSlice Watch 或 Mesh xDS 推送。

mermaid
flowchart TD
    A["业务请求"] --> B["读取当前实例快照"]
    B --> C["负载均衡选实例"]
    C --> D["HTTP或RPC调用"]
    E["后台发现线程"] --> F["收到推送或定期拉取"]
    F --> G["构造新实例快照"]
    G --> H["替换本地快照"]
    H --> B

如果每次请求都实时更新,注册中心会被业务 QPS 打爆,调用延迟也会变高。正确理解是:业务调用链路负责“读快照并发请求”,服务发现刷新链路负责“异步更新快照”。这也是为什么旧实例窗口无法彻底消除,只能通过推送、短 TTL、优雅下线、连接回收、超时重试和幂等补偿缩短和兜底。

更完整的跨组件对比、JDK 8 本地快照 Demo、Nacos/Eureka/Consul/Kubernetes/Service Mesh 差异和优雅上下线流程见:微服务服务发现:注册、订阅、本地缓存、健康检查与优雅上下线

3. Feign 创建代理对象

Spring Boot 启动时,@EnableFeignClients 会触发 Feign Client 扫描。Spring Cloud OpenFeign 会为 @FeignClient 接口创建代理对象。

java
@EnableFeignClients
@SpringBootApplication
public class OrderApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }
}

接口没有实现类:

java
@FeignClient("user-service")
public interface UserClient {
    @GetMapping("/users/{id}")
    UserDTO getUser(@PathVariable("id") Long id);
}

但 Spring 容器里会有一个代理对象。你注入的不是普通实现类,而是 Feign 创建的代理。

运行阶段的完整流程

以订单服务调用用户服务为例:

java
@RestController
@RequestMapping("/orders")
public class OrderController {
    private final UserClient userClient;

    public OrderController(UserClient userClient) {
        this.userClient = userClient;
    }

    @GetMapping("/{orderId}")
    public OrderVO getOrder(@PathVariable Long orderId) {
        Long userId = 1001L;
        UserDTO user = userClient.getUser(userId);
        return new OrderVO(orderId, user);
    }
}

调用链路如下:

mermaid
flowchart TD
    A["OrderController 调用 userClient.getUser"] --> B["Feign 代理拦截方法"]
    B --> C["Contract 解析注解<br/>GET /users/{id}"]
    C --> D["Encoder 处理请求参数"]
    D --> E["RequestInterceptor 添加请求头"]
    E --> F["生成 RequestTemplate"]
    F --> G["根据 user-service 找实例列表"]
    G --> H["LoadBalancer 选择实例"]
    H --> I["HTTP Client 发送请求"]
    I --> J["UserController 处理请求"]
    J --> K["返回 JSON"]
    K --> L["Decoder 转成 UserDTO"]

第一步:Feign 代理拦截方法

Feign 让远程调用看起来像本地方法,核心靠动态代理。

动态代理的作用是:当你调用接口方法时,不是真的执行某个 Java 实现类,而是进入代理逻辑。

代理逻辑会做这些事:

  • 找到当前方法对应的 HTTP Method。
  • 找到请求路径。
  • 找到参数如何放入 path、query、header、body。
  • 构造请求对象。
  • 调用底层 HTTP Client。
  • 把响应转成 Java 对象。

这就是为什么 Feign 是“声明式 HTTP 客户端”,不是“真正的本地调用”。

第二步:Contract 解析注解

Feign 需要知道接口方法和 HTTP 请求之间的映射关系。

java
@GetMapping("/users/{id}")
UserDTO getUser(@PathVariable("id") Long id);

会被解析成:

text
HTTP Method: GET
Path: /users/1001
ServiceId: user-service
Return Type: UserDTO

Spring Cloud OpenFeign 支持 Spring MVC 注解,所以你可以使用 @GetMapping@PostMapping@PathVariable@RequestParam@RequestBody

第三步:Encoder 编码请求

如果是 GET 请求,参数可能放在 path 或 query 中。

如果是 POST 请求:

java
@PostMapping("/users")
UserDTO createUser(@RequestBody CreateUserCommand command);

Encoder 会把 Java 对象序列化成 JSON 请求体。

json
{
  "name": "Tom",
  "age": 18
}

如果编码配置不一致,就会出现下游收不到参数、日期格式不对、枚举解析失败等问题。

第四步:RequestInterceptor 处理请求头

微服务调用通常要透传一些上下文:

  • X-Trace-Id:链路追踪 ID。
  • Authorization:用户 token。
  • X-Tenant-Id:租户 ID。
  • X-Request-Source:来源系统。

示例:

java
@Configuration
public class FeignHeaderConfig {

    @Bean
    public RequestInterceptor traceInterceptor() {
        return template -> {
            String traceId = TraceContext.getTraceId();
            if (traceId != null) {
                template.header("X-Trace-Id", traceId);
            }
        };
    }
}

如果请求头不透传,常见后果是:

  • 下游拿不到登录用户。
  • 多租户数据隔离失效。
  • 日志无法通过 traceId 串起来。
  • 排查问题只能按时间猜。

第五步:服务发现获取实例列表

Feign 知道要调用 user-service,但还不知道真实 IP。

这一步会通过服务发现组件拿到实例:

text
user-service
  -> 192.168.1.10:8081
  -> 192.168.1.11:8081
  -> 192.168.1.12:8081

服务发现组件只回答“有哪些实例”,不负责“本次选哪个实例”。

核心原理:注册中心到底做什么

注册中心解决的是服务地址动态变化问题。

没有注册中心时,调用方可能写死地址:

java
restTemplate.getForObject("http://192.168.1.10:8081/users/1001", UserDTO.class);

这样会有几个问题:

问题后果
服务扩容新实例没人调用
服务下线调用方还打到旧 IP
容器重启IP 变化后调用失败
多环境部署地址配置到处改
灰度发布很难按版本、标签、机房路由

有注册中心后,调用方只依赖服务名:

text
user-service

服务名到实例列表的映射由注册中心维护:

mermaid
flowchart TD
    A["服务提供者启动"] --> B["注册服务名、IP、端口、元数据"]
    B --> C["注册中心保存实例"]
    C --> D["消费者订阅或读取本地实例数据"]
    D --> E["业务调用按服务名取得候选列表"]
    E --> F["本地负载均衡选择实例"]

前三步属于提供者注册,后三步属于消费者发现和调用;纵向连接表示实例数据依赖,不表示由同一线程连续执行。

注册中心通常还会处理:

  • 心跳续约:实例定期证明自己还活着。
  • 健康检查:判断实例是否可用。
  • 服务剔除:长时间无心跳的实例下线。
  • 元数据管理:版本、集群、权重、区域等信息。

核心原理:客户端负载均衡

拿到实例列表后,需要决定本次请求发给谁。

text
user-service 实例列表:
  192.168.1.10:8081
  192.168.1.11:8081
  192.168.1.12:8081

LoadBalancer 会选择其中一个:

text
本次选择: 192.168.1.11:8081
mermaid
flowchart TD
    A["调用方"] --> B["实例列表"]
    B --> C["Spring Cloud LoadBalancer"]
    C --> D["先执行健康、区域和版本过滤"]
    D --> E["按轮询、随机或自定义算法选择"]
    E --> F["返回一个ServiceInstance"]

轮询、随机和自定义算法是互斥选择,不会在一次请求中依次执行;各算法和过滤器的输入、回退边界见LoadBalancer内部原理

客户端负载均衡和服务端负载均衡区别

客户端负载均衡:

mermaid
flowchart TD
    A["订单服务"] --> B["注册中心拿实例列表"]
    B --> C["订单服务本地选择实例"]
    C --> D["用户服务实例"]

服务端负载均衡:

mermaid
flowchart TD
    A["订单服务"] --> B["Nginx / SLB / Kubernetes Service"]
    B --> C["服务端负载均衡维护后端实例池"]
    C --> D["为本次请求选择一个用户服务实例"]
    D --> E["转发并返回响应"]

Spring Cloud 内部调用常见客户端负载均衡;公网入口、Kubernetes 集群入口、传统反向代理常见服务端负载均衡。

为什么微服务内部常用客户端负载均衡

优点:

  • 调用链路短,不需要所有内部请求经过统一转发层。
  • 调用方可以结合服务元数据做灰度、同机房优先。
  • 每个调用方本地选择实例,不容易形成单一转发瓶颈。

缺点:

  • 调用方会缓存实例列表,可能短时间拿到旧实例。
  • 每个语言、每个框架都要有客户端治理能力。
  • 负载策略分散在调用方,治理复杂度更高。

所以生产环境经常是两者一起用:

text
外部流量 -> Nginx / Ingress / Gateway
内部调用 -> OpenFeign + LoadBalancer

Spring Cloud LoadBalancer 和 Ribbon

Spring Cloud 负载均衡有两代常见方案:

方案状态说明
Ribbon老项目常见Netflix 体系的客户端负载均衡,已不推荐新项目继续选型
Spring Cloud LoadBalancer新项目推荐Spring Cloud 官方客户端负载均衡抽象和实现

新项目里更推荐理解 Spring Cloud LoadBalancer。老项目里遇到 Ribbon,也要知道它承担的是同一类职责:从服务实例列表中选一个实例。

常见算法:

算法说明适合场景
轮询每次选择下一个实例实例规格接近,默认通用
随机随机选择一个实例实例接近,简单均衡
权重按权重分配请求机器规格不同、灰度流量
同机房优先优先选择同区域实例降低跨机房延迟
版本优先按元数据选择版本灰度发布、金丝雀发布

核心原理:HTTP Client 真正发请求

负载均衡选出实例后,才会真正发 HTTP 请求。

text
GET http://192.168.1.11:8081/users/1001

底层 HTTP Client 可能是:

  • JDK 默认 HTTP 能力。
  • Apache HttpClient。
  • OkHttp。
  • Reactor Netty。

不同客户端的连接池、超时、线程模型不同,但核心都要处理:

  • 建立 TCP 连接。
  • 复用连接。
  • 写请求头和请求体。
  • 等待响应。
  • 读取响应体。
  • 处理超时和异常。

这说明 Feign 调用不是普通方法调用,它会受到网络、连接池、序列化、下游线程池、数据库等因素影响。

核心原理:服务端如何处理请求

下游服务收到请求后,和普通 Spring MVC 请求一样:

mermaid
flowchart TD
    A["HTTP 请求进入用户服务"] --> B["Tomcat / Undertow / Netty"]
    B --> C["Spring MVC DispatcherServlet"]
    C --> D["HandlerMapping 找 Controller"]
    D --> E["参数绑定和反序列化"]
    E --> F["Controller 调用 Service"]
    F --> G["返回对象"]
    G --> H["序列化 JSON 响应"]

所以服务调用链路横跨两个进程:

  • 调用方进程:Feign、LoadBalancer、HTTP Client。
  • 提供方进程:Web 容器、Spring MVC、业务代码。

只要其中任何一段慢,整体调用就慢。

核心原理:响应解码和异常处理

下游返回后,Feign 会根据状态码和响应体处理。

mermaid
flowchart TD
    A["收到HTTP响应"] --> B["按成功状态或错误状态分类"]
    B --> C["成功由Decoder解码<br/>错误由ErrorDecoder映射"]
    C --> D["最终返回Java对象或抛出异常"]

成功解码和错误映射是互斥分支,纵向合并用于窄屏阅读。404是否按成功路径处理还取决于dismiss404等配置,完整边界见Feign响应链

例如下游返回:

json
{
  "id": 1001,
  "name": "Tom"
}

Feign Decoder 会转成:

java
public class UserDTO {
    private Long id;
    private String name;

    public UserDTO() {
    }

    public UserDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

如果下游返回 500,不应该把 HTML 错误页直接交给业务。应该通过 ErrorDecoder 转成统一异常。

java
public class DownstreamErrorDecoder implements ErrorDecoder {
    @Override
    public Exception decode(String methodKey, Response response) {
        if (response.status() == 404) {
            return new DownstreamNotFoundException("下游资源不存在: " + methodKey);
        }
        if (response.status() >= 500) {
            return new DownstreamUnavailableException("下游服务异常: " + methodKey);
        }
        return new RuntimeException("远程调用失败, status=" + response.status());
    }
}

超时、重试、熔断、限流在链路中的位置

真实项目里,服务调用链路通常还会加治理能力。

mermaid
flowchart TD
    A["业务代码"] --> B["Feign 代理"]
    B --> C["限流判断"]
    C --> D["熔断器判断"]
    D --> E["LoadBalancer 选实例"]
    E --> F["HTTP Client 发请求"]
    F --> G["按成功、超时或异常记录结果"]
    G --> H["成功返回<br/>失败进入重试、熔断或降级判断"]
    H --> I["按治理结果返回兜底或向上抛错"]

一次请求不会同时执行成功和失败路径。治理组件的真实先后顺序取决于代理和过滤器装配,排查时应通过调用栈、Observation和条件报告确认,而不是只背固定图序。

超时

超时解决的是“不能无限等下游”。

如果没有超时,下游慢时,上游线程会一直卡住。请求量一上来,上游线程池被占满,最终上游也不可用。

常见配置:

yaml
spring:
  cloud:
    openfeign:
      client:
        config:
          default:
            connectTimeout: 2000
            readTimeout: 5000

两个超时要分清:

超时含义常见原因
连接超时建立连接等多久实例不可达、网络不通、防火墙问题
读取超时请求发出后等响应多久下游处理慢、数据库慢、线程池满

重试

重试解决的是“偶发失败”。

但重试不是越多越好。下游已经慢了,上游大量重试会放大流量。

mermaid
flowchart TD
    A["下游响应变慢"] --> B["上游超时"]
    B --> C["上游自动重试"]
    C --> D["同一请求变成多次请求"]
    D --> E["下游压力更大"]
    E --> F["更多请求超时并形成重试风暴"]

适合重试:

  • 查询接口。
  • 幂等写接口。
  • 网络抖动导致的短暂失败。

不适合盲目重试:

  • 创建订单。
  • 扣库存。
  • 支付扣款。
  • 发优惠券。

写操作如果要重试,必须有业务唯一键、幂等表或状态机保护。

熔断

熔断解决的是“下游已经明显不可用,不要继续打爆它”。

mermaid
flowchart TD
    A["正常调用持续统计失败和慢调用"] --> B["超过阈值后打开熔断器"]
    B --> C["后续请求快速失败或降级"]
    C --> D["等待恢复窗口后进入半开"]
    D --> E["使用少量请求探测下游"]
    E --> F["探测成功关闭熔断<br/>失败则重新打开"]

Hystrix、Sentinel、Resilience4j 都是在解决这类问题,只是实现和生态不同。

限流

限流解决的是“请求太多,系统来不及处理”。

限流可以发生在:

  • Gateway 入口。
  • 服务接口。
  • Feign 调用前。
  • 下游核心资源前,例如数据库、第三方接口。

限流不是错误,而是保护系统的一种主动策略。宁愿拒绝一部分请求,也不要让全部请求一起超时。

降级

降级解决的是“失败时给一个可接受的兜底结果”。

降级不能乱用。比如:

场景可以怎么降级不能怎么降级
查询用户头像返回默认头像返回别人的头像
查询推荐列表返回默认热门列表返回错误用户数据
库存扣减返回系统繁忙假装扣减成功
支付调用返回支付处理中假装支付成功

降级要遵守业务正确性,不能为了“接口不报错”牺牲数据一致性。

Gateway 调用链路

外部请求通常先进入 Gateway。

yaml
spring:
  cloud:
    gateway:
      routes:
        - id: order-route
          uri: lb://order-service
          predicates:
            - Path=/orders/**

lb://order-service 表示网关不写死订单服务地址,而是通过 LoadBalancer 选择订单服务实例。

Gateway 的处理流程:

mermaid
flowchart TD
    A["外部请求 /orders/1001"] --> B["Gateway 接收请求"]
    B --> C["RoutePredicateHandlerMapping 匹配路由"]
    C --> D["执行 GlobalFilter"]
    D --> E["执行 GatewayFilter"]
    E --> F["识别 lb://order-service"]
    F --> G["LoadBalancer 选择实例"]
    G --> H["NettyRoutingFilter 转发请求"]
    H --> I["订单服务处理"]

Gateway 适合做横切能力:

  • 鉴权。
  • 限流。
  • 路由。
  • 跨域。
  • 日志。
  • traceId 生成。
  • 灰度路由。

不要把复杂业务逻辑写进 Gateway。网关是入口和治理层,不应该变成大业务服务。

RestTemplate、WebClient、OpenFeign 的区别

Spring Cloud 服务调用不只有 Feign。

调用方式特点适合场景
RestTemplate命令式写法,老项目常见简单 HTTP 调用、历史项目
WebClient响应式、非阻塞高并发、流式、WebFlux 项目
OpenFeign声明式接口,最常见微服务内部 HTTP 调用

RestTemplate 示例:

java
@Bean
@LoadBalanced
public RestTemplate restTemplate() {
    return new RestTemplate();
}
java
String result = restTemplate.getForObject(
        "http://user-service/users/1001",
        String.class
);

@LoadBalanced 的作用是让 RestTemplate 具备服务名解析和负载均衡能力。

WebClient 示例:

java
@Bean
@LoadBalanced
public WebClient.Builder webClientBuilder() {
    return WebClient.builder();
}
java
Mono<UserDTO> userMono = webClientBuilder.build()
        .get()
        .uri("http://user-service/users/{id}", 1001L)
        .retrieve()
        .bodyToMono(UserDTO.class);

链路追踪为什么重要

一次用户请求可能经过很多服务:

text
Gateway -> order-service -> user-service -> coupon-service -> payment-service

如果没有 traceId,你只能看到每个服务各自的日志,很难知道哪些日志属于同一次请求。

推荐做法:

mermaid
flowchart TD
    A["Gateway 生成 traceId"] --> B["写入请求头 X-Trace-Id"]
    B --> C["order-service 日志打印 traceId"]
    C --> D["Feign 透传 traceId"]
    D --> E["user-service 日志打印同一个 traceId"]

相关技术:

  • Micrometer Tracing。
  • Zipkin。
  • SkyWalking。
  • OpenTelemetry。
  • 日志 MDC。

链路追踪不是锦上添花。微服务数量变多后,没有 traceId,排查跨服务问题会非常痛苦。

常见故障和排查思路

1. 服务找不到实例

表现:

text
No instances available for user-service

排查:

  • user-service 是否启动。
  • 服务名是否拼错。
  • 是否注册到同一个注册中心命名空间。
  • 注册中心是否能看到实例。
  • 消费者是否拉到最新实例列表。

2. 明明有实例却还调用失败

原因可能是:

  • 注册中心实例状态还没刷新。
  • 实例已经下线,但消费者本地缓存还没更新。
  • 健康检查不准确。
  • 网络不通。
  • 端口暴露错误。

这就是为什么必须配置超时和熔断,不能完全依赖注册中心剔除。

3. 下游慢导致上游也慢

链路:

mermaid
flowchart TD
    A["用户服务 DB 慢"] --> B["用户服务响应慢"]
    B --> C["订单服务 Feign 等待"]
    C --> D["订单服务线程被占用"]
    D --> E["订单服务请求堆积"]
    E --> F["订单服务也不可用"]

处理:

  • 设置读取超时。
  • 下游慢调用熔断。
  • 上游降级。
  • 优化下游数据库。
  • 避免循环 Feign 调用。
  • 能批量就批量。

4. 重试导致流量放大

假设一次请求超时后重试 2 次,原本 100 QPS 会变成最多 300 QPS。下游越慢,上游越重试,越容易雪崩。

处理:

  • 写接口不要盲目重试。
  • 查询接口重试次数要少。
  • 使用退避策略。
  • 配合熔断。
  • 所有写操作做幂等。

5. 循环远程调用导致性能差

错误写法:

java
for (Long userId : userIds) {
    UserDTO user = userClient.getUser(userId);
    result.add(user);
}

如果 userIds 有 1000 个,就会发 1000 次远程调用。

推荐改成批量:

java
List<UserDTO> users = userClient.getUsers(userIds);

远程调用的成本远高于本地方法,必须减少调用次数。

服务调用故障排查决策树

生产里“服务调用失败”不能只看 Feign 报错。一次调用横跨 Gateway、注册中心、本地实例缓存、负载均衡、HTTP Client、下游 Web 容器、下游业务线程池、数据库和网络。要先定位失败发生在哪一层。

mermaid
flowchart TD
    A["服务调用失败或超时"] --> B["保存入口、状态码、traceId和第一处cause"]
    B --> C["检查Gateway是否匹配和完成转发"]
    C --> D["检查调用方是否取得候选并选中实例"]
    D --> E["检查连接池、网络和请求是否发出"]
    E --> F["检查提供方是否收到并完成处理"]
    F --> G["检查响应状态、解码和治理组件"]
    G --> H["按根因恢复并回归真实链路"]

这张图表示取证顺序,不把所有异常压进一张左右决策树。Gateway 404/503/504、Feign无实例、连接超时、读取超时和4xx/5xx的具体证据见紧随其后的表格。

看状态码先判断层次

现象更可能的问题层优先检查
Gateway 404路由未匹配Path、Predicate、StripPrefix、路由顺序
Gateway 503没有可用实例或转发失败注册中心、服务名、实例健康、LoadBalancer
Gateway 504下游响应超时Gateway 超时、下游慢、网络
Feign No instances available服务发现层服务名、命名空间、注册中心缓存
Feign connect timeout网络连接层IP、端口、防火墙、Pod/容器网络
Feign read timeout下游处理层下游线程池、DB、锁、GC、第三方接口
Feign 401/403鉴权上下文Token、Header 透传、权限服务
Feign 500下游业务异常下游日志、traceId、ErrorDecoder

查调用方还是查提供方

很多人排查远程调用时只盯调用方日志。正确方式是先用 traceId 把调用方和提供方日志串起来。

证据说明
调用方发出请求,但提供方没有日志请求没到下游,查负载均衡、网络、网关
提供方收到请求,但响应慢查提供方业务、线程池、DB、锁、GC
提供方很快返回,调用方仍慢查网络、响应体过大、客户端连接池、反序列化
调用方没有发请求查熔断、限流、LoadBalancer、实例列表
mermaid
flowchart TD
    A["拿到 traceId"] --> B["查调用方日志"]
    B --> C["确认是否生成目标实例和发送记录"]
    C --> D["没有发送则查限流、熔断、无实例和参数"]
    D --> E["有发送则按traceId查提供方"]
    E --> F["未收到查网络与转发<br/>已收到查耗时与异常"]

“没有发送”和“已经发送”是互斥分支,纵向合并用于适配窄屏。实际判断以调用方发送事件、最终目标IP和提供方入口日志三份证据为准。

单实例压力特别大怎么判断

如果所有服务都在线,但某一台实例 CPU、线程池、连接数明显高于其他实例,要判断是负载均衡不均、实例能力不同、灰度元数据、长连接粘滞,还是某类请求天然集中。

原因现象处理
实例规格不同小规格实例更容易满权重负载或统一规格
负载算法不合适请求数均匀但耗时不均最少连接、权重、按耗时治理
灰度规则偏流某版本实例流量集中检查版本、Header、元数据路由
长连接或连接池复用流量粘在少数实例调整连接池和负载策略
热租户/热用户某些请求天然重按租户限流、隔离、缓存
实例自身慢它被选中后响应慢下线实例、查 GC/DB/线程

负载均衡不是“请求数绝对平均”。真正要看的是每个实例的 QPS、P95/P99、错误率、CPU、线程池和下游依赖。如果实例请求数一样,但某台耗时明显高,问题可能在实例本身,而不是负载均衡算法。

重试和熔断怎么一起看

远程调用失败时,重试、熔断、限流、降级经常同时出现。排查顺序建议:

  1. 先看是否设置了合理超时。
  2. 再看是否开启重试,以及重试次数是否放大流量。
  3. 再看熔断是否打开,是否导致请求还没发出就被降级。
  4. 再看限流规则是否误伤正常请求。
  5. 最后看 fallback 是否掩盖真实错误。
mermaid
flowchart TD
    A["远程调用失败"] --> B["检查超时"]
    B --> C["检查重试次数"]
    C --> D["计算总尝试数和下游放大倍数"]
    D --> E["检查熔断状态与触发原因"]
    E --> F["检查限流和fallback是否掩盖错误"]
    F --> G["收敛重试预算并修复下游根因"]

写接口尤其要谨慎重试。支付、扣库存、创建订单这类操作必须依赖业务唯一号、幂等表或状态机,不能靠 HTTP 客户端“再试一次”赌运气。

最小完整 Demo

用户服务接口

java
@RestController
@RequestMapping("/users")
public class UserController {

    @GetMapping("/{id}")
    public UserDTO getUser(@PathVariable Long id) {
        return new UserDTO(id, "user-" + id);
    }
}
java
public class UserDTO {
    private Long id;
    private String name;

    public UserDTO() {
    }

    public UserDTO(Long id, String name) {
        this.id = id;
        this.name = name;
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

订单服务 Feign Client

java
@FeignClient(
        name = "user-service",
        fallback = UserClientFallback.class
)
public interface UserClient {

    @GetMapping("/users/{id}")
    UserDTO getUser(@PathVariable("id") Long id);
}

降级实现

java
@Component
public class UserClientFallback implements UserClient {

    @Override
    public UserDTO getUser(Long id) {
        return new UserDTO(id, "unknown");
    }
}

注意:这个 fallback 适合非核心展示场景。如果是支付、扣库存、权限校验,不应该随便返回默认成功。

订单服务调用

java
@RestController
@RequestMapping("/orders")
public class OrderController {
    private final UserClient userClient;

    public OrderController(UserClient userClient) {
        this.userClient = userClient;
    }

    @GetMapping("/{id}")
    public OrderVO getOrder(@PathVariable Long id) {
        UserDTO user = userClient.getUser(1001L);
        return new OrderVO(id, user.getId(), user.getName());
    }
}
java
public class OrderVO {
    private Long orderId;
    private Long userId;
    private String userName;

    public OrderVO() {
    }

    public OrderVO(Long orderId, Long userId, String userName) {
        this.orderId = orderId;
        this.userId = userId;
        this.userName = userName;
    }

    public Long getOrderId() {
        return orderId;
    }

    public void setOrderId(Long orderId) {
        this.orderId = orderId;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }
}

配置示例

yaml
spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
    openfeign:
      client:
        config:
          default:
            connectTimeout: 2000
            readTimeout: 5000

这个 Demo 对应的真实链路:

mermaid
flowchart TD
    A["GET /orders/1"] --> B["OrderController"]
    B --> C["UserClient Feign 代理"]
    C --> D["GET /users/1001"]
    D --> E["Nacos 获取 user-service 实例"]
    E --> F["LoadBalancer 选择实例"]
    F --> G["UserController"]
    G --> H["返回 UserDTO JSON"]
    H --> I["Feign 转成 UserDTO"]
    I --> J["OrderController 组装 OrderVO"]

独立面试入口

调用链相关标准回答不再混在知识正文中:

学习跳转

  • 技术演进与选型:理解经典技术和当前技术如何对应。
  • Nacos:理解服务注册、发现、配置中心和命名空间。
  • Eureka:理解经典注册中心、续约、剔除和服务发现。
  • OpenFeign内部原理:理解Registrar、FactoryBean、MethodHandler、编解码、连接池和重试边界。
  • LoadBalancer内部原理:理解Supplier链、缓存、过滤、算法、健康检查和优雅下线。
  • Gateway:理解外部请求如何路由到微服务。
  • Sentinel:理解限流、熔断、降级和慢调用保护。
  • Hystrix:理解经典熔断器思想和故障隔离。
  • 链路追踪与可观测性:理解 traceId、span、日志和调用链如何关联。

小结

Spring Cloud 服务调用链路可以拆成三条线:

  • 地址线:注册中心维护服务名到实例列表的映射。
  • 调用线:Feign 或其他 HTTP 客户端把接口调用转换成网络请求。
  • 治理线:LoadBalancer、超时、重试、熔断、限流、降级、链路追踪保证调用可控。

真正掌握这条链路后,再看 Feign、Gateway、Nacos、LoadBalancer、Sentinel 就不会觉得它们是零散组件,而是一套围绕“服务如何稳定互相调用”的完整机制。