该文章内容发布已经超过一年,请注意检查文章中内容是否过时。
随着 Docker 和 Kubernetes 的出现,一个庞大的单体应用可以被拆分成多个独立部署的微服务,并被打包运行于对应的容器中。不同应用之间相互通信,以共同完成某一功能模块。微服务架构与容器化部署带来的好处是显而易见的,它降低了服务间的耦合性,利于开发和维护,能更有效地利用计算资源。当然,微服务架构也存在相应的缺点:
为解决这些痛点,Service Mesh诞生了。以经典的 Sidecar 模式为例,它通过在业务 Pod 中注入 Sidecar 容器,对代理流量实施治理和管控,将框架的治理能力下层到 Sidecar 容器中,与业务系统解耦,从而轻松实现多语言、多协议的统一流量管控、监控等需求。通过剥离SDK能力并拆解为独立进程,从而解决了强依赖于SDK的问题,从而使开发人员可以更加专注于业务本身,实现了基础框架能力的下沉,如下图所示(源自dubbo官网):
经典的 Sidecar Mesh 部署架构有很多优势,如减少SDK耦合、业务侵入小等,但增加了一层代理,也带来了一些额外的问题,比如:
为解决这些痛点,Proxyless Service Mesh 模式诞生了。传统服务网格通过代理的方式拦截所有的业务网络流量,代理需要感知到控制平面下发的配置资源,从而按照要求控制网络流量的走向。以istio为例,Proxyless 模式是指应用直接与负责控制平面的 istiod 进程通信,istiod 进程通过监听并获取 Kubernetes 的资源,例如 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 模式带来的性能损耗与部署架构复杂性。
从整体上看,istio control plane 和 dubbo 的交互时序图如上。Dubbo 里 xDS 处理的主要逻辑在 PilotExchanger 和各个 DS (LDS、RDS、CDS、EDS) 的对应协议的具体实现里。PilotExchanger 统一负责串联逻辑,主要有三大逻辑:
例如对于 lds 和 rds,PilotExchanger 会调用 lds 的 getResource 方法与 istio 建立通信连接,发送数据并解析来自 istio 的响应,解析完成后的 resource 资源会作 为rds 调用 getResource 方法的入参,并由 rds 发送数据给 istio。当 lds 发生变更时,则由 lds 的 observeResource 方法去触发自身与 rds 的变更。上述关系对于 rds 和 eds 同样如此。现有交互如下,上述过程对应图里红线的流程:
在第一次成功获取资源之后,各个DS会通过定时任务去不断发送请求给 istio,并解析响应结果和保持与 istio 之间的交互,进而实现控制面对流量管控、服务治理、可观测性方面的管控,其流程对应上图蓝线部分。
Dubbo Proxyless 模式经过验证之后,已经证明了其可靠性。现有 Dubbo proxyless 的实现方案存在以下问题:
改造完成后的交互逻辑:
目前 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<String, T>,其 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发送请求时会调用getResource方法先去cache查询,缺失了再聚合数据去istio请求数据,istio再返回相应的结果给dubbo。我们处理istio的响应有两种实现方案:
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));
}
}
踩坑点:
监听器consumerObserveMap和缓存池resourcesMap均可能产生并发冲突。对于resourcemap,由于put操作都集中在getResource方法,因此可以采用悲观锁就能锁住相应的资源,避免资源的并发监听。对于consumerObserveMap,同时存在put、remove和遍历操作,从时序上,采用读写锁可规避冲突,对于遍历操作加读锁,对于put和remove操作加写锁,即可避免并发冲突。综上,resourcesMap加悲观锁即可,consumerObserveMap涉及的操作场景如下:
踩坑点:
断线重连只需要用定时任务去定时与istio交互,尝试获取授信证书,证书获取成功即可视为istio成功重新上线,dubbo会聚合本地的资源去istio请求数据,并解析响应和刷新本地缓存池数据,最后再关闭定时任务。 踩坑点:
在这次功能的改造中,笔者着实掉了一波头发,怎么找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能力。