在CSDN学Golang分布式中间件(kafka)

一,kafka 体系结构以及读写机制

Golang Kafka 体系结构:

Kafka 是一个分布式的消息队列系统,其核心思想是基于发布/订阅模式来存储和处理消息。以下是 Kafka 的体系结构:

  1. Broker:Kafka 集群中的每个节点被称为 Broker。它们是通过 Zookeeper 协调器进行协作和管理。
  2. Topic:每条消息都属于一个特定的主题。主题是逻辑上类似于数据表的东西,它们用来分类消息。
  3. Partition:每个主题可以分成多个 Partition,每个 Partition 包含了一部分消息。Partition 中的每条消息都有一个唯一的序号(Offset),并且 Offset 在整个 Partition 中是连续不断的。
  4. Producer:生产者负责向 Kafka Broker 发送消息,并且可以指定要将消息发送到哪个 Partition。
  5. Consumer:消费者从 Kafka Broker 拉取数据,并且可以根据需要订阅一个或多个主题及其相应的 Partition。
  6. Consumer Group:一个消费者组由多个消费者实例组成,它们共同消费同一个主题下的不同 Partition。当某个消费者实例失败时,剩余实例会重新均衡以确保各自接收相等数量的消息。
  7. ZooKeeper:Kafka 使用 Zookeeper 进行元数据管理、Broker 故障检测、领导选举等操作。

Golang Kafka 读写机制:

在 Golang 中,我们可以使用第三方库 sarama 来进行 Kafka 的读写操作。

  1. 写入数据:
// 创建一个生产者对象
producer, err := sarama.NewSyncProducer([]string{"kafka-broker:9092"}, nil)
if err != nil {
    panic(err)
}
defer producer.Close()

// 构造消息
msg := &sarama.ProducerMessage{
    Topic: "test-topic",
    Value: sarama.StringEncoder("hello, kafka!"),
}

// 发送消息
partition, offset, err := producer.SendMessage(msg)
if err != nil {
    panic(err)
}
fmt.Printf("message sent to partition %d at offset %d\n", partition, offset)
  1. 读取数据:
// 创建一个消费者对象
consumer, err := sarama.NewConsumer([]string{"kafka-broker:9092"}, nil)
if err != nil {
    panic(err)
}
defer consumer.Close()

// 订阅主题及其 Partition
partitionList, _ := consumer.Partitions("test-topic")
for _, partition := range partitionList {
    pc, _ := consumer.ConsumePartition("test-topic", partition, sarama.OffsetNewest)

    // 消费消息
    go func(pc sarama.PartitionConsumer) {
        defer pc.AsyncClose()
        for msg := range pc.Messages() {
            fmt.Printf("partition:%d\toffset:%d\tvalue:%s\n", msg.Partition, msg.Offset, string(msg.Value))
        }
    }(pc)
}

// 阻塞程序不退出,等待消息到来或错误发生
select {}

这里的代码实现了从 test-topic 主题中消费消息,并且输出了每个消息所在的 Partition、Offset 以及具体的消息内容。我们可以使用 consumer.Partitions() 方法获取主题下所有的 Partition,然后循环遍历每个 Partition 创建一个消费者实例去消费消息。当然,这里也可以采用 Consumer Group 的方式进行消费。

二,kafka 异步处理

在 Golang 中,使用第三方库 sarama 进行 Kafka 异步处理也非常简单。

  1. 生产者异步发送消息:
// 创建一个生产者对象
producer, err := sarama.NewAsyncProducer([]string{"kafka-broker:9092"}, nil)
if err != nil {
    panic(err)
}
defer producer.AsyncClose()

// 构造消息
msg := &sarama.ProducerMessage{
    Topic: "test-topic",
    Value: sarama.StringEncoder("hello, kafka!"),
}

// 发送消息
producer.Input() <- msg

// 等待结果返回
select {
case success := <-producer.Successes():
    fmt.Printf("message sent to partition %d at offset %d\n", success.Partition, success.Offset)
case err := <-producer.Errors():
    fmt.Printf("failed to send message: %s\n", err.Error())
}

这里使用了 NewAsyncProducer() 方法创建了一个异步生产者实例。在向 Kafka Broker 发送消息时,我们将消息通过 Input() 方法写入到生产者的输入通道中,并且在 select 语句中等待结果返回。如果发送成功,则会从 Successes() 通道中获取到成功信息;如果发送失败,则会从 Errors() 通道中获取到错误信息。

  1. 消费者异步消费消息:
