Golang中如何正确的使用sarama包操作Kafka?
Golang中如何正确的使用sarama包操作Kafka?
一、背景

- 重复消费的问题。
- 乱序的问题。
二、Kafka消息丢失问题描述
三、生产端丢消息问题解决
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // -1
ack参数有如下取值:
const (
// NoResponse doesn't send any response, the TCP ACK is all you get.
NoResponse RequiredAcks = 0
// WaitForLocal waits for only the local commit to succeed before responding.
WaitForLocal RequiredAcks = 1
// WaitForAll waits for all in-sync replicas to commit before responding.
// The minimum number of in-sync replicas is configured on the broker via
// the `min.insync.replicas` configuration key.
WaitForAll RequiredAcks = -1
)
四、消费端丢消息问题
自动提交模式下的丢消息问题
// NewConfig returns a new configuration instance with sane defaults.
func NewConfig() *Config {
// …
c.Consumer.Offsets.AutoCommit.Enable = true. // 自动提交
c.Consumer.Offsets.AutoCommit.Interval = 1 * time.Second // 间隔
c.Consumer.Offsets.Initial = OffsetNewest
c.Consumer.Offsets.Retry.Max = 3
// ...
}
这里的自动提交,是基于被标记过的消息(sess.MarkMessage(msg, “"))
type exampleConsumerGroupHandler struct{}
func (exampleConsumerGroupHandler) Setup(_ ConsumerGroupSession) error { return nil }
func (exampleConsumerGroupHandler) Cleanup(_ ConsumerGroupSession) error { return nil }
func (h exampleConsumerGroupHandler) ConsumeClaim(sess ConsumerGroupSession, claim ConsumerGroupClaim) error {
for msg := range claim.Messages() {
fmt.Printf("Message topic:%q partition:%d offset:%d\n", msg.Topic, msg.Partition, msg.Offset)
// 标记消息已处理,sarama会自动提交
sess.MarkMessage(msg, "")
}
return nil
}
如果不调用sess.MarkMessage(msg, “"),即使启用了自动提交也没有效果,下次启动消费者会从上一次的Offset重新消费,我们不妨注释掉sess.MarkMessage(msg, “"),然后打开Offset Explorer查看:

func (h msgConsumerGroup) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
// 插入mysql
insertToMysql(msg)
// 正确:插入mysql成功后程序崩溃,下一次顶多重复消费一次,而不是因为Offset超前,导致应用层消息丢失了
sess.MarkMessage(msg, “")
}
return nil
}
func (h msgConsumerGroup) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
// 错误1:不能先标记,再插入mysql,可能标记的时候刚好自动提交Offset,但mysql插入失败了,导致下一次这个消息不会被消费,造成丢失
// 错误2:干脆忘记调用sess.MarkMessage(msg, “"),导致重复消费
sess.MarkMessage(msg, “")
// 插入mysql
insertToMysql(msg)
}
return nil
}
sarama手动提交模式
consumerConfig := sarama.NewConfig()
consumerConfig.Version = sarama.V2_8_0_0
consumerConfig.Consumer.Return.Errors = false
consumerConfig.Consumer.Offsets.AutoCommit.Enable = false // 禁用自动提交,改为手动
consumerConfig.Consumer.Offsets.Initial = sarama.OffsetNewest
func (h msgConsumerGroup) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
fmt.Printf("%s Message topic:%q partition:%d offset:%d value:%s\n", h.name, msg.Topic, msg.Partition, msg.Offset, string(msg.Value))
// 插入mysql
insertToMysql(msg)
// 手动提交模式下,也需要先进行标记
sess.MarkMessage(msg, "")
consumerCount++
if consumerCount%3 == 0 {
// 手动提交,不能频繁调用,耗时9ms左右,macOS i7 16GB
t1 := time.Now().Nanosecond()
sess.Commit()
t2 := time.Now().Nanosecond()
fmt.Println("commit cost:", (t2-t1)/(1000*1000), "ms")
}
}
return nil
}
五、Kafka消息顺序问题
msg := &sarama.ProducerMessage{
Topic: “msgc2s",
Value: sarama.StringEncoder(“hello”),
Partition: toUserId % 10,
}
partition, offset, err := producer.SendMessage(msg)
生产消息的时候,除了Topic和Value,我们可以通过手动指定partition,比如总共有10个分区,我们根据用户ID取余,这样发给同一个用户的消息,每次都到1个partition里面去了,消费者写入mysql中的时候,自然也是有序的。

p.config.Producer.Partitioner = sarama.NewHashPartitioner
然后,在生成消息之前,设置消息的Key值:
msg := &sarama.ProducerMessage{
Topic: "testAutoSyncOffset",
Value: sarama.StringEncoder("hello"),
Key: sarama.StringEncoder(strconv.Itoa(RecvID)),
}
4.扩展知识:多线程情况下一个partition的乱序处理


