alpakka-kafka(8)-kafka数据消费模式实现
上篇介绍了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数据消费模式实现的更多相关文章
- alpakka-kafka(7)-kafka应用案例,消费模式
上篇描述的kafka案例是个库存管理平台.是一个公共服务平台,为其它软件模块或第三方软件提供库存状态管理服务.当然,平台管理的目标必须是共享的,即库存是作为公共资源开放的.这个库存管理平台是一个Kaf ...
- Spark Streaming消费Kafka Direct方式数据零丢失实现
使用场景 Spark Streaming实时消费kafka数据的时候,程序停止或者Kafka节点挂掉会导致数据丢失,Spark Streaming也没有设置CheckPoint(据说比较鸡肋,虽然可以 ...
- Flume简介与使用(三)——Kafka Sink消费数据之Kafka安装
前面已经介绍了如何利用Thrift Source生产数据,今天介绍如何用Kafka Sink消费数据. 其实之前已经在Flume配置文件里设置了用Kafka Sink消费数据 agent1.sinks ...
- Kafka作为大数据的核心技术,你了解多少?
Kafka作为大数据最核心的技术,作为一名技术开发人员,如果你不懂,那么就真的“out”了.DT时代的快速发展离不开kafka,所以了解kafka,应用kafka就成为一种必须. 什么是kafka?K ...
- Spark Streaming和Kafka整合保证数据零丢失
当我们正确地部署好Spark Streaming,我们就可以使用Spark Streaming提供的零数据丢失机制.为了体验这个关键的特性,你需要满足以下几个先决条件: 1.输入的数据来自可靠的数据源 ...
- spark-streaming集成Kafka处理实时数据
在这篇文章里,我们模拟了一个场景,实时分析订单数据,统计实时收益. 场景模拟 我试图覆盖工程上最为常用的一个场景: 1)首先,向Kafka里实时的写入订单数据,JSON格式,包含订单ID-订单类型-订 ...
- kafka 清除topic数据脚本
原 kafka 清除topic数据脚本 2018年07月25日 16:57:13 pete1223 阅读数:1028 #!/bin/sh param=$1 echo " ...
- kafka查看消费数据
一.如何查看 在老版本中,使用kafka-run-class.sh 脚本进行查看.但是对于最新版本,kafka-run-class.sh 已经不能使用,必须使用另外一个脚本才行,它就是kafka-co ...
- 【Kafka】Kafka数据可靠性深度解读
转帖:http://www.infoq.com/cn/articles/depth-interpretation-of-kafka-data-reliability Kafka起初是由LinkedIn ...
- Kafka如何保证数据不丢失
Kafka如何保证数据不丢失 1.生产者数据的不丢失 kafka的ack机制:在kafka发送数据的时候,每次发送消息都会有一个确认反馈机制,确保消息正常的能够被收到,其中状态有0,1,-1. 如果是 ...
随机推荐
- mapboxgl 互联网地图纠偏插件(二)
前段时间写的mapboxgl 互联网地图纠偏插件(一)存在地图旋转时瓦片错位的问题. 这次没有再跟 mapboxgl 的变换矩阵较劲,而是另辟蹊径使用 mapboxgl 的自定义图层,重新写了一套加载 ...
- k8s部署docker容器
一.环境 需机器已部署好k8s和docker的环境 二.操作步骤 1.将制作好的镜像推送到docker仓库 docker tag nginx:test harbor:test-nginx docker ...
- [心得体会]Spring容器的初始化
1. Spring容器的初始化过程 public AnnotationConfigApplicationContext(Class<?>... annotatedClasses) { ...
- sshpass用法介绍
参考文章:http://www.mamicode.com/info-detail-1105345.html https://www.jianshu.com/p/a2aaa02f57dd p.p1 { ...
- 羊城杯wp babyre
肝了好久,没爆破出来,就很难受,就差这题没写了,其他三题感觉挺简单的,这题其实也不是很难,我感觉是在考算法. 在输入之前有个smc的函数,先动调,attach上去,ida打开那个关键函数. 代码逻辑还 ...
- linux学习之路第三天
开机,重启和用户登陆注销 关机&重启命令 shutdown shutdown -h now :表示立即关机 shutdown -h 1 :表示一分钟后关机 shutdown -r now :表 ...
- 小白都能理解的TCP三次握手四次挥手
前言 TCP在学习网络知识的时候是经常的被问到知识点,也是程序员必学的知识点,今天小杨用最直白的表述带大家来认识认识,喜欢的朋友记得点点关注哈. 何为TCP 上点官方的话:是一种面向连接(连接导向)的 ...
- Java 给PDF签名时添加可信时间戳
一.程序运行环境 编译环境:IntelliJ IDEA 所需测试文件:PDF..pfx数字证书及密钥.PDF Jar包(Free Spire.PDF for Java).签名图片(.png格式) 可信 ...
- Linux上生产环境源码方式安装配置postgresql12
1.Linux上源码方式安装postgresql12 01.准备操作系统环境 echo "192.168.1.61 tsepg61" >> /etc/hosts mou ...
- LeetCode 887. Super Egg Drop
题目链接:https://leetcode.com/problems/super-egg-drop/ 题意:给你K个鸡蛋以及一栋N层楼的建筑,已知存在某一个楼层F(0<=F<=N),在不高 ...