// 创建一个消费者对象
consumer, err := sarama.NewConsumer([]string{"kafka-broker:9092"}, nil)
if err != nil {
    panic(err)
}
defer consumer.Close()

// 订阅主题及其 Partition
partitionList, _ := consumer.Partitions("test-topic")
for _, partition := range partitionList {
    pc, _ := consumer.ConsumePartition("test-topic", partition, sarama.OffsetNewest)

    // 异步消费消息
    go func(pc sarama.PartitionConsumer) {
        defer pc.AsyncClose()
        for msg := range pc.Messages() {
            fmt.Printf("partition:%d\toffset:%d\tvalue:%s\n", msg.Partition, msg.Offset, string(msg.Value))
            // 处理完毕后异步提交 Offset
            pc.AsyncCommitOffset()
        }
    }(pc)
}

// 阻塞程序不退出,等待消息到来或错误发生
select {}

这里使用了 ConsumePartition() 方法创建了一个消费者实例,并且在循环遍历每个 Partition 时启动了一个 goroutine 来异步消费消息。当从 Messages() 通道中获取到一条消息时,我们可以进行相应的处理,并且通过 AsyncCommitOffset() 方法异步提交 Offset。这样就可以确保在出现异常情况下也能够正确地提交 Offset。

在 Golang 中使用 sarama 库进行 Kafka 的异步处理非常方便和灵活,可以有效提高应用程序的性能和可靠性

三,kafka 系统解耦

在分布式系统中,Kafka 可以作为一个非常好的消息队列来解耦不同的系统之间的依赖关系。使用 Kafka 作为消息传递机制,可以让系统之间相互独立,降低了耦合度,并且能够提供更加灵活和可靠的通信方式。

在 Golang 中,使用第三方库 sarama 可以轻松地实现 Kafka 的系统解耦。下面是一个简单的示例:

package main

import (
    "fmt"
    "os"
    "os/signal"
    "syscall"

    "github.com/Shopify/sarama"
)

func main() {
    // 创建消费者对象
    consumer, err := sarama.NewConsumer([]string{"kafka-broker:9092"}, nil)
    if err != nil {
        panic(err)
    }
    defer consumer.Close()

    // 订阅主题及其 Partition
    partitionList, _ := consumer.Partitions("test-topic")
    for _, partition := range partitionList {
        pc, _ := consumer.ConsumePartition("test-topic", partition, sarama.OffsetNewest)

        // 异步消费消息
        go func(pc sarama.PartitionConsumer) {
            defer pc.AsyncClose()
            for msg := range pc.Messages() {
                fmt.Printf("partition:%d\toffset:%d\tvalue:%s\n", msg.Partition, msg.Offset, string(msg.Value))
            }
        }(pc)
    }

    // 阻塞程序不退出,等待消息到来或错误发生
    sigchan := make(chan os.Signal, 1)
    signal.Notify(sigchan, syscall.SIGINT, syscall.SIGTERM)
    <-sigchan
}

在该示例中,我们创建了一个消费者对象并订阅了一个名为 "test-topic" 的主题。通过异步消费消息的方式,我们可以将不同系统之间的消息传递交给 Kafka 来处理,从而实现系统解耦。同时,在程序运行时我们也监听了 SIGINT 和 SIGTERM 信号,这样当程序接收到这些信号时就能够优雅地退出。

在 Golang 中使用 sarama 库来实现 Kafka 的系统解耦非常方便和灵活,可以帮助我们更好地构建分布式系统并提高系统的可靠性和扩展性。

四,kafka 流量消峰

Kafka 可以作为一个高吞吐量的消息队列,但是在实际应用中,当流量过大时,可能会造成消息处理能力不足的情况。因此,在 Golang 中,我们可以采取一些策略来消峰增强系统的可扩展性和稳定性。

  1. 使用多个消费者

使用多个消费者可以增加消息处理能力。在 Golang 中,我们可以创建多个 goroutine 来同时消费消息,并将它们分配到不同的 Kafka 分区中。这样既可以提高系统的并发性能,也可以减少每个消费者需要处理的消息数量。

