第5节 复制(replication)【设计数据密集型应用】

Replication(复制)

Replication就是把相同的数据复制到多个机器上。
有以下几个缘由mysql

  • 让数据的地理位置离用户更近(以减小延迟)
  • 保证系统仍然能提供正常的服务即便某些机器宕机(提升可用性)
  • scale out(横向扩展)机器能够提供读服务(提升吞吐量)

在本节咱们假设数据集足够小,每一个机器均可以存储整个数据集。git

若是你要复制的数据不随着时间变化,replication就很简单。你只需复制数据到每个节点,而后就作完了。可是现实却不是这样,当你复制数据的时候,源数据可能发生变化,replication的难点就在于此。github

接下来咱们会讨论三个流行的算法来处理处理节点间数据的变化:single-leader, multi-leader, and leaderless replication 。几乎全部分布式数据库都会用这些算法,每一个算法都有各自的优缺点。算法

Leaders and Followers(主从复制)

每一个节点保存数据库的一个copy,这个节点叫作replica(复制品),当replica愈来愈多的时候问题就来了,怎么保证数据都会到达每一个replica上。sql

每一次写数据到数据库,其余的replicas都要写这份数据,不然你们的数据就不一致了。数据库

常见的解决方案是leader-based复制,又叫主从复制。缓存

  1. replicas之一被指定为leader/master,当客户端写数据的时候,必需要发请求给leader节点,leader节点把新数据写到本地。
  2. 其余的replicas被叫作followers,不管何时当leader节点向本地写数据的时候,他都会把数据变化发给其余的followers,做为replication log 或 change stream的一部分。每个followers从leader拿到log以后,按照log上的数据变化顺序更新本身的copy。
  3. 当客户端想要读数据的时候,能够从leader或followers节点读,可是只有leader能够写。

图片描述

replication是许多关系数据库的内置特性,好比:PostgreSQL (since version 9.0), MySQL, Oracle Data Guard [2], and SQL Server’s AlwaysOn Availability Groups [3].网络

非关系数据库也在用,好比:MongoDB, RethinkDB, and Espresso数据结构

leader-based replication不只仅用于数据库,一些分布式消息中间件也在用,好比:Kafka,RabbitMQ,另一些文件系统,replicated block devices好比DRBD也是相似的原理架构

Synchronous Versus Asynchronous Replication(同步vs异步)

复制系统中重要一点是使用同步复制仍是异步复制。在关系数数据库中是能够配置的,其余系统通常是hardcode两者之一。

想一想图5-1发生了什么,当网站的用户更新本身的照片时,客户端发送更新请求给leader节点,leader节点收到以后,数据变化转发给follows,最终leader告诉客户端更新成功。

图5-2是系统中几个组件的交流状况:客户端,leader,followers,时间从左到右,请求和响应用箭头表示。

不难看出,follow1是同步复制,他复制完后会给leader响应,leader确认follower1复制完成后再告诉客户端更新成功。follower2相反是异步的,leader不会等待follower2,图中有明显的延迟。

图片描述

同步复制的优势是follower节点都会确保有最新的copy。若是leader节点挂了,follower节点还有完整的数据能够用。缺点是follower节点若是因为某些缘由(网络延迟,机器挂了等)不能响应,那么leader节点就得傻等着,写请求也不能处理。leader节点必须锁住全部写操做,直到同步复制响应或恢复正常。

因为以上缘由,让全部follower节点都同步复制是不现实的:任何一个节点中断都会致使整个系统暂停。在实战中,若是你在数据库中启动同步复制,通常是一个follower同步复制,其余followers异步复制,若是同步复制的节点很慢或不可用,那么会把另一个follower节点变成同步复制,从而这样就保证了至少两个节点拥有完整的copy:leader和同步复制的follower。这种配置被叫作semi-synchronous (半同步)
一般leader-based replication会被配置成彻底异步复制的。在这种状况中,若是leader挂了或不能回复,任何尚未来得及在followers上复制的的“数据变化”都会丢失。也便是说,写操做不保证持久化,即便已经被客户端确认。然而彻底异步复制的好处是leader能够继续进行写操做,即便全部followers节点挂了。

弱持久看起来是个很差的妥协,可是asynchronous replication却被普遍应用,尤为存在不少followers或者分布在不一样的地理位置。

Setting Up New Followers(设置新follower)

有时你须要添加新的followers节点,要么增长replicas的数量,要么替换失败的节点。问题来了,你怎么确保新的follower能获得准确的leader数据的copy呢?

简单的copy是远远不够的,由于客户端在持续的向数据库写数据,数据一直在不断的变化,因此在不用的时间点复制数据会产生不一样的版本。这样的结果是没有意义的。

添加新follower节点过程大体以下:

  1. 在某个时间点拍一个数据库的快照
  2. 把快照复制到新follower节点上
  3. follower节点链接leader节点,而后请求全部从拍快照起开始的全部数据变化。快照应该与leader的replication log中的精确位置相关联。这个“位置”有不少名字,在PostgreSQL叫log sequence number,在MySQL中叫binlog coordinates
  4. 当follower处理完从拍快照开始积压的数据变化后,咱们就说它跟上了。而后follower就能够继续处理来自leader数据变化了。

