大数据常用组件中的数据一致性问题
SparkStreaming与Kafka
Spark Streaming提供的零数据丢失机制需要满足以下几个先决条件:
- 输入的数据来自可靠的数据源和可靠的接收器
- 应用程序的metadata被application的driver持久化了(checkpointed ),元数据持久化(Metadata checkpointing)
- 启用了WAL特性(Write ahead log)
可靠的数据源和可靠的接收器
对于Kafka输入数据源,Spark Streaming可以对已经接收的数据进行确认。输入的数据首先被接收器(receivers )所接收,然后存储到Spark中(默认情况下,数据保存到2个执行器中以便进行容错)。数据一旦存储到Spark中,接收器可以对它进行确认(比如,如果消费Kafka里面的数据时可以更新Zookeeper里面的偏移量)。这种机制保证了在接收器突然挂掉的情况下也不会丢失数据:因为数据虽然被接收,但是没有被持久化的情况下是不会发送确认消息的。所以在接收器恢复的时候,数据可以被原端重新发送

元数据持久化(Metadata checkpoint)
可靠的数据源和接收器允许从接收器挂掉的情况下恢复(或者是接收器运行的Exectuor和服务器挂掉都可以)。但是如果Driver挂掉如何恢复?可以对应用程序的元数据进行Checkpoint。利用这个特性,Driver可以将应用程序的重要元数据持久化到可靠的存储中(HDFS);然后Driver可以利用这些持久化的数据进行恢复。元数据包括:
配置、代码、那些在队列中还没有处理的batch(仅仅保存元数据,而不是这些batch中的数据)

由于有了元数据的Checkpoint,所以Driver可以利用它们重构应用程序,而且可以计算出Driver挂掉的时候应用程序执行到什么位置
WAL(Write ahead log)
【可能存在数据丢失的场景】
即使是可靠的数据源、可靠的接收器和对元数据进行Checkpoint,仍然不足以阻止潜在的数据丢失。例如:两个Exectuor已经从接收器中接收到输入数据,并将它缓存到Exectuor的内存中,接收器通知输入源数据已经接收,Exectuor根据应用程序的代码开始处理已经缓存的数据,这时候Driver突然挂掉了,从设计的角度看,一旦Driver挂掉之后,它维护的Exectuor也将全部被kill,既然所有的Exectuor被kill了,所以缓存到它们内存中的数据也将被丢失77. 缓存的时候不可能恢复,因为它们是缓存在Exectuor的内存中,所以数据被丢失了。
为了解决上面提到的糟糕场景,Spark Streaming 1.2开始引入WAL机制。启用了WAL机制,所以已经接收的数据被接收器写入到容错存储中(HDFS)。由于采用了WAl机制,Driver可以从失败的点重新读取数据,即使Exectuor中内存的数据已经丢失了。在这个简单的方法下,Spark Streaming提供了一种即使是Driver挂掉也可以避免数据丢失的机制。

At-least-once语义
虽然WAL可以确保数据不丢失,它并不能对所有的数据源保证exactly-once语义。可能发生在Spark Streaming整合Kafka的糟糕场景:【1】接收器接收到输入数据,并把它存储到WAL中 【2】接收器在更新Zookeeper中Kafka的偏移量之前突然挂掉了。

【3】Spark Streaming假设输入数据已成功收到(因为它已经写入到WAL中),然而Kafka认为数据被没有被消费,因为相应的偏移量并没有在Zookeeper中更新。过了一会,接收器从失败中恢复。那些被保存到WAL中但未被处理的数据被重新读取 【4】一旦从WAL中读取所有的数据之后,接收器开始从Kafka中消费数据。因为接收器是采用Kafka的High-Level Consumer API实现的,它开始从Zookeeper当前记录的偏移量开始读取数据,但是因为接收器挂掉的时候偏移量并没有更新到Zookeeper中,所有有一些数据被处理了2次。
除了上面描述的场景,WAL还有其他两个不可忽略的缺点:
WAL减少了接收器的吞吐量,因为接受到的数据必须保存到可靠的分布式文件系统中。对于一些输入源来说,它会重复相同的数据。比如当从Kafka中读取数据,你需要在Kafka的brokers中保存一份数据,而且你还得在Spark Streaming中保存一份。
Kafka direct API
为了解决由WAL引入的性能损失,并且保证 exactly-once 语义,Spark Streaming 1.3中引入了名为Kafka direct API。Spark driver只需要简单地计算下一个batch需要处理Kafka中偏移量的范围,然后命令Spark Exectuor直接从Kafka相应Topic的分区中消费数据。换句话说,这种方法把Kafka当作成一个文件系统,然后像读文件一样来消费Topic中的数据。

在这个简单但强大的设计中:
- 不再需要Kafka接收器,Exectuor直接采用Simple Consumer API从Kafka中消费数据
- 不再需要WAL机制,我们仍然可以从失败恢复之后从Kafka中重新消费数据
- exactly-once语义得以保存,我们不再从WAL中读取重复的数据
Kafka

