工作中需要将原先的消息队列替换成kafka,于是接触了基于go实现的sarama,又因为sarama不支持consumer group,于是又使用了sarama cluster,同时又希望尽量保证消费一次的语义,说到这个exactly once,sarama从去年就立了issue要支持exactly once,结果到现在还没支持(https://github.com/Shopify/sarama/issues/901)。
于是就自己造了个简单的轮子,把sarama和sarama cluster封装到一起,同时实现了保证消费一次的语义,我给它起名为kago。
先附上kago的依赖,需要先进行安装:
go get github.com/Shopify/sarama
go get github.com/bsm/sarama-cluster
然后便可以安装kago:
go get go get github.com/JeffreyDing11223/kago
asyncProducer.go
负责初始化异步producer单个实例或实例group,以及发送消息,接受错误信息等。
syncProducer.go
负责初始化同步producer单个实例或实例group,同步发送消息等。
consumer.go
负责初始化consumer group单个成员或多个成员,以及初始化partition consumer,还有标记offset,提交offset,获取所有topics,以及获取某个topic下所有分区等等。
message.go
各类消息体的定义,基本都沿用了sarama和sarama cluster的消息类型。
offsetFile.go
初始化,修改,保存offset文件的相关操作。
offsetManager.go
offsetManager的初始化,标记offset等,这个主要结合partition consumer来使用。
config.go
kafka 生产者和消费者以及其他的各项配置。
util.go
各类功能函数。
这里有小伙伴一定有疑问了,既然已经有标记offset和提交offset了,为什么还要offsetFile.go
去操作文件来保存offset呢,这就是我上面说的尽可能保证消费一次的语义,试想一下,现在我拿到一条消息,各种加工处理,消费完了,当我要提交offset给kafka的时候,我的客户端出现网络问题了或者kafka server出了问题,导致offset提交失败。也就是说,下次继续消费的时候,就会继续从这条消息开始消费,那就相当于是重复消费了这条消息。于是我在处理消息和提交offset的中间,加了一步,就是文件保存offset,并且供使用者自己选择,继续消费是按照kafka server保存的offset来,还是按照本地文件来,或者取两者最大的,这些选项可以在config.go
中看到。
offsetFile.go
的实现也很简单,就是封装一把锁到os.file
中,并结合sync.map
来支持并发读写,文件内部统一使用json格式,以topic为单位来分类文件。具体可以看源码。
还有具体关于exactly once语义的内容,可以参考我之前发表在博客中的文章 “kafka消息交付语义的分析https://blog.csdn.net/jeffrey11223/article/details/80775080“ 。
附上使用例子:
//ayncProducer
import (
...
"github.com/JeffreyDing11223/kago"
...
)
config:=kago.NewConfig()
config.Producer.Return.Successes = true
config.Producer.Return.Errors = true
produ,_:=kago.InitManualRetryAsyncProducer([]string{"127.0.0.1:9092"}, config)
defer produ.Close()
go func(p *kago.AsyncProducer) {
for{
select {
case suc:=<-p.Successes():
bytes,_:=suc.Value.Encode()
value:=string(bytes)
fmt.Println("offsetCfg:", suc.Offset, " partitions:", suc.Partition," metadata:",suc.Metadata," value:",value)
case fail := <-p.Errors():
fmt.Println("err: ", fail.Err)
}
}
}(produ)
var value string
for i:=0;;i++ {
time11:=time.Now()
value = "this is a message 0805 "+time11.Format("15:04:05")
//发送的消息,主题,key
msg := &kago.ProducerMessage{
Topic: "0805_test",
}
//将字符串转化为字节数组
msg.Value = sarama.ByteEncoder(value)
//使用通道发送
produ.Send() <- msg
time.Sleep(500*time.Millisecond)
}
//consumerGroup
config:=kago.NewConfig()
config.Consumer.Return.Errors=true
config.Group.Return.Notifications =true
config.Consumer.Offsets.CommitInterval=1*time.Second
consumer,err:=kago.InitOneConsumerOfGroup([]string{"127.0.0.1:9092"}, "0805_test","cg1",config)
if err!=nil{
log.Println(err)
return
}
defer consumer.Close()
kago.InitOffsetFile() //初始化offset文件,全局执行一次即可
go func() {
for err := range consumer.Errors() {
log.Printf("Error: %s\n", err.Error())
}
}()
go func() {
for ntf := range consumer.Notifications() {
log.Printf("Rebalanced: %+v\n", ntf)
}
}()
for{
select {
case msg, ok := <-consumer.Recv():
if ok {
fmt.Fprintf(os.Stdout, ": %s/%d/%d\t%s\t%s\n", msg.Topic, msg.Partition, msg.Offset, msg.Key, msg.Value)
consumer.MarkOffset(msg.Topic,msg.Partition,msg.Offset,"cg1",true) // 提交offset,最后一个参数为true时,会将offset保存到文件中
}
}
}
//partition Consumer
config:=kago.NewConfig()
config.Consumer.Return.Errors=true
config.Group.Return.Notifications =true
config.Consumer.Offsets.CommitInterval=1*time.Second
config.OffsetLocalOrServer=0 //选项配置为优先读offset文件
kago.InitOffsetFile() //初始化offset文件,全局执行一次即可
pconsumer,err:=kago.InitPartitionConsumer([]string{"127.0.0.1:9092"}, "0805_test",0,"cg1",config) //会根据config.OffsetLocalOrServe来让pconsumer从指定的offset开始消费
if err!=nil{
log.Println(err)
return
}
defer pconsumer.Close()
pOffsetManager,err2:=kago.InitPartitionOffsetManager([]string{"127.0.0.1:9092"}, "0805_test","cg1",0,config)
if err2 !=nil{
fmt.Println(err2)
return
}
defer pOffsetManager.Close()
go func() {
for err := range pconsumer.Errors() {
fmt.Printf("Error: %s\n", err.Error())
}
}()
for{
msg := <-pconsumer.Recv()
fmt.Printf("Consumed message offsetCfg %d\n message:%s", msg.Offset,string(msg.Value))
pOffsetManager.MarkOffset("0805_test",0,msg.Offset,"cg1",true)
}