201903 Distributed Fault Tolerant System Design 学习笔记 (1) Elements of Distributed System Design - xiaoxianfaye/Learning GitHub Wiki

1 为何要引入分布式系统

1.1 微服务

从近几年最火的微服务说起。

通常会觉得微服务的拆分(如何把一个单体系统拆分成微服务系统)是最难的。但其实,不管用什么方法,把一个单体系统拆分好之后,才真是问题的开始。因为,一旦拆成微服务以后,就进入了分布式领域。

分布式领域跟之前的领域完全不同,原来的思维方法、设计技术在分布式领域中失效,因为它们的前提假设不一样。如果没有系统理解分布式领域的一些关键问题的话,把一个单体应用变成一个微服务应用非常危险。

单体系统很痛苦、很麻烦,但是如果没有适应分布式领域的话,你会发现拆完之后更痛苦。因为在分布式领域中系统出问题后带来的麻烦要远远高于单体系统。可能原来只是编译慢一点、部署升级麻烦一点,拆成微服务以后,变小了,每个都能独立升级、编译也快、部署也快,这是优点。但如果没有对分布式系统的认识,优点没得到,问题可能比单体应用加倍增多。

microservice goodbye

详见 https://segment.com/blog/goodbye-microservices/

并不是说微服务好或者不好,微服务只是一种架构风格,而且是一个分布式系统。如果没有把一个分布式系统设计好的基础的话,贸然到微服务,是一件非常可怕的事情,按下葫芦浮起瓢。

如果想做好微服务,可以学习微服务的各种拆分方法、部署、升级和协作等,但首先要学好分布式领域的设计。因为它首先必须是一个好的分布式系统,然后才能是一个好的微服务系统。或者说,如果你能够把一个分布式系统设计好的话,不用太在意是不是微服务。

1.2 从程序思维到系统思维

在日常工作中,潜意识里是有思维模式的,只是说可能没有察觉。

程序思维,比较好理解。每天编程序,定义多少函数、如何定义数据结构、函数名字是不是起得更清晰一点、函数不要太长、圈复杂度要小一点、面向对象、不面向对象、用哪个编程语言等。程序思维是我们工作中很明显的一个思维方式。

每个人一定有系统思维,只是是否意识到。在没有意识到的情况下,就会靠直觉,用随意的方法做事情。什么是系统思维?什么时候会切换到系统思维?出故障、跟别人对接、不断重启的时候,这时就是系统思维。这时,你考虑的什么问题呢?例如,重启是快是慢?跟别人对接是否满足协议语义、协作是否正确?另外,出故障需要Debug的时候,也需要系统思维。这时,不关心是否用面向对象设计、用的什么语言,更关心的是状态是否正确、状态序列是否正确、系统行为(要考虑顺序,这个顺序就是系统行为)是否正确。

在平时的工作中,这些事情都在做,但并没有把它们凸显出来。我们如果想提升自己的能力,一定要把系统思维显式化出来。要知道我是在考虑系统问题,有哪些重点系统问题,要提升系统设计能力,系统设计能力一定要有系统思维方法。

那么,如何提高系统思维能力?讲系统设计的资料非常多,海量信息。问题在于是不是看了这些信息能力就提升了?信息太多,不知道看哪一个,无所适从。对个体来说,如何把看到的知识变成能力?

跟所有的学习一样,也是个常识。你碰到过这个问题,试图去解决过这个问题,试图去想如何更好地解决这个问题。这个事情做了,第一,你的能力就在提升;第二,再去看相关知识的话,你的感受会更深。这个东西我思考过,这个方法是可行的,那个方法不匹配Context,所以不可行,这个方法要改造一下等等,你会有一个判断。而不是说,别人说很好,你就觉得很好,拿来就用,这样危害可能更大,还不如你不知道这个东西,老老实实按部就班地去解决问题。

我们在工作中会碰到很多问题,解决问题的态度很重要。是说问题不能复现就完了,还是说更深入地思考有没有更好的方法,并尝试应用这个方法,自己判断是好是坏,为什么好为什么坏。

从平时工作入手,从实际入手。

1.3 工作中,最怕的问题是什么?

在软件产品开发工作中,最怕的问题是什么?

故障不能复现?其实不是害怕,而是烦躁。 性能很低?也不是害怕,改嘛。

在实验室和在现场出问题是完全不一样的。

在现场,用户感知到系统行为跟预期不一致——这是最可怕的。

1.4 如何解决这些问题?

很多好的设计方法、思想是在碰到具体问题的时候闪现出来的,只是说有没有去深究它。 今天的过程就是让大家把脑子里的东西拿出来,显式化。

在测试部出问题,可以回来好好看看代码。当然这肯定是需要的,这是根本的解决问题的方法。把根源找出来,一定要这样做。

但在现场这个Context下,如何解决呢?因为系统已经运行起来,是一个活的系统。 重启看看。

这就是系统思维。这个时候不会考虑用什么编程语言,因为没有任何意义。

1.5 这样的解决方法为何有效?

把系统恢复到一个已知的状态重新运行。

首先要有一个已知状态,这个已知状态是我可以掌控的,我知道这个状态肯定是对的。其次,要能恢复到这个状态。

因为目前的系统状态已经乱了,我没办法去了解它,也很难去改变它,我的目标是让系统恢复到一个已知状态开始运行,重启是一个很有效的方法,其他方法无效。

目前所有的做一个高可用、高可靠系统的方法都来自于此。

1.6 产品中的Bug

1.6.1 发现产品中Bug的难易程度

稍微暂停一下,先看一下Bug的问题。先排除硬件问题。

重启有用,是不是所有问题都重启呢?程序随便编,重启就完了?这肯定是不行的。所以继续深究一下。

可复现 偶发
核心功能 容易
附属功能 容易,但常被忽略

产品功能分为两类:核心功能和附属功能。核心功能是系统本身存在的意义,附属功能是锦上添花、可有可无的,不影响使用的。

Bug按复现程度分为两类:可复现和偶发。

对于核心功能,肯定是测试很充分的,用户用得也很多,测试部90%的精力全放在核心功能测试。如果Bug容易复现的话,Bug很容易被发现,因为测得多、用得多、又容易复现。但如果Bug是偶发的就难以被发现了。

对于附属功能,Bug容易被发现,但经常被忽略。因为测得少,或者发现之后觉得无所谓。对于偶发的Bug,也是难以被发现的。偶发的Bug跟很多条件有关,环境、条件不一样的话,Bug可能就不出现了。

这个分类跟刚才的问题是有关系的。可以看到,核心功能的可复现的Bug很容易被发现,其他要么被忽略要么很难被发现。

1.6.2 产品中Bug出现的频率

来看一下,允不允许Bug总是出现?

可复现 偶发
核心功能 绝不允许出现 总是出现
附属功能 经常出现 总是出现

核心功能的可复现的Bug是绝对不允许出现的。偶发Bug很难定位,不在这儿出现,就在那儿出现,偶发Bug只是很难复现,但出现频率还是有的。

附属功能的可复现的Bug经常出现,但总是被忽略。偶发的Bug很难禁掉。

1.7 重启能否解决产品中的Bug?

再来看,重启能否解决问题?

可复现 偶发
核心功能 不能
附属功能 看情况

核心功能的可复现的Bug重启也没用,所以这些问题必须解决掉,重启后Bug照样会出现。

附属功能的可复现的Bug要看情况。能否解决问题,一方面要看系统能不能工作,另一方面要看用户是否要投诉。

对于偶发Bug,重启肯定能解决问题。因为偶发,可能一个月出现一次,或者每天关机重启,用户可能感知不到。

当然,对于很多电信产品,偶发Bug也是很严重的问题。这是电信产品的特点。但对于很多互联网应用,无所谓。这是电信产品的特点,但是Bug是不可避免的。

为什么Bug不可避免呢?

1.8 Area of Knowledge

来看一下Bug为什么会产生。

各种Bug能否复现、Bug是不是严重问题、问题能否解决等,这些在我们的工作中司空见惯,每天都在做,只是我们需要静下心来好好思考一下,把这些问题归归类:为什么能解决问题,如果能解决问题,那这些方法能否做更深层次的应用呢。

area of knowledge

