Warum ich von Kubernetes & Co. auf normales Webhosting wechselte

In diesem Artikel möchte ich meinen „Weg“ von einer „containerisierten“ Web-App zurück zu einer standard Web-App auf einem standard Webhosting Paket beschreiben, sowie die Gründe, welche mich dazu gebracht haben.

Dabei geht es um meine – mittlerweile fast 14 Jahre alte – Webseite mitbringen.net, welche im Laufe der Jahre schon einige technische Änderungen mitgemacht hat und die ich noch immer aktiv weiter entwickle. Auf der Webseite sind täglich zwischen 10 und 40 Benutzer gleichzeitig unterwegs.

Auf mitbringen.net kann man im Prinzip kostenlos, mit nur 2 Klicks und ohne Anmeldung eine Mitbringliste für eine beliebige Veranstaltung erstellen und an dieser mit anderen Leuten zusammen arbeiten. Sie hilft bei der Organisation von Schulfesten, Sportfesten, Turnieren, Gartenparties, Geburtstagen und vieles mehr und erfreut sich seit Jahren wachsender Beliebtheit, vor allem bei Schulen und Vereinen.

Warum?

Zunächst möchte ich klar stellen, dass ich kein Gegner von Container-basierten Anwendung oder der Microservice Architektur bin – im Gegenteil: Ich habe erst kürzlich mein Zertifikat als „Certified Kubernetes Application Developer (CKAD)“ von der Linux Foundation bzw. der Cloud Native Computing Foundation (CNCF) erhalten. Das Arbeiten mit Docker und vor allem Kubernetes macht mir sehr viel Spaß und ich würde es gerne für eines meiner Projekte produktiv nutzen. Ausserdem arbeite ich bei einem Cloud-Anbieter und nicht bei einem Webhoster.
Dennoch habe ich mich für normales Webhosting entschieden – und sogar meine bereits vor ca. 5 Jahren auf Container-Architektur umgebaute Webseite wieder zurückgebaut, so dass sie auf einem „normalen“ Webhosting läuft.

Ressourcen Überhang

Ich hatte mir schon länger Gedanken darüber gemacht, ob es für meine Webseite nicht ein bisschen übertrieben ist, für den Release einer kleinen Änderung im PHP-Code einen knapp 500MB großen Docker Container in der Github Pipeline zu bauen und diesen dann in die Docker Registry zu pushen und den Server diesen über das Internet wieder herunterladen zu lassen.
Ich hatte zu dieser Zeit immer einen Container mit der PHP Software und den benötigten Erweiterungen sowie dem PHP-Code der Webseite gebaut und ausgeliefert – sicherlich hätte man das sogar noch besser strukturieren, und den Webseiten-Code vom PHP Container trennen können. Dennoch „verschifft“ man mit Docker Containern immer viel zusätzlichen Ballast der sich nicht unbedingt ändert. Zwar werden einzelne Bestandteile von Docker-Containern in Layern gespeichert und auch hier ist Docker ressourcen-sparend und überträgt nur möglichst die geänderten Layer. Eine Kontrolle hat man als Entwickler aber nicht darüber.

Administrations-Aufwand oder hohe Kosten

Den eigentlichen Ausschlag hat aber nicht der größere Ressourcen-Bedarf gegeben, sondern die Tatsache dass mein (damals noch Nomad-)Cluster regelmäßig Probleme gemacht hat, wenn ich gerade nicht in der Nähe meines Rechners war – also Nachts oder am Wochenende. Mal war die Festplatte voll, mal gab es Probleme mit der zugewiesenen IP Adresse usw.
Als Entwickler möchte ich mich eigentlich nicht um den Betrieb des Clusters kümmern, sondern nur um meine eigene Software. Somit wäre ein managed Kubernetes Cluster das Richtige für mich gewesen.

a container ship

