Skip to content

1.8_深入数据一致性

elevenqq edited this page Sep 30, 2018 · 4 revisions

本小节将对Datalink中存在的数据一致性问题展开讨论,开始之前,请先了解一下datalink的(Re-)Balance机制:1.4_深入(Re-)Balance

概述

数据一致性是分布式系统的一个核心关注点,如何保证一致性、保证到何种程度,都因场景而异,需要系统设计师进行充分的考虑和折衷。根据CAP理论,一个分布式系统只能同时满足CAP三个指标中的两个,P是肯定要满足的,否则也就谈不上分布式了,最后基本上是在C和A上进行权衡,而绝大部分场景下A也是必须的,所以最终只能牺牲C(C指的是强一致),保证数据的最终一致。最终一致,也是Datalink的设计目标,下面对Datalink的数据一致性问题和解决方案进行详细介绍。

问题

Datalink的数据一致性问题主要有两个:

  • 数据重复消费
    重复消费是很多分布式系统的一个共性问题(如:Kafka、MetaQ等),对于datalink来说该问题同样存在,如:
    * Position位点是定时往zk刷新(见PositionManager),未刷新前如果Task关闭了,那么再次启动的时候会有一段数据重复消费
    * Reader和Writer之间的数据同步,在没有事务保证的情况下,如果writer同步了部分数据后发生了异常,Task在对该批次数据进行重试时,也会有重复消费
    做好[幂等],重复消费的问题一般都可以完美规避,对于datalink来说,面对的大部分存储介质都是DB类的产品,这些产品基本上都有主键的概念,基于主键的不可重复性做[幂等],会让问题变得容易很多,而没有主键的场景下,根据实际情况做对应的设计即可,场景举例如下:
    * 重复消费binlog事件(Mysql-Binlog同步到RDBMS)
    如果是insert类型的事件,会导致主键冲突,利用数据库的merge机制对数据进行合并或者直接忽略该事件即可;如果是update类型的事件,直接执行即可,binlog会保证事件的顺序性,数据最终会恢复到最后一个事件对应的状态
    * 重复消费binlog事件(Mysql-Binlog同步到HBase)
    不管重复消费的是什么类型的事件,在HBase里对应的都是一个put操作,不存在主键冲突的问题,和上述描述一样,数据最终会恢复到最后一个事件对应的状态
  • Duplicated Task 

    task-conflict
    Datalink目前主要依赖zookeeper保证Task的互斥,每个Task在zk上都对应一个status临时节点,Task启动前会尝试抢占该节点,保证同一个Task在同一时刻只能有一个运行实例。那么,何时会发生节点抢占冲突呢,以上图为例,进行说明:

    1. Worker-1宕机,Task-1重新分配给了Worker-2,但Worker-1和zk之间的session还未超时
    2. 因某种原因触发了Reblance,Task-1重新分配给了Worker-2,但Task-1在Worker-1上还未关闭(Task关闭的时候才会remove掉status节点)。
      场景一:Worker-1在发送JoinGroupRequest之前会先关闭Task-1,但是由于某种原因Task-1关闭的特别慢,Worker-1等待Task-1关闭的时间超过TASK_SHUTDOWN_GRACEFUL_TIMEOUT_MS_CONFIG配置的时间后,便不再等待,随后Reblance完成,Task-1分配给了Worker-2,但此时Task-1仍然还没有完成关闭。
      场景二:Worker-1和Manager之间网络出现问题,集群发生了脑裂,Manager和Worker-1相互感知不到对方了,只能依靠各种超时参数做决策,可能会出现这样的情况:Task-1已经重新分配给了Worker-2,但Worker-1还没来得及触发Task-1的关闭操作(当然也可能出现已经触发了Task-1的Stopping,但还没关闭成功的场景)。其它类似的场景此处不再一一举例,不管什么场景,本质原因都是一样的。
    出现节点抢占冲突,Datalink具体的处理策略是:不断轮询重试,并且当重试时间超过ZK_SESSION_TIMEOUT_MS_CONFIG配置的时间后触发报警(超过该时间还未抢占上,已经排除了Worker宕机的可能,那么很可能是Task关闭过程中触发了一些深层次的问题,触发报警人工干预)。其实,绝大部分场景下,在正常时间范围内Task-1都可以最终成功关闭,冲突都是可以自动解除的,我们还未在生产环境遇到过冲突超时报警。
    到目前为止,一切看起来都没问题,即使出现了1和2的场景,我们都可以保证Task不会重复运行。真正棘手的问题是,出现了1和2的场景,没有发生抢占冲突怎么办?场景举例如下:
    * Worker-1发生了Full-GC,并且触发了"stop word","stop word"时间持续了1分钟,在这1分钟的时间内,Heartbeat-1会话发生了超时,超时之后很快触发了Reblance,Reblance之后Task-1重新分配给了Worker-2,很不幸,Heartbeat-2会话也发生了超时,虽然Task-1此时还在Worker-1上处于运行状态,但status节点因会话超时已经被zk删除,Task-1在Worker-2上顺利抢占了status节点,也成功启动了,随后"stop word"结束,虽然Worker-1会很快触发Reblance,但是在一个时间范围内,Task-1在Worker-1和Worker-2上是同时运行的。
    那么,发生了上述情况,会导致什么问题呢?场景举例如下:
    * 假设Task-1的类型是MysqlTask,负责把Mysql数据同步到其它关系型数据库,有一条数据发生了从S1到S2再到S3的状态转移,并且S3是这条数据的最终状态。在Worker-1上Task-1拿到的是S1和S2对应的Binlog-Event,在Worker-2上Task-1拿到的是S1、S2和S3对应的Binlog-Event,Worker-2上的Event执行完成之后,Worker-1的"stop word"才结束,并且Task-1在Worker-1上有机会成功执行了S1和S2对应的Event.
    具体会出现什么样的问题,和不同的设计策略有直接关系,在下一小节进行详细介绍。

