Saturday, November 3. 2007Die Xenjo Cache Registry Part II
Da ich wieder mal ein paar Tage Urlaub aufbrauchen konnte, konnte ich die Zeit nutzen und mich dem Thema mod_magnet etwas hinzugeben. Im Juli 2007 hatte ich hier bereits ein paar Gedanken gepostet, die mit meiner Xenjo Cache Registry Part I zusammenhängen. Xenjo ist ja kein Produkt, sondern nur mein kleines Framework, in dem ich Spielchen teste. Ziel ist natürlich die Verwendung mit Zend-Framework.
![]() ![]() Der MagnetDas mod_cml meines Lieblingswebservers lighty ist tot. Es lebe das Modul mod_magnet. Das mod_magnet ist an sich noch nichts neues, für die Über eine Konfiguration des lighty kann bei jedem Request ein Script aufgerufen werden. Dieses Script muss ein LUA-Script sein, weil lighty mit dem mod_magnet auch immer einen LUA-Interpreter zur Verfügung stellt. Die Entscheidung für LUA liegt auf der Hand: LUA ist klein (eigentlich nur eine Bibliothek), sehr schnell und geizt mit der Verschwendung von Ressourcen. Es gibt keinen Lade-Overhead beim Aufruf eines LUA-Scripts wie bei PHP oder anderen Interpretern. Vor der Ausführung wird LUA-Code grundsätzlich nach Byte-Code kompiliert. mod_magnet erkennt jedoch jede Änderung und kompiliert das Script bei Bedarf neu. Zusätzlich zu den Möglichkeiten die LUA bietet (mod_magnet verwendet LUA 5.1), stellt der lighty noch etwas mehr Umgebung (u. a. den Zugriff den Request) und seine eigenen stat()-Routine bereit. Eigentlich bräuchte man, vereinfacht gesprochen, keine lighty-Module mehr zu entwickeln. Mir fallen auf die Schnelle keine wesentlichen Dinge ein, die man mit LUA nicht abdecken könnte. Und wenn doch (oder wenn man das ganze noch etwas beschleunigen will), schreibt man sich das Benötigte direkt in LUA oder baut sich LUA-Befehl in C... Aber ich bin ohnehin eine echte C-\0#, auch wenn es wirklich nicht so schwer ist. Das richtig fiese ist, dass so ein LUA-Script den Webserver "blockieren" kann - Lighty 1.4.x ist ein Single-Prozess-Webserver, und eben extrem schnell in der Auslieferung statischer Inhalte - und wenn das statische Ausliefern einer Seite dauert, kann kein anderer Request bearbeitet und ausgeliefert werden. Allerdings kann ich mir vorstellen, dass man hier etwas mit einer Max-worker-Konfiguration drehen könnte. Wenn ich einige Highlights der nächsten Lighty-Generation richtig verstehe, wird das mit dem 1.5.x Branch unproblematischer werden. Das gefährliche an den neuen Möglichkeiten ist also die falsche Anwendung. Nicht mehr und nicht weniger. Anwendungen gehörten nach wie vor auf das CGI- oder FastCGI-Interface. Nicht zuletzt, weil Software immer Fehler enthalten, und im Falle einer "Katastrophe", killt LUA den Lighty. Das kennt man ja noch aus der Apache-1.3-Zeit mit mod_php - wo Licht ist, kann man das auch ausknipsen. Nebenbei bemerkt: Es existiert für LUA bereits eine CGI-Schnittstelle - allerdings ist ein FastCGI bislang noch ein Traum der Leute des Kepler-Projekts. Ich halte fest: mit mod_magnet existiert im Lighty nun ein vollwertiger LUA-Scripting-Host, der die Antwortzeiten bei sinnvoller Anwendung vernachlässigbar belastet. Auf den ersten Blick fehlen sogar im mod_magnet gegenüber mod_cml Funktionalitäten. Die Lighty-Umgebung stellt weder die in mod_cml vorhandenen Teile der memcache-API noch Funktionen wie md5 bereit. Das macht aber nix, wieso auch, wenn das alles - wie oben erwähnt - auch in LUA oder einem in C implementierten LUA-Packet "reingeholt" werden kann! Memcached-Zugriffe können über die libsocket (ich hab' mehrere LUA-Packages gefunden, die die Funktionalität auch kapseln) und der MD5 kann über die libmd5 reingeholt werden. Es gibt einiges mehr, was für die vorderste Webserver-Front praktisch sein könnte. Da wären z.B. cUrl und svn um nur einige zu nennen. Bevor das hier aber alles zu perfekt klingt: Es ist leider nicht perfekt. Überhaupt nicht. Zum Thema killen musste ich die Woche so meine richtig traurigen Erfahrungen machen: mod_magnet ist im Moment für meine Memcache-Experimente nur bedingt einsatzfähig. Und der Urheber dafür ist, dass u. a. die libsocket den lighty killt. Keine Sockets, kein Memcached-Zugriff. So einfach ist das. LUA selber arbeitet mit der libsocket ohne Fehler, sodass ich wenigstens außerhalb des Lighty testen konnte. Die DatenDennoch macht es keinen gesteigerten Sinn, sich von verschiedenen Ideen abbringen zu lassen. Die da wären: Ein Konzept für eine Anwendungs-Cache-Registry, bei der sicher nicht die Infrastruktur wie lighty oder memcache das Problem darstellen, sondern die Vermeidung, in die Domaine-Schichten Logik für die Präsentations-Schicht zu hämmern. Dieses Problem ist ein architektonisches. Und mit zunehmenden SOAs wird das richtig gruselig (und ist deswegen nicht mein Thema!). Um ein Seiten-Caching einer Internet-Anwendung sehr schnell zu machen, müsste die Präsentations-Schicht einen Status über die Daten des Models erhalten. Angenommen, ich verwende in meiner heilen Welt ein ORM-Framework wie Doctine. Und angenommen, alle Anwendungen greifen nur noch über die ORM-Schicht zu. Dann könnte man die Models mit einfachen Triggern erweitern, die wenigstens ihren Status an die große Tafel werfen. Ganz unten versuche ich nochmal auf die Problematik einzugehen. Aspekte beim CachingLayouts können sich ändern. Nehmen wir den Fall, wir hätten eine MVC basierte Anwendung die in der Präsentations-Schicht mit einem Template-View arbeitet. Wenn sich ein Template auf dem Server geändert hat, kann das die Template-Engine, z. B. Smarty, ermitteln. Dazu muss aber die Template-Engine angeworfen werden. Und wenn ich das bei jedem Request tue, ist mein Caching nur bedingt zu gebrauchen, weil wenigstens ein Teil der View-Schicht meiner Anwendung aufgerufen werden muss. Eine gängige Lösung für dieses Problem ist simpel und effektiv: Mit der Änderung des Templates löscht man gleich die involvierten Cache-Files. Lighty muss in dem Fall nur nachsehen, ob ein Cachefile ausgeliefert werden kann oder nicht. Wenn nicht wird die Anwendung aufgerufen, die eines erstellen kann (das soll der Präsentations-Schicht der Anwendung überlassen bleiben). Dann schneiden wir mal die Sau auf:In einer PHP-Anwendung gibt es einen Front-Controller (index.php), der URIs in der Form module= Mit Hilfe des require läd man unter LUA 5.1 ein Modul oder ein Packet. Der Code ist hier nur drin, weil wir das später noch brauchen. Die Variable cache_path hält das Verzeichnis auf der Platte, in dem später unsere Cache-Files liegen. Interessant wird es erst jetzt: Die Funktion holt sich die Request-URI aus der lighty-Umgebung, parst sie und baut den Request für den Front-Controller auf, wenn soweit alles konform ist. Ist das nicht so, liefert die Funktion nichts zurück (wer Pascal oder Infochick kennt, kennt nil) - lighty kümmert sich wie gewohnt um den Request und arbeitet ihn ab. Im Erfolgsfall macht lighty mit unserer Version des Requests weiter und wird den Aufruf an einen FastCGI-PHP Prozess weiterleiten. Der Prozess muss sich nun selber um die Erstellung des Cache-Files kümmern (ich denke mal das ist trivial und hat fast jeder schon gesehen: Content erstellen, abspeichern und ausgeben…). Jetzt wollen wir die erste Cache-Decision treffen: Die Präsentations-Schicht hat nun das Cache-File zu einem Request auf der Platte gespeichert. Da will Lighty nun ran, ohne die Anwendung aufzurufen. Das könnte man wie folgt mit einer zweiten LUA Funktion erledigen: Die Funktion handle_cachefile() baut erst einmal den Namen des Cache-Files auf. Aus /wiki/123/lua.html wird /cachefile-pfad/_wiki_123_lua.html. Der führende Unterstrich im Filename zeigt an, dass ich ein fauler Hund bin. Jetzt kommt ein Zuckerl (ich befürchte ich werde erschossen, wenn ich weiterhin LUA-Speech verwende, aber Roberto Ierusalimschy ist auch so ein Zuckerl): Wir müssen keinen "langsamen" stat()-Call aus einer LUA-Bibliothek nehmen (LUA ist 100% ANSI-C, bis auf das Packetmanagement Schnittstellen...Schwierig wird es nun, wenn lorindol meinen Wiki-Artikel unter /wiki/123/lua.html korrigiert (weil er das kann). Das Wiki-Modul wird einen Datensatz in einer Datenbank updaten. Die einfache Antwort: "Na, dann löschen wir halt nach dem Speichern das Cache-File" wird nicht reichen, um die aktualisierte Version des Eintrags anzuzeigen. Jedenfalls nicht, wenn wir es mit einem Cluster zu tun haben. Und wir haben es nun mal schnell mit einem Cluster zu tun, wenn wir schnell ausliefern wollen, denn zufällig liegt die alte Version unseres Cache-Files noch auf n-1 Nodes! Die erste Lösung ist die auch die konservativste: Vor dem Aufruf von handle_cachefile() rufen wir die Funktion handle_memcache() auf. Diese nimmt den String _wiki_123_lua.html einfach als Schlüssel und fragt am Memcache-Demon an, ob ein Cache-File verwendet werden soll. Diese Info gilt dann im gesamten Cluster. Per default existiert dieser Schlüssel nicht; es soll kein Cache-File ausgeliefert, sonder die Anwendung direkt aufgerufen werden. Wenn der Schlüssel existiert, liefern wir einfach nur das Cache-File aus, in dem wir handle_cachefile() rufen. Wenn der Cluster 70 Nodes hält, würde zwar das Cache-File 70 mal erstellt, aber bei 70 Nodes unter Last passiert das (sag ich jetzt einfach mal so): schnell. Um das mod_magnet-Script abzuschließen: Die LUA-Funktion könnte dann so aussehen: Wie oben bereits erwähnt, hat das schon etwas krankes, auf n Nodes, n Kopien eines Cache-Files vorzuhalten. Und reden wir nicht davon, dass jedes Cache-File IO kostet. Ein gangbarer Weg wäre, die Cache-Files komplett im Memcached zu speichern. Die Funktion handle_memcache() müsste nur geringfügig verändert werden und mit handle_cachefile() kombiniert werden. Die Grenzen in denen sich Memcache bewegt, sind zwar durch das Betriebsystem und die Hardware limitiert, aber eine genug Raum für interessante Überlegung bleiben noch immer (z. B. durch das Aufteilen mehrerer Demons pro Server auf unterschiedlichen Ports). Zurück zur Anwendung"Jemand" macht ein manuelles Update auf der Datenbank-Console, oder über ein altes Script, dass noch nicht unsere Domänen-Logik des ORM-Frameworks verwendet, in dem wir einen Datenstatus delegieren können, damit die Präsentations-Schicht weiß, was zu tun ist. Nun bricht das schöne Kartenhaus vollends zusammen, wenn wir den Cache nicht regelmäßig löschen. Natürlich könnte man in seiner Datenbank Trigger verwenden, die z. B. bei jedem Update entweder Cache-Files löschen oder den weiter oben verwendeten Memcache-Schlüssel löschen. Der Teufel steckt hier einfach im Detail: Eine gecachte Sicht besteht meist aus vielen verschiedenen Daten. Und eine zusätzliche „Welche Cache-Files müssen jetzt gelöscht werden“ – Entscheidung ist definitiv auf der Datenbank-Schicht krank und nicht wartbar. Wenn man das Problem aber von oben betrachtet, fällt auf, dass ein weiteres, völlig neues Konzept bei der Thematik fehlt: Daten die wir nicht kontrollieren können, werden uns niemals einen Status über Ihre Aktualität liefern. Wir benötigen beim Abholen der Daten eine Expire-Angabe, auf die wir in der Präsentations-Schicht zugreifen können. Jetzt haben wir es mit dem etablierten Cache-Entscheidungen-Verfahren der Time to live zu tun. Die Seiten eines Mitarbeiter-Portals greifen direkt auf ein oder mehrere LDAP-Server zu. Angenommen, dieses System wird stark frequentiert. Wir können nicht so einfach wissen, wann ein Administrator mit seinem LDAP-Tool in welchen Datensätzen herumfuhrwerkt hat und wann wir deswegen unsere Cache-Files löschen müssen. Das erledigt man dann doch am sinnvollsten mit einem TTL. Mit Memcache zusammen natürlich kein Problem, weil wir die Haltbarkeitsdauer eines unserer Cache-Files beim setzen des Schlüsseln mitgeben können, ohne dass dafür besonders viel zu tun wäre. Eine wirklich schöne Lösung, das Caching komplett über eine Präsentations-Schicht zu steuern sieht also mau aus. Aber wir haben jedenfalls die Möglichkeit, an eine halbwegs brauchbare Lösung zu kommen, auch wenn die nicht omnipotent wird. Fortsetzung folgt… |
Calendar
QuicksearchArchivesBlog abonnierenBlog AdministrationStatistikenLetzter Artikel: 2010-03-25 12:23
106 Artikel wurden geschrieben
84 Kommentare wurden abgegeben
|
|||||||||||||||||||||||||||||||||||||||||||||||||
Der von mir sehr geschätzte Kollege Jo Brunner hat mal wieder tief in die Trickkiste gegriffen, und im Rahmen seiner Xenjo Cache Registry den wertvollen Urlaub dafür verwendet (Chapeau!), sich ausführlich mit lighttpd, mod_magnet und lua zu beschäftigen
Tracked: Nov 05, 08:59
A few days ago I got nice task. Jo discovered a nice bug while playing around with memcached, lighttpd and Lua. When lighttpd got a request it can call a Lua script before the fcgi take over the request. The lua script is able to modify the complete re
Tracked: Jan 21, 21:44