服务治理在猫眼娱乐的演进之路(二)- Servicemesh

avatar
猫小娱 @猫眼娱乐

首先我们看一下,微服务架构的演进历程中我们更关注的是什么?一般微服务业务应用架构的演进历程无非都是单体应用 -> 服务化 -> 云原生 -> Serverless。这个过程中,大家可以看到,每一次的进化,大体上可以认为都是通过拆分和抽象,伴随牺牲一定的性能,来达到最终提升人效的目的。所以我们可以得出一个结论:
在演进式的架构中,我们越来越关注人效,而非绝对的性能。
那么问题来了,既然人效如此重要,那我们如何提升服务治理的人效呢?
服务治理的包含了非常多的能力,比如服务通讯、服务注册发现、负载均衡、路由、失败重试等等。在整个服务治理的历程中,我认为涌现出三种思潮。

第一类,我们认为是中心式的治理。利用集中式的集群来完成治理。比如HAProxy、Nginx、Tengine、Codis、Mycat都可以认为是这类治理手段,他的好处就是能够跨语言,问题就是会有性能损耗以及链路单点问题。那怎么解决这些问题呢?于是有了第二种思潮如下。

第二类,我们认为是融合式的分散治理。怎么理解这个概念呢?即采用和业务进程通过sdk方式彻底融合,来达到去中心化的分散治理的目的。这也是目前基本最主流的服务治理的样貌。比如Dubbo、Ring-pop、Thrift、Grpc、Motan的主流样貌。这样的好处即能达到性能的最优和最短链路,但是问题即与业务强耦合带来的跨语言成本和运维升级的高昂代价。到了这边,感觉有点无解,既然中心式部署不行,融合式的部署也不行,那怎么办呢?于是出现了第三种思潮,我们接着看。

第三类,我们称之为贴合式分散治理。怎么理解呢?贴合相比于前面谈到的融合的区别,就在于离业务很近,但是不要作为一个SDK直接融入业务进程,这样的话既能最大程度降低对性能和稳定性影响的同时,同时解决业务耦合的问题。这种架构理念其实很早就被提出了,在linkerd、airbnb的smartstack治理体系、携程OSP以及各种云原生服务治理方案k8s、marathon中都可见到踪影。我们统称之为Servicemesh — 服务网格。

当然,Servicemesh本身是有一些不成熟或者待商榷的地方,接下来介绍下,服务网格在猫眼的一个实际落地的理论架构。
首先,我们先明确下我们的终极目的:
希望能够在牺牲一小部分绝对性能的前提下实现我们人效的大幅提升。
  1. 我们都知道要提升人效,最好的方式就是一切都自动化,这样就无需人工介入了。
  2. 而要实现自动化的前提则是完成标准化,非标的产品要实现自动化的成本、代价是非常之高的,往往即便实现了也容易出现这样那样的逻辑和非逻辑的坑。
  3. 而要完成标准化我们需要做什么呢?就是需要进行职责分离,类似于前后端分离的理念,我们需要将业务和服务治理中间件能力分离,否则异构的业务必然会带来服务治理能力的非标准化。
所以进一步推演,我们可以发现Servicemesh其实就是处在我们的推演起点的核心解决方案。Servicemesh在我们看来,核心理念是两个,
  1. 其一,治理与应用分离,业务应用和治理能力的就近物理切割。通过部署本地代理的方式加上Iptables拦截的方式来完成这个切割。
  2. 其二,强调执行和控制的分离,也即他非常著名的控制平面和数据平面的切分。
然而,这样就足够了吗,理想照进现实的模样是什么样的呢?
Servicemesh在生产环境应用中面临的难点和挑战部分如下:
  1. 控制平面的边界在哪?业内最出名的Istio的Mixer的check方法会带来很严重的性能挑战,即便加了本地cache仍然有严重的性能问题,而且可能会带来代理资源开销的指数级提升。Report方法则会带来2倍的网络开销。这种非常暴力的一刀切的方式其实并不为业内所接纳,包括Istio本身也正在将Mixer能力整合到控制面板中。
  2. 新的单体应用困局如何破局?我们是否注意到一点,即我们把各种服务治理甚至其他中间件能力下沉,必然会带来在PaaS中间件领域新的单体应用问题。你的配置管理、限流、熔断、混沌工程、服务注册发现,以及各种存储、日志、监控报警的中间件能力都会集中到这个单体代理上,这根本是不可能Work的一个架构。针对这种问题,如何解决呢?
  3. 零侵入业务真的现实吗?Servicemesh也很强调对业务进程的零侵入,希望将服务治理能力看待为协议栈的一部分。而我们实际使用中,我们需要或者已经有现成的RPC书写和调用的方式,这种方式可以让我们的业务之间的调用更规范、易于上手、不易出错。彻底抹杀掉这部分现实吗?
  4. 其他的就是Servicemesh本质上是寄生在业务资源中的一个大规模部署的形态,如何保障交付质量、如何降低性能和资源开销,如何最大程度保障可用性,都是需要探索的问题。
