上篇介绍了kafka at-least-once消费模式。kafka消费模式以commit-offset的时间节点代表不同的消费模式,分别是:at-least-once, at-most-once, exactly-once。上篇介绍的at-least-once消费模式是通过kafka自身的auto-commit实现的。事后想了想,这个应该算是at-most-once模式,因为消费过程不会影响auto-commit,kafka在每个设定的间隔都会自动进行offset-commit。如果这个间隔够短,比整个消费过程短,那么在完成消费过程前就已经保存了offset,所以是at-most-once模式。不过,如果确定这个间隔一定大于消费过程,那么又变成了at-least-once模式。具体能实现什么消费模式并不能明确,因为auto-commit是无法从外部进行控制的。看来实现正真意义上的at-least-once消费模式还必须取得offset-commit的控制权才行。

alpakka-kafka提供了一种CommittableSource:

  def committableSource[K, V](settings: ConsumerSettings[K, V],
subscription: Subscription): Source[CommittableMessage[K, V], Control] {...}

从这个CommittableSource输出的元素是CommittableMessage[K,V]:

  final case class CommittableMessage[K, V](
record: ConsumerRecord[K, V],
committableOffset: CommittableOffset
)

这个CommittableMessage除原始消息之外还提供了CommittableOffset。通过Flow或Sink都可以进行offset-commit。alpakka-kafka提供了Committer,通过Committer.sink, Committer.Flow帮助实现offset-commit,Committer.flow如下:

    Consumer
.committableSource(consumerSettings, Subscriptions.topics(topic))
.mapAsync(1) { msg =>
updateStock.map(_ => msg.committableOffset)
}
.via(Committer.flow(committerDefaults.withMaxBatch(1)))
.to(Sink.seq)
.run()

或Committer.sink:

   Consumer
.committableSource(consumerSettings, Subscriptions.topics(topic))
.mapAsync(1) { msg =>
updateStock.map(_ => msg.committableOffset)
}
.toMat(Committer.sink(committerSettings))(Keep.left)
.run()

下面是一个具体的at-least-once示范:

  val committerSettings = CommitterSettings(sys).withMaxBatch(commitMaxBatch)

  val stkTxns = new DocToStkTxns(trace)
val curStk = new CurStk(trace)
val pcmTxns = new PcmTxns(trace) val commitableSource = Consumer
.committableSource(consumerSettings, subscription) def start =
(1 to numReaders).toList.map { _ =>
RestartSource
.onFailuresWithBackoff(restartSource) { () => commitableSource }
// .viaMat(KillSwitches.single)(Keep.right)
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-msg: ${msg.record}")(Messages.MachineId("", ""))
}
_ <- stkTxns.docToStkTxns(msg.record.value())
pmsg <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-docToStkTxns: ${msg.record}")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
curstks <- curStk.updateStk(msg.record.value())
pmsg <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: curstks-$curstks")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
pcm <- pcmTxns.writePcmTxn(msg.record.value())
pmsg <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: writePcmTxn-$pcm")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- pcmTxns.updatePcm(msg.record.value())
} yield "Completed"
FastFuture.successful(msg.committableOffset)
}
.toMat(Committer.sink(committerSettings))(Keep.left)
.run()
}

消费过程其它部分的设计考虑和实现,如多线程、异常处理等可参考上篇讨论。

对于at-most-once消费模式的实现,alpakka-kafka提供了atMostOnceSource:

  def atMostOnceSource[K, V](settings: ConsumerSettings[K, V],
subscription: Subscription): Source[ConsumerRecord[K, V], Control] = {...}

下面是用这个Source实现at-most-once的示范:

 val atmostonceSource = Consumer
.atMostOnceSource(consumerSettings, subscription) def start =
(1 to numReaders).toList.map { _ =>
RestartSource
.onFailuresWithBackoff(restartSource) { () => atmostonceSource }
// .viaMat(KillSwitches.single)(Keep.right)
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-msg: $msg")(Messages.MachineId("", ""))
}
_ <- stkTxns.docToStkTxns(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-docToStkTxns: $msg")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updateStk: msg: $msg")(Messages.MachineId("", ""))
}
curstks <- curStk.updateStk(msg.value())
pmsg<- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updateStk: curstks-$curstks")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-writePcmTxn: msg: $msg")(Messages.MachineId("", ""))
}
pcm <- pcmTxns.writePcmTxn(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updateStk: writePcmTxn-$pcm")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updatePcm: msg: $msg")(Messages.MachineId("", ""))
}
_ <- pcmTxns.updatePcm(msg.value())
_ <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updateStk: updatePcm-$msg")(Messages.MachineId("", ""))
}
} yield "Completed"
}
.toMat(Sink.seq)(Keep.left)
.run()
}

