Dienstag, 14. Mai 2024

Cloud Serie: GCP Filestore vs. Cloud Storage - ESC Index-Applikation serverless mit Cloud Run betreiben


Die Google Cloud kennt verschiedene Werkzeuge, um Daten zu speichern. Es gibt verschiedene Datenbanken und mehrere Möglichkeiten, unstrukturierte Daten in Form von Dateien zu speichern. Jede Möglichkeit hat ihre Eigenschaften und die diesbezüglichen Vor- und Nachteile. Heute schauen wir uns an, wie man eine ESC Applikation mit einem persistenten Index serverless auf der GCP betreiben kann.

Um die Applikation serverless zu betreiben, verwenden wir Cloud Run. Bevor wir uns allerdings das konkrete Deployment mit Cloud Run anschauen können, müssen wir uns mit dem Speicher beschäftigen. Die ESC-Applikation verwendet einen Lucene-Index. Dieser ist Datei basiert, stellt gewisse Anforderungen an die API und kann sehr gross werden. Da der Index persistent sein muss und sehr gross werden kann, kommt eine Speicherung innerhalb des Cloud Rund Containers nicht in Frage. Wir brauchen eine Alternative.

Cloud Storage
Der Lucene-Index besteht aus einzelnen Dateien. Könnte man dafür also Cloud Storage verwenden? Spoiler: Nein, das geht nicht. Lucene bietet ein API an, mit welchem man eigene Storage-Implementierungen machen kann. So wäre es bspw. möglich, einen Lucene-Index in einer Datenbank zu speichern. Es gibt jedoch zwei Einschränkungen von Cloud Storage, welche eine robuste Implementierung verhindern:
  1. Cloud Storage kennt keinen Lock-Mechanismus. Es liegt nicht in der Natur dieses Speichers, dass Prozesse einzelne Objekte (Dateien) blockieren. Lucene braucht aber einen Lock-Mechanismus. Ohne diesen ist kaum abschätzbar, was die effektiven Seiteneffekte sind.

  2. Mit Cloud Storage kann man immer nur ganze Objekte (Dateien) speichern. Bei grossen Indizes ist dies bei vielen Schreiboperationen überhaupt nicht effizient. Lucene schreibt und entfernt auch nur Blöcke. Die Implementierung müsste dies simulieren, in dem jeweils das ganze Objekt gelesen, in Memory die Änderungen vorgenommen werden, und dann das ganze Objekt wieder gespeichert würde. Ineffizient und im Zusammenspiel mit dem fehlenden Lock gefährlich.
Cloud Storage kann entsprechend nicht für unseren servless Index-Storage verwendet werden. Diese Einschränkungen gelten auch, wenn FUSE verwendet wird, also ein Adapter mit welchem man Cloud Storage von der Semantik her wie ein "normales" Dateisystem verwenden könnte (Details siehe hier).

Filestore
Filestore ist ein verwaltet Dateispeicher, welcher POSIX kompatibel ist und Dateisperren sowie Nebenläufigkeit zulässt (gleichzeitiges Schreiben desselben Files). Mit der Verwendung von Filestore kann ESC ohne Anpassungen verwendet werden, da die Lucene Standardimplementierung der Storage-Klasse auch mit Filestore funktioniert. Daher werden wir im Folgenden Filestore für das serverless Deployment unserer ESC-Applikation verwenden.

Exkurs: Block-, Datei- und Objektspeicher
Wie Eingangs erwähnt bietet GCP verschiedene Speichermöglichkeiten. Diese lassen sich in die drei Kategorien: Blockspeicher, Dateispeicher und Objektspeicher einteilen. Blockspeicher (Block Storage) sind bspw. die Cloud Persistence Disks (HDD oder SSD). Ein Dateispeicher ist Filestore und ein Objektspeicher ist bspw. Cloud Storage.

ESC Beispiel-Applikation
Bevor wir eine Filestore-Instanz erstellen und unsere Applikation via Cloud Run deployen können, brauchen wir eine ESC-Applikation. Dazu schreiben wir eine ganz einfache ESC-Applikation, welche ein REST-API zur Verfügung stellt. Zu Testzwecken bietet die Applikation zwei Endpoints: Einen um eine Person hinzuzufügen (indexieren) und einen um eine Person zu suchen:

Beispiel ESC-Applikation