基于这些难点与挑战,猫眼摸索出的最终落地理论架构是
有节制、可插拔、半贴合式的无中心治理。
  1. 有节制指代对于控制平面和控制平面的切割,不追求一刀切。
  2. 可插拔是为了应对新单体应用的问题,数据面板的各种能力应该是可以服务化,各种能力是可插拔的
  3. 半贴合式即我们为了保护业务实际使用中的体验,不追求对业务进程完全无侵入,而采取尽可能低侵入的方式进行。
以及为了确保这个治理的高质量交付,我们在可用性、性能、交付质量上都做了充分的工作。下面我们会具体展开介绍。
在具体介绍之前,大家可能都会有这样一个疑问,“你们为什么不采用开源方案呢?” ,国内开源最为活跃的即蚂蚁的Sofa-Mosn,国外开源最主流的即为Google牵头的Istio。我们主要对标的是这两款产品,没有直接使用主要出于这么几个原因。
  1. 我们于18年启动的时候,其实国内主流开源方案的Sofa-Mosn也处于一个快速迭代,不稳定的状态。
  2. 由于业务特性决定,我们不希望上来就和云原生绑定,而这个和业内的主流解决方案不太相符。
  3. Istio本身开启Mixer之后存在较为严重的性能问题。而我们本身也希望在性能优化上能够有更多探索的空间。我们的一些性能优化的措施如自研的IPC框架和Istio本身架构整合成本就很大。
  4. 猫眼存在较多已经存在的服务治理中间件,比如前面的高可用治理中心就是其中之一。他们的功能其实和开源的内容存在很大Gap,无法直接套用,而如果进行整合的话,成本非常之高。基本无法接受。
  5. 最后一个则是更加现实的问题,猫眼并没有专业的C/C++的团队,所以我们不存在基于Envoy去开发的基本条件。
基于以上原因考虑,我们最终没有直接采用开源的产品,而是导向自研。但我们在整体自研过程中也充分借鉴了一些开源的优秀设计实现。
基于以上的推演,猫眼的下一代微服务治理体系也就呼之欲出了,代号盘古,为猫眼的服务治理中心。他是猫眼下一代的服务连接、注册、发现、中间件管理的一站式解决方案。
这个是我们盘古服务治理中心的系统架构。可以看到从大面上,仍然是划分为上下两层,即控制平台和数据平面。有几个模块是被飘红的,
  1. Portal是我们的数据平面。对标Envoy、Mosn这些数据平面。
  2. Dolphin是我们轻量级的Mesh SDK,供业务方进行实际各种服务治理能力使用。
  3. Pilot是我们的配置型控制平面的对接适配层。对接了我们的注册中心、配置中心和各种元数据中心。
  4. 控制平面还包括了监控、链路追踪、流控、地址服务、一站式治理平台等等的服务。
这个是Servicemesh的数据平面架构。整体架构上我们抽象出Server、Transport、Stream、Router、Cluster、Resource几个核心层。
  1. Server层负责服务启动、可用性保障、指标收集以及XDS交互
  2. Transport层负责底层通讯链路。
  3. Stream层负责协议解析、会话绑定/销毁,以及对Transport的连接和读写能力的封装。
  4. Router就是负责将请求路由至对应的cluster上。
  5. Cluster负责进行负载均衡、目标机器筛选、失败重试、连接管理。
  6. Resource负责对Mesh的资源进行管理,包括各类协程池、对象/字节池,以及SPI框架。
整体上,他和Envoy以及Sofa-Mosn的整体架构是非常类似的。
接下来,我们看下猫眼的Servicemesh的一些建设思路。

首先,我们对控制平面基进行了有节制的切割和优化。

我们最先去做的架构选择,就是将遥测下沉,限流接入自建流控,取消Mxier,解决性能问题。其次,我们将服务注册与发现整合,提供完整解决方案。改变了Servicemesh只关注服务发现而不关注服务注册的问题。