添加新follower在不一样数据库中的实现各有不一样。一些数据库是全自动实现,一些须要手动实现。

Handling Node Outages处理节点故障

系统中任何节点均可能挂掉,可能因为意外,也极可能因为维护重启。在重启的同时又不影响系统正常提供服务对于运维来讲是颇有利的。咱们的目标是保持整个系统运行,尽管有某些节点挂掉,同时将单点故障带来的影响降到最低。

那么咱们怎么用leader-based replication实现高可用呢?

Follower failure: Catch-up recovery (follower节点故障:catch-up恢复)
在本地磁盘上,每一个follower节点都保存一个log文件,用来记录来自leader的数据变化。若是follower节点挂了或者重启,或者leader和follower节点之间的网络中断了。follower节点能很快恢复,由于log记录了故障发生前的最后一次transaction。当follower连上leader以后,就能够请求获得因为故障而缺失的数据变化,把缺失补回来以后,就和leader一致了,而后就能够继续接受leader的数据变化流了。

Leader failure: Failover (leader节点故障:故障转移)

处理leader节点故障更棘手:一个follower节点要被晋升为leader,客户端要被从新配置,而后把写请求发给新leader。其余的followers开始接受新leader的数据变化。这个过程叫作故障转移

故障转移能够手动操做也能够自动完成。一个自动的故障转移流程包括如下几步:

  1. 确认leader挂了。死机,断电,网络问题均可能是缘由。目前没有一个完美的方法能够探测究竟是怎么挂的。因此大部分系统都用timeout:节点之间互相发消息,若是某个节点在必定时间内没有响应,咱们就假设这个节点挂了。
  2. 选新leader。选新leader能够经过一个选举流程(大部分replicas选举的),也能够被一个之前选举的controller节点来指定。最佳候选人是和leader数据最接近的那个(最小化数据丢失)。让全部节点都赞成以个新leader是consensus问题,之后会详细讨论。
  3. 从新配置系统去用新leader。客户端须要发送新的写请求到新leader。若是老leader回来后,其余节点可能忘了老leader已经下台了,可能还会把老leader当作leader。因此系统须要确保老leader变成follower并且可以识别新的leader。

故障转移充满各类容易出错的点:

  • 若是采用异步复制,老leader挂了,新leader可能尚未接收到全部来自老leader写请求。若是新leader上台后,老leader又加入了集群,会发生什么?新leader可能既接受老leader的写请求又接受客户端的写请求,这样就会形成冲突。常见的方案是抛弃老leader的尚未复制的写请求。这样会违反客户对数据持久化的指望。
  • 若是数据库以外的存储系统须要和数据库中的内容须要协调,那么直接抛弃写请求是很是危险的。好比github的一次事故,一个过期(落后于当前leader)的follower被选为新leader,数据库用的是自动增加主键,过期的follower因为落后于老leader而重用了部分已经用过的主键(老leader分配的),这些重用的主键Redis也在用,因此就致使了数据库和Redis的数据不一致(一些私有数据被暴露给错误的用户)。
  • 在某些场景,可能出现两个节点都认为本身是leader,这种状况叫作split brain,并且是很是危险的。由于两个节点同时接受写,没有进程来解决冲突,数据极可能丢失或崩溃。一些系统有本身的机制去关闭一个节点,可是这种机制设计很差的话,就容易关闭两个节点。
  • timeout多久合适(在leader宣布死亡以前)?timeout时间长,节点故障恢复时间就长。timeout时间短,就会形成不少没必要要的failover。好比一个加载高峰致使响应时间变长,网络小故障致使数据包延误。若是系统面临着高负载或者恶略的网络环境,没必要要的failover会使整个情况更糟糕。

目前没有容易的方案来解决这种问题,因此不少运维团队宁愿选择手动failover,即便系统支持自动failover。

节点失败,网络不可靠,围绕数据复制一致性的权衡,持久化,可用性,延迟是分布式中的基本问题,咱们会在后面章节详细讨论。

Implementation of Replication Logs

基于leader的replication在后台是怎么运行的?在实战中有几个不一样的方法在使用,让咱们一个个看。

Statement-based replication (基于语句的复制)
最简单的案例,leader记录下每一个写请求(statement语句),把写请求语句发给followers。对于关系数据库,就是把INSERT, UPDATE, or DELETE 语句发给followers,而后follower解析执行SQL语句。

这种方法看起来不错,可是在复制过程当中可能失败:

  • 调用不肯定函数的语句,好比调用了NOW(),RAND()的语句在不一样节点会产生不一样的值。
  • 若是语句用的是自增加的列,或者他们基于数据库已有的数据(e.g., UPDATE ... WHERE <some condition>),在每一个节点上他们必须按照特定的顺序执行,不然会有不一样的效果。当有并发transaction同时执行时,这会成为限制。
  • 语句有反作用(e.g., triggers, stored procedures, user-defined functions) 在每一个节点上会产生不一样的反作用,除非反作用是彻底肯定的。

