​1. 背景

随着互联网业务不断发展, 业务服务以及服务实例呈快速增长的趋势,然而传统微服务架构虽然能在一些场景满足服务高性能, 高可用, 可治理等需求,但同时也面临着耦合性高,灵活性差,管理复杂,可运维性低,缺乏多语言支持等问题。而如今云原生场景下,Service Mesh则越来越成为热议的话题。

ESA Mesh是OPPO互联网自研的Service Mesh组件, 隶属于ESA Stack微服务体系的一部分。ESA Mesh致力于提供云原生场景适合公司的Mesh方案,解决公司跨语言调用难,多语言服务治理生态匮乏,服务治理不统一等诸多问题。提供云原生场景下弹性易用的微服务架构基础组件,层层突破微服务落地难,Service Mesh落地难上难的困境。

2. Service Mesh的前世今生

随着近几年来云原生生态的不断壮大,CNCF基金会中的会员以及容纳的项目越来越多,CNCF为云原生进行了定位。

以下是CNCF对云原生的重新定义(中英对照):

Cloud native technologies empower organizations to build and run scalable applications in modern,dynamic environments such as public, private, and hybrid clouds. Containers, service meshes,microservices, immutable infrastructure, and declarative APIs exemplify this approach.

云原生技术有利于各组织在公有云、私有云和混合云等新型动态环境中,构建和运行可弹性扩展的应用。云原生的代表技术包括容器、服务网格、微服务、不可变基础设施和声明式API。

可见Service Mesh(服务网格)在CNCF的定义中已然成为云原生时代不可或缺的一部分, 并且同时与容器,微服务有着密不可分的关系。

最初的网络计算机交互

最初人们想要让不同计算机之间进行通讯时, 最简单的模型是这样的。

虽然要真正完成计算机之间的交互需要非常多的网络细节, 但是上图依然是用户最原始的需求:一个计算机上的服务调用另一个计算机上的服务。

但是实际上的交互需要更多的网络细节上

上图中网络通讯的细节是通过Networking Stack实现, 但是早年间这层网络细节仍然是需要人们人为的去管理网络连接等细节,直到计算机开始变得不是那么的昂贵,开始逐渐普及,计算机与计算机之间的连接需求开始了爆发式的增长,如何让计算机能发现其他的计算机, 如何有效控制计算机之间的流量, 特别是如何进行流量控制等成了普遍性的问题。

于是为了满足流量控制的功能需求, 人们在自己的应用中开发了流量控制的功能, 但是此功能逻辑的代码与业务逻辑交织于一处。

直到TCP/IP的出现以及兴起让网络细节问题以及流量控制等功能都得到了统一且标准化的解决, 同时成为计算机系统的一部分供用户透明的使用。

直至今天互联网大多都依赖着TCP/IP提供的基础能力完成着上层复杂的功能。

Microservices时代

微服务的出现可以说掀起了互联网服务实现与组织方式新的浪潮。同时也带来了一些新的技术以及功能上的挑战。

微服务强调着服务的细化(服务划分或者说拆分)以及架构的轻量化,同时出现了一些新的需求:服务发现, 熔断, 负载均衡等等。

初期面对这样的需求聪明的程序员总是能很快的在业务中便实现相应的功能, 但是遭遇了与最初网络计算机交互时代时同样的问,这些功能与业务逻辑混杂在一起, 难以管理与复用。

于是一些先驱者便将这些功能的实现打包成Library(或者说SDK)并公开给世界各地的程序员使用, 避免了重复造轮子,同时也让很多没有那么多精力去研究此类技术的公司或者个人能快速的享受到前人的智慧结晶。

此间变出现了Spring Cloud, Dubbo等优秀的微服务框架, Spring生态更甚至可以说当今Java生态中的“杀手锏”,这些优秀的框架或是组件很大程度上推进了微服务的发展和标准化。

Microservices is a silver bullet?

这个问题似乎已经有了比较明确的答案。

微服务普遍存在着落地困难的问题多语言支持困难

  • Library与业务耦合
  • Library升级地狱
  • 陡峭的学习曲线
  • 指数级增加的系统复杂度
  • ...