// 创建多个 PartitionConsumer
partitionList, _ := consumer.Partitions("test-topic")
for _, partition := range partitionList {
    pc, _ := consumer.ConsumePartition("test-topic", partition, sarama.OffsetNewest)
    // 将不同分区的 PartitionConsumer 交给不同的 worker 处理
    go worker(pc)
}
  1. 批量拉取和批量提交

在 Golang 中使用 sarama 库进行消息消费时,我们可以通过设置 Fetch.Max 参数来控制每次从 Kafka 中获取的最大记录数。例如:

config := sarama.NewConfig()
config.Consumer.Fetch.Max = 1000
consumer, err := sarama.NewConsumer([]string{"kafka-broker:9092"}, config)

这样就可以一次性拉取到更多的消息记录,并且减少网络传输的次数。同时,在消费者处理完一批消息后,我们也可以采用批量提交的方式来减少网络传输的负载:

for {
    select {
    case msg := <-messages:
        // 处理消息记录
        if needCommit() { // 判断是否需要提交 offset
            consumer.CommitOffsets()
        }
    }
}
  1. 消息过滤和合并

在实际应用中,有些消息可能是无关紧要的,或者不需要立即处理。因此,我们可以通过对消息进行过滤和合并来减轻系统压力。

例如,我们可以只消费特定类型的消息或者设置时间窗口,在某个时间范围内将多个相同类型的消息合并成一个批次进行处理。

for {
    select {
    case msg := <-messages:
        // 过滤非重要信息或将相同类型的消息合并到一起
        batch.AddMessage(msg)
        if batch.IsFull() || time.Now().After(batchDeadline) {
            processBatch(batch)
            batch.Reset()
            batchDeadline = time.Now().Add(10 * time.Millisecond)
        }
    }
}
  1. 增加分区数量

增加 Kafka 分区数量也是解决流量峰值问题的有效方法。增加分区数量后,Kafka 就会为每个分区都创建一个独立的 Consumer Group,并且每个 Consumer Group 都能够独立消费自己所属分区中的数据。

但是,在增加分区数量时需要注意,这可能会导致某些消息的顺序被打乱,因为同一主题下不同分区中的消息是并发处理的。

在 Golang 中使用 Kafka 消息队列时,我们可以通过上述策略来消峰增强系统的可扩展性和稳定性。同时也要根据实际应用场景进行合理调整,从而更好地满足业务需求。

五,kafka 日志处理

在实际应用中,Kafka 经常被用作日志处理的工具。Golang 提供了多个开源库来方便地使用 Kafka 进行日志处理。下面介绍两个常用的库:sarama 和 confluent-kafka-go。

  1. 使用 sarama 库进行日志处理

sarama 是一个支持生产者和消费者 API 的 Golang 库,可以与 Apache Kafka 版本 0.8 或更高版本兼容。它提供了一组简单而强大的 API 来访问 Kafka 集群,并支持消息批量发送和接收、异步发送和接收以及基于通道的消息读取等功能。

以下是使用 sarama 库进行日志处理的示例代码:

package main

import (
    "fmt"
    "os"
    "os/signal"

    "github.com/Shopify/sarama"
)

func main() {
    config := sarama.NewConfig()
    config.Producer.Return.Successes = true
    producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)
    if err != nil {
        panic(err)
    }
    defer producer.Close()

    // 发送一条带有时间戳的消息
    timestamp := time.Now().UTC().Format(time.RFC3339Nano)
    msg := &sarama.ProducerMessage{
        Topic:     "logs",
        Partition: int32(-1),
        Key:       sarama.StringEncoder("key"),
        Value:     sarama.StringEncoder(fmt.Sprintf("[%s] Hello, world!", timestamp)),
        Timestamp: time.Now().UTC(),
    }
    _, _, err = producer.SendMessage(msg)
    if err != nil {
        panic(err)
    }

    // 创建一个消费者组,订阅 "logs" 主题的所有分区
    consumer, err := sarama.NewConsumerGroup([]string{"localhost:9092"}, "my-group", config)
    if err != nil {
        panic(err)
    }
    defer consumer.Close()

    // 处理从 Kafka 中读取的消息
    go func() {
        for msg := range consumer.Messages() {
            fmt.Printf("Received message: %s\n", string(msg.Value))
            consumer.MarkOffset(msg, "") // 标记消息已被消费
        }
    }()

    // 捕获中断信号并优雅地退出程序
    sigterm := make(chan os.Signal, 1)
    signal.Notify(sigterm, os.Interrupt)
    <-sigterm
}

