可靠、可扩展与可维护的应用系统
数据密集型系统,CPU的处理能力往往不是第一限制性因素,关键在于数据量、数据的复杂度及数据的快速多变性。
三个度量:
- 可靠性(Reliability):当出现意外情况如硬件、软件故障、人为失误等,系统应可以继续正常运转:量
- 可扩展性(Scalability):随着规模的增长,例如数据量、流量或复杂性,系统应以合理的方式来匹配这种增长
- 可维护性(Maintainability):随着时间的推移,许多新的人员参与到系统开发和运维,以维护现有功能或适配 新场景等,系统都应高效运转。
数据模型与查询语言
常用数据模型:
- 关系模型
- 非关系模型:
文档模型:文档数据库的目标用例是数据来自于自包含文档,且一个文档与其他文档之间的关联很少。
图模型等:针对相反的场景,目标用例是所有数据都可能会互相关联。
文档数据库和图数据库有一个共同点,那就是它们通常不会对存储的数据强加某个模式,这可以使应用程序更容易适应不断变化的需求。但是,应用程序很可能仍然假定数据具有一定的结构,只不过是模式是显式 (写时强制)还是隐式(读时处理)的问题。
每个数据模型都有自己的查询语言或框架,如:SQL、MapReduce、MongoDB的聚合管道、Cypher、SPARQL和Datalog。
数据存储与检索
存储引擎分为两大类:
- 针对事务处理 (OLTP)优化的架构:通常面向用户,可能收到大量的请求。为了处理负载,应用程序通常在每个查询中只涉及少量的记录。应用程序基于某种键来请求记录,而存储引擎使用索引引来查找所请求键的数据。磁盘寻道时间往往是瓶颈。
- 针对分析型 (OL AP)的优化架构:不是直接面对最终用户,主要由业务分析师使用。 处理的查询请求数目远低于OLTP系统,但每个查询通常要求非常苛刻,需要在短时间内扫描数百万条记录。磁盘带宽(不是寻道时间)通常是瓶颈,而面向列的存储对于这种工作负载成为日益流行的解决方案。
OLTP数据存储结构:
- 日志结构流派:它只允许追加式更新文件和删除过时的文件,但不会修改已写入的文件。典型数据结构是LSM-Tree(基于日志的合并树),基于合并和压缩排序文件原理的存储引擎通常都被称为LSM存储引擎,如HBase。
性能优化:布隆过滤器、文件压缩合并等。
适用场景:点查、区间查询、高写入吞吐量 - 原地更新流派,将磁盘视为可以覆盖的一组固定大小的页。B-tree是这一哲学的最典型代表,它已用于所有主要的关系数据库,以及大量的非关系数据库。
性能优化:聚集索引(索引中存储值),如MySQL的InnoDB引擎中,主键都是聚集索引,二级索引引用主键。
OLAP数据存储结构:
当查询需要在大量行中顺序扫描时,索引的关联性就会显著降低。相反,最重要的是非常紧湊地编码数据,以尽量减少磁盘读取的数据量。常用优化手段:
列式存储:不是将每行、而是将每列中的所有值存储在一起。写入:首先进入内存存储区,将其添加到已排序的结构中,接着再准备写人磁盘(参考LSM-Tree)
列压缩:位图编码(bitmap)、游程编码
物化视图:针对大量读密集的数仓维度
数据复制
三种多副本方案 :
- 主从复制:所有的客户端写人操作都发送到某一个节点 (主节点),由该节点负责将数据更改事件发送到其他副本(从节点)。每个副本都可以接收读请求,但内容可能是过期值。
节点失效:从节点失效,追赶时恢复;主节点失效,节点切换 - 多主节点复制:系统存在多个主节点,每个都可以接收写请求,客户端将写请求发送到其中的一个主节点上,由该主节点负责将数据更改事件同步到其他主节点和自己的从节点。
- 无主节点复制:客户端将写请求发送到多个节点上,读取时从多个节点上并行读取,以此检测和纠正某些过期数据。
每种方法都有其优点和缺点。主从复制非常流行,主要是因为它很容易理解,也不需要担心冲突问题。而万一出现节点失效、网络中断或者延迟抖动等情况,多主节点和无主节点复制方案会更加可靠,不过背后的代价则是系统的复杂性和弱一致性保证。
处理复制滞后的三个一致性模型:
- 写后读一致性:保证用户总能看到自己所提交的最新数据。方法:判断是否从主节点读取
- 单调读:用户在某个时间点读到数据之后,保证此后不会出现比该时间点更早的数据。方法:确保用户从固定的同一副本读数据
- 前缀一致读:保证数据之间的因果关系,例如总是以正确的顺序先读取问题,然后看到回答。
多主节点和无主节点复制方案的并发问题:即由于多个写可能同时发生,继而可能产生冲突。为此,需要算法使得数据库系统可以判定某操作是否发生在另一个操作之前,或者是同时发生。接下来,采用合并并发更新值的方法来解决冲突。
数据分区
分区的目地是通过多台机器均匀分布数据和查询负载,避免出现热点。这需要选择合适的数据分区方案,在节点添加或删除时重新动态平衡分区。两种主要的分区方法:
- 基于关键字区间的分区。先对关键字进行排序,每个分区只负责一段包含最小到最大关键字范围的一段关键字。对关键字排序的优点是可以支持高效的区间查询,但是如果应用程序经常访问与排序一致的某段关键字,就会存在热点的风险。采用这种方法,当分区太大时,通常将其分裂为两个子区间,从而动态地再平衡分区。
- 哈希分区。将哈希的数作用于每个关键字,每个分区负责一定范围的哈希值。这种方法打破了原关键字的顺序关系,它的区间查询效率比较低,但可以更均匀地分配负载 。 采用哈希分区时,通常事先创建好足够多 (但固定数量)的分区,让每个节点承担多个分区,当添加或删除节点时将某些分区从一个节点迁移到另一个节点,也可以支持动态分区。
混合上述两种基本方法也是可行的,例如使用复合键:键的一部分来标识分区,而另一 部分来记录排序后的顺序。
分区与二级索引,二级索引也需要进行分区,有两种方法:
- 基于文档来分区二级索引(本地索引):二级索引存储在与关键字相同的分区中,这意味着写入时我们只需要更新一个分区,但缺点是读取二级索引时需要在所有分区上执行scatter/gather。
- 基于词条来分区二级索引(全局索引):它是基于索引的值而进行的独立分区。二级索引中的条目可能包含来自关键字的多个分区里的记录。在写入时,不得不更新二级索引的多个分区;但读取时,则可以从单个分区直接快速提取数据。
事务
事务隔离级别:
- 读-提交:读-提交是最基本生的事务隔离级别,它只提供以下两个保证:
读数据库时,只能看到已成功提交的数据(防止“脏读” )
写数据库时,只会覆盖已成功提交的数据(防止“脏写” )。 - 快照隔离 (或可重复读取): 每个事务都从数据库的一致性快照中读取,事务一开始所看到是最近提交的数据,即使数据随后可能被另一个事务更改,但保证每个事务都只看到该特定时间点的旧数据。支持快照级别隔离的存储引擎往往直接采用MVCC来实现读-提交隔离。
实现方法:多版本并发控制(MVCC)当事务开始时,首先赋子一个唯一的、单调递增的事务ID(txid)。每当事务向数据库写人新内容时,所写的数据都会被标记写入者的事务ID。更新操作会被转换为删除+创建。
数据对象可见性规则:事务开始的时刻,创建该对象的事务已经完成了提交;对象没有被标记为删除,或者即使标记了,但删除事务在当前事务开始时还没有完成提交。 - 可串行化:
严格串行执行事务:如果每个事务的执行速度非常快,且单个CPU核可以满足事务的吞吐量要求,严格串行执行是一个非常简单有效的方案。
两阶段加锁(2PL):几十年来,这一直是实现可串行化的标谁方式,但还是有很多系统出于性能原因而放弃使用它。大多数使用2PL的数据库实际上实现的是索引区间锁(或者next-keylocking),本质上它是对谓词锁的简化或者近似。
可串行化的快照隔离 (SSI):一种最新的算法,可以避免前面方法的大部分缺点。它秉持乐观预期的原则,允许多个事务并发执行而不互相阻塞,仅当事务尝试提交时, 才检查可能的冲突,如果发现违背了串行化,则某些事务会被中止。
边界:
- 脏读:客户端读到了其他客户端尚未提交的写入。读-提交以及更强的隔离级别可以防 止脏读。防止脏读:对于每个待更新的对象,数据库都会维护其旧值和当前持锁事务将要设置的新值两个版本。
- 脏写:客户端覆盖了另一个客户端尚未提交的写入。通常采用行级锁防止脏写,几乎所有的数据库实现都可以防止脏写。
- 读倾斜 (不可重复读) :客户在不同的时间点看到了不同值。快照隔离是最用的防范手段,即事务总是在某个时间点的一致性快照中读取数据。通常采用多版本并发控制(MVCC) 来实现快照隔离。
- 更新丟失:两个客户端同时执行读-修改-写入操作序列,出现了其中一个覆盖了另一个的写入,但又没有包含对方最新值的情况,最终导致了部分修改数据发生了丟失。快照隔离的一些实现可以自动防止这种异常,而另一些则需要手动锁定查询结果(SELECT FOR UPDATE) 。
- 写倾斜:事务首先查询数据,根据返回的结果而作出某些决定,然后修改数据库。当事务提交时,支持决定的前提条件已不再成立。只有可串行化的隔离才能防止这种异常。
- 幻读:事务读取了某些符合查询条件的对象,同时另一个客户端执行写入,改变了先前 的查询结果。快照隔离可以防止简单的幻读,但写倾斜情况则需要特殊处理,例如采用区间范围锁。
一致性与共识
一致性保证:
- 可线性化:代价太高
- CAP理论:分布式环境下 =“网络分区情况下,选择一致性还是可用”。不具实际价值
- 因果一致性:不会由于网络延迟而显著影响性能,又能对网络故障提供容错的最强的一致性模型。不管何种实现,最终都可以归结为共识问题。
共识问题(以下问题都可以归结为共识,且彼此等价):
- 可线性化的比较-设置寄存器
- 原子事务提交
- 全序广播
- 锁与租约
- 成员/协调服务
- 唯一性约束
共识的本质:集群中大部分节点认定的“事实”,即使它并非真正的事实;提议一经确认不可撤销。
分布式事务:两阶段提交(2PC)、引入事务协调者,就像西式婚礼中的牧师。
基于ZK实现共识,是一个不错的选择,非常不建议自建。