这本书从今年一月就开始看了, 一开始是思源推荐给我的,因为之前在欧特克的工作没有怎么接触大规模的数据需求项目,期间读了几次断断续续,也没完整读完一遍。 思忖着最近以太坊浏览器的开发,数据量的也算是初具规模,重新拜读神作。

image

当今开发应用程序的挑战瓶颈已经不在计算性能瓶颈,而是程序需要处理海量的数据,数据的高复杂度,以及数据频繁的变化速度。

写一个常规的跟数据相关的应用程序通常需要以下传统数据模块: 1. 存储数据,以待之后读取数据或者给其它程序读取。(数据库) 2. 记住耗时长,复杂的查询,方便提速之后同样的查询。(缓存) 3. 允许用户利用关键词进行快速搜索,或者进行数据删选。(索引) 4. 向另一个进程发送消息,异步处理。(流处理) 5. 定期压缩大量累积数据。(批量处理)

通常在写一个新的程序的时候,我们只是将上面的模块组合,大多数程序员并不会去深入思考各个部分,但随着数据量的增加,如何高效经济地处理数据要求我们了解各个模块,选择合适的模块,最后搭建出来的大厦才能美观,质量够硬。

我们谈及数据库,消息队列,缓存,我们会认为它们是不同的工具,尽管它们都是用来存储数据一段时间(或长或短),但我们都知道它们的访问方式,速度以及实现方式非常不同。

近些年来,涌现出了很多不同的数据系统工具,跟传统的数据模块定义不再那么吻合,比如K-V 数据库(Redis)也常常被使用在消息队列中,也有消息队列具有数据库才有的持久性(Apache Kafka), 传统的数据模块定义边界已经不再是那么的明确。 并且随着数据量的增大,需求的增加,传统的单一的数据工具已经不能满足程序处理数据跟储存数据的需求。

三原则

  • 可靠性:当硬件或者软件层面出现故障时,程序应该仍能再期望的程度上正确地运行。

  • 可扩展性: 当程序的数据量,访问量,或者复杂度增长时,程序可以有效地处理增长带来的压力。

  • 可维护性:随着时间的增长,会有很多不同的人在同一系统上工作(程序员,跟运维),彼此都应该能有效地在程序上工作。

可靠性

通常,可靠性指以下几个方面:

  1. 程序按期望地正常运行。
  2. 程序可以一定程度地处理用户刻意造成的错误或者异常输入。
  3. 在要求的场景下面,程序性能得以保障。
  4. 程序应该安全保障,防止异常访问,跟非法操作。

可靠性的几个典型案例跟解决措施

  • 硬件故障 硬盘崩溃,内存无法访问,电源闪断或者有人不小心碰掉电源,随着数据量增加,控制的机器增加,这种问题发生再所难免。 解决措施: 硬件层面,比如硬盘的RAID配备,多重电源,数据库快照镜像等等。
  • 软件故障 相比硬件故障,(各个模块比较独立,发生概率随机),一单软件层面发生故障,往往引起的是一大片的程序或者节点出现故障。
    1. 比如一个软件bug导致一个程序崩溃,那么所有安装该程序的机器实例基本都会遇到同样的bug而崩溃。
    2. 一个进程随着时间的推移,用光了所有的共享资源,比如CPU, 内存,磁盘,导致程序出现崩溃。
    3. 一个依赖的三方进程突然变慢,变得不再可靠,返回错误响应。
    4. 链式反应,当某一个部件出现故障时,导致一大片程序出现故障。 通常上述这些问题在程序运行初期,并不会马上发生,而是随着时间的推移,由于某些特殊情景触发。 解决措施:整体来讲没有太好的措施,软件层面允许程序的崩溃重启,心跳反应,对共享资源监测,LB。
  • 人为错误 人是不可靠的,多种因素,粗心,精神不集中…. 解决措施
    1. 精心设计系统,减少人工操作, 良好的抽象跟良好的API设计。
    2. 分析出容易犯错并导致失误的地方,使其解耦. 创建沙盒,使大家可以安全测试。
    3. 从单元测试到集成测试完整测试。
    4. 允许快速和容易地从人为错误中恢复以减少失误对系统造成的影响。比如快速roll-back。
    5. 监测.
    6. 良好的管理规范跟培训

可扩展性

及时一个系统当前可以稳定可靠的工作,但随着数据的增加,并不代表该系统在未来还能稳定工作。举个简单的例子就是,比如用户访问量从1万每秒增长到100万每秒,或者数据量也从几百个G增加到几个T。

可扩展性使我们用来描述一个系统对增加的负载的处理能力。

负载

负载可以指对服务器的rps(requests per second),也可以是数据库的每秒读写次数,缓冲的访问频率,聊天室同时在线人数。了解系统的负载跟瓶颈所在,将有效地找到解决措施。 书中给了一个推特的例子非常好。

Twitter 的故事 Twitter 在2012年11月时, Twitter的两个主要功能: 1. 推送新的消息 用户可以向他的关注者发送新的消息(4.6k rps 平均,巅峰可到12k rps) 2. 读取消息 用户可以在主页观看他关注者的最新消息(300k rps每秒)

如果只是单单处理12k 每秒的写入并不难,但是Twitter的主要瓶颈并不是在消息推送数量上面,而是在fan-out,每一个用户关注了许多其他的用户,而且一个用户同时可能被好多其他用户关注,因此当一个用户发送新的推文时,可能需要向几百万个用户发送最新推文。

推特一开始采取了这种处理方式, 每发送一条新的推特会向一个全局的推文集合里插入新的数据,当一个用户查看最近推文时,会搜寻他所有的关注者,然后找出这些用户发布的所有消息,合并然后按时间排序。如果是关系型数据库,可以这样查询

    SELECT tweets.*, users.* from tweets
        JOIN users ON tweets.sender_id = users.id
        JOIN follows ON follows.followee_id = users.id
        WHERE follows.follower_id = current_user

image

但上述方式在load最新推文时会遇到很大的瓶颈,后来公司替换到了第二种方式,速度得到了很大的提升。给每个用户创建一个缓存,像邮箱系统一样,每当一个用户发送一个新的推特时,查询关注该用户的用户群,将该条推特插入到这批用户群的缓存中。

image

第二种方式虽然在速度上有很多的提升,但有些特殊情况,比如 但是有些用户比如有几十万follower,方案2也会显得心有余而力不足。最后twitter采用了方案1和方案2结合的方式。再twitter中,每个用户的关注度是一个非常重要的权重指标。对于普通用户,采取方案2,对于明星用户则刻意采用方案1.

系统性能

一但确认系统的负载点,就可以监测当负载增加时,系统的表现良好与否。 1. 当负载增加时,保持系统的CPU,内存,网络带宽不变,看看程序的表现变化。 2. 当负载增加时,看看需要提升多少系统资源,以保持程序的性能。

在批处理系统Hadoop,我们关心的性能是Hadoop的吞吐量或者说给定一定大小的任务需要消耗的时间,对于在线系统,用户更关心的则是系统的响应时间。

可维护性

一个程序完整的生命周期,大部分是花在后期维护更新上的,为了让后期维护更新让他人自己更轻松,我们在设计程序的时候要尽量注意以下几点。

  1. 可操作性: 让运维组能够轻松的保持程序健康地运行。
  2. 简单性: 新程序员可以轻松愉快的上手(良好的抽象,跟文档很重要)
  3. 可进化型:可以轻松地给程序添加新的功能