Angebote gibt es viele – sowohl von den großen Hyperscalern wie Google Cloud (GKE), AWS, Microsoft Azure (AKS), als auch von kleineren Anbietern wie Digital Ocean oder Linode. Deren Angebote sind noch halbwegs erschwinglich, die meisten Anbieter sind jedoch aus den USA und ich möchte meine Seite gerne in Deutschland hosten. Einen managed Kubernetes Cluster gibt es natürlich auch in Deutschland, z.B. bei Ionos oder gridscale. Die Preise sind aber für ein privates Hobby-Projekt, welches noch nicht richtig etwas abwirft, einfach zu teuer.
Selbst mit nur einem Node wird man monatlich zwischen 160€ und 300€ los – dafür bekommt man dann gerade einmal 2 vCores und 2GB RAM per Node. Darauf müssen dann alle Komponenten der Applikation laufen – oder man bezahlt noch mehr. In meinem Fall wäre das neben dem PHP Container noch eine mySQL Datenbank und ein Websocket Server gewesen und somit wäre der RAM schon verdammt knapp bemessen.

Die Rettung

Nun gab es beim letzten Black-Friday ja nicht nur Elektrogeräte, welche man eigentlich nicht braucht, zum Knüller-Preis sondern auch viele Webhoster haben den Tag für Angebote genutzt.
So gab es u.A. bei Netcup ein Webhosting Paket für nur 3,76€ pro Monat (diese Angebote gibt es bei Netcup öfter zu verschiedenen Anlässen (https://www.netcup-sonderangebote.de/), und auch sonst sind die Preise – auch bei anderen Webhosting Anbietern wie Hetzner sehr günstig). Im Netcup Paket (Webhosting 4000) sind enthalten: 6GB garantiert nutzbarer RAM, 250GB SSD Speicherplatz, kostenloser Traffic, MySQL, PHP (verschiedene Versionen zur Auswahl) und E-Mail Server. Alles von Profis konfiguriert, verwaltet und überwacht. Obendrein gab es noch 6 .de Domains kostenlos dazu. Aber: Kein Docker, kein Kubernetes, kein Nomad, kein Root-Zugriff auf irgendetwas. Meinen Websocket Server würde ich auch nicht mehr betreiben können, das war mir klar. Dennoch habe ich mir dieses Angebot geholt (1 Jahr Mindestvertragslaufzeit war bei dem Preis aber okay für mich) und wollte einfach mal probieren ob ich meine Webseite nicht auch dort zum laufen bekomme.

Wie?

Da meine Webseite wie bereits beschrieben schon auf Container Architektur (bzw. Microservices wenn man so will) aufgebaut war, und auch individuelle Software-Komponenten nutzte, musste ich dieses wieder zurückbauen.

Der Websocker Server

Mir war von Anfang an klar, dass das größte Problem wohl der Websocket Server werden wird. Dieser sendet Push-Benachrichtigungen an die Browser Version von mitbringen.net (es gibt noch eine Android und eine iOS App – hier laufen Push-Benachrichtigungen über Google bzw. Apple).
Der Websocket Server hält einen bestimmten Port offen, ein HTTP Proxy leitet Anfragen auf eine bestimmte URL auf diesen Port weiter (und übernimmt zudem den SSL Handshake). Einen beliebigen Port auf einem Shared Hosting zu öffnen ist natürlich nicht möglich, denn dieser wäre ja für alle Kunden offen, bzw. würde andere Kunden blockieren die auf dem selben Server arbeiten. Somit musste ich mir auch anfängliche Ideen aus dem Kopf schlagen, den Server in einen Cronjob umzubauen der – solange es der Webhoster erlaubt – den Port offen hält und dann einfach mit dem nächsten Cron-Interval neu startet.
Diesen Teil musste ich also komplett anders lösen; hier kommt nun eine Browser-seitige „Pull Abfrage“ zur Anwendung. Hier wird über ein Javascript Interval nun im 10 Sekunden Takt der Server nach neuen „Push“ Nachrichten gefragt. Das ist zwar nicht mehr 100% Echtzeit, aber noch im Rahmen.

Limits

Die neue Art des „pushens“ (nun „pullens“) führte aber zum nächsten Problem; den Limits. Nun beanspruchte ein Webseiten Benutzer nämlich mindestens 2 Datenbank Verbindungen für sich (oder mehr wenn mehrere Tabs geöffnet waren). Somit erreichte ich schnell das „max_user_connections“ Limit von 50 Connections per mySQL User. Eine Nachfrage beim Webhoster Support (auf dessen Antwort ich 3 Tage warten musste – Massenhosting halt) ob das Limit gegen Aufpreis oder in einem größeren Paket erhöht werden könne ergab, dass dies nicht möglich sei. Mehr als 50 Connections pro User gibt es nur in den „Managed“ Paketen (min. 50€ im Monat). Es wären aber 300 Connections pro Kunde möglich.
Somit stand die Lösung fest: Das Pull-Script nutzt nun einen eigenen MySQL-Nutzer. Trotzdem meldete Sentry ab und zu noch das „already has more than 'max_user_connections“ Problem. Dies passierte zu Stoßzeiten, auch wenn eher keine 50 Benutzer gleichzeitig drauf waren (aber 40) – vermutlich durch den asynchronen Aufbau der Seite – so wurden mehrere Komponenten parallel angefragt. Mein Trick: Ich erstellte einen weiteren MySQL User, per Zufall wählt das Script nun bei jedem Aufruf einen von zwei MySQL Usern aus, mit dem es die Datenbankverbindung herstellt. Somit wären theoretisch (wenn der Zufallgenerator ideal verteilt – was er nicht tut) 100 gleichzeitige Datenbank Verbindungen für das Backend möglich +50 für das Pull Script. Dieses Prinzip könnte man natürlich noch etwas ausweiten – eben bis die 300 gleichzeitigen Verbindungen pro Kunde ausgeschöpft sind. Hier muss man aber dann ganz klar sagen: Wenn die Webseite einen so starken Nutzerzuwachs hat, lohnt es sich irgendwann vielleicht doch ein Managed Kubernetes Angebot zu nutzen. Aber das wäre dann eine andere Geschichte…

Langsamer Verbindungsaufbau zu externen Services

Menschliche Füsse auf Hängematte

Das nächste Problem betraf tatsächlich die Push-Benachrichtigung an die App-Versionen, die ja eigentlich über Apple/Google laufen. Zwar kann man auch im Standard Webhosting problemlos eine Verbindung zu deren Push-Diensten über HTTPS aufbauen, der Verbindungsaufbau ist aber recht langsam (ob nun die Namensauflösung, der SSL Handshake oder die Übertragung so langsam ist, weiss ich nicht). Jedenfalls war das Nutzererlebnis doch stark getrübt wenn eine Aktion, die einen Push an ein mobiles Gerät auslöste nun recht lange Ladezeiten hatte.
Dieses Problem habe ich durch einen Cronjob gelöst, der – ähnlich wie das Pull-Script – aufgelaufene „Push“ Benachrichtigungen abruft und diese dann an die Services von Apple/Google sendet. Die Aktion vom Nutzer löst dann nur noch einen Datenbank-Eintrag aus, keinen direkten Verbindungsaufbau zum Push-Service.

Updaten bitte!

Ein weiteres „Problem“ war die – altersbedingt recht alte – bisher benutzte PHP Version von 5.6. Diese wurde von Netcup nicht mehr angeboten. Aber für ein Update auf eine neuere Version war es ohnehin höchste Zeit, also wurde der Code auf PHP 8.2 umgebaut – das war nicht allzu aufwändig.

Deployment

Das letzte Problem betraf das Deployment der Anwendungen. Hier wurden bisher per Github Actions Docker container gebaut, in die Github Registry hochgeladen und anschließend wurde Nomad auf meinem Server per Nomad API aufgefordert den aktuellen Container herunterzuladen. Ganz so modern würde es künftig nicht mehr gehen, denn Netcup bietet zum Upload von Dateien nur SCP oder FTP an. Bei SCP stellte sich schnell heraus dass dies für tausende kleine Dateien viel zu langsam ist. Also blieb nur FTP. Hier fand ich zum Glück eine fertige Github Action die einen differenziellen FTP Upload anbietet, somit dauerte zwar das erste Deployment sehr lange, aber für eine Änderung an der Webseite werden ab jetzt immer nur die geänderten Dateien hochgeladen. Dies sind meist nur ein paar Kilobyte.

Eigenschaften Container vs. Webhosting

Hier nochmal eine Gegenüberstellung der Eigenschaften von Container-basiertem Hosting und normalem Webhosting. Die Punkte des Container-basierten Webhosting passen sowohl zu Kubernetes als auch Nomad.

Eine gute Übersicht über Monolithische Anwendungen vs. Microservice gibt es auch in diesem Artikel: https://dev.to/alex_barashkov/microservices-vs-monolith-architecture-4l1m

ContainerWebhosting
Fokus auf Hochverfügbarkeit und Ersetzbarkeit einzelner Services (Microservices)
Die Microservice-Architektur ist per Definition ineffizient beim Ressourcen-Verbrauch. Alle Komponenten kommunizieren per Netzwerk untereinander und es gibt einen Überhang gleicher Software-Teile in den verschiedenen Containern. Dafür können alle Teile problemlos ausgetauscht werden und somit kann auch ein riesiges Entwicklerteam an einem einzigen Software-Stack arbeiten, pro Tag mehrere neue Versionen veröffentlichen ohne dass die Anwendung kurzzeitig offline geht. Zudem können Redundanzen geschaffen werden, welche bei Software-Fehlern in einzelnen Containern die Verfügbarkeit sicherstellen.
Fokus auf Effizienz (möglichst viele Instanzen auf einem Server)
Beim normalen Webhosting gibt es zentrale Services (z.B. PHP, mySQL…) welche von verschiedenen Anwendungen genutzt werden. Die Parameter der Software können somit optimal an die verfügbare Leistung des Servers angepasst werden und diese vollständig ausnutzen. Für die Webanwendung steht dafür nur ein Verzeichnis zur Verfügung und der gesamte Stack der Anwendung liegt zusammen und hat Abhängigkeiten zueinander (=Software Monolith). Wird ein Teil der Software aktualisiert, betrifft dies direkt alle abhängigen Software-Teile. Ein kurzer Ausfall der Webseite beim Veröffentlichen einer neuen Version kann nicht ausgeschlossen werden.
Rollendes Update möglich
Aufgrund der Microservice-Architektur sind Abhängigkeiten über definierte Schnittstellen gegeben. Somit können einzelne Services ausgetauscht werden ohne dass abhängige Services davon beeinträchtigt werden. Dies macht ein sogenanntes „rollendes Update“ möglich bei dem entweder nur ein bestimmter Teil der Benutzer eine neue Version ausgeliefert bekommt um zu testen ob diese problemlos funktioniert (Blue/green deployment) oder wenigstens die alte Version so lange weiterläuft bis die neue Version vollständig ausgerollt wurde und bereit ist. Erst dann wird die alte Version gestoppt und der komplette Traffic wird auf die neue Version geleitet (Canary deployment)
Update betrifft sofort die Live-Version
Dadurch dass alle Services auf die gleiche Dateistruktur zugreifen und keine ganzen Container ausgeliefert werden, ist die gesamte Anwendung potentiell betroffen sobald die erste Datei neu hochgeladen wird. Gibt es beim Upload Probleme kann eine inkonsistente Version das Ergebnis sein. Die Webseite kann theoretisch ausfallen sobald die erste Datei durch eine Neue ersetzt wird. Die Seite muss daher vorher in einen Wartungsmodus versetzt werden.
Freiheit bei der Wahl der Komponenten
Da der Entwickler seine Container selbst beliebig definieren kann, ist er – abgesehen von Ressourcen-Limits – völlig frei in der Wahl seiner Software Komponenten und der benutzten Versionen. Dafür muss der Entwickler seine Konstellation aber auch selbst testen.
Gebunden an vom Webhoster installierte Komponenten
Beim Webhosting sind meist bestimmte Standard-Komponenten verfügbar, mit etwas Glück sogar in verschiedenen Versionen. Darüber hinaus gibt es aber keine Möglichkeit individuelle Software einzusetzen.
Keine Limits
Abgesehen von den „natürlichen Limits“ der Nodes bzw. den vom Cluster vorgegebenen Limits kann der Entwickler die Resourcen frei auf einzelne Software Komponenten aufteilen. Soll beispielsweise der MySQL Server den Großteil an RAM nutzen können und der PHP Server kommt mit wenig aus, kann der Entwickler dies konfigurieren.
Limits vom Webhoster
Beim (shared) Webhosting sollen alle Kunden garantierte Resourcen vom Server bekommen. Es muss daher verhindert werden, dass einzelne Kunden bestimmte Ressourcen komplett für sich vereinnahmen. Somit gibt es feste Limits pro Thread oder Verbindung, die so ausgerechnet sind, dass kein Kunde zu viele Ressourcen beansprucht. Die Anzahl der Verbindungen zum mySQL Server wird so z.B. pro Kunde begrenzt, ebenso wie der maximale Arbeitsspeicher die eine PHP Anwendung reservieren darf – auch wenn der Server gerade genug Kapazitäten hätte.
Abhängig von verschiedenen Drittanbietern
Der übliche Docker Container nutzt und installiert Software-Komponenten von verschiedenen Quellen. Angefangen beim Base Image, welches von irgendeiner Docker Registry geladen wird, über Linux Software welche über einen Paketmanager nachinstalliert wird, bis zu ggf. weiteren Paketen die über weitere spezifische Paketmanager nachgeladen werden. All das wird potentiell bei jedem Container Build neu geladen und installiert. Somit hat man als Entwickler die Wahl selber bestimmte Registries zu betreiben (=weitere Kosten) bzw. die Software irgendwie selber bereitzustellen – oder man begibt sich in die Abhängigkeit der Betreiber öffentlicher Registries und Software-Datenbanken. Auch können natürlich die Anbieter bestimmter Software-Komponenten jederzeit ihre Software aus den Registries entfernen („Dependency Hell„)
Software wird vom Webhoster bereitgestellt oder mit der Anwendung ausgeliefert
Ein Nachinstallieren von 3rd Party Software ist beim Standard-Webhosting eher unüblich. Die Software wird – wenn nicht vom Webhoster bereitgestellt – mit der Anwendung hochgeladen und stammt somit aus dem eigenen Repository. Hier muss ggf. die Lizenzfrage geklärt werden, aber die Software und die Quelle liegt unter der Kontrolle des Entwicklers.
Eigene Konfiguration der Services
Man kann zwar vorgefertigte Container für die meisten Services benutzen, die meist mit einer sinnvollen standard Konfiguration kommen, welche für viele Anwendungsfälle passt. Die Abstimmung der Konfiguration muss man aber selber austesten. So können oft nachjustierungen im laufenden Betrieb nötig sein.
Vorkonfiguriert vom Webhoster
Beim Webhosting hat der Webhoster bereits alle Services vorkonfiguriert. Eine Anpassung ist meist nicht nötig (und auch nicht möglich). Die Konfiguration ist meist über lange Zeit erprobt und auch von Profis eingestellt.
Microservices vs. Monolith

Fazit

Spider webs

Nicht bei jedem Projekt lohnt es sich das zu tun was alle tun und es über Kubernetes zu betreiben. Ich habe das Gefühl dass durch den aktuellen Kubernetes Hype, viele Mini-Webseiten mit viel zu viel Overhead betrieben werden, nur weil es aktuell „State of the Art“ ist.
Für manche Projekte macht es – zumindest in der Anfangszeit – Sinn diese über ein ganz normales Webhosting Paket zu betreiben. Auch wenn diese Angebote ein eher angestaubtes Image haben – sie sind oft günstig und haben definitiv noch eine Daseinsberechtigung.

Natürlich sollte man einen Plan B im Hinterkopf haben – wächst die Benutzerbasis der Anwendung schnell, ist es über ein normales Webhosting schwierig die Ressourcen nach oben zu skalieren.

Aber für Webseiten die keine speziellen Software-Komponenten benutzen und die erst wachsen müssen, lohnt es sich, alternativ zu denken und gegen den Strom zu schwimmen. Ich habe nun jedenfalls für weniger als 4€ / Monat eine Webseite die schnell läuft und bisher keinen einzigen Ausfall hatte. Sollte es doch mal einen Ausfall geben, muss nicht ich mich um die Behebung kümmern, sondern das erledigen Webhosting Profis im 24h Notdienst.
Zwar würde ich lieber Container mit freier Software-Wahl, Kubernetes und Linux Kommandos zur Verwaltung meiner Webseite benutzen anstatt mich durch die Plesk Oberfläche zu klicken, aber die Vorteile überwiegen im Moment.

Jan

Papa, Ehemann, Webentwickler, Hobby-Bastler

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert