
我进入Tarantool核心团队,并参与数据库引擎的开发,服务器组件的内部通信和复制。 今天,我将告诉您复制的工作原理。
关于复制
复制是将数据从一个存储复制到另一个存储的过程。 每个副本称为副本。 如果需要备份,实现热备用或水平扩展系统,则可以使用复制。 为此,必须能够在群集的计算机网络的不同节点上使用相同的数据。
我们通过两种主要方式对复制进行分类:
- 方向:master-master或master-slave 。 主从复制是最简单的选择。 您有一个要在其上更改数据的节点。 您可以将这些更改转换到应用它们的其他节点。 使用主-主复制,可以一次对多个节点进行更改。 在这种情况下,每个节点本身都会更改其数据,并将对其他节点所做的更改应用于其自身。
- 操作模式:异步或同步 。 同步复制意味着在至少通过最少数量的群集节点传播更改之前,不会提交数据,也不会向用户确认复制。 在异步复制中,提交事务(提交)和与用户交互是两个独立的过程。 要提交数据,只需要将它们放入本地日志中,然后这些更改才能以某种方式传输到其他节点。 显然,由于这个原因,异步复制有许多副作用。
复制在Tarantool中如何工作?
Tarantool中的复制具有以下功能:
- 它是用基本的砖块构建的,您可以用它们来创建任何拓扑的集群。 每个这样的基本配置项目都是单向的,也就是说,您始终拥有主从设备。 主服务器执行一些操作并生成操作日志,该日志将在副本服务器上使用。
- Tarantool复制是异步的。 也就是说,系统会确认对您的提交,而不管该事务看到了多少个副本,已对其应用了多少个事务以及是否最终完成了。
- Tarantool中复制的另一个特性是它是基于行的。 Tarantool保留操作日志(WAL)。 该操作逐行到达该位置,也就是说,当空间中的某些Tapla改变时,此操作将作为一行写入日志。 之后,后台进程从日志中读取此行,并将其发送到副本。 母版有多少个副本,那么多的后台进程。 也就是说,复制到群集不同节点的每个过程都是与其他节点异步执行的。
- 每个群集节点都有其自己的唯一标识符,该标识符在创建节点时生成。 此外,该节点在群集中还具有一个标识符(成员编号)。 这是一个数字常数,在连接到群集时会分配给副本,并且在副本在群集中的整个生命周期中都会保留下来。
由于异步,数据被传递到延迟的副本。 也就是说,您进行了一些更改,系统确认了提交,该操作已经在主服务器上应用,但是在副本服务器上将应用延迟,这取决于后台复制进程读取该操作,将其发送到副本的速度以及该应用程序是否适用。 。
因此,可能会出现数据不同步的情况。 假设我们有几个更改互连数据的主机。 事实证明,您使用的操作是不可交换的,并且引用相同的数据,因此两个不同的群集成员将具有不同的数据版本。
如果Tarantool中的复制是单向主从,那么如何制作主-主? 非常简单:创建另一个复制通道,但方向相反。 您需要了解,在Tarantool中,主-主复制只是两个彼此独立的数据流的组合。
使用相同的原理,我们可以连接第三个主服务器,从而建立一个完整的网状网络,其中每个副本都是所有其他副本的主服务器和从属服务器。
请注意,不仅复制了在此主服务器上本地启动的那些操作,而且还复制了他通过复制协议从外部收到的那些操作。 在这种情况下,在1号副本上创建的更改将两次出现在3号副本上:直接并通过2号副本。此属性允许我们在不使用完整网格的情况下构建更复杂的拓扑。 假设这个。