该示例代码中首先创建了一个生产者实例,然后发送一条带有时间戳的消息到名为 logs 的主题。接下来,创建一个消费者组并订阅 logs 主题的所有分区,并在另一个 goroutine 中处理从 Kafka 中读取的消息。最后,通过捕获中断信号来优雅地退出程序。

  1. 使用 confluent-kafka-go 库进行日志处理

confluent-kafka-go 是另一个用于 Golang 和 Apache Kafka 集成的开源库。它是 Confluent 公司维护的 Kafka 官方客户端之一,支持高级功能如事务、压缩、SSL 和 SASL 等。它还提供了易于使用的 API 来处理 Kafka 日志。

以下是使用 confluent-kafka-go 库进行日志处理的示例代码:

package main

import (
    "fmt"
    "os"
    "os/signal"

    "github.com/confluentinc/confluent-kafka-go/kafka"
)

func main() {
    producer, err := kafka.NewProducer(&kafka.ConfigMap{"bootstrap.servers": "localhost:9092"})
    if err != nil {
        panic(err)
    }
    defer producer.Close()

    // 发送一条带有时间戳的消息
    timestamp := time.Now().UTC().Format(time.RFC3339Nano)
    msg := &kafka.Message{
        TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
        Key:            []byte("key"),
        Value:          []byte(fmt.Sprintf("[%s] Hello, world!", timestamp)),
        Timestamp:      time.Now(),
    }
    err = producer.Produce(msg, nil)
    if err != nil {
        panic(err)
    }

    // 创建一个消费者实例,订阅 "logs" 主题的所有分区
    consumer, err := kafka.NewConsumer(&kafka.ConfigMap{
        "bootstrap.servers":  "localhost:9092",
        "group.id":           "my-group",
        "auto.offset.reset":  "earliest",
        "enable.auto.commit": false,
    })
    if err != nil {
        panic(err)
    }
    defer consumer.Close()

     // 订阅主题并处理从 Kafka 中读取的消息
     consumer.SubscribeTopics([]string{"logs"}, nil)

     for {
         msg, err := consumer.ReadMessage(-1)
         if err != nil {
             fmt.Printf("Consumer error: %v (%v)\n", err, msg)
             continue
         }
         fmt.Printf("Received message: %s\n", string(msg.Value))
         consumer.CommitMessage(msg) // 标记消息已被消费
     }

    // 捕获中断信号并优雅地退出程序
    sigterm := make(chan os.Signal, 1)
    signal.Notify(sigterm, os.Interrupt)
    <-sigterm
}

该示例代码中首先创建了一个生产者实例,然后发送一条带有时间戳的消息到名为 logs 的主题。接下来,创建一个消费者实例并订阅 logs 主题的所有分区,并在 for 循环中处理从 Kafka 中读取的消息。最后,通过捕获中断信号来优雅地退出程序。

无论是使用 sarama 还是 confluent-kafka-go 库进行日志处理,都需要注意以下几点:

  • 配置合理的参数值以满足应用场景需求;
  • 处理完每个消息后要及时提交 offset,以确保不会重复消费或漏掉任何消息;
  • 使用异步 API 或 goroutine 处理大量的消息记录时需要注意资源占用问题。

六,kafka 驱动包 生产者消息生产策略

在 Golang 中使用 Kafka 时,可以使用第三方驱动包来进行消息生产和消费。其中,常用的驱动包有 sarama 和 confluent-kafka-go。

对于生产者消息生产策略,Kafka 提供了两种方式:同步发送和异步发送。

  1. 同步发送

在同步发送模式下,当生产者调用 SendMessage() 方法时,会阻塞直到消息被成功写入或发生错误。这种方式虽然简单易用,但是由于需要等待服务器的响应,在高并发场景下可能会降低整体吞吐量。

以下是使用 sarama 库进行同步发送的示例代码:

producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, nil)
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &sarama.ProducerMessage{
    Topic: "test-topic",
    Value: sarama.StringEncoder("hello world"),
}

partition, offset, err := producer.SendMessage(msg)
if err != nil {
    panic(err)
}
fmt.Printf("Message sent to partition %d at offset %d\n", partition, offset)

