Facebook
Twitter
Google+
Kommentare
29

Caching, wie richtig?

Heute ist mal wieder ein Tag, an dem ich Hilfe von der Gemeinde brauche. Ist nicht so, als ob ich zum Thema Caching nichts zu sagen hätte, aber ich habe so ein paar Ideen in meinem Kopf, die ich loswerden will und dann hoffe ich, dass es ein paar Leute gibt, die Caching studiert haben und ihre Tipps noch abgeben wollen.

Jetzt aber erst mal zum Problem. Wir sind gerade dabei „eine“ Webseite zu bauen, die unter Hochlast steht und da darf eine ausgereifte Caching-Strategie natürlich nicht fehlen. Jetzt gibt es unterschiedliche Dinge, die man cachen kann und einige davon wird man anders behandeln, da es ja mehrere Caching-Schichten gibt. Nehmen wir also mal an, wir haben MemCache (verteilt), FileCache und APC zur Verfügung und wollen dort Objekte ablegen, die wir öfters verwenden. Jetzt gibt es ein paar Möglichkeiten, wie man dies machen kann:

  • Standardlösung: Wir überlegen uns bei jedem Objekt, in welchem Cache es am besten aufgehoben ist. Objekt A lege ich also explizit in den Cache B. Vorteil dabei ist, dass ich wohl die beste Art zu cachen für den Augenblick finde. Nachteil dabei ist, dass ich jede Menge Wissen über die verschiedenen Caches besitzen muss.
  • Wir lagern alles in Cache A und kümmern uns nicht um andere Caches. Vorteil ist, wir haben nur einen Cache, was bedeutet, dass die Wartbarkeit steigt und die Komplexität sinkt. Nachteil ist ganz klar, dass ich keine optimierten Caches für eine bestimmte Aufgabe besitze und damit das Resultat ein wenig schlechter ausfällt.
  • Wir tun so, als ob wir nur einen Cache habe, die Schicht, die dahinter liegt kümmert sich aber um die Verteilung auf die einzelnen Schichten. Da es hier mehrere Varianten gibt versuche ich die wichtigsten aufzuzählen
    • Im Hintergrund kümmert sich ein Team von Cache-Experten um das Mapping von Objekt-Typ nach Cache-Art. Die Effizienz, mit dem der Cache genutzt wird ist hervorragend, aber die Zuordnung kann maximal kompliziert werden.
    • Das Mapping von Objekt und Cache wird automatisiert gemacht. Zuordnung passiert ohne menschliches Zutun und kann gegebenenfalls je nach Lastsituation verändert werden. Leider ist diese Möglichkeit maximal komplex und wird wohl sehr teuer in der Umsetzung.
    • Wir lehnen uns an die Caches an, die bereits in Computern existieren und schmeißen alle zu cachenden Daten einfach in alle Caches gleichzeitig. Wenn es im schnellsten Cache nicht gefunden wird, dann schaut man halt im nächsten. In den meisten Fällen sind die schnellen Caches ja die kleineren und somit kann das System funktionieren. Nachteil dabei ist dass man nicht kontrollieren kann was in den schnellsten und somit kleinsten Cache wandert. Wenn es doof läuft schmeißt man eine 4 GB-Datei in den APC und schwupps ist der Speicher voll und alles andere fliegt raus. Dabei hätte man es in den „unendlich“ Große File-Cache schreiben sollen.

Wie ihr seht gibt es viele Möglichkeiten so eine Caching-Schicht aufzubauen. Bestimmt fallen euch auch noch dutzende ein (die ich übrigens gerne alle in den Kommentaren wiederfinden würde) und wahrscheinlich muss man von Fall zu Fall unterscheiden, welche Strategie die beste ist. Aber trotzdem würde mich interessieren, was ihr wählen würde, wenn ihr für euer nächstes Projekt ein System auswählen müsstet. Vielleicht kennt der ein oder andere aus anderen Programmiersprachen Lösungen, die man adaptieren sollte.

Über den Autor

Nils Langner

Nils Langner ist der Gründer von "the web hates me" und auch der Hauptautor. Im wahren Leben leitet er das Qualitätsmanagementteam im Gruner+Jahr-Digitalbereich und ist somit für Seiten wie stern.de, eltern.de und gala.de aus Qualitätssicht verantwortlich. Nils schreibt seit den Anfängen von phphatesme, welches er ebenfalls gegründet hat, nicht nur für diverse Blogs, sondern auch für Fachmagazine, wie das PHP Magazin, die t3n, die c't oder die iX. Nebenbei ist er noch ein gern gesehener Sprecher auf Konferenzen. Herr Langner schreibt die Texte über sich gerne in der dritten Form.
Kommentare