与上面的Networking Stack一样, 人们似乎迫切的想要屏蔽掉一些通用基础组件。

可是如今TCP/IP网络栈已经足够的稳定,似乎不允许人们直接将微服务能力下沉至此, 于是便有了Sidecar的概念。Sidecar就是与应用程序一起运行的独立进程,为应用程序提供额外的功能。

Service Mesh

每个服务都会有一个Sidecar与之配对。于是在错综复杂的服务部署结构下便会形成下图。

所有的服务通讯都经由Sidecar代理, 形成网状, 因此称之为:服务网格。

Service Mesh的概念最初由Buoyant 的 CEO William Morgan在博客上的一篇文章What's a service mesh? And why do I need one?中提出。

其定义

A service mesh is a dedicated infrastructure layer for handling service-to-service communication. It’s responsible for the reliable delivery of requests through the complex topology of services that comprise a modern, cloud native application. In practice, the service mesh is typically implemented as an array of lightweight network proxies that are deployed alongside application code, without the application needing to be aware. (But there are variations to this idea, as we’ll see.)

服务网格是一个基础设施层,用于处理服务间通信。云原生应用有着复杂的服务拓扑,服务网格保证请求可以在这些拓扑中可靠地穿梭。在实际应用当中,服务网格通常是由一系列轻量级的网络代理组成的,它们与应用程序部署在一起,但应用程序不需要知道它们的存在。

这个定义最强有力的部分在于,它不再把代理看成单独的组件,并强调了这些代理所形成的网络的重要性。

Cloud Native时代服务便如下图所示

一个Cloud Native App部署时都将自动部署一个Sidecar与之对应, 服务期间所有的服务通讯都经由Sidecar代理,同时Sidecar配置Control Plane(控制面板)完成诸如服务发现, 熔断, 负载均衡, 限流, 链路追踪等功能。而相对的业务服务仅仅需要关注自己的业务逻辑和一个仅仅用于通讯的轻量级RPC即可。业务无需关注Service Mesh层面的逻辑,甚至无法感知到它们的存在, 仅仅只需要像是我们最初的网络计算机交互时的模型一样, 当作仅仅是服务调用了另外一个服务即可。

行业先驱

业内已有许多优秀的Service Mesh开源组件

  • Linkerd(by: Buoyant)
  • Istio(by: Google, IBM)
  • Envoy(by: Lyft)
  • ServiceComb(by: 华为)
  • SOFA Mesh(by: 蚂蚁)
  • Nginmesh(by: Nginx)
  • TSF(by: Tencent)

3. ESA Mesh探索与实践

随着公司上“云”步伐的层层迈进, 我们已然具备了云原生时代雄厚的基础实力, 在此之上ESA Mesh致力于提供云原生场景适合公司的弹性, 易用, 可靠,可观察的Mesh方案。

ESA Mesh设计目标

  • 跨语言支持
  • 高性能,低延迟
  • 业务无感知
  • 服务发现
  • 负载均衡
  • 路由
  • 熔断/限流/隔离
  • 多协议支持
  • 故障注入
  • 链路追踪
  • ..

我们并不是开发一套新的微服务生态, 而是用新的方式服务业务。

流量拦截

Service Mesh架构中首当其冲的问题便是如何拦截业务流量, 并引流到Sidecar的问题。

iptables

Istio中的流量拦截方式即采用的iptables实现,通过一系列的iptables 规则将业务Pod中的Inbound流量以及Outbound流量均Redirect到Sidecar中随后由Sidecar处理。

此方式好处在于

  • 用户无感知

    用户无需感知Sidecar的存在, 像平常一样进行RPC调用即可。

  • 随意接管任意流量

    由于iptables的规则非常的灵活, 所有Netfilter之后流量均可通过不同的规则实现流量接管。

  • 老应用无缝迁移

    甚至可以在老应用完全不用变更的情况下, 接管所有的服务注册/发现,服务调用的流量。