方案

接着上一小节的场景,继续展开讨论

  1. 如果Reader和Writer不受全局事务统一管理,那么S2-Event执行成功后,便会提交到数据库,导致数据变脏(最终状态变成了S2)
  2. 如果Reader和Writer可以做到受全局事务统一管理,但commit时没有验证自己的commit权限,仍然会导致数据变脏(最终状态变成了S2);除了数据变脏,commit位点信息也可能会变脏

Datalink目前还未支持事务,所以问题1还无法规避,即使支持了,启用事务的成本、带来的编程复杂度、不同场景对事务的支持程度等,也都是需要考虑的因素。再说commit,datalink目前对commit也未做权限验证(即:commit时没有验证executionId),主要原因:其一,因为还未支持事务,对commit进行权限验证也无法规避问题1;其二,增加权限验证后,每次commit都需要和zk进行交互,会带来性能损失;其三,虽然没有做权限验证,但做了stopping验证,即commit前如果发现Task已经处于stopping状态了则不予提交,,我们目前采用的是一种【事后补偿】机制,即:发现问题并报警,然后事后基于报警日志,对数据进行补救。下面展开描述:

* 每个Task在发起一次执行的时候,都会产生一个executionId(全局不重复),这个id会放到zk的status节点中,在上面的场景中Worker-1上的Task-1最终会触发stop,stop结束的时候会尝试删除status节点,但它会发现节点中保存的executionId和其本身运行的不一样,说明发生了重复运行的情况,此时,把重复运行的时间段和相关上下文信息记录到日志表,发起报警,提示人工介入处理。上面提到的最终会触发stop,也是存在极端情况的:S2-Event刚执行完,系统宕机了,就没机会触发stop了,对于这种情况,我们还没有找到更好的解决方案。
Clone this wiki locally