我们认为控制平面可以分为两类,一类是配置型的控制平面,主要用以下发指令和配置。如注册中心、配置中心、各种中间件的元数据中心。一类是数据型的控制平面,会有大量的实时数据的存储或分析,比如分布式链路追踪、比如监控报警、比如日志。

  1. 配置型平面以MMCP(Maoyan-MCP)协议快速接入,协议提供Watch、UnWatch、Push、PassThrough四个通用能力接口,新的配置型平面只要按照这个协议来接入,就可以实现快速接入,整个过程Pilot零改造成本。
  2. 数据型控制平面和普通应用一致性对待,我们认为他们就是一个普通应用。也需要接入我们的Sidecar来做流量管控,所以我们这里也是对这类控制平面践行Pet&Cow理论,即你应该尽量少养宠物,多养奶牛,宠物生病需要治等等的特殊照顾,而奶牛生病直接杀了就行一视同仁。我们希望尽可能地对我们分布式拓扑下的节点一视同仁,这样能降低系统复杂度。
我们对于我们的Servicemesh也进行了较多的一个性能优化的尝试。
  1. 我们采用了基于Reactor+多级协程池的异步通讯模型
  2. 将我们的协程进行多级的池化,对于字节和对象资源进行池化。来降低调度和内存分配所带来的资源开销
  3. 我们采用Copy-On-Write的方式来对我们核心配置进行无锁化替换。也采用了CAS的方式来对一些核心请求状态进行原子修改。整个过程实现了无锁化的设计。
  4. 在协议层面,我们将Payload后置到协议尾,同时反序列化时将Header和Body的byte内容进行缓存。以此来达到加速请求传输的性能。
  5. 我们有很多地方都存在着心跳,比如从调用方Sidecar到服务提供方的Sidecar,当调用方依赖多个服务提供方,且服务提供方具有较多实例的时候,我们将不得不建立大量的心跳协程来检测健康状态,这很明显是不ok的。所以我们采用了时间轮的方式来进行心跳维持逻辑的优化。降低了资源开销。
  6. 我们同时也提供了类似于Java的SPI机制,对可以单例化的一些对象,比如各种Filter进行了单例化处理。
  7. 最后呢,我们的Servicemesh为了保障一些高流量应用以及后续可能会延伸到的基础设施层的服务的性能,所以也进行了高流量下IPC优化的探索,我们基于uds和mmap自研了一个RingBuffer,以mmap传递数据,以uds进行事件通知,进行了内存对齐、无锁化等等的优化。最后可以看到在高QPS下,其相比于tcp/uds,最大性能可提升30%。
我们进行了大量的性能测试,在压测环境下我们的RT小于0.1ms,在灰度场景下RT增加在0.5ms以内。稳定性达到了5个9,CPU消耗在1%以下,内存占用在30M左右。以上是采用TCP来进行本地通讯的结果数据,而如果采用我们自研的通讯框架,通讯RT可进一步最高提升30%。

从目前的情况来看,足以满足猫眼业务的要求。当然,在性能优化上,我们仍然会结合业务需要在合适的时候进行进一步的探索。

在可用性方面,猫眼Servicemesh面向猫眼业务,做了充分的保障。
为了方便起见,我们称服务调用方为C,服务调用方的Sidecar为CA,服务提供方为P,服务提供方的Sidecar为PA,我们来看下日常运维中可能碰到的一些主要场景:
  1. Mesh在前期灰度和迭代期间,避免不了会进行经常性的发布。这个时候需要保障业务方流量无损。我们在当前阶段的做法是,我们基于状态机的流转,针对PA重启的情况,会将链路从CA->PA切换为CA->P。针对CA重启的情况,会将链路由C->CA->PA->P直接切换为C->P。等重启之后状态变更回正常了,这个时候再进行回切。后续我们针对CA不可用的场景,也会进行句柄热迁移的能力实现。
  2. 第二个是业务应用发布需要能够平滑发布。我们采用的是通过对老注册中心的状态变更监听,来同步新注册中心对应的状态,这样就可以在不侵入老发布系统平滑发布全流程的时候完成应用的平滑发布。
  3. 第三个场景是Mesh宕机,首先我们会有对应的运维agent进行mesh的保活,以及我们也有流量防御的机制,主动/被动探测到mesh不可用后会做快速的链路切换。最坏情况下,我们的SDK会自动切换为直连情况,彻底绕过mesh。
  4. 在c->ca->pa->p以及和pilot,注册中心的交互链路中,任意一个节点出故障,我们都有对应的被动感知和主动探测的方式来发现并进行主动的failover。
  5. 下一个是实际推动业务试用的过程,必然需要考虑灰度的问题。我们能够进行服务、机器的多维度灰度,并可以在故障发生时一键回滚。
  6. 注册中心方面,注册中心可能会出现网络分区的情况,这个时候可能会导致注册中心误判服务提供方不可用而将其剔除,进而引发业务问题。我们采用了类似eureka会引入自我保护的机制,对于突发性的大批量节点下线,我们会不信任注册中心的结果,而主要依赖主动心跳健康检查的判断。我们没有采用Envoy的服务发现注册中心和健康检查共同决定的策略,是因为我们发现这样的case — 业务中有出现老注册中心显示机器已下线但是服务仍然短时间内可联通的情况。而这个时候如果仍然联通则是非常危险的。
  7. 注册中心如果不可用的情况下,会有Sidecar内存和文件的多级别缓存来保障可用性。
