三、第5章节-复制
接下来是第5章的内容,这章节的内容主要是复制,所谓复制就是同一份数据保留多个副本。 复制数据的原因呢?
1️⃣ 使得数据与用户在地理上接近(从而减少延迟)
2️⃣ 即使系统的一部分出现故障,系统也能继续工作(从而提高可用性)
3️⃣ 扩展可以接受读请求的机器数量(从而提高读取吞吐量)
本章主要讨论三种变更复制算法:单主复制、多主复制、无主复制。当存在多个副本时,会不可避免的出现一个问题:如何确保所有数据都落在了所有的副本上?所以,复本机制真正的麻烦在于如何处理复制数据的变更 。我们来看一个非常普遍且常用解决方案:单主复制
3.1-单主复制
来具体看一个场景,更换新的用户头像的实例:
总结一下单主复制符合以下的特点:
1️⃣ 多个副本中只有一个设置为leader,其他是flower
2️⃣ leader接受读请求和写请求,follower只接受读请求
3️⃣ follower从leader拉取日志,更新本地数据库副本
这种复制模式是很多关系型数据库内置的功能,比如PostgreSQL(9.0之后),MySQL,SQL Server,文档型数据库 MongoDB。基于领导者的复制不局限于数据库,像一些高可用的分布式MQ也在用,比如Kafka、RabbitMQ
关于主从和主备,主从中从是向外提供服务的,而主备中的备不是对外提供的,备的作用是待主crash的时候成为主
🎈过度🎈
接下来呢,我们讨论复制系统的一个重要细节:复制是**同步(synchronously)发生还是异步(asynchronously)**发生
3.2-同步/异步
如下图:
1️⃣ 用户id为1234的用户向主库提交数据变更请求
2️⃣ 主库将数据变更同步给从库1,并且等待从库1的响应,这里从库1复制的方式是同步
3️⃣ 主库将数据变更同步给从库2,但是不等待从库2的确认,这里从库2的复制方式是异步
整体的配置方式也被称作是半同步。我们来一起看下同步和异步复制的优劣势:
🅰️ 同步复制能够保证数据可靠性,但是如果从库迟迟不能响应主库,主库就不能接收新的读写请求
🅱️ 异步复制的优点是,即便从库落后了,主库也可以继续处理写入请求
我们再来看一下设置新从库的步骤:
1️⃣ 获取某个时刻主库的一致性快照
2️⃣ 将快照复制到从库节点
3️⃣ 从库连接主库,拉取快照之后发生的数据变更。拉取快照之后的变更,往往快照和主库复制日志关联,不同的数据库对于这个关联关系实现有着不同的名称:
- PostgreSQL 的叫做日志序列号(log sequence number, LSN)
- MySQL将其称为 二进制日志坐标(binlog coordinates)
🎈过度🎈 从库失效的问题很好解决,从库在重新和主库建立连接之后,可以从日志知道最后一个失败的事务。然后开始追赶主库。如果主库失效了,该如何处理呢?
3.3-处理故障节点
主库失效如何处理?
1️⃣ 确认主库失效
2️⃣ 选择一个新的主库,让所有的副本达成一致意见(共识问题)
3️⃣ 重新配置新的主库,并且启用新的主库
一些挑战:
- 如果使用异步复制,发生的数据丢失问题,GitHub,MySQL从库切换事故
- 脑裂的情况,设置新的主库之后,老的主库又一次活过来了,可能会存在两个主库的情况,同时接受写入,可能会导致数据损坏,解决的方案可能是,发送kill 去干掉一个主库
- 宣告主库死亡的阈值,主库在宣告死亡前,超时时间的设置。主库失效时,太长意味着恢复时间长,太短就会发生不必要的切换
基于主库的复制底层是如何工作的?
1️⃣ 基于语句的复制,有非确定性函数、自增列的问题
2️⃣ 传输预写式日志(WAL),日志包含所有数据库写入的仅追加字节序列,可以将其发给从库,比如说PostgreSQL 、Oracle。缺点是数据过于底层,WAL包含哪些磁盘块中的哪些字节发生了更改。这使复制与存储引擎紧密耦合,对数据库版本不友好,会对运维升级数据库造成困难
3️⃣ 逻辑日志复制(基于行),也即复制日志和存储引擎存储的日志采用不同的格式,这种复制日志被称为逻辑日志,逻辑日志有以下特点:
- 对于插入的行,日志包含所有列的新值
- 对于删除的行,日志包含足够的信息来唯一标识已删除的行。通常是主键,但是如果表上没有主键,则需要记录所有列的旧值
- 对于更新的行,日志包含足够的信息来唯一标识更新的行,以及所有列的新值(至少所有已更改的列的新值)
逻辑日志,可使领导者和跟随者能够运行不同版本的数据库软件甚至不同的存储引擎。MySQL的binlog日志有三种格式,分别是statement、row、mixed,现通常使用row模式,即逻辑日志的形式,该模式下数据库的变更流可以应用在MySQL从库,或者Debezium/Cancel解析后推送至三方系统(消息代理Kafka、存储系统)
4️⃣基于触发器的复制,触发器能够实现,在数据库系统中发生数据更改(写入事务)时自动执行的自定义应用程序代码,不同于数据库系统实现的复制
四、第6章-分区
分区1是一种切分大数据集的方法,即一份数据切成多块,分区的目的是为了可扩展性,从而提高吞吐量。在实践中,分区通常和复制结合使用,每个分区的副本将处在多个节点上,以此获得容错能力。下图是主从复制模型下,分区和复制相结合的示意图:
4.1-键值数据的分区方式:
分区最核心的问题是
🅰️避免倾斜(skew),即热点数据处理,放的角度
🅱️ 处理访问路由问题,即查询性能的保证,取的角度
下面我们介绍几种常见的分区方式:
1️⃣ 按照key的范围分区,这种方式天生可以解决访问路由的问题,对于热点数据,可根据数据状况进行拆分,如下12号分区Hbase,BigTable使用这种策略。在处理时间范围分区时,为避免一直写当前时间对应分区的这种写入过载问题,可引入其他列值+时间做分区
2️⃣ 散列2分区。散列分区可以很好的处理热点问题,弊端是查询能力无法保证,因为曾经相邻的密钥分散在所有分区中,这意味着如果执行范围查询,则该查询将被发送到所有分区中
改进的办法是多个列组成的复合主键,键中只有第一列会作为散列的依据,而其他列则被用作SSTables中排序数据的索引,此时如果第一列已经指定了固定值,则可以对该键的其他列执行有效的范围扫描。例如,在社交媒体网站上,一个用户可能会发布很多更新。若更新的主键被选择为(user_id, update_timestamp)
,那么可以有效地检索特定用户在某个时间间隔内按时间戳排序的所有更新。Casssandra使用了这种优化方式
4.2-分区和二级索引
上文中我们讨论了键值数据模型的分区方案,次级索引是关系型数据库/文档型数据库的基础,也是Solr和ElasticSearch等搜索服务器的基石,次级索引由于不具备主键唯一的特性,导致我们并不能整齐的映射到各自的分区,有2种用二级索引对数据进行分区的方法:
🅰️ 基于文档的分区(docment-based),如下图在汽车表/文档上建立颜色和厂商的次级索引,这种索引方法中,每个分区是完全独立,每个分区维护自己的次级索引,因此,文档分区索引也叫做本地索引(local index)。在执行特定的颜色的搜索(look for red)的时候,需要将查询发送到所有分区,并合并所有返回的结果。故,这种分区查询数据库的方式有时被称为分散/聚集(scatter/gather),这种基于二级索引上的查询可能会相当昂贵
🅱️ 基于关键词(term-based)的分区,对所有分区的数据构建全局索引的同时,对全局索引进行分区。这种索引称为关键词分区(term-partitioned) 3,关键词分区的全局索引的优势在于不需要分散/收集所有分区,客户端只需要向包含关键词的分区发出请求。全局索引的缺点在于写入速度较慢且较为复杂,因为写入单个文档现在可能会影响索引的多个分区。在实践中,全局二级索引的更新通常是异步的
(在任何时候)使用关键词本身进行分区适用于范围扫描,而对关键词的哈希分区提供更好的负载均衡能力
4.3-分区再平衡
将负载(数据存储和读写请求)从集群中的一个节点向另一个节点移动的过程称为再平衡(reblancing),再平衡应该满足以下几个要求
- 再平衡之后,负载(数据存储,读取和写入请求)应该在集群中的节点之间公平地共享
- 再平衡发生时,数据库应该继续接受读取和写入
- 节点之间只移动必须的数据4,以便快速再平衡,并减少网络和磁盘I/O负载
相应的,我们有三种分区方式,来进行处理
1️⃣ 固定数量的分区,创建比节点更多的分区,并为每个节点分配多个分区,如果一个节点被添加到集群中,新节点可以从当前每个节点中窃取一些分区,直到分区再次公平分配,如下图。Riak,Elasticsearch使用了这种再平衡的分区方式
2️⃣动态分区,当分区增长到超过配置的大小时(在HBase上,默认值是10GB),会被分成两个分区,每个分区约占一半的数据,反之进行合并,类似B树的页分裂/合并的过程
3️⃣分区数与节点数成正比,节点数量不变时,每个分区的大小与数据集大小成比例地增长,节点数增加时,分区数也增加,分区数据变少
4.4-请求路由
数据集已经分割到多个机器上运行的多个节点上,那么当客户想要发出请求时,如何知道要连接哪个节点呢?即请求应该路由给谁?这个问题也可以概括为服务发现(service discovery),有目前以下三种方案:
1️⃣允许客户联系任何节点(例如,通过循环策略的负载均衡(Round-Robin Load Balancer))。如果该节点恰巧拥有请求的分区,则它可以直接处理该请求,否则,它将请求转发到适当的节点,接收回复并传递给客户端
2️⃣首先将所有来自客户端的请求发送到路由层,它决定了应该处理请求的节点,并相应地转发。此路由层本身不处理任何请求,它仅负责分区的负载均衡
3️⃣要求客户端知道分区和节点的分配。在这种情况下,客户端可以直接连接到适当的节点,而不需要任何中介
以上三种方式都会面临一个问题:作出路由决策的组件(节点之一/路由层/客户端)如何了解分区-节点之间的分配关系变化?许多分布式数据系统都依赖于一个独立的协调服务(如ZooKeeper)来跟踪集群元数据,每个节点在ZooKeeper中注册自己,ZooKeeper维护分区到节点的可靠映射。 其他参与者(如路由层或分区感知客户端)可以在ZooKeeper中订阅此信息。 只要分区分配发生的改变,或者集群中添加或删除了一个节点,ZooKeeper就会通知路由层使路由信息保持最新状态。HBase,SolrCloud和Kafka使用ZooKeeper来跟踪分区分配
🤣以上是《设计数据密集型应用》读书笔记的第2部分,欢迎吐槽,欢迎关注公众号 stackoverflow
分区有很多种叫法,比如Solr Cloud中被称为分片(shard),在HBase中称之为区域(Region),Bigtable/Kudu中则是表块(tablet,Cassandra和Riak中是虚节点(vnode), Couchbase中叫做虚桶(vBucket),但是分区(partition)是约定俗成的叫法 ↩︎
该分区方式依赖于散列函数,一个32位散列函数,无论何时给定一个新的字符串输入,它将返回一个0到 2 32 2^{32} 232 -1之间的"随机"数 ↩︎
我们搜索的关键词决定了次级索引的分区方式,因此称之为关键词索引,关键词(term)一词来源于全文搜索索引(一种特殊的次级索引)。 ↩︎
即一致性哈希解决的问题,一致性哈希的主要应用就是降低路由成本 ↩︎