把方框当成我们所有的、整个的知识。蓝框表示代码,代码其实是知识的一种表现。代码很庞大,以前全都是自己写的,现在代码不可能一个人全清楚。红框表示Bug,Bug全在代码里面,各种Bug肯定都反映在代码上。黄框表示自己知道的。紫框表示我们觉得我们自己知道的。其他地方表示我们知道我们自己不知道。

有些Bug是在我自己知道的框里,这些Bug好解决,因为我很清楚。我自己认为我自己知道、但其实我并不知道,这类问题相对来说也好解决,只要你去思考一下,这个问题为什么会出现,原来我的假设错了,我是这样假设的,但其实是另一种情况。知识你有,但不是很全面,或者做了错误的假设。外面框里的就没办法了,你也不知道,因为你都根本不知道存在这些东西,需要了解,了解以后也无所适从,这类问题很难解决。即使在这种情况下,我们还是要编写代码,要交付。

那么对于这类问题,我们该如何去解决呢?如何让外面的未知领域小一点?或者说即使出问题了,也能够在我的控制之内呢?即使我不知道会出什么问题,但是出了问题之后,我还是有一个比较好的应对方法呢?

  • 第一,我让自己知道得更多。学习各种各样的知识,这样框就更大一点。
  • 第二,对我的设计进行探索。可能我做了一个设计,特别是分布式系统、并发系统,我觉得是对的,为什么觉得是对的呢?因为一个系统,有Bug或者没有Bug根本上是由系统行为决定的。行为就是顺序。比如,它只能按1->2->3->4->5->6->7的顺序执行,但其实它可能是按另外的顺序执行,我没有考虑到。这时怎么办呢?我们做各种评审,但也要利用工具。如果你做的是一个电信产品,或更质量关键的产品,需要TLA+这样的工具。这样的工具可以帮助我们拓展系统行为空间。做了设计以后,我觉得可能就这么多行为了,其实还有很多行为我没有考虑到。通过它,可以帮助我扩大。
  • 第三,实在到边了,没办法了,那就考虑最差情况,这些事情都做完了,还会有Bug。这些Bug更多情况下都是附属功能里的Bug、或者核心功能里的偶发Bug。这时我要考虑最差情况——如果出问题怎么办?其实重启就是解决这类问题的。重启是最后一步,前面工作都要做。一切都未知,不知道什么情况,系统行为至少目前已经超出我的理解范围了,而且短时间内我也没有办法去解决Bug,肯定要尽量让系统先恢复,让系统恢复到我能控制的状态。所以重启能解决问题的前提在于,重启使系统能回到一个我们能控制的状态开始执行,否则重启也没用。

为什么重启也没用? 数据被破坏掉了。所以,要想让重启有效,首先要保护好数据。数据没有保护好,重启也没用。

如何应对这种Bug?

  • 第一步:你要知道系统正常地恢复运行需要什么样的数据状态;
  • 第二步:看好你的数据。看不好的话,重启也没用。

这就是系统思维。

下面把这个思想用图来表达一下。

systematic thinking 1

椭圆代表系统。系统里有两部分数据:

  • Essential Data:要看好的核心数据,出问题的话重启都没用。
  • Derived Data:导出的数据,从系统最核心的数据来,系统运行有自己的运行状态,这些状态本身可能是暂时的、但对系统行为是关键的、可能会根据用户的输入改变。

系统出问题的原因就在于,系统的Derived Data出问题了,有Bug了,不对了。这时没有办法理解它,也恢复不了,重启,重新从Essential Data开始。

我们在做系统思维的时候,考虑的核心是数据。哪些数据是系统最本质的,哪些数据是非本质的,要把它们隔离开,要把Essential Data保护好。这样的话,重启就是解决问题的非常有效的方法。在我最着急的时候,没有任何办法的时候,这招是管用的,否则重启都没有用。

如果在现场重启都没有用的话,怎么办?那是一件很影响睡眠健康的事情。

可以看到,我们在工作中都是这样做的,只是说要把它更深入地思考一下。

systematic thinking 2

重启有效的话,发现有问题了,报障了,人去把它重启掉。这个工作,首先它不及时,其次人也不舒服。现在很多系统里有看门狗,看门狗的本质是监控系统,发现系统不好的话就重启系统。看门狗类似一个Supervisor,它不做业务的事情,它只监控系统目前是否正常,保护着Essential Data,发现系统一不正常就把系统重启了,然后记条Log,你事后去分析。

我们的系统里面的设计思想是这样的:有一个Supervisor,它可以是人,开始肯定是人,人来干这事,但人干这事太慢,系统宕机时间太长、不及时,就搞自动化,搞一个程序代理帮我们做这件事情,它保护着Essential Data,监控(supervise)着系统(System)。它的作用就在于系统不健康就重启。

我们目前所有系统里面都是这种模式,甚至更早的十年、二十年前都是这种模式。但我们可以更深入地去思考一下这个问题,这个模式是有效的,在实际工作中确实是有效的,但往往粒度很大,例如一块单板、或一块主控板。这种思想既然有效,是不是能更深入一下呢?为什么要更深入呢?

1.9 Availability

先来看一个外在的体现。

一个系统,不管怎么做,对用户来讲,系统好或不好,除了是否好用、功能是否实现得更加人性化、体验更好之外,就是系统服务是不是一直有,能不能一直访问。前者比较主观,先排除掉,如何设计一个用户体验更好的系统不是我们讨论的主题,我们讨论一下如何让用户感觉到系统一直能用。用户只要想用就都能用,而不是发现宕机不能用了。

availability definition

可用性就是一个系统的正常运行时间(uptime)除以正常运行时间加上不正常运行时间(uptime + downtime)。

可用性通常会说几个9的可用性。可以看一下差别。

一年宕机一个月就是1个9,听起来很高,但宕机时间很长,其实没法用。2个9四天,3个9不超过9个小时,4个9不超过1小时,5个9约5分钟,6个9约31秒。

虽然现在大家都对电信产品的期望降低了,但其实电信产品的可用性应该至少5个9。

如果现在出问题了,重启得快,能恢复,可用性就高。如果想提高用户对可用性的体验,一方面把系统做得好,另一方面让重启有效、且重启快,用户都感知不到重启。

如何才能重启快?

  • 检测要及时,这是一个双刃剑,太及时,没事老重启,这是个问题。检测准确度要高、要灵敏。
  • 数据要更易于加载。
  • 小粒度重启。

有可能目前的技术不支持,但你要这样去想,培养思维能力,然后这样去做,做的过程中碰到问题,碰到问题就会找工具、找知识,就提升了。而不是别人告诉我这样好我就这样做,那肯定做不好。

很多情况下,我们并不是需要一个非常好的平台或者工具才能把事情做好,而是我有一个好的想法,深入理解问题,因为每个问题都不一样,并不是说一定要做一个很通用的、100%匹配的方案,而是能跟问题匹配80%已经很好了,不要那么复杂。

systematic thinking 3

更系统性的方法是,希望把一个系统变成多个小系统。可以配置小块系统重启,这样重启就快了。本来要加载500M的数据,分成50份,一份只有10M数据,加载数据就快了。

这里的思维就是:我要数据加载快,怎么加载快呢?局部加载,加载一小部分。我要重启快,怎么重启快呢?不要全部重启,重启一小部分。编译慢,怎么解决呢?一个一个独立编译,不要每次全编译一遍。

沿着这个路线往下思考,你就会发现,你自然就得出这样的结论。

图中每个椭圆框都是独立的,能够自己独立重启的。在实现层面上,这些椭圆框到底是什么语义呢?能不能做到呢?

我不能100%做到跟我不能做是两个概念。不能这个问题解决不了,这个事情我就不做了。没有说,所有问题都解决完,事情才能做的。按这个思想来做,虽然不能100%,但自然会让系统容错程度高一点。任务之间的数据定义得好一点,不要互相搅和在一起,系统跑起来之后,本身的容错性就高一点。

仔细想一想,多进程模型、容器等这么多所谓的新技术在解决什么问题?本质上就是这个东西。因为对于一个分布式系统来讲,运用系统思维一定得出这个结果,就是这样,把它独立开,每个定义好核心数据。

