Dubbo3 Proxyless Mesh 源码分析

438 阅读7分钟

在云原生时代背景下,Dubbo进行了重大升级,由Dubbo2变更到Dubbo3。

较于之前人们所熟知的 Dubbo2 版本,Dubbo3 扩展并改造了很多特性。比如:

  • 服务由接口级变成应用级
  • 注册中心数据去瘦,设立元数据中心,配置中心分担不同类型的数据存储
  • 支持云原生,底层适配 Kubernetes、Service Mesh等

Service Mesh 是近年来在云原生背景下诞生的一种微服务架构,Istio是 Service Mesh的开源代表实现。Dubbo 已经实现了对 Istio 体系的全面接入。

Service Mesh 主要由控制面和数据面组成。

在控制面中,Istio 负责分析治理 Dubbo 服务。

在数据面中,主要有两种模式。

  • Proxy 模式。Dubbo 和 Envoy 一起部署,Dubbo作为编程框架和协议通信组件存在,流量管控由 Envoy 与 Istio 控制面交互实现。
  • Proxyless 模式。Dubbo 进程保持独立部署,Dubbo 通过 xDS 协议直接进入 Istio 等控制面组件。

相当于 Proxy 模式,Proxyless 模式落地成本较低,能够有效规避 Proxy 模式带来的性能损耗和部署架构复杂性。

参考文章:cn.dubbo.apache.org/zh-cn/blog/…

cn.dubbo.apache.org/zh-cn/overv…

在本文中,将对Proxyless 模式下的源码进行简单分析。

代码分支:3.1

重要的配置信息:

# 由于 Dubbo 3 应用级服务发现的元数据无法从 istio 中获取,需要走服务自省模式。
# 这要求了 Dubbo MetadataService 的端口在全集群的是统一的。
dubbo.application.metadataServicePort=20885
# 走xds协议
dubbo.registry.address=xds://istiod.istio-system.svc:15012

根据官网的 Proxyless debug指导案例,进行调试分析,指导文档:cn.dubbo.apache.org/zh-cn/overv…

服务发现

xDS接入以注册中心的模式对接,节点发现同其他注册中心的服务自省模型一致,对于 xDS 的负载均衡和路由配置通过 ServiceInstance 的动态运行时配置传出, 在构建 Invoker 的时候将配置参数传入配置地址。

服务发现时,有一步会执行 RegistryProtocol.refer方法,获取注册存储对象。URL资源信息如图所示。与一般注册类型URL不同,该协议头为 xds,故获取的 Registry 对象为 XdsRegistry。

RegistryProtocol.png

但是 XdsRegistry 对象是空实现,xDS 只支持 Service Discovery 模式,XdsServiceDiscovery 是真正的 xDS 实现。

与其他服务发现模型一致,都继承于 AbstractServiceDiscovery 对象,进行扩展。Proxyless 模式下,对应的服务发现模型为 XdsServiceDiscovery

XdsServiceDiscovery.png

对应的服务发现工厂类为 XdsServiceDiscoveryFactory,创建获取XdsServiceDiscovery,并进行初始化。从 debug 数据截图中,可以看出 url 的协议头为 xds,注册地址为 xds://istiod.istio-system.svc:15012

XdsServiceDiscoveryFactory.png

初始化时,会创建一个 PilotExchanger 对象。

Pilot 是一个重要的 Istio 组件,位于控制平面,归属于 Istiod组件(Pilot、Citadel、Galley)。其中 Pilot 组件负责提供服务发现、智能路由(如金丝雀发布)和弹性功能(如超时、重试);Citadel 负责安全,管理密钥和证书;Galley 负责对配置的验证和处理等功能。

当服务网格中的微服务存在调用和被调用的关系时,如服务A调用服务B,A的 sidecar 代理需要调用 Istio 控制面 Pilot 组件的服务发现接口,获得服务B的实例列表,才能将拦截的出口流量转到正确的目的地址。

初始化的逻辑可以分为下面几步:

  • 初始化 XdsChannel。

  • 创建 LdsProtocol,EdsProtocol,RdsProtocol 对象,都将上述的 XdsChannel 对象传入其中,并设置了 ScheduledExecutorService ,进行定时调度。

    • LDS,Listener 发现服务:Listener 监听器控制 [sidecar] 启动端口监听(目前只支持 TCP 协议),并配置 L3/L4 层过滤器,当网络连接达到后,配置好的网络过滤器堆栈开始处理后续事件
    • EDS,Endpoint 发现服务:用于动态维护端点信息,端点信息中还包括负载均衡权重、金丝雀状态等,基于这些信息,[sidecar] 可以做出智能的负载均衡决策。
    • RDS,Router 发现服务:用于 HTTP 连接管理过滤器动态获取路由配置,路由配置包含 HTTP 头部修改(增加、删除 HTTP 头部键值),virtual hosts (虚拟主机),以及 virtual hosts 定义的各个路由条目。