以上代码中创建了一个新的 SyncProducer 实例,并通过 SendMessage() 方法向主题为 test-topic 的分区发送一条字符串消息。如果发送成功,则返回该消息所在分区和偏移量;否则抛出异常。

以下是使用 confluent-kafka-go 库进行同步发送的示例代码:

producer, err := kafka.NewProducer(&kafka.ConfigMap{
    "bootstrap.servers": "localhost:9092",
})
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &kafka.Message{
    TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
    Value:          []byte("hello world"),
}

deliveryChan := make(chan kafka.Event)
err = producer.Produce(msg, deliveryChan)
if err != nil {
    panic(err)
}

e := <-deliveryChan
m := e.(*kafka.Message)
if m.TopicPartition.Error != nil {
    fmt.Printf("Delivery failed: %v\n", m.TopicPartition.Error)
} else {
    fmt.Printf("Message delivered to topic %s [%d] at offset %v\n",
        *m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset)
}

以上代码中创建了一个新的 Producer 实例,并通过 Produce() 方法向主题为 test-topic 的分区发送一条字符串消息。如果发送成功,则从 deliveryChan 中接收到消息交付事件并打印相关信息;否则抛出异常。

  1. 异步发送

在异步发送模式下,生产者不会等待服务器响应,而是将消息存储在本地缓冲区中,并立即返回。这种方式可以提高整体吞吐量,但需要注意缓冲区溢出的问题。

以下是使用 sarama 库进行异步发送的示例代码:

producer, err := sarama.NewAsyncProducer([]string{"localhost:9092"}, nil)
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &sarama.ProducerMessage{
    Topic: "test-topic",
    Value: sarama.StringEncoder("hello world"),
}

producer.Input() <- msg
select {
case success := <-producer.Successes():
    fmt.Printf("Message sent to partition %d at offset %d\n", success.Partition, success.Offset)
case err := <-producer.Errors():
    panic(err.Err)
}

以上代码中创建了一个新的 AsyncProducer 实例,并通过 Input() 方法将一条字符串消息发送到主题为 test-topic 的分区。然后使用 select 语句接收异步发送结果,如果发送成功则打印相关信息;否则抛出异常。

以下是使用 confluent-kafka-go 库进行异步发送的示例代码:

producer, err := kafka.NewProducer(&kafka.ConfigMap{
    "bootstrap.servers": "localhost:9092",
})
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &kafka.Message{
    TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
    Value:          []byte("hello world"),
}

deliveryChan := make(chan kafka.Event)
err = producer.Produce(msg, deliveryChan)
if err != nil {
    panic(err)
}

select {
case e := <-deliveryChan:
    m := e.(*kafka.Message)
    if m.TopicPartition.Error != nil {
        fmt.Printf("Delivery failed: %v\n", m.TopicPartition.Error)
    } else {
        fmt.Printf("Message delivered to topic %s [%d] at offset %v\n",
            *m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset)
    }
case <-time.After(3 * time.Second):
    fmt.Println("Delivery timed out")
}

以上代码中创建了一个新的 Producer 实例,并通过 Produce() 方法将一条字符串消息发送到主题为 test-topic 的分区。然后使用 select 语句等待异步发送结果,如果在 3 秒内未收到交付事件,则认为发送失败。

需要注意的是,在异步发送模式下,生产者会将消息缓存在本地缓冲区中,并尝试批量发送以提高效率。因此,当缓冲区已满时可能会发生阻塞或丢失消息的情况。可以通过调整驱动包的配置参数来解决该问题。

七,kafka 驱动包 生产者 ack 策略

在 Golang 中使用 Kafka 时,生产者的 ack 策略是指生产者等待服务器响应的方式。Kafka 提供了三种 ack 策略:0、1 和 all。

  1. acks=0

当 acks=0 时,生产者发送消息后不会等待服务器响应,直接返回成功。这种方式虽然速度最快,但也存在风险:如果消息没有被成功写入到任何一个副本中,则无法得知是否发送失败。

以下是使用 sarama 库进行 acks=0 发送的示例代码:

config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.NoResponse

producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &sarama.ProducerMessage{
    Topic: "test-topic",
    Value: sarama.StringEncoder("hello world"),
}

partition, offset, err := producer.SendMessage(msg)
if err != nil {
    panic(err)
}
fmt.Printf("Message sent to partition %d at offset %d\n", partition, offset)

