关于阿里云: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能力。

【腾讯云】轻量 2核2G4M,首年65元

阿里云限时活动-云数据库 RDS MySQL  1核2G配置 1.88/月 速抢

本文由乐趣区整理发布,转载请注明出处,谢谢。

您可能还喜欢...

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据