怎么拆分微服务呢? 有那么多原则告诉你业务上的依据,那些都有用,但最本质要考虑一下:Crash之后怎么办?Crash之后它们之间会不会有影响?一个Crash,另一个还能不能工作?如果能工作,两个就分开,如果不能工作,两个就合在一起。重启之后需要什么数据?最小数据是什么?就把数据拿出来。基于这个思维来考虑的话,得出来的设计一定是一个可行的、可用的设计。反过来,完全基于业务的原则拆分,拆完出来的东西完全不能用,这是一定的。因为,我们这个是血的教训得来的,那个是别人想出来的然后灌输给你。系统拆分微服务,最核心就是抓住这一点:容错、重启得快、重启后两边没有影响(Faye:我理解为隔离)。例如,一块单板重启会影响另一块单板吗?一块线卡重启会影响主控板吗?如果这样设计,系统还能用吗?肯定不能用。为什么不能影响?可以做到影响,为什么不能影响?要考虑到一个重启不能影响另一个。这是一个最基本的系统设计的思维方法。

可以再大胆一点,再往上扩充一下。

Faye:容错、隔离、重启快。

1.10 思考的要素

我们如果这样思考的话,我们不再思考什么编程语言、面向对象还是不面向对象、怎么设计类、怎么定义函数、怎么定义数据库,这些也很重要,并不是不重要。我们思考的是如下要素:

  1. 数据的分类与分层

Essential vs. Derived

哪些数据是高层Essential的、哪些数据是低层Essential的,哪些数据是Derived的。

  1. 隔离、自治

这个框重启,不要影响别的框。两个框不要耦合在一起。

  1. 无共享

copy一切

隔离、自治的手段就是最好不要有共享。一共享就完了。假设拿到另外一个指针指向另外一个Data,它一重启,指针历史就变了,对我来说就毫无意义了,我也崩溃了。所以不要有共享。

线卡和主控板就没有共享,我们就是这样做的。我们直接把这个思想再往前推一下,推到一个单板内部,后面再拉开。

  1. 异步消息

  2. 崩溃、重启

考虑死了怎么办。现在不会考虑调这个对象系统死了怎么办,死了就死了呗,死了就重启。现在要考虑到,你调用的每个框都会死掉,这时应该怎么编程、怎么做设计。

  1. 健康性监控

没有监控的话,死了你也不知道。

以上是系统思维考虑的要素。从考虑程序方面变成考虑这些要素。这些问题我们无时无刻不在考虑,因为你必须要考虑。出问题了,不考虑这些问题,你解决不了问题。只是说你要把它显式化出来,认真地考虑,再深入地考虑,就会得出一些非常有意义的结论,这些结论可以帮助我们解决更大的问题。

在电信领域,我们有很多标准,有接入设备、核心网设备、交换机等。如果把整个电信网看成一个系统的话,就会发现:

  • 数据是分类、分层的,交换机的数据、核心网的数据、接入网的数据都是不一样的。
  • 隔离、自治的,靠协议对接,考虑的是协议,而不是面向对象或不面向对象。
  • 程序之间没有共享,全是异步消息。
  • 崩溃、重启,互相不影响。
  • 健康检查

可以看到,如果我们把视野放大,把整个电信网看成一个系统的话,就是这种思想。这种思想在电信系统里,多少年前就已经是这样了。现在流行的容器等看上去很先进,其实思想全都来自于电信系统,只是说在新的平台上用新的工具把它包装了一下。如果理解了思想,工具用得更好。

1.11 船如何防止下沉?

ship

船如何防止下沉,设计是很有思想的。船舱用隔板隔开,全是封闭的,相互隔离,撞破一个,船不会沉的。

泰坦尼克号为什么会沉?为了美观、省材料,没有完全隔离。

各领域思想是一样的。

我们想设计一个容错的系统的话,隔离的思想是不能打折扣的,一旦打折扣,一定会在某天咬你一口。隔离是一个非常重要的因素。

1.12 数据复制

提升可用性,要重启快,要分成很多小的,那还是要重启,不重启不是更快吗?另外,硬件坏了怎么办?这就是一个很自然的推论了。要想做一个容错系统,至少需要两套硬件。例如,主控板两个。一个的话,只要硬件出问题,重启也不行。

data replication

所以,我们需要两个节点,可以认为是两套硬件。它们之间要进行数据复制。

数据复制这个问题比较头大,我们的系统中主备问题是最难解决的问题。为什么难解决呢?因为一旦开始数据复制,就进入到分布式系统的世界了。分布式系统世界里的一致性是一个非常非常难解决的问题,里面有很多选择和权衡,必须要很明确地做,你要知道想要哪些特性,舍弃哪些特性,而不是随便做一个,那一定会有很多问题。

提供一个容错API,访问其中一个节点,访问不了就访问另一个节点。对用户来讲,是一样的,但前提是要保证两个节点状态完全一致。否则,切换之后用户感知到行为发生变化了,或者发生错误。

这是一个自然的推导。我们想提升系统的可用性,重启,重启得要快,而且防止硬件错误,或者不重启、切换直接用更快。

还可以再往下推。

1.13 监控配置管理

supervisor configuration management

图中两个方框可以当作两个数据中心。整个数据中心里有个监控配置管理,里面保存整个集群的元数据。最上面的监控配置管理保存两个数据中心的元数据,它监控下面。两个数据中心可以根据我们的要求做数据复制,两个数据中心之间也可以做保护。只是粒度的变化,思想完全一模一样。一个数据中心里有自己的元数据,也有自己的Essential Data,整个数据中心宕了以后重启、断电重启,也是需要能够从一个干净的状态才能恢复的。

这是一个非常漂亮的自相似的结构。可以从一个单板或者一个节点上的系统到一个集群到整个跨数据中心的,思想是完全一样的。

首先你得知道你可以做哪些选择,其次你要做出一个跟你目前的问题匹配的选择。但是没有一个1、2、3的步骤告诉你这样做,你必须自己做,你的能力就体现在这个地方。完全满足业务目标、复杂性还低,这是最好的。也可以做得复杂性很高,也满足业务目标,但成本就很高了,这就不那么好了。如何做平衡,这是很难的问题,这是个艺术。迭代也是,刚开始,你知道有这么多东西,但不要全上,一步一步往上加,然后根据结果反馈调整。

一个Docker就是一个自治的小框,完全隔离,异步消息。Docker只是一个工具,能不能用得好,要看Docker中关联哪些数据,Docker之间Crash之后是不是互不影响。互不影响是由业务决定的。看业务的角度不一样,就会发现划分的方法也不一样。

Crash-oriented(面向崩溃)来考虑,面向数据的分类来考虑,再加上深入理解业务,才能把微服务做好。

Docker之类的只是工具。可能过段时间Docker消亡了,又出来另外一套工具,其实还是这种思想,只是说它可能比Docker更轻量、更好用。思想的寿命是很长的,工具的寿命是很短的。

1.14 程序思维与系统思维小结

  • 程序

    • 应用库
    • 运行时和核心库
    • 语言原语
  • 系统

    • 应用服务
    • 基础系统服务
    • 协议和格式

程序思维关注的是编程语言、原语、运行时库。系统思维关注的是协议(协议是系统语言)、格式、基础系统服务和应用服务。“服务”是个叫法,就是画的小框,可以定义成“可以自治运行的系统部件”。微服务的粒度不是越小越好,或越大越好,而是根据容错的需要来定义的,即我希望这个系统容错到什么程度。系统容错要求越高,成本(时间、人力、金钱)越高。

1.15 基础系统服务

  • 通信 (message queue)
  • 协调 (Zookeeper)
  • 控制流 (ASW simple workflow, storm)
  • 内存 (memcache, redis)
  • 硬盘 (S3, K/V store)

对于计算系统来讲,它的组成部分是一样的。

很早以前编写程序和现在类比,函数调用其实就是通信,mutex锁其实就是协调,if/else/while就是控制流,内存就是内存,硬盘就是硬盘。在系统思维层面上还有这些概念,message queue代替了函数调用。Zookeeper是在分布式系统里如何协作一大批节点能够一致地工作,可以认为它是一个mutex,但比mutex的作用要更多一点。控制流是一样的,workflow,storm是流处理,里面有很多类似if/else的判断。内存更大了,分布式内存。看不到硬盘了,看到的是分布式系统,对我来讲存储无上限,可以随便放,但本质还是一样,概念还是一样。在系统层面上考虑的不再是编程语言、面向对象,而是这些东西,基于它构建更高、更大的系统。

