Message queue cho .NET: RabbitMQ vs Azure Service Bus vs Kafka
Cách chọn message queue cho service .NET: RabbitMQ, Azure Service Bus, Kafka. Khi nào dùng queue vs topic, MassTransit vs client trần, semantic at-least-once.
Mục lục
Lần đầu một HTTP đồng bộ timeout và bạn nhận ra bên nhận chỉ chậm chứ không hỏng, bạn đã gặp ca yêu thích của queue. Chương này cho thấy khi nào queue thắng HTTP, queue nào chọn cho service .NET, và cách kết nối MassTransit khiến lựa chọn dễ tha thứ.
Khi nào message queue thực sự thay HTTP đồng bộ?
Ba tín hiệu cụ thể.
Việc dài vượt budget HTTP. Tạo PDF mất 5 giây; trình duyệt user không chờ. Nhận request, drop message vào queue, trả 202 Accepted với URL trạng thái.
Traffic bùng phát với downstream chậm. Flash sale nhảy checkout 100x. Cổng thanh toán hỗ trợ 200 req/s. Queue hấp thụ đỉnh; consumer rút theo tốc độ hỗ trợ.
Thông báo cross-service. Tạo đơn → giao kho → gửi email → update analytics. Mỗi bên nhận không nên làm bên ghi chậm; mỗi bên retry độc lập. Topic broadcast event; consumer subscribe theo nhịp riêng.
Nếu không, HTTP với Polly retry (chương 11) đơn giản và dễ debug hơn.
Ngân sách số nào cho lựa chọn queue?
Backend Throughput Latency p99 Độ bền lưu
RabbitMQ (1 node) ~30K msg/s ~10 ms disk tuỳ chọn
RabbitMQ (cluster) ~100K msg/s ~20 ms disk + mirror
Azure Service Bus Std ~2K msg/s ~50 ms sẵn có
Azure Service Bus Prem ~10-50K msg/s ~20 ms sẵn có
Amazon SQS ~3K msg/s/queue ~50-200 ms sẵn có
Kafka 100K-1M msg/s ~5 ms disk, replicate
Cho phần lớn service .NET các con số throughput không liên quan - backend nào cũng xử lý nổi traffic của bạn. Chọn theo semantic (queue vs topic, ordered vs unordered, replay vs no-replay) và phù hợp vận hành (self-host vs managed, dung sai cloud lock-in).
Kiến trúc tối thiểu trông thế nào?
flowchart LR
Producer[ASP.NET Core API] -->|publish| Q[(Queue RabbitMQ)]
Q -->|consume| Worker[Background Service]
Worker --> DB[(Postgres)]
Worker -. khi vượt retry .-> DLQ[(Dead-letter queue)]
Producer nhận request user, trả 202, publish message. Consumer (host
BackgroundService) rút queue, xử mỗi message, ack. Lỗi lặp lại
chuyển sang dead-letter queue để xem xét. Đây là hình dạng của 90%
việc nền .NET và scale bằng cách thêm replica consumer.
Cấu hình .NET 10 với MassTransit?
Phía producer, trong Program.cs:
builder.Services.AddMassTransit(x =>
{
x.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host(builder.Configuration.GetConnectionString("RabbitMq"));
cfg.ConfigureEndpoints(ctx);
});
});
// Publish từ controller:
public class OrderController(IPublishEndpoint bus) : Controller
{
[HttpPost]
public async Task<IActionResult> Create(OrderRequest req)
{
var orderId = Guid.NewGuid();
await bus.Publish(new OrderCreated(orderId, req.UserId, req.Items));
return Accepted(new { orderId });
}
}
Phía consumer, trong project worker riêng:
public record OrderCreated(Guid OrderId, Guid UserId, IReadOnlyList<Item> Items);
public class OrderCreatedConsumer(IOrderProcessor processor)
: IConsumer<OrderCreated>
{
public async Task Consume(ConsumeContext<OrderCreated> ctx)
{
// Idempotent - gọi hai lần với cùng OrderId vẫn an toàn.
await processor.ProcessAsync(ctx.Message.OrderId, ctx.CancellationToken);
}
}
builder.Services.AddMassTransit(x =>
{
x.AddConsumer<OrderCreatedConsumer>();
x.UsingRabbitMq((ctx, cfg) =>
{
cfg.Host(builder.Configuration.GetConnectionString("RabbitMq"));
cfg.ReceiveEndpoint("order-created", ep =>
{
ep.UseMessageRetry(r => r.Exponential(5,
TimeSpan.FromSeconds(1), TimeSpan.FromMinutes(1), TimeSpan.FromSeconds(5)));
ep.ConfigureConsumer<OrderCreatedConsumer>(ctx);
});
});
});
Ba điểm cần nhớ. Cùng code chạy với Azure Service Bus chỉ bằng đổi
một dòng (UsingAzureServiceBus). MassTransit xử lý retry với
exponential backoff sẵn. Consumer phải idempotent - đọc lại hai
lần phải ra cùng state.
Queue tạo ra failure mode nào?
Năm cái xuất hiện trước:
- Poison message - một message xấu luôn throw giết consumer nếu không có retry policy. Sửa: cấu hình retry rồi gửi sang dead-letter queue.
- Giao không thứ tự - queue không đảm bảo thứ tự xuyên partition hay consumer. Nếu logic phụ thuộc thứ tự, dùng partition key (Kafka, Service Bus session) hoặc số tuần tự.
- Consumer kẹt - một message chậm chặn cả consumer. Sửa: consumer concurrent, hoặc chuyển việc chậm sang queue riêng.
- Backlog phình - producer đẩy nhanh hơn consumer rút; queue lớn đến cạn disk hoặc memory. Sửa: alert độ sâu queue và scale consumer.
- Mất ack - consumer xử xong rồi crash trước khi ack; queue giao lại. Sửa: handler idempotent.
Chương 13 cho cách lộ
queue_depth, consumer_lag_seconds, dlq_count_total qua
OpenTelemetry.
Khi nào không nên dùng queue?
Ba mùi.
Một: việc đồng bộ user nhìn thấy. Nếu user đang nhìn spinner chờ kết quả, queue không giúp. Họ muốn câu trả lời trong 200 ms; queue ép họ chờ consumer lấy message. Dùng tầng cache hoặc query nhanh hơn.
Hai: throughput nhỏ. Queue ở 1 msg/phút là gánh vận hành không
lợi. Một BackgroundService poll database đơn giản hơn.
Ba: workflow đòi nhất quán cross-service nghiêm ngặt. Queue là at-least-once và không có thứ tự. Nếu cần exactly-once đa service, saga pattern là hình dạng đúng - và ngay cả khi đó, queue chỉ là transport, không phải câu trả lời.
Đi tiếp đâu từ đây?
Chương kế tiếp: phong cách API cho .NET - REST, gRPC, GraphQL, và khi nào mỗi cái thắng. API đồng bộ và queue async lắp ghép trong mọi service .NET thật; chương kế tiếp hoàn thiện nửa đồng bộ.