Rabbit MQ在Ack之后多次提取消息

时间:2018-09-26 16:36:06

标签: c# concurrency .net-core rabbitmq message-queue

Rabbit MQ实现存在一个奇怪的问题。我们正在使用容器来扩展处理器。我们目前正在运行2个容器作为试点测试。它是基本的控制台应用程序,它调用一种方法来处理队列中的消息。

我们正在处理的有效载荷有一个Guid。我们注意到同一GUID多次从队列中拉出。对消息进行确认后的事件。根据我们对Rabbit MQ的了解,这不应发生。这可能与我们当前使用C#RabbitMQClient库的实现有关。另外,可能是为我们的消费者使用了docker容器。我们无法在产品中重现此问题。

这只发生在Guid的一个容器中。因此,我们认为这与实际处理器本身中的某些问题无关。如果您需要更多完整日志,请询问。

当前体系结构

X

一些想法或想法

  • 在下一次尝试使消息出队之前,ack的到达速度可能不够快。
  • 我们的实现有些问题,这里的人可以指出。我们正在尝试建立一个单队列多用户模型以更快地处理消息。

问题:

  1. 如果您遇到了经验丰富的MQ兔子,那么对于上述方案的实现有何想法?
  2. 可能会发生什么? (下面是没有调用的代码外壳以及示例日志)

public class RabbitMQClient : IQueueClient
{
private IConnectionFactory _factory;
private IConnection _connection;
private ILoggerClient _logger;
private IWebApiClient _webApiClient;
private string _queueName;
private string _dlqName;
private string _rqName;
private int _maxRetryCount = 0;
private int _expiration = 0;
private decimal _expirationExponent = 0;

public RabbitMQClient(IConfigurationRoot config, ILoggerClient logger, IWebApiClient webApiClient)
{
    //Setup the ConnectionFactory
    _factory = new ConnectionFactory()
    {
        UserName = config["RabbitMQSettings:Username"],
        Password = config["RabbitMQSettings:Password"],
        VirtualHost = config["RabbitMQSettings:VirtualHost"],
        HostName = config["RabbitMQSettings:HostName"],
        Port = Convert.ToInt32(config["RabbitMQSettings:Port"]),
        AutomaticRecoveryEnabled = true,
        RequestedHeartbeat = 60,
        Ssl = new SslOption()
        {
            ServerName = config["RabbitMQSettings:HostName"],
            Version = SslProtocols.Tls12,
            CertPath = config["RabbitMQSettings:SSLCertPath"],
            CertPassphrase = config["RabbitMQSettings:SSLCertPassphrase"],
            Enabled = true
        }
    };

    _logger = logger;
    _webApiClient = webApiClient;

    _queueName = config["RabbitMQSettings:QueueName"];
    _dlqName = $"{_queueName}.dlq";
    _rqName = $"{_queueName}.rq";
    _maxRetryCount = int.Parse(config["RabbitMQSettings:MessageSettings:MaxRetryCount"]);
    _expiration = int.Parse(config["RabbitMQSettings:MessageSettings:Expiration"]);
    _expirationExponent = decimal.Parse(config["RabbitMQSettings:MessageSettings:ExpirationExponent"]);
}

public void ProcessMessages()
{
    using (_connection = _factory.CreateConnection())
    {
        using (var channel = _connection.CreateModel())
        {
            /*
             * Create the DLQ.
             * This is where messages will go after the retry limit has been hit.
             */
            channel.ExchangeDeclare(_dlqName, "direct");
            channel.QueueDeclare(_dlqName, true, false, false, null);
            channel.QueueBind(_dlqName, _dlqName, _queueName);

            /*
             * Create the main exchange/queue. we need to explicitly declare
             * the exchange so that we can push items back to it from the retry queue
             * once they're expired.
             */
            channel.ExchangeDeclare(_queueName, "direct");
            channel.QueueDeclare(_queueName, true, false, false, new Dictionary<String, Object>
            {
                { "x-dead-letter-exchange", _dlqName }
            });
            channel.QueueBind(_queueName, _queueName, _queueName);

            /*
             * Set the DLX of the retry queue to be the original queue
             * This is needed for the exponential backoff
             */
            channel.ExchangeDeclare(_rqName, "direct");
            channel.QueueDeclare(_rqName, true, false, false, new Dictionary<String, Object>
            {
                { "x-dead-letter-exchange", _queueName }
            });
            channel.QueueBind(_rqName, _rqName, _queueName);                    

            channel.BasicQos(0, 1, false);

            Subscription subscription = new Subscription(channel, _queueName, false);

            foreach (BasicDeliverEventArgs e in subscription)
            {
                Stopwatch stopWatch = new Stopwatch();
                try
                {
                    var payment = (CreditCardPaymentModel)e.Body.DeSerialize(typeof(CreditCardPaymentModel));

                    _logger.EventLog("Payment Dequeued", $"PaymentGuid:{payment.PaymentGuid}");

                    stopWatch.Start();

                    var response = //The Call to the Web API Happens here we will either get a 200 or a 400 from the WebService

                    stopWatch.Stop();

                    var elapsedTime = stopWatch.Elapsed.Seconds.ToString();

                    if (response.ResponseStatus == HttpStatusCode.BadRequest)
                    {
                        var errorMessage = $"PaymentGuid: {payment.PaymentGuid} | Elapsed Call Time: {elapsedTime} | ResponseStatus: {((int)response.ResponseStatus).ToString()}"
                                           + $"/n ErrorMessage: {response.ResponseErrorMessage}";
                        _logger.EventLog("Payment Not Processed", errorMessage);
                        Retry(e, subscription, errorMessage, payment.PaymentGuid);
                    }
                    else
                    {

                        //All the Responses are making it here. But even after the ACK they are being picked up and processoed again.
                        subscription.Ack(e);
                        _logger.EventLog("Payment Processed", $"--- Payment Processed - PaymentGuid : {payment.PaymentGuid} | Elapsed Call Time: {elapsedTime} | SourceStore : {payment.SourceStore} | Request Response: {(int)response.ResponseStatus}");
                    }
                }
                catch (Exception ex)
                {
                    Retry(e, subscription, ex.Message);
                    _logger.ErrorLog("Payment Not Processed", ex.ToString(), ErrorLogLevel.ERROR);
                }
            }
        }
    }
}

