kafka 消费并行度提升

本文介绍 Kafka 消费的一个例子,以及如何优化提升消费的并行度。

例子

Kafka 消费一般使用 github.com/Shopify/sarama 包实现,现已支持消费组消费。下面是一个消费组消费的例子:

func consume(){
    // 定义一个消费者,并开始消费
    consumer := Consumer{}
    ConsumerHighLevel.Consume(ctx, []string{Conf.topic}, &consumer); err != nil {
        sarama.Logger.Printf("[ERROR] Error from Consumer: %s", err.Error())
    }
}

type Consumer struct {}
func (consumer *Consumer) Setup(sarama.ConsumerGroupSession) error { return nil }
func (consumer *Consumer) Cleanup(sarama.ConsumerGroupSession) error { return nil }
func (consumer *Consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) (err error) {
    for {
        message := <-claim.Messages()
        println(message.Value)    // 消费逻辑
        session.MarkMessage(message, "") // 提交偏移
    }
    return nil
}

Client 支持并行消费多个Topic。消费时,会针对各Partition分别启动一个ConsumeClaim 的goroutine,获取队列数据并消费。如下图所示:

362b6868064ee562da52e378c4dcb685.png

每个goroutine 会标记消息的偏移量,以方便提交偏移至远程。当服务关闭重启后,会从远端获取当前消费的偏移,并继续消费。

存在的问题

为保证消费消息的顺序性,需一个队列由一个 goroutine 消费。此模式下并行度则由kafka的队列数量来控制。Kafka 队列数量多,并行度就越大,队列数越少,并行度就小。

假设每条消息需要处理的时间平均为10ms,则一个队列的最大消费个数就是100/s;设置32分片,可以达到3200/s的消息处理量。当业务增长时,可能需要增加kakfa Topic的分片数量来提升消息处理量了。

但Kafka的分片数量并不能无限增长。因设置太多的分片可能会造成 Broker 选举慢,客户端需要cache 的消息量过大等问题[1]。

下面看看提升并行度的另一种思路。

如何解决

提升并行度的另一种方案,就是在本地做二次Sharding,使用本地队列做真实消费。下面是一个示意图:

023e430b4c6ab9a3296bb8ae4a33de85.png

每个goroutine通过分片规则重新分配到多个本地队列中。本地的队列个数(消费goroutine 数量)可以自由控制,使消费的并行度可控。

当然,使用本地队列会有如下几个问题:

如何保证偏移提交的顺序性?

从本地队列中消费完成后,需要提交偏移到远程。如果提交顺序有问题,可能会出现消息漏掉的情况。举个例子:

消息M1,M2 均来自远程队列Q,且 M1 进入队列的时间早于 M2。在本地做分发时,M1 进入 LQ1, M2 进入 LQ2。若 M1 早于 M2 消费完成并提交偏移则没有问题;若 M1 晚于 M2 消费完成并提交了偏移,此时服务异常退出,当再次启动服务时M1 消息将不再被消费,造成M1 消息丢失。为此,需要在本地维护一个偏移提交的逻辑,保证提交偏移的有序性。逻辑如下:

  • • Kafka 的Offset 并不保证连续性[2],需要对每个kafka Partition 提供一个逻辑队列waitCommitQueue 保存当前正在消费的消息, 提供一个hashmap waitCommitMap 标记某个偏移已完成消费(偏移较小的消息未消费完成。)

  • • 当消息从Kafka 队列中取出后,放入waitCommitQueue队列队尾(标记正在消费),并将消息分发到本地队列。

  • • 当消息消费完成后,判断该消息是否为 waitCommitQueue 队首:

    • • 如果消费完成,说明可以继续提交偏移

    • • 如果未消费完成,则需要等待最小消息消费完成。

    • • 若是,则说明该消息是最小的offset,直接提交。并循环判断队列后面的消息偏移是否已消费完成?

    • • 若不是,则在waitCommitMap中标记已提交,等待最小偏移的消息出现。

下面是一段简要代码片段:

type Consumer struct {
    chMessage []chan *CMessage
}
type None struct{}{}
func (consumer *Consumer) ConsumeClaim(session sarama.ConsumerGroupSession, claim sarama.ConsumerGroupClaim) (err error) {
    waitCommitQueue := list.New()
    waitCommitMap := make(map[int64]None, 100000)
    var mutex sync.Mutex

    for {
        message := <-claim.Messages()
        mutex.Lock()
        waitCommitQueue.PushBack(message)
        mutex.Unlock()

        // 自定义的local 队列, 使用channel 实现
        // sharding 是自定义的算法
        consumer.chMessage[consumer.Sharding(message)] <- &CMessage{
            Message: message,
            MarkMessage: func() {
                mutex.Lock()
                defer mutex.Unlock()

                if waitCommitQueue.Front().Value.(*sarama.ConsumerMessage).Offset == message.Offset {
                    waitCommitQueue.Remove(waitCommitQueue.Front())
                    session.MarkMessage(message, "")
                    for waitCommitQueue.Len() > 0 {
                        item := waitCommitQueue.Front()
                        offset := item.Value.(*sarama.ConsumerMessage).Offset

                        if _, ok := waitCommitMap[offset]; !ok {
                            break
                        }
                        delete(waitCommitMap, offset)
                        session.MarkMessage(item.Value.(*sarama.ConsumerMessage), "")
                        waitCommitQueue.Remove(item)
                    }
                } else {
                    waitCommitMap[message.Offset] = None{}
                }
            },
        }
    }
}

在做消费时,仅需要启动协程,分别消费各个channel中的数据即可:

func (consumer *Consumer) consume(){
        queues := consumer.chMessage
        for i := 0; i < len(queues); i++ {
        wg.Add(1)
        go func(queue chan *kafkautils.CMessage) {
            defer wg.Done()
            for {
                select {
                case message := <-queue:
                    // time.Sleep(100 * time.Millisecond)
                    time.Sleep(time.Duration(rand.Int() % 20) * time.Millisecond)
                    message.MarkMessage()
                    atomic.AddInt32(&count, 1)
                case <-closed:
                    return
                }
            }
        }(queues[i])
    }
}

有了本地队列的加持,决定并行度的不再是远端队列数量,而是本地的消费队列数量,只要多开点channel,所有问题将迎刃而解。

如何保证本地队列不会暴涨导致OOM?

使用带缓冲的channel 作为本地队列,当队列满后将阻塞。避免了无限制的增长导致OOM.

应用场景

  1. 1. 适合于消费端消费慢,并行度过低的情况。如果通过增加一些kafka 的partition 的方式解决,建议直接用kafka 的partition,避免在消费端增加逻辑复杂度。

  2. 2. 消费的消息具有区分度,可以通过某些字段做分片。

  3. 3. 当然此类消费逻辑也适合于消息聚合的场景,通过调整本地消费队列的个数,减少消费的并行度,一定程度上降低消费的速度和服务器的负载。

  4. 4. 当服务宕机或者出现故障重启后,可能会出现重复消费的情况。因此消息需要保障最终一致性。

其他

还有个问题,为什么不是每个kafka 队列对应独立的协程池,而是共用同一个协程池?

通过公用协程池,可以实现资源公用,针对消费写入速度相差甚远的队列时,可以取长补短。

本文相关代码转到github查看[3]

引用

  • • [1] 如何设置kafka的分片数 https://www.confluent.io/blog/how-choose-number-topics-partitions-kafka-cluster/

  • • [2] Kafka Offset 不连续 https://stackoverflow.com/questions/54636524/kafka-streams-does-not-increment-offset-by-1-when-producing-to-topic

  • • [3] github https://github.com/lpflpf/util/tree/master/golang/kafkaconsumer

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
在Flink中,设置Flink Kafka并行度取决于所消费Kafka主题的分区数。如果只消费一个主题,则可以将并行度设置为主题的分区数。例如,如果一个主题有4个分区,你可以将并行度设置为4。这样每个Flink任务将消费一个分区。 如果要消费多个主题,可以选择两种方式来设置并行度。第一种方式是将并行度设置为所有主题分区数的总和。比如,如果有两个主题,一个主题有4个分区,另一个主题有6个分区,那么可以将并行度设置为10。这样每个Flink任务将消费多个主题的分区。 另一种方式是使用Robin的方式将数据以round-robin的方式写入不同的Kafka分区。使用这种方式时,可以不用设置sink的并行度。Flink会自动将数据以round-robin的方式写入所有Kafka分区。 综上所述,Flink Kafka并行度设置与所消费Kafka主题的分区数有关。根据需要选择合适的并行度设置方式。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* *2* [flink kafka 消费以及生产并行度设置](https://blog.csdn.net/longlovefilm/article/details/117400809)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *3* [【Flink实战系列】Flink 消费 kafka 并行度设置多少合理(kafka 的 partition 和 Flink 的 subtask 对应关系...](https://blog.csdn.net/xianpanjia4616/article/details/108983357)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值