以上代码中创建了一个新的 SyncProducer 实例,并将 RequiredAcks 设置为 NoResponse 表示不需要等待服务器响应。如果发送成功,则返回该消息所在分区和偏移量;否则抛出异常。

以下是使用 confluent-kafka-go 库进行 acks=0 发送的示例代码:

producer, err := kafka.NewProducer(&kafka.ConfigMap{
    "bootstrap.servers": "localhost:9092",
    "acks":              "0",
})
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &kafka.Message{
    TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
    Value:          []byte("hello world"),
}

deliveryChan := make(chan kafka.Event)
err = producer.Produce(msg, deliveryChan)
if err != nil {
    panic(err)
}

e := <-deliveryChan
m := e.(*kafka.Message)
if m.TopicPartition.Error != nil {
    fmt.Printf("Delivery failed: %v\n", m.TopicPartition.Error)
} else {
    fmt.Printf("Message delivered to topic %s [%d] at offset %v\n",
        *m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset)
}

以上代码中创建了一个新的 Producer 实例,并将 acks 设置为 0 表示不需要等待服务器响应。如果发送成功,则从 deliveryChan 中接收到消息交付事件并打印相关信息;否则抛出异常。

  1. acks=1

当 acks=1 时,生产者发送消息后会等待主副本成功写入后返回成功,无需等待所有副本都同步完成。这种方式速度较快且可靠性较高,在大多数情况下都是推荐的方式。

以下是使用 sarama 库进行 acks=1 发送的示例代码:

config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForLocal

producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &sarama.ProducerMessage{
    Topic: "test-topic",
    Value: sarama.StringEncoder("hello world"),
}

partition, offset, err := producer.SendMessage(msg)
if err != nil {
    panic(err)
}
fmt.Printf("Message sent to partition %d at offset %d\n", partition, offset)

以上代码中创建了一个新的 SyncProducer 实例,并将 RequiredAcks 设置为 WaitForLocal 表示等待主副本成功写入后返回成功。如果发送成功,则返回该消息所在分区和偏移量;否则抛出异常。

以下是使用 confluent-kafka-go 库进行 acks=1 发送的示例代码:

producer, err := kafka.NewProducer(&kafka.ConfigMap{
    "bootstrap.servers": "localhost:9092",
    "acks":              "1",
})
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &kafka.Message{
    TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
    Value:          []byte("hello world"),
}

deliveryChan := make(chan kafka.Event)
err = producer.Produce(msg, deliveryChan)
if err != nil {
    panic(err)
}

e := <-deliveryChan
m := e.(*kafka.Message)
if m.TopicPartition.Error != nil {
    fmt.Printf("Delivery failed: %v\n", m.TopicPartition.Error)
} else {
    fmt.Printf("Message delivered to topic %s [%d] at offset %v\n",
        *m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset)
}

以上代码中创建了一个新的 Producer 实例,并将 acks 设置为 1 表示等待主副本成功写入后返回成功。如果发送成功,则从 deliveryChan 中接收到消息交付事件并打印相关信息;否则抛出异常。

  1. acks=all

当 acks=all 时,生产者发送消息后会等待所有副本都同步完成后才返回成功。这种方式可靠性最高,但速度也最慢。

以下是使用 sarama 库进行 acks=all 发送的示例代码:

config := sarama.NewConfig()
config.Producer.RequiredAcks = sarama.WaitForAll

producer, err := sarama.NewSyncProducer([]string{"localhost:9092"}, config)
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &sarama.ProducerMessage{
    Topic: "test-topic",
    Value: sarama.StringEncoder("hello world"),
}

partition, offset, err := producer.SendMessage(msg)
if err != nil {
    panic(err)
}
fmt.Printf("Message sent to partition %d at offset %d\n", partition, offset)

以上代码中创建了一个新的 SyncProducer 实例,并将 RequiredAcks 设置为 WaitForAll 表示等待所有副本都同步完成后才返回成功。如果发送成功,则返回该消息所在分区和偏移量;否则抛出异常。

以下是使用 confluent-kafka-go 库进行 acks=all 发送的示例代码:

producer, err := kafka.NewProducer(&kafka.ConfigMap{
    "bootstrap.servers": "localhost:9092",
    "acks":              "all",
})
if err != nil {
    panic(err)
}
defer producer.Close()

