以前老的sarama版本不支持消费者组的消费方式,所以大多数人都用sarama-cluster。

后来sarama支持了消费者组的消费方式,sarama-cluster也停止维护了,但网上关于sarama的消费者组的解析很少,且官方的样例很简单,所以这里分析一下。

一、官方样例

官方样例比较简单:

1、通过 sarama.NewConfig 创建一个配置

2、通过 NewConsumerGroup 创建一个消费者组

3、通过 Consume 创建消费者组的会话,该函数的第三个参数即为该会话三个阶段的回调: Setup CleanupConsumeClaim ,分别在创建会话之前、会话结束之后 和 会话生存中(主要就是在此阶段进行消息读取)进行调用。

二、问题

1、当指定的topic在kafka中不存的时候,kafka会新建该topic,如果只想让用户消费已存在的topic,那么该如何获取kafka中已经存在的topic?

2、 setupCleanup 的调用流程是怎样的?会在哪些情况下被调用?

3、既然是消费者组,那如何查看组里某个消费者拥有哪些topic和partition?

4、如何使用指定的 offset 来消费某个 topic ?

5、如何实现消费的 Exactly-once?

注:以上测试使用的示例代码是自己写的样例代码的部分内容,完整的样例代码见文章最后

三、分析

1、在 sarama 中,获取 topic 的接口在 Client interface 中,所以需要先通过 NewClient 接口创建一个 client,然后就可以通过该 client 的 Topics 接口获取到 kafka 中所有的 topic。但消费者组使用的类型是 ConsumerGroup ,那该如何获取该类型呢?sarama 中提供 NewConsumerGroupFromClient 接口,可以从一个现存的 client 创建一个 ConsumerGroup ,所以,修改后的流程,由原先的 NewConsumerGroup 直接创建,变成:

a、使用 NewClient 创建一个 client

b、使用 NewConsumerGroupFromClient 创建 ConsumerGroup

具体代码实现如下:

  1. // 创建client
  2. newClient, err := sarama.NewClient(brokers, config)
  3. if err != nil {
  4. log.Fatal(err)
  5. }
  6. // 获取所有的topic
  7. topics, err := newClient.Topics()
  8. if err != nil {
  9. log.Fatal(err)
  10. }
  11. log.Info("topics: ", topics)
  12. // 根据client创建consumerGroup
  13. client, err := sarama.NewConsumerGroupFromClient(k.group, newClient)
  14. if err != nil {
  15. log.Fatalf("Error creating consumer group client: %v", err)
  16. }

这么做的好处就是:可以使用 client 的接口,获取一些信息,例如 kafka 的当前配置有哪些,controller 有哪些,brokers 有哪些,topic 总共有哪些,特定的 topic 有哪些 partitions,partition 当前的 offset 是多少 等等,具体功能可查看 Client interface

  1. type Client interface {
  2. // Config returns the Config struct of the client. This struct should not be
  3. // altered after it has been created.
  4. Config() *Config
  5. // Controller returns the cluster controller broker. It will return a
  6. // locally cached value if it's available. You can call RefreshController
  7. // to update the cached value. Requires Kafka 0.10 or higher.
  8. Controller() (*Broker, error)
  9. // RefreshController retrieves the cluster controller from fresh metadata
  10. // and stores it in the local cache. Requires Kafka 0.10 or higher.
  11. RefreshController() (*Broker, error)
  12. // Brokers returns the current set of active brokers as retrieved from cluster metadata.
  13. Brokers() []*Broker
  14. ......
  15. }

2、 setupCleanupConsumeClaim 是 s.handler.ConsumeClaim 的三个接口,需要用户自己实现。可以简单理解为:当需要创建一个会话时,先运行 setup ,然后在 ConsumerClaim 中处理消息,最后运行 Cleanup

setup 会在一个新会话开始之前运行,且也在 ConsumerClaim 接口之前运行。调用流程为: Consume —> newSession —> newConsumerGroupSession —> handler.Setup

在调用了 Setup 之后,后面会创建一个协程,该协程里面其实调用的就是 ConsumeClaim 接口,所以我们实现的 ConsumerClaim 其实是一个单独的协程,其调用流程为: Consume —> newSession —> newConsumerGroupSession —> consume —> s.handler.ConsumeClaim

