乐趣区

关于阿里云:Dubbo-在-Proxyless-Mesh-模式下的探索与改进

01 背景

随着 Docker 和 Kubernetes 的呈现,一个宏大的单体利用能够被拆分成多个独立部署的微服务,并被打包运行于对应的容器中。不同利用之间互相通信,以共同完成某一功能模块。微服务架构与容器化部署带来的益处是不言而喻的,它升高了服务间的耦合性,利于开发和保护,能更无效地利用计算资源。当然,微服务架构也存在相应的毛病:

  • 强依赖于 SDK,业务模块与治理模块耦合较为重大。除了相干依赖,往往还须要在业务代码中嵌入 SDK 代码或配置。
  • 对立治理难。每次框架降级都须要批改 SDK 版本,并从新进行回归测试,确认性能失常后再对每一台机器重新部署上线。不同服务援用的 SDK 版本不对立、能力参差不齐,增大了对立治理的难度。
  • 短少一套对立解决方案。目前市场不存在一整套功能完善、无死角的微服务治理与解决方案。在理论生产环境往往还须要引入多个治理组件来实现像灰度公布、故障注入等性能。

为解决这些痛点,Service Mesh 诞生了。以经典的 side car 模式为例,它通过在业务 Pod 中注入 Sidecar 容器,对代理流量施行治理和管控,将框架的治理能力上层到 side car 容器中,与业务零碎解耦,从而轻松实现多语言、多协定的对立流量管控、监控等需要。通过剥离 SDK 能力并拆解为独立过程,从而解决了强依赖于 SDK 的问题,从而使开发人员能够更加专一于业务自身,实现了根底框架能力的下沉,如下图所示(源自 dubbo 官网):

经典的 Sidecar Mesh 部署架构有很多劣势,如缩小 SDK 耦合、业务侵入小等,但减少了一层代理,也带来了一些额定的问题,比方:

  • SideCar 代理会损耗一部分性能,当网络结构层级比较复杂时尤其显著,对性能要求很高的业务造成了肯定的困扰。
  • 架构更加简单,对运维人员要求高。
  • 对部署环境有肯定的要求,须要其能反对 SideCar 代理的运行。

为解决这些痛点,Proxyless Service Mesh 模式诞生了。传统服务网格通过代理的形式拦挡所有的业务网络流量,代理须要感知到管制立体下发的配置资源,从而依照要求管制网络流量的走向。以 istio 为例,Proxyless 模式是指利用间接与负责管制立体的 istiod 过程通信,istiod 过程通过监听并获取 k8s 的资源,例如 Service、Endpoint 等,并将这些资源对立通过 xds 协定下发到不同的 rpc 框架,由 rpc 框架进行申请转发,从而实现服务发现和服务治理等能力。

Dubbo 社区是国内最早开始对 Proxyless Service Mesh 模式进行摸索的社区,这是因为相比于 Service Mesh,Proxyless 模式落地老本较低,对于中小企业来说是一个较好的抉择。Dubbo 在 3.1 版本中通过对 xds 协定进行解析,新增了对 Proxyless 的反对。Xds 是一类发现服务的总称,利用通过 xds api 能够动静获取 Listener(监听器),Route(路由),Cluster(集群),Endpoint(集群成员) 以及 Secret(证书) 配置。

通过 Proxyless 模式,Dubbo 与 Control Plane 间接建设通信,进而实现管制面对流量管控、服务治理、可观测性、平安等的对立管控,从而躲避 Sidecar 模式带来的性能损耗与部署架构复杂性。

02 Dubbo Xds 推送机制详解

@startuml

'======== 调整款式 =============' 单个状态定义示例:state 未提交 #70CFF5 ##Black
'hide footbox 可敞开时序图上面局部的模块' autoactivate on 是否主动激活
skinparam sequence {
ArrowColor black

LifeLineBorderColor black
LifeLineBackgroundColor #70CFF5

ParticipantBorderColor #black
ParticipantBackgroundColor  #70CFF5
}
' ======== 定义流程 =============

activate ControlPlane
activate DubboRegistry
autonumber 1


ControlPlane <-> DubboRegistry : config pull and push
activate XdsServiceDiscoveryFactory
activate XdsServiceDiscovery
activate PilotExchanger
       
DubboRegistry -> XdsServiceDiscoveryFactory : request
XdsServiceDiscoveryFactory --> DubboRegistry: get registry configuration