但这个领域是一个分布式领域,它的很多设计假设跟单体领域完全不一样。举个例子,函数调用,觉得应该立即返回、也不会出错,但这个假设在分布式领域就不合适了,message queue里可能会丢,可能会很长时间没有回应。进入分布式领域以后,要换一种思维方法,要把原来的设计假设全部抛弃掉,重新建立新的假设,才能把分布式系统设计好。认为分布式系统可以抽象成一台计算机,这是一种假设,也是一种抽象,问题是这种抽象跟现实不匹配,如果你认为匹配,系统问题肯定很多,称之为“抽象泄漏(Abstract Leak)”。现实是这样,你非要抽象成那样。什么是抽象?把现实问题里跟问题无关的东西去掉,问题在于把有关的也去掉了,那就出问题了。

从这一步开始,我们就进入了分布式系统。

2 分布式系统的关键概念和基本问题

如果想做好分布式系统,除了要有前面讲的系统思维方法以外,我们必须要理解这个领域。如果这个领域都不理解,就贸然进入,那是很痛苦的。

研究一个问题肯定要做抽象。就像物理学家研究天体运行,肯定不会关心月球上有多少个坑,而是做一个抽象,关心月球的质量、速度。我们也一样,要对分布式系统里的那些关键的东西做一个抽象,建立一个模型,基于这个模型去研究一些问题。

研究的目的在于:

  • 我知道在这个系统里面做事情的边界,哪些事情能做,哪些事情不能做。
  • 我知道有多少种选择,这个东西不能做到100%,但有很多选择,我可以做选择,我知道每一个选择的代价。
  • 我知道一些基本的设计技巧。

了解完这些东西是很重要的,做设计的时候加上一些设计要素,至少可以做出一个不差的分布式系统。并且再去看那些开源软件的时候,我就能知道它为何这样设计,它的承诺是否真地能达到,还是只是一个噱头,适不适合我们。

我们要知道这个领域水有多深,不见得要知道所有细节、研究清楚每一个算法,但我要知道这个算法能解决什么问题,什么问题解决不了。

所以,第二部分讲一下分布式系统的关键概念和基本问题。

在系统思维里,我们从一个工作里最怕的问题入手,在解决这个问题的过程里面深入思考,进入了分布式系统领域。

分布式系统能给我们带来什么好处? 如果你想做一个可靠、容错的系统,至少是两个节点,肯定是分布式的。另外,做系统有两个基础资源:计算资源和存储资源。单个节点的计算资源和存储资源都是有限的。我希望它们取之不尽用之不竭。可以不分布式,做一个理想的计算机,存储空间无限大,带宽无限宽,成本太高。任何一个系统都有物理限制,是突破不了的,必须了解这个物理限制。但我们还是期望系统一直可用。我们期望只要加机器,处理能力线性增长——伸缩性(Scalability)。

2.1 目标、约束、约束的影响

2.1.1 期望达成的目标

2.1.1.1 Scalability

  • 系统、网络或者过程在面对的负载量增长时,还能有效处理它们的能力(通过增大自己的能力以应对负载增长)

  • 期望:线性伸缩

伸缩性不光可以指一个系统,也可以指平时工作里的过程、方法。我本来要做的事情,系统也好、网络也好、过程也好,处理的量这么大,当负载量增加的时候,能不能有方法去处理这些新增加的负载量?方法就是增大自己的能力,我们的期望是线性的。不能说,假设处理能力要加一倍,现在要加十倍的机器,这就是伸缩性很差。

伸缩性会体现在很多方面:

  • 规模:加一台机器处理能力就增加了。
  • 位置:在A点来一个,B点来一个。A点和B点可能很远,还可以很好地处理A点和B点之间的延时,让用户能够接受。不能说A点和B点延时很大,不能用了。
  • 管理:本来十台机器需要一个管理员,现在一百台机器却需要二十个管理员。我还是希望是一个管理员或者两个管理员。管理开销。

很多情况下,伸缩性是综合的,并不是单一的。举个例子,我把规模扩大十倍,它有伸缩性,它其实并不仅仅指能够应对十倍的用户流量,它还指整个的运维开销也是合理的,不能没法运维了。

2.1.1.2 Performance

  • 系统使用单位资源和时间,能够完成的有用的工作量。

  • 请求的低延迟

  • 高吞吐量

  • 资源的低占用

性能是指系统使用单位资源和时间,能干多少事,干得越多,性能越高。

性能体现在系统处理:

  • 低延迟:延迟要低,性能好,跑得快。延迟涉及的东西很多,不是简单的CPU快就行了。单个处理的反应。
  • 高吞吐量:批量处理。一小时能处理一万个请求,一小时能处理两万个请求,吞吐量就是一倍。
  • 资源的低占用:处理同样的东西,资源占用低。一小时处理一万个,CPU只有20%,内存只有10%。另一个可能CPU有50%,内存有50%,这个性能就低。

这也是我们希望达成的目标。问题在于是有trade off,有权衡的。如果想降低延迟,吞吐量一般要下降,很难去批量处理。这样的话,开销就大,系统切换,处理完这个,处理下一个,批量处理一下就处理完了。必须要根据系统的目标,需要一个低延迟的系统,还是一个高吞吐量的系统,必须要做权衡,然后不停地试。而不是说,没考虑这些问题,突然发现延迟很大,这个时候就很难解决了。

2.1.1.3 Availability

  • 系统正常工作占用的时间比例。如果用户无法访问系统,那么系统就是不可用的。

  • 影响因素:

    • 系统自身
    • 网络
    • 依赖的第三方服务

可用性也是我们的目标,是系统正常工作占用的时间比例。简单来讲就是这样,还有平均无故障时间、平均修复时间等,本质上一样,只是更专业一点。

可用性的影响因素很多。系统自身Bug要少、重启要快、要有备份,这都是系统自身的设计。但考虑可用性的时候,要把系统放到端到端整体考虑,不要仅仅只包含系统自身。因为用户使用的系统是端到端的,其他的东西可能可用性很低,系统自身可用性再高都没用。

老师以前编过一个系统,当时部署在机房里,用的是电信宽带。后来发现电信用户跑得很快,联通用户跑得很慢,这个系统怎么解决呢?也不是系统自身的问题,只能出两个口:一个电信口和一个联通口,哪个用户就走哪条路。

所以可用性要整体、端到端考虑,不要仅仅只考虑自身。包括现在第三方服务,可用性可能很低,系统可用性也会很低。所以要先看一下,如果系统依赖的环境不可改变,比如可用性只有90%,在刚开始就不要投入很大精力去把可用性提升到超过90%,没意义。商业软件开发是一个经济活动。但是你要知道如何提升可用性,只是现在不做而已,做了也没有意义,反而增加很多开发成本和维护成本。

以上是我们在分布式系统期望达到的目标:可伸缩、性能好、可用性高。

2.1.2 面临的约束

既要又要,这话说出来很可怕。我们生活在现实世界里一定有很多约束。没有约束,就没有创造,就没有意义、没有乐趣了。约束是好事情,不见得都是坏事情。约束激发斗志、想象力和创造力。

  • 节点的数目
    • 随着计算和存储能力要求的增加而增加
  • 节点间的距离
    • 信息传输的延迟

在理想情况下,只要加机器就行了。但是在现实世界里,需要的计算能力越多,需要的存储空间越多,需要增加越多的节点。一个节点出问题的概率是1%的话,一百个节点铁定要出问题的。因此,考虑Crash是设计的第一要素,必须基于会出问题去做设计。如果你觉得不会出问题,你的软件肯定不会容错。

随着节点越来越多、节点间的距离越来越大,延迟越来越大,这是要考虑的。不能说,信息传输很快,不会出问题,最多1秒就传到了,但可能是2秒、5秒、10秒,也可能永远没反应,必须要考虑这些问题。

这些约束是物理现实的约束,其实给我们提供了一个设计空间。我们做分布式系统设计的时候,空间边界在什么地方,我们做的是哪一点,控制到哪一层。

2.1.3 约束的影响

看一下约束的影响。

  • 节点越多,系统出现故障的概率就越高
    • 降低可用性、增加管理成本
  • 节点越多,节点间的通信就会越多
    • 降低性能
  • 地理位置越远,节点间的最小通信延迟就越大
    • 降低性能

