消费者和消费组

  1. 消费者:消费kafka消息的实体,可以是一个进程,也可以是一个线程
  2. 消费组:消费组是一个逻辑概念,每个消费者都隶属于一个消费组;
  3. 一个消费组消费一个topic的所有消息;不同的消费组之间消费消息互补影响
  4. 一个topic存在多个分区,每个分区只能被一个消费组中的某一个消费者消费

消费者数量和topic分区数量的关系如下图,存在三种情况:

kafka 只消费一个分区 kafka一个分区多个消费者_多线程

  1. 消费者数量小于分区数量: 每个消费者至少消费一个分区,部分消费者消费多个分区
  2. 消费者数量等于分区数量:每个消费者消费一个分区
  3. 消费者数量多于分区数量:部分消费者消费一个分区,多余的消费者不消费消息

所以,配置消费者数量时应该注意,过多的消费者对消费速度没有意义

消费者实例构建

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位移的逻辑,但是位移提交就不够灵活

手动位移提交

手动提交位移可以让开发者灵活控制提交位移的时机和方式,不同的时机和方式会带来不同的性能消耗,也有各自的场景。几种常见的位移提交方式:

  1. 每次获取消息后即提交位移
  • 简单粗暴,容易造成消息丢失
  1. 每次获取消息,并全部处理完后再统一提交位移
  • 简单粗暴,容易造成消息重复
  1. 如果一个消费者消费了多个分区,可以划分分区粒度,每个分区的消息消费完后提交该分区offset
  2. 每消费一条消息即提交一次位移
  • 简单粗暴,同步提交位移方式会导致性能低下,异步提交位移的方式会导致消息重复,如异步提交x失败需要重试,此时异步提交x + 5已经成功,再次提交x成功会导致消息重复

指定位移消费

消费者位移确定

消费者的位移信息,会存储在kafka的默认主题__consumer_offsets的主题中,当从位移主题中找不到该消费位移(如新消费组建立、消费组订阅新topic、位移主题中关于该消费者的信息过期被删除等)时,会根据参数audo.offset.reset的值决定消费者从分区的哪里开始消费

  1. audo.offset.reset = latest:表示从分区末尾开始消费消息
  2. audo.offset.reset = earlist: 表示从分区起始处开始消费;起始未必是0,比如分区年久,老数据已被清理
  3. audo.offset.reset = none: 此时会抛错NoOffsetForPartitionException

指定offset开始消费

kafka中提供了seek方法,可以指定分区 + 位移消费,这在某些场景下很有用;比如在某个时间点上线的服务异常,消息处理并非预期,此时可能需要从指定offset重新消费数据

多线程并行消费

消费者本身是非线程安全的,但是在实际的应用场景中,我们可以通过多线程处理的方式提高消费速率,提高系统吞吐量

一般有以下几种多线程方式:

  1. 消费线程,即每个消费者独立为一个线程

这种多线程的方式受限于分区的个数,因为当消费者数大于分区数,会有部分消费者空闲,故而这种多线程方式有上限

  1. 多消费线程同时消费同一分区

可以通过assignseek,将同一分区的位移分段,让多个消费线程同时处理;这种方式其实打破了一个分区只能被一个消费者消费的限制,但这种情况会使得编码困难,且提交offset混乱,易出错,不推荐

  1. 多线程消费同一个消费线程的消息

即对于一个消费线程拿到的消息,可以在处理的代码中再做异步消费

重要的消费者参数

  1. fetch.min.bytes: 从kafka中一次拉取的最小数据量。数据量打不到这个值的时候,poll()阻塞
  2. fetch.max.bytes: 最大数据量;
  3. fetch.max.wait.ms: 与fetch.min.bytes相关,如果kafka消息大小小于该值时,最长等待时间
  4. max.poll.records: 一次拉取最大消息数
  5. connection.max.idle.ms: 多久之后关闭空闲的连接
  6. receive.buffer.bytes: socket接收缓冲区(SO_RECBUF)的大小,默认64KB