微服务架构的分布式容错
『看看论文』是一系列分析计算机和软件工程领域论文的文章,我们在这个系列的每一篇文章中都会阅读一篇来自 OSDI、SOSP 等顶会中的论文,这里不会事无巨细地介绍所有的细节,而是会筛选论文中的关键内容,如果你对相关的论文非常感兴趣,可以直接点击链接阅读原文。
本文要介绍的是 2019 年 SOSP 期刊中的论文 —— Aegean: Replication beyond the client-server model[^1],这篇论文实现的 Aegean 并不是复杂的系统,它更像是一个工具或者框架,能够在今天复杂的微服务架构中保证请求处理的正确性。经典的复制协议(Replication Protocol),例如:主从复制、Paxos 和 PBFT 能够很好地适用于常见的客户端 / 服务端模型(Client-Server Model),但是它们却很难在多服务的设置中保证正确性。
在传统的客户端 / 服务端模型中,客户端的请求往往都是由如下图所示的一组相同服务器副本处理的,不同的部分会运行相同的代码,只是它们的角色可能有所不同,这些副本在处理请求时也基本不会依赖其他的服务;但是在微服务架构中,接收用户请求的服务往往只是 HTTP 入口,它会调用系统中的其他服务完成用户期望的功能:
微服务架构的复杂性来源于服务之间的大量交互以及网络请求的不确定性,调用路径上的任何服务超时或者宕机都可能影响用户请求的处理,服务的宕机也可能会造成用户无法感知请求的结果、系统处于异常状态并无法回滚等问题。
该论文主要做了四部分工作,分别是陈述并定义微服务架构中的现有问题、提出三种用于解决上述问题的技术、在解决问题的基础上优化系统的性能以及实现 Aegean 框架在 TPC-W 基准测试[^2]下评估 Aegean 的性能,我们在这篇文章中更关注论文的前两部分工作:问题陈述以及解决方案。
问题陈述
在这里,我们会设定一些简单的前提条件来展示微服务架构中的问题,如下图所示,客户端会向服务端发送一组请求,这些请求会由服务端的一组中间服务(Middle Service)副本处理,中间服务在处理请求的过程中会调用后端服务(Backend Service)的接口,接收请求的中间服务会包含多个,而后端服务只会包含一个:
如果客户端发起了下单请求,那么中间服务是处理结账请求的服务,而后端服务是处理转账请求的服务。现实世界中微服务之间的交互远远比上图展示的复杂得多,但是这个简单的模型已经可以足够说明微服务架构中服务交互对保证正确性带来的影响了。
主从复制、类 Paxos 协议以及预测执行是在分布式系统中保证系统正确性最常见的三种技术,但是这三种技术在服务交互的场景下却不能很好地工作,这里简单介绍下几种技术的缺陷。
主从复制
在常见的主从复制算法中,主节点会负责处理所有的请求并将状态更新的结果同步到从节点,当从节点确认了状态的更新后,主节点才会将结果返回给客户端。如果我们在主从复制的模型中引入了中间服务,就会出现如下所示的情况:
当主节点向后端服务发送嵌套请求后宕机,后端服务会接收并处理该请求; 作为后端服务的观察者,主节点并没有来得及将消息复制给从节点就发生了宕机; 从节点超时并成为主节点后会丢失该请求相关的信息;
在这种情况下,原有的从节点不知道主节点处理了什么请求,它可能会重新执行嵌套请求、稍有不同的请求甚至可能会向客户端发送『商品已经售罄』的错误消息,然而该用户的转账请求已经被后端服务处理了。
这里出现的最根本问题是,主节点在向后端发送嵌套请求实际上是通知外部服务提交变更,然而在发出请求之前,它却没有将自己的状态同步到从节点,防止在故障时发生状态的丢失。
类 Paxos 算法
Paxos 算法和它的变种是目前使用最广泛的复制协议,这些协议会使用主动复制机制,也就是先确定请求的顺序,然后按照顺序执行客户端发出的所有请求。然而 Paxos 以及变种算法会遇到很多问题,首先,主动复制机制意味着所有的中间服务都会收到并处理请求,后端服务会收到并执行多份相同的嵌套请求:
检测重复请求并不能解决全部的问题,后端服务还需要引入响应缓存(Reply Cache)保证相同请求可以得到相同的响应,也就是幂等性;很多交易相关的服务都会使用唯一标识符来去重并防止重入。
正确性不是 Paxos 算法带来的唯一问题,在微服务的架构中,线性执行请求会对性能造成非常严重地影响,能够明显地降低服务的吞吐量以及服务的可扩展性。
预测执行
预测执行(Speculative execution)是高并发提高性能的一种重要工具,使用预测执行的复制协议会在一组副本对执行顺序达成一致之前向下游发出请求。预测执行在微服务复杂的架构可能会影响系统的正确性,因为在客户端服务器模型中,预测执行需要系统向客户端能够隐藏错误预测带来的不一致状态,服务端也需要支持状态的回滚:
而在微服务架构中,预测执行会导致问题变得更加复杂,当客户端通过预测执行调用中间服务时,中间服务会调用后端服务,客户端在这时如果突然发现预测发生了错误,我们需要级联回滚来恢复状态,这不仅需要回滚中间服务,还需要回滚后端服务。
小结
三种不同的复制协议都不能直接适用于包含服务交互的微服务架构,在对三种复制协议的研究中,论文提出了微服务架构带来的三大挑战:
复制客户端(Replicated client)— 在客户端服务器模型中,客户端是唯一一个向服务端发送请求的机器,然而在微服务架构中,一组复制的中间服务可能向其他服务发送请求,这些服务副本发送的不同请求应该在逻辑上被看做单个请求; 嵌套响应的持久化(Durability of nested responses)— 当中间服务接收到其他服务返回的响应时,它应该先保证该响应被足够的副本确认,这样才能在宕机时保证其他副本能够正确返回响应; 预测执行(Speculative execution)— 在预测状态下,中间服务不能发出任何的嵌套请求,这可能会导致后端服务提交未经确认的状态;
这些都是微服务架构中常见的问题,对服务端开发稍微有经验的读者应该对上述三大挑战非常熟悉,这篇论文只是使用了更加正式的模型进行了归纳和总结。
解决方案
为了解决微服务架构中最常见的三个挑战,论文中引入了服务器垫片(Server Shim)、响应持久化(Response Durability)和改良预测(Taming Speculation)三种技术分别解决复制客户端、嵌套响应的持久化和预测执行几个问题。
服务器垫片
当中间服务使用主动的复制协议时,后端服务会收到大量的重复请求,为了较少冗余请求的处理并保证所有的请求都得到相同的响应,我们可能需要对后端服务进行修改;然而修改所有的后端服务是比较大的工程,我们更希望对现有的架构造成较小的影响,所以论文提出了如下所示的简单抽象:
服务器垫片会与后端服务一起执行,中间服务发出的所有请求会先经过服务器垫片再到后端服务,垫片主要会负责以下两大功能:
接收请求:服务器垫片会认证其他服务发送的请求并确认发送方的身份,在收到请求时也不会立刻交给下游的后端服务处理,而是会等待一组(quorum)来自副本客户端的相同请求,收到足够的请求后会将请求转发给后端并忽略剩余副本的请求; 返回响应:当后端服务生成响应之后,服务器垫片会负责将响应广播给正在等待的一组客户端,同时为了防止消息的丢失,垫片还会为每个客户端持久化响应缓存;
响应持久化
在客户端服务器模型中,副本服务的输入仅仅来源于客户端的输入,而在微服务的架构中,嵌套请求的响应也是中间服务的输入;因为传统的复制协议需要所有的输入都必须持久化地存储到日志中,所以在多服务的设置中,我们需要保证来自客户端的输入和嵌套响应都存储在日志中,只有在持久化日志之后,我们才可以向客户端或者后端服务提交变更。
改良预测
在多服务的设置中,如果一组中间服务 A、B 和 C 在达成一致之前,A 服务就进行了预测执行调用下游服务提交变更,那么当 B 和 C 发现当前请求无效要求回滚时,我们就在系统中引入了不一致的状态。为了解决预测可能带来的不确定性问题,我们会在请求执行的不同阶段引入屏障来对齐多个线程或者副本之间的状态:
当请求执行遇到屏障时,它会等待其他副本直到它们的状态发生收敛,只有在状态达成一致之后,它们才会执行具有副作用的任务,例如:调用下游服务或者返回响应。
总结
作为 SOSP 中的论文,Aegean: Replication beyond the client-server model 中介绍的问题与我们在工程上遇到的非常相似,虽然它提出了一些解决方案,但是作者认为这些解决方案太过于正式和学术。
我们在工程上往往会使用更加粗糙和容易实现的方式解决服务的重入、幂等以及错误恢复的问题,例如:利用唯一标识符去重、通过 MySQL 或者 Redis 存储请求的响应、在后台启动线程重试失败的任务,这些方法虽然不能保证强一致性,但是它们在多数场景下都是够用的,只有当一致性和正确性成为较强的需求时,再考虑文中的几种稍微复杂的技术也是可以的。
更多精彩文章