六、重复消费和消息幂等
- 如果是存在redis中不需要持久化的数据,比如string类型,set具有天然的幂等性,无需处理。
- 插入mysql之前,进行一次query操作,针对每个客户端发的消息,我们为它生成一个唯一的ID(比如GUID),或者直接把消息的ID设置为唯一索引。
七、完整代码实例
type msgConsumerGroup struct{}
func (msgConsumerGroup) Setup(_ sarama.ConsumerGroupSession) error { return nil }
func (msgConsumerGroup) Cleanup(_ sarama.ConsumerGroupSession) error { return nil }
func (h msgConsumerGroup) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
for msg := range claim.Messages() {
fmt.Printf("%s Message topic:%q partition:%d offset:%d value:%s\n", h.name, msg.Topic, msg.Partition, msg.Offset, string(msg.Value))
// 查mysql去重
if check(msg) {
// 插入mysql
insertToMysql()
}
// 标记,sarama会自动进行提交,默认间隔1秒
sess.MarkMessage(msg, "")
}
return nil
}
func main(){
consumerConfig := sarama.NewConfig()
consumerConfig.Version = sarama.V2_8_0_0 // specify appropriate version
consumerConfig.Consumer.Return.Errors = false
//consumerConfig.Consumer.Offsets.AutoCommit.Enable = true // 禁用自动提交,改为手动
//consumerConfig.Consumer.Offsets.AutoCommit.Interval = time.Second * 1 // 测试3秒自动提交
consumerConfig.Consumer.Offsets.Initial = sarama.OffsetNewest
cGroup, err := sarama.NewConsumerGroup([]string{"10.0.56.153:9092", "10.0.56.153:9093", "10.0.56.153:9094"},"testgroup", consumerConfig)
if err != nil {
panic(err)
}
for {
err := cGroup.Consume(context.Background(), []string{"testAutoSyncOffset"}, consumerGroup)
if err != nil {
fmt.Println(err.Error())
break
}
}
_ = cGroup.Close()
}
func main(){
config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll // 等待所有follower都回复ack,确保Kafka不会丢消息
config.Producer.Return.Successes = true
config.Producer.Partitioner = sarama.NewHashPartitioner // 对Key进行Hash,同样的Key每次都落到一个分区,这样消息是有序的
// 使用同步producer,异步模式下有更高的性能,但是处理更复杂,这里建议先从简单的入手
producer, err := sarama.NewSyncProducer([]string{"10.0.56.153:9092"}, config)
defer func() {
_ = producer.Close()
}()
if err != nil {
panic(err.Error())
}
msgCount := 4
// 模拟4个消息
for i := 0; i < msgCount; i++ {
rand.Seed(int64(time.Now().Nanosecond()))
msg := &sarama.ProducerMessage{
Topic: "testAutoSyncOffset",
Value: sarama.StringEncoder("hello+" + strconv.Itoa(rand.Int())),
Key: sarama.StringEncoder("BBB”),
}
t1 := time.Now().Nanosecond()
partition, offset, err := producer.SendMessage(msg)
t2 := time.Now().Nanosecond()
if err == nil {
fmt.Println("produce success, partition:", partition, ",offset:", offset, ",cost:", (t2-t1)/(1000*1000), " ms")
} else {
fmt.Println(err.Error())
}
}
}
八、参考
- Kafka 的数据丢失和重复消费 https://zhuanlan.zhihu.com/p/54287819
- kafka什么时候会丢消息
- CAP 定理的含义 https://www.ruanyifeng.com/blog/2018/07/cap.html
- Kafka入门(3):Sarama生产者是如何工作的 https://www.cnblogs.com/hongjijun/p/13584373.html
- 超好用的 Kafka 客户端管理工具 Offset Explorer http://www.ibloger.net/article/3497.html
- 查看集群中kafka的Version(版本) https://blog.csdn.net/Damonhaus/article/details/54310868
- Kafka如何保证消息的顺序性 https://blog.csdn.net/qianshangding0708/article/details/103360193
Golang中如何正确的使用sarama包操作Kafka?的更多相关文章
- 在Golang中如何正确地使用database/sql包访问数据库
本文记录了我在实际工作中关于数据库操作上一些小经验,也是新手入门golang时我认为一定会碰到问题,没有什么高大上的东西,所以希望能抛砖引玉,也算是对这个问题的一次总结. 其实我也是一个新手,机缘巧合 ...
- golang中文件以及文件夹路径相关操作
获取目录中所有文件使用包: io/ioutil 使用方法: ioutil.ReadDir 读取目录 dirmane 中的所有目录和文件(不包括子目录) 返回读取到的文件的信息列表和读取过程中遇到的任何 ...
- android中正确导入第三方jar包
android中正确导入第三方jar包 andriod中如果引入jar包的方式不对就会出现一些奇怪的错误. 工作的时候恰好有一个jar包需要调用,结果用了很长时间才解决出现的bug. 刚开始是这样引用 ...
- golang中的reflect包用法
最近在写一个自动生成api文档的功能,用到了reflect包来给结构体赋值,给空数组新增一个元素,这样只要定义一个input结构体和一个output的结构体,并填写一些相关tag信息,就能使用程序来生 ...
- golang 中 sync包的 WaitGroup
golang 中的 sync 包有一个很有用的功能,就是 WaitGroup 先说说 WaitGroup 的用途:它能够一直等到所有的 goroutine 执行完成,并且阻塞主线程的执行,直到所有的 ...
- 『Golang』MongoDB在Golang中的使用(mgo包)
有关在Golang中使用mho进行MongoDB操作的最简单的例子.
- golang中Context的使用场景
golang中Context的使用场景 context在Go1.7之后就进入标准库中了.它主要的用处如果用一句话来说,是在于控制goroutine的生命周期.当一个计算任务被goroutine承接了之 ...
- java项目中可能会使用到的jar包解释
一.Struts2 用的版本是struts2.3.1.1 一个简单的Struts项目所需的jar包有如下8个 1. struts2-core-2.3.1.1.jar: Struts2的核心类库. 2. ...
- 正确的 Composer 扩展包安装方法
问题说明 我们经常要往现有的项目中添加扩展包,有时候因为文档的错误引导,如下图来自 这个文档 的: composer update 这个命令在我们现在的逻辑中,可能会对项目造成巨大伤害. 因为 com ...
随机推荐
- Unity_DOTween
DOTween官网 DG.Tweening.Ease枚举详解 2019.12.12补充: 问题:当前dotween动画没播放完,便再次播放有冲突的操作,如连续多次播放.正播.倒播,导致显示不正常或报错 ...
- 什么是DDoS黑洞路由?
1. 什么是DDoS黑洞路由? DDoS黑洞路由/过滤(有时称为黑孔)是缓解DDoS攻击的一种对策,网络流量将被路由到"黑洞"中并且丢失.如果在没有特定限制条件下实施黑洞过滤,合法 ...
- Redis之集群
Redis Cluster是 Redis的分布式解决方案,在3.0版本正式推出,有效地解决了Redis分布式方面的需求.当遇到单机内存.并发.流量等瓶颈时,可以采用Cluster架构方案达到负载均衡的 ...
- 试着给VuePress添加渐进式Web应用(PWA)支持,基于vuepress/plugin-pwa,点亮离线访问
背景 有时候,我们也希望VuePress构建的文档中心能支持离线访问,这时候我们需要给他添加渐进式Web应用(PWA,Progressive Web App)的支持,根据官方文档指引,我们可以借助插件 ...
- Linux系统安装-C7
1.安装部署操作系统 (1)创建虚拟机,加载系统镜像 (2)进入系统引导界面进行配置 补充:centos7系统网卡名称 默认系统的网卡名称为 eth0 eth1 –centos6 默认系统的网卡名称为 ...
- jdk keytool 自签证书
jdk keytool 自签证书 https需要用到ssl证书,可以从阿里等平台申请,本文采用jdk keytool进行自签证书. 生成环境:linux 用jdk自带keytool工具生成密钥库 ke ...
- gRPC四种模式、认证和授权实战演示,必赞~~~
前言 上一篇对gRPC进行简单介绍,并通过示例体验了一下开发过程.接下来说说实际开发常用功能,如:gRPC的四种模式.gRPC集成JWT做认证和授权等. 正文 1. gRPC四种模式服务 以下案例演示 ...
- Java:jar包与war包的差异
一般将项目分为两层:服务层和表现层(视图层),通常我们把服务层打包成jar,而把视图层的包打成war包. 仔细对比可以发现: jar包中包含了你写程序的所有服务或者第三方类库,它通常是作为幕后工作者, ...
- 可执行jar包在windows server2008下的自启动
最近要部署项目的服务端在windows server2008下面,所以把项目打包成可执行的jar包,然后希望它能开机自启动,毕竟每次都在cmd下输入java -jar xxx.jar才能启动太繁琐了. ...
- Linux学习之路第四天(运行级别)
linux 实用指令 指定运行级别 运行级别说明 0 :关机 1:单用户(找回丢失密码) 2.多用户状态没有网络服务 3.多用户状态有网络服务 4.系统未保留给用户 5.图形界面 6.系统重启 常用的 ...