深度剖析 Apache Dubbo 核心技术

深度剖析 Apache Dubbo 核心技术

SPI 扩展

Dubbo 支持扩展的核心接口上,都会通过类似 @SPI("dubbo") 这样的注解,来标识当前接口的默认实现。如果你想替换掉这个默认实现,那么需要两个步骤。第一,实现 Protocol 接口,然后在 META-INF/dubbo 目录下创建一个名字为 org.apache.dubbo.rpc.Protocol 的文本文件。这个 META-INF 目录如果使用的是 IDEA 开发,那么其应该放到 resources 目录下的顶层,这样打 jar 包的时候,其也会被复制到 jar 包的第一级目录。内容如下:

myProtocol = com.zk.MyProtocol

第二,需要在 XML 配置文件中,声明使用这个扩展实现:

<dubbo:protocol name="myProtocol">

其实 JDK 本身也提供了 SPI 扩展,Dubbo 之所以没有使用默认提供的实现,是因为:

  • JDK 标准的 SPI 一次性实例化扩展点的所有实现,如果有些没有使用到,那么会浪费资源。
  • 扩展点加载失败的异常提示不是很好。
  • 增强了 Ioc 和 AOP 的支持。

性能

Dubbo 会给每个服务提供者的实现类生产一个 Wrapper 类,这个 Wrapper 类里面最终调用服务提供者的接口实现类,Wrapper 类的存在是为了减少反射的调用。当服务提供方收到消费方发来的请求后,需要根据消费者传递过来的方法名和参数反射调用服务提供者的实现类,而反射本身是有性能开销的,Dubbo 把每个服务提供者的实现类通过 JavaAssist 包装为一个 Wrapper 类以减少反射调用开销。

其实就是由反射改为了比较方法名称,然后调用,伪代码如下:

GreetingServiceImpl impl = (GreetingServiceImpl) object;

if ("sayHello".equals(methodName) && argClass.length == 1) {
    return impl.sayHello((String) argObject[0]);
}

if ("testGeneric".equals(methodName) && argClass.length == 1) {
    return impl.testGeneric((Pojo) arrObject[0]);
}

容错

异常情况下的,代码逻辑应该怎么走?Dubbo 提供了如下几种容错方案:

  • 失败重试:通常用于读操作或者具有幂等的写操作。需要注意的是,重试会带来更长延迟
  • 快速失败:抛出异常。
  • 安全失败:忽略异常,场景:写入审计日志
  • 失败自动恢复:后台记录失败请求,并按照策略后期再重试,场景:消息通知
  • 并行调用:通常用于实时性要求较高的读操作,但需要浪费更多服务资源。
  • 广播调用:通常用于通知所有提供者更新缓存或日志等本地资源信息。

负载均衡

  • 随机策略
  • 轮循策略
  • 最少活跃调用数
  • 一致性 Hash 策略

协议设计

服务消费端如何把服务请求信息序列化为二进制数据、服务提供方如何把消费端发送的二进制数据反序列化为可识别的POJO对象、Dubbo的应用层协议是怎么样的?

Dubbo Header

看一下这个 “request flag and serialization id”:高四位标示请求类型:

Flag Request

低四位标示序列化方式,其枚举值如下:

Serialization Value

再后面的一字节是只在响应报文里才设置(在请求报文里不设置),用来标示响应的结果码,具体定义如下:

Response Code

在此列出这个编码格式,是想要学习 Dubbo 是如果用较少的字节头,编码较多的信息的。还有编码的粒度,响应码这部分,并没有直接定义与业务紧密关联的状态码,比如 “磁盘存储失败” 等状态码,相反定义的是较为粗粒度的状态码,更为细粒度的可以放到 “body” 里面。