· 阅读

读《设计数据密集型应用》

在我们的社会中,技术是一种强大的力量。数据、软件、通信可以用于坏的方面:不公平的阶级固化,损害公民权利,保护既得利益集团。但也可以用于好的方面:让底层人民发出自己的声音,让每个人都拥有机会,避免灾难。本书献给所有将技术用于善途的人们。

要问今年近几年读过最好的专业书籍,非《设计数据密集型应用》莫属了(原名是Designing Data-Intensive Applications,简称DDIA)。最开始接触这个书名时,感觉有点不知所谓,看完后却再也找不到更合适的名字了。

为什么值得读

这是一本关于数据系统的书籍,从数据的存储、查询、编码到分布式环境中的事务、一致性,再到批处理、流处理等更高级的数据系统。涉及范围非常广,不仅仅是数据库。我们常听到的名称例如NoSQL、大数据、CAP、最终一致性、MapReduce、实时等等,在这里都会找到。

考虑下面几个问题,如果你不清楚,这本书会告诉你答案:

  • 到底什么是一致性?
  • 如果保证系统的向后兼容与向前兼容?
  • 分布式系统中怎么确定事件发生的先后顺序?
  • 分布式锁可靠吗?

我认为所有的后端工程师都应该认认真真读一读,大学里面学习的各种数据库原理早已抛之脑后,当时理解的很浅显加上很多老师的照本宣科,并不能真正理解很多名称,比如ACID。本书深入浅出的讲解很多数据系统的发展过程、解决思路、本质原理。很多书浅显有余而深度不足,只能当科普入门,有些书晦涩拗口,让人望而却步,深入浅出很难得。

关于作者

Martin Kleppmann是剑桥大学分布式系统的研究员,此前在LinkedIn和Rapportive负责大规模数据基础架构。

Martin是一位常规会议演讲者,博主和开源贡献者。他认为,每个人都应该有深刻的技术理念,深层次的理解能帮助我们开发出更好的软件。

BTW,之前他与Redis作者关于Redlock的争论也很有意思。

印象最深刻的点

要说印象深刻,则是DDIA每一章的章首引言,会引用一些专家学者的采访或者论文,文章开头就是其中一段,下面摘抄其中一些:

语言的边界就是思想的边界。 — 《逻辑哲学》

与可能出错的东西比,“不可能”出错的东西最显著的特点就是:一旦真的出错,通常就彻底玩完了。 — 亚当斯《基本无害》

我们必须跳出电脑指令序列的窠臼。 叙述定义、描述元数据、梳理关系,而不是编写过程。 — 《未来的计算机及其管理》

带有太强个人色彩的系统无法成功。当最初的设计完成并且相对稳定时,不同的人们以自己的方式进行测试,真正的考验才开始。 — 高德纳

有效的复杂系统总是从简单的系统演化而来。 反之亦然:从零设计的复杂系统没一个能有效工作的。 — 约翰・加尔

这些引言恰当好处的点名每一章所要描述的内容,又非常有趣。

下面主要总结下给我启发较多的一些内容。

什么是数据密集

数据密集型任务主要面临的是处理、传输、存储和管理大量数据的挑战。此类任务的瓶颈通常在于数据的输入输出I/O)操作,而不是计算本身。程序的性能主要受限于数据访问速度、存储容量或网络带宽。例如大数据分析用户行为、流媒体服务、大量数据查询等任务。

相对的是计算密集,面临的是复杂的数学运算、算法执行或数据处理。这类任务的瓶颈通常是CPU/GPU的处理能力,而不是数据的输入输出。程序的性能主要受限于计算资源的处理速度、并行计算能力或算法的效率。例如图像识别、气象预测、加解密、实时渲染等任务。

面试中经常问道为什么Redis是单线程架构,重点就是Redis数据密集性系统而非计算密集性,主要涉及高效地存取数据。

数据系统的评估

数据密集型的系统便是数据系统,主要负责数据的存储、查询、处理(对应数据库、缓存、搜索引擎、流处理等),是更上层的抽象。

构建一个系统首先是得需要一个评价体系,不然系统做好做坏我们只能凭感觉,同样地企业通过KPI、OKR来评估员工也是一样的,只有构建一个标准评价体系才能使得团队更健康的发展。

数据系统首先必须满足需求才称得上有用,比如满足存储、搜索等功能。那么如何评价这些功能,通常有以下几个指标:

  • 可靠性,意味着即使发生故障,系统也能正常工作。如我们常说的5个9。
  • 可伸缩性,是用来描述系统应对负载增长能力。通常关注负载参数(QPS、读写比、活跃用户、缓存命中率等)与性能参数(吞吐量、响应时间),通常一些云服务会使用服务级别协议(SLA, service level agreements)来定义系统的可用性。
  • 可维护性,这个没有具体指标,但系统设计时需要考虑可操作性、简单性、扩展性。

数据编码

你有思考过数据库到底底层是怎么存储数据的?为什么我们经常使用JSON来传输数据?

在处理数据密集型应用时,数据的编码方式会直接影响系统的性能和复杂性。我们系统往往会随着时间升级,必须考虑到数据的兼容性:

  • 向后兼容 (backward compatibility),新的代码可以读取由旧的代码写入的数据。
  • 向前兼容 (forward compatibility),旧的代码可以读取由新的代码写入的数据。

书中详细讨论了文本编码和二进制编码的不同应用场景。

文本编码(如JSON、XML、CSV)因其易读性和广泛的工具支持,常用于API和配置文件等领域。这些格式的优点是人类和机器都能轻松解析,适合在开发和调试阶段使用。然而,文本编码的缺点在于数据冗余和解析速度慢,这在高性能场景下可能成为瓶颈。