通过以上手段,我们的可用性一直维持在6个9左右。很好地为业务提供了各种保障。
在CI/CD环节,我们完成了整个流程的闭环建设。在Pipeline中,我们进行了自动化的性能测试和混沌测试。我们整个性能测试过程中,需要模拟5*5*5一共125种的的场景、调度流量、探测容量上限、对过程中的各种指标进行采集,以及产出结果报告,正常做一轮下来,需要耗费大量的时间和精力,所以我们针对这种情况进行了自动化性能测试体系的搭建,将上述环节都进行了自动化处理,并集成进了CI流中,让我们的每一次发版都能够对于代码Diff带来的性能变化有更直观的了解。

另外,我们上线后的生产环境会有随机的各种场景出现,这些场景都可能会引发系统问题,所以我们也在CI环节中引入了自动化的混沌测试。针对我们模拟的复杂拓扑去触发随机概率事件,包括请求响应的包体大小和流量规模也会随机产生,同时会去模拟服务伸缩、服务重启、机器宕机等等的case。通过这种混沌式的沙盒测试,对系统进行更进一步的可用性和性能的探测。

最后,针对于前面提及的中间件能力的下沉所带来的新的单体应用的困局,我们采用的方式就是进行数据平面的“服务化”。

我们从上到下进行了三层切割

  1. 离业务最近的一层是中间件门面层,包括KV、RPC、Trace、Redis等等的中间件底层都基于和Mesh交互的统一SDK,上层由猫眼的脚手架工程统一封装。
  2. 第二层即有节制拆分的中间件服务化层,拆分出RPC、监控、存储等四个Mesh。这时候,可能就会产生一个问题,即我RPC mesh里面也需要监控能力,监控Mesh里面也需要RPC能力,这本身是一个相互依赖的关系,如何能够以更优雅可控的方式来进行拆解?我们的解决方案也就是构建服务化体系第三层 — 中间件模块化层。
  3. 第三层也是中间件Mesh服务化的基石。我们将最为通用的能力进行下沉,收敛出Mesh Stone这样的底层核心上,在这之上,提供了RPC、Log、Trace等等很多可插拔的模块,可以通过在Mesh初始化的时候自由组合拼装任意模块来快速完成一个Mesh的底座封装,并在这之上去实现自己独有的业务逻辑。如此一来,我们就可以在相互不影响的情况下来实现最大程度的复用。
所以可以看到,我们提炼的关键词即第一个,进行三层切割,分为门面、服务化、模块化三层,其次进行头尾合并,最底层有统一的模块化底座Mesh Stone来提供最通用的能力和整体框架。最上层有统一的门面封装来为业务方提供一致性的使用体验。最后,为了能够支撑起中间件服务化层的相互独立以及最大化复用,我们允许这种可插拔的模块之间的自由拼装组合。

通过这三点,我们可以实现数据平面的服务化。但我们需要特别警惕Mesh过渡拆分导致的Agent泛滥引发的运维问题,这块需要跟随着后期中间件的实际落地去把握里面的度,猫眼也是在一个探索的道路上。

前面提到了猫眼当前在提升系统稳定性和提升人效方面所做的一些探索。高可用治理中心目前已经在猫眼大规模铺开落地实践了,而猫眼基于Servicemesh的服务治理中心目前也已经在新业务中进行落地实践,整体上处于在生产环境中持续探索验证的阶段。未来,猫眼的服务治理主要会朝三个方向去探索与演进。
  1. 第一个就是希望将我们的治理能力AIOps化。比如基于更全面的健康度量体系来进行一些智能决策,比如治理策略无参化、智能报警、容量自动评估水位预警、故障诊断、异常探测、动态伸缩等等方面。
  2. 第二个就是希望借由Servicemesh数据面板的服务化能力,将中间件进行网格化。以此将网格的红利从RPC延伸到我们越来越多的PaaS设施中。
  3. 第三个则是希望建立在云原生的基础上进行Serverless的探索,目前的Serverless其实对于简单逻辑的应用比较友好,但是对于像基于Java/Golang之类的重度业务逻辑的应用来说,较难落地。所以我们也希望在未来探索Serverless如何真正意义上能够解放业务方的人力。
未来猫眼的服务治理会通过这三个方向的延伸,聚焦在解决人效、稳定性以及资源利用率的提升上。