    public void Retry(BasicDeliverEventArgs payload, Subscription subscription, string errorMessage, Guid paymentGuid = new Guid())
    {

        if(paymentGuid != Guid.Empty)
        {
            _logger.EventLog("Retry Called", $"Retry on Payment Guid {paymentGuid}");
        }
        else
        {
            _logger.EventLog("Retry Called", errorMessage);
        }

        //Get or set the retryCount of the message
        IDictionary<String, object> headersDict = payload.BasicProperties.Headers ?? new Dictionary<String, object>();
        var retryCount = Convert.ToInt32(headersDict.GetValueOrDefault("x-retry-count"));

        //Check if the retryCount is still less than the max and republish the message
        if (retryCount < _maxRetryCount)
        {
            var originalExpiration = Convert.ToInt32(headersDict.GetValueOrDefault("x-expiration"));
            var newExpiration = Convert.ToInt32(originalExpiration == 0 ? _expiration : originalExpiration * _expirationExponent);

            payload.BasicProperties.Expiration = newExpiration.ToString();
            headersDict["x-expiration"] = newExpiration;
            headersDict["x-retry-count"] = ++retryCount;

            payload.BasicProperties.Headers = headersDict;

            subscription.Model.BasicPublish(_rqName, _queueName, payload.BasicProperties, payload.Body);
            subscription.Ack(payload);
        }
        else //Reject the message, which will send it to the DLX / DLQ
        {
            headersDict.Add("x-error-msg", errorMessage);
            payload.BasicProperties.Headers = headersDict;

            subscription.Nack(payload, false, false);
            _logger.ErrorLog("Error", errorMessage, ErrorLogLevel.ERROR);
        }
    }
}

public static class DictionaryExtensions
{
    public static TValue GetValueOrDefault<TKey, TValue>(this IDictionary<TKey, TValue> dic, TKey key)
    {
        return (dic != null && dic.TryGetValue(key, out TValue result)) ? result : default(TValue);
    }
}
}

这些是容器日志以及我们所看到的。即使成功,您也可以看到多次提取同一付款guid。

Container 1

