【发布时间】:2019-11-06 02:20:39
【问题描述】:
我通过在 .NET Core 2.2 上使用 BackgroundService 将 Kafka 使用者实现为控制台应用程序。我正在使用 confluent-kafka-dotnet v1.0.1.1 作为 Apache Kafka 的客户端。我不太确定如何处理每条消息。
-
由于每条消息的处理可能需要一些时间(最多 24 小时),我为每条消息启动一个新任务,这样我就不会阻止消费者使用新消息。我认为如果我有太多消息,那么每次创建一个新任务都不是正确的方法。那么处理每条消息的正确方法是什么?是否可以为每条消息创建某种动态后台服务?
-
如果一条消息已经在处理,但应用程序崩溃或发生了重新平衡,我最终会多次使用和处理同一条消息。我应该自动提交偏移量(或在它被消耗后立即提交)并将消息(或任务)的状态存储在某处,比如数据库中吗?
我知道有 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<T>或ActionBlock<T>都是一个好的开始) -
偏移提交策略很大程度上取决于:1)进程是否幂等? 2) 处理顺序重要吗?
标签: c# .net-core apache-kafka confluent-platform