在演进式的架构中,我们越来越关注人效,而非绝对的性能。
第一类,我们认为是中心式的治理。利用集中式的集群来完成治理。比如HAProxy、Nginx、Tengine、Codis、Mycat都可以认为是这类治理手段,他的好处就是能够跨语言,问题就是会有性能损耗以及链路单点问题。那怎么解决这些问题呢?于是有了第二种思潮如下。
第三类,我们称之为贴合式分散治理。怎么理解呢?贴合相比于前面谈到的融合的区别,就在于离业务很近,但是不要作为一个SDK直接融入业务进程,这样的话既能最大程度降低对性能和稳定性影响的同时,同时解决业务耦合的问题。这种架构理念其实很早就被提出了,在linkerd、airbnb的smartstack治理体系、携程OSP以及各种云原生服务治理方案k8s、marathon中都可见到踪影。我们统称之为Servicemesh — 服务网格。
希望能够在牺牲一小部分绝对性能的前提下实现我们人效的大幅提升。
- 我们都知道要提升人效,最好的方式就是一切都自动化,这样就无需人工介入了。
- 而要实现自动化的前提则是完成标准化,非标的产品要实现自动化的成本、代价是非常之高的,往往即便实现了也容易出现这样那样的逻辑和非逻辑的坑。
- 而要完成标准化我们需要做什么呢?就是需要进行职责分离,类似于前后端分离的理念,我们需要将业务和服务治理中间件能力分离,否则异构的业务必然会带来服务治理能力的非标准化。
- 其一,治理与应用分离,业务应用和治理能力的就近物理切割。通过部署本地代理的方式加上Iptables拦截的方式来完成这个切割。
- 其二,强调执行和控制的分离,也即他非常著名的控制平面和数据平面的切分。
- 控制平面的边界在哪?业内最出名的Istio的Mixer的check方法会带来很严重的性能挑战,即便加了本地cache仍然有严重的性能问题,而且可能会带来代理资源开销的指数级提升。Report方法则会带来2倍的网络开销。这种非常暴力的一刀切的方式其实并不为业内所接纳,包括Istio本身也正在将Mixer能力整合到控制面板中。
- 新的单体应用困局如何破局?我们是否注意到一点,即我们把各种服务治理甚至其他中间件能力下沉,必然会带来在PaaS中间件领域新的单体应用问题。你的配置管理、限流、熔断、混沌工程、服务注册发现,以及各种存储、日志、监控报警的中间件能力都会集中到这个单体代理上,这根本是不可能Work的一个架构。针对这种问题,如何解决呢?
- 零侵入业务真的现实吗?Servicemesh也很强调对业务进程的零侵入,希望将服务治理能力看待为协议栈的一部分。而我们实际使用中,我们需要或者已经有现成的RPC书写和调用的方式,这种方式可以让我们的业务之间的调用更规范、易于上手、不易出错。彻底抹杀掉这部分现实吗?
- 其他的就是Servicemesh本质上是寄生在业务资源中的一个大规模部署的形态,如何保障交付质量、如何降低性能和资源开销,如何最大程度保障可用性,都是需要探索的问题。
有节制、可插拔、半贴合式的无中心治理。
- 有节制指代对于控制平面和控制平面的切割,不追求一刀切。
- 可插拔是为了应对新单体应用的问题,数据面板的各种能力应该是可以服务化,各种能力是可插拔的
- 半贴合式即我们为了保护业务实际使用中的体验,不追求对业务进程完全无侵入,而采取尽可能低侵入的方式进行。
- 我们于18年启动的时候,其实国内主流开源方案的Sofa-Mosn也处于一个快速迭代,不稳定的状态。
- 由于业务特性决定,我们不希望上来就和云原生绑定,而这个和业内的主流解决方案不太相符。
- Istio本身开启Mixer之后存在较为严重的性能问题。而我们本身也希望在性能优化上能够有更多探索的空间。我们的一些性能优化的措施如自研的IPC框架和Istio本身架构整合成本就很大。
- 猫眼存在较多已经存在的服务治理中间件,比如前面的高可用治理中心就是其中之一。他们的功能其实和开源的内容存在很大Gap,无法直接套用,而如果进行整合的话,成本非常之高。基本无法接受。
- 最后一个则是更加现实的问题,猫眼并没有专业的C/C++的团队,所以我们不存在基于Envoy去开发的基本条件。
- Portal是我们的数据平面。对标Envoy、Mosn这些数据平面。
- Dolphin是我们轻量级的Mesh SDK,供业务方进行实际各种服务治理能力使用。
- Pilot是我们的配置型控制平面的对接适配层。对接了我们的注册中心、配置中心和各种元数据中心。
- 控制平面还包括了监控、链路追踪、流控、地址服务、一站式治理平台等等的服务。
- Server层负责服务启动、可用性保障、指标收集以及XDS交互
- Transport层负责底层通讯链路。
- Stream层负责协议解析、会话绑定/销毁,以及对Transport的连接和读写能力的封装。
- Router就是负责将请求路由至对应的cluster上。
- Cluster负责进行负载均衡、目标机器筛选、失败重试、连接管理。
- Resource负责对Mesh的资源进行管理,包括各类协程池、对象/字节池,以及SPI框架。
首先,我们对控制平面基进行了有节制的切割和优化。
我们认为控制平面可以分为两类,一类是配置型的控制平面,主要用以下发指令和配置。如注册中心、配置中心、各种中间件的元数据中心。一类是数据型的控制平面,会有大量的实时数据的存储或分析,比如分布式链路追踪、比如监控报警、比如日志。
- 配置型平面以MMCP(Maoyan-MCP)协议快速接入,协议提供Watch、UnWatch、Push、PassThrough四个通用能力接口,新的配置型平面只要按照这个协议来接入,就可以实现快速接入,整个过程Pilot零改造成本。
- 数据型控制平面和普通应用一致性对待,我们认为他们就是一个普通应用。也需要接入我们的Sidecar来做流量管控,所以我们这里也是对这类控制平面践行Pet&Cow理论,即你应该尽量少养宠物,多养奶牛,宠物生病需要治等等的特殊照顾,而奶牛生病直接杀了就行一视同仁。我们希望尽可能地对我们分布式拓扑下的节点一视同仁,这样能降低系统复杂度。
- 我们采用了基于Reactor+多级协程池的异步通讯模型
- 将我们的协程进行多级的池化,对于字节和对象资源进行池化。来降低调度和内存分配所带来的资源开销
- 我们采用Copy-On-Write的方式来对我们核心配置进行无锁化替换。也采用了CAS的方式来对一些核心请求状态进行原子修改。整个过程实现了无锁化的设计。
- 在协议层面,我们将Payload后置到协议尾,同时反序列化时将Header和Body的byte内容进行缓存。以此来达到加速请求传输的性能。
- 我们有很多地方都存在着心跳,比如从调用方Sidecar到服务提供方的Sidecar,当调用方依赖多个服务提供方,且服务提供方具有较多实例的时候,我们将不得不建立大量的心跳协程来检测健康状态,这很明显是不ok的。所以我们采用了时间轮的方式来进行心跳维持逻辑的优化。降低了资源开销。
- 我们同时也提供了类似于Java的SPI机制,对可以单例化的一些对象,比如各种Filter进行了单例化处理。
- 最后呢,我们的Servicemesh为了保障一些高流量应用以及后续可能会延伸到的基础设施层的服务的性能,所以也进行了高流量下IPC优化的探索,我们基于uds和mmap自研了一个RingBuffer,以mmap传递数据,以uds进行事件通知,进行了内存对齐、无锁化等等的优化。最后可以看到在高QPS下,其相比于tcp/uds,最大性能可提升30%。
从目前的情况来看,足以满足猫眼业务的要求。当然,在性能优化上,我们仍然会结合业务需要在合适的时候进行进一步的探索。
- Mesh在前期灰度和迭代期间,避免不了会进行经常性的发布。这个时候需要保障业务方流量无损。我们在当前阶段的做法是,我们基于状态机的流转,针对PA重启的情况,会将链路从CA->PA切换为CA->P。针对CA重启的情况,会将链路由C->CA->PA->P直接切换为C->P。等重启之后状态变更回正常了,这个时候再进行回切。后续我们针对CA不可用的场景,也会进行句柄热迁移的能力实现。
- 第二个是业务应用发布需要能够平滑发布。我们采用的是通过对老注册中心的状态变更监听,来同步新注册中心对应的状态,这样就可以在不侵入老发布系统平滑发布全流程的时候完成应用的平滑发布。
- 第三个场景是Mesh宕机,首先我们会有对应的运维agent进行mesh的保活,以及我们也有流量防御的机制,主动/被动探测到mesh不可用后会做快速的链路切换。最坏情况下,我们的SDK会自动切换为直连情况,彻底绕过mesh。
- 在c->ca->pa->p以及和pilot,注册中心的交互链路中,任意一个节点出故障,我们都有对应的被动感知和主动探测的方式来发现并进行主动的failover。
- 下一个是实际推动业务试用的过程,必然需要考虑灰度的问题。我们能够进行服务、机器的多维度灰度,并可以在故障发生时一键回滚。
- 注册中心方面,注册中心可能会出现网络分区的情况,这个时候可能会导致注册中心误判服务提供方不可用而将其剔除,进而引发业务问题。我们采用了类似eureka会引入自我保护的机制,对于突发性的大批量节点下线,我们会不信任注册中心的结果,而主要依赖主动心跳健康检查的判断。我们没有采用Envoy的服务发现注册中心和健康检查共同决定的策略,是因为我们发现这样的case — 业务中有出现老注册中心显示机器已下线但是服务仍然短时间内可联通的情况。而这个时候如果仍然联通则是非常危险的。
- 注册中心如果不可用的情况下,会有Sidecar内存和文件的多级别缓存来保障可用性。
另外,我们上线后的生产环境会有随机的各种场景出现,这些场景都可能会引发系统问题,所以我们也在CI环节中引入了自动化的混沌测试。针对我们模拟的复杂拓扑去触发随机概率事件,包括请求响应的包体大小和流量规模也会随机产生,同时会去模拟服务伸缩、服务重启、机器宕机等等的case。通过这种混沌式的沙盒测试,对系统进行更进一步的可用性和性能的探测。
我们从上到下进行了三层切割。
- 离业务最近的一层是中间件门面层,包括KV、RPC、Trace、Redis等等的中间件底层都基于和Mesh交互的统一SDK,上层由猫眼的脚手架工程统一封装。
- 第二层即有节制拆分的中间件服务化层,拆分出RPC、监控、存储等四个Mesh。这时候,可能就会产生一个问题,即我RPC mesh里面也需要监控能力,监控Mesh里面也需要RPC能力,这本身是一个相互依赖的关系,如何能够以更优雅可控的方式来进行拆解?我们的解决方案也就是构建服务化体系第三层 — 中间件模块化层。
- 第三层也是中间件Mesh服务化的基石。我们将最为通用的能力进行下沉,收敛出Mesh Stone这样的底层核心上,在这之上,提供了RPC、Log、Trace等等很多可插拔的模块,可以通过在Mesh初始化的时候自由组合拼装任意模块来快速完成一个Mesh的底座封装,并在这之上去实现自己独有的业务逻辑。如此一来,我们就可以在相互不影响的情况下来实现最大程度的复用。
通过这三点,我们可以实现数据平面的服务化。但我们需要特别警惕Mesh过渡拆分导致的Agent泛滥引发的运维问题,这块需要跟随着后期中间件的实际落地去把握里面的度,猫眼也是在一个探索的道路上。
- 第一个就是希望将我们的治理能力AIOps化。比如基于更全面的健康度量体系来进行一些智能决策,比如治理策略无参化、智能报警、容量自动评估水位预警、故障诊断、异常探测、动态伸缩等等方面。
- 第二个就是希望借由Servicemesh数据面板的服务化能力,将中间件进行网格化。以此将网格的红利从RPC延伸到我们越来越多的PaaS设施中。
- 第三个则是希望建立在云原生的基础上进行Serverless的探索,目前的Serverless其实对于简单逻辑的应用比较友好,但是对于像基于Java/Golang之类的重度业务逻辑的应用来说,较难落地。所以我们也希望在未来探索Serverless如何真正意义上能够解放业务方的人力。