XdsServiceDiscoveryFactory -> XdsChannel: 返回列表信息(若数据没有导入实现,则不可见)XdsServiceDiscoveryFactory-> XdsServiceDiscovery: init Xds service discovery
XdsServiceDiscovery-> PilotExchanger: init PilotExchanger

alt PilotExchanger
  PilotExchanger -> XdsChannel: 初始化 XdsChannel
  XdsChannel --> PilotExchanger: return
  PilotExchanger -> PilotExchanger: get cert pair
  PilotExchanger -> PilotExchanger: int ldsProtocol
  PilotExchanger -> PilotExchanger: int rdsProtocol
  PilotExchanger -> PilotExchanger: int edsProtocol
end

alt PilotExchanger
  XdsServiceDiscovery --> XdsServiceDiscovery: 解析 Xds 协定
  XdsServiceDiscovery --> XdsServiceDiscovery: 依据 Eds 初始化节点信息
  XdsServiceDiscovery --> XdsServiceDiscovery: 将 Rds、Cds 的的负载平衡和路由规定写入结点的运行信息中
  XdsServiceDiscovery --> XdsServiceDiscovery: 回传给服务自省框架,构建 invoker
end

deactivate ControlPlane
deactivate XdsServiceDiscovery
deactivate XdsServiceDiscoveryFactory

@enduml

从整体上看,istio control plane 和 dubbo 的交互时序图如上。Dubbo 里 xds 解决的次要逻辑在 PilotExchanger 和各个 DS(LDS、RDS、CDS、EDS) 的对应协定的具体实现里。PilotExchanger 对立负责串联逻辑,次要有三大逻辑:

  • 获取授信证书。
  • 调用不同 protocol 的 getResource 获取资源。
  • 调用不同 protocol 的 observeResource 办法监听资源变更。

例如对于 lds 和 rds,PilotExchanger 会调用 lds 的 getResource 办法与 istio 建设通信连贯,发送数据并解析来自 istio 的响应,解析实现后的 resource 资源会作为 rds 调用 getResource 办法的入参,并由 rds 发送数据给 istio。当 lds 产生变更时,则由 lds 的 observeResource 办法去触发本身与 rds 的变更。上述关系对于 rds 和 eds 同样如此。现有交互如下,上述过程对应图里红线的流程:

在第一次胜利获取资源之后,各个 DS 会通过定时工作去一直发送申请给 istio,并解析响应后果和放弃与 istio 之间的交互,进而实现管制面对流量管控、服务治理、可观测性方面的管控,其流程对应上图蓝线局部。

03 以后 Dubbo Proxyless 实现存在的有余

Dubbo Proxyless 模式通过验证之后,曾经证实了其可靠性。现有 dubbo proxyless 的实现计划存在以下问题:

  • 目前与 istio 交互的逻辑是推送模式。getResource 和 observeResource 是两条不同的 stream 流,每次发送新申请都须要从新建设连贯。但咱们建设的 stream 流是双向流动的,istio 在监听到资源变动后由被动推送即可,LDS、RDS、EDS 别离只须要保护一条 stream 流。
  • Stream 流模式改为建设长久化连贯之后,须要设计一个本地的缓存池,去存储曾经存在的资源。当 istio 被动推送更新后,须要去刷新缓存池的数据。
  • 现有 observeResource 逻辑是通过定时工作去轮询 istio。当初 observeResource 不再须要定时去轮询,只须要将须要监听的资源退出到缓存池,等 istio 主动推送即可,且 istio 推送回来的数据须要依照 app 切分好,实现多点监听,后续 dubbo 反对其余 DS 模式,也可复用相应的逻辑。
  • 目前由 istio 托管的 dubbo 利用在 istio 掉线后会抛出异样,断线后无奈从新连贯,只能重新部署利用,减少了运维和治理的复杂度。咱们需减少断线重连的性能,等 istio 恢复正常后无需重新部署即可重连。

革新实现后的交互逻辑:

04 Xds 监听模式实现计划

4.1 资源缓存池

目前 Dubbo 的资源类型有 LDS,RDS,EDS。对于同一个过程,三种资源监听的所有资源都与 istio 对该过程所缓存的资源监听列表一一对应。因而针对这三种资源,咱们应该设计别离对应的本地的资源缓存池,dubbo 尝试资源的时候先去缓存池查问,若有后果则间接返回;否则将本地缓存池的资源列表与想要发送的资源聚合后,发送给 istio 让其更新本身的监听列表。缓存池如下,其中 key 代表单个资源,T 为不同 DS 的返回后果:

