用go实现的kafka客户端,基于sarama和sarama-cluster

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jeffrey11223/article/details/81436747

工作中需要将原先的消息队列替换成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

先看一下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)
    }

猜你喜欢

转载自blog.csdn.net/jeffrey11223/article/details/81436747