从微服务框架到服务网格
服务框架的构建理念是一个持续演进的过程。
小规模系统一般直接使用单体模式开发,一个服务即是一个应用。
随着业务规模的增长,单体应用的维护困难、迭代缓慢、扩展性差等问题就会暴露出来,这个时候就需要对单体应用进行拆解,将整个系统拆解为微服务架构。拆解方式可以参考AKF扩展立方。
拆解的过程中,诞生了SOA(Service-oriented architecture)的概念;随着拆解的推进,又出现了微服务(Micro-service)的概念,以应对不同服务的服务治理需求。
而随着微服务架构的演进,整个系统又有可能出现语言碎片化,协议碎片化等问题,业务上也很可能会产生自定义分流、统一安全控制等新需求,这个时候就可以考虑向服务网格演进。
微服务框架
要了解服务网格,首先要对微服务有清晰的认知。这里需要读者已经理解微服务的含义,并可以使用至少一种微服务框架的实践经验。
关于微服务的含义和使用,在这里不会详细展开,有需要的同学可以参考以下链接:
或者到 study.singgel.net/course_cent… 搜索“服务化框架”相关课程。
了解微服务的同学都知道,微服务框架相比单体应用有很多很多好处,但也存在一些问题。
问题1. 语言碎片化
字节跳动的在线服务编程语言比较分散,除了常见的Go和Python之外,还有Node.js, C++, Java, Rust, R等语言。
我们知道,要实现一个完善的微服务框架,需要有服务发现、负载均衡、超时控制、熔断、限流、访问控制、并发控制、流量调度等复杂的功能。
在微服务框架体系下,这些功能需要使用每种语言实现一遍,这将会导致__巨大的开发成本和维护成本__。
而由于语言本身的差异,各种__服务治理规则无法完全对齐__。
譬如:
- 以Python的进程模型,很难提供并发数限制的能力
- C++的流量分流策略与其他语言不一致
问题2. 更新困难
推动用户升级框架往往是一件非常困难的事情,用户有可能还会持续使用你一年以前发布的框架代码。
当某个框架版本发现重大Bug的时候,要推动相关服务升级到修复后的版本是一件非常困难的事情。
当需要推动一个全链路新功能的时候,譬如自定义分流,我必须推动整个链路的上下游全部升级,才能最终把整个功能上线。
假设推动了一半业务升级后,又发现了一个严重的Bug,那将是一场噩梦!
问题3. 协议多样化,难兼容
对于RPC来说,Thrift的不同版本之间是不兼容的,Thrift和FBThrift有一定的差异;
线上的流量是HTTP, Thrift, gRPC, MySQL等协议并存。
流量的协议不一样,但很多流量管理的需求却是类似的,如超时配置、跨机房流量调度等,开发者很想对不同的协议施加统一的管理能力。
当然,以协议本身的复杂混乱程度,仅靠服务化框架是很难统一管理的。
问题4. 服务治理消耗巨大
以Python为例,Python是一种解释性语言,它的性能是向来被人诟病的。上述超时控制、熔断、限流等一系列服务治理规则用python实现一遍,资源消耗非常高。
一般地,python服务中,用于处理各种服务治理部分的CPU消耗,占总CPU消耗的50%以上。
问题5. 业务侵入性强
框架向业务程序注入了很多与业务无关的代码。
业务开发者在分析自己的服务的时候,可能会发现有很多服务治理相关的并发线程,对于业务的研发和性能优化有很多干扰。
服务网格
Mesh是由架构-服务框架 小组自研的一套服务网格。
它实现了一个高性能多协议的代理和一个灵活可扩展的控制服务,将他们与云平台原生集成,最终通过轻量级的RPC框架来输出能力。
借助Mesh,开发者可以使用自己喜爱的语言( Go, Python2, Python3, Node.js, C++, Java, etc. )开发自己的服务,享受统一高效的服务发现、服务治理、安全审计等功能。
设计目标
- 完美兼容微服务框架体系中提供的服务发现、服务治理等一系列功能;
- 解决微服务框架体系中存在的上述问题;
- 支持包括访问控制与安全审计,更灵活的自定义流量调度等新功能。
Mesh结构介绍
先来了解一下Mesh
在非Mesh框架下,服务请求到达TLB之后,是直接打入Service A的,Service A 需要访问Service B,Service B 需要访问数据库的时候,全部都是直接连接。
嵌入业务服务的框架需要处理服务发现,服务治理等一系列工作。
在Mesh框架下,我们在每个服务的同一个容器内部嵌入了一个proxy,接管了服务所有的出流量和入流量。
Mesh由三个部分构成:
- 数据面(Data Plane)
- 控制面(Control Plane)
- 云原生支持层(Cloud-Native Support Layer)
其中,
数据面对应图中的proxy A、proxy B和API Gateway;
控制面对应图中的control plane;
云原生支持层更多地是辅助性的工作,譬如云平台融合、组件健康检查、管理proxy的热更新等,在本图中没有显式的表示。
proxy的本质是一个高性能的流量代理。
proxy接管流量后,服务不再负责服务发现和服务治理,可以更专注于业务逻辑。
proxy只是与服务在同一个容器共生的一个进程,它也不知道流量所需要的服务治理规则,也无法进行服务发现,相关内容都需要询问control plane。
control plane根据proxy上报的服务调用信息,下发服务发现和服务治理规则,由proxy负责执行。
为了防止流量过大和增加延迟,在proxy内部有一个缓存层,缓存control plane下放的配置信息。
proxy在第一次收到某类请求的时候会访问一次control plane,并在之后的过程中每隔几秒执行异步刷新。
云原生支持层保证整个系统对用户基本透明,用框架开发的用户只需要在APPEngine上点一下开关,即可使用Mesh。
各个组件的更新由我们统一负责,升级再也不用敦促业务做改动。
设计目标的达成
新的Mesh架构能否实现上文列举的三个主要目标呢?下面会依次讲解:
兼容服务化框架体系中的服务发现、服务治理等一系列功能
服务发现:control plane 通过访问consul获得服务发现的节点信息,返回给proxy,决定流量走向。
服务治理:将存量服务治理规则导入control plane的存储,并修改MS,使MS规则写入的时候双写。
control plane决定服务发现和服务治理以后,proxy只要相应执行即可。
问题1. 语言碎片化问题
由于服务治理和服务发现功能都已经转移到Mesh的组件来实现,因此框架只要实现很薄的一层即可。
只需要对相应的语言实现一套序列化方法,在很低的成本下即可使用统一的服务发现和服务治理能力。
问题2. 更新困难
由于框架只需要实现很薄的一层序列化层,逻辑简单可靠,因此框架层几乎没有更新的需求。
Mesh的组件,包括proxy和control plane,由Mesh负责热更新,业务无需感知。
问题3. 协议多样化,难兼容
proxy设定了一种TTHeader的协议标准。只要满足这个标准,不同的协议都可以进行接入。
以api gateway为特殊例子,api gateway 是一种特殊的proxy,它可以高效地完成从HTTP到Thrift的转换。
问题4. 服务治理消耗巨大
所有的服务治理执行逻辑转移到proxy,而proxy是基于开源的Envoy实现,使用C++作为开发语言,效率非常高。
如果公司的Python服务都可以迁移到Mesh,将会节约很多很多机器资源。
问题5. 业务侵入性强
嵌入业务的框架只保留一层非常薄的序列化层,对业务的侵入大幅减少。
支持包括访问控制与安全审计
control plane可以配置规则,对服务之间的流量进行服务授权,决定请求方是否有权限访问某个接口。
proxy层可以通过绑定token的方式,进行身份鉴定,防止流量伪造。
流量都转为proxy之间的流量之后,该部分流量可以使用mTLS进行加密,防止监听。
更灵活的自定义流量调度等新功能
由于整个流量轨迹都被Mesh控制,因此可以通过proxy向流量中注入标识,然后根据标识决定流量调度规则。
以PPE(Product Preview Environment)为例。
在TLB入口层,会根据选定的did,向对应的流量注入PPE标识。
在流量传播的过程中,如果对应的PPE标识在对应的服务有分流的需求,control plane会下放分流指令,把对应的流量打到指定下游服务。
用这种方式,就完成了服务的自定义分流调度。
新问题、新挑战
同样地,没有银弹。Mesh 也会带来一定的问题和隐患。
问题1. 单点故障
从架构图上可以发现 control plane 成为了全路径强依赖。这给整个系统的稳定性增加了一定的隐患,必须对控制面施加一系列容灾方案。
问题2. 延迟
从多数语言来看,使用Mesh之后,CPU的消耗都会出现一定的下降。(CPU: Mesh下业务进程+proxy进程 < UnMesh业务进程)
但是延迟会略有上升。毕竟整个调用过程中,增加了业务进程到proxy进程,和proxy进程到业务进程两步。
目前平均延迟的上升一般可以控制在0.5ms以内。
问题3. 维护复杂度
虽然Mesh从设计之初就希望对业务透明,但是不可避免地还是要引入一系列新的内容。这对于运维人员也是一个不小的挑战。