Ich musste mich damit beschäftigen wie man eine kleine REST-API mit Python erstellt. Django macht dabei an sich bei jeden Pups bei mir Probleme und war doch sehr umständlich, weil man sich noch mit dem gesamten MVC-Pattern darin beschäftigen musste. Nachdem es auch mit dem ORM schwieriger wurde (im Vergleich zu Spring Boot mit JPA/Hibernate) dachte ich mir, es müsse doch auch für Python was modernes geben. So kam ich zu Turbo Gears 2 und das macht schon mal genau was ich wollte. Einfach, schnell und übersichtlich.
from tg import expose, TGController, AppConfig
import jsonpickle
# --
class TestEntity(object):
def __init__(self, id ,name, sub):
self.id = id
self.name = name
self.sub = sub
class TestSubEntity(object):
def __init__(self, value):
self.value = value
# --
class RootController(TGController):
@expose("json")
def index(self):
test = TestEntity(42, 'blubb', TestSubEntity('sub-blubb'))
return jsonpickle.encode(test, unpicklable=False)
print("Serving on port 8090...")
httpd = make_server('', 8090, application)
httpd.serve_forever()
Mit Turbo Gears 2, Spring Boot und Meecrowave kann wirklich schnell und einfach Microservices erstellen und vieles des Overheads alter Zeiten ist einfach nicht mehr da bzw wurde so gut versteckt, dass man sich rein auf den Code und die Logik konzentrieren kann. Welche Lösung man da nimmt ist Geschmackssache. Von der Codestruktur her sieht alles an sich fast 100% gleich aus.
Bei Python sehen Flask, Bottle und Hug auch interessant aus
REST-Services sind ja nicht mehr neu und haben gerade die klassischen WebServices (SOAP) an vielen Stellen ersetzt. Wer mal mit SOAP, auch auf einen niedrigeren Level, gearbeitet hat, wird dies nicht als Verlust sehen. Es gibt viele Frameworks für REST-Services und auch lassen sich die meisten anderen Frameworks ohne Probleme so anpassen, dass sie keine HTML-Seite zurück liefern sondern das Ergebnis als JSON. XML wäre auch möglich als Ausgabeformat, aber ist sehr sehr wenig verbreitet. XML ist meistens sehr viel größer, da "name":"value" viel kürzer ist als <name>value</name> oder <attribute name="name" value="value"/>.
Meine Erfahrungen mit dem Parsen von XML und JSON in PHP ist aber, dass beide eigentlich gleich schnell sind. Ein Wechsel bei Config-Files von XML auf JSON hat keine Verbesserung der Ladezeiten ergeben. Auch die Serialisierung von Objekten bei Java war mit JSON nur minimal schneller als mit XML, wobei es dort auch sehr vom Verwendeten Framework abhängig war. Also kann es sein, das es in neueren PHP-Versionen doch Unterschiede geben wird, wenn dort noch mehr optimiert wird.
Mit das größte Problem auf dass ich bei REST-Services in PHP gestoßen bin ist das Parsen der URLs. Hier besteht einfach das Problem, dass es immer viele Methoden pro Service gibt und das einlesen der Mapping-Patterns somit doch relativ aufwendig sein kann. Während man in einem Java-Servlet nur einmal am Anfang alles parsen muss, muss man in PHP bei jeden Request von vorne beginnen. Eine Lösung wäre das Speichern der Ergebnisse in der Session, wobei hier dann aber das Problem bleibt, dass man bei Änderungen diese erkennen und den Inhalt der Session erneuern muss. Man kann das XML als String lesen und einen MD5-Hash bilden. Damit könnte man verschiedene Versionen des Services in der Session identifizieren. Am schönsten wäre natürlich den Zugriff auf das Dateisystem komplett einsparen zu können, aber dass scheint erstmal so nicht machbar zu sein.
Das Gleiche gilt auch für das Objekt, dass die Methoden enthält auf die die URLs gemappt werden. Man muss sich auch deutlich machen, dass ein REST-Service im Grunde kein Konstrukt der objekt-orientierten Programmierung ist, sondern die Facade im Grund klassischen prozedualen Prinzipen folgt. Gerade wenn man Sicherheits-Token und so verwendet und nicht die Session für die Speicherung des eingelogten Benutzer verwendet, merkt man dass man auch ohne Klasse mit einfachen functions hier genau so gut arbeiten könnte. Aber wir verwenden eine Klasse, die den Service beschreibt, um auch keinen Stilbruch zu haben. Ein Objekt zu erzeugen ist teuer und theoretisch könnte man sich dieses hier ersparen.
Mein AFP2-Framework benutzt die selben Klassen wie mein älteres aoop-Framework. Es implementiert momentan noch kein wirkliches Routing sondern nutzt einfach Regex-Ausdrücke um die gesamte URL mit einem Match auf die richtige Methode zu mappen. Hier wird also einfach von oben nach unten die Liste durch laufen und immer ein preg_match durchgeführt. Routing mit beliebig tiefer Verschachtelung und Sub-Routen, würde hier viel Zeit ersparen, da damit die Anfangs-Liste relativ klein wäre und schnell abzuprüfen wäre und erst dann in der nächsten Liste weiter geprüft wird. Das kommt dann später nochmal um etwas schneller zu werden.
Wie man erkennt wird im service-Tag die Klasse angeben, auf die die URLs gemappt werden sollen. Das Mapping der einzelnen Methoden wird dann innerhalb genauer definiert. Jede Method einen grundlegenden Pattern, der für ein Match auf die URL verwendet wird. Wenn dieser passt wird versucht die Argument (die Reihenfolge in der XML muss der Reihenfolge in PHP entsprechen!) aus der URL oder dem Request zu rekonstruieren. Jedes Argument kann ein eigenes Pattern haben. Wenn es ein Request-Value ist, entspricht das Pattern dem Namen im Request. Sollte kein Pattern angeben sein, wird dass Pattern der Method verwendet. Das erspart viel Text, weil man so nur ein allgemein gültiges Pattern schreiben muss. Mit group wird die Gruppe beim preg_replace angeben. Also die die den benötigten Wert enthält, der als Argument übergeben werden soll. Im Pattern der Method können also schon alle Werte in Gruppen markiert werden und bei den Arguments muss immer nur noch angegeben werden, welche Gruppe verwendet werden soll.
URLs zu REST-Services folgen in aoop und APF2 folgender Regel:
http://..../rest/{modulename}/.....
Über den Modul-Namen wird gesteuert welche rest.xml geladen wird. /rest/ ist die Markierung, dass hier ein REST-Service angesprochen werden soll. Der Rest der URL kann nach Belieben definiert werden.
Für unser Beispiel wäre z.B. folgende URL lauffähig:
Das Ergebnis der Methode wird dann in JSON umgewandelt und auch der Header entsprechend auf "text/json" gesetzt.
Damit lässen sich nun pro Modul REST-Services definieren und deployen. Bei PHP hat man zum Glück nicht das Problem, dass sich ändernde Klasse von anderen REST-Services anderer Module zu Laufzeitfehlern führen können. Wenn man ein Modul ändert, wird die Klasse ja beim nächsten Request erneut geladen und nicht wie bei Java in der VM gehalten. Probleme kann es nur geben, wenn man die Session verwendet und versucht dort gespeicherte serialisierte Daten einer alten Version in ein Objekt der neuen Version der Klasse zu deserialisieren.
Das macht diese Umgebung in PHP für Microservices interessant. Da man hier wirklich einzelne Module zur Laufzeit austauschen kann, ohne dabei die anderen Module zu beeinflussen (wenn man natürlich vorher drauf geachtet hat, dass Klassen, die auch von anderen Modulen verwendet werden, nicht zu Fehlern bei diesen führen). Zu überlegen wäre auch diese Klassen zwischen den Services als JSON und nur über eine URL dem anderen Service verfügbar zu machen. Also die URL auch zu Intra-Service Kommunikation zu verwenden.
Und den JSON-String dann auf eine eigene Klasse zu mappen. So etwas werde ich für die nächste Version dann einbauen.
Das war jetzt ein grober Überblick wie meine Frameworks REST-Services implementieren und versuchen dabei möglichst flexibel zu sein, aber auch nicht dabei zu sehr auf Kosten der Performance zu arbeiten. Die Arbeiten an APF2 sind noch in Gange und deswegen kann sich da auch noch einiges Ändern und vielleicht lerne ich von anderer Seite noch einiges, wie sich auch die letzten Probleme lösen lassen, um so REST-Services noch besser implementieren zu können.
Nachdem ich mich in den letzten Tagen irgendwie mehr wieder Services und Service-Strukturen
gedanklich beschäftigt habe (in Staus hat man viel Zeit für so etwas), habe ich mal angefangen in paar kurzen Gedanken zusammen zu fassen, wie ich mir eine Umgebung vorstelle, mit der man sehr flexibel und fehler-tollerant arbeiten und entwickeln kann.
Es geht hier hauptsächlich um REST-Services, die man z.B. von einer AngularJS Anwendung oder einen Java-Desktop-Client Aufruft. Oder auch REST-Services die zwischen Servern zur Kommunikation verwendet werden.
Wichtig dabei ist auf jeden Fall, dass man Config-Dateien an einem separaten Ort ablegen kann. Also getrennt vom Service-Code, so dass man ohne Probleme eine neue Version deployen kann, ohne sich darum Gedanken machen zu müssen, ob man die Config überschreibt oder eine für ein falsches System darin enthalten sein kann. Die Anpassung des Services wird extern von der Umgebung gemanagt.
Sollte keine eigene Config-Datei vorhanden sein, sollte der deployte Service mit default Einstellungen laufen.
Die Umgebung sollte einen Service deployen, undeployen und pausieren können. So dass man einen Service für Wartungen deaktivieren kann und das System Anfragen entweder so lange hält (bis der Service wieder da ist oder eine Timeout kommt) oder eine allgemeine Antwort zurück liefert, die dem Anfragenden darüber informiert, dass der Service zur Zeit nicht verfügbar ist.
Es sollte auch ein Reload eines Services möglich sein, um z.B. geänderte Config-Daten zu erkennen. Da würde ja ein Call auf eine Methode des Service-Objekts reichen, dass in einem bestimmten Interface vorgegeben wird.
Wenn man eine große Anwendung hat, muss man früher oder später die Services auch untereinander kommunizieren lassen. Konzept wie @ejb halte ich für zu unflexibel, da man direkte Pfade und so berücksichtigen muss. Ein Service sollte intern einen selbst gewählten eindeutigen Namen haben. Der alleine reicht um einen Service anzufordern. Da wird dann aber auch nicht ein Interface un eine Proxy-Klasse zurück geliefert sondern ein Descriptor-Objekt, dass angibt ob der Service verfügbar ist und die Root-URL. Es müsste auch eine Version mit in der Anfrage nach dem Service sein, um zu verhindern, dass Dependencies ein nicht handhabbares Problem werden. Wenn man die Verfügbarkeit über das Objekt prüft, wird immer der aktuelle Status des Services ermittelt.
Man kann also Versionen abhängige Logiken implementieren, falls der eigene Service sich mit einer aktuelleren Version anders verhalten sein, weil die vielleicht eine bessere Methode für irgendwas hat. Sollte ein Service gar nicht vorhanden sein oder plötzlich offline gehen, muss man gucken, ob meinen Fehler zurück liefert oder eine Fallback-Lösung bemüht. Wenn man z.B. einen zentralen Logging-Service verwendet und dieser während der Laufzeit in den Wartungsmodus versetzt wird, könnte man für die Zeit auf eine eigene Datei-basierte Lösung zurück greifen, bis der Logging-Service wieder verfügbar ist.
Die Umgebung hat ein zentrales Verzeichnis mit allen deployten Services. Man könnte überlegen, dass dieses Verzeichnis die Request nicht nur auf die Objekte mapped sondern auch auf andere Server weiterleiten könnte, so dass man Services sogar über verschiedene Server verteilen kann,
aber für den Client weiterhin einen zentralen Aufrufpunkt hat.
Auch könnte man implementieren, dass ein Service eine asynchrone Nachricht an das System schickt und die Nachricht dann an jeden Service verteilt wird.
Wichtig wäre bei der Kommunikation zwischen Services auf jeden Fall, dass Requests es auch asynchron bearbeitet werden können. Also das z.B. bei unserem Logging-Service zwar die Verfügbarkeit geprüft wird dann aber nach dem Fire-And-Forget Prinzip weiter verfahren wird. Ansonsten ist es auf der Service-Seite gerade mit inter-Service Kommunikation es wohl meistens einfacher synchron zu arbeiten. Auf Client-Seite sollte man ja asynchron arbeiten, da es die UX sehr verbessert.
DataSources und Transaction-Management sollte auch von der Umgebung alles bereit gestellt und verwaltet werden. In der Config-Datei für einen Service trägt man dann ein "verwende DataSource 'CRM_ARTICLES'". Der Service alleine darf sich selbst keine Datenbankverbindungen erstellen.
Mit soetwas sollten sich auch größere Anwendungen mit getrennten Modulen/Services entwickeln lassen ohne zu schnell in Probleme zu laufen.. natürlich nur wenn man die Möglichkeiten nutzt und nicht versucht am System vorbei Dinge zu erledigen. Ob so ein System für solche Service-Module performant realisieren kann, müsste man einfach mal ausprobieren.. wenn man die Zeit dafür hätte. In Java sollte sich so etwas gut Umsetzen lassen. In PHP sehe ich viele Probleme, weil man keine zentralen Application-Scope oder so hat.
Neben DevOps hört man viel über Microservices. Oft sogar zusammen. Der gute Gedanke bei Microservices ist meiner Meinung nach jeden Falls, dass eine gute Modularisierung angetrebt wird. Etwas was ich auch immer in meinen Projekten versuche, aber viele doch es immer wieder für zu kompliziert halten und bei riesigen monolithischen Klötzen bleiben wollen. Aber sich auch dann beschweren, dass das Deployment immer so lange dauert und dass einige Entwickler einfach so frech waren und ihre Methoden geändert haben. Bei diesen riesigen monolitisches Klötzen ohne Abschottungen neigen viele dazu direkt mit fremden Klassen oder NamedQueries zu arbeiten. Wenn ich aber eine Service-Fascade hat und dort meine Methode auf eine andere interne umleite, die sehr viel schneller ist als die alte und die alte entferne, kommen Beschwerden, weil die eine Methode nicht mehr auffindbar ist und ein anderer Teil der Anwendung jetzt nicht mehr funktioniert.
Antworten wie "Benutz einfach die Service-Methode, die liefert dir immer genau was du brauchst", sind dann aber nicht gerne gehört.
Microservices haben eben diesen Vorteil, dass viel mit Schnittstellen gearbeitet werden muss. Damit kommt dann vielleicht auch ein Gefühl dafür Service-Methoden kompatibel zu halten. Nicht einfach vorhandene Methoden von der Aufrufstruktur zu vrerändern, sondern eine neue Methode anzulegen und die alte dann mit standard Werten auf die neue weiter zu leiten.
Der einfachste Gedanke zu Microservices ist bei mir immer eine WAR-Datei. Eine Anwendung auch auf mehrere WAR zu verteilen. Die inter-Modul Kommunikation ist dann auch über URLs oder intern über vielleicht JNDI zu realisieren. Aber alles ist sehr isoliert und man kann einzelene
Teile deployen ohne dass andere auch neu deployed werden müssen.
In PHP habe ich das mit meinem Framework von Anfang an (bzw es hat sich entwickelt) so umgesetzt. Module sind 100%ig getrennt und ein Update eines Modules ist durch einfaches Kopieren in das richtige Verzeichnis möglich. Die Module orientieren sich an sich schon etwas an klassischen WAR-Dateien aus der Java-Welt. Jedes Modul bringt seine eigenen Klassen, seine eigene Admin-Oberfläche, seinen eigenen REST-Service und eben Views mit. Es ist nichts fest in Config-Dateien eingetragen, deswegen sich das System auch nicht beschweren kann, wenn ein Modul plötzlich weg ist. Das dynamische Zusammensuchen und Instanzieren von Modulen und Addons kostet natürlich Zeit, aber hält das System sehr flexibel und robust, weil meistens nichts vom Grundsystem vorraus gesetzt wird. Caching ist hier natürlich doch ein gewisses Problem, aber das kann man auch lösen. Meistens reicht es Module zu dekativieren und man muss sie nicht löschen. Neue Klassen werden einfach gefunden und Klassen bei denen sich der Pfad geändert hat sind nach ein oder zwei Refreshes meistens auch wieder da. Das System ist also im Notfall sogar in der Lage sich selbst zu heilen.
Aber in den meisten Fällen kommt ja immer nur neues hinzu.
Also jeden Teil einer Anwendung als eigenes Modul realisieren. Module isoliert von einander halten und so ein unabhängiges Deployment ermöglichen.
Ein wirkliches Problem sind dann nur Oberflächen und Oberflächen-Fragmente. Wären ich es oft über die Addons gelöst habe wäre es bei Anwendungen mit AngularJS und REST-Backend sehr viel einfacher über einzelene Templates zu realisieren, die dann immer bei Bedarf und Vorhandensein nachgeladen werden können.
Aber es ist und bleibt so erst einmal mit das größte Problem.
Wirklich "micro" sind die Services am Ende auch nicht, aber sie sind doch sehr viel kleiner und einfacher zu handhaben als eine riesige monolithische Anwendung, wo alles von einander abhängig ist und eine Änderung an einem Teil theortisch Auswirkungen auf andere Teile haben kann, weil man nicht sicherstellen kann, dass nicht andere Entwickler die geänderten Klassen wo anders auch direkt verwenden. Auch wenn Microservices nicht die Lösung für alles ist und viele Problem
dadurch auch nciht gelöst werden können, sollte man wenigstens das Prinzip der isolierten Module und deren Fähigkeit unabhängig von einander deployed werden zu können aus diesen Konzept mitnehmen. Wenn das Deployment dann nur noch 5 Sekunden an Stelle von 30 Sekunden dauert, hat man oft gefühlt schon viel gewonnen. (Ich hab mit solchen Zeiten gearbeitet und es ist sehr nervig immer solange warten zu müssen nur um eine kleine Änderung in einer Zeile testen zu können!)
Möchtest Du AdSense-Werbung erlauben und mir damit helfen die laufenden Kosten des Blogs tragen zu können?