Blog >>> Techtalk_

Laravel Livewire in einem Kubernetes Cluster

Wir betreiben und betreuen mehrere große Laravel Anwendungen. Von einigen dieser Anwendungen laufen viele Instanzen verteilt in Kubernetes Clustern – bei Laravel Livewire gibt es ein paar Dinge zu beachten. Was genau, erfährst du hier.
10.02.2022
Simon Hansen
Simon Hansen
Entwickler, Gründer
Laravel Livewire in einem Kubernetes Cluster

Was sind LiveApps?

Möchte man eine moderne Web-Anwendung entwickeln, gibt es viele verschiedene Bereiche, in denen man sich auskennen muss. Tools wie React oder Vue.js bringen jede Menge Möglichkeiten, aber auch Komplexität mit sich.

Bereits seit ein paar Jahren entwickeln wir sogenannte LiveApps unter anderem mit Elixir und Phoenix LiveView – mehr über den PETAL-Stack erfährst du in diesem Blogbeitrag. Mittlerweile gibt es in diversen Sprachen eine Möglichkeit, Anwendungen zu entwickeln, die sich wie Single Page Applications (SPAs) anfühlen, jedoch weitestgehend ohne JavaScript auskommen. dbohdan hat diese Bibliotheken gruppiert nach Programmiersprachen aufgelistet und bei GitHub zur Verfügung gestellt. Im Elixir-Ökosystem können LiveApps mit Phoenix LiveView umgesetzt werden, bei PHP gibt es Laravel Livewire. Die Funktionsweise und die dadurch möglichen Funktionen dieser Bibliotheken unterscheidet sich dabei teilweise massiv.

Laravel Livewire & Kubernetes

In diesem Jahr haben wir die sogenannte Techniker-Ansicht für unseren langjährigen Kunden SEPP.One® entwickelt. Die Techniker-Ansicht ist eine Erweiterung von SEPP.One® und wird somit auf der gleichen Infrastruktur, einem Kubernetes Cluster, laufen. In einem Kubernetes Cluster können mehrere Instanzen einer Anwendung betrieben und dynamisch skaliert werden. Auf welcher Hardware die einzelnen Anwendungen laufen, ist dabei abstrahiert.

Als wir die letzten Tests abgeschlossen hatten und ein Release der Techniker-Ansicht erstellt hatten, mussten wir ein sehr ungewöhnliches Verhalten auf der Produktiv-Umgebung feststellen – manche Komponenten haben sich zufällig neu gerendert, obwohl sich die Daten nicht geändert hatten. Eine Regelmäßigkeit konnte im ersten Moment nicht festgestellt werden.

Wie konnte das überhaupt passieren? Jedes Feature wird lokal bei der Entwicklung getestet, bei jeder Änderung automatisch in der GitLab-Pipeline durch Software-Tests überprüft und anschließend auf eine Testumgebung (Staging-Umgebung) gespielt. Eine Staging-Umgebung sollte exakt gleich konfiguriert sein, wie die Produktiv-Umgebung – in der Regel mit etwas weniger Leistung und außerdem enthält sie ausschließlich anonymisierte Testdaten. Auf dieser Umgebung können die Entwickler:innen und der Kunde die Qualitätssicherung vornehmen. Die neue Techniker-Ansicht hatte all diese Schritte bereits durchlaufen. Also wie konnte es passieren, dass ein nicht funktionsfähiger Stand ausgespielt wurde?

Es hat sich herausgestellt, dass sich die Staging- und Produktiv-Umgebungen in einem Punkt unterschieden haben. In der Staging-Umgebung lief nur eine Instanz der Anwendung, in der Produktiv-Umgebung mehrere – ein erster Hinweis.

Wenn bei einem Deployment ein Container neu startet, werden die Laravel Blade Views kompiliert und via php artisan view:cache ein Cache erzeugt. Der erzeugte Cache wird standardmäßig unter storage/framework/views abgelegt und kann mit ls -l storage/framework/views aufgelistet werden:

f802e0c02fbcd03f2ac105789f2109201b9b9450.php
f877ee5b7d6804a43991688050e581decbf2dd3a.php
fcad2365c73746ef1260585d33b15be680299a7b.php
...

Teile dieser gecachten PHP-Dateien sehen wie folgt aus:

<?php

if (! isset($_instance)) {
    $html = \Livewire\Livewire::mount('components.search-bar', [])->html();
} elseif ($_instance->childHasBeenRendered('j8rMB72')) {
    $componentId = $_instance->getRenderedChildComponentId('j8rMB72');
    $componentTag = $_instance->getRenderedChildComponentTagName('j8rMB72');
    $html = \Livewire\Livewire::dummyMount($componentId, $componentTag);
    $_instance->preserveRenderedChild('j8rMB72');
} else {
    $response = \Livewire\Livewire::mount('components.search-bar', []);
    $html = $response->html();
    $_instance->logRenderedChild('j8rMB72', $response->id(), \Livewire\Livewire::getRootElementTagName($html));
}
echo $html;

Wir kommen der Sache näher. In diesem Code-Schnipsel wird überprüft, ob eine Child-Komponente einer Livewire Komponente bereits dargestellt wird, um diese vor einem erneuten Rendern zu bewahren. Als Identifier wird der Hash j8rMB72 verwendet, welcher ebenfalls während der Cache-Erzeugung, also bei Container-Start, generiert wird. Und genau hier liegt das Problem.

Der Hash wird während des Aufbaus des Caches erzeugt. Läuft die Anwendung verteilt auf mehrere Container, wird in jedem Container ein neuer View Cache und somit ein anderer Hash erzeugt.

Eindeutige Keys pro Komponente

Um zu verhindern, dass die Livewire Komponenten durch Hashes identifiziert werden, kann einer Komponente mit wire:key ein eindeutiger Name zugewiesen werden. Der erzeugte View Cache sieht dann wie folgt aus:

<?php

if (! isset($_instance)) {
    $html = \Livewire\Livewire::mount('components.search-bar', [])->html();
} elseif ($_instance->childHasBeenRendered('tool-instance-search-bar-settings')) {
    $componentId = $_instance->getRenderedChildComponentId('tool-instance-search-bar-settings');
    $componentTag = $_instance->getRenderedChildComponentTagName('tool-instance-search-bar-settings');
    $html = \Livewire\Livewire::dummyMount($componentId, $componentTag);
    $_instance->preserveRenderedChild('tool-instance-search-bar-settings');
} else {
    $response = \Livewire\Livewire::mount('components.search-bar', []);
    $html = $response->html();
    $_instance->logRenderedChild('tool-instance-search-bar-settings', $response->id(), \Livewire\Livewire::getRootElementTagName($html));
}
echo $html;

Der Hash wurde durch den von uns gewählten wire:key ersetzt. Somit ist der Identifier der Komponenten auf allen Instanzen gleich.

In Zukunft werden wir dafür sorgen, dass die Staging-Umgebung ebenfalls redundant betrieben wird, damit Fehler dieser Art zukünftig frühzeitig im Entwicklungsprozess auffallen.

Alternativ können die Laravel View Caches auch beim Erzeugen des Docker Images generiert werden; macht das Image größer, verbessert aber die Startzeit des Containers.

Teilen @
Lust auf was Neues?
Aktuelle Stellenangebote >>>