01 背景

随着 Docker 和 Kubernetes 的出现,一个庞大的单体使用能够被拆分红多个独立布置的微服务,并被打包运转于对应的容器中。不同使用之间彼此通讯,以共同完结某一功用模块。微服务架构与容器化布置带来的好处是显而易见的,它降低了服务间的耦合性,利于开发和保护,能更有效地使用核算资源。当然,微服务架构也存在相应的缺陷:

  • 强依赖于SDK,事务模块与管理模块耦合较为严峻。除了相关依赖,往往还需求在事务代码中嵌入SDK代码或配置。
  • 一致管理难。每次结构晋级都需求修正 SDK 版别,偏从头进行回归测验,确认功用正常后再对每一台机器从头布置上线。不同服务引证的 SDK 版别不一致、才能良莠不齐,增大了一致管理的难度。
  • 短少一套一致处理计划。目前市场不存在一整套功用完善、无死角的微服务管理与处理计划。在实践出产环境往往还需求引入多个管理组件来完结像灰度发布、故障注入等功用。

为处理这些痛点,Service Mesh诞生了。以经典的side car方式为例,它经过在事务 Pod 中注入 Sidecar 容器,对署理流量施行管理和管控,将结构的管理才能基层到 side car 容器中,与事务系统解耦,然后轻松完结多语言、多协议的一致流量管控、监控等需求。经过剥离 SDK 才能并拆解为独立进程,然后处理了强依赖于 SDK 的问题,然后使开发人员能够愈加专注于事务本身,完结了基础结构才能的下沉,如下图所示(源自dubbo官网):

Dubbo 在 Proxyless Mesh 模式下的探索与改进

经典的 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(证书)配置。

Dubbo 在 Proxyless Mesh 模式下的探索与改进

经过 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

Dubbo 在 Proxyless Mesh 模式下的探索与改进

从全体上看,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同样如此。现有交互如下,上述过程对应图里红线的流程:

Dubbo 在 Proxyless Mesh 模式下的探索与改进

在第一次成功获取资源之后,各个 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恢复正常后无需从头布置即可重连。

改造完结后的交互逻辑:

Dubbo 在 Proxyless Mesh 模式下的探索与改进

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

Dubbo 在 Proxyless Mesh 模式下的探索与改进

部分关键代码如下:

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才能。