protected Map<String, T> resourcesMap = new ConcurrentHashMap<>();

有了缓存池咱们必须有一个监听缓存池的构造或者容器,在这里咱们设计为 Map 的模式,如下:

protected Map<Set<String>, List<Consumer<Map<String, T>>>> consumerObserveMap = new ConcurrentHashMap<>();

其中 key 为想要监听的资源,value 为一个 List, 之所以设计为 List 是为了能够反对反复订阅。List 存储的 item 为 jdk8 中的 Consumer 类型,它能够用于传递一个函数或者行为,其入参为 Map,其 key 对应所要监听的单个资源,便于从缓存池中获取。如上文所述,PilotExchanger 负责串联整个流程,不同 DS 之间的更新关系能够用 Consumer 进行传递。以监听 LDS observeResource 为例,  大抵代码如下:

// 监听
void observeResource(Set<String> resourceNames, Consumer<Map<String, T>> consumer, boolean isReConnect);

// Observe LDS updated
ldsProtocol.observeResource(ldsResourcesName, (newListener) -> {
    // LDS 数据不统一
    if (!newListener.equals(listenerResult)) {
        // 更新 LDS 数据
        this.listenerResult = newListener;
        // 触发 RDS 监听
        if (isRdsObserve.get()) {createRouteObserve();
        }
    }
}, false);

Stream 流模式改为建设长久化连贯之后,咱们也须要把这个 Consumer 的行为存储在本地的缓存池中。Istio 收到来自 dubbo 的推送申请后,刷新本身缓存的资源列表并返回响应。此时 istio 返回的响应内容是聚合后的后果,Dubbo 收到响应后,将响应资源拆分为更小的资源粒度,再推送给对应的 Dubbo 利用告诉其进行变更。

踩坑点

  • istio 推送的数据可能为空字符串,此时缓存池子无需存储,间接跳过即可。否则 dubbo 会绕过缓冲池,一直向 istio 发送申请。
  • 思考以下场景,dubbo 利用同时订阅了两个接口,别离由 app1 和 app2 提供。为防止监听之间的互相笼罩,因而向 istio 发送数据时,须要聚合所有监听的资源名一次性发动。

4.2 多点独立监听

在第一次向 istio 发送申请时会调用 getResource 办法先去 cache 查问,缺失了再聚合数据去 istio 申请数据,istio 再返回相应的后果给 dubbo。咱们解决 istio 的响应有两种实现计划:

  1. 由用户在 getResource 计划中 new 一个 completeFuture,由 cache 剖析是否是须要的数据,若确认是新数据则由该 future 回调传递后果。
  2. getResource 建设资源的监听器 consumerObserveMap,定义一个 consumer 并把取到的数据同步到原来的线程,cache 收到来自 istio 的推送后会做两件事:将数据推送所有监听器和将数据发送给该资源的监听器。

以上两种计划都能实现,但最大的区别就是用户调用 onNext 发送数据给 istio 的时候需不需要感知 getResource 的存在。综上,最终抉择计划 2 进行实现。具体实现逻辑是让 dubbo 与 istio 建设连贯后,istio 会推送本身监听到资源列表给 dubbo,dubbo 解析响应,并依据监听的不同 app 切分数据,并刷新本地缓存池的数据,并发送 ACK 响应给 istio,大抵流程如下:

@startuml

object Car
object Bus
object Tire
object Engine
object Driver

Car <|- Bus
Car *-down- Tire
Car *-down- Engine
Bus o-down- Driver

@enduml

局部要害代码如下:

public class ResponseObserver implements XXX {
        ...
        public void onNext(DiscoveryResponse value) {
            // 承受来自 istio 的数据并切分
            Map<String, T> newResult = decodeDiscoveryResponse(value);
            // 本地缓存池数据
            Map<String, T> oldResource = resourcesMap;
            // 刷新缓存池数据
            discoveryResponseListener(oldResource, newResult);
            resourcesMap = newResult;
            // for ACK
            requestObserver.onNext(buildDiscoveryRequest(Collections.emptySet(), value));
        }
        ...
        public void discoveryResponseListener(Map<String, T> oldResult, 
                                              Map<String, T> newResult) {....}  
}
// 具体实现交由 LDS、RDS、EDS 本身
protected abstract Map<String, T> decodeDiscoveryResponse(DiscoveryResponse response){
  // 比对新数据和缓存池的资源,并将不同时存在于两种池子的资源取出
    ...
    for (Map.Entry<Set<String>, List<Consumer<Map<String, T>>>> entry : consumerObserveMap.entrySet()) {
    // 本地缓存池不存在则跳过
    ...
  // 聚合资源
    Map<String, T> dsResultMap = entry.getKey()
        .stream()
        .collect(Collectors.toMap(k -> k, v -> newResult.get(v)));
    // 刷新缓存池数据
    entry.getValue().forEach(o -> o.accept(dsResultMap));
    }
}

