消费者和消费组
- 消费者:消费kafka消息的实体,可以是一个进程,也可以是一个线程
- 消费组:消费组是一个逻辑概念,每个消费者都隶属于一个消费组;
- 一个消费组消费一个topic的所有消息;不同的消费组之间消费消息互补影响
- 一个topic存在多个分区,每个分区只能被一个消费组中的某一个消费者消费
消费者数量和topic分区数量的关系如下图,存在三种情况:
- 消费者数量小于分区数量: 每个消费者至少消费一个分区,部分消费者消费多个分区
- 消费者数量等于分区数量:每个消费者消费一个分区
- 消费者数量多于分区数量:部分消费者消费一个分区,多余的消费者不消费消息
所以,配置消费者数量时应该注意,过多的消费者对消费速度没有意义
消费者实例构建
package kafka
import (
"fmt"
"github.com/Shopify/sarama"
"sync"
"time"
)
var (
consumerConfig = initConsumerConfig()
consumer = initConsumer()
wg sync.WaitGroup
)
func initConsumerConfig() (config *sarama.Config) {
config = sarama.NewConfig()
return
}
func initConsumer() (consumer sarama.Consumer) {
consumer, err := sarama.NewConsumer([]string{brokerList}, consumerConfig)
if err != nil {
panic(err)
}
return
}
func partitionConsumer(partition int32) {
defer wg.Done()
// 获取一个分区消费者实例
pc, err := consumer.ConsumePartition(topic, partition, sarama.OffsetOldest)
if err != nil {
panic(err)
}
defer pc.AsyncClose()
// 获取该consumer的消息通道
msgChan := pc.Messages()
ticker := time.NewTicker(2 * time.Second)
// 起一个无限循环从consumer中获取消息,2s拿不到消息后退出
for {
select {
case msg := <-msgChan:
fmt.Printf("Partition: %d Offset: %d Key: %v Value: %v\n", msg.Partition, msg.Offset, string(msg.Key), string(msg.Value))
case <-ticker.C:
fmt.Printf("Partition: %d exist..................\n", partition)
// 这里用break不会退出,为什么???
return
}
}
}
func testConsumer() {
// get all partitions
partitionList, err := consumer.Partitions(topic)
if err != nil {
panic(err)
}
// fmt.Println(partitionList) // => [0, 1, 2]
for _, partition := range partitionList {
wg.Add(1)
go partitionConsumer(partition)
}
wg.Wait()
}
func TestConsumer() {
testConsumer()
}
位移提交
重复消费和消息丢失
kafka在消费消息,由于提交位移的时机问题,会产生重复消费或者消息丢失的现象。假如某消费者当前消费位置为x,并获取了5条新消息,offset为x+1 -> x+5
- 重复消费:消费者处理完这5条消息再提交offset,若消费到第x+3条消息时消费者挂掉,重启后将继续从x+1条消息开始消费,产生消费重复
- 消息丢失: 消费者拿到这5条消息后即提交offset,若消费到第x+3条消息时消费者挂掉,重启后将从x + 6条消息开始消费,产生消息丢失
位移提交的时机
自动位移提交
当配置enable.auto.commit=true
时,消费者会自动提交位移,此时另一个配置参数 auto.commit.interval.ms
,表示自动提交位移的时间间隔,默认5s
当每次开始调用poll方法(获取新消息)时,提交上次poll返回的所有消息
consumer每隔5s提交一次位移;
这两个提交策略同时进行???每5s提交的位移是多少呢???待定。。。
自动提交容易造成重复消费,即未到下一次提交时机消费者异常退出;同时也可能出现消息丢失,在消费端使用缓存缓存消息,并异步真实处理的情况
自动提交位移的方式会使编码简单,不用commit位移的逻辑,但是位移提交就不够灵活
手动位移提交
手动提交位移可以让开发者灵活控制提交位移的时机和方式,不同的时机和方式会带来不同的性能消耗,也有各自的场景。几种常见的位移提交方式:
- 每次获取消息后即提交位移
- 简单粗暴,容易造成消息丢失
- 每次获取消息,并全部处理完后再统一提交位移
- 简单粗暴,容易造成消息重复
- 如果一个消费者消费了多个分区,可以划分分区粒度,每个分区的消息消费完后提交该分区offset
- 每消费一条消息即提交一次位移
- 简单粗暴,同步提交位移方式会导致性能低下,异步提交位移的方式会导致消息重复,如异步提交x失败需要重试,此时异步提交x + 5已经成功,再次提交x成功会导致消息重复
指定位移消费
消费者位移确定
消费者的位移信息,会存储在kafka的默认主题__consumer_offsets
的主题中,当从位移主题中找不到该消费位移(如新消费组建立、消费组订阅新topic、位移主题中关于该消费者的信息过期被删除等)时,会根据参数audo.offset.reset
的值决定消费者从分区的哪里开始消费
-
audo.offset.reset = latest
:表示从分区末尾开始消费消息 -
audo.offset.reset = earlist
: 表示从分区起始处开始消费;起始未必是0,比如分区年久,老数据已被清理 -
audo.offset.reset = none
: 此时会抛错NoOffsetForPartitionException
指定offset开始消费
kafka中提供了seek
方法,可以指定分区 + 位移消费,这在某些场景下很有用;比如在某个时间点上线的服务异常,消息处理并非预期,此时可能需要从指定offset重新消费数据
多线程并行消费
消费者本身是非线程安全的,但是在实际的应用场景中,我们可以通过多线程处理的方式提高消费速率,提高系统吞吐量
一般有以下几种多线程方式:
- 消费线程,即每个消费者独立为一个线程
这种多线程的方式受限于分区的个数,因为当消费者数大于分区数,会有部分消费者空闲,故而这种多线程方式有上限
- 多消费线程同时消费同一分区
可以通过
assign
和seek
,将同一分区的位移分段,让多个消费线程同时处理;这种方式其实打破了一个分区只能被一个消费者消费
的限制,但这种情况会使得编码困难,且提交offset混乱,易出错,不推荐
- 多线程消费同一个消费线程的消息
即对于一个消费线程拿到的消息,可以在处理的代码中再做异步消费
重要的消费者参数
-
fetch.min.bytes
: 从kafka中一次拉取的最小数据量。数据量打不到这个值的时候,poll()阻塞 -
fetch.max.bytes
: 最大数据量; -
fetch.max.wait.ms
: 与fetch.min.bytes
相关,如果kafka消息大小小于该值时,最长等待时间 -
max.poll.records
: 一次拉取最大消息数 -
connection.max.idle.ms
: 多久之后关闭空闲的连接 -
receive.buffer.bytes
: socket接收缓冲区(SO_RECBUF
)的大小,默认64KB