在分布式 RPC 框架中,Dubbo 是 Java 类项目中卓越的框架之一,它提供了注册中心机制,解耦了消费方和服务方动态发现的问题,并提供高可靠能力,大量采用微内核加富插件设计思想,包括框架自身核心特性都作为扩展点实现,提供灵活的可扩展能力。
- Provider 启动时会向注册中心把自己的元数据注册上去(比如服务 IP 和端口等)。
- Consumer 在启动时从注册中心订阅(第一次订阅会拉取全量数据)服务提供方的元数据,注册中心发生数据变更时会推送给订阅的 Consumer。
- 在获取服务元数据后,Consumer 可以发起 RPC 调用,在 RPC 调用前后会向监控中心上报统计信息。
使用示例
服务提供者发布服务:
ServiceConfig<DemoService> serviceConfig = new ServiceConfig<>();
serviceConfig.setApplication(new ApplicationConfig("dubbo-sample-api-provider"));
serviceConfig.setInterface(DemoService.class);
serviceConfig.setRef(new DemoServiceImpl());
serviceConfig.setRegistry(new RegistryConfig("zookeeper://180.76.234.113:2181"));
serviceConfig.setProtocol(new ProtocolConfig("dubbo", -1));
serviceConfig.setVersion("1.0.0");
serviceConfig.export();
服务消费者消费服务:
ReferenceConfig<DemoService> reference = new ReferenceConfig<>();
reference.setApplication(new ApplicationConfig("dubbo-sample-api-consumer"));
reference.setRegistry(new RegistryConfig("zookeeper://180.76.234.113:2181"));
reference.setInterface(DemoService.class);
reference.setVersion("1.0.0");
DemoService demoService = reference.get();
demoService.sayHello("dubbo")
Dubbo 分层架构
1. Business 层
业务代码的接口和实现。我们实际使用的 Dubbo 的业务层级。该层是与实际业务逻辑相关的,根据服务提供方和服务消费方的业务设计对应的接口和实现。
2. RPC 层
1)Config 配置层
Config 层是为了让 Dubbo 使用方方便地发布服务和引用服务,主要围绕 ServiceConfig(暴露的服务配置)和 ReferenceConfig(引用的服务配置)两个实现类展开。Dubbo 服务发布与引用初始化配置信息,可以直接 new 配置类,也可以通过 Spring 解析配置生成配置类。该层管理了整个 Dubbo 的配置。
2)Proxy 代理层
该层主要是对服务消费端使用的接口进行代理,把本地调用透明的转换为远程调用,代理层会自动做远程调用并返回结果,即让业务层对远程调用完全无感。Proxy 层的 SPI 扩展接口为 ProxyFactory,Dubbo 提供的实现类主要有 JavassistProxyFactory 和 JdkProxyFactory,用户可以实现 ProxyFactory SPI 接口,自定义代理服务层的实现。
3)Registry 注册中心层
服务提供者启动时会把服务注册到服务注册中心,消费者启动时会去服务注册中心获取相应服务提供者的地址列表,当有新的服务加入或旧服务下线时,注册中心都会感知并通知给所有订阅方,整个过程不需要人工参与。Registry 层的主要功能是封装服务地址的注册与发现逻辑,扩展接口 Registry 对应的扩展实现为 ZookeeperRegistry、RedisRegistry、MulticastRegistry、DubboRegistry 等。扩展接口 RegistryFactory 对应的扩展接口实现为 ZookeeperRegistryFactory、RedisRegistryFactory、DubboRegistryFactory。用户可以实现该层的一系列扩展接口,自定义该层的服务实现。
4)Cluster 集群容错层
该层主要负责节点管理、负载均衡、服务路由、服务容错等。Invoker 是对服务提供者节点的抽象,Invoker 封装了服务提供者的地址以及接口信息。
节点管理:Directory 负责从注册中心获取服务节点列表,并封装成多个 Invoker,可以把它看成 “List
“ ,它的值可能是动态变化的,比如注册中心推送变更时需要更新。 服务路由:Router 负责从多个 Invoker 中按路由规则选出子集,比如读写分离、机房隔离等。
负载均衡:LoadBalance 负责从多个 Invoker 中选出某一个用于发起调用,选择时可以采用多种负载均衡算法,比如 Random、RoundRobin、LeastActive 等。
服务容错:Cluster 将 Directory 中的多个 Invoker 伪装成一个 Invoker,对上层透明,伪装过程包含了容错逻辑,比如采用 Failover 策略的话,调用失败后,会选择另一个 Invoker,重试请求。
5)Monitor 监控层
进行埋点数据采集,包括 RPC 调用次数和调用时间等,然后上报给监控系统。在 Dubbo 框架中,无论是服务提供者还是服务消费者,在执行服务调用的时候,都会经过 Filter 调用链拦截,来完成一些特定功能,监控数据埋点就是通过在 Filter 调用链上装备了 MonitorFilter 来实现的。
6)Protocol 远程调用层
封装 RPC 调用具体过程,Protocol 是 Invoker 暴露和引用的主功能入口,负责管理 Invoker 的整个生命周期。Invoker 是 Dubbo 的核心模型,框架中所有其他模型都向它靠拢,或者转换成它,它代表一个可执行体。允许向它发起 invoke 调用,它可能是执行一个本地的接口实现,也可能是一个远程的实现,还可能是一个集群实现。
3. Remote 层
1)Exchange
信息交换层。建立 Request-Response 模型,封装请求响应模式,如把同步请求转化成异步请求。扩展接口为 Exchanger、ExchangeChannel、ExchangeClient、ExchangeServer。是整个远程调用非常核心的部分。
2)Transport
网络传输层。把网络传输抽象为统一的接口,如 Mina 和 Netty 虽然接口不一样,但是 Dubbo 在它们上面又封装了统一的接口,核心扩展接口为 Transporter、Channel。在进行网络传输时,通过 Transporter 的具体扩展实现类,可以进行 bind 和 connect 操作,获取到相应的 Client 和 RemotingServer 实现,就可以建立 Channel 与远端进行交互了。无论是 Client 还是 RemotingServer,都会使用 ChannelHandler 处理 Channel 中传输的数据,其中负责编解码的 ChannelHandler 被抽象出为 Codec2 接口。
3)Serialize
数据序列化层。如果数据要通过网络进行发送,则需要先进行序列化,变成二进制流。序列化层负责管理整个框架网络传输时的序列化和反序列化工作。Dubbo 同样也支持多种序列化格式,比如 Dubbo、Hession 2.0、JSON、Java、Kryo 以及 FST 等,扩展接口为 Serialization。
Dubbo 远程调用
1. 服务发布
服务器端(服务提供者)在框架启动时,会初始化服务实例,ServiceConfig 类引用对外提供服务的实现类 ref,然后通过 ProxyFactory 接口的扩展实现类的 getInvoker() 方法使用 ref 把服务端要暴露的接口封装成 Invoker 实例,真实类型是 AbstractProxyInvoker,到这一步就完成了具体服务到 Invoker 的转化。
接下来就是 Invoker 转换到 Exporter 的过程,Exporter 是用于暴露到注册中心的对象,它内部持有 Invoker 对象,我们可以认为它是在 Invoker 上包了一层。Dubbo 协议的 Invoker 转为 Exporter 发生在 Protocol 接口的扩展实现类的 export() 方法中,在这个过程中会先启动 Netty Server 监听服务连接,打开服务端口并记录服务实例到内存中,最后通过 Registry 将服务元数据注册到服务注册中心,完成服务的注册和发现。
2. 服务消费
消费方在启动的时候会通过 Registry 在注册中心订阅服务端的数据(包括 IP 和端口),这样就可以得到服务端暴露的服务了。
首先,调用过程也是从一个 Proxy 开始的,Proxy 持有了一个 Invoker 对象,然后触发 invoke 调用。在 invoke 调用过程中,需要使用 Cluster,Cluster 负责容错,如调用失败的重试。Cluster 在调用之前会通过 Directory 获取所有可以调用的远程服务 Invoker 列表(一个接口可能有多个节点提供服务)。由于可以调用的远程服务有很多,此时如果用户配置了路由规则,那么还会根据路由规则将 Invoker 列表过滤一遍。
然后存活下来的 Invoker 可能还会有很多,此时要调用哪一个呢?于是会继续通过 LoadBalance 方法做负载均衡,最终选出一个可以调用的 Invoker。这个 Invoker 在调用之前又会经过一个过滤器链,这个过滤器链通常是处理上下文、限流、计数等。
接着,会使用 Client 做数据传输,如常见的 Netty Client 等。传输之前肯定要做一些私有协议的改造,此时就会用到 Codec 接口。构造完成后就对数据包做序列化,然后传输到服务提供者端。服务提供者收到数据包,也会使用 Codec 处理协议头及一些半包、粘包等。处理完成后再对完整的数据报文做反序列化处理。
随后,这个 Request 会被分配到线程池(ThreadPool)中进行处理。Server 会处理这些 Request,根据请求查找相应的 Exporter(它内部持有 Invoker)。Invoker 是被用装饰器模式一层一层套了非常多 Filter 的,因此在调用最终的实现类之前,又会经过一个服务提供者端的过滤器链。最终,得到了具体接口的真实实现并调用,再原路把结果返回。