关于go:MatrixOne-logservice-原理解析

6次阅读

共计 4990 个字符,预计需要花费 13 分钟才能阅读完成。

导读

Logservice 在 MatrixOne 中扮演着十分重要的角色,它是独立的服务,通过 RPC 的形式供内部组件应用,用来对日志进行治理。

Logservice 应用基于 raft 协定的 dragonboat 库(multi-raft group 的 golang 开源实现),通常状况下应用本地磁盘,以多正本的形式保留日志,能够了解为对 WAL 的治理。事务的提交只须要写入 Logservice 中就能够,不须要将数据写入到 S3,有另外的组件异步地将数据批量写入到 S3 上。这样的设计,保障了在事务提交时的低提早,同时多个正本也保障了数据的高牢靠。

本文将重点分享 MatrixOne 中 logservice 模块的次要原理。

上面是本文目录概览:

  1. 整体架构
  2. 客户端
  3. 服务端
  4. Write&Read
  5. Truncation

Part 1 整体架构

如下图所示是 logservice 的整体架构。在 server 中包含 handler、dragonboat、以及 RSM 几个模块。


Part 2 客户端

Logservice client 次要是提供给 DN 调用,要害接口阐明如下:

  • Close() 敞开客户端连贯。
  • Config() 获取 client 相干配置。
  • GetLogRecord() 返回一个 pb.LogRecord 变量,该构造包含 8 字节的 Lsn,4 字节的 record type,以及类型为 []byte 的 Data。Data 局部包含 4 字节的 pb.UserEntryUpdate,8 字节的 replica DN ID,曾经 payload []byte。
  • Append() append pb.LogRecord 到 logservice,返回值为 Lsn。在调用端,参数 pb.LogRecord 能够复用。
  • Read() 从 logservice 中读取 firstLsn 开始的日志,达到 maxSize 时完结读取,返回值 Lsn 作为下一次读取的终点。
  • Truncate() 删除 lsn 之前的日志,开释磁盘空间。
  • GetTruncatedLsn() 返回最新删除的日志 Lsn。
  • GetTSOTimestamp() 向 TSO 申请总数为 count 的工夫戳,调用之前的工夫戳,调用者占用 [returned value, returned value + count] 这个范畴。该办法暂未应用。

Client 通过 MO-RPC 向 logservice 的 server 端发送申请,server 端与 raft/drgonboat 交互返回后果。


Part 3 服务端

3.1 Server Handler

Logservice 的 server 端接管到 client 发送的申请做解决,入口函数是 (*Service).handle(),不同的申请会调用不同的办法进行解决:

  1. Append,Append 日志到 logservice,最终会调用 dragonboat 的 (*NodeHost) SyncPropose() 办法同步提交 propose 申请,须要日志 commit 并且 apply 之后能力返回,返回值是日志写胜利后的 Lsn。
  2. Read,从 log db 中读取 log entry。首先调用 (*NodeHost) SyncRead() 从状态机中线性读取到以后的 Lsn,再依据 Lsn,调用 (*NodeHost) QueryRaftLog() 从 log db 中读取到 log entry。
  3. Truncate,截断 log db 中的日志,开释磁盘空间。留神,这里只是向状态机中更新以后能够 truncate 的 lsn,不是真正的做 truncate 操作。
  4. Connect,与 logservice server 建设连贯,尝试读写状态机进行状态查看。
  5. Heartbeat,包含 logservice、CN 和 DN 的心跳,该申请向 HAKeeper 的状态机更新各自的状态信息,并且同步 HAKeeper 的 tick,在 HAKeeper 做 check 时依据 tick 比拟离线工夫,如果离线,就登程 remove/shutdown 等操作。
  6. Get XXX,从状态机中获取信息。

3.2 Bootstrap

Bootstrap 是 logservice 服务启动的时候进行的,通过 HAKeeper shard(shard ID 是 0)来实现,入口函数是 (*Service) BootstrapHAKeeper。

不论配置中设置了几个正本,每个 logservice 过程启动的时候都会启动 HAKeeper 的一个正本,每个正本在启动时设置了 members,HAKeeper shard 以这些 members 作为默认的正本数启动 raft。

在实现 raft 的 leader election 之后,执行 set initial cluster info,设置 log、DN 的 shard 数以及 log 的正本数。

设置完正本数之后,会把多余的 HAKeeper 正本停掉。

3.3 Heartbeat

该心跳是 logservice、CN 以及 DN 发送到 HAKeeper 的心跳,并不是 raft 正本之间的心跳!次要有两个作用:

  1. 通过心跳发送各正本状态信息给 HAKeeper,HAKeeper 的 RSM 更新正本信息;
  2. 心跳返回时,从 HAKeeper 中获取到须要正本执行的命令。

Logservice 的 heartbeat 流程如下图,CN、DN 的流程与之相似。

心跳默认每 1 秒执行一次,其原理如下:

  1. 以 store 级别,生成该 store 上所有 shard 的正本的心跳信息,包含 shard ID、节点信息、term、leader 等;
  2. 发送 request 到 logservice 的 server 端;
  3. Server 收到申请后调用 (*Service) handleLogHeartbeat() 做解决,调用 propose 将心跳发送到 raft;
  4. HAKeeper 的 RSM 收到心跳后,调用 (*stateMachine) handleLogHeartbeat() 做解决,次要做两件事:
  • 更新状态机中的 LogState:调用 (*LogState) Update() 更新 stores 和 shards;
  • 从状态机的 ScheduleCommands 中获取到 commands,返回给发动端执行 command。

CN 和 DN 到 HA keeper 的心跳原理也是一样的。