所有三个母版共同构成了群集的全网状核心,并附加了一个单独的副本。 由于日志的代理是在每个主服务器上执行的,因此所有三个“干净”的从服务器将包含在任何群集节点上执行的所有操作。
此配置可用于多种任务。 您无法在群集的所有节点之间创建冗余链接,并且如果副本位于附近,则副本将具有最小的延迟的主副本。 所有这些操作都是使用基本的主从复制元素完成的。
标记集群操作
出现了一个问题:
如果在所有集群成员之间代理了操作,并且多次访问每个副本,那么我们如何理解需要执行哪些操作而又不需要执行呢? 这需要过滤机制。 从日志读取的每个操作都分配有两个属性:
- 在其上启动此操作的服务器的标识符。
- 服务器上的操作的序号lsn是其发起者。 每个服务器在执行操作时,会为每个接收到的日志行分配一个递增的数字:1、2、3、4、5、6、7、8、9、10 ...因此,如果我们知道对于具有特定标识符的服务器,我们将操作应用于lsn 10,则不需要通过其他复制通道对lsn 9、8、7、10进行操作。 相反,我们应用以下内容:11、12,依此类推。
副本状态
Tarantool如何存储有关已应用的操作的信息? 为此,有一个Vclock时钟-这是应用于集群中每个节点的最后一个lsn的向量。
[lsn 1 , lsn 2 , lsn n ]
其中,
lsn i
是来自服务器的标识符为i的最后一次已知操作的编号。
Vclock也可以称为此副本已知的整个群集状态的某个快照。 知道已到达的操作的服务器ID后,我们将所需的本地Vclock组件隔离开来,将接收到的lsn与lsn操作进行比较,然后决定是否使用此操作。 结果,特定主机启动的操作将被顺序发送和应用。 同时,由于异步复制,可以将在不同主服务器上创建的工作流相互混合。
集群创建
假设我们有一个由两个元素master和slave组成的集群,我们想将第三个实例连接到它。 它具有唯一的UUID,但还没有群集标识符。 如果Tarantool尚未初始化,想加入集群,它必须将JOIN操作发送给可以执行集群的主机之一,即它处于读写模式。 作为对JOIN的响应,主服务器将其本地快照发送到连接副本。 副本将在家里滚动,但它仍没有标识符。 现在,稍微滞后的副本将与群集同步。 之后,运行JOIN的主服务器为此副本分配一个标识符,该标识符被记录并发送到副本。 将标识符分配给副本后,它便成为完整的节点,此后,它可以启动日志复制到其一侧。
日志中的行从此副本的状态开始发送,即从主服务器请求复制日志时开始,即从JOIN过程中接收到的vclock,或从副本停止的位置开始。 如果副本由于某种原因掉线,那么下次它连接到群集时,它将不再执行JOIN,因为它已经具有本地快照。 她只要求她在群集中不在期间发生的所有操作。
在集群中注册副本
为了存储有关群集结构的状态,使用了特殊的空间-群集。 它包含集群中的服务器标识符,它们的序列号和唯一标识符。
[1, 'c35b285c-c5b1-4bbe-83b1-b825eb594aa4']
[2, '37b12cb7-d324-4d75-b428-cde92c18e708']
[3, 'b72b1aa6-42a0-4d73-a611-900e44cdd465']
不需要按顺序排列标识符,因为可以删除节点并添加节点。
这是第一个陷阱。 通常,群集不是由一个节点收集的:您运行某个应用程序,它会立即部署整个群集。 但是,Tarantool中的复制是异步的。 如果两个主节点同时连接新节点并为其分配相同的标识符怎么办? 会有冲突。
这是一个错误和正确的JOIN的示例:

