共计 4990 个字符,预计需要花费 13 分钟才能阅读完成。
导读
Logservice 在 MatrixOne 中扮演着十分重要的角色,它是独立的服务,通过 RPC 的形式供内部组件应用,用来对日志进行治理。
Logservice 应用基于 raft 协定的 dragonboat 库(multi-raft group 的 golang 开源实现),通常状况下应用本地磁盘,以多正本的形式保留日志,能够了解为对 WAL 的治理。事务的提交只须要写入 Logservice 中就能够,不须要将数据写入到 S3,有另外的组件异步地将数据批量写入到 S3 上。这样的设计,保障了在事务提交时的低提早,同时多个正本也保障了数据的高牢靠。
本文将重点分享 MatrixOne 中 logservice 模块的次要原理。
上面是本文目录概览:
- 整体架构
- 客户端
- 服务端
- Write&Read
- 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(),不同的申请会调用不同的办法进行解决:
- Append,Append 日志到 logservice,最终会调用 dragonboat 的 (*NodeHost) SyncPropose() 办法同步提交 propose 申请,须要日志 commit 并且 apply 之后能力返回,返回值是日志写胜利后的 Lsn。
- Read,从 log db 中读取 log entry。首先调用 (*NodeHost) SyncRead() 从状态机中线性读取到以后的 Lsn,再依据 Lsn,调用 (*NodeHost) QueryRaftLog() 从 log db 中读取到 log entry。
- Truncate,截断 log db 中的日志,开释磁盘空间。留神,这里只是向状态机中更新以后能够 truncate 的 lsn,不是真正的做 truncate 操作。
- Connect,与 logservice server 建设连贯,尝试读写状态机进行状态查看。
- Heartbeat,包含 logservice、CN 和 DN 的心跳,该申请向 HAKeeper 的状态机更新各自的状态信息,并且同步 HAKeeper 的 tick,在 HAKeeper 做 check 时依据 tick 比拟离线工夫,如果离线,就登程 remove/shutdown 等操作。
- 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 正本之间的心跳!次要有两个作用:
- 通过心跳发送各正本状态信息给 HAKeeper,HAKeeper 的 RSM 更新正本信息;
- 心跳返回时,从 HAKeeper 中获取到须要正本执行的命令。
Logservice 的 heartbeat 流程如下图,CN、DN 的流程与之相似。
心跳默认每 1 秒执行一次,其原理如下:
- 以 store 级别,生成该 store 上所有 shard 的正本的心跳信息,包含 shard ID、节点信息、term、leader 等;
- 发送 request 到 logservice 的 server 端;
- Server 收到申请后调用 (*Service) handleLogHeartbeat() 做解决,调用 propose 将心跳发送到 raft;
- 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
- 如果连贯的不是 leader,会转发到 leader 下面;leader 接管到申请后,将 log entries 写入本地磁盘。
- 同时,异步地发送给 follower 节点,follower 节点接管到申请后,将 log entries 写入本地磁盘。
- 当本次 append 在大多数节点上实现后,更新 commit index,并通过心跳告诉给其余 follower 节点。
- Leader commit 之后开始 apply 状态机操作。
- Apply 实现后,返回客户端
- Follower 接管到来自 leader 的 commit index 之后,各自 apply 本人的状态机
4.2 Read
读取数据分为两种:
- 从状态机中读取数据
- 从 log db 中读取 log entries
从状态机中读取数据
从状态机中读取数据,如下图:
- 客户端发动 read 申请,达到 leader 节点时,会记录此时的 commit index;
- Leader 向所有的节点发送心跳申请,确认本人的 leader 位置,大多数节点回复时,确定依然是 leader,能够回复读申请;
- 期待 apply index 大于等于 commit index;
- 此时才能够读取状态机中的数据返回给客户端
从 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 机制会导致问题:
- 如图中所示,tae 发送一个 truncate 申请,truncate index 是 100,然而此时 logservice 状态机 applied index 是 200,即 200 之前的日志会被删掉,而后在这个地位生成快照。留神:truncate_index != applied_index。
- 集群重启。
- Logservice 状态机 apply snapshot,index 为 200,并设置 first index 为 200(200 之前的日志都曾经删除),而后 logservice 状态机开始回放日志,回放实现后对外提供服务。
- Tae 从 logservice 读取 log entries,读取起始地位是 100,然而无奈读取,因为 200 之前的日志都曾经删除,产生谬误。
为了解决下面形容的问题,目前 truncation 的整体工作流程如下图所示:
- Tae 发送 truncate 申请,更新 logservice 状态机中的 truncateLsn,此时只是更新该值,不做 snapshot/truncate 操作。
- 每个 logservice server 外部都会启动一个 truncation worker,每隔一段时间就会发送一次 Truncate Request,留神,这个 Request 其中的参数 Exported 设置为 true,表明这个 snapshot 对系统不可见,只是将 snapshot 导出到某个目录下。
- Truncation worker 中还会查看以后曾经 export 的 snapshot 列表,是否有 index 大于以后 logservice 状态机中的 truncateLsn 的,如果有,将最靠近 truncateLsn 的那个 snapshot 导入到零碎中,使之失效,对系统可见。
- 所有的正本都执行雷同的操作,这样就保障了两个状态机的 snapshot lsn 是一样的,在集群重启时,能够读取到对应的 log entries。
以上就是 logservice 模块的次要工作原理。