• 作者:老汪软件技巧
  • 发表时间:2024-08-24 11:05
  • 浏览量:

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

要问今年近几年读过最好的专业书籍,非《设计数据密集型应用》莫属了(原名是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来评估员工也是一样的,只有构建一个标准评价体系才能使得团队更健康的发展。

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

数据编码

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

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

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

「文本编码」(如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的格式,使用Golang的binary.BigEndian来编码。

但如果要更改底层结果

查询引擎

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

隔离性

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

总结

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

Explore more in qingwave.github.io