我们有两个希望连接的母版和两个副本。 他们在不同的母版上加入JOIN。 假设副本获得相同的标识符。 然后,主服务器和设法复制其日志的服务器之间的复制将崩溃,集群将崩溃。
为防止这种情况发生,您需要随时严格在一个主服务器上启动副本。 为此,Tarantool引入了诸如初始化领导者的概念,并实现了选择该领导者的算法。 想要连接到群集的副本首先与它从传输的配置中知道的所有主数据库建立连接。 然后,副本选择已启动的副本(在部署群集时,并非所有节点都能赚到钱)。 然后从中选择可用于录制的母版。 在Tarantool中,有读写和只读两种,我们无法在只读节点上注册。 之后,从过滤节点列表中,选择具有最低UUID的节点。
如果在连接到集群的未初始化实例上使用相同的配置和相同的服务器列表,则它们将选择相同的主服务器,这意味着JOIN最有可能成功。
从这里我们得出一个规则:将副本并行连接到群集时,所有这些副本必须具有相同的复制配置。 如果我们在某处省略某些内容,则可能会在不同的主服务器上启动具有不同配置的实例,并且群集将无法组装。
假设我们弄错了,或者管理员忘了修复配置,或者Ansible坏了,集群仍然崩溃了。 有什么可以证明这一点? 首先,可插拔副本将无法创建其本地快照:副本不会启动并报告错误。 其次,在日志的母版上,我们将看到与空间集群冲突有关的错误。
我们如何解决这种情况? 很简单:
- 首先,我们需要验证为连接副本设置的配置,因为如果不修复它,那么其他所有内容将无用。
- 之后,我们清除集群中的冲突并拍照。
现在,您可以尝试再次初始化副本。
解决冲突
因此,我们创建了一个集群并进行了连接。 所有节点都以订阅模式工作,也就是说,它们接收由不同主服务器生成的更改。 由于复制是异步的,因此可能发生冲突。 当您同时在不同的主数据库上更改数据时,不同的副本将获得数据的不同副本,因为可以按不同的顺序应用操作。
这是执行JOIN之后的示例集群:
我们有三个主从站,它们之间传输日志,它们以不同方向代理,并应用于从站。 数据不同步意味着每个副本将具有其自己的vclock更改历史记录,因为来自不同主服务器的流可以混合在一起。 但是,实例上的操作顺序可能会有所不同。 如果我们的操作不是可交换的(例如REPLACE操作),那么我们在这些副本上接收的数据将有所不同。
一个小例子。 假设我们有两个主时钟,其中vclock = {0,0}。 两者都将执行两个操作,分别指定为op1、1,op1、1,op2、1,op2、2。 这是每个主机执行一次本地操作的第二个时间片:

绿色表示对相应的vclock组件进行了更改。 首先,两个主机都更改其vclock,然后第二个主机执行另一个本地操作并再次增加vclock。 第一个主机从第二个主机接收复制操作,这由第一个群集节点的vclock中的红色数字1表示。

然后,第二个主机从第一个主机接收操作,第二个主机从第二个主机接收第二个操作。 最后,第一个主机执行其最后一个操作,第二个主机接收它。