Cloud Run vorbereiten
Unsere Beispiel-Applikation müssen wir zu einem Assembly-JAR-File builden. Dies erleichtert anschliessend die Erstellung des Docker-Images.

Als nächstes erstellen wir ein Batch-Script, welches das Filestore-Verzeichnis mounted und die ESC-Applikation startet. Am besten wird das Script direkt im Root-Verzeichnis der Applikation erstellt, run.sh:

run.sh Bash-Script


Nun können wir das Docker-Image erstellen:

Docker-Image für ESC Beispiel-Applikation


Filestore-Instanz erstellen
Bis auf die Filestore-Instanz haben wir nun alles zusammen für unser Deployment. Die Filestore-Instanz ist einfach via CLI erstellt. Dazu brauchen wir noch einen VPC um anschliessend von Cloud Run darauf zugreifen zu können. Wir exportieren die IP-Adresse der Filestore-Instanz anschliessend als Variable und erstellen einen dedizierten Service-Account. Das ganze kann via CLI wie folgt erstellt werden (Platzhalter entsprechend ersetzen):

Filestore-Instanz mit VPC und Service-Account erstellen.

Cloud Run deployen
Nun haben wir alles zusammen und können unsere ESC-Applikation mit Cloud Run deployen (Platzhalter anpassen):

Cloud Run Deployment mit Docker-File


Landingzone, Security & Co.
Die im Beitrag von Cloud Function (hier) erwähnten Aspekte der Landingzone und Sicherheit etc. gelten auch für Cloud Run. Insbesondere das Thema Load Balancer, um eine eigene Domain für den Service zu erhalten.

Fazit
Eine Applikation kann schnell, mit einem einfachen Docker-File, in Cloud Run deployed werden. Eine Filestore-Instanz ist schnell erstellt und kann (auch) in einer Cloud Run Applikation verwendet werden. Damit lässt sich ein Grossteil von Applikationen recht einfach serverless auf der GCP betreiben.

Donnerstag, 28. März 2024

4 Wochen 4 Sprachen: Epilog

Die letzten vier Wochen habe ich vier verschiedene Sprachen vorgestellt, von denen ich aktuell überzeugt bin, dass sie für ihr jeweiliges optimales Einsatzgebiet als bevorzugte Sprache verwendet werden sollten und die eine Zukunft haben. Alle Sprachen sind unter einer freien Lizenz oder gar als internationaler Standard verfügbar. Das ist ein wichtiges Kriterium. Nur so ist sichergestellt, dass nicht der Profit einer einzelnen Firma über den Fortbestand der Sprache entscheiden kann. Nachfolgend eine Übersicht der vier Sprachen mit den wichtigsten Einsatzgebieten:

Wie erwähnt ist dies meine aktuelle Sicht im "Sprachen-Dschungel". Dazu kommen natürlich noch weitere Computer-Sprachen, die je nach Gesichtspunkt auch als Programmiersprache oder eben auch nicht, angesehen werden. Es sind u.a. die folgenden Sprachen und Formate, welche jeder Entwickler, je nach Tätigkeitsgebiet versteht sich, ebenfalls anwenden können sollte: SQL, HTML, CSS, JSON, YAML etc. Dazu kommen Standards wie OpenAPI und Konsorten. All die Frameworks, die noch viel mehr als die Sprachen selber wie Pilze aus dem Boden schiessen, lassen wir an dieser Stelle einmal aussen vor. Wie schrieb es einst Michael Ende: "Aber das ist eine andere Geschichte und soll ein andermal erzählt werden." Wir werden sehen, ob ich dieses Fass hier einmal öffnen werde.

"meine worte sind sortiert"

