happenlee

坦白说也是机缘巧合,在硕士生阶段进入分布式系统领域学习。无论是大规模存储或计算,其核心也是运用分布式技术利用并行性来解决数据密集型应用的需求。最近开始在啃这本《Designing Data-Intensive Applications》大部头,作者Martin Kleppmann在分布式数据系统领域有着很深的功底,并在这本书中完整的梳理各类纷繁复杂设计背后的技术逻辑,不同架构之间的妥协与超越,很值得开发人员与架构设计者阅读。
很可惜的是国内目前并没有对应的中文版本,这个系列算是一个读书感悟,同时也夹带私货,阐述一些自己的理解与看法,抛砖引玉,希望大家多交流学习。这本书共有12个章节,接下来我会一个章节更新一篇读书笔记。(囧rz,感觉自己又开了一个坑)同时也希望国内的出版社可以尽快引入版权,我也想要参与翻译工作啊(,,• ₃ •,,) !!

1.数据密集应用

作为一个开发者来说,目前绝大多数应用程序都是数据密集型的,而不是计算密集型的。CPU的计算能力不再成为这些应用程序的限制因素,而更加亟待解决的问题是海量的数据、数据结构之间的复杂性,应用的性能。

先看看我们经常打交道的数据系统:

  • 存储数据,以便它们或其他应用程序稍后再找到它(数据库
  • 记住昂贵操作的结果,以加快读取速度。(缓存
  • 允许用户按关键字搜索数据或通过各种方式过滤数据(搜索索引
  • 将消息发送到另一个进程,异步处理(流处理
  • 周期性地压缩大量的累积数据(批处理

而很多时候,我们所谓应用程序的绝大工作就是将这些数据系统进行组合,然后添加我们的运行逻辑,但是如何更加合理的整合这些数据系统,对我们来说仍然是一个值得学习和思考的问题。而数据系统也在慢慢变得越来越相似,不同的数据系统也在各自学习彼此的优点。如Redis这样的缓存系统可以支持数据落地,很多时候的应用场合我们可以替代传统的RDBMS。而Kafka这样的数据队列也可以支持数据落地来存储消息。更加深刻的理解这些数据系统,来更好的权衡架构设计,是一门很精深的课题。
结合多个数据系统的应用

上图是一个典型的由多种数据系统构成的应用程序,随着数据量和数据逻辑的复杂,就成为了一个数据密集型的应用。

2.设计数据密集型应用的三原则

  • 可靠性
    具有容错性(面对硬件或软件故障,甚至是人为错误),系统仍应继续正常工作(在期望的性能水平上执行正确的功能)。
  • 可扩展性
    随着系统的增长(在数据量、流量或复杂度),应该有合理的方法来处理这种增长。
  • 可维护性
    随着时间的推移,许多不同的人将致力改善数据系统(既保持当前的行为,并使系统适应新的环境),他们都应该能够卓有成效地工作。

显然,这三个原则不单单是数据密集型应用应当遵循的原则,在绝大多数软件系统中同样是很重要的问题,接下来我们一一梳理一下。

(1)可靠性

  • 硬件故障
    硬盘崩溃,内存出现故障时,电网停电,有人拔了网线,几乎硬件故障在数据中心总是不间断的出现。
    解决方案

    • 在软件与硬件层面考虑冗余,来确保硬件的故障不会演变为系统的故障。
  • 人为的错误
    人是很不可靠,从驾驶技术的演变就可以看出来,人为的疏失会带来巨大的灾难。而且,人经常犯错。
    解决方案

    • 最小化错误机会的方式设计系统。例如,精心设计的抽象,API和管理界面可以很容易地做“正确的事情”,阻止“错误的事情”。
  • 人们犯最多错误的地方和那些可能导致失败的地方解耦。

  • 全面测试,从单元测试到整个系统集成测试和手动测试。

  • 允许快速和容易地从人为错误中恢复,以尽量减少在失败的情况下的影响。例如,使其快速回滚更改配置,逐步推出新的代码(所以任何意想不到的错误只影响一小部分用户),并提供工具来重新计算数据(如果原来旧的计算是不正确的)。

(2)可扩展性

即使一个系统今天工作可靠,但这并不意味着它将来一定会可靠地工作。一个常见原因是负载增加:也许系统已经从10000个并发用户发展到100000个并发用户,或者从100万个增加到1000万个。

“如果系统以特定的方式增长,我们应对增长的选择是什么?” “我们怎样才能增加计算资源来处理额外的负载?”

  • 描述负载
    首先,我们需要简洁地描述系统当前的负载,负载可以用几个数字来描述,我们称之为负载参数。
    参数的选择取决于系统的体系结构,如:
  • 每秒对Web服务器的请求
  • 数据库中的读写比
  • 聊天室中的活跃用户数量
  • 缓存的命中率

  • 描述性能
    一旦描述了系统上的负载,就可以讨论负载增加时发生的情况。可以从两方面看:
    1.增加负载参数并保持系统资源(CPU、内存、网络带宽等)不变时,系统的性能如何受到影响?
    2.当增加负载时,如果希望保持性能不变,需要增加多少资源?

所以我们需要有描述性能的尺子:

  • 平均响应时间:给定n值的算术平均值,全部加起来,除以n。然而这不是一个很好的指标,因为它不告诉你有多少用户真正体验了延迟。
  • 百分比响应时间:把响应时间列表,从最快到最慢排序,那么中间值是中间点:例如,如果平均响应时间是200毫秒,那意味着一半请求在少于200毫秒时返回,而一半请求花费的时间比那个要长。
  • 高百分比的响应时间:可以看看高百分位数:95th,99th,和99.9th百分位数是常见的(简称P95,P99,和p999),来参考响应时间的阈值。

负载情况与性能情况是很重要的,有时系统的瓶颈是由少数极端情况引起的。作者举了一个Twitter的例子,我觉得很好,这里详细分享一下这个例子:

Twitter的故事

Twitter在2012年11月16日公布的数据。
Twitter的两个主要操作是:

  • 发出Tweet
    用户可以发布一个Tweet给他们的订阅者。(平均4.6k请求/秒,峰值超过1.2万的请求/秒)。
  • 获取Tweet
    用户可以查看他们关注者发布Tweet。(约300K的请求/秒)。

Twitter在扩展性的挑战主要不是由于Tweet的数量,而主要是在每个用户都有很多订阅者,每个用户也有很多关注者。执行这两种操作大致是两种方法:

  • 1、发布一条推特,只需将新的推文插入到全球的推文集合中即可。当用户请求他们关注者的Tweet时,可以查找他们所关注的所有人,并找到每个用户的所有Tweet,并将它们合并(按时间排序)。在关系数据库中,可以编写如下查询,例如:
    java 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
    如下图所示:
    关系型数据库的实现格式
  • 2、为每个用户订阅的Tweet维护一个缓存,就像每个收件人的Twitter邮箱一样。当用户发布一条推文时,请查找所有关注该用户的人,并将新的Tweet推送到他们的缓存中。所以读取Tweet列表是很划算的,因为它的结果提前计算好了。

Twitter的数据管道,用于发送消息给订阅者

如上图所示的结构显然更合适Tweet的发布,因为发布的Tweet的写操作几乎比读的操作低两个数量级,所以在这种情况下,最好是在写时做更多的工作,而不是在读时做更多的工作。但是方法2并不适用于有大量关注者的账号,假设某人有3000W粉丝,一次发布Tweet产生的写操作可能是巨大的。所以目前在Twitter的Tweet系统中,Twitter将这两种方法混合。大多数用户的推文在发布时仍然会被扩展到Tweet缓存之中,但只有少数用户拥有大量的关注者(即名人)。用户可以跟踪的任何名人的Tweet,并单独读取并与用户的Tweet缓存中进行合并。这种混合方法能够始终如一地提供良好的性能。

这个例子很精炼的描述了架构设计的妥协与精妙,依据业务特点,最大化的优化了数据系统的性能。很佩服Twitter的工程师在架构设计上的功力。同时也很好奇如微博,微信是不是也是采用类似的架构进行设计。

  • 怎么扩展
    放大(垂直缩放,移动到更强大的机器)和缩放(横向缩放,在多台更小的机器上分配负载)之间的二选一。实际上,好的架构通常涉及到一种实用的混合方法:例如,使用几个功能强大的机器仍然比大量的小型虚拟机更简单、更便宜。无节制的分布式会给系统混入复杂度,这是软件工程中危险的地方,虽然在多台机器上分发无状态服务相当简单,但将有状态数据系统从单个节点转移到分布式安装程序会带来许多额外的复杂性。
    没有这样的东西,一个通用的,一个适合所有的应用的可伸缩的架构。(写的真好

(3)可维护性

这部分教导了一些构建可维护系统的方法。软件的大部分成本不是在最初的开发中,而是在持续的维护中修复bug、保持系统运行、使其适应新业务、添加新特性。

  • 可操作性
    让操作运维团队保持系统运行的顺利。

  • 简单
    让新工程师很容易理解系统,通过尽可能地从系统中删除尽可能多的复杂性。

  • 可进化性
    让工程师很容易在将来对系统进行更改,以适应需求变化时的意料之外的用例。也被称为可扩展性、可修改性、可塑性。

维护别人留下的烂摊子真的是很痛苦的事情,文档,注释真的是重中之重!!!

相关文章: