Cache Objekt resetten im thread Umfeld...

Django, Flask, Bottle, WSGI, CGI…
Antworten
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Um SQL Abfragen zu vermeiden packe ich Daten in ein cache Dictionary. Beim ändern der Datenbank Daten wird das dict einfach mit .clear() "geleert"...

Das Problem: Mit fastCGI laufen ja mehrere Python Prozesse unabhängig voneinander. Das cache Dictionary also auch. Wird im einem Prozess der cache geleert "überträgt" sich das ja nicht auf die anderen...

Wie könnte man das lösen???

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
noisefloor
User
Beiträge: 3853
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

Memcached oder ein KV-Store, welches im RAM läuft, als Cache-Server?

Gruß, noisefloor
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Der einfachste Weg veraltete Caches zu verhindern, ist es ja alle Threads/Prozesse neu zu starten.

Nur wie?

Die Hammer Methode: killall python :shock: ...was ich sogar in einem externen Plugin über die Web Oberfläche zugänglich gemacht habe, siehe: https://github.com/jedie/PyLucid-system ... er.py#L274
Mit killall erschlägt man natürlich alle seine python Prozesse, was nicht immer gut ist :wink:

Eine neue Idee: Der Prozess die Daten ändern, hält diesen Zeitpunkt in der Datenbank fest. Jeder Prozess schaut als erstes in die Datenbank nach und unternimmt was.

Genauer: Man könnte man das ganze in einer WSGI/Django middleware machen. Mit einem STARTUP = time.time() auf Modulebene, weiß der Thread seinen Startzeitpunkt.
Vor der Request Abarbeitung schaut er in der DB nach, ob es dort einen neueren Timecode gibt, wenn ja, schießt er sich selbst mit os.kill(pid) ab.

Einige Nachteile hat das ganze allerdings: Es gibt mehr DB-Queries und das abschießen mit os.kill() hat wahrscheinlich Nebenwirkungen. Führt der aktuelle Request zu einem Fehler beim Client oder gleicht das fastCGI/mod_wsgi automatisch aus?

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
deets

Das killen von Prozessen wird 100%ig zu einem Fehler fuehren. Und ausserdem deine Cache-Performance deutlich mehr belasten, als noetig. Denn so kommen ja die gesamten setup-Kosten immer wieder auf dich zu - bloss um den Cache zu leeren?!?

Dein DB-Ansatz ist schon ok, ist eine Abart einer simplen message-queue. Und genau sowas koennte deine Loesung sein - zeromq zB.
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

Python-Prozesse zu töten, weil ein Dict nicht passt, halte ich für Wahnsinn :) Mach es wie noisefloor vorschlägt. Redis oder memcached sind hier gute Kandidaten.

Oder aber baue selbst etwas in Python. Für gute Performance bietet sich z.B. Tornado an. Da reicht dann wohl ein Prozess. Sieht ungefähr so (ungetestet, aus dem Kopf aufgeschrieben) aus:

Code: Alles auswählen

import tornado.ioloop, tornado.web

class CacheHandler(tornado.web.RequestHandler):
    def initialize(self):
        self.cache = {}
        
    def get(self, key):
        self.finish(self.cache[key])
    
    def put(self, key):
        self.cache[key] = self.request.body
        self.finish()
    
    def delete(self, key):
        del self.cache[key]

Application([('/(.*)', CacheHandler)]).listen(8888)
tornado.ioloop.IOLoop.instance().start()
Jetzt will man wahrscheinich noch einbauen, das Dinge nur eine gewisse Zeitspanne im Cache bleiben oder das der Cache nach einem LRU-Algorithmus eine maximale Speichergröße nicht überschreitet. Ich würde dafür einfach alle Größen der Bytestrings aufaddieren.

Nutze dann den asynchronen Tornado-HTTP-Client, um aus deinem Webserver auf diesen Caching-Server zuzugreifen.

Natürlich müsste man mit diesem Server nicht per HTTP sprechen, aber so war es extrem einfach.

Schließlich kann ich nicht enden, um darauf hinzuweisen, dass so ein Dienst auch eine nette Gelegenheit ist, mal nodejs auszuprobieren, denn node ist genau für derartige kleine Server-Prozesse ideal geeignet - und JavaScript ist auch nicht viel komplizierter als Python ;)