Main
AutomaticPaymentQueue
1
EventName: Payment Dequeued | EventMessage: PaymentGuid:32d065a9-57e8-4359-afac-b7339b4904cc
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : 32d065a9-57e8-4359-afac-b7339b4904cc | Elapsed Call Time: 9 | SourceStore : C0222 | Request Response: 200
EventName: Payment Dequeued | EventMessage: PaymentGuid:65ad87a8-4cfe-47e8-863c-88e0c83fcd6f
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : 65ad87a8-4cfe-47e8-863c-88e0c83fcd6f | Elapsed Call Time: 2 | SourceStore : C0222 | Request Response: 200
EventName: Payment Dequeued | EventMessage: PaymentGuid:5dc2d38f-cbc9-492b-bd41-37531974c66d
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : 5dc2d38f-cbc9-492b-bd41-37531974c66d | Elapsed Call Time: 2 | SourceStore : C0222 | Request Response: 200
EventName: Payment Dequeued | EventMessage: PaymentGuid:5dc2d38f-cbc9-492b-bd41-37531974c66d
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : 5dc2d38f-cbc9-492b-bd41-37531974c66d | Elapsed Call Time: 1 | SourceStore : C0222 | Request Response: 200
EventName: Payment Dequeued | EventMessage: PaymentGuid:dad2616c-924d-4255-ad91-a262e3bcd245
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : dad2616c-924d-4255-ad91-a262e3bcd245 | Elapsed Call Time: 1 | SourceStore : C0222 | Request Response: 200

Container 2

Main
AutomaticPaymentQueue
1
EventName: Payment Dequeued | EventMessage: PaymentGuid:cb4fcb7a-48a7-422f-86d4-69c881366f05
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : cb4fcb7a-48a7-422f-86d4-69c881366f05 | Elapsed Call Time: 4 | SourceStore : C0222 | Request Response: 200
EventName: Payment Dequeued | EventMessage: PaymentGuid:5dc2d38f-cbc9-492b-bd41-37531974c66d
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : 5dc2d38f-cbc9-492b-bd41-37531974c66d | Elapsed Call Time: 2 | SourceStore : C0222 | Request Response: 200
EventName: Payment Dequeued | EventMessage: PaymentGuid:dad2616c-924d-4255-ad91-a262e3bcd245
EventName: Payment Processed | EventMessage: --- Payment Processed - PaymentGuid : dad2616c-924d-4255-ad91-a262e3bcd245 | Elapsed Call Time: 2 | SourceStore : C0222 | Request Response: 200

发布消息的类

public class RabbitMQClient : IQueueClient
{
    private static ConnectionFactory _factory;
    private static IConnection _connection;
    private static IModel _model;
    private const string QueueName = "AutomaticPaymentQueue";



    private void CreateConnection()
    {
        _factory = new ConnectionFactory();

        //Basic Login Infomration
        _factory.UserName = ConfigurationManager.AppSettings["RabbitMQUserName"]; ;
        _factory.Password = ConfigurationManager.AppSettings["RabbitMQPassword"];
        _factory.VirtualHost = ConfigurationManager.AppSettings["RabbitMQVirtualHost"];
        _factory.Port = Int32.Parse(ConfigurationManager.AppSettings["RabbitMQPort"]);


        //TLS Settings
        _factory.HostName = ConfigurationManager.AppSettings["RabbitMQHostName"];
        _factory.Ssl.ServerName = ConfigurationManager.AppSettings["RabbitMQHostName"];

        //SSL
        _factory.Ssl.Version = SslProtocols.Tls12;
        _factory.Ssl.CertPath = ConfigurationManager.AppSettings["RabbitMQSSLCertPath"];
        _factory.Ssl.CertPassphrase = ConfigurationManager.AppSettings["RabbitMQSSLCertPassphrase"];
        _factory.Ssl.Enabled = true;

        _connection = _factory.CreateConnection();
        _model = _connection.CreateModel();

    }

    public void SendMessage(Payload payload)
    {
         CreateConnection();
        _model.BasicPublish("", "AutomaticPaymentQueue", null, payload.Serialize());
    }
}

1 个答案:

答案 0 :(得分:1)

根据您提供的代码,看起来就像问题出在生产方面一样。话虽如此,最好的做法是使消息处理为idempotent操作。实际上,幂等性设计是几乎所有外部接口的关键假设(我认为它在内部接口中同样重要)。

即使您确实设法在发布者上找到并解决了问题,您也应该意识到,这不能保证“恰好一次”交付。无法保证。相反,您可以使用以下两种方法之一(相互排斥):

  • 最多一次交货(0
  • 至少一次交货(1 <= n)

RabbitMQ documentation

  

使用确认保证至少一次交付。如果没有确认,则在发布和使用操作期间可能会丢失消息,并且只能保证一次发送

发布和使用消息时发生了几件事。由于消息处理系统(尤其是AMQP协议)的异步特性,因此无法保证处理一次恰好一次,同时仍能提供消息传递系统所需的性能(实质上,它强制执行一切都通过重复数据删除过程中的串行过程完成)。您已选择至少要处理一次 ,因此,系统的设计应确保重复不会导致不必要的状态更改。