msg := &kafka.Message{
    TopicPartition: kafka.TopicPartition{Topic: &topic, Partition: kafka.PartitionAny},
    Value:          []byte("hello world"),
}

deliveryChan := make(chan kafka.Event)
err = producer.Produce(msg, deliveryChan)
if err != nil {
    panic(err)
}

e := <-deliveryChan
m := e.(*kafka.Message)
if m.TopicPartition.Error != nil {
    fmt.Printf("Delivery failed: %v\n", m.TopicPartition.Error)
} else {
    fmt.Printf("Message delivered to topic %s [%d] at offset %v\n",
        *m.TopicPartition.Topic, m.TopicPartition.Partition, m.TopicPartition.Offset)
}

以上代码中创建了一个新的 Producer 实例,并将 acks 设置为 all 表示等待所有副本都同步完成后才返回成功。如果发送成功,则从 deliveryChan 中接收到消息交付事件并打印相关信息;否则抛出异常。

需要注意的是,ack 策略越高可靠性越高,但速度也会越慢。

八,kafka 驱动包 消费者组 rebalance机制

在 Golang 中使用 Kafka 时,Kafka 的消费者组使用了 rebalance 机制来分配分区和重新分配分区。rebalance 是指在以下情况下发生的一种事件:

  • 新的消费者加入到消费者组中;
  • 消费者从消费者组中离开;
  • 分区数量发生变化。

当出现以上情况时,Kafka 将会触发 rebalance 事件,以重新平衡每个消费者所负责的分区。

以下是 sarama 库实现 Kafka 消费者组 rebalance 机制的示例代码:

config := sarama.NewConfig()
config.Consumer.Group.Rebalance.Strategy = sarama.BalanceStrategyRoundRobin

consumer, err := sarama.NewConsumerGroup([]string{"localhost:9092"}, "test-group", config)
if err != nil {
    panic(err)
}
defer consumer.Close()

handler := &myHandler{}

for {
    err = consumer.Consume(context.Background(), []string{"test-topic"}, handler)
    if err != nil {
        panic(err)
    }
}

以上代码中创建了一个新的 ConsumerGroup 实例,并将 Rebalance 策略设置为 RoundRobin 表示轮询方式。然后循环调用 Consume() 方法并传入自定义处理函数 myHandler,以实现对消息的处理。

在自定义处理函数中需要实现 sarama.ConsumerGroupHandler 接口,并重写三个方法:Setup()Cleanup() 和 ConsumeClaim()。其中,Setup() 方法会在 consumer group 成功启动时被调用,而 Cleanup() 方法则会在 consumer group 关闭时被调用。ConsumeClaim() 方法则是实际处理消息的方法。

以下是示例代码中自定义处理函数的实现:

type myHandler struct{}

func (h *myHandler) Setup(sess sarama.ConsumerGroupSession) error {
    fmt.Printf("New session setup: %v\n", sess)
    return nil
}

func (h *myHandler) Cleanup(sess sarama.ConsumerGroupSession) error {
    fmt.Printf("Session cleanup: %v\n", sess)
    return nil
}

func (h *myHandler) ConsumeClaim(sess sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) error {
    for msg := range claim.Messages() {
        fmt.Printf("Message claimed: value = %s, topic = %s, partition = %d, offset = %d\n",
            string(msg.Value), msg.Topic, msg.Partition, msg.Offset)

        // do something with the message

        sess.MarkMessage(msg, "")
    }
    return nil
}

以上代码中实现了 sarama.ConsumerGroupHandler 接口,并重写了三个方法:Setup()Cleanup() 和 ConsumeClaim()。在 Setup() 方法中打印出新的 session 信息,在 Cleanup() 方法中打印出关闭 session 的信息。在 ConsumeClaim() 方法中对 claim 中的每条消息进行处理,并标记已经处理过的消息。

需要注意的是,当消费者组发生 rebalance 事件时,Kafka 会调用一次 Cleanup() 方法来释放掉当前所有分配给该消费者组的分区,然后再调用一次 Setup() 方法来分配新的分区。在这个过程中,消费者需要暂停消息处理并等待新的分区分配完成后再继续处理消息。

以上是 sarama 库实现 Kafka 消费者组 rebalance 机制的示例代码,其他库也大同小异。

你可能感兴趣的:(golang,分布式,开发语言,中间件,后端)