概述
服务刚重启完,第一次调接口要 500 多毫秒,再刷一次就只剩 20 多毫秒------这个差距不是网络抖动,是 Ribbon 默认懒加载造成的。这篇讲清楚它为什么会慢,以及用 ribbon.eager-load 怎么把这笔"首次调用延迟"提前到启动阶段还掉。
纲要
- 问题背景 :Ribbon 默认懒加载,第一次调用才建
LoadBalancerClient、拉服务列表、装配ILoadBalancer - 容易被忽略的点:初始化按服务名各算一次,有几个下游就有几次"第一次慢";应用一重启就重新计数
- 两条时间线:Mermaid 时序图对比懒加载与饥饿加载,看延迟到底被挪到了哪里
- 配置方式 :
ribbon.eager-load.enabled+ribbon.eager-load.clients,来自本工程order-service的真实 yml - 工程决策:饥饿加载与懒加载在首次延迟 / 启动耗时 / 内存 / 适用场景上的权衡表
- 怎么验证:开启前后测首次调用耗时,以及启动日志里要找的那个观察点
- 版本衔接 :
spring-cloud-starter-loadbalancer的对应能力与 Ribbon 体系的差别 - 实战坑位 :服务名写错静默不生效、
clients写成字符串、启动期依赖注册中心、实例变化后仍有抖动
懒加载是怎么"慢"起来的
Ribbon 负责的是客户端负载均衡:请求发往 userservice 之前,得先有一份可用的实例列表,再从里面挑一个。这套东西由 LoadBalancerClient(实现类 RibbonLoadBalancerClient)持有,内部靠 ILoadBalancer 管服务列表。
默认情况下,这些对象不是应用启动时创建的,而是第一次真正调用该服务时才创建,也就是懒加载(lazy load)。第一次创建要走完整条链路:
- 构造
RibbonLoadBalancerClient与对应的 Spring 容器子上下文(SpringClientFactory为每个服务名单独维护一份) - 触发服务列表拉取,从注册中心(本工程是 Nacos)拿到实例清单
- 用实例清单初始化
DynamicServerListLoadBalancer及其内部的ServerList、ServerListFilter、IRule
这一轮做完,服务列表会缓存在内存里,后续请求直接复用。所以现象就是第一次慢、后面快。在这个 demo 里实测:应用重启后首次请求约 550ms,紧接着再刷新一次只有 27ms------中间那 500 多毫秒基本都花在初始化上,跟业务代码没关系。
有一个细节值得单独拎出来:这套初始化是按服务名独立进行的 。SpringClientFactory 给每个服务名维护一套独立的 ApplicationContext,你调了几个不同的下游服务,就有几次"第一次慢"。order-service 如果同时调 userservice 和别的服务,第一个调用的服务会慢,另一个服务的首次调用照样会慢一遍。而且应用一重启,计数清零,重来一次。
懒加载 vs 饥饿加载
饥饿加载(eager load)做的事很简单:把上面那套初始化从"第一次请求时"提前到"应用启动时" 。启动阶段就把指定服务的实例列表拉好、ILoadBalancer 装配好,第一次请求进来时直接命中缓存。
注册中心(Nacos) Ribbon(LoadBalancerClient) order-service 浏览器/调用方 注册中心(Nacos) Ribbon(LoadBalancerClient) order-service 浏览器/调用方 #mermaid-svg-LAYTBT5oPNrVwdrD{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-LAYTBT5oPNrVwdrD .error-icon{fill:#552222;}#mermaid-svg-LAYTBT5oPNrVwdrD .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-LAYTBT5oPNrVwdrD .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-LAYTBT5oPNrVwdrD .marker{fill:#333333;stroke:#333333;}#mermaid-svg-LAYTBT5oPNrVwdrD .marker.cross{stroke:#333333;}#mermaid-svg-LAYTBT5oPNrVwdrD svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-LAYTBT5oPNrVwdrD p{margin:0;}#mermaid-svg-LAYTBT5oPNrVwdrD .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LAYTBT5oPNrVwdrD text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-LAYTBT5oPNrVwdrD .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LAYTBT5oPNrVwdrD .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-LAYTBT5oPNrVwdrD .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-LAYTBT5oPNrVwdrD .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-LAYTBT5oPNrVwdrD #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-LAYTBT5oPNrVwdrD .sequenceNumber{fill:white;}#mermaid-svg-LAYTBT5oPNrVwdrD #sequencenumber{fill:#333;}#mermaid-svg-LAYTBT5oPNrVwdrD #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-LAYTBT5oPNrVwdrD .messageText{fill:#333;stroke:none;}#mermaid-svg-LAYTBT5oPNrVwdrD .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LAYTBT5oPNrVwdrD .labelText,#mermaid-svg-LAYTBT5oPNrVwdrD .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-LAYTBT5oPNrVwdrD .loopText,#mermaid-svg-LAYTBT5oPNrVwdrD .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-LAYTBT5oPNrVwdrD .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-LAYTBT5oPNrVwdrD .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-LAYTBT5oPNrVwdrD .noteText,#mermaid-svg-LAYTBT5oPNrVwdrD .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-LAYTBT5oPNrVwdrD .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LAYTBT5oPNrVwdrD .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LAYTBT5oPNrVwdrD .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-LAYTBT5oPNrVwdrD .actorPopupMenu{position:absolute;}#mermaid-svg-LAYTBT5oPNrVwdrD .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-LAYTBT5oPNrVwdrD .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-LAYTBT5oPNrVwdrD .actor-man circle,#mermaid-svg-LAYTBT5oPNrVwdrD line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-LAYTBT5oPNrVwdrD :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 懒加载:启动阶段什么都不做 第一次请求 /order/101 1 需要 userservice 实例 2 创建 LoadBalancerClient + ILoadBalancer 3 拉取 userservice 服务列表 4 返回实例清单(缓存到内存) 5 选中一个实例 6 返回(首次约 550ms) 7 第二次请求 8 直接用缓存列表 9 立即返回实例 10 返回(约 27ms) 11
浏览器/调用方 注册中心(Nacos) Ribbon(LoadBalancerClient) order-service 浏览器/调用方 注册中心(Nacos) Ribbon(LoadBalancerClient) order-service #mermaid-svg-Sb8hKI4H4tEZQyy3{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .error-icon{fill:#552222;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .marker.cross{stroke:#333333;}#mermaid-svg-Sb8hKI4H4tEZQyy3 svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Sb8hKI4H4tEZQyy3 p{margin:0;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sb8hKI4H4tEZQyy3 text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Sb8hKI4H4tEZQyy3 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-Sb8hKI4H4tEZQyy3 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .sequenceNumber{fill:white;}#mermaid-svg-Sb8hKI4H4tEZQyy3 #sequencenumber{fill:#333;}#mermaid-svg-Sb8hKI4H4tEZQyy3 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .messageText{fill:#333;stroke:none;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .labelText,#mermaid-svg-Sb8hKI4H4tEZQyy3 .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .loopText,#mermaid-svg-Sb8hKI4H4tEZQyy3 .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-Sb8hKI4H4tEZQyy3 .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .noteText,#mermaid-svg-Sb8hKI4H4tEZQyy3 .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .actorPopupMenu{position:absolute;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-Sb8hKI4H4tEZQyy3 .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-Sb8hKI4H4tEZQyy3 .actor-man circle,#mermaid-svg-Sb8hKI4H4tEZQyy3 line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-Sb8hKI4H4tEZQyy3 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 饥饿加载:启动阶段就初始化 Startup completed, 列表已就绪 应用启动,读取 clients 列表 1 拉取 userservice 服务列表 2 返回实例清单(缓存到内存) 3 ILoadBalancer 装配完成 4 第一次请求 /order/101 5 直接读缓存 6 立即返回实例 7 返回(首次也很快) 8
两张图对比下来,饥饿加载并没有让总工作量变少,它只是把"初始化"这段成本从用户请求路径上挪到了应用启动路径上。
配置方式
配置写在发起调用的一方 ,也就是 order-service 的 application.yml。本工程 order-service/src/main/resources/application.yml 里这一段是真实生效的配置:
yaml
ribbon:
eager-load:
enabled: true # 开启饥饿加载
clients: # 指定饥饿加载的服务名称
- userservice
两个键各管一件事:
enabled: true只是把开关打开,它本身不指名任何服务。clients才是真正决定"提前初始化谁"的清单。它是一个列表,而且必须手写服务名 ------服务名是以spring.application.name注册到 Nacos 的那个名字(这里是userservice),不是类名、不是包名,也不是ribbon自己的名字。
有多个下游要提前加载时,就往列表里继续加:
yaml
ribbon:
eager-load:
enabled: true
clients:
- userservice
- orderservice
- pay-service
讲义
SpringCloud01.md的 4.4 节把这段写成了clients: userservice(单个字符串)。这在 Spring Boot 的松散绑定下确实能被解析,但clients的语义是列表,只有一个元素时用行内写法没问题,一旦以后要加第二个就必须改成列表形式。工程里实际采用的是列表写法,本文以工程为准。
顺带说一句本工程的负载均衡体系:这里用的是 Ribbon (spring-cloud-starter-netflix-ribbon,配合 com.alibaba.cloud.nacos.ribbon.NacosRule 规则),所以配置键是 ribbon.eager-load.*。新版 Spring Cloud 官方推荐的是 spring-cloud-starter-loadbalancer,它也有对应的提前加载能力,配置大致挂在 spring.cloud.loadbalancer 下(如 eager-load),具体键名和层级以你所用版本的官方文档为准,不同版本有过调整,照抄容易踩空。本篇讨论的是 Ribbon 体系。
何时该开,何时别开
这不是一个"开了就更好"的开关,它有明确代价:启动阶段多了一次(或多次)注册中心拉取和对象装配,被中间的服务越多、每个服务的实例越多,启动时间就越长。做决策时按表看:
| 对比项 | 懒加载(默认) | 饥饿加载(eager-load) |
|---|---|---|
| 首次调用延迟 | 高,含完整初始化成本 | 低,首次即命中缓存 |
| 应用启动耗时 | 短 | 变长,随 clients 数量与实例规模上升 |
| 运行期首次的缓存 | 首次请求时建立 | 启动时建立 |
| 启动期对注册中心的依赖 | 无 | 有,拉不到会报错或阻塞 |
| 内存占用 | 按需增长 | 启动即为指定服务常驻 |
| 适用场景 | 调用关系稀疏、服务不常被首请求触达、启动速度敏感 | 调用链路固定、对首请求延迟敏感的 C 端核心接口 |
落到工程上可以这么判断:像 order-service 这种启动后就必然要调 userservice 的核心链路,值得开------用户不会成为"帮系统暖机"的那个人;反过来,一个内部管理后台,某个下游服务可能整晚都没人点到,开了反而白白拖慢启动、白占内存,那就保持默认。
怎么验证生效
最直接的办法是对比开启前后的首次调用耗时:固定"重启 → 立刻发第一次请求"这个流程,量两次的时间。开启前首次通常明显偏高(demo 里是 550ms 量级),开启后应下降到与后续请求同一量级。
更可靠的观察点在启动日志 。开启饥饿加载后,服务列表的拉取不再发生在你发请求的那一刻,而是紧跟在启动完成那一行之后。看 order-service 的控制台:出现 Started OrderApplication in X seconds 之后,紧接着应该就能看到针对 userservice 的服务列表拉取与负载均衡器初始化日志;如果这些日志还是在你发第一次请求时才成批打出来,说明配置没生效。本工程日志级别已开到 cn.itcast: debug,观察这两类日志足够。
实战坑位
服务名写错或干脆没写,静默不生效。 这是最坑的一个:enabled: true 加了,clients 里名字写成 user-service(多了个横杠)或漏写,应用照样正常启动、不报任何错,你会以为开好了,实测首次调用还是慢。排查时就盯着启动日志里有没有该服务名的初始化记录------没有就是名字不对。
clients 写成字符串而非列表。 单个服务时 clients: userservice 能跑,但一旦想加第二个服务仍然这么写,YAML 层面就出问题了(多值要么报解析错误,要么被当成一个奇怪的单值忽略),正确姿势是列表。列表项前的 - 和缩进别写错,YAML 对缩进敏感。
启动阶段依赖注册中心可用。 饥饿加载把"拉服务列表"搬到了启动期,意味着启动时注册中心(Nacos/Eureka)必须可达。注册中心没起来或网络不通,应用可能启动阶段就报错甚至卡住。原本懒加载下"注册中心晚几秒起来也不影响应用启动"的容错没了,部署编排时要把注册中心的启动顺序考虑进去。
以为开了就"绝对没有首次延迟"。 只能保证"启动那一刻存在的实例列表"被提前拉好;如果启动之后实例发生上下线、列表需要刷新,或某个服务的列表被清空重拉,仍可能出现短暂延迟。饥饿加载解决的是冷启动那一次,不是全部。
官方文档
总结
- Ribbon 默认懒加载,初始化(建
LoadBalancerClient、拉列表、装配ILoadBalancer)全部压在第一次调用上,所以首次慢、后续快。 - 初始化按服务名独立,有几个下游就有几次"第一次慢",重启后重新计数。
- 饥饿加载用
ribbon.eager-load.enabled: true+clients列表把这段成本前移到启动阶段;clients必须是列表,且要写注册中心里的真实服务名。 - 代价是启动变慢、启动期强依赖注册中心,值不值得开取决于该下游是不是启动后立刻要用的核心链路。
- 配了没效果时,第一件事是去启动日志里确认目标服务名有没有被初始化------多数情况是名字写错了。
- 验证手段 :对比开启前后"重启 → 立刻发第一次请求"的耗时,以及启动日志中
Started ...之后是否紧跟目标服务的列表拉取与负载均衡器初始化日志。 - 决策建议:核心链路(如 order-service 启动后必调 userservice)值得开;调用稀疏、启动速度敏感的内部后台保持默认即可。
- 版本提醒 :Ribbon 属历史体系,新版 Spring Cloud 推荐
spring-cloud-starter-loadbalancer,对应配置挂在spring.cloud.loadbalancer下,键名与层级以所用版本官方文档为准。