【问题标题】:How to properly implement kafka consumer as a background service on .NET Core如何在 .NET Core 上正确地将 kafka 消费者实现为后台服务
【发布时间】:2019-11-06 02:20:39
【问题描述】:

我通过在 .NET Core 2.2 上使用 BackgroundService 将 Kafka 使用者实现为控制台应用程序。我正在使用 confluent-kafka-dotnet v1.0.1.1 作为 Apache Kafka 的客户端。我不太确定如何处理每条消息。

  1. 由于每条消息的处理可能需要一些时间(最多 24 小时),我为每条消息启动一个新任务,这样我就不会阻止消费者使用新消息。我认为如果我有太多消息,那么每次创建一个新任务都不是正确的方法。那么处理每条消息的正确方法是什么?是否可以为每条消息创建某种动态后台服务?

  2. 如果一条消息已经在处理,但应用程序崩溃或发生了重新平衡,我最终会多次使用和处理同一条消息。我应该自动提交偏移量(或在它被消耗后立即提交)并将消息(或任务)的状态存储在某处,比如数据库中吗?

我知道有 Hangfire,但我不确定是否需要使用它。如果我目前的方法完全错误,请给我一些建议。

这里是 ConsumerService 的实现:

public class ConsumerService : BackgroundService
{
    private readonly IConfiguration _config;
    private readonly IElasticLogger _logger;
    private readonly ConsumerConfig _consumerConfig;
    private readonly string[] _topics;
    private readonly double _maxNumAttempts;
    private readonly double _retryIntervalInSec;

    public ConsumerService(IConfiguration config, IElasticLogger logger)
    {
        _config = config;
        _logger = logger;
        _consumerConfig = new ConsumerConfig
        {
            BootstrapServers = _config.GetValue<string>("Kafka:BootstrapServers"),
            GroupId = _config.GetValue<string>("Kafka:GroupId"),
            EnableAutoCommit = _config.GetValue<bool>("Kafka:Consumer:EnableAutoCommit"),
            AutoOffsetReset = (AutoOffsetReset)_config.GetValue<int>("Kafka:Consumer:AutoOffsetReset")
        };
        _topics = _config.GetValue<string>("Kafka:Consumer:Topics").Split(',');
        _maxNumAttempts = _config.GetValue<double>("App:MaxNumAttempts");
        _retryIntervalInSec = _config.GetValue<double>("App:RetryIntervalInSec");
    }

    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        Console.WriteLine("!!! CONSUMER STARTED !!!\n");
        
        // Starting a new Task here because Consume() method is synchronous
        var task = Task.Run(() => ProcessQueue(stoppingToken), stoppingToken);

        return task;
    }

    private void ProcessQueue(CancellationToken stoppingToken)
    {
        using (var consumer = new ConsumerBuilder<Ignore, Request>(_consumerConfig).SetValueDeserializer(new MessageDeserializer()).Build())
        {
            consumer.Subscribe(_topics);

            try
            {
                while (!stoppingToken.IsCancellationRequested)
                {
                    try
                    {
                        var consumeResult = consumer.Consume(stoppingToken);

                        // Don't want to block consume loop, so starting new Task for each message  
                        Task.Run(async () =>
                        {
                            var currentNumAttempts = 0;
                            var committed = false;

                            var response = new Response();

                            while (currentNumAttempts < _maxNumAttempts)
                            {
                                currentNumAttempts++;

                                // SendDataAsync is a method that sends http request to some end-points
                                response = await Helper.SendDataAsync(consumeResult.Value, _config, _logger);

                                if (response != null && response.Code >= 0)
                                {
                                    try
                                    {
                                        consumer.Commit(consumeResult);
                                        committed = true;
                                        
                                        break;
                                    }
                                    catch (KafkaException ex)
                                    {
                                        // log
                                    }
                                }
                                else
                                {
                                    // log
                                }
                                
                                if (currentNumAttempts < _maxNumAttempts)
                                {
                                    // Delay between tries
                                    await Task.Delay(TimeSpan.FromSeconds(_retryIntervalInSec));
                                }
                            }
                                                    
                            if (!committed)
                            {
                                try
                                {
                                    consumer.Commit(consumeResult);
                                }
                                catch (KafkaException ex)
                                {
                                    // log
                                }
                            }
                        }, stoppingToken);
                    }
                    catch (ConsumeException ex)
                    {
                        // log
                    }
                }
            }
            catch (OperationCanceledException ex)
            {
                // log
                consumer.Close();
            }
        }
    }
}

【问题讨论】:

  • 不要在每条新消息上Task.Run,它会为每个新消息生成一个新线程,这非常消耗资源。如果您想解放消费者,请使用 prod-consumer 模式(BlockingCollection&lt;T&gt;ActionBlock&lt;T&gt; 都是一个好的开始)
  • 偏移提交策略很大程度上取决于:1)进程是否幂等? 2) 处理顺序重要吗?

标签: c# .net-core apache-kafka confluent-platform


【解决方案1】:

同意 Fabio 的观点,您不应该 Task.Run 来处理消息,因为您最终会遇到大量线程浪费资源并切换它们的执行,从而影响性能。

此外,由于 Kafka 使用拉模式,您的应用程序可以按照自己的节奏处理消息,因此可以在同一个线程中处理消费的消息。

关于多次处理消息,我建议存储已处理消息的偏移量,以便跳过已处理的消息。由于 offset 是一个基于 long 的数字,因此您可以轻松跳过 offset 小于之前提交的消息。当然,这仅在您有一个分区时才有效,因为 Kafka 在分区级别提供偏移计数器和顺序保证

您可以在my article 中找到 Kafka Consumer 的示例。如果您有任何问题,请随时提出,我很乐意为您提供帮助

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2022-10-26
    • 2020-05-01
    • 1970-01-01
    • 2022-10-23
    • 2020-08-20
    • 2018-10-31
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多