Warum funktionieren die Programme in den vorgestellten Sprachen beim String "Meine Worte sind sortiert" alle scheinbar nicht korrekt? Um diese Frage zu beantworten, müssen wir etwas tiefer in die Implementierung eintauchen. Die meisten Sprachen, resp. eigentlich präziser, die meisten API-Implementierungen von Arrays oder Listen, die eine alphabetische Sortierung anbieten, sortieren die einzelnen Elemente anhand der Unicode-Zahlen, welche die einzelnen Zeichen repräsentieren. Unicode ist ein Standard der definiert, welche Codes (Zahlen), welches Zeichen repräsentieren sollen, wenn man die Zahl als Zeichen interpretiert (was bspw. bei einem String der Fall ist). Nun ist es so, dass in Unicode die Grossbuchstaben vor den Kleinbuchstaben kommen. Sortiert man nun nach Unicode, so kommt bspw. der "Z" vor dem "a". Entsprechend sind die Wörter "Meine Worte sind sortiert", nach Unicode sortiert identisch, nämlich immer noch "Meine Worte sind sortiert". Die Programme funktionieren also richtig per Design. Auch wenn wir Menschen als User in der Regel wohl ein anderes Resultat erwarten würden. Wollen Sie es jeweils unabhängig von der Gross- und Kleinschreibung, müssen Sie den String jeweils vor der Sortierung in Gross- oder Kleinschreibung umwandeln. Sortieren und dann die einzelnen Elemente wieder der Ursprungsform zuordnen zur korrekten Ausgabe (in Gross- und Kleinbuchstaben).

Nun gut, dass ist aber nicht die einzige Eigenheit von Unicode. Eine weitere ist, dass gewisse Sonderzeichen auf zwei Arten abgebildet werden können. Zum einen als ein Zeichen inkl. dem "Sonderzeichenteil", bspw. "ñ". Zum anderen kann man es auch getrennt machen: "n" und ein höher gestelltes "~". Der erste Fall ist mit dem Unicode-Zeichen \xF1 codiert, der zweite mit "n" und \u0303. Optisch wird beides gleich dargestellt. Man sieht den Unterschied jedoch, wenn man bspw. in JavaScript ein String auf Char-Basis in ein Array umwandelt und dieses dann neu sortiert. Man kann dies korrigieren, wenn man Strings vor der Verwendung normalisiert, bspw. mit normalize('NFC'). Nachfolgend Beispiele in JavaScript, die das veranschaulichen:
Im zweiten Fall oben, landet die "Schlange" ober dem Buchstaben "n" plötzlich ober dem Buchstaben "b". Daran sieht man, dass im zweiten Fall der Buchstabe ñ mit zwei Unicode-Zeichen codiert worden ist: n\0303. Es sind zwei Chars. Dies sieht man übrigens auch an der Länge:
Obwohl in beiden Fällen der Output optisch identisch ist: abñam, ist die String-Länge im zweiten Fall um 1 grösser. Es werden eben die Chars gezählt. Daher ist es wichtig, vor String-Operationen eine Normalisierung vorzunehmen.


Die erste wird die letzte sein

Die vorliegende Serie war inspiriert vom Buch, Sieben Wochen sieben Sprachen. Zwischendurch ist es inspirierend, den Fokus zu öffnen und sich 2-3 andere Sprachen und vor allem, andere Sprachkonzepte, anzuschauen. Es erweitert den Horizont und gibt vielleicht auch für die bereits bekannten Sprachen neue Inputs.

Wir starteten die Miniserie mit dem Prolog. Der Name ist Programm. Prolog heisst eine bereits recht alte, interessante Programmiersprache. Ich habe sie bereits einmal in einem Beitrag erwähnt. Prolog steht für "PROgramming in LOGics". Prolog ist eine logische und vor allem deklarative Programmiersprache. Abgesehen von SQL, wo man es meist gar nicht so wahr nimmt, sind es heute viele Entwickler gewohnt, imperativ zu programmieren. Prolog lässt sich nicht befehlen, wie etwas zu tun ist. In Prolog kann man die Spielregeln definieren. Wie es gelöst wird, ist Prolog überlassen. Ich würde nicht behaupte, dass man Prolog können muss und viel brauchen wird. Ehrlich, ich brauche es operativ auch nicht. Dennoch ist es erfrischend, bspw. in den Ferien, einmal damit zu experimentieren. Gewisse Problemstellungen lassen sich damit einfach ungemein schneller lösen als mit anderen, vorallem imperativen Sprachen. Eine nette Abwechslung, in Form einer kalten Dusche. Da denkt man, man kann in fast allen Sprachen rasch programmieren, und dann dass. Sie können es bspw. hier online ausprobieren, ohne zuerst Software installieren zu müssen: onecompiler.com/prolog. Daher, wie im Prolog bereits angedeutet, die Programmiersprache Prolog als Bonus zum Abschluss dieser Mini-Serie.

In dem Sinne, happy coding!

#rust #javascript #python #scala #prolog #unicode