节点越多,系统出现故障的概率就越高,可用性就降低了,管理成本高了。这就是代价。我们希望系统可以伸缩,但是问题就来了。

节点越多,节点需要的协调就越多,节点间的通信就越多,性能就降低了。

地理位置越远,节点间的最小通信延迟就越大。可能一个机房放不下,放两个机房。

这是约束给我们带来的影响。我们最大能做到什么程度,这是有一个理论研究的。但这个理论研究对实际设计意义不是非常大,那个极限我们也达不到,研究多少规模的情况下达到极限,再多就不行了。当然,可以通过根据特定问题的一些权衡去规避掉。很多结论都是通用的结论,并不是说做不到,而是说要舍弃一些东西才能做到。如果舍弃的东西跟问题无关,我就可以这样去设计。

以上是我们的目标、约束、约束给我们带来的影响,这些东西是我们的边界,后续在这里面去考虑我们的问题。

2.2 抽象与模型

  • 通信模型 (异步/同步)
  • 故障模型 (崩溃/分区/Byzantine)
  • 一致性模型 (强一致、最终一致)

我们要研究一个现实问题,分布式系统是一个现实问题,很多机器连在一起,像其他学科一样,一定要有个模型做抽象。我们要把现实里跟问题无关的东西抛弃掉,把有关的东西留下来。用什么网卡、用光纤还是无线,都不用考虑,但是计算机能不能持久化存储,要考虑。研究的问题跟抽象的方式有关,它决定了哪些能干、哪些不能干。如果系统不能抽象成模型,好多东西是做不到的。

首先要对分布式系统做抽象,哪些东西能抛掉,哪些东西不能抛掉。用这些抽象好的概念搭建一个模型,这个模型是现实的一个抽象的对应物。因为模型已经去掉无关细节了,比现实要简单,但是跟问题是高度相关的,现实的很多复杂性我们不需要。然后,在这个模型里去研究我能做哪些事情、不能做哪些事情,给我一个结论。

对分布式系统来讲,一般抽象会这样来做:

  1. 通信方式是同步还是异步

这里的同步和异步,简单来说,是指有没有一个全局同步时钟。每个节点上的执行体每一步时间都是一样的。但这个不是关键。关键在于,节点之间通信的时候,消息是不是有上界。假设消息是2秒,超过2秒没收到,可以断定节点Crash。这个假设可以在同步模型里做出来。但是在业务模型里没有一个全局时钟。通信也一样,发消息以后,延时不确定,可能会丢掉,可能永远不返回,这时,我就没法判断对方是Crash还是包丢了。

可以看到,同步模型其实是一个理想模型。在纯的异步模型里,你不能使用定时器。现实系统是一个混合模型,可以使用定时器,但是5秒钟不回消息就认为对方Crash是一个概率性的,它不是一个0和1的问题。现实系统是一个具有概率性质的半同步、半异步模型。这就很麻烦了,为什么难就在于此。定时器设得长还是短呢,2秒、3秒可能都不合适。很多开源软件里都会告诉你百分之多少概率Crash、百分之多少概率消息丢了。

  1. 故障模型

节点Crash,重启是最干净利落的,重启完之后肯定正常工作。

分区,就是网络故障,网络会不会分区、会不会故障,网线会不会断等。

Byzantine,拜占庭故障,这个故障并不是Crash重启,它会捣乱、会做假。它给我发错误的包,告诉我虚假消息。在这个模型下,就很麻烦,很多问题就很难解决了。只有在安全性要求很高的,例如金融等领域,才会有这种故障模型。在大部分领域里,都不会有这种模型,解决问题的算法非常非常复杂,开销成本很高。

Faye:当网络由于发生异常情况,导致分布式系统中部分节点之间的网络延时不断增大,最终导致组成分布式系统的所有节点中,只有部分节点之间能够进行正常通信,而另一些节点则不能——我们将这个现象称为网络分区,就是俗称的“脑裂”。

  1. 一致性模型

复制之后,能否假设这些副本之间永远是强一致的。只要写下一个数据,立马读出来就是这个数据,在另一个节点读出来。这些都叫假设。

你在做设计之前,必须要把这些搞清楚,基于这些模型我们才能判断哪些事情能做,哪些事情不能做。

有一部分书里讲的算法和设计基于同步模型,对实际参考意义不大。同步模型是一个简化模型。它的好处在于,很容易分析问题,很容易判定系统模型的性能。因为有一个同步时钟,要么收到要么Crash,可以很简单地判断。

一致性模型并不是一个0和1的分界,也是一个谱线。

一般情况下,我们会认为:

  • 具有概率性质半同步、半异步的通信模型。我可以用定时器,但定时器不准确。
  • 崩溃/分区的故障模型。没有捣乱、告诉我虚假消息。
  • 自己选择一致性模型。

Faye:从这里开始往下,这篇笔记对应的视频没有了,只能根据课堂笔记和查找的资料整理一下。

2.3 分布式计算的误区

2.3.1 误区

把脑子里的想法显式化,慢慢能力就提升了。

能不能把多台机器看成一台机器(有人帮我做好了,Wishful Thinking)。这个假设提供了很好的语义,但是不满足业务目标,伸缩性、可用性很差。

错误的构建原语,语义是假象——CORBA、EJB等很难用(很好的抽象,但不好用)。

fallacies of distributed computing

详见 https://en.wikipedia.org/wiki/Fallacies_of_distributed_computing

以上都是错误的假设。

做模型,既要考虑给使用者提供尽量清晰的语义,又要保留分布式系统中最关键的特点。

只有解决问题和不解决问题,没有好坏。

同步 -> 消息、定时器、重试、隔离

2.3.2 分布式领域中导致麻烦的根本

  • Delay
  • Concurrency:并发系统中系统行为太多了
  • Partial Failure:局部失败,行为空间急速扩大

2.3.4 《面对软件错误构建可靠的分布式系统》

paper of joe armstrong

分布式系统设计必读之一。

既然这些问题不能抽象掉(延时、并发等),不盖起来也不是不管,而是提供一个工具箱(原语)专门解决这些问题。

在分布式系统中,如何提炼原语给程序员用。

如何从问题出发,问题有哪些假设,有哪些需求,... DDD/DSL。

2.4 分布式系统两种基本技术——分区和复制

partitioned and replicated

  • 分区:扩展能力
  • 复制:可靠性

2.4.1 分区(Partition)

  • 提升性能
    • 限制处理的数据规模
    • 相关数据放在同一分区
  • 提升可用性
    • 不同分区隔离,互不影响
  • 应用相关
    • 主要数据访问模式
    • 跨分区访问的低性能操作
    • 均衡性

分区和应用高度相关。

一致性哈希算法:任何一个值都可以均匀哈希到0-1的值。

2.4.2 复制(Replication)

  • 提升性能
    • 新的数据拷贝、新的计算能力和带宽
  • 提升可用性
    • 冗余备份

复制并不仅仅为了提高可靠性,也可以提升性能,还可以提升可用性。

  • 一致性问题
    • 清晰语义 vs. 业务设计目标

抽象层次过高,语义很清楚,但是满足不了业务设计目标,所以不能太抽象。抽象层次过低,语义不清楚,很难用,会出Bug。

选择抽象层次,抽象到什么程度。

2.4.3 抽象的层次

  • 系统模型
    • 一组关于环境和工具的假设,分布式系统构建于这些假设之上
      • 节点的能力和失效方式
      • 通信链路的工作和失效方式
      • 时间和顺序

假设:

  • 节点能持久化存储,Crash后重启,不会错乱。
  • 异步为主,可以用不准确定时器的模型。
  • 时间和顺序:有没有全局时间、有无顺序。

node link fault

节点失效或链路失效。

如何在抽象系统里理解复制、分区。

建模的目的是为了研究问题。

2.5 Consensus Problem

consensus problem

详见 https://en.wikipedia.org/wiki/Consensus_(computer_science)

共识问题:N个节点,每个节点提一个值,通过一个方法,达成一致。注意,目标是达成一致。

  • Safety:正确性属性。

  • Liveness:最终能正常工作。

  • Termination说明了Liveness属性。

  • Integrity和Agreement说明了Safety属性。

数据一致性、复制一致性,分布式系统里所有需要协调的问题都是共识问题,所以是基石。

