Development | Symfony2 und RabbitMQ

RabbitMQ zur verteilten Bearbeitung von Aufgaben und Entkopplung von Applikationsteilen
Kommentare

Eine PHP-Webanwendung wird innerhalb eines HTTP-Requests ausgeführt. Ein Request hat eine endliche Laufzeit, die zumeist durch Konfiguration des HTTP-Servers, der Script-Engine oder einer Nutzerinteraktion (z. B. eines Abbruchs) begrenzt ist. Diese „natürliche“ Grenze verhindert die Ausführung von langlaufenden, nebenläufigen oder verknüpften Operationen wie bspw. einem Videoupload, der Ausführung einer Kette von abhängigen Prozessen mit möglichst hoher Ausfallsicherheit in jedem Prozess oder verteiltem Logging.

Eine wirkliche parallele und asynchrone Berechnung ist mit dem HTTP-Request-Konzept nicht ohne Weiteres umzusetzen. Die Forderung nach Nebenläufigkeit in der Anwendung wird oft gestellt, wenn zeitintensive Berechnungen oder die Verteilung auf nachgelagerte Systeme die Lösung für fachliche Anforderungen sind. Der Ablauf ist dabei meist sehr ähnlich zu dem dargestellten Beispiel eines Videouploads (Abb. 1).

Abb. 1: Ein Request wird relativ zügig abgearbeitet und stellt in diesem Fall eine zu bearbeitende Datei in ein Verzeichnis ein. Das Bearbeiten der Datei (Encoding) wird aber von einem ganz anderen Prozess übernommen

Abb. 1: Ein Request wird relativ zügig abgearbeitet und stellt in diesem Fall eine zu bearbeitende Datei in ein Verzeichnis ein. Das Bearbeiten der Datei (Encoding) wird aber von einem ganz anderen Prozess übernommen

Dabei hat der Nutzer den Vorteil, dass Berechnungen unabhängig von seinem Upload ausgeführt werden können. Es ist in der Regel nicht wichtig, wann die Berechnung erfolgt, sondern dass der Nutzer, wenn sie erfolgt ist, benachrichtigt wird. Auch in PHP-Applikationen kann mithilfe verschiedener Werkzeuge diese Nebenläufigkeit oder gleichzeitige Ausführung von mehreren Prozessen erreicht werden. In unserem Unternehmen (ICANS GmbH) setzen wir RabbitMQ als Message Broker ein, um mittels Nachrichten (Messages) Prozesse, die auf unterschiedlichen Systemen laufen, anzustoßen. Dieser Artikel zeigt, wie wir Anforderungen an Nebenläufigkeit mithilfe von RabbitMQ gelöst haben und auftretenden Problemen begegnet sind.

RabbitMQ – Basics

RabbitMQ ist ein in Erlang und auf Basis des OTP-Frameworks implementierter Message Broker. Ein Message Broker verteilt Nachrichten von einem System zum anderen und kann konfiguriert werden, um Nachrichten nach bestimmten Regeln zuzustellen. RabbitMQ kann als programmiersprachenunabhängiges Übergangswerkzeug von einer Applikation zur anderen eingesetzt werden und hilft bei der Entkoppelung verschiedener unabhängiger und möglicherweise verschiedenartiger Applikationen, dem asynchronen Bearbeiten von Aufgaben in einer Prozesskette oder bei der Anwendung des Publish/Subscribe-Patterns.

Ausfallsicherheit und Datenintegrität sind Teil der Architektur von RabbitMQ. So kann zum Beispiel für jede Nachricht entschieden werden, ob der Erhalt vom Empfänger dem sog. Publisher bestätigt werden muss, oder ob ein „Fire and forget“-Verhalten ausreichend ist. Zur Ausfallsicherheit können mehrere Instanzen von RabbitMQ-Server zu einem Cluster zusammengeschaltet werden. Dabei ist ein Ausfall eines einzelnen Servers unproblematisch, da alle Queues auf allen Servern des Clusters gespiegelt werden können. Das Clustermanagement ist dabei so intelligent, dass der Verlust einer Nachricht sehr unwahrscheinlich ist. Als RabbitMQ in unserem Unternehmen Gearman für neue Projekte abgelöst hat, waren unsere Administratoren bei einer Vorstellung der Features von RabbitMQ bezüglich Ausfallsicherheit und Datenintegrität sofort begeistert.

Zur Kommunikation verwendet RabbitMQ das AMQP-Protokoll zwischen Client, Broker und Consumer.

