1. 项目概述:为什么Dubbo的这几个能力值得深挖
做Java后端的人,尤其是混过几年微服务的,几乎都绕不开Dubbo。这个框架在国内企业级应用里的普及率相当高,很多核心交易链路、订单系统、用户中心,底层都是它撑着的。但说实话,大部分人对Dubbo的使用还停留在“写个Interface、配个注册中心、调一下注解”的层面,一旦遇到性能瓶颈、流量不均、接口异常,就开始抓瞎。
这篇内容,围绕的是Dubbo里几个和“高可用、可扩展、易测试”强相关的能力点:SPI扩展机制、路由与负载均衡、集群容错、泛化调用、Mock测试。这几个点放在一起,其实就是一条线:当一个Dubbo应用从“能跑通”走向“跑得稳、跑得好”时,你必须啃下来的技术点。
我从实际项目角度出发,把这些机制的原理、坑点、最佳实践拆开揉碎讲清楚。适合的人群很明确:已经会用Dubbo做基础RPC调用、但想深入理解框架内部机制的后端工程师;以及正在排查线上Dubbo问题时理不清头绪的运维或开发同学。看完之后,你对Dubbo的认知层级会从“会用”提升到“能用好、能排障、能二次开发”。
2. SPI扩展机制:Dubbo灵活性的基石
2.1 从Java SPI说起,理解什么是“按需加载”
很多人一听到SPI就头疼,其实它没有那么神秘。SPI的全称是Service Provider Interface,也就是服务提供者接口。它解决的核心问题只有一个:框架在运行时,怎么决定用哪个具体实现类?
Java原生SPI的做法是:在META-INF/services目录下放一个以接口全限定名为文件名的文件,内容写实现类的全限定名。框架启动时用ServiceLoader加载这个文件,把实现类全部实例化。听起来很美好,但它有个让人恨得牙痒痒的毛病——它会把所有实现类全部加载并实例化,不管你用不用得上。这导致两个问题:一是启动变慢,因为很多类是无用加载;二是有可能因为某个实现类初始化时抛异常,整个启动流程直接崩掉。
Dubbo没有用Java原生的SPI,而是自己搞了一套。核心区别就在ExtensionLoader这个类上。Dubbo的SPI配置文件放在META-INF/dubbo或META-INF/dubbo/internal目录下,格式是key=value的形式,例如random=com.alibaba.dubbo.rpc.cluster.loadbalance.RandomLoadBalance。这个设计解决了原生SPI的两个核心痛点:
- 按需加载:你想用哪个实现,就指定哪个的key,只有被指定的实现类才会被加载。比如配置
loadbalance=roundRobin,仅仅加载轮询策略类,其他负载均衡类根本不理会。 - key标识:给每个实现类一个短名称,配置时写短名称即可,不需要写全类名,配置文件的简洁性和可读性大幅提升。
2.2 Dubbo SPI的三大增强机制:扩展点自动包装、依赖注入、动态适配
Dubbo的ExtensionLoader比原生SPI复杂很多,它在加载扩展点时动了三个非常重要的手脚:
第一个是扩展点自动包装(Wrapper)。如果某个扩展类的构造函数只有一个参数,并且这个参数是扩展接口类型,那么Dubbo会认为这是一个包装类。包装类的典型应用场景是ProtocolFilterWrapper、ProtocolListenerWrapper这类横切逻辑。当多个包装类存在时,它们会形成一个类似装饰器模式的链路,逐层包裹真正的实现类。这意味着你可以在不修改源码的情况下,给扩展点叠加额外的行为,比如加个日志、加个过滤器、做超时控制。
第二个是依赖注入(IOC)。扩展点实现类里如果有setter方法,并且参数是其他扩展点类型,ExtensionLoader在加载时会把对应扩展实现注入进去。其实质就是通过反射处理setter注入。这一点极大地支持了扩展点之间的组合使用。举个例子,你自定义了一个LoadBalance,在这个LoadBalance里需要用到某个Registry的扩展,只需要加一个setter方法,Dubbo自动帮你注入。
第三个是动态适配(Adaptive)。这才是Dubbo SPI最精髓的地方。在某些场景下,框架也不知道具体该用哪个实现类,要等到运行时根据URL中的参数来决定。ExtensionLoader会生成一个动态代理类,这个代理类根据调用时传入的URL参数中的某个key值,决定委托给哪个具体实现。比如Protocol扩展点,有dubbo协议、http协议、rest协议,代理类会根据URL的protocol字段自动选择对应的实例去处理请求。
这三重机制叠加在一起,让Dubbo的扩展能力变得极其强大。经常有互联网公司对Dubbo做二次开发,比如自研负载均衡策略、自研注册中心、自研协议层,都是在不侵入框架核心代码的前提下,通过自定义SPI扩展来完成的。这套设计,是Dubbo能保持十几年生命力的关键原因。
实操心得:自定义扩展点时的文件目录有讲究。和dubbo内部扩展区分开,建议放在
META-INF/dubbo/下,不要放在META-INF/dubbo/internal/,否则会被当成框架内部扩展处理,某些场景下行为会不一样。
2.3 手写一个自定义LoadBalance扩展,完整流程参考
光看原理容易飘,我来一个实际可操作的例子。假设我们需要一个自定义的负载均衡策略,实现“优先调用本机IP”的逻辑。这个在多机房部署或本地开发联调时很实用。
第一步,创建实现类,继承LoadBalance接口:
package com.example.dubbo.loadbalance; import com.alibaba.dubbo.common.URL; import com.alibaba.dubbo.rpc.Invocation; import com.alibaba.dubbo.rpc.Invoker; import com.alibaba.dubbo.rpc.cluster.LoadBalance; import java.net.InetAddress; import java.util.List; public class LocalFirstLoadBalance implements LoadBalance { @Override public <T> Invoker<T> select(List<Invoker<T>> invokers, URL url, Invocation invocation) { // 优先选择本机IP对应的Provider String localIp = getLocalIp(); for (Invoker<T> invoker : invokers) { String providerIp = invoker.getUrl().getHost(); if (localIp.equals(providerIp)) { return invoker; } } // 没有本机服务时,回退到随机策略 return invokers.get(ThreadLocalRandom.current().nextInt(invokers.size())); } private String getLocalIp() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (Exception e) { return "127.0.0.1"; } } }第二步,在META-INF/dubbo/com.alibaba.dubbo.rpc.cluster.LoadBalance文件中添加一行:
localFirst=com.example.dubbo.loadbalance.LocalFirstLoadBalance第三步,在消费者的XML配置或注解配置中指定:
<dubbo:reference id="demoService" interface="com.example.DemoService" loadbalance="localFirst" />这个扩展写完后,在本地联调的时候,消费端会优先调用部署在本机上的Provider节点,网络开销直接归零,接口联调响应速度会提升一个感知级别。
3. 路由与负载均衡策略:流量调度背后的逻辑
3.1 Dubbo路由和负载均衡的区别与配合
在Dubbo内部,路由和负载均衡是两个不同层级的机制,很多人混淆了。
路由(Router)发生在服务列表获取之后、负载均衡之前。它负责做的事情是“过滤”,即根据条件筛选出符合调用要求的Provider子集。比如按机房路由、按版本路由、按标签路由,都属于Router的范畴。而负载均衡(LoadBalance)负责的是“选择”,在路由过滤后的Provider列表中,按某种策略选出最终要调用的那一台机器。
这么设计的好处非常明显:路由减少了负载均衡的决策域,负载均衡则在一组等价节点中做权衡。二者职责单一、清晰。假设你有100台Provider,分布在两个机房,如果不做路由直接负载均衡,跨机房调用消耗的带宽和延迟是巨大的。但先通过路由把消费端机房外的机器过滤掉,负载均衡只在本机房的50台机器里做选择,整体调用链路的RT和稳定性会好很多。
3.2 四个内置负载均衡策略详解与选型建议
Dubbo内置了四种负载均衡策略,很多同学在配置时都是随便选一个,或者一直用默认的random,其实它们的适用场景差异很大。
随机策略(RandomLoadBalance)。默认策略,按权重设置随机概率。它的好处是简单、均衡效果平滑,在流量较大的情况下能自动趋于均分。缺点是在流量较小时,有可能出现请求扎堆到同一台机器的情况。应对措施是可以搭配最小活跃数策略做补充。
轮询策略(RoundRobinLoadBalance)。按公约后的权重设置轮询比率。这里有个坑点值得注意:早期的轮询实现,在处理慢提供者时会出现请求堆积。假设Provider A处理请求需要1秒,Provider B只需要100毫秒,轮询策略会均匀分配请求,但A会持续积压。所以轮询策略的前提是各Provider处理能力基本一致,否则反而容易拖垮性能差的节点。
最少活跃调用数策略(LeastActiveLoadBalance)。活跃数这个概念,指的是请求尚未完成的数量,Dubbo会调用性能好的机器。慢提供者的活跃数会越积越高,最后几乎接不到新请求;快提供者活跃数低,接到的请求多。这是一种自动的负载反馈调节机制,特别适合Provider处理能力参差不齐的场景,也是我比较推荐在核心链路上使用的策略。
一致性哈希策略(ConsistentHashLoadBalance)。相同参数的请求总是发到同一个Provider。注意这里说的是“相同参数”,Dubbo的实现是对第一个参数进行哈希,所以如果入参对象里包含了时间戳这类变化的字段,哈希结果会变,绑定意义也就没了。它最常见的应用场景是缓存类服务,或者有状态服务——比如用户会话、分片数据等,保证同一用户请求落到同一台机器,减少缓存穿透或重复计算。
选型建议我整理成一个表,方便查阅:
| 策略 | 核心逻辑 | 推荐场景 | 风险点 |
|---|---|---|---|
| Random | 按权重随机 | 默认配置、常规接口 | 小流量下可能不均 |
| RoundRobin | 按权重轮询 | Provider性能均衡场景 | 慢节点会导致请求堆积 |
| LeastActive | 按活跃数最小优先 | Provider性能差异大 | 需要正确统计活跃数,过滤器会影响结果 |
| ConsistentHash | 按入参哈希绑定节点 | 有状态服务、缓存 | 参数设计不合理时绑定失效 |
3.3 标签路由和条件路由的实战用法
路由这块,我在线上用得最多的是条件路由和标签路由。
条件路由通过dubbo-admin或直接修改配置中心规则实现,语法格式类似host = 10.20.153.10 => host = 10.20.153.11。含义是:当消费端来自某个IP时,只调用指定的Provider。这在灰度发布时非常有用——把新版本的Provider部署到一台机器上,然后配置“测试人员的IP访问新版本,其余走老版本”,配合路由优先级设置,就能实现非常灵活的金丝雀发布。
标签路由是Dubbo 2.7才稳定起来的功能。Provider在启动时可以通过JavaSystemConfig或配置中心给机器打标签,比如设置dubbo.provider.tag=gray表示这是一台灰度机器,消费端配置dubbo.consumer.tag=gray,只会调用打了灰度标签的机器。相比条件路由,标签路由的管理方式更轻量,不用写复杂的表达式,适合快速切换流量方向。
这里有一个很重要的细节:路由规则的生效时机不是实时的。配置中心推送路由规则后,Consumer端需要几秒钟来感知规则变化并重建路由链。如果规则写错了想快速回滚,不要只删除规则,最好同时重启消费端应用,否则路由链不会立刻失效。
4. 集群容错策略:异常场景下的最后防线
4.1 五种容错策略的语义与取舍
Dubbo的集群容错策略,决定了当调用某个Provider失败时,消费端该怎么做。它是分布式系统里“最后一道防线”,选错策略的后果可能比不选更严重。
Failover(失败自动切换)。调用失败后,自动换一台Provider重试。这是默认策略,适合读操作或幂等写操作。关键参数是retries,默认是2,表示除了第一次调用外最多再重试两次,总共最多3次调用。这里最容易踩的坑是:如果写接口没有做幂等设计,失败后重试会导致数据重复插入。我在实际项目里就遇到过用户在提交订单时网络抖动触发重试,结果订单表里出现两条一模一样的记录。所以非幂等写接口,强烈建议关闭重试,或者换成Failfast。
Failfast(快速失败)。只发起一次调用,失败立即报错。它适用于普通同步写操作,比如新增、修改,至少能保证不会因为重试造成脏数据。
Failsafe(失败安全)。调用失败时直接吞掉异常,只打日志。这个策略的语义是“调用失败也当成功”。它适合一些非核心链路的调用,比如记录日志、上报统计数据、发送通知。你肯定不希望因为一条短信通知发送失败,而影响用户主流程下单的成功率。
Failback(失败自动恢复)。失败后记录下这次失败的调用,定时重发。它和Failover的区别在于重试是异步的。适合一些实时性要求不高但最终必须送达的任务,比如消息推送、数据同步。但要注意,如果任务量大且长时间不可恢复,内存里积压的任务会占用大量内存空间。
Forking(并行调用)。同时调用多个Provider,只要有一个成功就算成功。它把本来要串行重试的时间,换成了并行节约的时间。通常配合forks=2来设置并行调用的节点数。这个策略对资源消耗比较大,适合读操作或高实时性要求且资源充足的场景。
4.2 容错策略与重试参数的最佳搭配
说句实在话,容错策略不是孤立生效的,它和超时时间、重试次数、线程池大小几个参数强关联。我见过太多团队只调了cluster配置,其他什么都不动,结果线上该报错还是报错。
梳理一下搭配逻辑:
- 超时时间(timeout):一条RPC调用从发出到返回的最大等待时间,默认1000毫秒。如果业务接口平均耗时800毫秒,高峰期可能到1.5秒,那超时设置1000就太激进了。建议按照TP99耗时的两倍设置。
- 重试次数(retries):Failover策略下生效。每重试一次,调用耗时可能会翻倍,因为你要等待上一次调用超时后才能发起下一次。所以超时时间大 + 重试次数多 = 接口响应极慢,严重时把消费端线程池打满。
- 消费端线程池:默认的线程池策略是fixed,大小为200。如果一次调用因为超时和重试消耗过多线程,后续请求会被直接拒绝,表现为
Thread pool is exhausted。
一个我常用的黄金配比是:读接口timeout=1000, retries=1,写接口timeout=2000, retries=0,核心链路全部开启sendHeartbeatToProvider和check=false。这样配置的核心思路是:读操作可以容忍重试但不要等太久,写操作宁可快速失败也不能重复执行。
4.3 容错策略报错信息的排查思路
Failover策略还有一个让新手困惑的点:报错信息里会出现多个异常。比如控制台提示Failed to invoke the method ... in the service ... Tried 3 times of the providers。注意“Tried 3 times”里的三台机器,可能一台是超时、一台是连接拒绝、还有一台是线程池满了。这时不要抓瞎,逐台机器去看指标。
排查优先级我建议按这个顺序来:
- 先看是否有机器处于不健康状态(连接拒绝、心跳异常);
- 再看Provider端的线程池使用率,如果持续在90%以上,说明业务处理能力已达上限;
- 最后再看单次调用的RT分布,如果超时占比高,优先考虑是业务逻辑慢还是网络慢。
把这个排查顺序做到条件反射,线上出问题时能省至少半小时的定位时间。
5. 泛化调用:没有接口依赖的RPC调用方案
5.1 泛化调用到底解决了什么问题
Dubbo的泛化调用,核心解决的是“消费端没有接口jar包时,该怎么完成RPC调用”的问题。
常规情况下,Consumer要调用Provider,必须在本地引入接口类。但在某些真实场景里,这个条件是不成立的。最典型的就是网关场景。一个API网关需要代理成百上千个Dubbo服务,这些服务的接口五花八门,不可能在网关工程里把所有接口类都引进来。更不现实的是,新上一个服务还得重新发版网关。
泛化调用的思路是把接口调用的过程标准化:不依赖具体接口类,通过统一的GenericService入口,传入接口名、方法名、参数类型和参数值完成调用。Provider端接收到泛化请求后,会把参数值还原成真正的接口入参对象,完成调用后再把返回值转换成Map结构回传。
5.2 消费端泛化调用的代码实现
在消费端做泛化调用,有两种方式,一种基于API,一种基于Spring XML配置。我用API方式的示例,因为它更直观,也适合在非SpringBoot的普通工程里使用。
ApplicationConfig application = new ApplicationConfig(); application.setName("generic-consumer"); RegistryConfig registry = new RegistryConfig(); registry.setProtocol("nacos"); registry.setAddress("127.0.0.1:8848"); ReferenceConfig<GenericService> reference = new ReferenceConfig<GenericService>(); reference.setApplication(application); reference.setRegistry(registry); reference.setInterface("com.example.OrderService"); // 注意:只写接口名,不需要引入接口类 reference.setGeneric(true); // 开启泛化调用 GenericService genericService = reference.get(); // 参数1:方法名;参数2:参数类型列表;参数3:实际参数值列表 Object result = genericService.$invoke("queryOrder", new String[]{"java.lang.Long"}, new Object[]{1001L}); Map<String, Object> resultMap = (Map<String, Object>) result; System.out.println(resultMap.get("orderNo"));这里有两个坑点值得重点提醒:
- 接口里的返回值如果是自定义对象,泛化调用返回的是一个
Map结构,字段名对应Java对象里的属性名。如果有嵌套对象,嵌套也是Map,需要逐层还原。 $invoke的第一个参数是方法名,第二个参数是参数类型的全限定名。java.lang.String不能简写成String,否则Provider在方法匹配时会失败。
5.3 Provider端泛化调用的暴露方式
Provider端要支持泛化调用,需要做一层包装。如果业务没有特殊要求,最简单的做法是在ServiceConfig上设置generic标识。
在XML配置中,可以通过这样暴露:
<dubbo:service interface="com.example.OrderService" ref="orderServiceImpl" generic="true" />当generic=true时,Provider把服务暴露成GenericService的实例,但会委托给具体的orderServiceImpl执行。也就是说,外部调用方用泛化接口调进来后,Provider会把它翻译成真实接口的调用。
这里有个性能方面的提示:泛化调用的性能比普通RPC调用稍低,因为它多了一层参数反序列化和类型转换。在一些超高QPS的核心接口上,不建议全面泛化调用,只对网关入口、测试平台这类流量可控的场景开启就好。
5.4 泛化调用最佳实践:搭建一个轻量级Dubbo网关
结合我做过的实际项目,这里分享一个用泛化调用搭建轻量级Dubbo调试网关的思路。这个网关不需要引入任何业务接口jar包,接口提供方的同学只需要在网关的配置中心注册接口元数据(接口名、方法名、参数列表),网关就能动态完成调用了。
整体流程可以拆成四步:
- 在网关维护一张元数据表:接口名、方法名、参数类型列表、所属分组。
- 收到HTTP请求后,网关从URL或请求体中解析出接口名、方法名和参数值。
- 从元数据表中取到对应的ReferenceConfig,如果是第一次调用,先构建并缓存到本地Map中。
- 调用
$invoke方法执行RPC调用,把返回的Map结构转成JSON响应给前端。
这套方案在公司内部用来给前端同学做接口联调看板非常实用,前端不用了解Dubbo的任何细节,只需要按接口文档传入参数,就能拿到返回结果。
6. Mock测试与本地伪装策略
6.1 Dubbo Mock的两种模式:本地伪装与降级
Dubbo的Mock机制,设计上考虑到了两类场景:本地伪装和调用降级。
本地伪装(Local Mock)指的是在消费端提供一个本地Mock实现类,当远程调用尚未完成、或者无法访问远程Provider时,消费端直接调用Mock实现完成业务逻辑,而不是直接抛出异常。这本质上是容错策略的一种补充。区别在于容错策略决定的是“要不要换个机器再试”,Mock决定的是“这次调用到底成功还是失败、以及失败时怎么做”。
降级则是指调用失败时返回兜底结果,比如返回空集合、返回默认配置值等,保证主流程不受影响。
比较典型的业务场景是:用户下单时需要调用库存服务扣减库存。如果因为库存服务抖动导致下单失败,用户体验很差。但如果配置了Mock,在库存服务不可用时,Mock里可以走一个“锁定库存名额”的本地缓存方案,先把订单流程走完,后续异步补偿库存。当然这个方案需要考虑最终一致性,但在业务上远比一个硬错误提示要好。
6.2 如何正确配置和使用Mock
在消费端配置Mock,有三种常见方式。
第一种是通过配置接口的mock属性直接指定Mock类的全限定名:
<dubbo:reference id="inventoryService" interface="com.example.InventoryService" mock="com.example.mock.InventoryServiceMock" />Mock类需要实现对应的业务接口,并且提供一个无参构造函数:
public class InventoryServiceMock implements InventoryService { @Override public boolean deductStock(Long skuId, Integer count) { // 返回一个兜底结果,让主流程继续 System.out.println("InventoryService mock is active, skuId=" + skuId); return true; } }第二种是通过mock="true",再配合接口同包名下的接口名Mock后缀类来生效。
第三种是通过mock=force:return null或mock=fail:return null的形式来设置强制Mock和失败Mock。force表示强制执行,不管Provider是否正常都会走Mock;fail表示仅当调用失败时才走Mock。
6.3 Mock测试在单元测试中的另类用法
除了线上降级,Mock机制在测试中也有大用途。在编写集成测试时,我不喜欢真正启动一个完整的Provider端——太慢而且依赖太多中间件(数据库、MQ等)。这时候可以利用Dubbo的Mock机制,把消费端调用的Provider全部替换成Mock实现,把集成测试降级为单元测试,只验证消费端自身的逻辑。
操作方法是:在测试环境的配置文件中,把消费端的mock属性指定为测试专用的Mock类。比如,测试订单服务的下单流程,把库存服务、优惠券服务、积分服务全部Mock掉:
public class StockServiceMock implements StockService { @Override public boolean deductStock(Long skuId, Integer count) { return true; // 永远扣减成功 } }这种方式跑测试很快,而且不依赖任何外部服务。但要注意,测试完以后一定要记得把Mock配置移出正式环境,否则线上会带病运行——所有库存扣减永远返回成功,这会出大事故。
除了Dubbo自带的Mock,也可以结合项目里常用的Mock工具达到更细粒度的测试效果。比如我在调试一个消费端调用链时,会比较喜欢用可编程的Mock框架,能够做到对某些特定入参返回特定出参,对其他入参走真实逻辑。典型做法是实现一个Mock类,内部放一个策略Map,key是入参特征,value是返回结果:
public class FlexibleMockService implements SomeService { private final Map<String, Object> stubMap = new HashMap<>(); public void stub(String param, Object result) { stubMap.put(param, result); } @Override public Object invoke(String param) { if (stubMap.containsKey(param)) { return stubMap.get(param); } return "default"; } }这种做法在联调时非常灵活,可以让测试人员在一个Mock服务上模拟出各种异常分支,而不需要真的去构造复杂的Provider环境。
实操心得:Mock的兜底结果要谨慎设计。我在项目里见过有人把Mock直接返回null,然后上游对null没有判空直接NPE,导致排查了很久才发现是Mock惹的祸。所以Mock返回值建议使用空集合、空对象这类有明确语义的兜底值,而不是简单的null。
6.4 Mock和容错策略的联动配置最佳实践
Mock和容错的配合是有层次讲究的。我推荐的设计是:
- 核心写操作:容错用
failfast,Mock开fail:return兜底。这样既不会重复调用导致脏数据,又能在失败时给出一个温和的反馈。 - 核心读操作:容错用
failover配合retries=1,Mock开force:return兜底。强制Mock在Provider全挂时还能返回预设数据,对于展示型接口非常管用。 - 非核心操作:容错用
failsafe,同时Mock开fail:return null,吞掉异常保证主流程不受影响。
这套组合配置,在线上压测和故障演练时尤其有用。故障演练的常见做法是直接把一台或多台Provider停掉,看消费端行为是否符合预期,而Mock配置的存在能让系统在部分故障下依然保持可用状态,服务降级的效果一目了然。
7. 最后的经验总结:从会用Dubbo到能驾驭Dubbo
讲了这么多,最后聊聊我个人在实际项目里反复摸爬滚打后的体会。
Dubbo这套框架里,SPI、路由、负载均衡、容错、泛化调用、Mock这些能力,单看每一个都不算特别复杂,但它们组合在一起,能搭建出一套非常健壮的微服务基础设施。我踩过最大的坑,不是某个配置不会写,而是没有把整个调用链路串起来理解。比如负载均衡选型、容错重试参数、超时配置,这三者是强耦合的,只调一个往往会引发其他问题,必须作为一个整体来设计。
另一个非常重要的建议是:任何对Dubbo配置的改动,都要先在压测环境做故障演练验证,再上生产。因为配置本身的语法错误往往不会导致启动失败,只会在特定故障场景下暴露问题。等线上出了问题再去查配置,代价就大了。
泛化调用和Mock这两个能力,我认为是很多团队还没有充分利用的价值点。泛化调用可以帮你快速搭出网关、测试平台这类赋能工具,Mock则能让你的故障演练和集成测试效率翻倍。如果你们公司Dubbo服务多、接口杂,我强烈建议先把这两个能力用起来,体感会非常明显。