2.6 FLP Impossibility

论文 https://groups.csail.mit.edu/tds/papers/Lynch/jacm85.pdf

flp impossibility paper

中文参考:

Faye引用:在异步通信场景,即使只有一个进程失败,也没有任何算法能保证非失败进程达到一致性!因为同步通信中的一致性被证明是可以达到的,因此在之前一直有人尝试各种算法解决以异步环境的一致性问题,有个FLP的结果,这样的尝试终于有了答案。

异步系统:发了消息,不知道对方死活。发了消息,没法确定是否crash。

Safety和Liveness不能兼得。要么结果没错但一直不结束,要么结束但结果是错的。

FLP Impossibility提供了边界。

Paxos为什么可以?

Paxos也是共识问题。理论和工程的区别,理论上不可行,工程上可以规避。出问题的可能性很小,工程上可以接受。理论上,Paxos可能不会终结(放弃Liveness,保证Safety),通过Random算法把概率降低,保证现实中可用。

Faye引用:Paxos算法的场景比FLP的系统模型还要松散,除了异步通信,Paxos允许消息丢失(通信不健壮),但Paxos却被认为是最牛的一致性算法,其作者Lamport也获得2014年的图灵奖,这又是为什么?其实仔细回忆Paxos论文会发现,Paxos中存在活锁,理论上的活锁会导致Paxos算法无法满足Termination属性,也就不算一个正确的一致性算法。Lamport在自己的论文中也提到“FLP结果表明,不存在完全满足一致性的异步算法...”,因此他建议通过Leader来代替Paxos中的Proposer,而Leader则通过随机或其他方式来选定(Paxos中假如随机过程会极大降低FLP发生的概率)。也就是说Paxos算法其实也不算理论上完全正确的,只是在工程实现中避免了一些理论上存在的问题。但这丝毫不影响Paxos的伟大性!

2.7 CAP

cap theorem

详见 https://en.wikipedia.org/wiki/CAP_theorem

英文参考:

中文参考:

cap 1

cap 2

  • 在CAP里,这里的一致性是指强一致。
  • 可用性:任何时候都可以读写。
  • Partition Tolerance:链路会Crash。

三选二,不能兼得。

  • 2PC:两阶段提交协议,假设不会分区。
  • Paxos:认为会分区,牺牲一点可用性。
  • Gossip:放弃强一致。

假设节点不会Crash、链路会Crash。

网络分区是一定存在的。大部分情况下,网络分区是必选项。某些情况下网络分区的可靠性高于节点可靠性。

一致性不是0和1的问题,而是一个谱线,要多少一致性,多少可用性。

业务需要什么,一定要理解业务。

Faye引用:在CAP原理中,分别说了C(Consistency 一致性)、A(Availability 可用性)、P(Partition tolerance 分区容错性)。在分布式系统中很难做到三者兼顾,一般我们只能做到其中两者完善。

  • C(Consistency 一致性):任何操作都是原子性的,后面的时间发生都能看到前面时间的结果,又或者说,任一节点上所看到的事件都应该是一样。
  • A(Availability 可用性):在有限的时间内,任何非宕机节点都能应答。
  • P(Partition tolerance 分区容错性):网络可能会发生分区,即节点间的通信无法保障时,整个网络还是可以容忍这部分分区存在的。

所以,在网络分区的时候,系统是无法同时保证一致性和可用性的。要么,节点收到请求后因为没有收到其他节点的确认而不作应答(牺牲可用性);要么,节点只能应答非一致性的结果(牺牲一致性)。

由于当前的网络硬件肯定会出现延迟丢包等问题,所以分区容忍性是我们必须需要实现的。所以我们只能在一致性和可用性之间进行权衡。

2.8 Consistency

  • Strong consistency
    • Linearizable
    • Sequential
  • Weak consistency
    • Client-Centric
    • Causal
    • Eventually

详见 https://en.wikipedia.org/wiki/Consistency_model

中文参考:

不是强一致,就是弱一致。

  • 弱一致性
    • Client-Centric
    • Causal:因果关系,B的发生是因为A导致的。A2导致A5,A2在A5之前就行了,其他顺序不关心。
    • Eventually:最终一致性,比较含糊,最终是多久,最后以谁为准。

根据业务做谱线。顺序要求越强,性能就越低,要根据业务做权衡。

FLP、CAP提供了边界,在框架内做选择,要知道放弃了什么。

2.9 Time and Order

  • Global Clock
  • Local Clock
  • No Clock

顺序很重要。程序为什么对,修改状态的顺序是对的。为什么不对,顺序不对。并发为什么难?并发顺序多。

单进程里,顺序是自然保证的,不用关心。分布式系统里,顺序是要考虑的,要保证顺序,就要做协调,需要成本。不要顺序,并发性高,但可能出错。

时间是保证顺序的手段。

  • 定时器:关心时间的间隔,不关心具体时间。
  • 语义解释:几点几分几秒,系统Crash。

单进程是全序的,还有偏序、部分有序。全序很难达成,且影响性能。部分序需要权衡,跟业务相关。

global clock

local clock

假设需要精确算法,两个时钟都不能用,所以有下面的时钟。

vector clock

详见 https://en.wikipedia.org/wiki/Vector_clock

没有全局时间。不需要真实时间,只需要逻辑时间轴。建立因果关系,是部分序的。

2.10 Failure Detector

  • Strong Completeness
    • Every crashed process is eventually suspected by every correct process.
  • Weak Completeness
    • Every crashed process is eventually suspected by some correct process.
  • Strong Accuracy
    • No correct process is suspected ever.
  • Weak Accuracy
    • Some correct process is never suspected.

详见 https://en.wikipedia.org/wiki/Failure_detector

英文参考:

告诉我们边界。

强完备性、弱完备性、强精确性、弱精确性。

ZAB:Zookeeper Atomic Broadcast

在检测错误的时候,假设是什么,如果能这样检测错误,能够达成什么样的结果。

problem solvability

2.11 Replication

复制有很多协议,关注模式:同步和异步。

2.11.1 Synchronous

replication synchronous

同步:最强的可靠性、性能差。S1要等到S2和S3都复制完,通知S1后才能结束。

2.11.2 Asynchronous

replication asynchronous

异步:快,可靠性降低。S1发了就完了。

2.11.3 Strong Consistency

  • 1n messages (asynchronous primary/backup)
  • 2n messages (synchronous primary/backup)
  • 4n messages (2-phase commit, Multi-Paxos)
  • 6n messages (3-phase commit, Paxos with repeated leader election)

N台机器像一台机器一样。越往下一致性越强,消息量越大。

2PC:先准备,都准备好了再写。如果事务协调性和节点同时死了,就有问题了。

真正强一致的算法只有一个——Paxos。

2.11.4 Weak Consistency

  • Eventual consistency with probabilistic guarantee
    • Partial quorums
  • Eventual consistency with strong guarantee
    • CRDTs (convergent and commutative replicated datatypes)
    • CALM (consistency as logic monotonicity)

概率性地保证最终一致性,交给客户来处理了。

CRDTs:根据领域,关注数据类型操作,如果数据类型满足交换律、结合律和幂等,顺序就没关系了,那么定义这样的数据结构就可以了。增量至少满足交换律。集合的交和并满足交换律、结合律和幂等,这样可以放松顺序,性能就提高了。

CALM:SQL语言里没有顺序,但表达力有限。SQL是一种计算模型。根据业务设计一个专用的计算模型,不用考虑顺序。

2.11.5 Summary

replication summary

  • Quorum:Paxos、Raft
  • 左边异步、右边同步

3 分布式系统设计要素

一切都是权衡,要知道权衡的结果,这是学习的目的。

3.1 重试策略和数据共享策略

retry policy and data share policy

3.1.1 期望的性质

  • 一致性
  • 可靠性
  • 伸缩性
  • 可用性

选择的基础就是前面讲的东西。

3.1.2 消息投递语义(重试策略)

  • At Most Once
  • At Least Once
  • Exactly Once

Exactly Once很难满足,但“At Least Once + 幂等”效果上可以满足。

中文参考:

3.1.3 数据共享选择(复制策略)

  • Share Nothing
  • Share Everything
  • Share Something

Share Nothing:复制一切

提高可靠性。

data share 1

data share 2

data share 3