零时间量子中的vclock我们具有相同的-{0,0}。 在最后一个时间范围内,我们还具有相同的vclock {2,2},似乎数据应该相同。 但是在每个主机上执行的操作顺序是不同的。 并且,如果这是一个替换操作,并且相同的键具有不同的值? 然后,尽管最后使用相同的vclock,我们将在两个副本上获得不同版本的数据。
我们也能够解决这种情况。
- 分片记录 。 首先,我们不能对随机选择的副本执行写操作,而是以某种方式对其进行分片。 他们只是打破了跨不同母版的写入操作,并最终获得了一致性系统。 例如,一个主机上的密钥已从1更改为10,另一主机上的密钥已从11更改为20-节点将交换其日志并获得完全相同的数据。
分片意味着我们有一个特定的路由器。 它完全不必是一个单独的实体,路由器可以是应用程序的一部分。 它可以是一个分片,将写操作应用于其自身或以一种或另一种方式将其转移到另一个主机。 但是它以这样的方式传递:关联值的更改将传递给某个特定的主机:一个值块传递给一个主机,另一个值传递给另一个主机。 在这种情况下,读取操作可以发送到群集中的任何节点。 并且不要忘了异步复制:如果您在同一母版上录制,则可能还需要从中读取。
- 操作的逻辑顺序 。 假设根据问题的情况,您可以以某种方式确定操作的优先级。 说,加上时间戳,版本或其他标签,使我们能够更早地了解实际发生的操作。 也就是说,我们正在谈论外部订购来源。
Tarantool有一个before_replace
触发器,可以在复制期间执行。 在这种情况下,我们不受路由请求的限制,我们可以将它们发送到任何需要的地方。 但是,当在数据流的输入端执行复制时,我们会有一个触发器。 他读取发送的行,将其与已存储的行进行比较,并确定哪一行具有更高的优先级。 就是说,触发器要么忽略复制请求,要么应用复制请求(可能需要进行必要的修改)。 我们已经采用了这种方法,尽管它也有其缺点。 首先,您需要一个外部时钟源。 假设移动电话沙龙中的操作员对订户进行了更改。 对于此类操作,您可以在运营商的计算机上使用时间,因为多个运营商不太可能同时在一个订户上进行更改。 操作可以以不同的方式进行,但是如果可以为每个操作分配特定的版本,则在通过触发器时,将仅保留那些相关的触发器。
该方法的第二个缺点:由于触发器适用于复制每个请求的复制所带来的每个增量,因此会产生额外的计算负担。 但随后,我们将在集群规模上获得一致的数据副本。
同步处理
我们的复制是异步的,也就是说,通过提交执行,您不知道此数据是否已经在其他群集节点上。 如果您对master进行了提交,则已确认您的提交,并且master由于某种原因而立即停止工作,那么您无法确定数据是否已保存在其他位置。 为了解决此问题,Tarantool复制协议具有一个ACK。 每个主机都掌握来自每个从机的最后一个ACK。
什么是ACK? 当从设备接收到带有主机lsn及其标识符标记的增量时,作为响应,它将发送一个特殊的ACK数据包,在应用此操作后,它将在其中打包其本地vclock。 让我们看看它如何工作。
我们有一位大师亲自进行了4次手术。 假设从属从属接收到前三行,并且其vclock增加到{3.0}。
ACK尚未到达。 接收到这三行后,从设备将发送ACK包,该ACK包在发送时已与其vclock相连。 让从属主机在同一时隙发送另一条线,即从属主机的vclock增加了。 基于此,第一号主站肯定知道他执行的前三个操作已应用于该从站。 这些状态为主机与之一起工作的所有从机存储;它们是完全独立的。
最后,从机以第四个ACK数据包响应。 之后,主机知道从机已与其同步。
此机制可以在应用程序代码中使用。 提交操作时,您不会立即确认用户,而是先调用特殊功能。 提交完成时,它等待主机知道的lsn从服务器等于主机的lsn。 因此,您无需等待完全同步,只需等待提到的那一刻。
假设我们的第一个电话更改了三行,而第二个电话更改了一行。 首次通话后,您要确保数据已同步。 上面显示的状态已经意味着第一个调用已在至少一个从属设备上同步。
在哪里可以找到有关此信息的确切位置,我们将在下一部分中考虑。
监控方式
如果复制是同步的,则监视非常简单:如果复制失败,则会向您的操作发出错误。 而且,如果复制是异步的,那么情况将变得混乱。 师父回答您一切都很好,它可以正常工作,被接受并写下来。 但是同时,所有副本都已失效,数据没有冗余,如果丢失了主数据库,则将丢失数据。 因此,我真的很想监视集群,了解异步复制的情况,副本的位置以及副本的状态。
对于基本监视,Tarantool有一个box.info实体。 值得在控制台中调用它,因为您将看到有趣的数据。
id: 1 uuid: c35b285c-c5b1-4bbe-83b1-b825eb594aa4 lsn : 5 vclock : {2: 1, 1: 5} replication : 1: id: 1 uuid : c35b285c -c5b1 -4 bbe -83b1 - b825eb594aa4 lsn : 5 2: id: 2 uuid : 37 b12cb7 -d324 -4 d75 -b428 - cde92c18e708 lsn : 1 upstream : status : follow idle : 0.30358312401222 peer : lag: 3.6001205444336 e -05 downstream : vclock : {2: 1, 1: 5}
最重要的指标是id
id
。 在这种情况下,1表示该主机的lsn将存储在所有vclock的第一位置。 一个非常有用的东西。 如果您与JOIN发生冲突,则只能通过唯一标识符将一个主机与另一个主机区分开。 同样,本地数量包括lsn之类的数量。 这是该主节点执行并写入其日志的最后一行的编号。 在我们的示例中,第一个主机执行了五个操作。 Vclock是他知道自己适用于自己的操作状态。 最后,对于主号码2,他执行了其中一项复制操作。
在指示本地状态之后,您可以看到该实例对集群复制状态的了解;为此,有一个
replication
部分。 它列出了实例已知的所有群集节点,包括自身。 第一节点具有标识符1,id对应于当前实例。 第二个节点的标识符为2,其lsn 1对应于写入vclock的lsn。 在这种情况下,我们考虑主-主复制,当第1个主节点既是集群第二个节点的主节点又是集群的从节点时,即跟随它的节点。
upstream
的本质。 status follow
属性表示主机1跟随主机2。空闲是指自从与该主机最后一次交互以来在本地经过的时间。 我们不会连续发送流,master仅在发生变化时才发送增量。 当我们发送某种ACK时,我们也会进行通信。 显然,如果空闲时间变大(几秒钟,几分钟,几小时),则说明出现了问题。lag
属性。 我们谈到了滞后。 除了lsn和server id
日志中的每个操作还标有时间戳记-该操作在执行该操作的主服务器上的vclock中记录的本地时间。 同时,从站将自己的本地时间戳与收到的增量时间戳进行比较。 从站将在监视中显示最后一行收到的最新当前时间戳。downstream
属性。 它显示了主人了解他的特定奴隶。 这是从站发送给他的ACK。 上面显示的downstream
意味着他的奴隶(又名2)主人最后一次向他发送他的vclock,即5.1。 这位大师知道他在自己的位置完成的所有5条生产线都移至另一个节点。
XLOG丢失
考虑一下师父倒下的情况。
lsn : 0 id: 3 replication : 1: <...> upstream : status: disconnected peer : lag: 3.9100646972656 e -05 idle: 1602.836148153 message: connect, called on fd 13, aka [::1]:37960 2: <...> upstream : status : follow idle : 0.65611373598222 peer : lag: 1.9550323486328 e -05 3: <...> vclock : {2: 2, 1: 5}
首先,状态将改变。
Lag
不会改变,因为我们应用的行保持不变,我们没有得到任何新的行。 同时,
idle
时间
idle
增长,在这种情况下,
idle
时间已经等于1602秒,因此主设备死了很多时间。 并且我们看到一些错误消息:没有网络连接。
在类似情况下该怎么办? 我们确定主机发生了什么,吸引管理员,重新启动服务器,提升节点。 重复执行复制,当主服务器进入系统时,我们连接到该系统,订阅其XLOG,自己获得它们,集群稳定。
但是有一个小问题。 想象一下,我们有一个奴隶,由于某种原因,奴隶被关闭了很长一段时间。 在此期间,服务它的主服务器删除了XLOG。 例如,磁盘已满,垃圾收集器已收集日志。 返回的奴隶如何继续? 没办法 因为他需要应用以与集群同步的日志已消失,并且没有地方可以使用它们。 在这种情况下,我们将看到一个有趣的错误:状态不再
disconnected
,而是
stopped
。 还有一条特定的消息:没有与lsn匹配的日志文件。
id: 3 replication : 1: <...> upstream : peer : status: stopped lag : 0.0001683235168457 idle : 9.4331328970147 message: 'Missing .xlog file between LSN 7 1: 5, 2: 2 and 8 1: 6, 2: 2' 2: <...> 3: <...> vclock : {2: 2, 1: 5}
实际上,这种情况并不总是致命的。 假设我们有两个以上的母版,并且在其中一些上仍保留了这些日志。 我们将它们一次倒给所有主人,而不是仅将它们存放在一个主人上。 然后事实证明,该副本连接到她知道的所有母版上,在其中一些母版上可以找到她所需的日志。 她将在家中执行所有这些操作,她的vclock将增加,并且将达到集群的当前状态。 之后,您可以尝试重新连接。
如果根本没有日志,我们将无法继续复制副本。 它仅用于重新初始化。 记住它的唯一标识符,您可以将其写在纸上或文件中。 然后,我们在本地清理副本:删除其映像,日志等。 之后,使用相同的UUID重新连接副本。
剥离群集或将UUID重新用于新副本:
box.cfg{instance_uuid = uuid}
。
, . UUID space cluster, . , . UUID, master, JOIN, , UUID, , .
, UUID , space cluster , . . , , .
, - . , . , , .
Tarantool .
replication_connect_quorum: 2
replication_connect_timeout: 30
replication_sync_lag: 0.1
, , , , , , master' 0,1 . 30 . , . 0,1 . , .
Keep alive
, ip tables drop. , - 30 30 , , . , keep alive-.
keep alive- :
box.cfg.replication_timeout
.
master' , keep alive-, , . 4 master slave keep alive- , . master'.
, . 6 , 5 . 10 , 9 . .
, , . , master', . - . .
6 , 3. , . , 5 , 3 .
, :
, Telegram-, . , GitHub, .