上篇描述的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应用案例,消费模式的更多相关文章

  1. kafka channle的应用案例

      kafka channle的应用案例 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 最近在新公司负责大数据平台的建设,平台搭建完毕后,需要将云平台(我们公司使用的Ucloud的 ...

  2. SpringBoot2 整合Kafka组件,应用案例和流程详解

    本文源码:GitHub·点这里 || GitEE·点这里 一.搭建Kafka环境 1.下载解压 -- 下载 wget http://mirror.bit.edu.cn/apache/kafka/2.2 ...

  3. Kafka(3)--kafka消息的存储及Partition副本原理

    消息的存储原理: 消息的文件存储机制: 前面我们知道了一个 topic 的多个 partition 在物理磁盘上的保存路径,那么我们再来分析日志的存储方式.通过 [root@localhost ~]# ...

  4. kafka实战教程(python操作kafka),kafka配置文件详解

    kafka实战教程(python操作kafka),kafka配置文件详解 应用往Kafka写数据的原因有很多:用户行为分析.日志存储.异步通信等.多样化的使用场景带来了多样化的需求:消息是否能丢失?是 ...

  5. CentOS 7部署Kafka和Kafka集群

    CentOS 7部署Kafka和Kafka集群 注意事项 需要启动多个shell脚本交互客户端进行验证,运行中的客户端不要停止. 准备工作: 安装java并设置java环境变量,在`/etc/prof ...

  6. Kafka记录-Kafka简介与单机部署测试

    1.Kafka简介 kafka-分布式发布-订阅消息系统,开发语言-Scala,协议-仿AMQP,不支持事务,支持集群,支持负载均衡,支持zk动态扩容 2.Kafka的架构组件 1.话题(Topic) ...

  7. Apache Kafka安全| Kafka的需求和组成部分

    1.目标 - 卡夫卡安全 今天,在这个Kafka教程中,我们将看到Apache Kafka Security 的概念  .Kafka Security教程包括我们需要安全性的原因,详细介绍加密.有了这 ...

  8. kafka - Confluent.Kafka

    上个章节我们讲了kafka的环境安装(这里),现在主要来了解下Kafka使用,基于.net实现kafka的消息队列应用,本文用的是Confluent.Kafka,版本0.11.6 1.安装: 在NuG ...

  9. Python+SparkStreaming+kafka+写入本地文件案例(可执行)

    从kafka中读取指定的topic,根据中间内容的不同,写入不同的文件中. 文件按照日期区分. #!/usr/bin/env python # -*- coding: utf-8 -*- # @Tim ...

  10. kafka拦截器原理|案例实操

    拦截器原理 Producer拦截器(interceptor)是在Kafka 0.10版本被引入的,主要用于实现clients端的定制化控制逻辑. 对于producer而言,interceptor使得用 ...

随机推荐

  1. sys用户权限不足,本地登录失败 |ORA-01031 insufficient privileges|

    机器总喜欢挑放假的时候出问题,"双节"(中秋.国庆)快到了,对于搞系统运维的工程师来说其实并不轻松,于是今天赶紧装起一台数据库备用服务器以备半夜"机"叫. 安装 ...

  2. POJ 1082 Calendar Game 原来这题有个超简单的规律

    万能的discuss.只需要月份和天数同奇同偶即可,9月30和11月30例外 #include <iostream> #include <cstdio> using names ...

  3. 配置tomcat虚拟主机

    实例说明 本实例介绍如何配置tomcat的虚拟主机. 关键技术 关于server.xml中host这个元素,只有在设置虚拟主机是才会修改.虚拟主机是一种在一个Web服务器上服务多个域名的机制,对这个域 ...

  4. RabbitMQ 常用知识点总结

    基础 为什么使用 MQ? 1.削峰:在某个模块接收到超过最大承受的并发量时,可以通过 MQ 排队来使这些削减同一时刻处理的消息量.减小并发量. 2.解耦:在发送 MQ 处理业务时,可以使业务代码与当前 ...

  5. Hibernate框架(四)缓存策略+lazy

    Hibernate作为和数据库数据打交道的框架,自然会设计到操作数据的效率问题,而对于一些频繁操作的数据,缓存策略就是提高其性能一种重要手段,而Hibernate框架是支持缓存的,而且支持一级和二级两 ...

  6. 1.3.2、通过Cookie匹配

    server: port: 8080 spring: application: name: gateway cloud: gateway: routes: - id: guo-system4 uri: ...

  7. K8S(Kubernetes)学习笔记

    Kubernetes(k8s)是google提供的开源的容器集群管理系统,在Docker技术的基础上,为容器化的应用提供部署运行.资源调度.服务发现和动态伸缩等一系列完整功能,提高了大规模容器集群管理 ...

  8. buu crypto 变异凯撒

    一.由题目就可知是凯撒加密,但是是变异,说明有改动,但是凯撒的本质移位是不变的,将密文afZ_r9VYfScOeO_UL^RWUc,和flag进行比较,字符表查一下,发现 a:97 f:102 f:1 ...

  9. 使用Octotree插件在Edge上以树形结构浏览GitHub上的源码

    先预览效果左侧的目录通过点击,就可以到达对应的源码位置. 首先点击打开Edge中的浏览器扩展在右上角...=>点击扩展=>点击获取Microsoft Edge扩展按钮=>在左侧搜索所 ...

  10. create-react-app 入门学习

    全局安装 create-react-app npm install create-react-app -g 创建项目 在全局安装了create-react-app 后 创建项目,如果按照下面的第一种办 ...