Cleanup 会在一个会话结束的时候运行。调用流程为:Consume —>release —> s.handler.Cleanup

了解了调用流程之后,哪些情况又会调用到他们呢?—> 1、新建consumeGroup的时候。2、发生rebalance的时候。

我们可以在setup和cleanup中加一个打印:

  1. func (k *Kafka) Setup(session sarama.ConsumerGroupSession) error {
  2. log.Info("setup")
  3. close(k.ready)
  4. return nil
  5. }
  6. func (k *Kafka) Cleanup(sarama.ConsumerGroupSession) error {
  7. log.Info("cleanup")
  8. return nil
  9. }

然后启动一个consumer,可以观察到打印:

  1. INFO[0000] setup

然后按 Ctrl + C 关闭 consumer,可以观察到打印:

  1. INFO[0101] cleanup

说明新建consumer然后退出时,会调用 setup 和 cleanup。

我们再试一下发生rebalance的情况:先启动一个consumer,然后再启动一个同一组的consumer,可以看到打印为:

  1. 第一个启动的 consumer 打印为:
  2. INFO[0000] setup
  3. INFO[0006] cleanup
  4. INFO[0006] setup
  5. 第二个启动的 consumer 打印为:
  6. INFO[0002] setup

说明在发生 reblance 的时候,会先关闭原先的会话,并调用 cleanup,然后再调用 setup,最后生成一个新的会话。

3、在 ConsumerGroupSession 接口中,有一个 Claims 接口,可以用来查看当前 consumer 被分配到哪些 topic 和 partition。我们可以在 Setup 接口中进行打印:

  1. func (k *Kafka) Setup(session sarama.ConsumerGroupSession) error {
  2. log.Info("setup")
  3. log.Info(session.Claims())
  4. close(k.ready)
  5. return nil
  6. }

这里使用 range 分区策略,订阅的topic为t1p4和t2p4,每个topic都有4个分区,然后创建3个consumer,产生的打印为:

  1. consumer1:
  2. INFO[0000] setup
  3. INFO[0000] map[t1p4:[0 1 2 3] t2p4:[0 1 2 3]]
  4. INFO[0009] cleanup
  5. INFO[0009] setup
  6. INFO[0009] map[t1p4:[0 1] t2p4:[2 3]]
  7. INFO[0015] cleanup
  8. INFO[0015] setup
  9. INFO[0015] map[t1p4:[0] t2p4:[3]]
  10. consumer2:
  11. INFO[0002] setup
  12. INFO[0002] map[t1p4:[2 3] t2p4:[0 1]]
  13. INFO[0009] cleanup
  14. INFO[0009] setup
  15. INFO[0009] map[t1p4:[1 2] t2p4:[0]]
  16. consumer3:
  17. INFO[0000] setup
  18. INFO[0000] map[t1p4:[3] t2p4:[1 2]]

当只有consumer1的时候,它被分配到所有的分区: t1p4:[0 1 2 3] t2p4:[0 1 2 3]

当consumer2加入的时候,consumer1被分配的是: t1p4:[0 1] t2p4:[2 3], consumer2被分配的是: t1p4:[2 3] t2p4:[0 1]

当consumer3加入的时候,consumert1被分配的是: t1p4:[0] t2p4:[3] ,consumer2被分配的是: t1p4:[1 2] t2p4:[0], consumer3被分配的是: t1p4:[3] t2p4:[1 2]

有兴趣的可以再依次删除consumer1,consumer2。

4、kafka的config配置中,指定消费的offset只有两个: OffsetNewestOffsetOldest ,如果想指定 offset 进行消费,该怎么做呢?

前面说过, Setup 是运行在会话最一开始的地方,且这个时候已经能够获取到所有的 topic 和 partition,所以这里可以使用 ConsumerGroupSessionResetOffset 接口进行设置,具体实现如下:(这里使用的主题:t2p4 已存在,且0分区中的offset已经到18)

  1. func (k *Kafka) Setup(session sarama.ConsumerGroupSession) error {
  2. log.Info("setup")
  3. session.ResetOffset("t2p4", 0, 13, "")
  4. log.Info(session.Claims())
  5. close(k.ready)
  6. return nil
  7. }
  8. func (k *Kafka) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
  9. for message := range claim.Messages() {
  10. log.Infof("[topic:%s] [partiton:%d] [offset:%d] [value:%s] [time:%v]",
  11. message.Topic, message.Partition, message.Offset, string(message.Value), message.Timestamp)
  12. session.MarkMessage(message, "")
  13. }
  14. return nil
  15. }