3.4 RSM

Logservice 和 HAKeeper 的状态机都是基于内存的状态机模型,所有的数据都只保留在内存中。它们都实现了 IStateMachine 接口。次要办法阐明如下:

  • Update(),在一次 propose 实现 commit 之后(即少数正本实现写入),会调用 Update 接口,更新状态机中的数据。Update() 办法的实现由用户实现,必须是无副作用的(Side effect),雷同的输出必须失去雷同的输入后果,否则会导致状态机的不稳固。Update() 的后果通过 Result 构造返回,如果产生谬误,error 不为空。
  • Lookup(),查找状态机中的数据,通过 interface{} 来指明须要查找什么数据,返回的后果也是 interface{} 类型,因而须要用户本人定义好状态机中的数据,传入相应的数据类型,返回对应的数据类型,做类型断言。Lookup() 是一个只读的办法,不应该去批改状态机中的数据。
  • SaveSnapshot(),创立 snapshot,将状态机中的数据写入 io.Writer 接口,通常是文件句柄,因而最终会保留到本地磁盘文件中。ISnapshotFileCollection 是除了状态机中的数据以外的文件系统中的文件列表,如果有的话,也会转存到快照中。第三个参数用来告诉 snapshot precedure,raft 正本曾经进行,终止打快照的操作。
  • RecoverFromSnapshot(),复原状态机数据,从 io.Reader 中读取最新的 snapshot。[]SnapshotFile 是一些额定的文件列表,间接复制到状态机数据目录中。第三个参数用来管制 raft 正本时也终止掉复原快照的操作。
  • Close(),敞开状态机,做一些清理工作。

Part 4 Write&Read

简略阐明一下 logservice 中一次读写申请的流程。

4.1 Write

  1. 如果连贯的不是 leader,会转发到 leader 下面;leader 接管到申请后,将 log entries 写入本地磁盘。
  2. 同时,异步地发送给 follower 节点,follower 节点接管到申请后,将 log entries 写入本地磁盘。
  3. 当本次 append 在大多数节点上实现后,更新 commit index,并通过心跳告诉给其余 follower 节点。
  4. Leader commit 之后开始 apply 状态机操作。
  5. Apply 实现后,返回客户端
  6. Follower 接管到来自 leader 的 commit index 之后,各自 apply 本人的状态机

4.2 Read

读取数据分为两种:

  • 从状态机中读取数据
  • 从 log db 中读取 log entries

从状态机中读取数据
从状态机中读取数据,如下图:

  1. 客户端发动 read 申请,达到 leader 节点时,会记录此时的 commit index;
  2. Leader 向所有的节点发送心跳申请,确认本人的 leader 位置,大多数节点回复时,确定依然是 leader,能够回复读申请;
  3. 期待 apply index 大于等于 commit index;
  4. 此时才能够读取状态机中的数据返回给客户端

从 logdb 读取
从 log db 中读取 log entries,如下图:

过程比较简单,通常产生在集群重启的时候。

重启时,正本须要先从 snapshot 中复原状态机的数据,而后从 snapshot 中记录的 index 地位开始读取 log db 中的 log entries,利用到状态机中。该操作实现后才能够参加 leader 的选举。当集群选举出 leader 后,DN 会连贯到 logservice cluster,从其中一个正本的 log db 的上一次 checkpoint 地位开始读取 log entries,replay 到 DN 本人的内存数据中。


Part 5 Truncation

Log db 的 log entries 如果始终增长会导致磁盘空间有余,因而须要定期开释磁盘空间,通过 truncation 实现。

Logservice 应用的是基于内存的状态机,状态机中没有记录用户数据,只记录了一些元数据和状态信息,比方 tick、state 和 LSN 等。用户数据由 DN tae 本人记录。能够了解为在 MO 中,状态机是拆散的,tae 和 logservice 别离保护了一个状态机。

在这种状态机拆散的设计下,简略的 snapshot 机制会导致问题:

  1. 如图中所示,tae 发送一个 truncate 申请,truncate index 是 100,然而此时 logservice 状态机 applied index 是 200,即 200 之前的日志会被删掉,而后在这个地位生成快照。留神:truncate_index != applied_index。
  2. 集群重启。
  3. Logservice 状态机 apply snapshot,index 为 200,并设置 first index 为 200(200 之前的日志都曾经删除),而后 logservice 状态机开始回放日志,回放实现后对外提供服务。
  4. Tae 从 logservice 读取 log entries,读取起始地位是 100,然而无奈读取,因为 200 之前的日志都曾经删除,产生谬误。

为了解决下面形容的问题,目前 truncation 的整体工作流程如下图所示:

  1. Tae 发送 truncate 申请,更新 logservice 状态机中的 truncateLsn,此时只是更新该值,不做 snapshot/truncate 操作。
  2. 每个 logservice server 外部都会启动一个 truncation worker,每隔一段时间就会发送一次 Truncate Request,留神,这个 Request 其中的参数 Exported 设置为 true,表明这个 snapshot 对系统不可见,只是将 snapshot 导出到某个目录下。
  3. Truncation worker 中还会查看以后曾经 export 的 snapshot 列表,是否有 index 大于以后 logservice 状态机中的 truncateLsn 的,如果有,将最靠近 truncateLsn 的那个 snapshot 导入到零碎中,使之失效,对系统可见。
  4. 所有的正本都执行雷同的操作,这样就保障了两个状态机的 snapshot lsn 是一样的,在集群重启时,能够读取到对应的 log entries。

以上就是 logservice 模块的次要工作原理。

正文完
 0