NOGROUP No such key 'messages' or consumer group 'symfony': wenn der Shopware-Worker seine Redis-Gruppe verliert
Der Supervisor meldet nichts, die Prozesse laufen, und trotzdem kommt seit Stunden nichts mehr aus der Queue. Der Shop selbst läuft normal weiter. Auffällig wird es erst, wenn keine Bestellbestätigungen mehr rausgehen oder die Produktindexierung stehenbleibt. Im var/log/prod-*.log steht dann dieser Eintrag, meistens ein paar hundert Mal hintereinander.
Die Fehlermeldung
Error thrown while running command "messenger:consume async low_priority failed".
Message: "NOGROUP No such key 'messages' or consumer group 'symfony' in XREADGROUP with GROUP option"
{"exception":"[object] (Symfony\Component\Messenger\Exception\TransportException(code: 0):
NOGROUP No such key 'messages' or consumer group 'symfony' in XREADGROUP with GROUP option
at /var/www/shop/vendor/symfony/redis-messenger/Transport/Connection.php:556)"}
Was dahintersteckt
Der Redis-Transport von Symfony Messenger arbeitet mit Redis Streams. Beim Versenden landet die Nachricht per XADD in einem Key, der standardmäßig messages heißt. Gelesen wird sie per XREADGROUP über eine Consumer-Group namens symfony. Beides muss existieren, sonst antwortet Redis mit NOGROUP, und Symfony reicht das als TransportException nach oben durch.
Angelegt wird die Gruppe von der Option auto_setup. Die steht per Default auf true und feuert beim Aufbau der Verbindung ein XGROUP CREATE ... MKSTREAM. Einmal. Danach geht der Worker davon aus, dass sein Stream da ist, und fragt nicht mehr nach.
Genau das ist die Stelle, an der es auseinanderfällt: Verschwindet der Key, während der Prozess schon läuft, legt ihn niemand neu an. Der Worker rennt bis zu seinem Neustart in denselben Fehler.
Ein leerer Stream ist dabei nicht das Problem. Wer delete_after_ack=true fährt, löscht mit jedem XACK den Eintrag, der Key bleibt aber samt Gruppe bestehen. NOGROUP heißt: der Key ist komplett weg.
Vier Wege, wie er wegkommt:
- Eviction. Läuft die Redis-Instanz auf
allkeys-lruoderallkeys-randomund erreichtmaxmemory, wirft sie auch Keys ohne TTL raus. Der Message-Stream hat keine TTL, ist also Freiwild. Das ist mit Abstand der häufigste Fall. FLUSHDBoderFLUSHALL: ein Deploy-Skript, ein Monitoring-Tool oder ein zweites Projekt auf derselben Instanz räumt auf und nimmt die Queue mit.- Redis-Neustart ohne Persistenz. Ohne RDB oder AOF ist nach dem Restart alles leer, inklusive Gruppen.
- Falscher
dbindex. Der Worker schaut in eine andere Datenbank als der Sender. Dann war der Key nie da, wo gesucht wird.
Die Shopware-Doku beschreibt das Grundproblem unter Redis ziemlich direkt: Eine Eviction-Policy gilt pro Redis-Instanz, nicht pro Datenbank. Cache-Daten wollen volatile-lru, Sessions allkeys-lru, kritische Daten dürfen gar nicht verdrängt werden. Wer Cache und Message Queue in dieselbe Instanz legt und dort eine allkeys-Policy fährt, hat sich für die Queue gegen die eigene Doku entschieden, auch wenn die Datenbanknummern unterschiedlich sind.
Auf Symfony-Seite gibt es dafür bis heute keine Selbstheilung. Im Issue #45489 hat jemand 2022 genau den Patch vorgeschlagen, der bei NOGROUP das Setup noch einmal anstößt. Übernommen wurde er nicht, das Issue ist geschlossen und trägt das Label "Stalled". Rechne also nicht damit, dass ein Update das für dich löst.
Prüfen, ob es wirklich das ist
Erst nachsehen, ob Key und Gruppe existieren. Die Datenbanknummer nimmst du aus deinem MESSENGER_TRANSPORT_DSN.
redis-cli -n 2 EXISTS messages
redis-cli -n 2 XINFO GROUPS messages
redis-cli -n 2 XINFO STREAM messages
Kommt bei EXISTS eine 0 zurück, ist der Stream weg und der Rest der Suche erübrigt sich. Liefert XINFO GROUPS eine leere Liste, gibt es den Key, aber die Gruppe fehlt.
Danach die Frage, ob Redis aufräumt:
redis-cli CONFIG GET maxmemory-policy
redis-cli CONFIG GET maxmemory
redis-cli INFO stats | grep evicted_keys
redis-cli INFO keyspace
Ein evicted_keys im fünf- oder sechsstelligen Bereich bei gleichzeitig allkeys-lru ist die Bestätigung. Wenn zusätzlich mehrere Projekte auf derselben Instanz liegen, zeigt dir INFO keyspace, wie voll die einzelnen Datenbanken sind.
Die Lösung
Vorweg, damit klar ist, was du hier liest: Der Shop, aus dem dieses Log stammt, steht noch unter Beobachtung. Die Ursache ist eindeutig, die dauerhafte Bestätigung über mehrere Wochen steht aus.
Akut wieder zum Laufen bringen. messenger:setup-transports legt Stream und Gruppe neu an, danach müssen die Worker neu starten, weil sie ihre Verbindung sonst weiter mit dem alten Zustand betreiben:
bin/console messenger:setup-transports
supervisorctl restart all
Die eigentliche Arbeit ist die Redis-Konfiguration. Läuft die Queue auf einer eigenen Instanz, ist noeviction richtig. Teilst du dir die Instanz mit dem App-Cache, dann volatile-lru, weil dabei nur Keys mit TTL verdrängt werden und die Queue-Einträge keine haben:
# /etc/redis/redis.conf
maxmemory-policy volatile-lru
Sauberer ist die Trennung. Ab Shopware 6.6.8.0 kannst du benannte Verbindungen in der shopware.yaml konfigurieren und die Queue auf eine Instanz zeigen lassen, die nichts verdrängt:
# config/packages/shopware.yaml
shopware:
redis:
connections:
ephemeral:
dsn: '%env(REDIS_CACHE)%/0'
queue:
dsn: '%env(REDIS_QUEUE)%/0'
Und dann noch auto_setup=true im DSN:
# .env.local
MESSENGER_TRANSPORT_DSN=redis://:PASSWORD@HOST:PORT/2?auto_setup=true
Hier muss ich ehrlich sein, weil die Option an vielen Stellen als der Fix verkauft wird: auto_setup steht laut Symfony-Doku ohnehin auf true. Explizit hinzuschreiben schließt nur aus, dass irgendwo im Setup jemand auto_setup=0 gesetzt hat, etwa um Deployments zu beschleunigen. Gegen eine Eviction zur Laufzeit hilft es nicht.
Wirksam wird es erst in Kombination mit einem Worker, der regelmäßig stirbt. Ein --time-limit sorgt dafür, dass der Prozess in überschaubaren Abständen neu startet, und beim Neustart greift auto_setup wieder:
bin/console messenger:consume async low_priority failed --time-limit=600 --memory-limit=512M
Das ist ein Workaround, kein Fix. Er verkürzt den Ausfall von "bis jemand hinschaut" auf "höchstens zehn Minuten", verhindert aber nicht, dass Nachrichten beim Löschen des Streams verloren gehen.
Wenn das nicht hilft
Prüf den Namen deiner Env-Variablen. Ein Buchstabendreher im Variablennamen fällt nicht auf, weil der Transport dann still auf den Default-DSN zurückfällt und am Ende alle Transports auf demselben Stream landen. bin/console debug:config framework messenger zeigt dir, was tatsächlich ankommt.
Zweiter Kandidat: mehrere Shopware-Installationen auf einem Server, die sich dieselbe Redis-Instanz und denselben Stream-Namen teilen. Dann konsumieren fremde Worker deine Nachrichten. Unterschiedliche Datenbanknummern oder ein eigener stream-Parameter im DSN trennen das.
Nicht die Ursache, obwohl es oft vermutet wird: stream_max_entries. Die Option trimmt den Stream, entfernt aber den Key nicht, die Gruppe überlebt das Trimming. Wer zu klein trimmt, verliert Nachrichten, sieht aber kein NOGROUP.
Auffällig war dieser Fehler nur, weil jemand ins Log geschaut hat. Der Worker-Prozess lief die ganze Zeit weiter, Supervisor war zufrieden, und ein Healthcheck auf "Prozess vorhanden" hätte grün gemeldet. ShopSignal prüft stattdessen, ob die Queue tatsächlich abgearbeitet wird, und behält Redis, OPcache und Elasticsearch im Blick. Wenn der Stream verschwindet, bekommst du eine Meldung, statt es drei Tage später an den fehlenden Bestellmails zu merken.