homework-jianmu/documentation20/webdocs/markdowndocs/replica-ch.md

26 KiB
Raw Blame History

TDengine 2.0 数据复制模块设计

数据复制概述

数据复制(Replication)是指同一份数据在多个物理地点保存。它的目的是防止数据丢失,提高系统的高可用性(High Availability),而且通过应用访问多个副本,提升数据查询性能。

在高可靠的大数据系统里,数据复制是必不可少的一大功能。数据复制又分为实时复制与非实时复制。实时复制是指任何数据的更新(包括数据的增加、删除、修改)操作,会被实时的复制到所有副本,这样任何一台机器宕机或网络出现故障,整个系统还能提供最新的数据,保证系统的正常工作。而非实时复制,是指传统的数据备份操作,按照固定的时间周期,将一份数据全量或增量复制到其他地方。如果主节点宕机,副本是很大可能没有最新数据,因此在有些场景是无法满足要求的。

TDengine面向的是物联网场景需要支持数据的实时复制来最大程度保证系统的可靠性。实时复制有两种方式一种是异步复制一种是同步复制。异步复制(Asynchronous Replication)是指数据由Master转发给Slave后Master并不需要等待Slave回复确认这种方式效率高但有极小的概率会丢失数据。同步复制是指Master将数据转发给Slave后需要等待Slave的回复确认才会通知应用写入成功这种方式效率偏低但能保证数据绝不丢失。

数据复制是与数据存储写入、读取密切相关的但两者又是相对独立可以完全脱耦的。在TDengine系统中有两种不同类型的数据一种是时序数据由TSDB模块负责一种是元数据(Meta Data), 由MNODE负责。这两种性质不同的数据都需要同步功能。数据复制模块通过不同的实例启动配置参数为这两种类型数据都提供同步功能。

在阅读本文之前,请先阅读《TDengine 2.0 整体架构了解TDengine的集群设计和基本概念

特别注明:本文中提到数据更新操作包括数据的增加、删除与修改。

基本概念和定义

TDengine里存在vnode, mnode, vnode用来存储时序数据mnode用来存储元数据。但从同步数据复制的模块来看两者没有本质的区别因此本文里的虚拟节点不仅包括vnode, 也包括mnode, vgoup也指mnode group, 除非特别注明。

版本(version)

一个虚拟节点组里多个虚拟节点互为备份来保证数据的有效与可靠是依靠虚拟节点组的数据版本号来维持的。TDengine2.0设计里对于版本的定义如下客户端发起增加、删除、修改的流程无论是一条记录还是多条只要是在一个请求里这个数据更新请求被TDengine的一个虚拟节点收到后经过合法性检查后可以被写入系统时就会被分配一个版本号。这个版本号在一个虚拟节点里从1开始是单调连续递增的。无论这条记录是采集的时序数据还是meta data, 一样处理。当Master转发一个写入请求到slave时必须带上版本号。一个虚拟节点将一数据更新请求写入WAL时需要带上版本号。

不同虚拟节点组的数据版本号是完全独立的互不相干的。版本号本质上是数据更新记录的transaction ID但用来标识数据集的版本。

角色(role)

一个虚拟节点可以是master, slave, unsynced或offline状态。

  • master 具有最新的数据容许客户端往里写入数据一个虚拟节点组至多一个master.
  • slave与master是同步的但不容许客户端往里写入数据根据配置可以容许客户端对其进行查询。
  • unsynced: 节点处于非同步状态,比如虚拟节点刚启动、或与其他虚拟节点的连接出现故障等。处于该状态时,该虚拟节点既不能提供写入,也不能提供查询服务。
  • offline: 由于宕机或网络原因无法访问到某虚拟节点时其他虚拟节点将该虚拟节点标为离线。但请注意该虚拟节点本身的状态可能是unsynced或其他但不会是离线。

Quorum:

指数据写入成功所需要的确认数。对于异步复制quorum设为1具有master角色的虚拟节点自己确认即可。对于同步复制需要至少大于等于2。原则上Quorum >=1 并且 Quorum <= replication(副本数)。这个参数在启动一个同步模块实例时需要提供。

