Kafka入門實戰教程(5):吞吐量與可靠性的實踐

1 提高Producer吞吐量的實踐

在實際環境中,用戶似乎總是願意用較小的延時增加的代價,去換取 TPS 的顯著提升。畢竟,從 2ms 到 10ms 的延時增加通常是可以忍受的。

事實上,Kafka Producer 就是採取了這樣的設計思想。每當 producer 發佈一個立即就發送 到 producer聚集一堆發佈後批量發送,如下圖所示:

我們可以在客戶端做一些配置,來實現producer的高吞吐量,涉及到的一些重要配置如下:

  • 批次大小,它和等待時間只要有一個滿足就會發送,默認16K,可以修改爲32K~512K。

  • 等待時間,它和批次大小隻要有一個滿足就會發送,建議設置爲5~100ms(根據你的場景來修改)。

  • 壓縮算法,使用壓縮算法網絡傳遞效率高,但也會相應耗費CPU,建議設置爲LZ4或zstd。

  • 緩衝區大小,默認1G,基本無需修改,最大可改爲2GB。

下面的示例展示了基於Confluent.Kafka客戶端組件如何對上面的配置項進行設置(均需要在Publish操作之前設置好),請注意查看帶有註釋的區域:

public async Task PublishAsync<T>(string topicName, T message) where T : class
{
    var config = new ProducerConfig 
    { 
        BootstrapServers = KAFKA_SERVERS,
        QueueBufferingMaxKbytes = 2097151, // 修改緩衝區最大爲2GB,默認爲1GB
        CompressionType = CompressionType.Lz4, // 配置使用壓縮算法LZ4,其他:gzip/snappy/zstd
        BatchSize = 32768, // 修改批次大小爲32K
        LingerMs = 20 // 修改等待時間爲20ms
    };
    
    using (var producer = new ProducerBuilder<string, string>(config).Build())
    {
        producer.Produce(topicName, new Message<string, string>
        {
            Key = Guid.NewGuid().ToString(),
            Value = JsonConvert.SerializeObject(message)
        }); ;
    }
}

2 高可靠性消息的實踐

在MQ中,一般存在兩種情況的消息丟失:

  • producer端消息丟失

  • consuer端消息丟失

對於producer端消息丟失,一般會採用帶回調函數的produce方法,且設置acks=all和設計一個較大的retry次數來避免消息丟失。

對於consumer端消息丟失,一般會採用關閉自動提交位移來避免消息丟失。

此外,要避免消息丟失,broker端也需要進行一些優化配置。

下面,我們就一起來看看。

Producer端

基於Confluent.Kafka的示例配置設置示例:重點關注註釋部分

public async Task PublishAsync<T>(string topicName, T message) where T : class
{
    var config = new ProducerConfig 
    { 
        BootstrapServers = KAFKA_SERVERS,
        Acks = Acks.All, // 表明只有所有副本Broker都收到消息纔算提交成功
        MessageSendMaxRetries = 50, // 消息發送失敗最大重試50次
        ......
    };
    

    using (var producer = new ProducerBuilder<string, string>(config).Build())
    {
        var numProduced = 0;
        var key = Guid.NewGuid().ToString();
        var value = JsonConvert.SerializeObject(message);
        // 使用帶回調函數的Produce方法
        producer.Produce(topicName, new Message<string, string> { Key = key, Value = value },
            (deliveryReport) =>
            {
                if (deliveryReport.Error.Code != ErrorCode.NoError)
                {
                    // 發送失敗
                    Console.WriteLine($"[Error] Failed to deliver message: {deliveryReport.Error.Reason}");
                }
                else
                {
                    // 發送成功
                    Console.WriteLine($"[Info] Produced event to topic {topicName}: key = {key} value = {value}");
                    numProduced += 1;
                }
            });

        // 等待所有回調函數執行完成,參數是超時時間,也就是最大的等待時間
        var queueSize = producer.Flush(TimeSpan.FromSeconds(5));
        if (queueSize > 0)
          Console.WriteLine($"[Warn] Producer event queue has {queueSize} pending events on exit.");

        Console.WriteLine($"[Info] {numProduced} messages were produced to topic {topicName}");
    }

    await Task.CompletedTask;
}

