数据库故障容错之系统时钟故障
本文通过对 CnosDB 数据库系统时钟故障的讨论,提出了在单机环境和集群环境中解决系统时钟故障的方案,具有非常重要的指导意义。
可能并不存在的情况引发的讨论
有一天,CnosDB 实习生小邵在做时间乱序数据处理逻辑的设计,时序数据的产生一般有以下几个规律可以遵循:
1. 一般情况下,时序数据由一条条时间线组成,每一条时间线背后都是一个独立的数据源。
2. 时序数据的产生往往是实时的,时间线上的数据也是按照时间顺序排列的,大多数时序数据库的底层数据存储设计也基于此。
3. 在一些特殊的情况下,一条时间线上的时间顺序可能会被破坏,一段时间之前的数据可能现在才到达数据库;由于处理起来太过麻烦,一些数据库会拒绝这种数据写入。
问题出在“一些特殊的情况上”,小邵异想天开地提出了一个我没有想到过的可能情况:
1. 如果客户端发来的数据是没有时间戳的,那么数据的时间戳由数据库服务端的系统时间戳决定。
2. 如果数据库服务端的系统时钟发生了修改,比如本来是 5 点,调成了 3 点,就会出现时间乱序数据。
不得不说,这是比“一些特殊的情况”还要特殊的情况,而比较典型的时间乱序数据场景是这样的;一个远程设备不断向数据库端发送自带时间戳的数据,可能有两种情况出现:
1. 网络方面的故障,导致从设备发来的数据,并没有按照数据发出的时间到达。
2. 设备发出的数据在到达数据库前丢失或损坏了,设备在之后的时间里将这些数据重新发给数据库。
经过了一段时间的思考,我发现小邵说的那种情况,不应该放在时间乱序数据的问题里讨论,而是一个需要单独处理的问题。
系统时钟故障
相比于掉电宕机、网络故障等常见又严重的故障,系统时钟发生变化的情况其实很少被考虑到,一般主要影响的也都是日志时间戳之类,不会直接影响到用户的部分。
近年来,由于 NewSQL 的概念普及,许多分布式数据库选择依赖系统时钟与 NTP 实现混合逻辑时钟,这时系统时钟故障可能才真正被广泛讨论。不过,CnosDB 是个典型的原生时序数据库,不是那种拼接改造的数据库项目,并不支持事务,所以在这里并不是讨论混合逻辑时钟的问题。
定义
在具体讨论之前,先给系统时钟故障一个明确的定义:指由于恶意攻击、用户误操作等原因,导致数据库部署环境的系统时钟突然发生跨度较大的变化,比如前一秒系统时钟为 17:00,下一秒变成了 3:19。
由于从数据库的角度无法确认这种变化是否正确、是否符合用户的预期,所以将这种变化视为故障。
集群中的情况
单机下的情况比较简单,就是上文中的定义情况,而集群中的情况可以大致分为三种:
1. 只有少数节点的系统时钟发生变化。
2. 超过半数节点的系统时钟发生变化。
3. 所有节点同步切换到新的、相同的系统时钟。
对时序数据库的影响
就像上文中小邵所说的“特殊情况”,系统时钟故障会影响没有自带时间戳的数据,其背后是时序数据库对这类数据处理的策略:
1. 要求用户写入的所有数据都必须自带时间戳。
2. 对于没有时间戳的数据,自动添加客户端的系统时间戳。
3. 对于没有时间戳的数据,自动添加数据库服务端的系统时间戳。
据我观察,目前策略三应该是比较主流的方案,所以一个时序数据库最好还是要考虑如何处理系统时钟故障。否则,一旦出现问题,就会有大量“时间戳异常”的数据写入到数据库中,影响到数据库的性能还是小事,出现了用户角度的“脏”数据就是大麻烦了。
故障的处理
根据既往的经验和知识,一个足够健壮的系统,在处理故障时需要依循以下四个原则:
1. Fail-Fast:能够第一时间发现故障的产生。
2. Fail-Safe:尽将故障的影响范围控制在最小。
3. Fail-Over:在发生故障时能够进行主备切换,由没有发生故障的备份/备用节点继续提供服务。
4. Fail-Back:在主副本/主节点从故障状态恢复到正常状态之后,可以切回到主副本/主节点提供服务。
基于这样的原则,针对单机和集群两种情况,做出了以下设计。
单机:发现故障然后服务降级
在单机的环境中,系统发现故障之后,需要做以下的操作:
1. 周期性获取当前系统时间戳,并与上一次获取到的值对比,如果两者的偏差超过了容忍阈值,判定为出现了时钟故障。
2. 用户可以通过查看系统表中的某项,查看当前 CnosDB 节点是否处于时钟故障状态,此状态不会因 CnosDB 进程的重启而变化。
3. 出现故障后,只允许用户自行指定时间戳的数据写入,拒绝未指定时间戳的数据写入。
4. 用户可以通过使用管理员权限执行一条命令,解除当前 CnosDB 节点的时钟故障状态。
我们在处理单机故障时,可以用以下的用户接口设计去定义:
统一的命名规则
clock_fault_XX
系统变量/参数
1. clock_fault_check_interval
a. 获取系统时间戳并进行检查的时间间隔。
b. 默认值 5min,可在线变更。
2. clock_fault_last_timestamp
a. 最后一次获取到的系统时间戳。
b. 只读。
3. clock_fault_check_threshold
a. 时钟故障的触发阈值。
b. 当前系统实际时间戳与 clock_fault_last_timestamp + clock_fault_check_interval 的差值超过此阈值时,当前 CnosDB 节点切换为时钟故障状态。
c. 默认值为 1h,可在线变更。
4. clock_fault_state
a. 当前 CnosDB 节点是否处于时钟故障状态
b. 处于时钟故障状态,此值为 error;反之,为 normal。
c. CnosDB 将此值设置为 error 时,需要同时在日志中进行记录。
d. 可在线变更,但只能由 CnosDB 管理员手动设置为 normal,代表 CnosDB 管理员对目前的情况已经完全掌握并进行了相应的处理。
系统时间线
clock_fault_timestamp:记录每次进行检查时获取的系统时间戳,用户可以通过查看此时间线来判断时钟故障发生的具体时间点。
集群:复杂的故障发现与主备切换机制
在集群环境中,因为多节点的原因,处理的过程相对于单机的机制会更加的复杂,那么也有以下的相应操作:
1. 以每个 CnosDB 节点的单节点故障处理为基础。
2. 当一个节点处于时钟故障状态时,未指定时间戳的数据写入将自动路由到其他节点进行执行。
3. 定期检查集群所有节点的系统时钟:
a. N 为当前集群内的主机(物理机、虚拟机或容器)总数量, μ为集群内全部主机某个子集,μ包含的主机数量超过 N/2。
b. 如果存在一个μ在两个节点间的系统时间戳差值不超过一个预先设定的阈值,这个μ就可以称为时钟基准主机组。
c. 时钟基准主机组应尽可能包含更多主机。
d. 如果部署在时钟基准主机组中的所有节点都没有处于时钟故障状态,这些节点的集合就可以成为时钟基准节点组。
e. 当集群中无法找出一个时钟基准节点组时,整个集群将被判定为处于时钟异常状态。
f. 只有当集群中可以找到时钟基准节点组时,CnosDB 集群管理员才可以将集群解除时钟异常状态。
g. 当某一节点的系统时钟与时钟基准主机组中一个节点的系统时钟的差值超过了一个预先设定的阈值,集群会将该节点的 clock_fault_state 设置为 error(通过集群内部的一个账号,一般用户无法进行这样的操作)。
我们在处理集群故障时,可以用以下的用户接口设计去定义:
统一的命名规则
cluster_clock_fault_XX
系统变量/参数
1. cluster_clock_fault_check_interval
a. 集群范围内进行 clock_fault_check 的时间间隔。
b. 默认值 5min,可在线变更。
2. cluster_clock_fault_check_threshold
a. 用于判定时钟基准节点是否存在的阈值。
b. 默认值为 10s,可在线变更。
3. cluster_clock_fault_state
a. 当前 CnosDB 集群处于时钟故障状态时,此值为 error;反之,为 normal。
b. CnosDB 将此值设置为 error 时,需要同时在日志中进行记录。
c. 可在线变更,但只能由 CnosDB 管理员手动设置为 normal,代表 CnosDB 管理员对目前的情况已经完全掌握并进行了相应的处理。
系统时间线
1. cluster_clock_fault_base_clock_node_list:记录当前基准时钟节点组包含的节点列表。
2. cluster_clock_fault_base_clock_host_list:记录当前基准时钟主机组包含的节点列表。
结语
尽管系统时钟故障并不是一个时序数据库首要去考虑如何处理的故障,对于集群层面的时钟故障检测与处理,可能也不需要直接放在时序数据库这一层来实现,但这个思考和做大体设计的过程还是很有趣的,于是决定分享给大家。
如果大家有真实遇到这种问题的案例,可以告诉我们,这样本文中的设计会考虑得更加严谨,也会更快地在 CnosDB 中落地。
CnosDB 简介
CnosDB 是一款高性能、高易用性的开源分布式时序数据库,现已正式发布及全部开源。
欢迎关注我们的代码仓库,一键三连🙇🙇🙇:https://github.com/cnosdb/cnosdb
版权声明: 本文为 InfoQ 作者【CnosDB】的原创文章。
原文链接:【http://xie.infoq.cn/article/49276306ef9bc093ca48244ef】。文章转载请联系作者。
评论