Kolejki w e-commerce - RabbitMQ, Redis, SQS, Kafka
Każdy projekt sklepu B2B w skali ma moment, w którym kolejki przestają być „dobrym pomysłem na później" i stają się ratunkiem. Najczęściej moment ten przychodzi w nocy, gdy ERP klienta restartuje się na 4 godziny i wszystkie nowe zamówienia walą się 500-tkami. Z kolejkami - nikt nawet nie zauważy. Bez nich - telefon o 3 nad ranem.
Spis treści (12)
W skrócie
- 1. Kolejki = każda operacja asynchroniczna w architekturze sklepu
- 2. Co przez kolejkę: zamówienia → ERP, synchronizacje, emaile, reindeksowanie, marketplace sync
- 3. Co synchronicznie: koszyk, checkout, walidacje przy zamówieniu
- 4. Technologie polskiego B2B: RabbitMQ (najczęściej), Redis Streams (mniejsza skala), SQS (na AWS)
- 5. Must-have wzorce: idempotencja, retry z exponential backoff, dead letter queue, monitoring
Co konkretnie powinno przejść przez kolejkę
Lista operacji typowego sklepu B2B i decyzja kolejka / synchronicznie:
| Operacja | Kolejka? | Powód |
|---|---|---|
| Zamówienie → ERP | TAK | ERP może być wolne / niedostępne |
| Email "Dziękujemy za zamówienie" | TAK | SMTP może być wolne |
| SMS o statusie | TAK | API SMS może mieć rate limit |
| Synchronizacja stanów ERP → sklep | TAK | Wolumen + retry |
| Synchronizacja cenników ERP → sklep | TAK | Wolumen + retry |
| Webhook do BaseLinkera o nowym zamówieniu | TAK | External system, niestabilność |
| Reindeksowanie Elasticsearch | TAK | Powolne, ale niekrytyczne |
| Walidacja koszyka | NIE | Klient czeka |
| Walidacja limitu kredytowego przy checkout | NIE | Decyzja synchroniczna |
| Płatność online (callback z bramki) | TAK | Bramki to external |
| Generowanie raportu PDF | TAK | Może trwać sekundy |
| Push notification | TAK | External provider |
Reguła: jeśli klient nie czeka na rezultat - kolejka. Jeśli czeka (koszyk, checkout) - synchronicznie.
RabbitMQ - domyślny wybór dla średniego B2B
RabbitMQ to message broker oparty na AMQP, najpopularniejszy w polskim PHP/Symfony świecie.
Mocne strony:
- Bardzo dojrzały (od 2007)
- Dobre wsparcie w PHP/Symfony (Symfony Messenger ma RabbitMQ transport)
- Exchange + Queue + Binding = elastyczne routing
- Dead letter queue (DLQ) wbudowane
- Management UI (przeglądasz kolejki w przeglądarce)
- Klastrowanie i replikacja
Słabe strony:
- Wymaga osobnego procesu (RabbitMQ server) i konfiguracji
- Persistence przez Mnesia (specyficzna baza Erlanga)
- Performance dobry, ale nie świetny dla bardzo dużych wolumenów (powyżej setek tysięcy / sekundę)
Typowa konfiguracja w sklepie B2B:
Exchange: orders (direct)
Queue: orders.to_erp (binding: routing_key=erp)
Queue: orders.to_baselinker (binding: routing_key=baselinker)
Queue: orders.to_email (binding: routing_key=email)
Exchange: products (fanout)
Queue: products.reindex_search
Queue: products.invalidate_cache
Queue: products.sync_marketplace
Dead Letter Exchange (DLX): dlq
Queue: dlq.all (catch-all dla failed messages)
Hosting RabbitMQ:
- Self-hosted: 1-2 maszyny (HA cluster), ~500-1500 zł / mies.
- Managed: CloudAMQP (od $20/mies. do $1000+ dla większych)
- AWS: Amazon MQ for RabbitMQ
Redis Streams - lekka alternatywa
Redis Streams (dodane w Redis 5.0, 2018) to wbudowany w Redis mechanizm kolejek. Lżejszy niż RabbitMQ.
Mocne strony:
- Jeden serwis (Redis), nie dwa
- Bardzo szybki (memory-based)
- Consumer groups dla skalowania
- Persistence przez Redis (RDB/AOF)
- Dobry dla mniejszej skali
Słabe strony:
- Mniej funkcji niż RabbitMQ (brak exchange-routing, słabsze DLQ patterns)
- Persistence słabsza (Redis może gubić dane przy awarii)
- Mniej dojrzały ekosystem
Kiedy Redis Streams:
- Mała / średnia skala (do ~10k messages/min)
- Już używasz Redis dla cache i sesji
- Chcesz minimum dodatkowej infrastruktury
Kiedy NIE Redis Streams:
- Duża skala (powyżej 50k+ messages/min)
- Skomplikowane routing (fanout, topic exchanges)
- Krytyczne wymagania persistence (nie chcesz tracić nawet jednego message)
AWS SQS / Google Pub/Sub - dla cloud-hosted
Jeśli sklep na AWS - naturalny wybór to SQS. Plusy:
- Managed (nie utrzymujesz)
- Skalowanie automatyczne
- Wbudowany DLQ
- Tania (kilka USD / mies. dla średniego sklepu)
Minus:
- Lock-in w AWS
- Mniej elastyczne routing niż RabbitMQ
Analogicznie Google Cloud Pub/Sub i Azure Service Bus.
Kafka - gdy skala wymusza
Apache Kafka to message broker dla bardzo dużej skali. Eventy persistowane jako log, replay możliwy.
Kiedy Kafka:
- Skala 100k+ events / sec
- Multi-konsument (kilku konsumentów tego samego eventu)
- Event sourcing
- Stream processing
W polskim B2B średniej skali: rzadko. Kafka to znaczna inwestycja w setup i operacje.
Symfony Messenger - abstrakcja, którą warto
Symfony Messenger to component frameworka Symfony, który abstraktuje konkretną technologię kolejek.
// Wysyłanie message - niezależnie od transportu
$bus->dispatch(new OrderToERPMessage($orderId));
// Konfiguracja transportu w yaml - można zmienić bez ruszania kodu
framework:
messenger:
transports:
erp_sync: 'amqp://guest:guest@rabbitmq:5672/%2f/messages'
# lub: 'redis://redis:6379/messages'
# lub: 'sqs://...'
Plus: zaczynasz z Redis (proste), przechodzisz na RabbitMQ (skala), bez zmian w kodzie aplikacji.
Dla Magento / Shopware: używaj wbudowanego mechanizmu kolejek (Magento Message Queue Framework, Shopware Symfony Messenger).
Idempotencja - must-have
Idempotencja = wykonanie tej samej operacji wielokrotnie ma ten sam skutek co wykonanie jednokrotne.
Dlaczego ważne: retry. Jeśli operacja padnie, kolejka spróbuje ponownie. Jeśli operacja nie jest idempotentna - przy retry stworzysz duplikat.
Przykład złej praktyki:
// ZŁY KOD: każde wywołanie tworzy nowy dokument w ERP
$erp->createOrder($order);
Przykład dobrej praktyki:
// DOBRY KOD: external_reference jako idempotency key
$externalRef = 'WEB-' . $order->getId();
$existing = $erp->findOrderByReference($externalRef);
if ($existing) {
return $existing; // już istnieje, nic nie rób
}
$erp->createOrder($order, externalReference: $externalRef);
Idempotency keys w praktyce:
- Każde zdarzenie ma unikalny ID (UUID lub combo
sklep_id + entity_id) - Consumer sprawdza czy już przetworzył ten ID
- Jeśli tak - skip, oznaczy jako sukces
- Jeśli nie - przetwórz, zapisz
processed_event_idw tabeli idempotency
Retry i Dead Letter Queue
Retry z exponential backoff:
Message padnie? Nie próbuj natychmiast. Próbuj po: 1s, 2s, 4s, 8s, 16s, 32s, 60s. Razem 8 prób w okolicach 2 minut.
Powód: jeśli ERP padł - natychmiastowy retry nic nie da. Exponential backoff daje ERP-owi czas na powrót.
W Symfony Messenger:
framework:
messenger:
transports:
erp_sync:
dsn: 'amqp://...'
retry_strategy:
max_retries: 8
delay: 1000 # 1s
multiplier: 2 # 2x każdy retry
max_delay: 60000 # max 60s
Dead Letter Queue (DLQ):
Po wyczerpaniu retry - message ląduje w DLQ. To kolejka „wszystko poszło źle, zobacz".
DLQ to alarm. Każda wiadomość w DLQ to incydent do zbadania. Bez monitorowania DLQ - wiadomości giną.
Monitoring i alerty
Kolejka bez monitoringu = bomba zegarowa.
Co monitorować:
- Liczba wiadomości w kolejce - rośnie? Consumer nie nadąża, problem.
- Liczba wiadomości w DLQ - > 0 = alarm.
- Czas oczekiwania message (age) - wiadomość czeka 30 minut zamiast 30 sekund = problem.
- Consumer health - czy procesy konsumentów działają (Supervisor, systemd, k8s pods).
- Throughput - ile messages / sec przetwarzane.
Narzędzia:
- RabbitMQ Management Plugin (UI)
- Prometheus + Grafana
- Datadog APM
- New Relic
Alerty:
- DLQ > 0 → ticket / pager
- Queue depth > 10000 → warning
- Consumer down > 5 min → pager
Workery - jak je uruchamiać
Workery to procesy, które konsumują wiadomości z kolejek. Musi być coś, co je uruchamia i restartuje przy awariach.
Opcje:
1. Supervisor (Linux service manager).
Klasyczny wybór dla VPS / dedykowanych. Konfiguracja w INI, restart przy awarii, zarządzanie logami.
[program:order-sync-worker]
command=php bin/console messenger:consume erp_sync --time-limit=3600 --memory-limit=512M
process_name=%(program_name)s_%(process_num)02d
numprocs=4
autostart=true
autorestart=true
2. systemd.
Nowsze, w nowoczesnych Linuxach. Podobne do Supervisor.
3. Kubernetes Pods.
Każdy worker jako pod, replicas konfigurowalne, autoscaling. Dla projektów na K8s.
4. AWS ECS / Fargate.
Managed, dla AWS.
Liczba workerów:
Reguła: jeden worker per kolejka per CPU rdzeń. Czyli na 8-rdzeniowej maszynie z 4 kolejkami: 2 workery per kolejka.
Skalowanie: gdy queue depth rośnie - dodaj workerów. Gdy maleje - zmniejsz.
Najczęstsze błędy
1. Brak idempotencji. Retry → duplikaty.
2. Brak DLQ. Wiadomości giną cicho.
3. Synchronous calls w workerze. Worker czeka 30s na response z ERP, blokuje kolejkę.
4. Brak time limit / memory limit workerów. Workery puchną w pamięci, OOM po dniu.
5. Nieobsługiwane exceptions. Wyjątek nie złapany → worker crash → message zgubione.
6. Zbyt duże messages. Wpychanie całych obiektów do queue. Lepiej entity_id, dane pobiera worker.
Co dalej
- Architektura w szerszym kontekście: Architektura dużych sklepów
- Middleware używające kolejek: Middleware vs. bezpośrednio
- Integracja z ERP - gdzie kolejki kluczowe: Comarch XL
- Skalowanie bazy: Skalowanie DB
- Kolejki to fundament event-driven: Event-driven architecture
- Jak zobaczyć, czy kolejki stoją: Observability - logi, metryki, tracing
Najczęstsze pytania
Czy mogę używać tylko cron'a zamiast kolejek?
Czy Magento ma własne kolejki?
Co z Shopware?
Kafka czy RabbitMQ?
Czy potrzebuję managed RabbitMQ (CloudAMQP) czy self-hosted?
O autorze
Jakub Owsianka
Architekt rozwiązania silnika platform B2B. Zaczynał po stronie biznesu (własne sklepy), potem deweloper, dziś projektuje wdrożenia dla sklepów z katalogami w dziesiątkach tysięcy SKU. W ostatnich latach wdrożył AI-development w zespole i funkcjonalności oparte o AI bezpośrednio w silniku sklepu.
Masz pytanie do tego artykułu?
Dodatkowy kontekst, problem z własnym wdrożeniem, druga opinia - napisz wprost. Odpowiadam osobiście w 1-2 dni robocze.