Consumer端

基於Confluent.Kafka的示例配置設置示例:重點關注註釋部分

public async Task SubscribeAsync<T>(IEnumerable<string> topics, Action<T> messageFunc, CancellationToken cancellationToken = default) where T : class
{
    var config = new ConsumerConfig
    {
        BootstrapServers = KAFKA_SERVERS,
        .....
        EnableAutoCommit = false, // 禁止AutoCommit
        Acks = Acks.All, // 需要所有副本響應纔算消費完成
        ......
    };
    
    using (var consumer = new ConsumerBuilder<Ignore, string>(config).Build())
    {
        consumer.Subscribe(topics);
        try
        {
            while (true)
            {
                try
                {
                    var cr = consumer.Consume(cancellationToken);
                    var message = JsonConvert.DeserializeObject<T>(cr.Message.Value);
                    if (message != null)
                        messageFunc(message);
                    consumer.Commit(cr); // 手動提交位移,會產生阻塞,影響吞吐量
                    Console.WriteLine($"[Info] Consumed record successfully! Received message: {message}");
                }
                catch (ConsumeException e)
                {
                    Console.WriteLine($"[Error] Error occured in consuming: {e.Error.Reason}");
                }
            }
        }
        catch (OperationCanceledException)
        {
            // Ctrl+C Pressed
            Console.WriteLine("[Info] Ctr+C pressed, now closing consumer.");
            consumer.Close();
        }
    }
         
    await Task.CompletedTask;
}

Broker端

對於Broker端,可以修改以下三個配置以適應高可靠性的要求:

  • unclean.leader.election.enable = false

  • replication.factor >= 3

  • min.insync.replicas > 1

  • 確保 replication.factor > min.insync.replicas

(1)設置 unclean.leader.election.enable = false

這是 Broker 端的參數,它控制的是哪些 Broker 有資格競選分區的 Leader。如果一個 Broker 落後原先的 Leader 太多,那麼它一旦成爲新的 Leader,必然會造成消息的丟失。故一般都要將該參數設置成 false,即不允許這種情況的發生。

從Kafka 0.11版本開始,這個選項的默認值就變成了false。

(2)設置 replication.factor >= 3

這也是 Broker 端的參數(Topic參數)。其實這裏想表述的是,最好將消息多保存幾份,畢竟目前防止消息丟失的主要機制就是冗餘。

(3)設置 min.insync.replicas > 1

這依然是 Broker 端參數(Topic參數),控制的是消息至少要被寫入到多少個副本纔算是“已提交”。設置成大於 1 可以提升消息持久性。在實際環境中千萬不要使用默認值 1。

(4)確保 replication.factor > min.insync.replicas

如果兩者相等,那麼只要有一個副本掛機,整個分區就無法正常工作了。我們不僅要改善消息的持久性,防止數據丟失,還要在不降低可用性的基礎上完成。推薦設置成 replication.factor = min.insync.replicas + 1。

示例:設置replication.factor=3, min.insync.replicas=2

kafka-topics.sh --create --zookeeper zk-server-master:2181/kafka --replication-factor 3 --partitions 3 --topic testtopic--config min.insync.replicas=2

上述配置其實就是實現一個類似MongoDB副本集的WriteConcern=Major的效果。

3 總結

本文介紹了提高producer吞吐量 與 提高消息可靠性 的實踐,重點介紹了在Confluent.Kafka組件下如何進行配置的代碼實踐,相信會對你有所幫助。

參考資料

極客時間,胡夕《Kafka核心技術與實戰》

B站,尚硅谷《Kafka 3.x入門到精通教程》

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章