消息队列-kafka实践 | 青训营

100 阅读3分钟

解决问题

生产方生产数据与消费方消费数据异步的问题。 如果生产方和消费方是多对多的关系,程序会变得十分复杂,主要会造成三个问题。

  1. 各个程序之间会进行重复的工作,造成信息的浪费。
  2. 信息过多无法及时同步时,会造成信息的丢失。
  3. 各个程序之间耦合度过高,牵一发而动全身。

所以用kafka来做中间转接,就变成了生产者发布信息,消费者订阅信息

核心概念

  • Topic: 以Topic为标识,生产者将不同主题的信息发布到kafka,消费者通过订阅不同的主题来消费自己想要的信息。
  • Partition: 每一个Topic下有Partition,可以实现不同的Partition分布到不同的server上。Kafka 只保证分区内的记录是有序的,而不保证主题中不同分区的顺序。
  • Partition Key: 传递给一个 Hash 函数,由计算结果决定写入哪个 Partition。例如使用 User ID 作为 Partition Key,那么此 ID 的消息就都在同一个 Partition,这样可以保证此类消息的有序性。
  • offset: 在每一个消费者中唯一保存的元数据是offset(偏移量)即消费在log中的位置.偏移量由消费者所控制:通常在读取记录后,消费者会以线性的方式增加偏移量,但是实际上,由于这个位置由消费者控制,所以消费者可以采用任何顺序来消费记录。

  • Consumer Group: 消费者使用一个 消费组 名称来进行标识,发布到topic中的每条记录被分配给订阅消费组中的一个消费者实例.消费者实例可以分布在多个进程中或者多个机器上。

kafka-go实践

//生产者
writer := &kafka.Writer{
    Addr:                   kafka.TCP("localhost:9092"),
    Topic:                  topic,
    Balancer:               &kafka.Hash{}, 
    WriteTimeout:           1 * time.Second, 
    RequiredAcks:           kafka.RequireNone,
    AllowAutoTopicCreation: true,
}
  • Balancer: &kafka.Hash{}:负载均衡器将根据消息的 key 使用哈希函数将消息分发到不同的分区。
  • RequiredAcks: kafka.RequireNone: 这个设置表示写入消息后,不需要等待任何确认(acknowledgment)
  • AllowAutoTopicCreation: true:这个设置允许自动创建主题,如果指定的主题不存在,程序将会自动创建它。如果设置为 false,则不会自动创建主题(通常是运维创建,此处是为了方便测试)。
for i := 0; i < 3; i++ {
    if err := writer.WriteMessages(
        ctx,
        kafka.Message{Key: []byte("1"), Value: []byte("big")},
        kafka.Message{Key: []byte("1"), Value: []byte("big")},
    ); err != nil {
        if err == kafka.LeaderNotAvailable {
            time.Sleep(500 * time.Millisecond)
            continue
        } else {
            fmt.Printf("批量写入kafka失败:%v\n", err)
        }
    } else {
        break
    }
}

  • if err == kafka.LeaderNotAvailable 这可能意味着主题还未完全创建,因此无法写入,等一会就好了。
  • 循环3次是给重试的机会,成功就break。
	reader = kafka.NewReader(kafka.ReaderConfig{
		Brokers:        []string{"localhost:9092"},
		Topic:          topic,
		CommitInterval: 1 * time.Second,
		GroupID:        "rec_team",
		StartOffset:    kafka.FirstOffset, //从最开始消费(创建时生效)
	})
  • CommitInterval: 1 * time.Second:指定消息的提交间隔,即在多长时间内自动提交消费的消息。在这里,设置为每秒提交一次。
  • GroupID: "rec_team":指定消费者组的标识符。这是为了实现多个消费者协同消费一个主题时的协调。
  • StartOffset: kafka.FirstOffset:设置从最早的消息开始消费。如果设置为 kafka.LastOffset,则从最新的消息开始消费。
// 监听2和15,收到时关闭reader,避免reader直接kill无法正常关闭 
func listenSignal() {
    c := make(chan os.Signal, 1)
    signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
    sig := <-c
    fmt.Printf("接收到信号 %s", sig.String())
    if reader != nil {
        reader.Close()
    }
    os.Exit(0)
}
  • signal.Notify(c, syscall.SIGINT, syscall.SIGTERM):通过调用 signal.Notify 函数,将 SIGINTSIGTERM 两个信号注册到通道 c,以便在接收到这些信号时进行处理。

总结

kafka作为消息队列可以更高效地完成消息的订阅和发布功能,不只kafka,还有RocketMQ、RabbitMQ等着学习,要根据不同的场景来选择不同的中间件。

参考资料

图说kafka

golang操作kafka

细说 Kafka Partition 分区

小朋友也可以懂的Kafka入门教程