Replication(复制)
Replication就是把相同的数据复制到多个机器上。
有如下几个原因
- 让数据的地理位置离用户更近(以减少延迟)
- 保证系统仍然能提供正常的服务即使某些机器宕机(提高可用性)
- scale out(横向扩展)机器可以提供读服务(提高吞吐量)
在本节我们假设数据集足够小,每个机器都可以存储整个数据集。
如果你要复制的数据不随着时间变化,replication就很简单。你只需复制数据到每一个节点,然后就做完了。但是现实却不是这样,当你复制数据的时候,源数据可能发生变化,replication的难点就在于此。
接下来我们会讨论三个流行的算法来处理处理节点间数据的变化:single-leader, multi-leader, and leaderless replication 。几乎所有分布式数据库都会用这些算法,每个算法都有各自的优缺点。
Leaders and Followers(主从复制)
每个节点保存数据库的一个copy,这个节点叫做replica(复制品),当replica越来越多的时候问题就来了,怎么保证数据都会到达每个replica上。
每一次写数据到数据库,其他的replicas都要写这份数据,否则大家的数据就不一致了。
常见的解决方案是leader-based复制,又叫主从复制。
- replicas之一被指定为leader/master,当客户端写数据的时候,必须要发请求给leader节点,leader节点把新数据写到本地。
- 其他的replicas被叫做followers,无论什么时候当leader节点向本地写数据的时候,他都会把数据变化发给其他的followers,作为replication log 或 change stream的一部分。每一个followers从leader拿到log之后,按照log上的数据变化顺序更新自己的copy。
- 当客户端想要读数据的时候,可以从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节点过程大致如下:
- 在某个时间点拍一个数据库的快照
- 把快照复制到新follower节点上
- follower节点连接leader节点,然后请求所有从拍快照起开始的所有数据变化。快照应该与leader的replication log中的精确位置相关联。这个“位置”有很多名字,在PostgreSQL叫log sequence number,在MySQL中叫binlog coordinates
- 当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的数据变化。这个过程叫做故障转移。
故障转移可以手动操作也可以自动完成。一个自动的故障转移流程包括以下几步:
- 确认leader挂了。死机,断电,网络问题都可能是原因。目前没有一个完美的方法可以探测到底是怎么挂的。所以大部分系统都用timeout:节点之间互相发消息,如果某个节点在一定时间内没有响应,我们就假设这个节点挂了。
- 选新leader。选新leader可以通过一个选举流程(大部分replicas选举的),也可以被一个以前选举的controller节点来指定。最佳候选人是和leader数据最接近的那个(最小化数据丢失)。让所有节点都同意以个新leader是consensus问题,以后会详细讨论。
- 重新配置系统去用新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一致了。所以叫做最终一致。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。