WAL

TDengine的WAL(Write Ahead Log)与cassandra的commit log, mySQL的bin log, Postgres的WAL没本质区别。没有写入数据库文件还保存在内存的数据都会先存在WAL。当数据已经成功写入数据库数据文件相应的WAL会被删除。但需要特别指明的是在TDengine系统里有几点

  • 每个虚拟节点有自己独立的wal
  • WAL里包含而且仅仅包含来自客户端的数据更新操作每个更新操作都会被打上一个版本号

复制实例:

复制模块只是一可执行的代码复制实例是指正在运行的复制模块的一个实例一个节点里可以存在多个实例。原则上一个节点有多少虚拟节点就可以启动多少实例。对于副本数为1的场景应用可以决定是否需要启动同步实例。应用启动一个同步模块的实例时需要提供的就是虚拟节点组的配置信息包括

  • 虚拟节点个数即replication number
  • 各虚拟节点所在节点的信息包括node的end point
  • quorum, 需要的数据写入成功的确认数
  • 虚拟节点的初始版本号

数据复制模块的基本工作原理

TDengine采取的是Master-Slave模式进行同步与流行的RAFT一致性算法比较一致。总结下来有几点

  1. 一个vgroup里有一到多个虚拟节点每个虚拟节点都有自己的角色
  2. 客户端只能向角色是master的虚拟节点发起数据更新操作因为master具有最新版本的数据如果向非Master发起数据更新操作会直接收到错误
  3. 客户端可以向master, 也可以向角色是Slave的虚拟节点发起查询操作但不能对unsynced的虚拟节点发起任何操作
  4. 如果master不存在这个vgroup是不能对外提供数据更新和查询服务的
  5. master收到客户端的数据更新操作时会将其转发给slave节点
  6. 一个虚拟节点的版本号比master低的时候会发起数据恢复流程成功后才会成为slave

数据实时复制有三个主要流程:选主、数据转发、数据恢复。后续做详细讨论。

虚拟节点之间的网络连接

虚拟节点之间通过TCP进行连接节点之间的状态交换、数据包的转发都是通过这个TCP连接(peerFd)进行。为避免竞争两个虚拟节点之间的TCP连接总是由IP地址(UINT32)小的节点作为TCP客户端发起。一旦TCP连接被中断虚拟节点能通过TCP socket自动检测到将对方标为offline。如果监测到任何错误比如数据恢复流程虚拟节点将主动重置该连接。

一旦作为客户端的节点连接不成或中断它将周期性的每隔一秒钟去试图去连接一次。因为TCP本身有心跳机制虚拟节点之间不再另行提供心跳。

如果一个unsynced节点要发起数据恢复流程它与Master将建立起专有的TCP连接(syncFd)。数据恢复完成后,该连接会被关闭。而且为限制资源的使用,系统只容许一定数量(配置参数tsMaxSyncNum)的数据恢复的socket存在。如果超过这个数字系统会将新的数据恢复请求延后处理。

任意一个节点无论有多少虚拟节点都会启动而且只会启动一个TCP server, 来接受来自其他虚拟节点的上述两类TCP的连接请求。当TCP socket建立起来客户端侧发送的消息体里会带有vgId全局唯一的vgroup ID), TCP 服务器侧会检查该vgId是否已经在该节点启动运行。如果已经启动运行就接受其请求。如果不存在就直接将连接请求关闭。在TDengine代码里mnode group的vgId设置为1。

选主流程

当同一组的两个虚拟节点之间(vnode A, vnode B)建立连接后他们互换status消息。status消息里包含本地存储的同一虚拟节点组内所有虚拟节点的role和version。