3.1.4 权衡(可用性)

trade off message and data availability

对业务进行分析后,选择跟业务目标匹配的方案。

一个有用的系统一定是有状态的,只是状态在哪里。能力强的小团队把困难的做完。分布式存储系统难做(有状态、数据复制)。

3.2 容量规划

  • 了解业务节点对资源的要求,优化成本
  • 确保系统符合设计负载
  • 发现、消除瓶颈、建立平衡的系统
  • 确保系统在部分节点故障时依然可以应对设定的负载

3.2.1 Amdahl's Law

amdahl law

详见 https://en.wikipedia.org/wiki/Amdahl%27s_law

3.2.2 权衡(伸缩性)

trade off message and data scalability

3.2.3 各种测试

tests

  • Soak测试:基于平台的系统可靠性
  • Stress测试:找瓶颈
  • Spike测试:可恢复性
  • Load测试:高负载

3.2.4 A Balanced System

system with bottleneck

会有局部点占用过大。

system without bottleneck

一个平衡的系统,高负载的情况下,吞吐量基本不变,延迟会大。长时间过高的话,需要反压、流控。

系统的伸缩性和能力是由系统中串行部分决定的。要找到系统的瓶颈,并行化。把不是系统固有的瓶颈,并行化,消除掉。

系统内在顺序保留,其他瓶颈全部消掉,提升系统伸缩性。

Faye:从这里开始往下,这篇笔记对应的视频又有了。

3.2.5 过载控制

little law

详见 https://en.wikipedia.org/wiki/Little%27s_law

我知道水的流速、水在管子里面的时间,肯定可以知道管的长度。同理,如果系统是一个稳态系统,我知道请求的负载情况,也知道延时(请求在系统里停留的时间),肯定可以知道队列长度。如果希望延时大一点,就要队列长一点;希望延时小一点,就要队列短一点。稳定的系统,进出一样。平衡系统,通过调整队列,控制响应时间。

做分布式系统设计的时候,微服务系统一样,队列是非常重要的概念,是first-class设计要素。队列无处不在,不在这儿,就在那儿,可能是个显式队列,也可能是个隐式队列,我们一定要把隐式队列显式化。把队列显式化才能容易看出瓶颈,改后容易看到效果。一般来说,瓶颈一定是反映在队列长度上,只是队列可能是隐式的。因此能够很快地对系统目前的负载情况、处理是否有效等做一个判断。

来看一个例子。

overload control 1

有个用户发请求,系统处理请求,有个Cache,调用服务API,可以认为访问数据库,Cache就是把数据库的数据做一个Cache备份。目前几乎所有系统都是Reactive系统(系统的行为是由外部刺激的),上网、打开手机App,包括电信系统里的打电话,摘机、听到拨号音等,单纯的批处理系统很少,目前跟人交互的都是Reactive系统。一个用户请求来了,怎么处理呢?一般的设计方法是,有一个线程池,可以配置线程的多少,线程池接收用户请求,线程拿到用户请求以后,先查Cache有没有,有的话直接返回,没有的话,调用服务API查询数据然后返回。

线程池多大合适?太大,浪费资源,太小,处理能力可能会降低整个系统的性能,好多CPU利用不了。假设每秒500个请求,服务API这部分响应时间是30毫秒(一个请求在系统里待30ms),根据Little's Law,系统队列长度是多少?0.03 * 500 = 15。没有显式队列就有隐式队列,队列在什么地方呢?线程池里的15个线程。

overload control 2

基于线程的队列是隐式的,你看不见,不好衡量和控制。要控制一个系统的行为,就希望把队列显式化,这样就能看到排队的15个线程,可以根据增加处理能力的要求让队列短一点,或者发现队列很长,系统已经过载了,响应时间开始恶化。可以看到,只要是Reactive系统,里面一定是有队列的,只是说你是显式地把它设计出来,还是你也不知道它在什么地方。一旦把队列显式化,就可以很容易地看系统是否平衡、哪个地方是瓶颈。在系统监控与运维里,包括系统健康性,相当一部分工作就是看队列,队列越来越长肯定有地方出问题了。

假设队列很长,系统响应时间越来越慢,吞吐的性能依赖于第三方服务,怎么办呢?

overload control 3

分区。分成两个队列,这样队列就短了。

可以把队列显式化,看看系统瓶颈在什么地方,一般情况下,瓶颈一定反映在队列长度上。队列可能是隐式的,内存队列的内存暴涨,线程池线程全部吊住等,都是队列,可以通过分区的方式来处理。

通过这样一个衡量,可以明确知道系统目前的负载情况如何,改完之后是否有效。队列长度和希望的性能之间有很强的关系。

很简单的思维,但是做设计的时候,要把这些关键点都拿出来,显式化出来。微服务之间全是message queue,当然也可以REST调用,适用于不同的场合,根据业务选择。在系统的关键部件里,包括系统内部、边界和外部的队列都要兼顾。

如果发现系统过载了,怎么办?伸缩性,提升能力,但能力提升是有代价的,成本很高。云服务可以任意伸缩其实是假的,到一定能力它也伸缩不了了。这时,要做过载控制:

  • 反压:在源头上控制。你给我请求,我给你回应之后,你才给我下一个请求,把异步变成同步。
  • 卸载:假设我控制不了别人发的东西,那只能丢包、扔掉,减轻负载。

过载控制的触发需要有负载监控机制,监控队列长度的变化,告诉我负载情况。CPU是一种非常差劲的度量方式,CPU很高照样能处理得很好,CPU很低照样堵得一塌糊涂。

3.3 Operability

系统上线之后都要运维,DevOps,讲一些里面最重要的、可能没有深入思考过的内容。

详见 https://ferd.ca/operable-software.html

operability simple architecture

系统架构如图,一个WEB SERVER访问数据库,没问题,可以理解。但如果要求运维这个系统,那这个图就太简单了,没什么用,或容易误解,对运维完全没有帮助。

operability architecture

希望看到这张图。

很多模型,包括框图,一定要知道它干什么用。要运维一个系统,不能仅仅理解我自己编写的那一部分,要把它整个依赖的环境全部纳入范围内。因为出问题的时候,不见得是你编的程序出问题。我要知道网卡,网卡驱动的结构,里面有RING Buf,要中断告诉CPU,有协议栈,有POLL SET(文件描述符,Socket上事件的订阅和发布),有TCP,同步QUEUE,ACCEPT QUEUE。然后画条线,上面都是内核空间,下面才是用户空间,才到我的系统里。

上线系统,必须要看到这张图,才可能把系统运维好。可以说,系统上线之后,绝大部分问题都是系统性问题,用户量大了以后,要调优TCP协议栈,要把内核的驱动优化一下等。这些跟我自己的系统没关系,但我要知道问题的存在。对于运维来说,不能简单地只理解自己的系统,要看整个运行环境。

operability layer abstract

如果用抽象层次来看的话,看到这张图。APP是我们自己编的,可能用到FRAMEWORK、LIBS、标准库(STDLIB)、编程语言(LANG)、硬件(HARDWARE)、DRIVERS。这些东西,每一层都给我提供了一种抽象。运维的目的是保证系统正常运行。如果系统出了问题,我知道问题是什么,怎么解决。如果系统目前没出问题,我最好能够根据目前的数据来判定系统是不是要出问题。而且这些行为是在各个层次上的,有应用的行为、操作系统的行为、Driver的行为、FRAMEWORK的行为等。我们的运维,希望在每一个不同的抽象层次上去理解系统的行为,而不是简单的一些指标。

什么是行为?行为就是状态的变化。每一个层次的状态是不一样的。在驱动层次上看到的、每个包什么业务,我不关心,在APP上看到的是业务的状态。每个层次上都有自己的抽象层次,每个抽象层次上都提供这个层次上的粒度的状态和状态的变化。在高层看到的状态,步骤很大,在底层看到的状态,步骤很小。举个例子,用C语言编个程序,在C语言层面上看到的都是定义的结构、函数,但在汇编层面上没有结构的概念,再往底层完全是bit。同理,每个层次都提供这个层次的抽象,越往下走,语义层次越低,解决问题的能力越大,但花的时间越多,因为语义层次低。越往上走,语义层次越高,解决问题的施展空间越小,但是越限定,越好理解。所以,我们解决问题都是从上往下解决,不会上来就抓包,抓包肯定是最底层的,先看行为,实在解决不了,再抓包看看。每个层次都要提供合适的工具,工具的目的是提供理解这个层次行为的手段。log是很重要的一种方式,log就是要把这个层次的行为展现出来,供别人理解,而不是随便加的。你有了这个层次的行为,有了自己的一些假设和属性的判断,看属性是否满足。TLA+是我们理解一个抽象模型行为的一个很重要的手段。学习TLA+对做设计的好处在于,它能够帮我们更清晰地建立每个层次系统的抽象行为,对运维也是一样。

