之前在搜索时无意中发现了这本书《Designing Data-Intensive Applications》,豆瓣评分高达9.4,于是尝试进行阅读,发现真是堪称教科书一般的存在,深入浅出的介绍了后端开发中这么多年来大热的各种技术相关知识,如从数据库语言的诞生一直到NoSQL的火热,分布式等等,由于此书包含内容很多,且无中文翻译,因此阅读起来难免会遗忘之前的内容,因此写下此笔记帮助日后复习回忆。

当今许多应用都应算为数据密集型,这与传统的计算密集型相对。因为相对于海量的数据,数据的复杂性,以及数据快速变化,CPU计算能力对这些应用来说几乎并不算限制因素。

常见的应用需要完成以下功能:

  • 存储数据,以便自身或其他应用在日后进行访问(数据库)
  • 存储复杂操作的计算结果,以便提升读取效率(缓存)
  • 允许用户以不同方式使用关键字来查询或者过滤数据(索引)
  • 以异步的方式向其他进程发送消息(流处理)
  • 周期性地处理一大批积累的数据(批处理)

但对于编写一个新的应用,开发者根本不会去编写一个新的数据存储引擎,因为对于这个工作来说,数据库已经是一个十分适合的工具了。但是现实并没有那么简单,因为当下存在很多数据库,它们拥有不同特性,不同的缓存方式,不同的创建索引方式等等。因此当我们编写一个新的应用的时候,如何去选择最合适工具以及方式来解决手头的问题仍然是我们需要解决的问题。本书从原理以及实践出发,对数据系统,以及如何使用这些工具来编写数据密集型应用进行了说明。

数据系统

我们通常认为数据库、队列、缓存等式不同类型的工具,那么为什么我们要将它们都归结为数据系统呢?

  1. 首先,近些年来许多用于数据存储和处理的工具呈现合并的趋势。比如可充当消息队列的数据存储工具Redis,以及可提供类数据库持久化的消息队列(Apache Kafka)。
  2. 其次,随着当下大量应用需求的飞速发展,单一工具已经不能再满足所有的数据处理以及存储需求。例如,假设你有一个由应用管理的缓存层(使用Memcached等工具),或是一个独立于主数据库的全文搜索服务器(如Elasticsearch),那么正常情况下应该由应用程序自身来与主数据库同步并保留相关缓存或索引,如下图所示。

当你结合多个工具来完成一项服务的提供时,其API通常会对客户端隐藏实现细节。此时你本质上是通过更小的通用型的工具来创建了一个新的特殊用途的数据系统。如缓存将会正确无效化或在数据库写入时更新,使得外界客户端得到一致的结果。此时你不仅是一个应用开发者,同时也是一个数据系统设计者。

当你设计如此一个数据系统或服务时,将会出现很多棘手的问题。在内部出错的情况下如何保证数据正确且完整?如何保证即使在部分系统降级的情况下始终如一地向客户端提供良好服务?如何扩容来满足处理负载的增加?一个优秀的API应是什么样?

有许许多多的因素来影响一个数据系统的设计,在本书中主要着重讲述大多数软件系统中最重要的三点。

  • 可靠性(Reliability):系统即使在遭遇灾难(硬件或软件错误,甚至是人为错误)的时候仍应保证正常运行
  • 可扩展性(Scalability):应存在合理的方式来处理系统的增长(数据量,流量,复杂性)
  • 可维护性(Maintainability):随着时间推移,会有不同的人于该系统上工作(维护现有任务并调整系统适应新的需求),工作人员应当能够高效的完成这些工作。

可靠性

每个人对某个东西可靠或不可靠都有不同的定义,对软件来说,对其的期望主要包括:

  • 应用程序表现出用户预期的功能
  • 其可容忍用户错误操作或使用非预期方式来使用软件
  • 在预期负载量以及数据量的情况下,其性能表现良好
  • 系统能够阻止任意非授权的访问或者攻击