如果一个虚拟节点(vnode A)检测到与同一虚拟节点组内另外一虚拟节点vnode B的连接中断vnode A将立即把vnode B的role设置为offline。无论是接收到另外一虚拟节点发来的status消息还是检测与另外一虚拟节点的连接中断该虚拟节点都将进入状态处理流程。状态处理流程的规则如下

  1. 如果检测到在线的节点数没有超过一半则将自己的状态设置为unsynced.
  2. 如果在线的虚拟节点数超过一半会检查master节点是否存在如果存在则会决定是否将自己状态改为slave或启动数据恢复流程。
  3. 如果master不存在则会检查自己保存的各虚拟节点的状态信息与从另一节点接收到的是否一致如果一致说明节点组里状态已经稳定一致则会触发选举流程。如果不一致说明状态还没趋于一致即使master不存在也不进行选主。由于要求状态信息一致才进行选举每个虚拟节点根据同样的信息会选出同一个虚拟节点做master无需投票表决。
  4. 自己的状态是根据规则自己决定并修改的并不需要其他节点同意包括成为master。一个节点无权修改其他节点的状态。
  5. 如果一个虚拟节点检测到自己或其他虚拟节点的role发生改变该节点会广播它自己保存的各个虚拟节点的状态信息role和version)。

具体的流程图如下:

选择Master的具体规则如下

  1. 如果只有一个副本该副本永远就是master
  2. 所有副本都在线时版本最高的被选为master
  3. 在线的虚拟节点数过半而且有虚拟节点是slave的话该虚拟节点自动成为master
  4. 对于2和3如果多个虚拟节点满足成为master的要求那么虚拟节点组的节点列表里最前面的选为master

