如果有人说分布式系统不难,很可能是他还不知道自己不知道。AWS 的文章:分布式系统相关挑战 很好地描述了布式系统面临问题。本文针对其中的一些内容,做些笔记,记录下自己的想法。

分布式系统的类型

三种类型:

  1. 离线分布式系统,一般与线上业务无关,如批处理,大数据分析平台等
  2. 软实时分布式系统,宕机几分钟或几个小时不影响线上业务,如索引生成器
  3. 硬实时分布式系统,也称为 请求/响应 服务,如 web 前端服务器,交易系统

原文章主要考虑的是“硬实时系统”,就博主接触的业务来说,一般“硬实时”要求在 200~500ms 内一个请求要返回,其它行业的业务可能要求更宽一些。

一般硬实时系统跟终端用户直接相关,因此要求有很高的容错性。

容错域

硬实时分布式系统的难点在于网络允许将消息从一个容错域发送到另一个容错域 。发送消息似乎没有什么危害。但事实上,发送消息是一切变得比正常情况更加复杂 的源头

容错域指的是一个边界,当错误发生时,边界内的所有内容都受到影响。例如我们的的程序调用一个方法 save(id),这个方法需要写入硬盘,但是现在硬盘满了,那么我们可以假设,不仅仅 save 方法受影响了,接下来所有跟写硬盘相关的方法都会错误,我们也不必花心思从这个错误中恢复了,反正都没用。

传统上,一台机器(或虚拟机)就可以认为是一个容错域,机器里的程序要是因为 CPU、内存、硬盘等等问题而发生错误,经常情况下程序内部也无能为力去恢复,所以索性也可以不管。

而分布式是将消息从一个容错域(机器)发到另一个,因此需要考虑当另一个容错域(机器)出错时,我们要如何处理,是重试还是放弃?而最恶心的是,很多时候甚至无法得知对方发生了什么。

通过网络收发消息

通过网络收发消息至少需要 8 个步骤,而每个步骤都可能出错(整合自原文):

  1. POST REQUEST:Client 将消息放到网络上,过程中可能因为网络问题或 SERVER 拒绝而发送失败
  2. DELIVER REQUEST:网络将消息发到 Server,可能 Server 接到消息立即崩溃而失败(Server 没收到)
  3. VALIDATE REQUEST:Server 验证请求有效性,可能由于数据包损坏,版本不兼容或其它错误而失败
  4. UPDATE SERVER STATE:Server 根据请求更新内部状态,可能由于 Server 内部问题出错
  5. POST REPLY:Server 向网络发送响应,可能由于网卡或其它问题导致失败
  6. DELIVER REPLY:即使网络在上面的步骤中正常工作,这时也可能无法正确将响应传递给 Client
  7. VALIDATE REPLY:Client 验证响应的有效性,Client 可能判定响应无效
  8. UPDATE CLIENT STAT:Client 根据响应更新自己的状态,可能由于消息不兼容或其它问题而失败

这意味着每一个网络调用,你都需要处理上述的 8 种可能的错误。通常结果就是发现业务代码被淹没在错误处理代码中。客户端代码通常像这样(来自原文):

(error, reply) = network.send(remote, actionData)
switch error
case POST_FAILED:
// 处理 Server 没有接收到请求的情形
case RETRYABLE:
// Server 接收到请求,但未处理(transient failure),需要重试
case FATAL:
// Server 接到请求,但是请求不符合要求
case UNKNOWN: // 超时
// 最恶心的情况,只知道消息发出去了,但不知道 Server 收没收到?处理了没?结果是成功还是失败?
// P.S. 原文说“这里我们只知道 Server 收到消息,但不知道结果”,但博主理解其实是不知道 Server 是否收到消息的。
case SUCCESS:
if validate(reply)
// 根据响应更新状态
// do something with reply object
else
// 响应有问题或不兼容等

爆炸的测试

例如一个功能需要 4 次网络调用,则对每个请求,客户端都需要测试 5 种错误情况,共记 20 个测试。如果单机版本的程序有 10 个测试,则现在需要 200 个测试。同样,服务端也需要类似的测试用例。

处理未知的错误

当调用超时的时候,客户端要怎么处理呢?这时我们只知道消息发送了,不知道 Server 收到没有?开始处理没有?处理的结果如何?当一个 API 不是幂等的时候,问题尤为严重。

例如下订单超时了,客户端应该重试吗?如果消息已经处理了,重试就会重复下单;如果消息只是网络延时了,重试之后,之前的消息又到来了,还是会重复下单;不重试的话,如果 Server 确实没收到消息,则业务逻辑出错。

难处理是因为不知道真实的情况,没有办法做准确的应对。这也是为什么微服务的 API 最好是幂等的,客户端就可以无脑重试了。

硬实时分布式系统群

天启的八种故障模式可以发生在分布式系统中的任何抽象层。上文的示例仅限于一台客 户端计算机、一个网络和一台服务器计算机。即使在这种简单的场景中,故障状态矩阵 的复杂性也会呈爆炸式增长。与单台客户端计算机示例相比,实时分布式系统具有更复 杂的故障状态矩阵

每过一层网络就会有 8 个步骤,就可能会有 8 个错误。考虑客户端 -> 负载均衡 -> 服务器,多了一层网络就多了 8 个可能的错误。

分布式错误通常是潜在的

通常这些错误并不是立马发生(和稳定复现)的。

这些故障不仅普遍而且成本高昂,而且几个月前部署到生产中的错误也可能引发这些故 障。然后需要一段时间来触发实际导致这些错误发生(并蔓延到整个系统)的场景的组 合

分布式错误的病毒式传播

原文举了很有意思的例子:

  • 一台 catalog server 硬盘满了,于是总是返回空
  • 负载均衡发现它的响应特别快,将更多的请求发到这台机器上
  • 接到空请求的业务系统出错,整个网站瘫痪了

分布式系统中的问题总结

  • 工程师无法对错误状况进行组合。相反,他们必须考虑许多故障排列。大多数错误可以随时发生,与任何其他错误状况无关(因此,可能会与其他错误状况相结合)。
  • 任何网络操作的结果都可能是 UNKNOWN,在这种情况下,请求可能已成功、失败或已接收但未处理。
  • 分布式问题发生在分布式系统的所有逻辑层级,而不仅仅是低层级的物理计算机。
  • 由于递归,分布式问题在更高层级的系统上会变得更加严重。
  • 分布式错误通常会在部署到系统后很长时间才出现。
  • 分布式错误可能会蔓延到整个系统。
  • 上述许多问题都源自联网的物理定律,无法更改

写在后面

原文描述的挑战主要是容错/故障方面的,分布式系统的“难”还有其它方面的体现,例如(不全面):

  • 业务上,如何管理分布式状态,如何处理数据一致性
  • 开发上,不同服务可能使用不同技术栈,如何抹平差异,服务间如何通信,隔离?
  • 安全上,如何管理授权,如何止恶意破坏、修改,如何防止可用性的干扰
  • 管理上,机器多,服务多,配置多,如何有效管理(部署、更新、排查、扩展、监控等)。

分布式系统是十分复杂的,要心存敬畏。