首先说了,这一部分是讲解分布式数据存储的
那么分布式数据存储诞生的原因是什么?
是因为单台机器的处理能力跟不上?
是因为希望在单台机器出现问题之后还能继续工作吗
是因为希望降低不同地区的用户对于数据获取请求的延迟吗
如果是希望提供更高负载能力,那么选择垂直伸缩的方式解决问题就可以,比如将当前的数据直接放在一个更大的机器上进行保存,但实际上由于某些特殊原因,往往双倍处理器,双倍内存,双倍硬盘的服务器并不能承载双倍的载荷。
相比较垂直伸缩,后来又出现了水平伸缩,这种架构中,运行数据库的每个机器/虚拟机 都是节点,各种使用各自的处理器,内存和磁盘,而在上层使用软件进行协调数据。
这样使用软件来进行处理的话,就可以使用任意性能,任意数量的机器。而且可以做到异地多活的部署,当然这也是接下来的主要部分,但是这样的水平架构也引入了额外的复杂度,接下来也会讲解
数据具体在多节点上分布有几种不同的方式
复制 分区
复制是将同一份数据放在不同的位置上,从而保证了高可用,如果一个节点不可用,剩余的节点仍然可以提供数据服务。
分区是将一个大型的数据库拆分为较小的子集,不同的子集放在不同的节点上
两者往往在项目中结合使用
那么就按照先讲解复制,其次是分区,然后还有分布式系统中的其他问题这样的一个顺序进行讲解。
对于复制,这个解决方案主要就是为了解决高可用性出现的,其次是吞吐量,以及将数据放置在不同位置来加速读取。
那么因为数据不是不变的,对于可变的数据,复制需要考虑数据同步问题,为了解决这个问题,数据变更算法提供了 单领导者,多领导者,无主等解决方案。
那么作者先拿着领导者模型讲述了复制的原理。对于每个数据节点,都称之为副本,
在领导者模型中,有的副本会被称为主库/领导者,所有的写入请求都会发给主库
其他的副本都被称为追随者,接受从主库传来的数据变更,并更新本地的数据库副本
对于读取操作,则是可以向着领导者或者任意一个副本发起查询请求。
这种主从复制的架构,在很多关系型数据库中常见,比如PostgreSQL MySQL Oracle中使用,或者像是Kafka,RabbitMQ这样的分布式消息代理也会用到。
上面给出了主从复制的基本原理,也提到了读取请求可以向任意一个副本发起查询请求查询。那么就存在一个问题,就是副本是否会出现延迟同步的问题。
对于这个问题,通常业界存在两种复制模式,一种是同步复制,一种是异步复制
对于同步复制则是在向用户报告写入成功之前,需要等待从库的确认,确保从库复制完成,从而可以向用户返回成功。
对于异步复制,则是主库发送消息,但不等待从库的响应。
看着必然是同步复制更加优越,但是也存在着延迟增加,以及当某一节点宕机时,整个系统都会崩溃的问题。所以往往都会混合使用,设置某几个从库是同步复制,更多的则是异步复制,从而确保某些节点上具有最新的数据副本,这种配置称为半同步。
如果将全部从库都配置为了异步,那么一旦主库出现了故障,十有八九会出现数据丢失了问题,这就对持久化没法做出良好的保证。
那么我们讨论下这种主从复制架构下的一些问题。
比如如何设置新的副本
对于添加一个新的从库,直接的复制硬盘存储文件是不可取的,这样会看到数据库的不同部分不同时间的内容。
通常是获取某个主库的一致性快照
然后将该快照复制到新的从库节点
然后从库会连接到主库,从快照生成之后的数据开始拉取,同步到新从库上,作为方便定位的数据结构,在不同的数据库中名称不同,比如Postgre称为日志序列号,MySQL称为二进制日志坐标
最后追上主库之后,就是加入完成了,这个过程可以是自动化的,也可以使手动交给管理员执行的。
其次是面对分布式数据库中最常见的问题节点宕机,
如果是从库宕机,那么只需要在重新启动后,利用上线说过的定位,开始追赶主库就可以了。
如果是主库宕机,那么就需要考虑故障切换,故障切换的流程基本是,首先确定主库出现了故障,这一般是利用超时来进行判断
然后是选择一个新的主库,这一步往往是通过选举流程来完成,由剩下的数据节点进行选举
之后配置系统,将选举出的新主库,设置在系统中,并让客户端可以访问新的主库。
当然,在这个数据库切换过程中,有些地方可能会有冲突
如果是异步复制,可能出现新主库比之前的主库出现数据缺失,而在一个关系型数据库中,数据丢失是很可怕的事情,比如涉及金额的转账,或者是让用户看到其他人的信息。
而且有些时候,可能出现两个节点都认为自己是主库的情况,这种情况称为脑裂,如果没有冲突解决机制,必然导致数据出现丢失。不过可以尝试在节点上添加版本来进行解决
之后说一下如何在节点间同步数据,也就是复制日志如何实现。
最简单的是复制语句,对于每一个接受到的语句,都直接发给从库,从库再将其执行一次来同步数据,但是存在着某些问题,比如有些函数会产生不确定的数值,比如NOW()获取当前时间
并且因为同步语句,就导致必须要严格按照相同的顺序来执行。
即使存在解决的方式,但由于存在隐患太多了,所以通常选择其他的复制方法。
传递WAL,因为对于磁盘的修改,都会先存入一个WAL日志,这样我们可以直接传递WAL日志
但是由于直接记录了磁盘修改,这就需要数据库提供方保证不同版本间可以通用。
最后是基于行进行复制,对于插入的行,则是包含所有值,对于删除的行,要么有唯一标识行,要么就记录了全部删除的值,对于更新的行,则和删除类似。
利用这种方式,可以保证了不同版本的兼容性。而且往往我们还会将这种方式和上面的语句复制结合起来,如果存在不确定的执行,就保存行原始数据,如果不存在,就传递語句。
简单介绍了整个一主多从架构之后,作者想要讨论下在这个架构中常见的复制延迟带来的问题
毕竟在一个大集群的数据库架构中,如果全是同步复制必然不可能,常见肯定是异步复制,但是一步复制必然不能忽视其和同步复制之间的差距。虽然有着最终一致性的保障,但是这个最终一致性也模糊了,副本落后本身是不固定的,可能只有几秒钟的落后,可能有着几分钟的落后
比如,作为一个用户,如果我提交了一个评论,可能我在后续刷新的过程中,如果遇到了延迟低的从库,那么必然可以看见这个新评论,但是万一再次刷新的时候遇到了一个延迟很高的从库呢?这样导致我一会看得见,一会看不见。
这种问题成为读已之写,也就要求写后读一致性,可以考虑从主库读取,不过这种集群架构就没用了,或者设定在更新后的一段时间内,从主库读取,其实之前也有思考过这个问题,这种问题也可以通过阻塞一段时间后响应,或者利用某些位移标识来确定从库是否有这个数据来解决。
不过往往现实生活中存在着更多的不确定因素,比如多设备同一用户,可能一个用户拥有多台设备,多台设备的网络不通,链接到的数据中心不同,这要求最终能连接到同一主库
其次还有着单调读的问题,就是每次查询可能分配到不同的服务器,不同服务器连接着不同的数据副本,这就导致某次查询查询到了最新的消息,之后查询因为延迟没有查到最新消息,导致人比较疑惑。这要求一个用户最好可以顺序的进行多次读取,也就是能够连接到相同的数据库
解决方案就是利用用户ID来散列的选择副本,保证副本一致性。
或者说,本来产生了一个有顺序的对话,但是因为某些原因,导致对话的前后错乱,回答先于询问发生。这种问题就是要求某些写入如果是具有顺序的,那么读取也应该具有顺序。这种情况常出现在数据分片的情况中,那么解决方案也是将具有因果的对话写入相同的分片中。
对于这种往往具有某些因果关系的数据,如果数据库能够提供出事务,那么其实可以解决大部分,那么之后作者也表示了,会在其中讨论关于分布式事务的实现。
那么说完了一主多从的架构我们按照顺序,讲解多主复制和无主复制的基本原理
在多主复制的过程中,往往是存在于分区架构中,每个分区都有一个主库,这样每一个主库往往还会是别的分区的主库的从库,应对的常见也是在多个数据中心中进行同步。
但是这里讨论的是每个主库都拥有相同的数据副本,也就是不分区的情况,这种情况不推荐使用,也有很多需要确保的问题。
这样就能保证在某个数据中心出现问题后,不在导致整个系统停摆。
这种架构已经实现在Postgre的BDR或者Oracle的GoldenGate中了
不过这种架构也带了写冲突的问题,比如不同的数据中心会同时修改相同的数据这种问题,
就好比,同一个在线编辑文档,可能两个用户会同时修改同一行,这种问题在多主复制中,可能因为链接的主库不同,从而出现复制时才发现冲突。
对于这种出现的冲突,最好的方式是避免这种问题,就是确保来自特定的用户或者用户请求特定的文档时候,链接固定的数据中心。
并且在冲突解决中,如果一个字段有多个更新,那么往往最后一个写操作会决定这个字段的最终值,这就是所谓的收敛性
为了实现这种收敛性,可以考虑给每一个写入分配一个唯一的ID,选择最新的ID为胜利者,这种方式很流行,不过会导致数据丢失
或者考虑将冲突数据返回给用户,交给用户进行解决冲突。
无论是那种解决方案,本质上都有些或大或小的缺陷,即使后来引入了不同的数据模型,仍然无法彻底消除这种问题。
而且在多主的架构中,也存在着一个问题,就是主库同步的拓扑结构如何实现呢?
常见的复制拓扑结构分为三种,分别是环形拓扑,星型拓扑和全部拓扑
如果是全部拓扑,那么就是每个主库都会将写入发送到其他的主库,
环形拓扑则是每个节点收到写入后,加上自己的写入,转发给另一个节点,
星型拓扑则是从一个根节点开始,将写入转发到其他的节点
为了避免循环复制的问题,往往都会在发出的数据中加上自己的唯一标识符,当一个节点收到了自己的标识符标记的数据时候,就会忽略这些数据。避免循环复制。
最后就是无主复制
无主复制其实常用于NoSQL数据库,往往是在其中存在着名为coordinator 协调者的概念
利用了Quorum NWR 和 Gossip协议来进行实现
这里简单的介绍下
NWR就是每一条数据都具有一个类似时间戳的标记,可以区分数据的先后
而假设一个集群具有n个副本,那么这个集群中需要保证写入w个节点,并且读取时查询r个节点,从而确定最新的值,这就好比有三个副本,写入时候写入2个节点就认为成功,读取的时候读取2个节点就认为成功,只要w+r > n 那么就可以认为每次去读都可以获取最新的值
常见的设置就是n是一个奇数,而w=r = (n+1)/2 向上取整
利用NWR的机制,我们可以做到每次读取到最新数据
其次是Gossip,这个机制是一种自我纠错的机制,也就是集群内部会定期的进行通信,从而同步最新数据
而且当节点失效的时候,只要还满足大于w并且r就可以
如果节点小于w或者r,则写入会出现错误
但即使是wrn,也存在一些情况,是会返回陈旧值的
最简单就是w+r < n 如果是希望能够快速的读取和写入,而且能够容忍一定的延迟的话,这样是可以设置的
其次是两个写入或者写和读同时操作,都是可能出现读取到老值的情况
而作者提到了一种,携带新的值得节点发生了故障,需要从老的副本中进行恢复,导致一段时间内存在老值的节点小于w,可能读到老值,这种情况符合最终一致性,所以个人认为还是可以接受的。
其次还有种情况,就是集群具有大于n个节点的时候,如果集群出现了故障,那么无法达到了w或者r个节点的数量的时候,可以先写入集群中n之外的节点,直到等待n之内的节点上线之后,再将数据从n之外的副本同步到n之内的副本中。
这种情况下,虽然可以写入w个节点,但是由于可能写入到n之外的节点,所以读的时候,不能读到最新的值。
而且在多个数据中心的集群中,往往只需要在本地数据中心满足nwr即可,其他数据中心还是类似多主复制的异步同步。
最后借助这个机会,讨论下分布式中的并发写入
常见的解决方案被称为LWW Last write wins 最后写入胜利,也就是副本只存储最新的数据,每个写入都带有一个时间戳,然后挑选最大的时间戳作为最近的,其他没这么近的都丢弃。不过这必然带来了写入丢失的问题,但是对于无主复制这也是可以接受的。
其次还有种解决写入丢失的问题,将数据进行合并,不过在讲解这种合并之前,我们需要了解什么是并发。
简单来说,如果两个操作,是具有先后顺序的,那么我们说他们具有因果依赖关系
相反如果两个操作是两个客户端,不知道彼此也在写入的话,那么这两个操作不存在因果关系,也就是并发
那么并发的话可以如下进行解决
介绍服务器为每一个键都维护一个版本号,每次写入的时候都递增版本,并返回给客户端版本号
每次客户端写入的时候,携带着版本号去读取一次,这样服务器可以将之前的所有值合并返回,让客户端合并之后写入。
服务器端会再次生成更高的版本号来保存所有值。
这种交由客户端来合并写入值的方式,也在Riak中体现到了
如果希望在这种架构中进行移除,则以考虑加上删除标记,这里称为墓碑,从而标记删除。
当然如果是显式的由应用代码区合并的话,是复杂且容易出错的,所以设计了特别的数据结构来自动执行合并,比如Riak设计的CRDT就能自动的合并数据,并且自动删除。
那么总结一下本章
我们讨论了下复制的概念
从复制解决问题来进行讨论,解决高可用性 降低延迟,增加可伸缩性来探讨
之后是按照复制的不同架构来进行讲解
分别是单主复制,客户端先写入到某个节点,然后由这个主库来进行同步
多主复制,每次写入发到某几个主库节点之一,任何一个主库都可以接受写入,主库再进行同步
无主复制,客户端将每个写入发送到几个节点,从多个节点并行读取
并且还探讨了一些常见问题,
写后读一致性,单调读,一致前缀读