此时,无论运行多少次,都可以消费 13 到 18 之间的消息:

  1. INFO[0000] setup
  2. INFO[0000] map[t1p4:[0 1 2 3] t2p4:[0 1 2 3]]
  3. INFO[0000] [topic:t2p4] [partiton:0] [offset:13] [value:a] [time:2021-10-12 23:02:35.058 -0400 EDT]
  4. INFO[0000] [topic:t2p4] [partiton:0] [offset:14] [value:b] [time:2021-10-12 23:02:35.087 -0400 EDT]
  5. INFO[0000] [topic:t2p4] [partiton:0] [offset:15] [value:c] [time:2021-10-12 23:02:35.092 -0400 EDT]
  6. INFO[0000] [topic:t2p4] [partiton:0] [offset:16] [value:d] [time:2021-10-12 23:03:18.882 -0400 EDT]
  7. INFO[0000] [topic:t2p4] [partiton:0] [offset:17] [value:e] [time:2021-10-12 23:03:18.898 -0400 EDT]
  8. INFO[0000] [topic:t2p4] [partiton:0] [offset:18] [value:f] [time:2021-10-12 23:03:18.903 -0400 EDT]

5、前面已经分析了 Setup 的调用流程,以及可以在 Setup 中可以做的事情,那么就可以手动记录 topic 的 offset 到磁盘中(比如文本、数据库等),在 Setup 的接口中,读取之前记录的 offset ,通过 ResetOffset 接口进行重新设置即可。当然,更新 offset 与 消息处理这部分的一致性,需要业务自己保证(例如使用数据库的事务来实现)。