假设分区的副本为3,其中副本0是 Leader,副本1和副本2是 follower,并且在 ISR 列表里面。虽然副本0已经写入了 Message4,但是 Consumer 只能读取到 Message2。因为所有的 ISR 都同步了 Message2,只有 High Water Mark 以上的消息才支持 Consumer 读取,而 High Water Mark 取决于 ISR 列表里面偏移量最小的分区,对应于上图的副本2,这个很类似于木桶原理。
这样做的原因是还没有被足够多副本复制的消息被认为是“不安全”的,如果 Leader 发生崩溃,另一个副本成为新 Leader,那么这些消息很可能丢失了。如果我们允许消费者读取这些消息,可能就会破坏一致性。试想,一个消费者从当前 Leader(副本0) 读取并处理了 Message4,这个时候 Leader 挂掉了,选举了副本1为新的 Leader,这时候另一个消费者再去从新的 Leader 读取消息,发现这个消息其实并不存在,这就导致了数据不一致性问题。
当然,引入了 High Water Mark 机制,会导致Broker间的消息复制因为某些原因变慢,那么消息到达消费者的时间也会随之变长(因为我们会先等待消息复制完毕)。延迟时间可以通过参数 replica.lag.time.max.ms 参数配置,它指定了副本在复制消息时可被允许的最大延迟时间。
Partition存储结构
每个Partition分为多个Segment,每个Segment包含两个文件:log文件和index文件,分别命名为start_offset.log和start_offset.index。log文件包含具体的msg数据,每条msg会有一个递增的offset。Index文件是对log文件的索引:每隔一定大小的块,索引msg在该segment中的相对offset和在log文件中的位置偏移量
根据offset查找msg的过程
根据msg的offset和log文件名中的start_offset,找到最后一个不大于msgoffset的segment,即为msg所在的segment;根据对应segment的index文件,进一步查找msg在log文件中的偏移量。从log文件的偏移量开始读取解析msg,比较msgoffset,找到所要读取的msg
Partition recovery过程
每个Partition会在磁盘记录一个RecoveryPoint, 记录已经flush到磁盘的最大offset。当broker fail 重启时,会进行loadLogs。首先会读取该Partition的RecoveryPoint,找到包含RecoveryPoint的segment及以后的segment, 这些segment就是可能没有完全flush到磁盘segments。然后调用segment的recover,重新读取各个segment的msg,并重建索引。这样做的优点:
- 以segment为单位管理Partition数据,方便数据生命周期的管理,删除过期数据简单
- 在程序崩溃重启时,加快recovery速度,只需恢复未完全flush到磁盘的segment
- 通过命名中offset信息和index文件,大大加快msg查找时间,并且通过分多个Segment,每个index文件很小,查找速度更快
Partition Replica同步机制
- Partition的多个replica中一个为Leader,其余为follower
- Producer只与Leader交互,把数据写入到Leader中
- Followers从Leader中拉取数据进行数据同步
- Consumer只从Leader拉取数据
ISR:所有不落后的replica集合, 不落后有两层含义:距离上次FetchRequest的时间不大于某一个值或落后的消息数不大于某一个值, Leader失败后会从ISR中选取一个Follower做Leader
数据一致性保证
数据一致性定义:若某条消息对client可见,那么即使Leader挂了,在新Leader上数据依然可以被读到。
- HighWaterMark简称HW: Partition的高水位,取一个partition对应的ISR中最小的LEO作为HW,消费者最多只能消费到HW所在的位置,另外每个replica都有highWatermark,leader和follower各自负责更新自己的highWatermark状态,highWatermark <= leader. LogEndOffset
- 对于Leader新写入的msg,Consumer不能立刻消费,Leader会等待该消息被所有ISR中的replica同步后,更新HW,此时该消息才能被Consumer消费,即Consumer最多只能消费到HW位置
这样就保证了如果Leader Broker失效,该消息仍然可以从新选举的Leader中获取。对于来自内部Broker的读取请求,没有HW的限制。同时,Follower也会维护一份自己的HW,Folloer.HW = min(Leader.HW, Follower.offset)
数据可靠性保证
当Producer向Leader发送数据时,可以通过acks参数设置数据可靠性的级别
- 0: 不论写入是否成功,server不需要给Producer发送Response,如果发生异常,server会终止连接,触发Producer更新meta数据
- 1: Leader写入成功后即发送Response,此种情况如果Leader fail,会丢失数据
- -1: 等待所有ISR接收到消息后再给Producer发送Response,这是最强保证
仅设置acks=-1也不能保证数据不丢失,当Isr列表中只有Leader时,同样有可能造成数据丢失。要保证数据不丢除了设置acks=-1, 还要保证ISR的大小大于等于2,具体参数设置:
request.required.acks:设置为-1 等待所有ISR列表中的Replica接收到消息后采算写成功;
min.insync.replicas: 设置为大于等于2,保证ISR中至少有两个Replica
Producer要在吞吐率和数据可靠性之间做一个权衡
探讨SparkStreaming结合Kafka实现数据一致性的机制,包括可靠的数据源、元数据持久化、WAL、At-least-once语义及Kafka Direct API,确保数据处理过程中不丢失信息。
1197

被折叠的 条评论
为什么被折叠?