Stefan
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Noch ein paar Worte zum Anwendungsfall:

Es geht nicht um HTML-Seiten cache. Dazu nutzte ich das Django cache framework.
Es geht um kleinere Cache Objekte, die DB-Queries einsparen oder evtl. um den url-cache von Django.
sma hat geschrieben:Python-Prozesse zu töten, weil ein Dict nicht passt, halte ich für Wahnsinn :)
Das es keine Optimale Lösung ist, war klar.

Am besten wäre es natürlich, wenn man gezielt die Objekte (Liste/tuple/dict) in jedem Thread/Prozess "resetten" könnte.

Welche Möglichkeiten hat man mit anderen Threads/Prozessen zu kommunizieren? (Außer über die Datenbank)

Das der Cache veraltet ist, kommt recht selten vor. Nur dann, wenn man eine Seite löscht oder im Baum verschiebt oder umbenennt. Das macht man ja nicht jede Sekunde ;)

Bei einer Lösung mit Datenbank sehe ich zwei Möglichkeiten:
1. Ein DB-Query pro Request: Jeder Prozess schaut am Anfang eines Requests nach, ob alle Caches aktuell sind. Bzw. ob es einen Cache resetten muß:
  • * Jeder Prozess merkt sich seine Startzeit (START=time.time() auf Modulebene)
    * Der Prozess der die Daten ändert, schreibt in die DB die Änderungszeit
    * Bei Request-Start holt sich jeder Prozess aus der DB die letzte Änderungszeit und vergleicht sie mit der eigenen Startzeit.
2. DB-Query nur beim App-Start + Änderung: Jeder Prozess schreibt seine pid + Startzeit in die DB.
  • * Der Prozess der die Daten ändert, weiß welche pids veraltete Daten hat -> abschießen oder irgendwie mit allen pids kommunizieren, das die Caches resetten werden müßen.
Eine Abwandlung der ersten Variante wäre es, nicht die Datenbank, sondern die Informationen in den Django-Cache zu speichern.

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
sparrow
User
Beiträge: 4183
Registriert: Freitag 17. April 2009, 10:28

Was spricht denn dagegen, dass du dafür auch das Django-Cache-Framework verwendest?
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Ja, das hab ich mich auch schon gefragt ;)

Schätze mal die Reihenfolge nach Kosten/Nutztenfaktor ist so:
1. lokales dict/tuple/liste etc.
2. Django cache
3. Datenbank

Wobei es beim Django cache auf das verwendete Backend ankommt:
  • 'django.core.cache.backends.db.DatabaseCache'
    'django.core.cache.backends.dummy.DummyCache'
    'django.core.cache.backends.filebased.FileBasedCache'
    'django.core.cache.backends.locmem.LocMemCache'
    'django.core.cache.backends.memcached.MemcachedCache'
    'django.core.cache.backends.memcached.PyLibMCCache'
Doch auch beim LocMemCache oder Memcache dürfte ein lokales dict schneller sein. Denn die Daten im cache werden durch pickle gejagt.

Wenn ich mir die Sourcen von LocMemCache ansehe, dürfte man auch da Probleme haben, das ein Cache in einem Thread/Prozess nicht mehr aktuell ist, wenn ein andere die Basisdaten verändert hat... In der Doku steht's auch:
Note that each process will have its own private cache instance, which means no cross-process caching is possible. This obviously also means the local memory cache isn't particularly memory-efficient, so it's probably not a good choice for production environments. It's nice for development.
Diese Probleme wird man wohl nicht bei Memcache, DatabaseCache und FileBasedCache haben, oder?

Wie wäre eine Kombination?
Die Daten sind eigentlich in einem lokalen dict. Die Informationen, ob die Cache Daten aktuell sind speichert man aber im Django cache backend.

Das dürfte sich dann lohnen, wenn mehr mehrmals auf den Cache innerhalb eines Requests zugreift. Und wenn man wesentlich öfters "lesend" und selten "schreibend" auf den Cache zugreift.

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
deets

Es ist doch im Grunde eine ganz einfache Sache:

- du hast mehrere Prozesse
- du hast prozess-lokale Caches
- du hast konkurrierende Aenderungen der Daten hinter diesen Caches

Und da gibt es jetzt nur 2 Moeglichkeiten:

