分布式系统笔记(一)可靠性,可拓展性和可维护性

最近看了一本好书:Designing Data Intensive Applications。这本书条理清晰地分析了分布式系统的设计, 各种 trade off,各种 corner case,很适合我这种初学者。 分布式系统笔记系列将会根据这本书的框架,对分布式系统的相关知识进行归纳。

要对分布式系统的不同设计进行分析,首先就要知道如何衡量两种设计哪个更好。一般从可靠性,可拓展性和可维护性三个方面来进行分析。

可靠性

可靠性指的是系统能在多大程度上容错。分布式系统考虑到的错误主要来自于硬件故障,软件错误和人为的操作失误,而恶意的网络攻击一般不在讨论范围内。

硬件故障一定程度上可以认为是随机的,不同设备没有相关性。由于大型分布式系统的节点数量往往成千上万,有机器出现硬件故障是非常常见的。IBM 曾经发表过一个利用机器学习预测硬盘故障的研究,用这种思路可以帮助运维团队及时发现并更换可能故障的设备。

软件错误就是平时说的 bug, 这种错误是系统性的,且大部分 bug 是可重现的,因此这种错误的危害可能遍及整个系统,一旦发作危害很大。预防软件错误主要靠现代软件设计的方法,比如代码审查,完整的测试,解耦等等。有时我们可以选择采用一些随机算法来提高性能,这也会引入一定的错误,需要顶层有对应的容错机制来解决。

人为的操作失误主要是开发/运维成员误操作导致的事故。有名的例子有 gitlab 删库事件。要防止这类事故,应该尽量避免运维在命令行界面操作,并进行合理的权限管理。为运维提供图形界面进行规范的管理,监控不仅可以降低失误,还能提高系统的可维护性。

提高可靠性的方法很多,除了以上的预防措施之外,最基础的是做好故障检测和数据备份,使得错误发生时能做到及时诊断,及时恢复。之后会提到数据库的 WAL 和数据备份,调度系统的心跳检测和 fail over,都体现了这种思想。

可拓展性

可拓展性是指系统应对负载增长的能力。根据应用的不同,负载有很多衡量方式,比如网游的负载可能是指在线人数的峰值,微博的负载可能是指每天的新微博数,小说网站的负载可能是指小说的总数。在负载增长后保持性能的代价,就反映了这个系统的可拓展性。

系统的性能主要通过客户端观察到的响应时间来衡量。这里有个叫做 percentile 的概念。一般我们关心的是最慢的 1%的响应时间,对应的 percentile 就是 p99。Amazon 使用的衡量标准是 p99.9,也就是最慢的 0.1%的响应时间。造成这些响应时间较长的原因有很多,可能是用户做了一个复杂的操作,网络故障,系统繁忙,结点恢复或者其他随机的原因。高 percentile 的衡量使我们的优化目标主要放在最慢的那一部分操作上,比较适合购物网站,即时通信等用户对响应时间要求较高的情况。在 OLAP 和一些批处理系统中,则会将 percentile 设的低一点,甚至直接用平均数/中位数作为衡量标准。

应对负载增长主要有两类方式,一个是纵向拓展,就是换更好的机器,一个是横向拓展,就是加更多的结点。两类方式都没法无限制的使用,硬件条件越好价格增长越快,结点数越多系统越复杂,实践中经常两类方式混合使用。

下面举两个例子说明不同业务对分布式系统架构性能优化的影响。

Twitter 的例子

一个很经典的例子是 Twitter 的系统。根据 Twitter 在 2012 年公布的数据,Twitter 的负载主要由两个操作影响:第一个是发布推特,平均每秒 4.6k 次请求,峰值每秒 12k 请求;第二个是获取关注者发布的推特,平均每秒 300k 请求。

实现这些功能可以用两种架构,Push 架构是为每个用户建一个 mailbox,每当用户发推就将推特推送到关注他的用户的 mailbox 里,获取推特时直接拉取自己的 mailbox 即可。这种架构特点是发推可能很慢,看推很快。Pull 架构是发推时直接存在数据库里,看推时查询关注用户的推特然后拼起来。这种架构特点是发推很快,看推很慢。

考虑两种操作的请求数,显然第一种架构的总体性能更好。然而,用户中存在一些“大 V”(如 Trump),这种用户被关注者很多,发推也比较频繁,Push 架构要在大 V 发推的时候给上万人的 mailbox 发信息,导致 p99 的操作很慢,而这部分操作对应的正是对 Twitter 价值很高的大 V 用户。除此之外,给每个人建 mailbox 造成的空间浪费也很严重。

为了提高大 V 的用户体验,同时也为了节省空间,Twitter 后来采用了一种混合策略:根据被关注者的数量将用户分成普通用户和大 V 两种,普通用户发推时,将消息送到被关注者的 mailbox 里,大 V 发推时直接存到数据库。看推时,在数据库查询用户关注的大 V 的推特,并跟用户的 mailbox 拼起来。

Indeed 的例子

Indeed 是做 Job Search Engine 的。根据 Indeed 的技术博客,他们存储的 Job 信息特点是:读多写少(少到几分钟一个修改操作),用户不需要做 Join 操作,数据较少,可以每个结点存下 full copy(当然也有可能是内存太大:)。

数据存内存 + 不需要 join,首先想到的肯定是用哈希表。数据更新非常少,于是采用了 snapshot+change log 的存储形式。好处是 snapshot 可以异步生成,修改操作非常快,查询操作需要多查一个 change log,这个 log 可以用平衡树之类的结构维护,因为 log 的 size 很小,考虑到可持久化哈希表能做的优化,代价还是可以接受的。

剩下的优化都在可持久化哈希表上了。那篇博客主要讲的就是用 mph 把哈希表存在数组里。mph 是指找到一种哈希方式,把 n 个 key 映射成 0 到 n-1 的整数。这样就不会存在哈希冲突,哈希表的空间也可以大大节省。还可以不存 key,直接用另一个哈希做校验,将容错的任务丢到上一层,也能在一定程度上节省空间。

可维护性

软件最大的成本往往不是开发成本,而是后续的维护成本。衡量可维护性主要有三个方面,一个是易操作,一个是易理解,一个是易修改。

易操作是针对运维团队的。运维的主要任务就是让系统顺利运行,好的运维需要做到系统监测,错误追踪,发布新版本,维护系统配置,扩容,维持生产环境的稳定以及其他的复杂任务。运维直接在生产环境工作,良好的文档,脚本和可视化界面都是必须的。

易理解要求系统结构清晰,代码易读。解耦,减少依赖,使用合适的抽象,使用设计模式都可以让系统变得更简单易读。

易修改是指工程师可以方便地对系统进行修改,近年兴起的敏捷开发就提供了很多思路,比如 TDD,重构。还有系统级别的修改,比如 Twitter 如何从 Push 架构转换成混合架构。如何在修改过程中减少错误,减少 down time 对可维护性的影响是很大的。