▧踩坑点

  • 本来多个 stream 流的状况下,会用递增的 requestId 来复用 stream 流,改成长久化连贯之后,一种 resource 会有多个 requestid,可能会互相笼罩,因而必须去掉这个机制。
  • 初始实现计划并没有对资源进行切分,而是一把梭,思考到后续对其余 DS 的反对,对 istio 返回的数据进行切分,也导致 consumerObserveMap 有点奇形怪状。
  • 三种 DS 在发送数据时能够共享同一 channel,但监听所用到的必须是同一 channel,否则数据变更时 istio 不会进行推送。
  • 建设双向 stream 流之后,初始计划 future 为全局共享。但可能有这样的场景:雷同的 ds 两次相邻工夫的 onNext 事件,记为 A 事件和 B 事件,可能是 A 事件先发送,B 随后;但可能是 B 事件的后果先返回,不确定 istio 推送的工夫,因而 future 必须是局部变量而不是全局共享。

4.3 采纳读写锁防止并发抵触

监听器 consumerObserveMap 和缓存池 resourcesMap 均可能产生并发抵触。对于 resourcemap,因为 put 操作都集中在 getResource 办法,因而能够采纳乐观锁就能锁住相应的资源,防止资源的并发监听。

对于 consumerObserveMap,同时存在 put、remove 和遍历操作,从时序上,采纳读写锁可躲避抵触,对于遍历操作加读锁,对于 put 和 remove 操作加写锁,即可防止并发抵触。综上,resourcesMap 加乐观锁即可,consumerObserveMap 波及的操作场景如下:

  • 近程申请 istio 时候会往 consumerObserveMap 新增数据,加写锁。
  • CompleteFuture 跨线程返回数据后,去掉监听 future,加写锁。
  • 监听缓存池时会往 consumerObserveMap 新增监听,加写锁。
  • 断线重连时会往 consumerObserveMap 新增监听,加写锁。
  • 解析 istio 返回的数据,遍历缓存池并刷新数据,加读锁。

▧踩坑点

  • 因为 dubbo 和 istio 建设的是是双向 stream 流,雷同的 ds 两次相邻工夫的 onNext 事件,记为 A 事件和 B 事件,可能是 A 事件先发送,B 随后;但可能是 B 事件的后果先返回,不确定 istio 推送的工夫。因而须要加锁。

4.4 断线重连

断线重连只须要用定时工作去定时与 istio 交互,尝试获取授信证书,证书获取胜利即可视为 istio 胜利从新上线,dubbo 会聚合本地的资源去 istio 申请数据,并解析响应和刷新本地缓存池数据,最初再敞开定时工作。

▧踩坑点

  • 采纳全局共享的定时工作池,不能进行敞开,否则会影响其余业务。

05 感想与总结

在这次性能的革新中,笔者着实掉了一波头发,怎么找 bug 也找不到的情景不在少数。除了上述提到的坑点之外,其余的坑点包含但不局限于:

  • dubbo 在某一次迭代里更改了获取 k8s 证书的形式,受权失败。
  • 本来的性能没问题,merge 了下 master 代码,grpc 版本与 envoy 版本不兼容,各种报错,最初靠升高版本胜利解决。
  • 本来的性能没问题,merge 了下 master 代码,最新分支代码里 metadataservice 发成了 triple,然而在 Proxyless 模式下只反对 dubbo 协定,debug 了三四天,最初发现须要减少配置。

……

但不得不抵赖,Proxyless Service Mesh 的确有它本身的劣势和广大的市场前景。自 dubbo3.1.0 release 版本之后,dubbo 曾经实现了 Proxyless Service Mesh 能力,将来 dubbo 社区将深度联动业务,解决更多理论生产环境中的痛点,更好地欠缺 service mesh 能力。

退出移动版