a) du informierst im Falle einer Aenderung alle Prozesse darueber, was sich geaendert hat. Oder mindestens *das* sich was geaendert hat.
b) die Prozesse fummeln das selbst raus, und dazu muessen sie irgendwie auf eine geteilte Resource wie zB die Datenbank, einen shared-memory oder ein Dateisystem zugreifen.

Loesung a ist theoretisch das "eleganteste", weil du - wenn keine Aenderungen am System stattfinden - keinerlei Kosten hast - vorrausgesetzt, das IPC-System hat keine versteckten Kosten. Praktisch ist es aber ziemlich komplex, nicht zuletzt, weil du ueberhaupt erstmal wissen musst, wer alle deine Prozesse sind, die muessen sich dann anmelden an dem IPC-System usw.

Fuer Loesung b spricht daher, dass sie simpel zu implementieren ist. Einfach die geteilte Resource bestimmen, und da zB ueber einen Timestamp oder Counter kommunizieren.

Und weil du eh schon eine geteilte Resource hast, naemlich die Datenbank, wuerde ich auch die einfach nehmen. Pro request einmal zu fragen "gab's Aenderungen?" geht im Rauschen unter, und damit ist der Drops gelutscht.

Ob einer der django-cache-Backends sowas implementiert kann ich nicht sagen. Aber zur Not bastelst du das basierend auf dem LocMemCache halt selbst, das sollte sich in ein paar Zeilen erledigt haben. Und dafuer ist die Nutzung dann uniform.
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Jep, du hast die Sache genau verstanden...
deets hat geschrieben: a) du informierst im Falle einer Aenderung alle Prozesse darueber, was sich geaendert hat. Oder mindestens *das* sich was geaendert hat.
b) die Prozesse fummeln das selbst raus, und dazu muessen sie irgendwie auf eine geteilte Resource wie zB die Datenbank, einen shared-memory oder ein Dateisystem zugreifen.
Genau!

Lösung A ist besser. Doch wie alle Prozesse informieren? Somit ist Lösung B attraktiver.

Wenn man als "geteilte Resource" ein cache Backend von Django nimmt, kann der Anwender sogar ab Django v1.3 sich separat dafür ein Backend wählen.

Vielleicht lohnt es sich daraus ein kleines Allgemeine Django-App zu machen...

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
sparrow
User
Beiträge: 4183
Registriert: Freitag 17. April 2009, 10:28

Vielleicht habe ich das überlesen, aber was steht denn eigentlich in diesem dict das du unbedingt cachen willst?

Wenn das nur eine einfache Key-Value Zuordnung ist, ist das natürlich genau das was du über das Django-Caching-System abbilden kannst.
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Wie schon geschrieben, im Django-Cache kann man alles schmeißen, was mit pickle verarbeitet werden kann ;)

In einem Anwendungsfall, ist es allerdings nur ein ID-URL dict: https://github.com/jedie/PyLucid/blob/c ... ee.py#L341

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Hab eine erste Implementierung: https://github.com/jedie/django-tools/c ... 5e78d52f3d

So ganz bin ich davon aber noch nicht überzeugt... Irgendwelchen Kommentare?

EDIT: Und die Änderungen in einem PyLucid branch dazu: https://github.com/jedie/PyLucid/commit ... c718c97fea

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Benutzeravatar
jens
Python-Forum Veteran
Beiträge: 8502
Registriert: Dienstag 10. August 2004, 09:40
Wohnort: duisburg
Kontaktdaten:

Hab es nochmal überarbeitet: https://github.com/jedie/django-tools/c ... 5bcb10d9cf

Neben Dokumentation habe ich auch noch eine weitere Funktionalität eingebaut: Komischerweise bleibt die Information des letzten reset Zeitpunktes nicht immer im django cache. Warum weiß nicht nicht genau.

Deswegen speichre ich den reset Zeitpunkt nochmal lokal in self._OWN_RESET_TIMES. Beim Request start, also in LocalSyncCache.check_state(), wird der timestamp nochmal in den cache eingetragen, wenn er nicht mehr existiert...

Eigentlich unschön. Aber besser als wenn der Reset Zeitpunk verloren geht und die anderen Threads mit veraltetem Cache weiter machen würden...

GitHub | Open HUB | Xing | Linked in
Bitcoins to: 1JEgSQepxGjdprNedC9tXQWLpS424AL8cd
Antworten