Rabbit MQ实现存在一个奇怪的问题。我们正在使用容器来扩展处理器。我们目前正在运行2个容器作为试点测试。它是基本的控制台应用程序,它调用一种方法来处理队列中的消息。
我们正在处理的有效载荷有一个Guid。我们注意到同一GUID多次从队列中拉出。对消息进行确认后的事件。根据我们对Rabbit MQ的了解,这不应发生。这可能与我们当前使用C#RabbitMQClient库的实现有关。另外,可能是为我们的消费者使用了docker容器。我们无法在产品中重现此问题。
这只发生在Guid的一个容器中。因此,我们认为这与实际处理器本身中的某些问题无关。如果您需要更多完整日志,请询问。
当前体系结构
一些想法或想法
问题:
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());
}
}
答案 0 :(得分:1)
根据您提供的代码,看起来就像问题出在生产方面一样。话虽如此,最好的做法是使消息处理为idempotent操作。实际上,幂等性设计是几乎所有外部接口的关键假设(我认为它在内部接口中同样重要)。
即使您确实设法在发布者上找到并解决了问题,您也应该意识到,这不能保证“恰好一次”交付。无法保证。相反,您可以使用以下两种方法之一(相互排斥):
使用确认保证至少一次交付。如果没有确认,则在发布和使用操作期间可能会丢失消息,并且只能保证一次发送。
发布和使用消息时发生了几件事。由于消息处理系统(尤其是AMQP协议)的异步特性,因此无法保证处理一次恰好一次,同时仍能提供消息传递系统所需的性能(实质上,它强制执行一切都通过重复数据删除过程中的串行过程完成)。您已选择至少要处理一次 ,因此,系统的设计应确保重复不会导致不必要的状态更改。