按照上面的规则如果所有虚拟节点都是unsynced(比如全部重启只有所有虚拟节点上线才能选出master该虚拟节点组才能开始对外提供服务。当一个虚拟节点的role发生改变时sync模块回通过回调函数notifyRole通知应用。

数据转发流程

如果vnode A是master, vnode B是slave, vnode A能接受客户端的写请求而vnode B不能。当vnode A收到写的请求后遵循下面的流程

  1. 应用对写请求做基本的合法性检查,通过,则给改请求包打上一个版本号(version, 单调递增)
  2. 应用将打上版本号的写请求封装一个WAL Head, 写入WAL(Write Ahead Log)
  3. 应用调用API syncForwardToPeer如多vnode B是slave状态sync模块将包含WAL Head的数据包通过Forward消息发送给vnode B否则就不转发。
  4. vnode B收到Forward消息后调用回调函数writeToCache, 交给应用处理
  5. vnode B应用在写入成功后都需要调用syncAckForward通知sync模块已经写入成功。
  6. 如果quorum大于1vnode B需要等待应用的回复确认收到确认后vnode B发送Forward Response消息给node A。
  7. 如果quorum大于1vnode A需要等待vnode B或其他副本对Forward消息的确认。
  8. 如果quorum大于1vnode A收到quorum-1条确认消息后调用回调函数confirmForward通知应用写入成功。
  9. 如果quorum为1上述678步不会发生。
  10. 如果要等待slave的确认master会启动2秒的定时器可配置如果超时则认为失败。

对于回复确认sync模块提供的是异步回调函数因此APP在调用syncForwardToPeer之后无需等待可以处理下一个操作。在Master与Slave的TCP连接管道里可能有多个Forward消息这些消息是严格按照应用提供的顺序排好的。对于Forward Response也是一样TCP管道里存在多个但都是排序好的。这个顺序SYNC模块并没有做特别的事情是由APP单线程顺序写来保证的(TDengine里每个vnode的写数据都是单线程

数据恢复流程

如果一虚拟节点(vnode B) 处于unsynced状态master存在vnode A)而且其版本号比master的低它将立即启动数据恢复流程。在理解恢复流程时需要澄清几个关于文件的概念和处理规则。

  1. 每个文件无论是archived data的file还是wal)都有一个index, 这需要应用来维护(vnode里该index就是fileId*3 + 0/1/2, 对应data, head与last三个文件。如果index为0表示系统里最老的数据文件。对于mode里的文件数量是固定的对应于acct, user, db, table等文件。
  2. 任何一个数据文件(file)有名字、大小还有一个magic number。只有文件名、大小与magic number一致时两个文件才判断是一样的无需同步。Magic number可以是checksum, 也可以是简单的文件大小。怎么计算magic换句话说如何检测数据文件是否有效完全由应用决定。
  3. 文件名的处理有点复杂因为每台服务器的路径可能不一致。比如node A的TDengine的数据文件存放在 /etc/taos目录下而node B的数据存放在 /home/jhtao目录下。因此同步模块需要应用在启动一个同步实例时提供一个path这样两台服务器的绝对路径可以不一样但仍然可以做对比做同步。
  4. 当sync模块调用回调函数getFileInfo获得数据文件信息时有如下的规则
    1. index 为0表示获取最老的文件同时修改index返回给sync模块。如果index不为0表示获取指定位置的文件。
    2. 如果name为空表示sync想获取位于index位置的文件信息包括magic, size。Master节点会这么调用
    3. 如果name不为空表示sync想获取指定文件名和index的信息slave节点会这么调用
    4. 如果某个index的文件不存在magic返回0表示文件已经是最后一个。因此整个系统里文件的index必须是连续的一段整数。
  5. 当sync模块调用回调函数getWalInfo获得wal信息时有如下规则
    1. index为0表示获得最老的WAL文件, 返回时index更新为具体的数字
    2. 如果返回0表示这是最新的一个WAL文件如果返回值是1表示后面还有更新的WAL文件
    3. 返回的文件名为空那表示没有WAL文件
  6. 无论是getFileInfo, 还是getWalInfo, 只要获取出错(不是文件不存在),返回-1即可系统会报错停止同步

整个数据恢复流程分为两大步骤第一步先恢复archived data(file), 然后恢复wal。具体流程如下

  1. 通过已经建立的TCP连接发送sync req给master节点
  2. master收到sync req后以client的身份向vnode B主动建立一新的专用于同步的TCP连接syncFd)
  3. 新的TCP连接建立成功后master将开始retrieve流程对应的vnode B将同步启动restore流程
  4. Retrieve/Restore流程里先处理所有archived data (vnode里的data, head, last文件后处理WAL data。
  5. 对于archived datamaster将通过回调函数getFileInfo获取数据文件的基本信息包括文件名、magic以及文件大小。
  6. master 将获得的文件名、magic以及文件大小发给vnode B
  7. vnode B将回调函数getFile获得magic和文件大小如果两者一致就认为无需同步如果两者不一致 就认为需要同步。vnode B将结果通过消息FileAck发回master
  8. 如果文件需要同步master就调用sendfile把整个文件发往vnode B
  9. 如果文件不需要同步master(vnode A)就重复5678直到所有文件被处理完

对于WAL同步流程如下

  1. master节点调用回调函数getWalInfo获取WAL的文件名。
  2. 如果getWalInfo返回值大于0表示该文件还不是最后一个WAL因此master调用sendfile一下把该文件发送给vnode B
  3. 如果getWalInfo返回时为0表示该文件是最后一个WAL因为文件可能还处于写的状态中sync模块要根据WAL Head的定义逐条读出记录然后发往vnode B。
  4. vnode A读取TCP连接传来的数据按照WAL Head逐条读取如果版本号比现有的大调用回调函数writeToCache交给应用处理。如果小直接扔掉。
  5. 上述流程循环直到所有WAL文件都被处理完。处理完后master就会将新来的数据包通过Forward消息转发给slave。

从同步文件启动起sync模块会通过inotify监控所有处理过的file以及wal。一旦发现被处理过的文件有更新变化同步流程将中止会重新启动。因为有可能落盘操作正在进行比如历史数据导入内存数据落盘把已经处理过的文件进行了修改需要重新同步才行。

对于最后一个WAL (LastWal)的处理逻辑有点复杂,因为这个文件往往是打开写的状态,有很多场景需要考虑,比如:

  • LastWal文件size在增长需要重新读
  • LastWal文件虽然已经打开写但内容为空
  • LastWal文件已经被关闭应用生成了新的Last WAL文件;
  • LastWal文件没有被关闭但数据落盘的原因没有读到完整的一条记录;
  • LastWal文件没有被关闭但数据落盘的原因还有部分记录暂时读取不到

sync模块通过inotify监控LastWal文件的更新和关闭操作。而且在确认已经尽可能读完LastWal的数据后会将对方同步状态设置为SYNC_CACHE。该状态下master节点会将新的记录转发给vnode B而此时vnode B并没有完成同步需要把这些转发包先存在recv buffer里等WAL处理完后vnode A再把recv buffer里的数据包通过回调writeToCache交给应用处理。

等vnode B把这些buffered forwards处理完同步流程才算结束vnode B正式变为slave。

Master分布均匀性问题

因为Master负责写、转发消耗的资源会更多因此Master在整个集群里分布均匀比较理想。

但在TDengine的设计里如果多个虚拟节点都符合master条件TDengine选在列表中最前面的做Master, 这样是否导致在集群里Master数量的分布不均匀问题呢这取决于应用的设计。

给一个具体例子系统里仅仅有三个节点IP地址分别为IP1, IP2, IP3. 在各个节点上TDengine创建了多个虚拟节点组每个虚拟节点组都有三个副本。如果三个副本的顺序在所有虚拟节点组里都是IP1, IP2, IP3, 那毫无疑问master将集中在IP1这个节点这是我们不想看到的。

但是如果在创建虚拟节点组时增加随机性这个问题就不存在了。比如在vgroup 1, 顺序是IP1, IP2, IP3, 在vgroup 2里顺序是IP2, IP3, IP1, 在vgroup 3里顺序是IP3, IP1, IP2。最后master的分布会是均匀的。

因此在创建一个虚拟节点组时应用需要保证节点的顺序是round robin或完全随机。

少数虚拟节点写入成功的问题

在某种情况下写入成功的确认数大于0但小于配置的Quorum, 虽然有虚拟节点数据更新成功master仍然会认为数据更新失败并通知客户端写入失败。

这个时候系统存在数据不一致的问题因为有的虚拟节点已经写入成功而有的写入失败。一个处理方式是Master重置(reset)与其他虚拟节点的连接该虚拟节点组将自动进入选举流程。按照规则已经成功写入数据的虚拟节点将成为新的master组内的其他虚拟节点将从master那里恢复数据。

因为写入失败客户端会重新写入数据。但对于TDengine而言是OK的。因为时序数据都是有时间戳的时间戳相同的数据更新操作第一次会执行但第二次会自动扔掉。对于Meta Data(增加、删除库、表等等的操作也是OK的。一张表、库已经被创建或删除再创建或删除不会被执行的。

在TDengine的设计里虚拟节点与虚拟节点之间是一个TCP连接是一个pipeline数据块一个接一个按顺序在这个pipeline里等待处理。一旦某个数据块的处理失败这个连接会被重置后续的数据块的处理都会失败。因此不会存在Pipeline里一个数据块更新失败但下一个数据块成功的可能。

Split Brain的问题

选举流程中有个强制要求那就是一定有超过半数的虚拟节点在线。但是如果replication正好是偶数这个时候完全可能存在splt brain问题。

为解决这个问题TDengine提供Arbitrator的解决方法。Arbitrator是一个节点它的任务就是接受任何虚拟节点的连接请求并保持它。

在启动复制模块实例时在配置参数中应用可以提供Arbitrator的IP地址。如果是奇数个副本复制模块不会与这个arbitrator去建立连接但如果是偶数个副本就会主动去建立连接。

Arbitrator的程序tarbitrator.c在复制模块的同一目录, 编译整个系统时会在bin目录生成。命令行参数“-”查看可以配置的参数比如绑定的IP地址监听的端口号。

与RAFT相比的异同

数据一致性协议流行的有两种Paxos与Raft. 本设计的实现与Raft有很多类同之处下面做一些比较

相同之处:

  • 三大流程一致Raft里有Leader election, replication, safety完全对应TDengine的选举、数据转发、数据恢复三个流程。
  • 节点状态定义一致Raft里每个节点有Leader, Follower, Candidate三个状态TDengine里是Master, Slave, Unsynced, Offline。多了一个offlince, 但本质上是一样的因为offline是外界看一个节点的状态但该节点本身是处于master, slave 或unsynced的。
  • 数据转发流程完全一样Master(leader)需要等待回复确认。
  • 数据恢复流程几乎一样Raft没有涉及历史数据同步问题只考虑了WAL数据同步。

不同之处:

  • 选举流程不一样Raft里任何一个节点是candidate时主动向其他节点发出vote request, 如果超过半数回答Yes, 这个candidate就成为Leader,开始一个新的term. 而TDengine的实现里节点上线、离线或角色改变都会触发状态消息在节点组类传播等节点组里状态稳定一致之后才触发选举流程因为状态稳定一致基于同样的状态信息每个节点做出的决定会是一致的一旦某个节点符合成为master的条件无需其他节点认可它会自动将自己设为master。TDengine里任何一个节点检测到其他节点或自己的角色发生改变就会给节点组内其他节点进行广播的。Raft里不存在这样的机制因此需要投票来解决。
  • 对WAL的一条记录Raft用term + index来做唯一标识。但TDengine只用version类似index)在TDengine实现里仅仅用version是完全可行的, 因为TDengine的选举机制没有term的概念。

如果整个虚拟节点组全部宕机重启但不是所有虚拟节点都上线这个时候TDengine是不会选出master的因为未上线的节点有可能有最高version的数据。而RAFT协议只要超过半数上线就会选出Leader。

Meta Data的数据复制

TDengine里存在时序数据也存在Meta Data。Meta Data对数据的可靠性要求更高那么TDengine设计能否满足要求呢下面做个仔细分析。

TDengine里Meta Data包括以下

  • account 信息
  • 一个account下面可以有多个user, 多个DB
  • 一个DB下面有多个vgroup
  • 一个DB下面有多个stable
  • 一个vgroup下面有多个table
  • 整个系统有多个mnode, dnode
  • 一个dnode可以有多个vnode

上述的account, user, DB, vgroup, table, stable, mnode, dnode都有自己的属性这些属性是TDengine自己定义的不会开放给用户进行修改。这些Meta Data的查询都比较简单都可以采用key-value模型进行存储。这些Meta Data还具有几个特点

  1. 上述的Meta Data之间有一定的层级关系比如必须先创建DB才能创建table, stable。只有先创建dnode才可能创建vnode, 才可能创建vgroup。因此他们创建的顺序是绝对不能错的。
  2. 在客户端应用的数据更新操作得到TDengine服务器侧确认后所执行的数据更新操作绝对不能丢失。否则会造成客户端应用与服务器的数据不一致。
  3. 上述的Meta Data是容许重复操作的。比如插入新记录后再插入一次删除一次后再删除一次更新一次后再更新一次不会对系统产生任何影响不会改变系统任何状态。

对于特点1本设计里数据的写入是单线程的按照到达的先后顺序给每个数据更新操作打上版本号版本号大的记录一定是晚于版本号小的写入系统数据写入顺序是100%保证的绝对不会让版本号大的记录先写入。复制过程中数据块的转发也是严格按照顺序进行的因此TDengine的数据复制设计是能保证Meta Data的创建顺序的。

对于特点2只要Quorum数设置等于replica那么一定能保证回复确认过的数据更新操作不会在服务器侧丢失。即使某节点永不起来只要超过一半的节点还是online, 查询服务不会受到任何影响。这时如果某个节点离线超过一定时长系统可以自动补充新的节点以保证在线的节点数在绝大部分时间是100%的。

对于特点3完全可能发生服务器确实持久化存储了某一数据更新操作但客户端应用出了问题认为操作不成功它会重新发起操作。但对于Meta Data而言没有关系客户端可以再次发起同样的操作不会有任何影响。

总结来看只要quorum设置大于一本数据复制的设计是能满足Meta Data的需求的。目前还没有发现漏洞。