github
中文文档
docker
# docker-compose.yml 因为consumer-group只能是集群的方式
version: '3.3'
services:
zookeeper:
image: wurstmeister/zookeeper
container_name: zookeeper
ports:
- 2181:2181
volumes:
- /d/dockerFile/kafka_cluster/zookeeper/data:/data
- /d/dockerFile/kafka_cluster/zookeeper/datalog:/datalog
- /d/dockerFile/kafka_cluster/zookeeper/logs:/logs
restart: always
kafka1:
image: wurstmeister/kafka
depends_on:
- zookeeper
container_name: kafka1
ports:
- 9092:9092
environment:
KAFKA_BROKER_ID: 1
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka1:9092
KAFKA_LISTENERS: PLAINTEXT://kafka1:9092
KAFKA_LOG_DIRS: /data/kafka-data
KAFKA_LOG_RETENTION_HOURS: 24
volumes:
- /d/dockerFile/kafka_cluster/kafka1/data:/data/kafka-data
restart: unless-stopped
kafka2:
image: wurstmeister/kafka
depends_on:
- zookeeper
container_name: kafka2
ports:
- 9093:9093
environment:
KAFKA_BROKER_ID: 2
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka2:9093
KAFKA_LISTENERS: PLAINTEXT://kafka2:9093
KAFKA_LOG_DIRS: /data/kafka-data
KAFKA_LOG_RETENTION_HOURS: 24
volumes:
- /d/dockerFile/kafka_cluster/kafka2/data:/data/kafka-data
restart: unless-stopped
kafka3:
image: wurstmeister/kafka
depends_on:
- zookeeper
container_name: kafka3
ports:
- 9094:9094
environment:
KAFKA_BROKER_ID: 3
KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka3:9094
KAFKA_LISTENERS: PLAINTEXT://kafka3:9094
KAFKA_LOG_DIRS: /data/kafka-data
KAFKA_LOG_RETENTION_HOURS: 24
volumes:
- /d/dockerFile/kafka_cluster/kafka3/data:/data/kafka-data
restart: unless-stopped
# create topic
/opt/bitnami/kafka/bin/kafka-topics.sh --create --bootstrap-server kafka_container:9092 --replication-factor 1 --partitions 1 --topic test
# list topic
/opt/bitnami/kafka/bin/kafka-topics.sh --bootstrap-server kafka_container:9092 --list
# delete topic
/opt/bitnami/kafka/bin/kafka-topics.sh --delete --bootstrap-server kafka_container:9092 --topic test
/opt/bitnami/kafka/bin/kafka-console-producer.sh --bootstrap-server kafka_container:9092 --topic test
/opt/bitnami/kafka/bin/kafka-console-consumer.sh --bootstrap-server kafka_container:9092 --topic test --from-beginning
集群中的每个kafka服务就是一个broker
topic是一类待消费信息的集合,是逻辑上的划分。例如订单消息的集合
每个topic可以划分为多个分区(物理划分,对应在磁盘上的不同文件夹讷),生成者推送的信息会随机分配到此topic下的不通分区。每个消息在被添加到分区时,都会被分配一个 offset,它是消息在此分区中的唯一编号,Kafka 通过 offset 保证消息在分区内的顺序,offset 的顺序不跨分区,即 Kafka 只保证在同一个分区内的消息是有序的。
我们从创建 ProducerRecord 对象开始, ProducerRecord 对象需要包含目标主题和要发
的内容。我们还可以指定键或分区。在发送 Producer ecord 对象时,生产者要先把键和
值对象序列化成字节数组,这样它们才能够在网络上传输
接下来,数据被传给分区器。如果之前在 Produc rR cord 对象里指定了分区,那么分区器
就不会再做任何事情,直接把指定的分区返回。如果没有指定分区 ,那么分区器会根据
Prod uc巳rRecord 对象的键来选择 个分区。选好分区以后 ,生产者就知道该往哪个主题和
分区发送这条记录了。紧接着,这条记录被添加到 个记录批次里,这个批次里的所有消
息会被发送到相同的主题和分区上。有 个独立的线程负责把这些记录批次发送到相应的
broker 上。
服务器在收到这些消息时会返回一个响应。如果消息成功写入 Kafka ,就返回
RecordMetaData 对象,它包含了主题和分区信息,以及记录在分区里的偏移量。如果写入
失败, lj 会返回 个错误。生产者在收到错误之后会尝试重新发送消息,几次之后如果还
是失败, 就返回错误信息。
发送消息主要有以下 种方式。
我们把消息发送给服务器,但井不关 它是否正常到达。大多数情况下,消息会正常到
达,因为 fk 是高可用的,而且生产者会自动尝试重发。不过,使用这种方式有时候
也会丢失 些消息。
使用 send () 方怯发送消息 它会返回 Future 对象,调用 get () 方法进行等待
就可以知道悄息是否发送成功。
我们调用 send () 方怯,并指定 个回调函数, 务器在返回响应时调用该函数。
配置文件/path/to/config/producer.properties
Avro
消费者有两种获取数据的方式 pull, push
pull方式:消费者主动向broker拉取数据(一般都是这种)
push方式:broker主动推消息给消费者
分区的所有权从 个消费者转移到另 个消费者,这样的行为被称为再均衡rebalance。再均衡非常重要, 它为 肖费者群组带来了高可用性和伸缩性(我们可以放心地添加或移除梢费者),不过在正常情况下,我们并不希望发生这样的行为。在再均衡期间,消费者无法读取消息,造成整个群组 小段时间的不可用。另外,当分区被重新分配给另 个消费者时,消费者当前的读取状态会丢失,它有可能还需要去刷新缓存,在它重新恢复状态之前会拖慢应用程序。
创建消费者的3个必要属性:
bootstrap.servers:集群连接字符串,broker列表,host:port
key.deserializer:键的反序列化方法
value.deserializer:值得反序列化方法
group.id:如果是消费者组是必要属性,指定消费组的编号
问题:订阅多个主题时,如何管理offset
消息轮询是消费者API的核心,通过一个简单的轮询向服务器请求数据。一旦消费者订阅了主题,轮询就会处理所有的细节,包括:群组协调,分区再平衡,发送心跳和获取数据
配置文件/path/to/config/consumer.properties
https://blog.csdn.net/qq_31617409/article/details/82317267
消费者可以使用kafka来追踪消息在分区的位置(offset),更新分区当前位置的操作叫做提交
消费者往一个叫做_consumer_offset
的特殊主题发送消息,消息里包含了每个分区的偏移量。当消费者崩溃或者有新的消费者加入时,会发生rebalance,为了能够继续之前的工作,消费者要读取每个分区最后一次提交的偏移量,然后从这个偏移量继续处理
通过上图会发现处理偏移量的方式对客户端有很多影响,所以kafka consumer API提供了很多方式来提交偏移量
设置enable.auto.commit = true
, 每隔5s poll()轮询会自动提交接收到的最大偏移量。
设置auto.commit.interval.ms
控制自动提交时间、
自动提交并不知道哪些消息被处理,哪些处理成功
消费者 API 提供了另一种提交偏移量的方式 开发者可 要的时候提交当前偏移量,而不是基于时间间隔。
设置enable.auto.commit = false
让应用程序决定 时提交 移量。使用 commit.Sync()提交偏移量最简单 最可靠。这个 PI 会提交由 poll () 方能返回 最新偏移量,提交成功后马上返回,如果提交失败就抛出异常。
手动提交有一个不足之处,在 broker 对提交请求作出回应之前,应用程序会 直阻塞,这样会限制应用程序的吞吐量。可以通过降低提交频率来提升吞吐盆,但如果发生了均衡, 会增 重复消息的数量。
这个时候可以使用异步提交 PI 。我 只管发送提交请求,无需等待 broker 的响应。
一般情况下,针对偶尔出现的提交失败,不进行重试不会有太大问题,因为如果提交失败是因为临时问题导致的,那么后续的提交总会有成功的。但如果这是发生在关闭消费者或rebalance前的最后 次提交,就要确保能够提交成功。
package main
import (
"fmt"
"log"
"sync"
"time"
"github.com/Shopify/sarama"
)
func main() {
//flag.Parse()
//Produce()
//Consume()
//GroupConsume()
OffsetManager("first", 0) // 如果需要重复消费,需要把resetFlag设置成1,重置offset
}
func OffsetManager(topic string, resetFlag int) {
config := sarama.NewConfig()
// 配置开启自动提交 offset,这样 samara 库会定时帮我们把最新的 offset 信息提交给 kafka
config.Consumer.Offsets.AutoCommit.Enable = true // 开启自动 commit offset
config.Consumer.Offsets.AutoCommit.Interval = 5 * time.Second // 自动 commit时间间隔
config.Consumer.Offsets.Initial = sarama.OffsetOldest // 从最老的未提交的消息开始
client, err := sarama.NewClient([]string{"kafka1:9092"}, config)
if err != nil {
log.Fatal("NewClient err: ", err)
}
defer client.Close()
// offsetManager 用于管理每个 consumerGroup的 offset
// 根据 groupID 来区分不同的 consumer,注意: 每次提交的 offset 信息也是和 groupID 关联的
offsetManager, _ := sarama.NewOffsetManagerFromClient("myGroupID", client) // 偏移量管理器
defer offsetManager.Close()
var wg sync.WaitGroup
for i:=0;i<3;i++ {
wg.Add(i)
go func(i int32) {
// 每个分区的 offset 也是分别管理的,demo 这里使用 0 分区,因为该 topic 只有 1 个分区
partitionOffsetManager, _ := offsetManager.ManagePartition(topic, i) // 对应分区的偏移量管理器
defer partitionOffsetManager.Close()
// 重置offset
if resetFlag == 1 {
partitionOffsetManager.ResetOffset(sarama.OffsetNewest, "")
nextOffset, _ := partitionOffsetManager.NextOffset() // 取得下一消息的偏移量作为本次消费的起点
fmt.Printf("reset result: partition: %d, offset: %d \n", i, nextOffset)
return
}
// defer 在程序结束后在 commit 一次,防止自动提交间隔之间的信息被丢掉
defer offsetManager.Commit()
consumer, _ := sarama.NewConsumerFromClient(client)
// 根据 kafka 中记录的上次消费的 offset 开始+1的位置接着消费
nextOffset, _ := partitionOffsetManager.NextOffset() // 取得下一消息的偏移量作为本次消费的起点
fmt.Printf("partition: %d, offset: %d \n", i, nextOffset)
pc, _ := consumer.ConsumePartition(topic, i, nextOffset)
defer pc.Close()
for message := range pc.Messages() {
value := string(message.Value)
log.Printf("[Consumer] partitionid: %d; offset:%d, value: %s\n", message.Partition, message.Offset, value)
// 每次消费后都更新一次 offset,这里更新的只是程序内存中的值,需要 commit 之后才能提交到 kafka
partitionOffsetManager.MarkOffset(message.Offset+1, "modified metadata") // MarkOffset 更新最后消费的 offset
//if message.Offset < 5 {
// offsetManager.Commit()
//}
time.Sleep(5 * time.Second)
}
wg.Done()
}(int32(i))
}
wg.Wait()
}
从上图可以看出,kafka记录了 每个topic的分区的 未提交或未生成的 offset 并且对应了 消费者组。这样消费者就可以从未提交的消息数据开始,并且如果有多个消费者组消费一个topic也能互不影响