举个例子,看这幅地图:

operability map 1

什么是地图?我们有一个真实的世界,地图是一个模型。我们有一个真实的系统,我们脑子里有一个系统的概念,是个模型。地图各种各样,这幅地图够详细了,跟真实也有区别。同理,我们脑子里再详细地理解我们的系统,其实也是有区别的,只是个模型。那真实的地图有用吗?

operability map 2

这是旅游地图,有用吗?

operability map 3

这是地铁地图,有用吗?

地图是否有用,要看目的。同理,模型一定要知道服务于谁。我们做DevOps也一样,一定要知道这些DevOps工具、提供的这些log给谁用,不同的人希望看到不同的东西。地图也一样,如果只是想旅游的话,地铁地图肯定是最好的,但这个地图跟真实差别也是最大的。第一幅地图跟真实最接近,但最没用,后两幅地图都跟真实差别很大,但最有用。模型一定要服务于你的目的,你要知道你这个东西给谁用的,在TA的观念里,哪些行为的展现方式是TA所关心的,是对TA解决问题有用的,这很重要。我们开发系统和做运维一定要提供这样的东西,不能随便打个log就结束。

operability devops tools

一个系统上线之后,很多人关心这个系统。开发人员要代码入库,有人关心这部分数据,要做CI、CD、Pipeline,这个过程也会产生数据,有人关心度量数据,有人关心告警数据,每个人关心的都不一样。即使关心告警数据的人,可能目的也不一样,有些可能是定位问题的,有些只是看一下系统的运转情况。系统上线之后,想让系统能够让不同的人能够解决不同的问题,DevOps工具一定是各种各样的。不同的人有不同的模型,模型是把系统里TA关注的那些点放大,不关注的点抛掉,可能跟真实不相干,但没问题,只要解决问题就行。这是一种很重要的思考问题的方式。

operability log

假设一个程序跑起来之后,它的行为看不见摸不着,出问题也不知道,怎么知道它的行为?打log。log本质上可以认为是给没有窗户的房子上加窗户。如图,我们的程序就是这样,到处都是窗户,因为看不见,加窗户看一看。可以认为,这就是我们目前程序的可视化,一个黑箱运行的程序到处都是窗户,大家不觉得难看是因为看不见,现在觉得难看是因为看得见了。那么,加多少窗户合适呢?

operability apple headquarters

透明的窗户,所有都是窗户,一切都可以打出来,这是一种方式。注意,一定要有抽象层次。抽象层次越低,数据量越大,但代价越高,因为海量数据查找有效信息很困难。而且每个抽象层次上都有自己专用的工具,不会把系统里的包全log出来,没意义,因为有抓包工具。一定要在每个抽象层次上打造相应的工具给别人用,哪些工具已经有了,就直接用,哪些工具没有,就要提供,把它们串成工具链,然后提供相应的培训。

既不能乱加,也不能全透明,一定要有抽象层次,在每个层次上提供一个工具,能够看到这个层次你希望看到的、跟语义相匹配的行为,提供工具让TA能看到因果关系,出现这样的行为导致什么问题,出现那样的行为导致什么问题。

operability operator view

如何做一个可运维的软件:

  • 首先,不能光考虑系统本身。
  • 其次,要考虑各种抽象层次,在每个层次上要考虑谁来用,有没有已有工具,有就用,没有就自己打造。

4 Summary

  • 数据分类 (Essential/Derived)
  • 隔离 (自治)
  • 异步消息 (协议与格式)
  • 重试策略 (At Least Once/At Most Once/Exactly Once)
  • 数据共享策略 (Nothing/Something/Everything)
  • 容量规划
    • 容量测试、消除瓶颈,平衡、反压和负载管控
  • 集群蓝图
    • 节点类型、资源要求、网络结构、API、节点比例关系
  • 运维
    • 业务、系统健康性指标,业务质量指标,系统、业务行为log,告警
  • 自动化、智能化
    • 理解指标和系统、业务情况的关系,建立自动化闭环

系统思维是我们都在做的事情,例如微服务,如何去做系统思维?考虑数据、分类、分层。考虑崩溃,目的在于希望系统的一些普通部件崩溃不影响,系统还能活,以这种方式出发去分割系统。考虑协议,它们之间的语言,就像程序思维考虑函数调用、面向对象方法调用一样。考虑容错机制,重试策略、数据共享策略。通过容量规划、各种测试,找到瓶颈、节点比例,得到一个可伸缩的系统。前提是要通过对业务的理解,把业务里不必要的顺序化全去掉,顺序化多一点点,整个伸缩性就会打很大折扣。可能换个角度理解业务,就不需要串行。在业务层面上一点点深入的探讨,得到的成果要远远大于采用新的技术、新的工具方法。回到业务层面是根本,包括重试策略和数据共享策略完全是跟业务需要有关系的。运维有很多指标,要考虑各种抽象层次。智能化,可以从运维数据里发现模式(Pattern),Pattern基于Action,Action再反馈回来,再形成闭环。智能化运维,这是一个美好的愿望,作为一个这么高智能的人对大系统的运维尚且有很多困难,怎么可能把困难交给机器,不可能的,最多辅助一下。微信的运维没有什么智能化,包括负载控制、流控,也没有智能化,就用了一个很简单、很不智能的算法,但效果非常好。

一切都基于了解分布式系统,有哪些基本问题,边界在什么地方,可以做哪些选择,哪些能,哪些不能,我想要什么,我可以放弃什么,我能做哪些补偿,这些是我们能设计一个好的系统要考虑的问题以及必须具备的基础知识。

5 Papers

  1. Fallacies of Distributed Computing, Fallacies of Distributed Computing

https://pages.cs.wisc.edu/~zuyu/files/fallacies.pdf

  1. A Note on Distributed Computing, Jim Waldo

https://www.cc.gatech.edu/classes/AY2010/cs4210_fall/papers/smli_tr-94-29.pdf

  1. Making reliable distributed systems in the presence of software errors, Joe Armstrong

http://erlang.org/download/armstrong_thesis_2003.pdf

  1. On Designing and Deploying Internet-Scale Services, James Hamilton

https://mvdirona.com/jrh/TalksAndPapers/JamesRH_Lisa.pdf

  1. End-to-End Arguments in System Design, J.H.Saltzer

http://web.mit.edu/Saltzer/www/publications/endtoend/endtoend.pdf

  1. There is No Now Problems with simultaneity in distributed systems, Justin Sheehy

https://queue.acm.org/detail.cfm?id=2745385

  1. Convenience over Correctness, Steve Vinoski

http://steve.vinoski.net/pdf/IEEE-Convenience_Over_Correctness.pdf

  1. Overload Control for Scaling WeChat Microservices, Hao Zhou etc

https://www.cs.columbia.edu/~ruigu/papers/socc18-final100.pdf

  1. Data on the Outside versus Data on the Inside, Pat Helland

http://cidrdb.org/cidr2005/papers/P12.pdf

  1. Life Beyond Distributed Transactions, Pat Helland

https://queue.acm.org/detail.cfm?id=3025012

  1. Building on Quicksand, Pat Helland

http://www-db.cs.wisc.edu/cidr/cidr2009/Paper_133.pdf

  1. Immutability Changes Everything, Pat Helland

http://cidrdb.org/cidr2015/Papers/CIDR15_Paper16.pdf

  1. Highly Available Transactions: Virtues and Limitations, Peter Bailis

http://www.bailis.org/papers/hat-vldb2014.pdf

  1. Sagas, Hector Garcia-Molina and Kenneth Salem

https://www.cs.cornell.edu/andru/cs711/2002fa/reading/sagas.pdf

  1. Distributed Systems for fun and profit

http://book.mixu.net/distsys/index.html