如果以上代表了“正常工作”,那么我们可以大致的理解可靠性为:“即使在事情出错的情况下仍能继续正常工作”。

硬件故障

当我们讨论系统失效的原因的时候,很容易就会想到硬件故障。硬盘崩溃,RAM出错,电网停电,错拔网线。任何有过大型数据中心工作经验的人都会告诉你当你有大量机器的时候这些问题随时都在发生。

硬盘通常拥有10到50年的平均失效前时间。因此,对于一个拥有10000个硬盘的存储集群来说,我们可以计算出平均每天都有一个硬盘失效。

我们第一个应对措施便是为系统的独立硬件组件增加冗余,来减少失效率。磁盘可以通过组件RAID,服务器可以拥有双电源的热插拔CPU,数据中心可以增加电池以及柴油发电机作为备用电源。当一个部件出错,冗余部件能够及时代替出错部件工作。这种方法不能完全阻止硬件故障带来的系统错误,但是确实一个常用的且效果还不错的方式。

但是近些年随着数据量以及应用计算能力需求的增加,许多应用开始使用大量的机器,这导致了硬件故障的概率大大增加。因此在系统能容忍丢失整个机器这一点上,有了长足的进步,具体实现方式是优先使用软件层面的容错技术或作为增加硬件冗余的补充。这样的系统在操作上具有优势:一个单机系统如果需要重启机器的话那么需要一定的服务离线时间(如应用系统安全补丁等操作),然而一个能够容忍机器失效的系统可以一次操作一个结点,而不会引起整个系统服务的离线(滚动升级)。

软件错误

另一类错误是系统错误,这类错误更难以预料,因为它们常常是跨不同结点关联,相对于硬件故障,这类错误更容易导致更多的系统失效,包括:

  • 一个软件bug导致应用服务器的每一个实例在接收到一个特殊错误输入的时候崩溃。如2012年6月30日闰秒带来的问题,由Linux内核引起的一个bug导致许多程序被同时挂起。
  • 一个失效进程用尽共享资源,如CPU,内存,磁盘,网络带宽等。
  • 一个系统依赖的服务变慢,反应迟钝,或返回错误结果。
  • 级联错误,当一个部件中的小错误引起了另一个部件中的错误,而该错误又会继续引起其他错误。

该类错误没有快速的解决方法。但是有许多细节可以帮助:小心思考系统的假设以及交互;完备的测试;进程隔离;允许进程崩溃重启;在生产环境中监测、评价以及分析系统的行为。

人为错误

人类设计并搭建了软件系统,保证系统运转的操作者仍然是人类。即使他们有最好的意图,人类仍然是不可靠的。例如,一个面向大量互联网服务的调查表明引起运行中断的首要原因是操作者的配置错误,而硬件故障(服务器或者网络)只占比10-25%。

我们如何使我们的系统更加可靠,尽管有不可靠的人类?优秀的系统常常包含以下方法:

  • 设计一个最大程度下减少出错可能的系统。如良好的抽象,API,提供管理员界面使得“做正确的事”简单并不鼓励“做不正确的事”。然而,如果管理员界面如果限制太多,人们将会绕过它们,否定它们的好处,所以这是一个棘手的平衡。
  • 解耦人们最容易犯错的地方与会造成错误的地方。特别的,提供完成的非生产沙盒环境供用户安全地探索及实验,使用真实数据,却不影响真实用户。
  • 在所有层面都进行完备的测试,从单元测试到整个系统的集成测试以及手动测试。自动测试是一个被广泛使用,广泛认知,尤其对覆盖在正常运行中很少出现的角落案例非常有用的工具。
  • 允许从用户错误中快速且方便的恢复,以最大程度上减少该错误带来的影响。
  • 建立详细且清晰的监测,例如性能度量以及错误率。
  • 实施良好的管理实践和培训。

未完待续…