#golang


前面几次着重讨论了在分布式系统中出现错误之后该如何处理。虽然长篇累牍谈了很多,但所谈到的情况还是过于乐观,现实则会更加复杂。我们可以根据墨菲定律做一个非常悲观的假定:即所有可能出错的事情最终一定会出错。

作为开发者,我们的核心任务是构建可靠的系统,即使系统面临各种出错的可能,也需要完成预定的工作(确保满足用户期望)。

所以,首先我们要充分认识目前所面临的挑战。

比如:故障可能来自网络问题,以及时钟与时序问题等等……

在有一段工作经历中,我们的线上系统使用的是公有云,其中用到了不同地区的实例。两地区之间使用的是一条百兆带宽的专线。某个星期天的下午,领导通知我们有个服务出问题了,我查了程序日志之后,看到有许多访问上游服务网络超时的日志,即网络问题。随机运维查看之后告诉我们,上面提到的那条百兆专线被跑满了。至此,服务出问题的罪魁祸首已经找到——网络原因。当然,带宽被占满是由于业务增长还是某些服务出现bug抑或是恶意攻击,这就是另一个话题了。

所以,在我看来,所谓网络的不可靠性并不一定特指网络本身出了什么问题。

故障与部分失效

我们所开发的单点程序,通常会以一种确定的方式运行:要么工作,要么出错。单台节点上的软件通常不应该出现模棱两可的现象。而在分布式系统中,可能会出现系统的一部分正常工作,但其他部分出现难以预测的故障,我们称之为"部分失效"。

问题的难点就在于这种部分失效是不确定的:如果涉及多个节点和网络,几乎肯定会碰到有时网络正常,有时则莫名地失败。

正是由于这种不确定性和部分失效大大提高了分布式系统的复杂性。

不可靠的网络

我们目前关注的主要是分布式无共享系统,即通过网络连接多个节点。所以网络是跨节点通信的唯一途径,并且每台机器都有自己的内存和磁盘,一台机器不能直接访问另一台机器的内存或磁盘除非通过网络向对方发出请求。

诚然,无共享并不是构建集群系统的唯一方式,但却是当下构建互联网服务的主流方式。主要因为:硬件成本低廉,可以采用跨区域多数据中心来实现高可靠性,同时也可以给不同地域的用户提供更高的访问效率。

在我们的网络中一个节点发送数据到另一个节点,但是网络并不能保证他什么时候到达,甚至于,不能保证何时到达。

发送请求等待响应的过程中,有很多错误可能出现:

  • 请求已经丢失(比如有人拔了网线,当然在系统上云之后这种物理层面的小问题,基本可以由底层的虚拟化系统来避免和保障)
  • 请求可能在某个队列中等待,无法马上发送或响应(比如网络发送方或接收方已经超负荷,正如文章开头所提到的例子)
  • 远程接收节点可能已经失效(比如依赖的上游服务崩溃,不过目前基于kubernetes的系统可以在一定程度上保障服务的稳定性)
  • 远程节点可能暂时无法响应(比如正在运行长时间的垃圾回收。可以对服务进行内存的调优,可以在基于kubernetes的系统限制内存大小同时增加实例数等等)
  • 远程接收节点已经完成了请求处理,但回复却在网络中丢失(比如交换机配置错误)
  • 远程接收节点已经完成了请求处理,但回复却被延迟处理(比如网络或发送者的机器超出负荷)

处理类似的问题通常可以采用超市机制:在等待一段时间之后,如果仍然没有收到回复则选择放弃,并认为响应不会到达。

检测故障

许多系统都有自动检测节点失效这种的功能,比如

  • ES中节点超过1分钟无响应则踢出集群,而后数据分片在正常的节点上进行重建。
  • kubernetes中节点失效后,集群也会自动将失效节点的任务自动负载到其他节点之上。

超时与无限期的延迟

如果超时是故障检测唯一可行的方法,那么超时应该设置多长呢?很不幸,这并没有一个标准的答案。在上面提到的ES的例子是采用了一分钟的时间,延迟1分钟是为了防止偶尔的网络延迟等,因为将某个节点踢出集群后数据分片在集群中重新分配是需要消耗资源的。

设置较长超时则意味着更长时间的等待,才能宣告节点失效(在这期间,用户只能等待或者看到错误信息)。而较短的超时设置可以帮助更快地检测故障,但可能会出现误判,例如实际上节点只是出现短暂的性能波动(由于节点或者网络上的高负载峰值)。

参考链接

《数据密集型应用系统设计》