29 Comments

  1. Selbstverständlich die Variante bei der man aus Programmiersicht nur ein Interface bedienen muss (hier 3.). Welcher Objekttyp wohin kommt wird dann konfiguriert. Ob nun durch ausgesprochene Spezialisten oder erst einmal durch den Durchschnittsprogrammierer sei dahin gestellt. Ich würde zB Annotations empfehlen um solche Daten zu hinterlegen. Ab in die Klassendefinition damit und beim hinzufügen zum Cache wird die Konfiguration ausgewertet. Kann ja auch wieder in den Cache geschrieben werden 🙂

    /**
    * @cache:Strategy(
    * site=“apc“
    * …
    * )
    */

    Reply
  2. Also ich würde dir folgendes empfehlen.
    Wenn das Projekt unter Hochlast, dann würde ich dir als erstes den HTTP Cache empfehlen. Leg die Seiten im Proxy oder im Browser Cache ab.
    Und wenn das immer noch nicht reicht, dann würde ich auf eine Kombination aus Memcache und APC nutzen. Diese beiden Cachingsysteme sind zusammen unschlagbar. Beide sind sehr schnell und erzeugen wenig Last auf dem Server.

    Eventuell würde ich dir noch einen Gateway Cache wie Varnish und Squid empfehlen. Die können ja schon alles. Warum das Rad neu erfinden wenn es derartige Lösungen schon gibt?

    Reply
  3. Eine Strategie die ich einmal angewandt habe:

    Die fertig gerenderten HTML-Schnippel liegen im APC.
    Diese können sehr schnell ausgeliefert werden, wenn ein Zeitstempelvergleich (SELECT MAX(changed) FROM artikel) keine Änderung der zugrunde liegenden Tabelle anzeigt.
    Diese Abfrage ist sehr billig, da die erste Timestamp-Spalte bei mysql gesondert optimiert wird.

    Trifft dieser Vergleich nicht, wird im memcache eine „distributed semaphore“ ab geprüft.
    Wenn schon einer der Server gerade am rendern des neuen Inhaltes ist, wird in der Übergangszeit noch der alte Inhalt aus dem APC ausgeliefert.
    Das ist wichtig, damit bei einer Änderung des Contents sich nicht alle Server gleichzeitig auf den selben Artikel stürzen.

    Hat der Server den Artikel fertig. Legt er Ihn im Memcache ab.
    Somit können ihn die anderen Maschinen im APC Aktualisieren.

    Die Seite selber besteht aus 3 Cache- Schichten.
    1, Die ganze Seite
    2, Ein ganzer Bereich
    3, Ein Artikel

    Das bedeutet:
    Solange sich in der DB gar nichts ändert, wird die ganze Seite aus dem Cache geliefert.
    Ändert sich ein Artikel oder die Zuordnung eines Artikels zu einem Bereich dann muss nur der eine Bereich neu gerendert werden.

    Reply
  4. Ich habe es so umgesetzt, dass ich mir immer ein passendes Cache-Objekt hole, entsprechend der Eigenschaften der zu cachenden Daten:

    1) Sind die Daten auch allen Servern gültig (dann nutze nach Möglichkeit einen verteilten Cache, also Memcache) oder sind die Daten abhängig vom aktuellen Server (dann nutze einen lokal begrenzen Cache, z.B. APC, WinCache, XCache, File…).

    2) Werden die Daten häufig/schnell benötigt? Dann würde ich sie nicht im Filesystem cachen (würde ich eh nach Möglichkeit vermeiden).

    Vorteil ist, dass ich mich nicht an bestimmte CacheHandler binden muss, da diese auf dem Zielsystem ggf. nicht immer zur Verfügung stehen. Als letzte Möglichkeit gibt es dann immer das Cachen im Filesystem.

    In besonderen Fällen lege ich Daten aber auch gezielt im Filesystem ab, um große Datenmengen gezielt lange zu cachen und damit die Daten auch einen Neustart überstehen (z.B. führe ich alle standardmäßig inkludierten Dateien in einer Datei zusammen, um die Zugriffe auf das Filesystem zu verringern).

    ciao
    Christoph

    Reply
  5. @All: So wie ich das sehe, verwendet ihr alle die erste Strategie, bei der man für jedes Objekt explizit angibt, wo die Daten landen sollen. Niemand einen Automatismus implementiert? Niemand gleich in mehrere Caches geschrieben?

    Reply
  6. Komisch, irgendwie bin ich der einzige, der auf die zweite Lösung setzen würde. 🙂 Wenn ich etwas in den letzten Jahren gelernt habe, dann ist es, die Komplexität von Applikationen gaaaaanz unten zu halten. Fast um jeden Preis. Sonst muss man mit Caches Probleme beseitigen, die man sonst gar nicht hätte.

    Damit meine ich jetzt nicht, dass man auf Caches verzichten kann. Aber ich würde eher auf ein kleines bisschen Performance verzichten als die Komplexität enorm zu erhöhen.

    Reply
  7. Ich halte es für ziemlich schwer das automatisiert zu machen. Du kannst zwar den Typ bestimmen (Array, Objekt, String), aber was genau da drin ist weißt du ja nicht. Ich mach es deshalb auch händisch, hab aber nur ein Interface, das die Daten dann an den gewünschten Cache weiterreicht.

    Die Wahl des Caches hängt ja von vielen Faktoren ab. Was für ein Typ von Inhalt ist es (Objekt, HTML-Snippet etc.), wie „teuer“ ist die Generierung des Inhalts, wie oft wird der Inhalt abgerufen, wird der Cache nur lokal verwendet oder sollen auch dritte Server darauf zugreifen können.

    Nach meinen Erfahrungen ist Memcache zwar in aller Munde, aber wirklich schnell ist er nicht. Durch den ganzen Netzwerk-Overhead ist der Abruf von Daten nicht so schnell wie man vermuten würde. Er ist allerdings perfekt, wenn sehr viele Server darauf zugreifen können. Also wenn die Generierung eines Cache-Objekts sehr lange dauert (z.B. 200ms o.ä.) ist das sicherlich perfekt, weil es wirklich nur ein Server machen muss.

    Der Cache von APC ist dagegen wahnsinnig schnell. Dafür muss jeder Server sein eigenen Cache generieren. Aber z.B. um etwas komplexere SQL-Abfragen zu cachen finde ich den optimal. Gibt es einen Cache-Miss ist es für den Benutzer nicht wirklich problematisch, gibt es aber einen HIT ist es sehr sehr flott..

    Ein File-Cache ist auch nicht schlecht. Zwar etwas langsamer, da aber das OS häufig genutzte Daten in den Arbeitsspeicher ladet ist er auch sehr flott. Bei meinen Tests war es deutlich flotter als Memcache… Vorteil: Die Cache-Datei kann beliebig lange gültig sein. Kompilierte Konfigurationsdateien (ich denke da an YAML oder XML) wären z.B. ein super Beispiel für die Nutzung. Die ändern sich im Normalfall ja nie…

    Auch komplette Seiten würde ich eher im Dateisystem ablegen. Dann reicht im Skript eine einfaches include() auf die Cache-Datei. Außerdem ist der APC oder Memcache für solche Datenmengen (können ja schnell mehrere 100 MB werden) einfach nicht ausgelegt.

    Man muss aufjedenfall sehr gut testen und jede Änderung erstmal messen. Hatte schön öfter den Fall das die ganze Cache-Logik soviel Overhead erzeugt hat, das der Performance-Gewinn komplett aufgefressen wurde. SQL-Abfragen sind nämlich oft schneller als man denkt…

    Gruß,
    Max

    Reply
  8. @Nils Langner
    „So wie ich das sehe, verwendet ihr alle die erste Strategie, bei der man für jedes Objekt explizit angibt, wo die Daten landen sollen.“

    Dann hast du meinen Beitrag nicht gelesen. Wenn ich feststelle das der Beitrag im lokalen Speicher(APC) veraltet oder nicht vorhanden ist, dann wird im übergeordneten Cache(memcache) gesucht. Ist er auch dort veraltet oder nicht vorhanden, dann generiert ihn *eine* Maschine neu.

    Reply
  9. @Harald:
    Ist das Suchen in mehreren Caches nicht extrem ineffizient? Gerade Cache Hits müssen ja extrem schnell sein. Wenn man jetzt aber dauernd in mehreren Caches sucht, ist das irgendwie kontraproduktiv.

    Reply
  10. Ich finde du hast deine Anforderungen nur unzureichend beschrieben. Die sind nämlich absolut ausschlaggebend wofür du dich entscheiden solltest.

    Dazu gehört erst ein mal, reden wir von einer Infrastruktur die aus einem einzelnen Rechner besteht, aus einem Rechnerverbund mit direktverbindungen oder sogar von einem Netzwerk, in dem die einzelnen Rechner geografisch voneinander getrennt stehen.

    Dann hast du leider nicht beschrieben, in wie weit sich die Daten unterscheiden die du cachen willst. Geht es da nur um Last Verteilung? Sprechen wir von unterschiedlichen Caches?
    In den Kommentaren hört es sich nun so an, als ob du große Datenverbunde im Giga Bereich Cachen willst.

    Generell würde ich aber sagen, ein einziges Interface nur, und erspart euch die Arbeit das optimieren zu wollen. Nehmt einfach nen Server mit 100GB Ram(vorrausgesetzt es handelt sich um nen einzelnen Server)

    Reply
  11. Ich gehe mal von einer verteilten Lösung aus, also multiple Webserver.

    Was maschinenbezogen ist, kommt in SHM / APC (local).
    Was seitenbezogen ist, kommt in memcache (global).
    Was bei jedem Request benötigt wird, kann zur Not zwischen global und local synchronisiert werden.
    Das ist eine triviale Unterscheidung.

    Die viel interessanteren Fragen bzgl. Caching hast du aber gar nicht gestellt:
    * welchen Serializer benutze ich? Standard-PHP oder sowas wie igbinary [1]?
    * cache ich granular(st) und habe viele lookups, oder kann ich Datenmengen cachen?
    * kann ich evtl. teil-gerenderte Ausgaben cachen? (Warum immer wieder über die selben Daten iterieren, wenn sich da nichts verändert hat?) [3]

    [1] http://pecl.php.net/package/igbinary/
    [3] Smarty (http://smarty.net/) kann unterschiedliche Segmente des selben Templates unabhängig von einander behandeln. So kann ein Teil als fertiger String in den cache wandern, wo ein anderer Teil bei erneutem Aufruf abermals ausgeführt werden muss. Andere TPL-Systeme bieten das vielleicht auch.

    Reply
  12. @Harald Stowasser: Das ging wirklich unter. Sorry. Habt ihr damit gute Erfahrungen gemacht? Ich meine, da baut man ja Komplexität mit auf.

    Habt ihr euch mal Varnish, ESI und stale-while-revalidate angesehen?

    Reply
  13. @Christoph:

    im Gegenteil. Die meisten Hits werden ja schon im APC gefunden. Und der Zeitstempel-Abgleich ist durch die Timestamp-Optimierung in MySQL auch sehr billig. Nur wenn das schief geht, wird in den höher geordneten Levels gesucht

    Wenn du Dir die Seite http://www.badische-zeitung.de/ anguckst, dann fallen die einzeln gecachten Bereiche auf. Das sind die mit den Blauen Überschriften, der Kopfbereich mit den aktuellen Aufmachern, die Boxen in der rechten Spalte, und das User-Lasso am Ende der Seite.

    Ein komplettes-Rendern der Seite dauert ca. 10 Sekunden. (Was eigentlich nie vorkommt)
    Wenn nur ein einzelner Bereich neu gerendert wird ist ein einzelner Server für etwa 2-3 Sekunden beschäftigt.

    Reply
  14. Ganz klar Ansatz 2 (nur ein Cache).
    Wenn man Daten in mehreren Caches hat, wird es verdammt schwer die gecachten Daten zu invalidieren. Wächst die Applikation noch und es werden mehrere Server benötigt wird es schon deutlich komplizierter wie man von Server A Daten im APC-Cache von Server B invalidieren soll, da wird dann das Cache-Management schon mehr Performance kosten, als man gewinnt.

    Die Frage ist auch, ob man überhaupt noch so viel Cachen muss. Mit z.B. dem HandlerSocket Plugin [1] für MySQL lassen sich Geschwindigkeiten erreichen, die fast einem Cache würdig sind. Hierbei muss eine Anfrage nicht mehr den ganzen MySQL-Server durchlaufen (Query Parsing, Query Optimization, …) sondern kann direkt auf die Daten von InnoDB zugreifen, da InnoDB selbst schon einen großen Cache hat (nennt sich „Buffer Pool“) kann dieser je nach Anwendung und Größe des Arbeitsspeichers schon vollkommen ausreichend sein. Und es hat einen Vorteil: Man muss sich nicht mit Cache-Management, dem Invalidieren von Daten usw. beschäftigen, da man gleich auf die aktuellsten Daten in InnoDB zugreifen kann, der die Daten selbst wieder cached.
    Dann müssten nur noch wenige komplex zusammenzustellende Daten aktiv gecacht werden.

    [1] http://yoshinorimatsunobu.blogspot.com/2010/10/using-mysql-as-nosql-story-for.html

    Reply
  15. Ich würde auch eindeutig auf eine Cache-Hierarchie setzen, z.B. also auf diese Abfolge:
    * Applikations-Speicher (Objekte, die bereits während eines Requests in die Applikation geladen wurden, sollten bei weiterem Zugriff verwendet werden, und keine weitere Kopie geladen werden)
    * APC (nur für Objekte, für die clusterweite Cache-Kohärenz nicht zwingend erforderlich ist, i.A. also nicht für Objekte, die aus dem Cache geladen, modifiziert und wieder gespeichert werden könnten)
    * Memcached/Redis
    * Platte?

    Jegliche Cache-Ebene sollte Objekte nach LRU vorhalten, höhere Cache-Ebenen müssen schneller als darunterliegende sein, darunterliegende sollten mehr Objekte als höhere speichern können, sonst bringt das nichts. Genau so, wie es bei modernen CPUs mit L1/L2/L3-Cache und Memory funktioniert.

    Bei Objekten, bei denen man bei dem Speichern in den Cache bereits prophezeien kann, dass sie nur selten benötigt werden, können die oberen Cache-Schichten übersprungen und nur in tieferliegende, größere gespeichert werden. D.h. ja, ich würde beim Anlegen von Cache-Einträgen eine Art „hint“ mitgeben.

    Reply
  16. @Nils

    Die Erfahrungen sind sehr gut. Allerdings steigt natürlich die Komplexität. Vor allem die Semaphore sollte gut getestet sein.

    Dazu kommt noch das einige angemeldete User (z.B. Redakteure) immer die Aktuellste Version sehen. Dort wird dann immer auf das Ergebnis der rendernden Maschine gewartet.

    Beim stale-while-revalidate wird ein assynchrones Verfahren zum neu generieren des Contents genutzt. Kann man durchaus machen. Ansonsten ist das durchaus ähnlich.

    ESI sagt mir jetzt erst mal gar nichts.

    Varnish ist IMHO nur ein weiterer Caching-Proxy. Hier muss man genau abwägen, welche Latenzen man in kauf nimmt. Wir sparen uns jeglichen Proxy. Damit wir in „Echtzeit“ Nachrichten ausliefern können.

    Reply
  17. @Harald: Bei ESI kannst du deine Webseite in Teilabschnitte aufsplitten, die dann separat gecached werden. So kannst du zum Beispiel den eigentlichen Artikel 24h haltbar machen, aber die Sitemap nur 30min.

    PS: Vielleicht kann man sich ja mal zusammensetzen und quatschen. Sitzt ihr in Freiburg?

    Reply
  18. @Nils

    Dann entspricht das Konzept ESI, Teilabschnitte machen wir. Allerdings war mir das zeitabhängige Cachen schon immer ein Dorn im Auge. Darum habe ich den relativ komplizierten Zeitstempel-Abgleich eingebaut.
    Mehrere Trigger sorgen dafür das untergeordnete Datensätze eine Änderung nach oben hin auch anzeigen.

    Ich bin mittlerweile allerdings nicht mehr bei der BZ. Schreib mich einfach mal PM an.

    Reply
  19. Wir verwenden ausschliesslich Memcache.
    APC, XCache und Konsorten laufen lokal auf jedem einzelnen Webserver und bereiten damit viele Probleme bei der Invalidierung von Caches.

    Wenn eine Datenabstraktionsschicht genutzt und komplett auf JOINs verzichtet wird, kann man innerhalb der Abstraktion jeden Datensatz einzeln cachen. Indexer, welche Sets von Datensaetzen liefern sollen, cachen nur noch die IDs. Mit diesen dann die einzelnen Werte per Multiget aus dem Memcache beziehen. Die Erfahrungen damit sind sehr gut. Dieser Mechanismus wird z.B. auf http://www.kwick.de und weiteren firmeninternen Projekten genutzt.

    Frontendcaching mit Varnish wird damit komplett umgangen. Ist bei rein dynamischen Seiten sowieso nervig.

    Reply
  20. Ich hab vor ner Woche einen Gastbeitrag bei drweb.de zu dem Thema eingereicht, der aber wohl irgendwie noch in der Prüfung ist (die Neuen sind nicht die Schnellsten).

    Also am Anfang steht IMMER die Datenbank, die optimieren (z. B. query cache, memtables usw.) bis der Arzt kommt, vielleicht auch mit o. g. Socket handler. Aufwendige Seitenteile finden und so lange wie möglich cachen, um Flaschenhälse zu vermeiden – der Server muss sich ständig langweilen, damit es keine Probleme gibt, wenn mal was Unvorhersehbares passiert. Lieber ein Server zu viel, als einer zu wenig. Ganze Seiten cachen, wo es Sinn macht. Komplette Seiten immer als Gzip sichern (darum ging es hauptsächlich im Dr. Web Beitrag).

    Der APC Cache ist immer (wesentlich) schneller als Memcache, aber Memcache ist besser skalierbar. Da muss man dann abwägen, beides würde ich persönlich nicht empfehlen, aber Abhängigkeiten sind nantürlich machbar z. B. Memcache abfragen, wie lange der Cache gültig ist, und damit APC steuern. Ob das schneller ist, als alle Daten direkt über Memcache zu rödeln, wage ich aber zu bezweifeln.

    Viel wichtiger als die Caches im Detail ist der Webserver und die verwendete Software, da trennt sich dann die lahme Ente vom der Rennmaus.

    Setup, das ich empfehlen kann:
    Cherokee (bringt den Proxy mit, kann Ressourcen in den Arbeitsspeicher laden)
    PHP-FPM (a propos lahme Webmaster :-P)
    APC o. ä. (ohne geht gar nicht)
    MariaDB (beta, aber schnell)

    Reply
  21. @Nils
    Ok, dann mach ich das doch mal. 🙂

    @Harald Stowasser
    Ich hab mich gewundert, warum sich die Seite so lahm anfühlt, aber mir war schnell klar, warum.

    Empty Cache:
    HTTP Requests – 179
    Total Weight – 955.3K

    Primed Cache:
    HTTP Requests – 155
    Total Weight – 432.1K

    Dann
    1 HTML/Text 25.8K
    30 JavaScript File 167.5K
    17 Stylesheet File 51.8K
    1 IFrame 4.0K
    1 Flash Object 19.2K
    21 CSS Image 13.6K
    107 Image 671.9K
    1 Favicon 1.1K

    There are 10 JavaScript scripts found in the head of the document
    There are 1931 DOM elements on the page

    Da hatte ich dann keine Fragen mehr, da nutzt nämlich auch alle serverseitige Optimieren nix mehr.

    Reply
  22. @Oliver

    Ja. Die BZ ist sehr überladen mit Bildern und Ajax-gerafel, damit das dann so wird wie die Projekt-Manager sich das vorstellen.
    Aber das ist die Baustelle der Designer und tangierte mich eigentlich gar nicht.

    Reply
  23. @Harald

    Dann sollte das mal einer den BWL’ern erklären, dass das richtig Besucher kostet. 🙂 Was ich aber eigentlich sagen wollte. Wichtig für ein performantes System ist auch eine schnelle Auslieferung. Je weniger ich den Webserver mit Requests (und vor allem Requests nach großen Dateien) nerve, um so schneller liefert er den Rest aus.

    Performanceprobleme kann man immer mit Hardware erschlagen, aber dass es sich beim Kunden schnell anfühlt ist die wahre Kunst.

    Reply
  24. Ich stelle gerade fest, ich verstehe nur Bahnhof! Wieder etwas, mit dem man sich vielleicht einmal intensiver beschäftigen müsste.. :oS

    Reply

Leave a Comment.

Link erfolgreich vorgeschlagen.

Vielen Dank, dass du einen Link vorgeschlagen hast. Wir werden ihn sobald wie möglich prüfen. Schließen