四、完整样例代码

  1. package main
  2. import (
  3. "context"
  4. "os"
  5. "os/signal"
  6. "sync"
  7. "syscall"
  8. "github.com/Shopify/sarama"
  9. log "github.com/sirupsen/logrus"
  10. )
  11. type Kafka struct {
  12. brokers []string
  13. topics []string
  14. startOffset int64
  15. version string
  16. ready chan bool
  17. group string
  18. channelBufferSize int
  19. assignor string
  20. }
  21. var brokers = []string{"192.168.1.101:9092"}
  22. var topics = []string{"t1p4", "t2p4"}
  23. var group = "grp1"
  24. var assignor = "range"
  25. func NewKafka() *Kafka {
  26. return &Kafka{
  27. brokers: brokers,
  28. topics: topics,
  29. group: group,
  30. channelBufferSize: 1000,
  31. ready: make(chan bool),
  32. version: "2.8.0",
  33. assignor: assignor,
  34. }
  35. }
  36. func (k *Kafka) Connect() func() {
  37. log.Infoln("kafka init...")
  38. version, err := sarama.ParseKafkaVersion(k.version)
  39. if err != nil {
  40. log.Fatalf("Error parsing Kafka version: %v", err)
  41. }
  42. config := sarama.NewConfig()
  43. config.Version = version
  44. // 分区分配策略
  45. switch assignor {
  46. case "sticky":
  47. config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategySticky
  48. case "roundrobin":
  49. config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRoundRobin
  50. case "range":
  51. config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRange
  52. default:
  53. log.Panicf("Unrecognized consumer group partition assignor: %s", assignor)
  54. }
  55. config.Consumer.Offsets.Initial = sarama.OffsetNewest
  56. config.ChannelBufferSize = k.channelBufferSize // channel长度
  57. // 创建client
  58. newClient, err := sarama.NewClient(brokers, config)
  59. if err != nil {
  60. log.Fatal(err)
  61. }
  62. // 获取所有的topic
  63. topics, err := newClient.Topics()
  64. if err != nil {
  65. log.Fatal(err)
  66. }
  67. log.Info("topics: ", topics)
  68. // 根据client创建consumerGroup
  69. client, err := sarama.NewConsumerGroupFromClient(k.group, newClient)
  70. if err != nil {
  71. log.Fatalf("Error creating consumer group client: %v", err)
  72. }
  73. ctx, cancel := context.WithCancel(context.Background())
  74. wg := &sync.WaitGroup{}
  75. wg.Add(1)
  76. go func() {
  77. defer wg.Done()
  78. for {
  79. if err := client.Consume(ctx, k.topics, k); err != nil {
  80. // 当setup失败的时候,error会返回到这里
  81. log.Errorf("Error from consumer: %v", err)
  82. return
  83. }
  84. // check if context was cancelled, signaling that the consumer should stop
  85. if ctx.Err() != nil {
  86. log.Println(ctx.Err())
  87. return
  88. }
  89. k.ready = make(chan bool)
  90. }
  91. }()
  92. <-k.ready
  93. log.Infoln("Sarama consumer up and running!...")
  94. // 保证在系统退出时,通道里面的消息被消费
  95. return func() {
  96. log.Info("kafka close")
  97. cancel()
  98. wg.Wait()
  99. if err = client.Close(); err != nil {
  100. log.Errorf("Error closing client: %v", err)
  101. }
  102. }
  103. }
  104. // Setup is run at the beginning of a new session, before ConsumeClaim
  105. func (k *Kafka) Setup(session sarama.ConsumerGroupSession) error {
  106. log.Info("setup")
  107. session.ResetOffset("t2p4", 0, 13, "")
  108. log.Info(session.Claims())
  109. // Mark the consumer as ready
  110. close(k.ready)
  111. return nil
  112. }
  113. // Cleanup is run at the end of a session, once all ConsumeClaim goroutines have exited
  114. func (k *Kafka) Cleanup(sarama.ConsumerGroupSession) error {
  115. log.Info("cleanup")
  116. return nil
  117. }
  118. // ConsumeClaim must start a consumer loop of ConsumerGroupClaim's Messages().
  119. func (k *Kafka) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
  120. // NOTE:
  121. // Do not move the code below to a goroutine.
  122. // The `ConsumeClaim` itself is called within a goroutine, see:
  123. // https://github.com/Shopify/sarama/blob/master/consumer_group.go#L27-L29
  124. // 具体消费消息
  125. for message := range claim.Messages() {
  126. log.Infof("[topic:%s] [partiton:%d] [offset:%d] [value:%s] [time:%v]",
  127. message.Topic, message.Partition, message.Offset, string(message.Value), message.Timestamp)
  128. // 更新位移
  129. session.MarkMessage(message, "")
  130. }
  131. return nil
  132. }
  133. func main() {
  134. k := NewKafka()
  135. c := k.Connect()
  136. sigterm := make(chan os.Signal, 1)
  137. signal.Notify(sigterm, syscall.SIGINT, syscall.SIGTERM)
  138. select {
  139. case <-sigterm:
  140. log.Warnln("terminating: via signal")
  141. }
  142. c()
  143. }