AMQP

Das Advanced Message Queuing Protocol (AMQP) ist ein binäres programmiersprachenunabhängiges Kommunikationsprotokoll wie zum Beispiel FTP, POP oder IMAP. Es wurde ursprünglich für und von Firmen der Finanzindustrie entwickelt. Es wurde recht schnell deutlich, dass das Protokoll zur Kommunikation von Applikationen über Systemgrenzen hinweg hervorragend geeignet ist. Ein großer Vorteil dabei ist, dass der Produzent einer Nachricht nicht wissen muss, wie viele Konsumenten mit welchem Zweck es gibt.

RabbitMQ ist ein AMQP-Server, der das AMQP-Protokoll v0.9 implementiert. Exchanges, Bindings und Queues werden innerhalb des Brokers genutzt, um eingehende Nachrichten nach zuvor konfigurierten Regeln abzulegen, zu verteilen und zu verarbeiten.

Message Flow

Der Producer erzeugt eine Nachricht mit allen Inhalten und Nachrichtenoptionen, gibt der Nachricht einen Routing Key (mit Ausnahme bei Verwendung der Fanout Exchange) und sendet sie an eine Exchange des Message Brokers. Die Exchange nimmt die Nachricht entgegen und leitet sie abhängig von Kriterien, die in den Bindings definiert sind, sowie den verwendeten Exchange Type an eine oder mehrere Queues weiter. Letztlich kann dann ein Consumer, der auf eine Queue registriert ist, die Nachricht annehmen und verarbeiten (Abb. 2).

Abb. 2: Message Flow

Abb. 2: Message Flow

Exchange Types

Innerhalb von AMQP sind drei Standard-Exchange-Typen definiert, die näher betrachtet werden. Zusätzlich zu diesen drei Exchange-Typen könnten eigene Exchanges definiert werden, die beispielsweise anhand einer komplexen Regel für den Inhalt einer Nachricht (und nicht eines Routing Keys) eine Nachricht zustellt. In der Regel reichen allerdings die Möglichkeiten der drei Standard-Exchange-Typen aus, um eine Vielzahl an Anforderungen zu erfüllen.

Fanout Exchange

Mit der Fanout Exchange (Abb. 3) werden die eingehenden Nachrichten an alle verbundenen Queues gesendet. Die Nachrichten werden dabei nicht über Binding-Konfigurationen in bestimmte Queues sortiert.

Abb. 3: Fanout Exchange

Abb. 3: Fanout Exchange

Direct Exchange

Bei der Direct Exchange (Abb. 4) wird anhand eines vom Producer erstellten Routing Keys in einem Binding einer Queue zur Exchange entschieden, in welche Queue die Nachricht gesendet wird. Dies passiert genau dann, wenn der Routing-Key dem Binding-Key entspricht.

Abb. 4: Direct Exchange

Abb. 4: Direct Exchange

Topic Exchange

Die Topic Exchange (Abb. 5) ist sehr ähnlich zur Direct Exchange mit dem Unterschied, dass die Routing und Binding Keys zusammengesetzt sein können. Dabei ist ein „.“ das Trennzeichen zwischen den Tupeln eines Keys. Zusätzlich können die Binding Keys auch spezielle Sammelzeichen (Wildcards), * und #, beinhalten, die dann ein Wort (*) oder mehrere Worte (#) vor oder nach einem Trenner ersetzen. Beispiele: *.usa.stocks, gold.*.stocks, #.stocks, gold.#.

Abb. 5: Topic Exchange

Abb. 5: Topic Exchange

Plug-ins

RabbitMQ kann mittels Plug-ins erweitert werden, die neben einer einfacheren Wartung auch eine erweiterte Funktionalität bieten. Zur Verwaltung der Plug-ins kann das rabbitmq-plugins-Konsolenprogramm verwendet werden. rabbitmq-plugins list listet alle registrierten Plug-ins auf, die dann mit dem Befehl rabbitmq-plugins enable/disable aktiviert bzw. deaktiviert werden können. Für Einsteiger ist besonders das Management-Plug-in hilfreich, während die Shovel-Erweiterung für komplexere Aufgaben geeignet ist.

Management

