alpakka-kafka(7)-kafka应用案例,消费模式
上篇描述的kafka案例是个库存管理平台。是一个公共服务平台,为其它软件模块或第三方软件提供库存状态管理服务。当然,平台管理的目标必须是共享的,即库存是作为公共资源开放的。这个库存管理平台是一个Kafka消费端独立运行的软件。kafka的生产方即平台的服务对象通过kafka生产端producer从四面八方同时、集中将消息写入kafka。库存管理平台在kafka消费端不间断监控kafka里新的未读过的消息并及时读取,解析消息获取发布者对库存管理的指令,然后按指令更新库存状态。
设计这个库存管理平台最主要的目的先是为了保证库存状态的时效性、准确性,然后才是库存更新的效率。由于库存更新指令的产生是在一个高并发、异类系统、分布式环境里,上篇已经提到多线程环境下更新共享数据会产生的问题。不过通过kafka把并发产生的指令转换成队列然后按顺序单线程逐句执行就能解决主要问题了。现在,平台的数据来源变成kafka消费端口上的一个数据流了,数据的读取和消费自然也变成了逐条的。kafka提供了某种游标机制来记录数据读取的最新位置,防止数据消费过程中的遗漏、重复。记录当前读取位置offset的方式就是所谓数据消费模式代表数据消费不同程度的安全/效率比例,安全系数越高,流量越低。具体读取位置offset可以存放在kafka内部,或者保存在某种数据库表里。简单来讲,数据消费模式分三种:至多一次at-most-once,至少一次at-least-once,只此一次exactly-once。
从由kafka中读出指令到成功完成执行指令整个消息消费过程可能经历多个步骤。每个步骤都可能有失败的可能,从而中断过程影响数据消费结果。保存offset即offset-commit的时间点代表了三种消费模式的特性:
1、至多一次at-most-once:读出数据立即commit-offset,然后才开始消费数据。无论消费过程中发生异常与否,下次都会从新的位置开始读取,过去不再。如果一条数据在消费过程中发生事故中断了过程,那这条数据就没有发生应有的作用,就等于遗失了。
2、至少一次at-least-once:读出数据、消费数据、然后才commit-offset。如果消费过程出现问题中断,那么offset就得不到保存,下次再读取时还是从原先位置重新开始。所以,一条数据有可能被多次读取,造成重复消费的效果。
3、只此一次exactly-once:把保存offse和消费过程放到同一个事务transaction里。这种模式需要数据库事物处理支持,也就是说offset-commit和数据处理都必须在同一种提供事物处理支持的数据库环境里进行。offset-commit只会在确保消费过程成功完成后才进行。
at-most-once和at-least-once都使用kafka内部commit机制保存offset。at-least-once可以利用kafka的自动commit机制实现offset保存,只要通过kafka配置就可以了。下面是这个配置的示范:
val consumerSettings =
ConsumerSettings(consumerConfig, new StringDeserializer, new StringDeserializer)
.withBootstrapServers(bootstrapServers)
.withGroupId(group)
.withProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset)
.withProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")
.withProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs.toString)
ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG = "true" 代表开启auto-commit模式。ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG设置了auto-commit之间的毫秒时间间隔。在这个间隔内如果中断消费过程,那么在这个间隔内读取所有数据的offset都未能commit,但其中有些数据已经完成消费了。重启读取就会从这个间隔开始时的offset从头读取,那么之前消费的数据就会再次消费,等于重复消费了。auto-commit间隔设置的越短,重复消费的数据就越少,不过kafka需要更密集的进行commit-offset,运行效率就越低。反之,重复消费的数据量就越大,消费计算精确度越低,但运行效率就会提高。
在alpakka-kafka里用一个普通的Source就可以实现at-least-once消费模式了:
val consumerSettings =
ConsumerSettings(consumerConfig, new StringDeserializer, new StringDeserializer)
.withBootstrapServers(bootstrapServers)
.withGroupId(group)
.withProperty(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, autoOffsetReset)
.withProperty(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "true")
.withProperty(ConsumerConfig.AUTO_COMMIT_INTERVAL_MS_CONFIG, autoCommitIntervalMs.toString) var subscription = Subscriptions
.topics(topic) val stkTxns = new DocToStkTxns(trace)
val curStk = new CurStk(trace)
val pcmTxns = new PcmTxns(trace) val plainSource = Consumer
.plainSource(consumerSettings,subscription)
run这个plainSource形成的akka-stream就实现了一个完整kafka-reader功能:
plainSource
.mapAsync(1) {msg =>
updateStock(msg)
}
.toMat(Sink.seq)(Keep.left)
.run()
offset-commit在这个reader-stream里是不可控的,是kafka按预先设置自动进行的。
plainSource是一个独立的stream,代表单个reader。为了充分利用平台的硬件资源,首先考虑的是同时运行多个stream,如下:
(1 to numReaders).toList.map { _ =>
plainSource
.mapAsync(1) {msg =>
updateStock(msg)
}
.toMat(Sink.seq)(Keep.left)
.run()
}
这样可以同时运行numReaders条stream。不过,现在设计方案又返回了多线程环境,好像又要面临多并发所产生的一系列问题了。我们来分析分析:首先,前面描述的库存更新多线程竞争问题主要是针对同一门店,同一商品,同时更新库存状态引发的。以上设计中每条stream,即每个reader,如果属于同一个reader-group(group-id相同)的话,应共同分别负责所有partition中的部分partition,是不会共享partition的。那么,写入每个partition的数据是否交叉重复就很关键了。实际上,在上游消息发布阶段决定了消息应该写入的具体partition,如下:
def writeToKafka(posTxn: PosTxns)(implicit producerKafka: ProducerKafka) = {
val doc = BizDoc.fromPosTxn(posTxn)
if (producerKafka.producerSettings.isDefined) {
implicit val producer = producerKafka.akkaClassicSystem.get
SendProducer(producerKafka.producerSettings.get)
.send(new ProducerRecord[String, String](producerKafka.publisherSettings.topic, doc.shopId, toJson(doc)))
} else FastFuture.successful(Completed)
}
ProducerRecord[K,V] 的key设定为shopId,具体目标partition由kafka的默认指派算法根据key的值产生,保证同一key值一定会指派给同一个partition。虽然在门店数量>partition数量的情况下每个partition可以包含多个shopId, 但各partition所包含的shopId不会交叉重复。所以,以上多reader同时运行的设计中,只要属于同一个reader-group,shopId就不会相同,就不会产生线程竞争问题。
那么,在同一个reader的消费过程中是否能使用多线程方式呢?上面的例子中使用了mapAsync(parallelism=1),这个代表了stream里的一个阶段。这个阶段容许收到上游数据后以parallelism个future来并行处理,同时可以保证流出下游的数据遵守上游流入数据的顺序。但是,在同一阶段用多线程方式计算方式在遇到同门店、同商品库存更新时同样会产生多线程竞争问题,所以只能取parallelism=1。不过,可以考虑把数据处理过程分割成几个阶段,因为每个阶段流入流出的数据是同循序的,所以可以容许多个阶段在在各自的线程里运算。如:
(1 to numReaders).toList.map { _ =>
plainSource
.mapAsync(1) {msg =>
produceStkTxns(msg)
}
asyn.mapAsync(1) {msg =>
updateCurStock(msg)
}
asyn.mapAsync(1) {msg =>
updatePurchase(msg)
}
.toMat(Sink.seq)(Keep.left)
.run()
}
可以用asyn.mapAsync来分割异线程域async-boundary以实现多线程运算效果。
下面的完整例子里把异常处理和重启也考虑了进去:
def start =
(1 to numReaders).toList.map { _ =>
RestartSource
.onFailuresWithBackoff(restartSource) { () => plainSource }
// .viaMat(KillSwitches.single)(Keep.right)
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-msg: $msg")(Messages.MachineId("", ""))
}
_ <- stkTxns.docToStkTxns(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-docToStkTxns: $msg")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: msg: $msg")(Messages.MachineId("", ""))
}
curstks <- curStk.updateStk(msg.value())
pmsg<- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: curstks-$curstks")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-writePcmTxn: msg: $msg")(Messages.MachineId("", ""))
}
pcm <- pcmTxns.writePcmTxn(msg.value())
pmsg <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: writePcmTxn-$pcm")(Messages.MachineId("", ""))
msg
}
} yield pmsg
}
.async.mapAsync(1) { msg =>
for {
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updatePcm: msg: $msg")(Messages.MachineId("", ""))
}
_ <- pcmTxns.updatePcm(msg.value())
_ <- FastFuture.successful {
log.step(s"AtLeastOnceReaderGroup-updateStk: updatePcm-$msg")(Messages.MachineId("", ""))
}
} yield "Completed"
}
.toMat(Sink.seq)(Keep.left)
.run()
}
下面是几个消费模式的测试示范代码:
package com.datatech.txn.server
import akka.actor.ActorSystem
import scala.concurrent._
import MgoRepo._
import com.typesafe.config.ConfigFactory
import scala.jdk.CollectionConverters._ object ConsumeModeTest extends App with JsonConverter {
val config_onenode = ConfigFactory.load("onenode")
implicit val system = ActorSystem("kafka-sys",config_onenode)
var config = ConfigFactory.load() implicit val ec: ExecutionContext = system.dispatcher //mat.executionContext var httpport: Int = 53081
var mongohosts = List("localhost:27017")
var elastichost = "http://localhost:9200"
var _http_parallelism: Int = 8
var _seednodes: String = "" val txnCfg = ConfigFactory.load("txnserver.conf").getConfig("txn.server")
try {
mongohosts = txnCfg.getStringList("mongohosts").asScala.toList
elastichost = txnCfg.getString("elastichost")
_http_parallelism = txnCfg.getInt("http_parallelism")
_seednodes = txnCfg.getString("seednodes")
httpport = txnCfg.getInt("httpport")
}
catch {
case excp: Throwable =>
httpport = 53081
mongohosts = List("localhost:27017")
elastichost = "http://localhost:9200"
_http_parallelism = 8
} implicit val mgoClient = mongoClient(mongohosts) val readerConfig = config.getConfig("akka.kafka.consumer")
val readerSettings = ReaderSettings(config.getConfig("kafka-txnserver-consumer")) implicit val idxer = new TxnIndex(elastichost,true) readerSettings.consumeMode.toLowerCase() match {
case "atleastonce" =>
val readerGroup = AtLeastOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
case "atmostonce" =>
val readerGroup = AtMostOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
case "exactlyonce" =>
val readerGroup = ExactlyOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
case _ =>
val readerGroup = AtLeastOnceReaderGroup(readerConfig,readerSettings, true)
readerGroup.start
} scala.io.StdIn.readLine()
idxer.close()
scala.io.StdIn.readLine()
system.terminate() }
alpakka-kafka(7)-kafka应用案例,消费模式的更多相关文章
- kafka channle的应用案例
kafka channle的应用案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 最近在新公司负责大数据平台的建设,平台搭建完毕后,需要将云平台(我们公司使用的Ucloud的 ...
- SpringBoot2 整合Kafka组件,应用案例和流程详解
本文源码:GitHub·点这里 || GitEE·点这里 一.搭建Kafka环境 1.下载解压 -- 下载 wget http://mirror.bit.edu.cn/apache/kafka/2.2 ...
- Kafka(3)--kafka消息的存储及Partition副本原理
消息的存储原理: 消息的文件存储机制: 前面我们知道了一个 topic 的多个 partition 在物理磁盘上的保存路径,那么我们再来分析日志的存储方式.通过 [root@localhost ~]# ...
- kafka实战教程(python操作kafka),kafka配置文件详解
kafka实战教程(python操作kafka),kafka配置文件详解 应用往Kafka写数据的原因有很多:用户行为分析.日志存储.异步通信等.多样化的使用场景带来了多样化的需求:消息是否能丢失?是 ...
- CentOS 7部署Kafka和Kafka集群
CentOS 7部署Kafka和Kafka集群 注意事项 需要启动多个shell脚本交互客户端进行验证,运行中的客户端不要停止. 准备工作: 安装java并设置java环境变量,在`/etc/prof ...
- Kafka记录-Kafka简介与单机部署测试
1.Kafka简介 kafka-分布式发布-订阅消息系统,开发语言-Scala,协议-仿AMQP,不支持事务,支持集群,支持负载均衡,支持zk动态扩容 2.Kafka的架构组件 1.话题(Topic) ...
- Apache Kafka安全| Kafka的需求和组成部分
1.目标 - 卡夫卡安全 今天,在这个Kafka教程中,我们将看到Apache Kafka Security 的概念 .Kafka Security教程包括我们需要安全性的原因,详细介绍加密.有了这 ...
- kafka - Confluent.Kafka
上个章节我们讲了kafka的环境安装(这里),现在主要来了解下Kafka使用,基于.net实现kafka的消息队列应用,本文用的是Confluent.Kafka,版本0.11.6 1.安装: 在NuG ...
- Python+SparkStreaming+kafka+写入本地文件案例(可执行)
从kafka中读取指定的topic,根据中间内容的不同,写入不同的文件中. 文件按照日期区分. #!/usr/bin/env python # -*- coding: utf-8 -*- # @Tim ...
- kafka拦截器原理|案例实操
拦截器原理 Producer拦截器(interceptor)是在Kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑. 对于producer而言,interceptor使得用 ...
随机推荐
- MySQL数据库复制技术应用实战(阶段二)
MySQL数据库复制技术应用实战(阶段二)文档 作者 刘畅 时间 2020-9-27 服务器版本:CentOS Linux release 7.5.1804 主机名 ip地址 服务器配置 安装软件 密 ...
- 学习django的日子
bilibii这个网站是个学习者网站,里面有很多学习视频
- SpringCloud:SpringBoot整合SpringCloud项目
划分模块 这里我划分了四个模块 Common: 存放bean和Dao模块 Consumer: 消费者模块,提供对外暴露接口服务 EurekaServer: Eureka注册中心模块,主要用于启动注册中 ...
- ExtJs4学习(四):Extjs 中id与itemId的区别
为了方便表示或是指定一个组件的名称,我们通常会使用id或者itemId进行标识命名.(推荐尽量使用itemId,这样可以减少页面唯一标识而产生的冲突) id: id是作为整个页面的Compo ...
- Mybatis学习(6)与Spring MVC 的集成
前面几篇文章已经讲到了mybatis与spring 的集成.但这个时候,所有的工程还不是web工程,虽然我一直是创建的web 工程.今天将直接用mybatis与Spring mvc 的方式集成起来,源 ...
- 阿里云ECS问题 Login Incorrect , all available gssapi merchanisms failed
1.阿里云ECS无法登录 Login Incorrect 阿里云ECS密码包含2个密码: 1.重置密码(实例密码也就是我们SSH远程连接的密码): 2.修改远程连接密码(在阿里云网页控制台上远程连接的 ...
- 『心善渊』Selenium3.0基础 — 25、unittest单元测试框架
目录 1.unittest基本简介 2.unittest基本概念 (1)unittest核心的四个概念 (2)如何创建一个测试类 (3)test fixture常用的四个方法 (4)unittest编 ...
- Pytest学习笔记12-配置文件pytest.ini
前言 pytest配置文件可以改变pytest的运行方式,它是一个固定的文件pytest.ini文件,读取配置信息,按指定的方式去运行. 常用的配置项 marks 作用:测试用例中添加了自定义标记( ...
- ARTS第四周
补第四周 1.Algorithm:每周至少做一个 leetcode 的算法题2.Review:阅读并点评至少一篇英文技术文章3.Tip:学习至少一个技术技巧4.Share:分享一篇有观点和思考的技术文 ...
- CF1444D Rectangular Polyline[题解]
Rectangular Polyline 题目大意 给定 \(h\) 条长度分别为 \(l_1,l_2,--,l_h\) 的水平线段以及 \(v\) 条长度分别为 \(p_1,p_2,--.p_v\) ...