可同时iptables也存在着诸多问题

  • 性能不理想

    iptables的性能总是令人诟病的地方, 最初其存在的目标是用于网络防火墙使用, 并且多年来Linux中的iptables并无太大改变(虽然随后推出了nftables), 但是随着iptables规则的增加,遍历带来的消耗剧增, 性能以及网络延迟严重下降。

  • 无法增量更新

    每次添加新规则时,必须更新整个规则列表。装配2万个Kubernetes服务产生16万条的iptables规则需要耗时5个小时

  • 应用流量复杂,复杂度高

    Istio中是拦截所有的业务Pod流量, 而实际业务中除了RPC调用之外往往还存在着很多别的流量,错综复杂的流量对于Sidecar处理来说相对比较困难。

  • 暂时无法使用eBPF

    现阶段Linux内核版本大多为3.x, 不太建议采用eBPF拦截方案(理论可行, 但通常需要4.x, 甚至4.8+使用XDP)。

理想中的流量拦截方式 - eBPF

eBFP为比较理想的流量拦截方式, 具有性能高,灵活性强, 功能丰富等诸多特点。

BPF

在eBPF之前不得不聊聊BPF, BPF全称Berkeley Packet Filter, 顾名思义这是一个用于过滤(filter)网络报文(packet)的架构。

BPF的架构非常的简洁, 途经网卡驱动层的报文在上报给协议栈的同时会多出一路来传送给 BPF,再经后者过滤后最终拷贝给用户态的应用。所有的过滤操作都在内核空间完成。单这么看可能会有些许陌生, 但是如果提到大名鼎鼎的tcpdump以及wireshark想必便了然于心了, BPF即为tcpdump以及wireshark的基础, 乃至许多网络监控领域的基石。最初在Linux中为LSF(Linux Socket Filter),其实现几乎与BPF无异。后更名为cBPF(classical BPF)。

eBPF

Linux 3.15版本伊始,eBPF便进入人们的视野, 并在随后v3.17被添加到kernel/bpf 下(获得一等公民待遇),并以eBPF命名(extended BPF), 同时先前的LSF更名为cBPF(classical BPF)。

相比于cBPF而言eBPF此次升级属革命性改变

  • 全新的开发接口
  • 基于 map 的内核与用户空间的交互方式
  • 丰富了指令集
  • In-kernel verifier
  • C语言编写程序

早期的cBPF所覆盖的功能范围很简单而 eBPF 的利用范围则要广的多

  • XDP(eXpress Data Path)
  • 流量控制
  • 网络包跟踪
  • 防火墙
  • 应用性能调优/监控
  • cgroups

eBPF相较于cBFP带来了大幅度的性能提升,同时在内核追踪(Kernel Tracing)、应用性能调优/监控、流控(Traffic Control)等领域也带来更多更丰富的特性和可能性。

现实中的流量拦截 - Mesh SDK

ESA Mesh初期并未采用流量拦截的方式(虽然很想)来导入流量到Sidecar, 而是采用了轻量级Mesh SDK的方式直接从RPC客户端定向打到Sidecar。

上图可以看到实际通讯时,采用了Unix Domain Socket的方式进行业务与Sidecar的通讯以求获取更高的性能, 因为Sidecar始终会与业务Pod在同一个Node节点(物理机), 因此没必要通过端口地址的方式, 直接进程间通讯即可。

此种方式的好处

  • 流量已知,可控

    Sidecar所有接收到的流量都是自己期望的流量, 不会受到干扰。

  • 服务治理参数传递方便

    通常Sidecar进行服务治理时或多或少都需要一些特定的参数(比如AppId), 而使用SDK便可随意传递想要的参数。

  • 可规避协议探测逻辑

    可以将不同的协议分别在不同的端口上启动, 避免笼统的绑定一个地址接受所有流量时频繁的协议探测(有的协议理论上是无法探测的, 比如Http协议)。

但是同样也存在着问题

  • 多语言问题

    又回到了多语言需要提供SDK的问题。

  • 业务SDK侵入

    不可避免的造成了一定程度的SDK侵入

出于前期简单化考虑, 我们还是选择Mesh SDK的方式与Sidecar进行通讯。

如何选择Sidecar部署架构方案