LdsProtocol、EdsProtocol、RdsProtocol都继承了 AbstractProtocol 抽象类,其中 node 变量id 包含 调用方pod-name,pod-ip 信息

AbstractProtocol.png

Pod.png

  • 向Istio发起gRpc请求,获取 Listener 信息
this.listenerResult = ldsProtocol.getListeners();

Istio端侧提供的服务为 envoy.service.discovery.v3.AggregatedDiscoveryService

getResource.png

返回的 listenerResult 是一个 HashSet

ListenerResult.png

  • 根据上一步骤获取的 listenerResult 向 istio 查询 RouteResult, 存储形式为 Map<String, Set<String>> domainMap;

本次调试案例的服务提供方应用名为 dubbo-samples-xds-provider,在k8s中的命名空间为 dubbo-demo,相关的部分路由信息有:

RouteResult.png

  • Observer RDS 更新。
  • Observer LDS 更新。 上述的两个 Observer 更新逻辑实际上大致相同,都会调用一些公用的抽象方法,我将公共抽象方法的分析放到下面的 Observer Resource

以上是 XdsServiceDiscovery 的初始化过程。

在 XdsServiceDiscovery 中有一个获取服务的方法,getInstance,服务引用订阅url时,会调用该方法,获取服务地址等信息。

getInstance.png

Endpoint 对象记录了端点信息和权重等信息。在 XdsServiceDiscovery 中会将 Endpoint 转化为 ServiceInstance 对象。

首先会从上述初始化中的 RouteResult 中获取路由信息,结果为数量为1的Set集合,outbound|50051||dubbo-samples-xds-provider.dubbo-demo.svc.cluster.local

然后调用 edsProtocol.getResource 方法,获取 Endpoint 信息

getEndpoints.png

上图中显示有两个 Endpoint,实际就是 k8s 中的部署的两个 pod,地址为 podIp

provider-node.png

public class Endpoint {
    private String address;
    private int portValue;
    private boolean healthy;
    private int weight;
}

Observer Resource

1)创建获取一个 requestId,保存在名为 requestParam 的 ConcurrentHashMap 中,key 为 requestId, value 为 resourceNames集合。 2)从 Istio 中获取这些 resource 信息 3)Consumer 对象处理 4)channel复用,将结果存放在名为 requestObserverMap 的 ConcurrentHashMap 中,key 为 requestId,value 为 StreamObserver 5)创建一个定时任务 从 requestParam 获取 resourceNames集合 定义一个 CompletableFuture 对象,并存放在名为 streamResult 的 ConcurrentHashMap 中,key 为 requestId,value 为 CompletableFuture。 从 requestObserverMap 中获取 StreamObserver,如果没有,重新创建一个 StreamObserver,并重新存放进去。

通过 StreamObserver 发送请求到 控制平面。 获取结果,并 Consumer.accept 处理。 之后从 StreamObserver 移除该 CompletableFuture

6)将该定时任务存放到名为 observeScheduledMap 的 ConcurrentHashMap 中,key 为 requestId,value 为 ScheduledFuture

那如果 k8s 部署的服务发生了变动,比如修改 deployment 的 replicas 数量,pod数量发送了变化,Dubbo 如何感知呢

Dubbo服务引用时,会订阅url,并添加 Listener 进行监听处理,具体方法如下所示:

addServiceInstancesChangedListener

该方法被调用的时机是服务引用时订阅url时。

1)根据传入的 ServiceInstancesChangedListener ,获取所有的 ServiceName,监听每个服务的 Endpoints。

ServiceInstancesChangedListener.png

2) 监听 Endpoints 时,先从 routeResult 获取相关的 路由信息,然后根据路由信息向 Istio 发起 grpc 请求,获取最新的 Endpoints信息,并转化为 ServiceInstance,然后交由 notifyListener 方法处理。 2.1)将 ServiceInstance 集合转化为 Json 字符串,并计算 md5。

然后从 serviceInstanceRevisionMap 中获取该服务之前的 md5 值,进行对比是否相等,如果相等,则无须继续处理,结束。

当发生变化时,会从缓存的 cachedServiceInstances 的 ConcurrentHashMap 中根据服务名获取之前的 ServiceInstance 集合信息,并比新的 ServiceInstance 做对比,移除过期 ServiceInstance,并消除对应的 Metadata,并处理 ServiceInstancesChangedEvent 事件。

serviceInstanceRevision.png

在这里简单介绍下 md5,md5计算,是对原始消息做有损的压缩算法,无论消息(输入值)的长度是多少字节,都会生成一个固定长度(128位/16字节)的消息摘要(输出值)。文件经过网络传输、拷贝或其他操作后,可以通过文件的md5值判断文件内容是否发生了改变。文件名改变、但文件内容不变时,文件的md5值会保持不变。所以,md5 可以用来进行 数据完整性校验等。

3)创建一个 ScheduledFuture,定期处理步骤2的逻辑。

  • END -