Das Management-Plug-in (Abb. 6) bietet neben einem HTTP-API eine Webschnittstelle zur Verwaltung eines RabbitMQ-Brokers. Das Plug-in ist in der Standarddistribution von RabbitMQ enthalten und kann mittels rabbitmq-plugins enable rabbitmq_management aktiviert werden. Es verwendet den MochiWeb-Server (https://github.com/mochi/mochiweb), der Erlang in der Version >= R13B01 benötigt.

Abb. 6: Die Weboberfläche des RabbitMq-Management-Plug-ins zeigt einfache Statistiken an

Abb. 6: Die Weboberfläche des RabbitMq-Management-Plug-ins zeigt einfache Statistiken an

Die Webschnittstelle bietet eine Verwaltung der Nutzer, Virtual-Hosts, Queues und Exchanges des aktuellen Brokers an. Zudem werden einfache Statistiken bereitgestellt, und die Inhalte einzelner Queues können bearbeitet werden. Neben dem Publizieren neuer Nachrichten können in der Queue befindliche Nachrichten angezeigt oder die Queue geleert werden. Hierbei ist problematisch, dass Nachrichten nicht formatiert dargestellt werden können, und dass die Auswahl, welche Nachrichten angezeigt werden sollen, bis auf die Anzahl nicht eingeschränkt werden kann.

Shovel

Mittels des Shovel-Plug-ins ist es möglich, Nachrichten einer Queue eines Brokers – die sog. source – auf eine Exchange eines anderen Brokers – die sog. destination – zu transferieren. Das Plug-in ist ebenfalls in der Standarddistribution von RabbitMQ enthalten und kann über rabbitmq-plugins enable rabbitmq_shovel aktiviert werden. Zusätzlich kann das rabbitmq_shovel_management-Plug-in aktiviert werden, das eine einfache Statusanzeige der konfigurierten Shovels in der Managementoberfläche anbietet. Allerdings wird leider nicht die komplette Konfiguration der Shovels angezeigt, was die Fehlersuche erschwert, da diese nur in den Konfigurationsdateien eingesehen werden kann.

Shovels ermöglichen eine hohe Entkoppelung von Systemkomponenten. Produziert eine Webanwendung bspw. Log-Messages, entstehen potenziell sehr große Datenmengen. Sollen diese verarbeitet werden, macht es Sinn, sie in einem separaten System abzuarbeiten, das aus Sicherheitsgründen ggf. in einem separaten Netzwerk läuft. Nun kann die Webanwendung Nachrichten bei einem lokalen Broker publizieren, die durch eine Shovel in einen Broker in dem Zielnetzwerk weitergeleitet werden.

RabbitMQ – Nutzungsbeispiele

Wie zuvor erwähnt, wird RabbitMQ bei der ICANS GmbH in diversen Systemen als Message Broker genutzt. Um eine Vorstellung der Möglichkeiten von RabbitMQ zu bekommen, werden im Folgenden nun einige Beispiele für einen Einsatz vorgestellt. Zunächst wird der Upload von Videos betrachtet und anschließend eine Abstraktion von aufeinanderfolgenden Prozessschritten beschrieben, die eine nebenläufige Verarbeitung von Reports erlaubt. Abschließend wird ein verteiltes Logging basierend auf RabbitMQ vorgestellt.

Videoupload

Werden auf einer Webseite Videos verwendet, besteht in der Regel der Wunsch, sie in einem einheitlichen Format vorliegen zu haben. Wird Nutzern die Möglichkeit gegeben, eigene Videos hochzuladen, kann dies in sehr unterschiedlichen Formaten geschehen, die in das einheitliche Format umgewandelt werden müssen. Um zudem eine hohe Verfügbarkeit der Videos zu gewährleisten und beim Streamen Last von den eigenen Server zu nehmen, bietet sich zudem die Nutzung von Cloud-Diensten wie Amazon S3 an.

Soll dieser Anwendungsfall umgesetzt werden, wird deutlich, dass die Encodierung und der Upload von Videos zu Drittanbietern enorme Zeit in Anspruch nehmen kann. Es ist klar, dass ein Webseitennutzer nach dem Videoupload nicht auf eine sequenzielle Abarbeitung warten kann. Um dies dennoch umzusetzen, bietet sich eine parallele Verarbeitung der Videodaten an: Lädt ein Nutzer ein Video hoch, kann diesem, nachdem innerhalb der PHP-Anwendung alle relevanten Videodaten gespeichert und eine Nachricht an einem RabbitMQ Broker versendet wurden, mitgeteilt werden, dass er benachrichtigt wird, sobald sein Video verfügbar ist. In einem parallel zur Webanwendung laufenden Consumer kann nun das Video verarbeitet und auf den Drittanbieter verteilt werden. Nach der Verarbeitung kann der Nutzer benachrichtigt werden, dass sein Video verfügbar ist.

Durch diese Architektur kann Last von der Webseite genommen werden, und der Nutzer kann ohne Wartezeiten weiterarbeiten. Zudem wäre es möglich, zu Spitzenzeiten, in denen viele Videos verarbeitet werden müssen, mehrere Consumer parallel laufen zu lassen.

Abstraktion von Schritten in einer Chain

Sollen Daten externer Partner verarbeitet werden, gibt es in der Regel immer wiederkehrende Herausforderungen. Vereinfacht können diese in einer Abfolge von Verarbeitungsschritten abgebildet werden. Dabei müssen Daten beim Partner extrahiert und in ein internes Format encodiert werden. Anschließend können Datenvalidierungen sowie Plausibilitätsprüfungen durchgeführt werden, um die Daten abschließend je nach Bedarf verarbeiten zu können. Einzelne Schritte können je nach Daten aufwendiger sein als andere und somit die Verarbeitung blockieren.

Betrachtet man die einzelnen Schritte als vollständig entkoppelte Anwendungen, kann dies durch den Einsatz von RabbitMQ verhindert werden. Hierbei laufen die einzelnen Anwendungen als eigenständige Instanzen und kommunizieren mittels Messages mit den jeweils vor- bzw. nachgelagerten Verarbeitungseinheiten. Hat die Extraktion Daten eingelesen, wird anschließend eine Nachricht an die Encoding-Queue gesendet. Diese kann durch eine Encoding-Instanz verarbeitet werden, die anschließend wiederum eine Nachricht in der nächsten Queue publiziert usw.

Diese Entkoppelung ermöglicht eine nebenläufige Verarbeitung von Programmlogiken. Ist bspw. bekannt, dass die Plausibilitätsprüfung ein langläufiger Prozess ist, können einfach mehrere solcher Instanzen dieses Schritts gestartet werden, wobei jede Instanz eine Nachricht der vorherigen Queue selbstständig verarbeiten kann.

Verteiltes Logging

Businesskritische Entscheidungen basieren in allen Geschäftsbereichen auf einer komplexen Analyse von gesammelten Daten. Dies gilt auch für professionelle Webapplikationen, für die Entscheidungen von Stakeholdern getroffen werden. Die Grundlage für diese Entscheidungen bilden Nutzungsdaten der Kunden der Webapplikationen. Ein sehr bekanntes Beispiel ist die Analyse des Kaufabbruchs bei großen Onlineshops. Der Prozess des Kaufens wird, basierend auf den Daten zum Kaufabbruch, so weit optimiert, dass die Zahl der Kaufabbrüche geringer wird. Zusätzlich zu den Nutzungsdaten sind auch Fehlerdaten interessant. Diese sammeln und analysieren zu können, ist für das Bugfixing und die Weiterentwicklung der Webapplikation immens wichtig. In beiden Fällen darf die Auslieferungszeit der Seite nicht großartig beeinträchtigt werden.

Innerhalb von Symfony2 steht mit Monolog ein umfangreicher Logger zur Verfügung, der so erweitert werden kann, dass ein Logging zu RabbitMQ möglich ist. Damit kann dann eine RabbitMQ-Instanz auf jedem Webserver laufen und die Applikation schnell zum lokalen Broker loggen. Dieser wiederum verwendet ein Plug-in (Shovel), um die Lognachrichten zu einer zentralen RabbitMQ-Instanz (Cluster) weiterzuleiten. Daran sind dann Consumer angeschlossen, die die Daten entsprechend weiterverarbeiten können, um zum Beispiel Fehlernachrichten in eine zentrale Graylog-Instanz zu senden oder im HDFS abzulegen, damit man später die Daten per MapReduce analysieren kann.

Für einen Request sollen zusätzlich möglichst alle Daten zusammengefasst werden können, damit Bugfixing und die Zuordnung von Aktionen zu Usern einfacher ist. Innerhalb der ICANS GmbH verwenden wir dafür eine so genannte Pulse-ID, die in einem dem Kunden sehr nahen System (Load Balancer) erstellt wird und sich durch alle Server-(Apache, MySQL, MongoDB, elasticsearch usw.) und Applikations-Logs hindurchzieht. Anhand dieser sind dann alle Aktionen und ggf. Probleme einem Request zuzuordnen.

Verschiedene Produzenten können nun diese Pulse-ID nutzen, mit im Message-Body einfügen und über einen AMQP-Client eine Nachricht an eine lokale RabbitMQ-Instanz senden.

Der Vorteil dabei ist, dass sowohl Fehler-Logging als auch Event-Logging von jedem System über ein- und dasselbe Protokoll erfolgt. Das heißt, dass es nur eine Logging-Infrastruktur für alle Arten von Log-Nachrichten gibt (Abb. 7).

Abb. 7: Logging-Infrastrukturs

Abb. 7: Logging-Infrastrukturs

Für Symfony2 gibt es ein IcansLoggingBundle, das eine PHP-Bibilothek (IcansLoggingComponent) nutzt,die auf Monolog aufsetzt und eine einfache Möglichkeit bietet, aus einer PHP und/oder Symfony2-Applikation für das Logging auf RabbitMQ zuzugreifen.

Symfony2 und RabbitMQ

Mit Symfony2 ist ein PHP-Framework erschienen, das sehr modular designt ist. Es gibt viele Schnittstellen, um in einen Request-Ablauf einzugreifen und Veränderungen an Objekten ohne harte Abhängigkeiten zum Framework durchzuführen. Zudem bietet Symfony2 einfache Möglichkeiten, neue Funktionalitäten zu einem Projekt hinzuzufügen. Dabei sind diese Funktionalitäten in eigenen Modulen den sog. Bundles untergebracht. Verschiedene Ebenen des Frameworks können in einem Bundle genutzt werden, um das Bundle zu konfigurieren oder auf bestimmte Laufzeitevents zu reagieren. Die Liste der verfügbaren Bundles ist sehr lang. Die Entscheidung zur Nutzung von Drittanbieter-Bundles birgt viele Chancen:

  • Schnelles Hinzufügen von neuer Funktionalität zum Projekt
  • Größere Nutzer- und Entwicklerbasis zur Weiterentwicklung des Bundles
  • Erweiterbarkeit, da (meist) Open Source
  • Securityupdates (bei gepflegten Sourcen)

und einige Risike:

  • Bundle wird vom Hauptentwickler nicht mehr weiterentwickelt
  • Bundle wird nicht mehr in der neuen Frameworkversion weiterentwickelt
  • Securityupdates

Im Folgenden wird zunächst das OldSound/RabbitMqBundle vorgestellt, das eine Schnittstelle zwischen Symfony2 und RabbitMQ bereitstellt. Anschließend werden einige Probleme und Lösungen bei der Verwendung von RabbitMQ und PHP beschrieben.

Ein Bundle vorgestellt – OldSound/RabbitMqBundle

Das OldSound/RabbitMqBundle bietet eine einfache Einbindung von RabbitMQ in eine Symfony2-Anwendung. Für die Kommunikation wird hierbei die php-amqplib-Bibliothek verwendet. Nach der Installation lassen sich Connections, Producer und Consumer in der Konfigurationsdatei unter dem Punkt old_sound_rabbit_mq der jeweiligen Anwendung definieren. Die Konfiguration in Listing 1 definiert jeweils eine solche Instanz.

Listing 1

old_sound_rabbit_mq:
    connections:
        default:
            host:      'localhost'
            port:      5672
            user:      'guest'
            password:  'guest'
            vhost:     '/'
    producers:
        upload_video:
            connection: default
            exchange_options: {name: 'upload-video', type: direct}
    consumers:
        upload_video:
            connection: default
            exchange_options:   {name: 'upload-video', type: direct}
            queue_options:      {name: 'upload-video'}
            callback:                upload_video_service

Das Bundle verwendet die Symfony2-Dependency-Injection-Implementierung zur Verwaltung der jeweiligen Instanzen. Alle definierten Producer bzw. Consumer können mittels des Serviceschlüssels old_sound_rabbit_mq.upload_video_producer bzw. old_sound_rabbit_mq.upload_picture_consumer verwendet werden. Der bei Consumern definierte callback stellt wiederum einen Service dar, der aufgerufen wird, sobald der Consumer eine Nachricht abarbeitet. Dieser Service muss das Consumer-Interface implementieren und enthält die jeweilige Businesslogik, die bei einer aufkommenden Nachricht ausgeführt werden soll. Solche Consumer können mittels ./app/console rabbitmq:consumer gestartet werden. Dies kann manuell aber auch als regelmäßiger Task ausgeführt werden.

Probleme und Lösungen mit PHP-Consumern

Obwohl der Einstieg in die Verwendung von RabbitMQ im PHP- bzw. im Symfony2-Umfeld durch das vorgestellte Bundle relativ leicht fällt, gibt es einige Probleme. Im Folgenden werden zunächst einige allgemeine Probleme und mögliche Lösungen dafür vorgestellt. Anschließend werden einige praxisbezogene Probleme näher beschrieben.

Generelle Probleme

Der Einsatz von PHP als Message-Producer und -Consumer erscheint im Umfeld von Webapplikationen, die hauptsächlich mit PHP umgesetzt sind, als logischer Schluss. Speziell beim Einsatz des Symfony2-Frameworks im Projekt und der Möglichkeit, das RabbitMqBundle zu nutzen, muss auf die generellen Probleme von PHP als Message-Consumer hingewiesen werden.

Damit die Consumer in bestimmten Intervallen die Queues anfragen können, müssen sie entweder von außen, zum Beispiel über einen Cronjob aufgerufen werden, oder sie steuern in sich die Intervalle zum Abfragen neuer Nachrichten. Das kann nur dann funktionieren, wenn die Programme permanent laufen und zum Beispiel über eine Schleife gesteuert die Nachrichten abfragen.

Die Steuerung von außen ist eine weitere Abhängigkeit zu einem Dienst, die eigentlich nicht notwendig ist, und die Gesamtapplikation komplexer macht, da ggf. immer sichergestellt sein muss, dass die Consumer die Nachrichten aus der Queue abrufen.

Die Steuerung aus dem Consumer heraus ist das, was man nutzen möchte. Dabei gibt es in PHP leider ein großes Problem: Langlebige PHP-Prozesse allozieren immer mehr Speicher. Bei der Nutzung von PHP-Consumern muss also beachtet werden, dass die Programme nur eine begrenzte Zeit laufen können, ohne den RAM-Speicher aufzubrauchen. In vielen Fällen reicht es aus, die Gesamtzahl der bearbeiteten Nachrichten als Limit in das Programm zu geben. Aber selbst dann stellt sich die Frage, wie der Consumer wieder neu gestartet werden kann, wenn die Gesamtzahl erreicht wurde. Außerdem ist ein zu hoch gewähltes Limit wieder gleichbedeutend mit zu großem Speicherverbrauch. Ein weiteres Problem wird durch ein Limit alleine gar nicht gelöst: Was passiert, wenn ein PHP-Consumer abstürzt?

Bei der ICANS GmbH haben wir zwei verschiedene Lösungen für die skizzierten Probleme gefunden. Die erste Lösung ist recht trivial: Ein externes Programm überwacht den oder die PHP-Consumer und startet sie neu, wenn sie sich selbst beendet haben oder aufgrund eines Fehlers beendet wurden. Dieses externe Programm ist supervisord [19]. Es bringt eine Konsole mit, mit der der Prozessstatus überwacht werden kann.

Konfigurationen können in Gruppen zusammengefasst werden, damit sie einfacher zu handhaben sind. Eine Konfigurationsdatei für supervisord könnte so aussehen wie in Listing 2.

Listing 2


[group:ecg]
programs=userService-propagateHealthState

[program:userService-propagateHealthState]
command=/var/www/application/current/app/console icans:userService:propagateHealthState
directory=/var/www/application/current
numprocs=1
startsecs=5
autorestart=true
autostart=false
user=php-process

Es wird eine Gruppe ecg definiert, der ein Programm userService-propagateHealthState angehört. Dieses Programm besteht aus:

  • dem Ausführungskommando (command)
  • einem Verzeichnis (directory), in dem das Kommando ausgeführt werden soll
  • die Anzahl der Prozesse (numprocs) dieses Kommandos
  • einem Wert (startsecs), der beschreibt, wie lange supervisord warten soll bevor eine Fehlermeldung mit dem Hinweis, dass das Programm nicht gestartet werden konnte, erstellt wird
  • autorestart konfiguriert, ob bei einem Fehler oder nach Beendigung des Programms supervisord dieses Programm neu starten soll
  • autostart meint, dass supervisord beim Starten auch dieses Programm startet
  • user ist der User (Unix), unter dem das Programm ausgeführt werden soll

Ein Monitoring der Programme ist über eine HTTP-Schnittstelle oder über ein Kommandozeilenprogramm (supervisorctl) möglich. Über die Kommandozeile sieht der Status so aus:

 sudo supervisorctl ecg:userService-propagateHealthState RUNNING  pid 6615, uptime 4 days, 6:27:52 

Innerhalb des Programms können nun mit den Befehlen stop ecg: oder start ecg: alle Programme einer Gruppe gestoppt oder gestartet werden. Der Supervisor wird das Programm immer wieder neu starten, wenn es abgestürzt ist oder beendet wurde. Es hat sich in unserem Set-up herausgestellt, dass eine Konfiguration von siebzig Durchläufen für einen PHP-Consumer gut funktioniert.

Die zweite Lösung ist die Verwendung einer komplett anderen Technologie. Hier wird ein Erlang-Programm als Dispatcher zwischen Queue und PHP verwendet. Die Aufrufe der PHP-Programme erfolgen dann über das Fast-CGI-Protokoll. Diese Lösung ist eleganter, aber auch komplexer als die Überwachung von PHP-Prozessen per supervisord

Spezifische Probleme mit RabbitMqBundle

Das RabbitMqBundle verhält sich bei einer fehlerhaften Konfiguration nicht sehr fehlertolerant. In der Regel werden konfigurierte RabbitMQ-Producer bzw. Queues erstellt, falls sie noch nicht auf dem Zielbroker existieren. Existiert auf dem Broker mit dem konfiguriert eindeutigen Namen schon eine Queue und sollte die Konfiguration in Anwendung nur in einem kleinen Detail von der definierten Queue abweichen, ruft dies eine Ausnahme innerhalb des Bundles hervor. Dies kann gerade zu Beginn der Entwicklung eines Projekts zu einigem Kopfzerbrechen führen, und da einem zunächst keine Ansatzpunkte für das Verhalten gegeben werden, um mit diesem Umstand umgehen zu können, sollten Entwickler sich Gedanken um Graceful Degradation machen. Zudem ist gerade bei intensiver RabbitMQ-Nutzung eine umfassende Dokumentation der Konfiguration auf Anwendungs- und Brokerseite unabdingbar. Besonders bei vielen unterschiedlichen Systemen wie Entwicklungsrechner, Test-, Stage- und Livesystemen können sich schnell Probleme bei den einzelnen Konfigurationen einschleichen.

Wie zuvor erwähnt, kann RabbitMQ genutzt werden, um langläufige Prozesse parallel zu der Hauptanwendung ausführen zu können. Ein möglicher Anwendungsfall ist hierbei bspw. das Neuberechnen von Zugangsdaten, die auf einer komplexen Struktur basieren. Um dies umzusetzen, kann innerhalb der Anwendung jedes Mal, wenn sich die besagte Struktur ändert, eine Nachricht an einen Message Broker gesendet werden. Zur Abarbeitung dieser Nachrichten ist wiederum ein Cosumer zuständig. Dieser löscht in einem ersten Schritt die Queue, in der die Nachrichten liegen. Anschließend wird die Neuberechnung durchgeführt, und das Ergebnis wird in einem gemeinsamen Speicher abgelegt. Die execute-Methode eines solchen Consumers kann wie in Listing 3 aussehen.

Listing 3

public function execute(AMQPMessage $message) 
{
/* @var $channel AMQPChannel */
$channel = $message->delivery_info['channel'];
$channel->queue_purge();

...
// store in shared Storage...
...

return true; // acknowledge
}

Die vorgestellte Idee setzt voraus, dass die Queue aus den zu dem aktuellen Zeitpunkt t aufgelaufenen Nachrichten tatsächlich nur eine verarbeitet und anschließend alle weiteren Nachrichten bis zu diesem Zeitpunkt t gelöscht werden. So werden keine doppelten Berechnungen durch Nachrichten, die zu t aufgelaufen sind, angestoßen. Problematisch ist allerdings, dass standardmäßig beim Aufbau der Verbindung zu RabbitMQ eine Menge von Nachrichten vorgeladen werden, was das Löschen der Nachrichten auf dem Broker hinfällig macht. Ist dies geschehen, wird die Neuberechnung zum Zeitpunkt t n-mal ausgeführt, wobei n der Anzahl der vorgeladenen Nachrichten entspricht. Man kann das Vorladen der Nachrichten mittels der Quality-of-Service-Eigenschaften des verwendeten RabbitMQ-Channels definieren [20]. Der prefetch-count legt fest, wie viele Nachrichten vorgeladen werden sollen. Setzt man diesen Wert auf 1, kann die vorgeschlagene Idee umgesetzt werden. Dies kann mit der Konfiguration aus Listing 4 durchgeführt werden.

Listing 4

 test_consumer:
            connection: default
            exchange_options: {name: 'test_exchange', type: direct}
            queue_options:    {name: 'test_queue' }
            callback: service.name
            qos_options:
                prefetch_count: 1

Die Lösung mittels der Quality-of-Service-Konfiguration ist durch ein Pull Request der ICANS GmbH in das RabbitMqBundle zurückgeflossen.

Weiterhin war die Verwendung des RabbitMqBundle mit Symfony 2.0.17 im Rahmen eines Projekts problematisch. Es sind Probleme bzgl. der Konfiguration von zusätzlichen Parametern für die Definition von Queues und Exchanges aufgetreten. Sollen Queues in einem High-Availability-Umfeld genutzt werden, kann dies mittels des Parameters x-ha-policy wie folgt definiert werden:

queue_options: {name: 'test_queue', arguments: {'x-ha-policy': ['S', 'all']}}

Sollte innerhalb des Projekts nun ein Consumer zu einer solchen High Availability Queue aufgebaut werden, ist die Anwendung mit einer Ausnahme beendet worden. Der Grund dafür ist, dass Symfony intern den Wert x-ha-policy in x_ha_policy umwandelt, was nicht mit RabbitMQ High Availability kompatibel ist. Dies kann behoben werden, indem das Laden der Konfiguration in der OldSoundRabbitMqExtension ein wenig angepasst wird. Die simple Methode in Listing 5 formatiert die Argumente in ein RabbitMQ-kompatibles Konfigurations-Array.

Listing 5

private function normalizeArgumentKeys(array $config) 
{
if (isset($config['arguments'])) {
$arguments = $config['arguments'];

$newArguments = array();
foreach ($arguments as $key => $value) {
if (strstr($key, '_')) {
$key = str_replace('_', '-', $key);
}
$newArguments[$key] = $value;
}
$config['arguments'] = $newArguments;
}
return $config;
}

Anschließend müssen in derselben Klasse alle Argumente der addMethodCall, bei denen die Konfigurationzeichenketten exchange_options oder queue_options genutzt werden, wie folgt angepasst werden:

$definition->addMethodCall( 'setExchangeOptions', array(   $this->normalizeArgumentKeys($configuration('exchange_options')) ) )

Die vorgestellten Änderungen sind bisher nicht in den Hauptentwicklungszweig des Bundles zurückgeflossen, da die Lösung eher einen Workaround als eine sinnvolle Lösung des Problems darstellt. So wäre es nicht möglich, Optionen anzugeben, die einen Unterstrich in der Definition benötigen.

Fazit

RabbitMQ und PHP ergänzen sich sehr gut, da RabbitMQ für PHP-Applikationen eine einfache Möglichkeit zur nebenläufigen bzw. verteilten Bearbeitung von Aufgaben und Entkopplung von Applikationsteilen voneinander bietet. Die Anbindung von PHP-Consumern an RabbitMQ ist bei Verwendung des RabbitMqBundles für Symfony2-Anwendungen sehr einfach, birgt aber auch einige Probleme, derer man sich bewusst sein muss, wenn PHP-Consumer genutzt werden sollen. Gerade für große Applikationen mit vielen Nachrichten und Systemkomponenten ist es wichtig, eine umfassende Dokumentation zu pflegen, um die Wartbarkeit der Systemlandschaft zu gewährleisten.

Zusätzlich zu den Standard-RabbitMQ-Features ist die Erweiterbarkeit der Funktionalität des Brokers über Plug-ins wie Shovel oder der Managementkonsole hervorzuheben. Gerade für den Einsatz im Big-Data-Bereich eignet sich RabbitMQ hervorragend, um Systembereiche voneinander unabhängig zu gestalten. Ohne diese Eigenschaften wäre unter anderem der Aufbau eines verteilten Loggings mit RabbitMQ nicht ohne Weiteres möglich.

Stehen Entwickler vor Anforderungen, die zeitintensive Berechnungen oder die Verteilung von nachgelagerten Prozessen auf heterogene Systemteile erfordern, bietet RabbitMQ Grundlagen für die Umsetzung von Lösungen. Die Einstiegshürden sind gerade in Verbindung mit Symfony2 und dem OldSound/RabbitMqBundle gering.

Aufmacherbild: violinist bunny von Shutterstock / Urheberrecht: nuanz

Unsere Redaktion empfiehlt:

Relevante Beiträge

Meinungen zu diesem Beitrag

X
- Gib Deinen Standort ein -
- or -