业内通常存在着两种部署方案, 一种是Sidecar与业务在同一个Pod,分属不同的Container(称之为Sidecar注入模式), Sidecar注入模式也是Istio采用的部署方案。而另一种模式则是将Sidecar独立使用DaemonSet部署, 让每个Node节点都启动一个Sidecar实例为当前节点的业务Pod服务

Sidecar注入

Sidecar注入的方式可以说是Service Mesh中Sidecar部署模式的最终形态

它具有以下特点

  • 隔离性强

    所有诸如配置, 限流,连接等资源都是业务独享, 不会和其他业务互相影响。

  • 扩展性强

    Sidecar随着业务Pod发布自动注入, 业务扩缩容均不影响Sidecar提供服务。

  • 可用性高

    一个Sidecar仅服务于单个业务Pod, 即使一个Sidecar故障也仅会影响一个业务实例

  • 资源占用-按需

    Sidecar随着业务Pod发布自动注入, 并且可以根据业务需求分配不同的资源给Sidecar, 做到按需使用。

  • 服务治理简单

    仅需对单个目标用户(当前业务)进行服务治理, 简单高效。

  • 可持续发展性高

    符合业内Sidecar趋势, 方便吸收开源优秀的架构设计。

  • 用户可接受程度高

等优点, 可以说是比较理想的部署模型, 但是考虑前期投入,则存在一些需要考量的问题

  • 不支持Sidecar 独立升级

    试想一下如果Sidecar做了版本升级(即使新增的特性并不是一些业务所需要的)也要求业务去重启一下自己的服务, 这似乎违背用户无感知的设计原则。

  • 不支持 sidecar 监控(异常无法告警)

    这个几乎可以说是致命的了, Sidecar自身作为基础组件都无法具备监控能力又拿什么去像业务保证可用性呢。

  • 不支持登录 Sidecar Container进行故障排查

    这个可以说是致命的了(而不是”几乎“), Sidecar出错无法登陆到对应的Container去进行问题排查, 应该没有人敢发布这样的服务。

  • 无法控制业务Container和Sidecar启动顺序

    通常我们要求Sidecar要先于业务Container启动。

DaemonSet模式

DaemonSet模式属于介于传统网关与Sidecar之间的一种, 或者说一种折中。

借助于DaemonSet, 在每个Node节点上都会有一个Sidecar的实例, 用于服务当前Node中的所有业务(即使业务的Pod会经常的被调度)。

相较于Sidecar注入模式DaemonSet

  • 隔离性较低

    同时服务多个业务, 难免会有一些CPU/线程, 网络, 甚至是内存资源上的共享。

  • 扩展性较低

    DaemonSet模式的部署模式已经相对比较固定, 无法灵活的做扩展。

  • 可用性较低

    一旦Sidecar故障便会影响所有当前Node节点中的服务, 需要额外的高可用机制。

  • 资源占用高(前期较低)

    因为K8s随时都有可能调度不同的Pod到当前Node节点, 因此需要预先预分配能服务整个Node节点的资源(即使可以超卖) 。理论上要真的能服务好所有的Pod就得占用当前Node一半的资源(虽然实际这样不太可能)。

  • 服务治理难度较高

    需要在Sidecar中同时维护多个业务的服务治理, 加重Sidecar本身资源占用的同时, 甚至比常规RPC更加复杂(因为RPC通常只需要在Client端做自己的服务治理就可以了)。

  • 可持续发展性一般

    鲜有采用DaemonSet方案的用户, 后期难以进行开源跟进。

  • 用户可接受程度一般

缺点虽多, 但是考虑实际情况, DaemonSet仍旧有优点

  • 部署简单

    独立部署, 不需要对业务部署做侵入。

  • 支持监控与故障排查

    由于是独立分配的容器, 支持使用CMDB登陆排查问题以及监控等。

  • 可独立升级

因此我们初期选择了DaemonSet作为部署方案。

DaemonSet带来的可用性问题

上面提到DaemonSet由于是一对多的部署, 因此一旦Sidecar故障将会造成大面积的影响。