我也能够绕过这些问题,好比在语句被记录下的时候,把不肯定的函数调用全换成肯定的返回值。这样每一个follower都会获得相同的值。然而,现实场景会有大量的边界案例,因此其余的replication方法会被预先考虑。

Mysql5.1版本以前采用的是基于语句的replication。今天仍在应用,由于他很紧凑。在有不肯定的语句中,Mysql默认转换成基于行的replication。

Write-ahead log (WAL) shipping(预写日志运送)
第三节咱们讨论了,存储引擎怎么在磁盘上存储数据。咱们发现每次写都会追加到到一个日志中:

  • 在日志结构的存储引擎中(see “SSTables and LSM-Trees” on page 76),日志是主要的存储的地方。日志segment在后台进行压缩和垃圾回收。
  • 在B-Tree结构的存储引擎中(see “B-Trees” on page 79),它重写每一个独立的磁盘块,每一次修改首先写到预写日志中,这样节点挂了以后,索引就能从日志中恢复,从而数据一致。

不管哪一种状况,日志是一个只能追加的字节序列,它包括了全部的写操做。咱们可使用彻底相同的日志在另一个节点上去生成一份replica。除了把日志写到磁盘上,leader也会把经过网络把日志发到其余follower上。当follower处理日志的时候,它会生成一份和leader相同的数据结构。

这种replication方法被应用在PostgreSQL和Oracle等数据库。主要缺点是,日志描述的数据很详细,好比,一个WAL包含哪一个磁盘块的哪一个字节被改变。这就致使了replication和存储引擎牢牢耦合在一块儿。好比数据库的存储格式从一个版本换成另外一个版本,通常leader和follower不能运行不一样的版本,因此就不能顺利的升级版本。

这看起来是个小的实现细节,可是对运维有很大的影响。若是replication协议容许follower运行比leader更高的版本,那么能够先升级全部的followers,而后进行failover,把某个已升级的follower选为leader。若是replication协议不容许版本不匹配,WAL shipping一般是这样的,这种状况须要停机来升级。

Logical (row-based) log replication 基于逻辑日志
WAL shipping中replication和存储引擎使用一样的日志格式,这也是耦合的缘由。让存储引擎和replication用不一样的日志格式,就能够解耦了。这种日志叫逻辑日志(logical log),区分于存储引擎的物理数据表示。

对于关系数据库,逻辑日志是描述对数据库表写操做的序列记录,粒度是行(row)。

  • 插入一行,逻辑日志会包含全部列的值
  • 删除一行,逻辑日志会包含主键或惟一肯定一行的信息,若是没有主键,会包含全部的旧值。
  • 更新一行,逻辑日志会包含主键和须要更新的列。

一个修改好几行的transaction会生成这样的日志记录,紧接着是一条记录代表transaction被提交。mysql的binlog(若是配置成row-based replication)就会用这种方法。

因为逻辑日志和存储引擎内部解耦了,因此很容易向下兼容,这样leader和follower就能够用不一样的版本,或者用不一样的存储引擎。

逻辑日志也能够很简单的被外部的应用解析。若是你想把数据库的内容发给外部的应用,这点事很是有用的。好比把数据给数据仓库作离线分析,定制索引和缓存。这种技术叫change data capture,第11节接着讨论。

Trigger-based replication 基于触发器
以上讨论的replication方法都是在数据库内部实现的,没有任何的应用层的代码。在不少状况下,你可能须要更灵活。好比,replicate数据库数据的一部分,把数据从一个数据库复制到另外一个数据库,或者你须要添加解决冲突的逻辑代码,那么你就须要把replication挪到应用层。

一些工具,好比Oracle GoldenGate,应用可使用这些工具从日志中读取数据变化。另一个方法是用关系数据库的特性:trigger和stored procedure。

trigger可让你注册本身的代码,当数据变化时,代码会运行。trigger也能够把数据变化存到另外一张表,外部应用能够读着张表来获取数据变化。Databus for Oracle和 Bucardo for Postgres就是这样的。

Trigger-based replication比其余replication方法有更大的开销。对于素菊开内置的replication方法,更容易出问题和限制。而后这种方法因为他的灵活性而很是有用。

Problems with Replication Lag 延迟带来的问题

容忍单节点错误是使用replication的一个缘由,另外还有扩展性(增长节点从而处理更多的请求),延迟(根据地理位置来放置节点)。

Leader-based replication中,写要经过leader来执行,读能够经过全部节点执行。在多读少写的场景,简单的增长follower节点就能够减小leader的负担。而后这种read-scaling架构只适用于异步replication,若是使用同步replication,一个节点挂了,就会致使整部系统不可用。节点越多,越可能出现问题。因此彻底同步的replication是不可靠的。

不幸的是,当应用从一个异步的follower节点读数据后,它可能获得过期的数据,由于那个节点可能落后于leader节点。这就致使了明显的数据不一致,由于全部的写操做并无当即反应在followers节点上。这种不一致时暂时的,等你中止写数据后,过一会,followers节点都跟上后,就和leader一致了。因此叫作最终一致。

相关文章
相关标签/搜索