由于offset-commit和消息消费是两个独立的过程,无论如何努力都无法保证只读一次,必须把这两个过程合并成一个才有可能实现。所以,exactly-once可以通过数据库系统的事务处理transaction-processing来实现,就是把offset-commit和数据更新两个动作放到同一个事务transaction里,通过事务处理的ACID原子特性保证两个动作同进同退的一致性。这也意味着这个exactly-once消费模式必须在一个提供事务处理功能的数据库系统里实现,也代表kafka-offset必须和其它交易数据一起存放在同一种数据库里。mongodb4.0以上支持事务处理,可以用来作示范。

首先,先研究一下exactly-once模式的框架:

  val mergedSource = Consumer
.plainPartitionedManualOffsetSource(consumerSettings,subscription,
loadOffsets)
.flatMapMerge(maxReaders, _._2)
.async.mapAsync(1) { msg =>
for {
cmt <- stkTxns.stkTxnsWithRetry(msg.value(), msg.partition(), msg.offset()).toFuture().map(_ => "Completed")
pmsg <- FastFuture.successful {
log.step(s"ExactlyOnceReaderGroup-stkTxnsWithRetry: committed transaction-$cmt")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.mapAsync(1) { msg =>
for {
curstks <- curStk.updateStk(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updateStk: curstks-$curstks")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.toMat(Sink.seq)(Keep.left)
.run()
}
}

在上面的例子里使用了plainPartitionedManualOffsetSource:

def plainPartitionedManualOffsetSource[K, V](
settings: ConsumerSettings[K, V],
subscription: AutoSubscription,
getOffsetsOnAssign: Set[TopicPartition] => Future[Map[TopicPartition, Long]],
onRevoke: Set[TopicPartition] => Unit = _ => ()
): Source[(TopicPartition, Source[ConsumerRecord[K, V], NotUsed]), Control] = {...}

getOffsetsOnAssign提供指定partition的offset(从数据库里读出指定partition的offset值),如下:

  private def loadOffsets(partitions: Set[TopicPartition]): Future[Map[TopicPartition,Long]] = {
offsetStore.getOffsets(partitions)
} def getOffsets(partitions: Set[TopicPartition])(
implicit ec: ExecutionContext) = {
log.step(s"OffsetStore-getOffsets: ($partitions)")(Messages.MachineId("", "")) def getOffset(tp: TopicPartition) = {
val query = and(equal(KfkModels.SCHEMA.TOPIC, tp.topic()),
equal(KfkModels.SCHEMA.PARTITION,tp.partition()))
def offset: Future[Seq[Document]] = colOffset.find(query).toFuture()
for {
docs <- offset
ofs <- FastFuture.successful(if(docs.isEmpty) None
else Some(Offsets.fromDocument(docs.head)))
} yield ofs
}
val listFut = partitions.toList.map(getOffset)
val futList: Future[List[Option[KfkModels.Offsets]]] = FastFuture.sequence(listFut)
futList.map { oofs =>
oofs.foldRight(Map[TopicPartition,Long]()){(oof,m) =>
oof match {
case None => m
case ofs => m + (new TopicPartition(ofs.get.topic,ofs.get.partition) -> ofs.get.offset)
}
}
}
}

注意loadOffset的函数类型:  Set[TopicPartition] => Future[Map[TopicPartition, Long]],返回的是个Map[partition,offset]。

另外,plainPartitionedManualSource返回Source[...Source[ConsumerRecord[K, V]],要用flatMapMerge打平:

  /**
* Transform each input element into a `Source` of output elements that is
* then flattened into the output stream by merging, where at most `breadth`
* substreams are being consumed at any given time.
*
* '''Emits when''' a currently consumed substream has an element available
*
* '''Backpressures when''' downstream backpressures
*
* '''Completes when''' upstream completes and all consumed substreams complete
*
* '''Cancels when''' downstream cancels
*/
def flatMapMerge[T, M](breadth: Int, f: Out => Graph[SourceShape[T], M]): Repr[T] =
map(f).via(new FlattenMerge[T, M](breadth))

参数breadth代表需合并的source数量。

还有,saveOffset和writeStkTxns在同一个事务处理里:

 def docToStkTxns(jsonDoc: String, partition: Int, offset: Long, observable: SingleObservable[ClientSession]) = {
val bizDoc = fromJson[BizDoc](jsonDoc)
log.step(s"TxnalDocToStkTxns-docToStkTxns: $bizDoc")(Messages.MachineId("", "")) observable.map(clientSession => {
val transactionOptions = TransactionOptions.builder()
.readPreference(ReadPreference.primary())
.readConcern(ReadConcern.SNAPSHOT)
.writeConcern(WriteConcern.MAJORITY)
.build()
clientSession.startTransaction(transactionOptions)
val txns = StkTxns.docToTxns(dbStkTxn,dbVtx,dbVendor,bizDoc,trace)
StkTxns.writeStkTxns(clientSession,colStkTxn,colPcm,txns,trace)
offsetStore.saveOffset(clientSession,partition,offset)
clientSession.commitTransaction()
clientSession
}) }

注意:mongodb的事务处理必须在复制集replica-set上进行。这也很容易理解,在复制集上才方便交易回滚rollback。

完整的exactly-once实现代码如下:

  private def loadOffsets(partitions: Set[TopicPartition]): Future[Map[TopicPartition,Long]] = {
offsetStore.getOffsets(partitions)
} val mergedSource = Consumer
.plainPartitionedManualOffsetSource(consumerSettings,subscription,
loadOffsets)
.flatMapMerge(maxReaders, _._2) def start = {
(1 to numReaders).toList.map {_ =>
RestartSource
.onFailuresWithBackoff(restartSource) { () => mergedSource }
// .viaMat(KillSwitches.single)(Keep.right)
.async.mapAsync(1) { msg =>
for {
cmt <- stkTxns.stkTxnsWithRetry(msg.value(), msg.partition(), msg.offset()).toFuture().map(_ => "Completed")
pmsg <- FastFuture.successful {
log.step(s"ExactlyOnceReaderGroup-stkTxnsWithRetry: committed transaction-$cmt")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
curstks <- curStk.updateStk(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updateStk: curstks-$curstks")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
pcm <- pcmTxns.writePcmTxn(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtMostOnceReaderGroup-updateStk: writePcmTxn-$pcm")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- pcmTxns.updatePcm(msg.value())
} yield "Completed"
}
.toMat(Sink.seq)(Keep.left)
.run()
}
}

只有第一个异步阶段使用了事务处理。也就是说保证了writeStkTxns只执行一次。这个函数的功能主要是把前端产生的交易全部固化。为了避免消费过程中出现异常中断造成了前端交易的遗失或者重复入账,必须保证前端交易只固化一次。其它阶段的数据处理都是基于已正确固化的交易记录的。如果出现问题,可以通过重算交易记录获取正确的状态。为了保证平台运行效率,选择了不使用事务处理的方式更新数据。

alpakka-kafka(8)-kafka数据消费模式实现的更多相关文章

  1. alpakka-kafka(7)-kafka应用案例,消费模式

    上篇描述的kafka案例是个库存管理平台.是一个公共服务平台,为其它软件模块或第三方软件提供库存状态管理服务.当然,平台管理的目标必须是共享的,即库存是作为公共资源开放的.这个库存管理平台是一个Kaf ...

  2. Spark Streaming消费Kafka Direct方式数据零丢失实现

    使用场景 Spark Streaming实时消费kafka数据的时候,程序停止或者Kafka节点挂掉会导致数据丢失,Spark Streaming也没有设置CheckPoint(据说比较鸡肋,虽然可以 ...

  3. Flume简介与使用(三)——Kafka Sink消费数据之Kafka安装

    前面已经介绍了如何利用Thrift Source生产数据,今天介绍如何用Kafka Sink消费数据. 其实之前已经在Flume配置文件里设置了用Kafka Sink消费数据 agent1.sinks ...

  4. Kafka作为大数据的核心技术,你了解多少?

    Kafka作为大数据最核心的技术,作为一名技术开发人员,如果你不懂,那么就真的“out”了.DT时代的快速发展离不开kafka,所以了解kafka,应用kafka就成为一种必须. 什么是kafka?K ...

  5. Spark Streaming和Kafka整合保证数据零丢失

    当我们正确地部署好Spark Streaming,我们就可以使用Spark Streaming提供的零数据丢失机制.为了体验这个关键的特性,你需要满足以下几个先决条件: 1.输入的数据来自可靠的数据源 ...

  6. spark-streaming集成Kafka处理实时数据

    在这篇文章里,我们模拟了一个场景,实时分析订单数据,统计实时收益. 场景模拟 我试图覆盖工程上最为常用的一个场景: 1)首先,向Kafka里实时的写入订单数据,JSON格式,包含订单ID-订单类型-订 ...

  7. kafka 清除topic数据脚本

    原 kafka 清除topic数据脚本 2018年07月25日 16:57:13 pete1223 阅读数:1028     #!/bin/sh       param=$1   echo " ...

  8. kafka查看消费数据

    一.如何查看 在老版本中,使用kafka-run-class.sh 脚本进行查看.但是对于最新版本,kafka-run-class.sh 已经不能使用,必须使用另外一个脚本才行,它就是kafka-co ...

  9. 【Kafka】Kafka数据可靠性深度解读

    转帖:http://www.infoq.com/cn/articles/depth-interpretation-of-kafka-data-reliability Kafka起初是由LinkedIn ...

  10. Kafka如何保证数据不丢失

    Kafka如何保证数据不丢失 1.生产者数据的不丢失 kafka的ack机制:在kafka发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到,其中状态有0,1,-1. 如果是 ...

随机推荐

  1. 升级IDEA后Lombok不能用了,如何解决?

    今天到工作室比较晚,在电脑前吃着早饭,看到提示IDEA提示升级,寻思已经有好久没有升过级了.一样等着,就升级下吧. 升级完毕重启之后,突然发现好多错误,原来的应用也没法启动了.仔细一看报错信息,是由于 ...

  2. 你真的懂 export default 吗?

    export default A 和 export { A as default } 乍一看是一样的,但是里面有一些细微的区别比较容易留坑.本文介绍两种写法的不同之处. import 语句导入的是引用 ...

  3. shell运维习题训练

    注:初学shell,以下为本人自己写的答案,如果有更好的,请指教! 1. 求2个数之和: 2. 计算1-100的和 3. 将一目录下所有的文件的扩展名改为bak 4.编译并执行当前目录下的所有.c文件 ...

  4. buu [MRCTF2020]keyboard

    密文: ooo yyy ii w uuu ee uuuu yyy uuuu y w uuu i i rr w i i rr rrr uuuu rrr uuuu t ii uuuu i w u rrr ...

  5. Flink进入大厂面试准备,收藏这一篇就够了

    1. Flink 的容错机制(checkpoint) Checkpoint机制是Flink可靠性的基石,可以保证Flink集群在某个算子因为某些原因(如 异常退出)出现故障时,能够将整个应用流图的状态 ...

  6. Java核心基础第4篇-Java数组的常规操作

    Java数组 一.数组简介 数组是多个相同类型数据的组合,实现对这些数据的统一管理 数组属引用类型,数组型数据是对象(Object) 数组中的元素可以是任何数据类型,包括基本类型和引用类型 数组类型是 ...

  7. 整理C#获取日期显示格式

    C#获取当前日期的几种显示格式 有时候需要用一些不常用的日期格式时,总是要去网上查找,很多都是复制粘贴,还不完整.就整理一下. DatetimeTextBox.Text += DateTime.Now ...

  8. asp.net mvc中的安全性

    1.重复提交攻击:通过Bind特性指定要绑定和不绑定的值. 2.Cookie盗窃:阻止脚本对站点中Cookie的访问,webconfig文件中添加<HttpCookies domain=&quo ...

  9. Linux sudo命令——sudoers文件的配置

    Linux sudo命令与其配置文件/etc/sudoers   对linux有一定了解的人多少也会知道点关于sudo命令.sudo命令核心思想是权限的赋予 ,即某个命令的所属用户不是你自己,而你却有 ...

  10. homestead

    前言 之前写过一篇文章(https://www.jianshu.com/p/5f30280a3c18),说不需要这玩意儿一样可以开发.是的,但是对于团队来说,使用统一的环境.开发工具.编码规范等,对于 ...