二进制编码(如Protobuf、Thrift、Avro)则专注于高效的传输和存储。由于其紧凑的表示方式,二进制编码在网络传输和存储空间的优化上表现出色。但相应地,二进制数据的可读性差,调试困难,且需要依赖于专门的工具进行解析。特别是在大规模分布式系统中,二进制编码因其压缩性和速度优势,成为常见选择。

关于时间

时间是一个复杂的哲学问题,计算机的时间是物理世界的抽象,你知道日历时钟单调钟吗?

由于不同集群的硬件差异,每台机器都有自己的时间,通过网络时间协议(NTP)来协调时间,那么如何计算时间间隔呢?比如日历时钟会不断变化,单调钟会一直累加,例如Linux上的clock_gettime(CLOCK_MONOTONIC),和Java中的 System.nanoTime()go中的Time{ mono int64}都是单调时钟。

由于不同机器的时钟并不可靠,在不同节点上怎么能保证事件的顺序呢,例如,如果两个客户端写入分布式数据库,谁先到达?

为解决这些问题,逻辑时钟(如Lamport时钟)和向量时钟成为分布式系统中处理时间的标准工具。逻辑时钟通过递增的计数器为事件排序,但不能解决并发冲突;向量时钟进一步扩展了逻辑时钟,允许系统检测并发事件,从而更精确地重构事件的因果关系。例如微信就使用向量时钟来判断朋友圈的因果关系。

ACID

ACID经常能听到,那它对于数据系统到底是什么?

周所周知ACID代表原子性(Atomicity),一致性(Consistency),隔离性(Isolation) 和持久性(Durability),旨在为数据库中的容错机制建立精确的术语。

但实际上,不同数据库的ACID实现并不相同。今天,当一个系统声称自己符合ACID时,实际上能期待的是什么保证并不清楚。不幸的是,ACID现在几乎已经变成了一个营销术语。

不符合ACID标准的系统有时被称为BASE,它代表基本可用性(Basically Available),软状态(Soft State) 和 最终一致性(Eventual consistency),这比ACID的定义更加模糊。

原子性

ACID原子性的定义特征是:能够在错误时中止事务,丢弃该事务进行的所有写入变更的能力。 或许可中止性(abortability)是更好的术语。

一致性

一致性这个词被赋予太多含义:最终一致性、一致性哈希、CAP中的一致性、ACID的一致性。

ACID一致性的概念是,对数据的一组特定约束必须始终成立。即不变式(invariants)。例如,在会计系统中,所有账户整体上必须借贷相抵。

原子性,隔离性和持久性是数据库的属性,而一致性(在 ACID意义上)是应用程序的属性。应用可能依赖数据库的原子性和隔离属性来实现一致性,但这并不仅取决于数据库。

隔离性

隔离性ACID中最复杂的属性,确保事务之间不会互相干扰。书中介绍了不同的隔离级别,如读已提交(Read Committed)、可重复读(Repeatable Read)和可串行化(Serializable)。尽管可穿行化隔离是最强的隔离级别,但其实现复杂且可能导致性能问题,因此很多数据库在实际中采用更弱的隔离级别如快照隔离以获得更好的性能。

持久性

数据库系统的目的是,提供一个安全的地方存储数据,而不用担心丢失。持久性是一个承诺,即一旦事务成功完成,即使发生硬件故障或数据库崩溃,写入的任何数据也不会丢失。通过日志(WAL)和快照等技术来实现。

完美的持久性是不存在的 :如果所有硬盘和所有备份同时被销毁,那显然没有任何数据库能救得了你。

分布式一致性

我们经常会说到强一致性、弱一致性到底是什么含义?

一致性从强到弱有以下几种:

线性一致性(Linearizability)是最强的一致性模型,确保所有操作在全局时间上都有一个一致的顺序,即只要一个客户端成功完成写操作,所有客户端从数据库中读取数据必须能够看到刚刚写入的值。

线性一致性虽然直观,但代价高昂,因为它需要在分布式系统中实现精确的时间同步和严格的操作顺序。例如Google Spanner, etcd。

顺序一致性保证所有操作在每个节点上的顺序是一致的,但在不同节点之间,操作的顺序可能会有所不同。例如ZooKeeper。

因果一致性保证操作的因果关系得到维护,即如果一个操作依赖于另一个操作,则所有节点必须按因果顺序看到这些操作。它关注操作间的因果关系,而不是操作的全局顺序。如Cassandra。

最终一致性意味着如果你停止向数据库写入数据并等待一段不确定的时间,那么最终所有的读取请求都会返回相同的值。例如DynamoDB。

审视MossDB

正好之前了写一个内存数据库MossDB,详细介绍见Golang实现一个事务型内存数据库,使用DDIA来审视。

编码

MossDB存储key/value,使用二进制编码,用户可以自己定义value的格式,使用Golangbinary.BigEndian来编码。

但如果要更改底层存储结构

  • ✅ 向后兼容,修改代码后可以兼容旧版本数据,需要追加到结构末尾。
  • ❌ 向前兼容,不支持。

查询引擎

内置提供了HashMapRadixTree两种方式,HashMap实现简单通过简单封装map可以快速进行查询与插入,但范围搜索性能差。RadixTree即前缀树,查询插入的时间复杂度只与 Key 的长度相关,而且支持范围搜索

隔离性

MossDB使用全局锁来实现事务,所以事务并不会并行执行,是一个非常简单的实现,如果要考虑读写分离、或者多段锁就要考虑隔离性的问题。

总结

DDIA是一本相见恨晚、值得反复去读的书。

Explore more in https://qingwave.github.io