于是在DaemonSet之外我们追加了一个Common集群, 用于本地Sidecar故障的Failover。

  • 当本地Sidecar请求故障后降级到Common集群
  • 本地Sidecar恢复后回退到本地Sidecar正常运行

这无疑又增加了SDK的复杂性。

4. ESA Shaft

ESA Shaft是ESA Mesh中的高性能sidecar实现, 相当于envoy, Linkerd的角色。

初期考虑开发效率以及Control Plane, 以及公司微服务生态等因素决定采用Java, Netty实现。

协议上支持

  • Http1/Http2
  • Dubbo
  • gRPC
  • HttpToDubbo

服务治理支持

  • Service Discovery
  • Loadbalance
  • Rate Limit
  • Circuit Breaker
  • Concurrent Limit
  • Tracing

架构设计

ESA Shaft架构上整体分为

  • Listener

    监听本地地址并分发IO事件

  • L4 Filter

    处理网络事件及协议编解码

  • L7 Filter

    7层过滤器,负责服务治理及请求转发

不同的协议由不同的Listener启动(包含着不同的L4/L7 Filter), 通过不同的Filter组合完成协议解析, 服务治理等复杂的功能。

宏观架构

通过集成ESA Registry注册中心SDK, 服务治理框架Service Keeper以及ESA Conf作为配置中心下发动态配置完成动态化服务治理功能

Threading Model

ESA Shaft的线程模型非常简单

Boss线程:负责监听 & 处理连接

Worker:负责处理I/O, L4 Filter, L7 Filter,请求转发等所有后续操作。

值得注意的是这里的Worker线程数默认使用和CPU相同的数量, 意在尽量减少线程切换带来的开销(虽然Java暂时无法比较方便的做线程亲和性)。

HTTP1.1场景下的性能表现

性能测试环境

Sidecar Echo

直接在Sidecar层面返回Echo数据, 不做请求代理

最高TPS超过25W

正常负荷平均RT:avg(rt)<0.5ms

Sidecar Proxy

代理到3个后端节点, 负载均衡方式为随机。

最高TPS接近12W

正常负荷平均RT:avg(rt)<1ms

由图中可看出性能上Shaft还是比较高的, 但是在活跃连接较多的场景则表现稍差(线程数量设置偏小)。

5. The Future of ESA Mesh

总的来说初期实践阶段我们采用适合公司环境的较为折中的方案(Mesh SDK, DaemonSet, Java), 也踩了不少的坑, 未来ESA Mesh将着眼于行业领先的Mesh解决方案, 进行进一步的演进。

其中包括

  • 采用流量拦截方式, 去除SDK
  • 采用Sidecar注入方式部署
  • 接入统一服务治理平台(ESA Sailor)
  • 自动化部署/运维

ESA Shaft

  • Rust重写

    Java语言确实不太适合做Sidecar, 即使有协程(现在还没有)也有着内存占用高的问题, 再加上GC带来的硬伤,因此很难在Sidecar这个领域施展拳脚。Rust是一门非常好的语言(除了学习曲线异常陡峭之外), 优秀的语言设计以及强大的编译器让应用能达到几乎与C++媲美的性能和资源占用, 也能保有一定的开发效率。目标内存占用在10M级别完成Sidecar的重写。

  • 兼容XDS

    与开源靠拢, 兼容XDS协议。

  • 独立升级 & 热更新
  • 自定义RPC协议(与ESA RPC保持一致)

    采用更高效的RPC协议完成Sidecar之间的通讯, 例如Coap, Quic, 基于UDP自定义协议等。

  • 鉴权/加密
  • Back Pressure

ESA Sailor统一服务治理平台

目前我们已经完成公司统一的服务治理平台基本功能研发

  • 兼容XDS标准, 采用XDS与客户端通讯
  • 单元化的分级架构, 避免加载全量数据

6. 结语

ESA Mesh仍处于积极探索与实践的时期, 期间可能会走弯路, 但随着Mesh架构及技术的演进我们希望提供给用户一个开箱即用的Mesh解决方案, 在行业Service Mesh的演进之路上留下一个脚印甚至是一个里程碑。

image.png


OPPO数智技术
612 声望950 粉丝