DDIA:Designing Data-Intensive Applications The Big Ideas Behind Reliable, Scalable, and Maintainable Systems (Martin Kleppmann)
本书将介绍构建数据系统的原则和实例;以及不同的数据工具的共同点、差异性和它们是如何实现这些特点的。
目录
前言
除了CPU算力,数据密集型、计算密集型的应用面临的更大挑战是:巨量的数据、数据的极端复杂性和迅速变化性。
这些应用通常需要完成以下功能:存储、缓存(记住代价高昂的某些操作的执行结果以便加速运算)、检索 / 筛选、流处理、批处理。
如今貌似每个功能都有现成的优秀引擎可供使用。但是当开发新的应用时,工程师还是需要耐心选择最适合当前任务的工具(比如cache、index都有不同的实现方式,各有优缺点)。此外,总有些问题是单个工具无法完全胜任的,要将不同的工具结合起来也是一个十分棘手的事。
第一章将介绍:搭建一个可靠的、可扩展、易维护的数据系统的基本原则和思维方式。
开发系统需要满足功能性需求和非功能性需求(稳定性、可扩展性等)。
没有一个简单的公式可以套用。但是有一些模式和技术持续出现在不同的应用中以满足上述需求。
关于 Data System
什么是数据系统:结合了多个不同的数据工具开发的应用。
为什么需要“数据系统”这个名字:一,不同的数据工具之间的边界越来越模糊 二,单一的数据工具越来越难以满足生产需要。
本来用于存储的Redis也可以用作消息队列;本来是消息队列的Kafka又具有像数据库一样的持久性保证。
什么是一个好的数据系统 / 开发者应关注什么:可靠的、可扩展、易维护
可靠性 Reliability
可靠性的具体要求
- 系统能按照期望的功能运行。
- 对客户的误操作有容忍性。
- 系统能在期望的运行压力下有符合期待的运行表现。
- 阻止无权限的用户访问 / 滥用系统。
failure VS fault
failure是整个系统失效。fault是系统中的部分组件失效。可靠性要求阻止fault导致failure。
prevent VS cure
顾名思义,prevent阻止故障发生,cure是具有故障恢复的能力。cure通常被选择的更多。但有些错误是不存在cure的能力的,比如黑客访问到了安全级别极高的数据、客户账户上的存款被盗走。这些情况prevent是唯一且必须的选择。
硬件失效
硬盘失效、断电、网线插错等等。
常见方案是设置冗余设备:有效但不能完全避免。此外,随着数据量和计算量的增长,按概率来算失效设备数也在增长。对于AWS这些云服务公司,它们并不在乎单台机器的可靠性,系统扩展性和灵活性才是更重要的问题。
通过软件方法解决也是一种途径,优势在于当某个硬件出错时不需要停止整个系统等待新设备备份和上线;在系统升级时也可以在不停机的情况下逐步完成整个系统的更新。
软件错误
软件层面的系统错误包括:操作系统内核bug、进程死锁等。
这类问题往往难以排查且后果比硬件出错更严重。硬件出错往往是互相独立的(除非是环境条件导致大规模失效,比如低温、地震),而软件层面的系统错误则会导致集联问题。
没有一劳永逸 / 简单的方法解决:
严谨的设计、彻底的测试、进程隔离、允许进程终止和重启、实时监控和告警等
人为错误
人为错误导致宕机的比例远高于硬件故障。
可以从以下角度入手规避人为错误:
- 严格设置的接口(需要在灵活性之间做一个权衡)
- 各个层面上彻底的测试,每个小单元到整个系统。
- 设置沙箱实验,real data but not real users
- 快速回滚配置和数据的机制,但是逐步更新代码,这样更新后的代码只会对一小部分有影响。(数据也要回滚,因为旧配置在新数据下可能是错误的)
- 详细和清晰的监控指标,有助于快速诊断问题。
- 良好的员工管理和训练机制。
可扩展性 Scalability
一个当前稳定的系统在业务量和数据量增长后还能保持稳定需要可扩展性。
扩展性应从多个维度考量。不能说“Y系统是可扩展的” ,说“X系统在Z方面是可扩展的”比较合适。
如何考量负载
不同的业务需求有不同的指标,对不同操作之间的偏向也不同。
以推特为例,考虑到其fanout(扇出?)特性,推特目前的策略结合了消息队列和数据库连表的方式:
对于粉丝较少的人发布的推文,采用消息队列,用提前成倍写的方式获得更高效的读效率;
对于粉丝量巨大的名人,采用数据库连表,当有关注该网红的人发起读操作时,从数据库里获得网红发布的推文,按时间顺序插入到其他follower的推文里。
如何考量性能
- 两个角度考虑负载增长的代价:负载增加时,如果资源数不变,性能下降了多少;负载增加时,要想维持性能不变,需要增加多少资源?
- 常见的性能指标:吞吐量(throughput)、响应时间(response time)、延迟(latency)
区分响应时间(response time)和延迟(latency)
响应时间 = 系统真正的处理时间(service time) + 网络延迟时间 + 队列延迟时间
延迟 = 任务排队等待处理的时间
- 即使在一个会话里重复执行相同的任务,也会有不同的性能。影响性能的因素有很多:
a context switch to a background process, the loss of a network packet and TCP retransmission, a garbage collection pause, a page fault forcing a read from disk, mechanical vibrations in the server rack
- 使用平均值来考量性能不如使用中位数 / 百分位数
分位数高的响应时间 (尾部延迟 tail latencies)直接影响用户体验。
往往经常体验到最大响应时间的客户是最有价值的(数据量最大,购买的服务最多):AWS 响应时间没增加100ms,销售额就会降低1%。
当然要提高99%分位数的性能,需要付出的代价是非常昂贵的。
- 队列延迟是tail latencies的主要原因。队头阻塞(head-of-line blocking)又是队列延迟的的主要原因。
- 进行扩展性测试的时候,发送请求的间隔和上一个请求的响应时间应互相独立。如果总是等到上一个请求执行结束后才发送下一个请求,相当于人为解决了数据倾斜的问题(skew),测试得到的响应时间会低于实际情况。
- 监控响应时间的建议:
最简单的方法:划定时间窗口,将时间窗口内的所有请求的响应时间排序,计算百分位数。
更高效的近似方法(估计CPU时间,内存占用):forward decay, t-digest , or HdrHistogram
不要轻易计算平均值,平均值在数学上是没有意义的。
解决负载增加的办法
- 纵向扩展(scaling up / vertical scaling) VS 横向扩展(scaling out / horizontal scaling)
横向扩展的一种典型结构:shared-nothing architecture
- elastic systems :能自动扩展的系统(识别到压力增大时自动增加资源),当负载极度不可预测时需要这种系统。但是相比手动扩展的系统,会更复杂也更容易出现“operational surprises”。
- 当扩展成本不是太高或可用性要求不高时,不建议将有状态系统从单个节点改变成分布式系统,改造的复杂度代价太大。
- 目前没有通用的可扩展的系统架构能适用于所有的应用。往某个方向错误的进行扩展最好的结果是浪费资源,最坏的结果是妨碍原有的功能。对于新产品而言快速的在产品功能上迭代比无头苍蝇式的在某个假设的性能上扩展更有效。
可维护性 Maintainability
Operability:降低平时的维护难度
Simplicity:降低新工程师上手的难度
降低复杂度不意味着减少系统功能。而是要追求降低意外复杂度(accidental complexity):
如果不是系统要解决的问题所固有的复杂度,而是在实现系统的过程中产生的复杂度,被称为accidental complexity。
最好的消除accidental complexity的方法是进行抽象。好的抽象可以被应用到多个不同的应用中、可以隐藏底层的很多细节,向上提供友好的使用界面。(C++里的类、关系型数据库实体关系模型)
Evolvability:降低进行修改时的难度
强烈依赖于Simplicity和抽象。
敏捷联盟:The Agile community
TDD:test-driven development