sarama的消费者组分析、使用的更多相关文章

  1. 从零开始实现lmax-Disruptor队列(二)多消费者、消费者组间消费依赖原理解析

    MyDisruptor V2版本介绍 在v1版本的MyDisruptor实现单生产者.单消费者功能后.按照计划,v2版本的MyDisruptor需要支持多消费者和允许设置消费者组间的依赖关系. 由于该 ...

  2. Kafka消费者组再均衡问题

    在Kafka中,当有新消费者加入或者订阅的topic数发生变化时,会触发Rebalance(再均衡:在同一个消费者组当中,分区的所有权从一个消费者转移到另外一个消费者)机制,Rebalance顾名思义 ...

  3. Kafka消费者组静态成员(static consumer member)

    Kafka 2.3发布后官网的Consumer参数中增加了一个新的参数:group.instance.id.下面是这个参数的解释: A unique identifier of the consume ...

  4. 深入理解RocketMQ的消费者组、队列、Broker,Topic

    1.遇到的问题:上测试环境,上次描述的鸟问题又出现了,就是生产者发3条数据,我这边只能收到1条数据. 2.问题解决: (1)去控制台看我的消费者启动情况,貌似没什么问题 , (2)去测试服务器里看日志 ...

  5. kafka Poll轮询机制与消费者组的重平衡分区策略剖析

    注意本文采用最新版本进行Kafka的内核原理剖析,新版本每一个Consumer通过独立的线程,来管理多个Socket连接,即同时与多个broker通信实现消息的并行读取.这就是新版的技术革新.类似于L ...

  6. 详细解析kafka之 kafka消费者组与重平衡机制

    消费组组(Consumer group)可以说是kafka很有亮点的一个设计.传统的消息引擎处理模型主要有两种,队列模型,和发布-订阅模型. 队列模型:早期消息处理引擎就是按照队列模型设计的,所谓队列 ...

  7. 用R进行市场调查和消费者感知分析

    // // 问题到数据 理解问题 理解客户的问题:谁是客户(某航空公司)?交流,交流,交流! 问题要具体 某航空公司: 乘客体验如何?哪方面需要提高? 类别:比较.描述.聚类,判别还是回归 需要什么样 ...

  8. Kafka管理与监控——查看和重设消费者组位移

    kafka 0.11.0.0版本丰富了kafka-consumer-groups脚本的功能,用户可以直接使用该脚本很方便地为已有的consumer group重新设置位移. 前提必须consumer ...

  9. kafka 消费组功能验证以及消费者数据重复数据丢失问题说明 3

    原创声明:作者:Arnold.zhao 博客园地址:https://www.cnblogs.com/zh94 背景 上一篇文章记录了kafka的副本机制和容错功能的说明,本篇则主要在上一篇文章的基础上 ...

随机推荐

  1. 2018.7.31-2018.8.2记:关于maven

    maven的使用,用得好,则省力省事,但是用不好则会造成一堆莫名其妙的错误,maven在使用的时候,jar包下载异常终止尤为需要注意,很容易就终止了,并且会出现一些下载出空jar包的情况,即:jar包 ...

  2. jQuery中的效果(九):hide()、show()、slideUp()、slideDown()、slideToggle()、fadeOut()、fadeIn()、fadeTo()、animate等

    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"> <html> <hea ...

  3. 关于int和Integer缓存(一):以及设计构想(享元模式)

    关于Integer的值缓存:在介绍Integer的值缓存之前,我们需要了解的是,java中的包装类型,我们都知道java中有包装类型int                     Integer    ...

  4. 虚拟dom?diff算法?key?Vue原理的核心三问?打包教你搞定。

    为什么需要虚拟DOM 先介绍浏览器加载一个HTML文件需要做哪些事,帮助我们理解为什么我们需要虚拟DOM.webkit引擎的处理流程,如下图所示: 所有浏览器的引擎工作流程都差不多,如上图大致分5步: ...

  5. ubuntu apt-get Failed to fetch Temporary failure resolving 'security.ubuntu.com'

    发现是因为代理设置原因,导致无法上网,设置代理后问题解决. System Setting -> Network -> Network Proxy -> input IP+Port - ...

  6. K8S——Pod

    一.Pod概念 二.Pod存在的意义 三.Pod的实现机制 四.Pod镜像拉取策略 五.Pod资源限制 六.Pod重启机制 七.Pod的健康检查 八.Pod调度策略(创建Pod流程)

  7. CentOS管理firewalld防火墙

    1.查看防火墙某个端口是否开放 firewall-cmd --query-port=80/tcp 2.开放防火墙端口80 firewall-cmd --zone=public --add-port=8 ...

  8. 20210713考试-2021noip14

    T1 队长快跑 #include<bits/stdc++.h> using namespace std; const int N=1e6+5,INF=0x7fffffff; int n,a ...

  9. adb 常用命令大全(6)- 模拟按键输入

    语法格式 input [<source>] <command> [<arg>...] 物理键 # 电源键 adb shell input keyevent 26 # ...

  10. DPDK应用示例指南简介(汇总)

    DPDK应用示例指南简介 <DPDK示例>系列文章主要是学习.记录.翻译DPDK官方示例文档.为了更好地理解和学习DPDK, 特通过对源码中的经典示例进行整理,供大家学习.交流和讨论. A ...