cPickle Datenbank + threading

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Naja 600-1000 ist nichts.

Als ich noch Plugins mit EventScripts geschrieben habe (für CounterStrike:Source), hat mein rank-Plugin (getestet bis 100k Einträge in einer shelve-db, welche auch pickle nutzt), keinen Lag verursacht. Und wie gesagt, 600-1000 ist nichts. Außerdem könntest du alle Daten im Speicher halten und z.B. zum Rundenende oder Beginn speichern, was wiederum, falls es Laggs geben sollte, das erträglich macht.
the more they change the more they stay the same
BlackJack

@Brainsucker: Ich weiss nicht ob wir das selbe unter dem Begriff „Datenbank“ verstehen. Wenn Du nicht auf den gesamten Datenbestand zugreifen musst, dann ist die Schlussfolgerung ganz sicher nicht, dass man viele kleine Datenbanken anlegt, sondern dass man nicht alles auf einmal in den Speicher lädt, sondern nur das was man benötigt. Aber eben aus *einer* Datenbank. Das was Du da machst — eine grosse Datenstruktur auf einen Schlag zu de-/serialisieren — würde ich auch nicht unbedingt als Datenbank bezeichnen. Ich weiss man kann den Begriff recht weit fassen, aber Datenbank impliziert für mich in der Regel mehr Daten als man im Speicher halten kann oder möchte und mindestens einen einfachen Key/Value-Store um auf die Daten per Schlüssel zuzugreifen.

Das mit den Threads ist sicher nicht zwingend notwendig, denn wie schon mehrfach gesagt, wird das Dein Problem zumindest in CPython mit an Sicherheit grenzender Wahrscheinlichkeit wegen dem GIL nicht lösen.

Du hast noch nicht wirklich etwas zur Beschaffenheit der Datenstruktur und zum Zugriffsmuster gesagt. Deshalb ist es schwer einen konkreten Vorschlag zu machen. Bei den Bordmitteln ist `shelve` als einfache Key/Value-Datenbank und `sqlite` als einfache SQL-Datenbank dabei. Ansonsten kann man sich ausserhalb der Standardbibliothek auch „grössere“ SQL-Datenbanksysteme wie MySQL oder PostgreSQL anbinden, oder eine der NoSQL-Datenbanken für die es eine Python-Anbindung gibt. ZODB oder Durus (falls das Projekt noch „lebt“) sind Objektdatenbanken für Python.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Dav1d hat geschrieben:Naja 600-1000 ist nichts.

Als ich noch Plugins mit EventScripts geschrieben habe (für CounterStrike:Source), hat mein rank-Plugin (getestet bis 100k Einträge in einer shelve-db, welche auch pickle nutzt), keinen Lag verursacht. Und wie gesagt, 600-1000 ist nichts. Außerdem könntest du alle Daten im Speicher halten und z.B. zum Rundenende oder Beginn speichern, was wiederum, falls es Laggs geben sollte, das erträglich macht.

uedi?!

Oder lieg ich da wohl falsch?

Ach ja und lese bitte mal richtig... Ich hab nicht von 600-1000 Einträgen gesprochen sondern von 600-1000 PRO TAG. Was dann mit der zeit eine "relativ" große Datenmenge für eine CSS cPickle Datenbank ergibt ;)
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

BlackJack hat geschrieben:@Brainsucker: Ich weiss nicht ob wir das selbe unter dem Begriff „Datenbank“ verstehen. Wenn Du nicht auf den gesamten Datenbestand zugreifen musst, dann ist die Schlussfolgerung ganz sicher nicht, dass man viele kleine Datenbanken anlegt, sondern dass man nicht alles auf einmal in den Speicher lädt, sondern nur das was man benötigt. Aber eben aus *einer* Datenbank. Das was Du da machst — eine grosse Datenstruktur auf einen Schlag zu de-/serialisieren — würde ich auch nicht unbedingt als Datenbank bezeichnen. Ich weiss man kann den Begriff recht weit fassen, aber Datenbank impliziert für mich in der Regel mehr Daten als man im Speicher halten kann oder möchte und mindestens einen einfachen Key/Value-Store um auf die Daten per Schlüssel zuzugreifen.
Naja wir reden ja hier von cPickle und ich habe bisher noch nicht gehört, dass man mit cPickle.load nur einen Teil der "Datenbank" laden kann oder etwa doch?!

Und wie vorhin schon gesagt, benötige ich permanent zugriff auf ALLE Daten. Was die schlussfolgerung hat, dass ich auch ALLE Daten laden muss :)
BlackJack

@Brainsucker: *Du* redest von `cPickle`. Ich wollte ja gerne mal über die Daten und die Zugriffsmuster reden. Denn vielleicht ist `cPickle` ja nicht das einzige was man verwenden kann. Und vielleicht kann man an der Datenstruktur oder dem Algoritmus auch etwas ändern.

Das Du wirklich immer Zugriff auf alle Daten brauchst, muss man jetzt wohl einfach mal glauben. Das kann ich mir eigentlich nur vorstellen wenn ständig alle Werte für eine Berechnung benötigt werden und dabei auch immer alle Werte geändert werden müssen. Denn sonst erscheint mir der alles auf einmal Laden und alles auf einmal Speichern Ansatz suboptimal.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

BlackJack hat geschrieben:Das kann ich mir eigentlich nur vorstellen wenn ständig alle Werte für eine Berechnung benötigt werden und dabei auch immer alle Werte geändert werden müssen.

So ist es auch. Ich will eine statistik erstellen, die aus den gesamte Daten der Datenbank errechnet wird.

Wie schon gesagt, besteht die Datenbank im wesentlichen aus einem datetime.datetime objekt für jeden Spieler der auf dem Gameserver ist/war.

Eventuell kommen später noch ein paar andere Keys hinzu. Aber ich würde mal sagen 95% des Scripts besteht im wesentlichen aus einem Key, der den Spieler eindeutig identifiziert und einem *subkey*, mit dem datetime objekt als wert.

Edit:

Um den grundsätzlichen *aufbau* der "Datenbank" als Pythoncode ein kleinwenig zu verdeutlichen:

Code: Alles auswählen

visitors = {}
today = datetime.datetime.today()
if not visitors.has_key('creationdate'):
	# Wann wurde die Datenbank erstellt?
	visitors['creationdate'] = today
	
if not visitors.has_key('players'):
	# In diesem Key befinden sich die Daten der Spieler
	visitors['players'] = {}
	
if not visitors['players'].has_key('unique_ip'):
	# Dieser Key enthält die IPs der Spieler
	visitors['players']['unique_ip'] = {}
	
if not visitors['players'].has_key('unique_id'):
	# Dieser Key enthält einen Wert für jeden Spieler, der den Spieler exakt identifiziert.
	visitors['players']['unique_id'] = {}
BlackJack

@Brainsucker: Ich weiss es ist nur Pseudocode, aber `dict.has_key()` ist „deprecated“. In Python 3 gibt es die Methode nicht mehr, aber auch bei Python 2 macht es Sinn stattdessen den ``in``- beziehungsweise den ``not in``-Operator zu verwenden. Es ist kürzer, mindestens im Falle der Negierung (IMHO) auch leichter lesbar, und last but not least (in CPython) geringfügig schneller, da statt eines dynamischen Methodenaufrufs ein einzelner Bytecode für den Operator tritt, der eine von mehreren „magischen“ Methoden aufruft (z.B. `__contains__()`), die schneller erreichbar und aufrufbar sind als die `has_key()`-Methode. Also statt ``if not a_dict.has_key(key):`` besser ``if key not in a_dict:``.

Aus der Beschreibung wird mir nicht ersichtlich wo die Daten alle geändert werden? Denn nur dann macht es Sinn immer alle wieder zu schreiben. Es sieht eher so aus, als wenn da immer neue Daten hinzu kommen. Dann sollte man auch nur diese in eine DB wegschreiben müssen.

Der Aufbau ist mir auch noch nicht ganz klar. Was ist in den innersten Wörterbuchern enthalten?

Code: Alles auswählen

{
    'creationdate': datetime.datetime(2011, 12, 31, 10, 5, 38, 833585),
    'players': {
        'unique_ip': {}, # ?
        'unique_id': {}, # ?
    }
}
Das sieht bis jetzt so aus, als wenn man den grossteil der Daten in eine relationale Datenbank packen könnte. Oder in ein `shelve`. Oder eine Objektdatenbank.

Kannst Du die Berechnung der Statistiken nicht vielleicht so formulieren, dass sie nicht immer komplett neu berechnet werden müssen, sondern dass man ein altes Ergebnis mit neuen Daten aktualisieren kann? Dann bräuchtest Du sowohl beim Lesen als auch beim Schreiben nicht mehr alle Daten, sondern nur noch die aktuellen noch nicht verarbeiteten beziehungsweise gespeicherten Daten.

Bei einer „richtigen“ Datenbank mit Transaktionen könnte man die Statistikberechnung vielleicht sogar komplett aus dem Gameserver heraus nehmen und nebenher regelmässig machen. Das im Spiel eingebundene Skript muss dann nur neue „Rohdaten“ in die DB schreiben und von dort die fertige(n) Statistik(en) auslesen.
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

@Brainsucker, das habe ich gelesen:

Code: Alles auswählen

from datetime import datetime
from cPickle import dump, load
from time import time

stimes = 800*360 # 800 = mittel aus 600 und 1000, 360 für ein Jahr

example = {'foo' : datetime.now(), # ein etwas größeres als dein dict
           'bar' : {'dunno' : dict((i,i) for i in range(15)),
                    'dunno2' : dict((i,i) for i in range(10))},
            'baz' : 123}

example_big = [example for _ in range(stimes)] 
            
dump_start = time()
with open('/tmp/data.pkl', 'w') as f:
    dump(example_big, f)

dump_end = time()
    
with open('/tmp/data.pkl', 'r') as f:
    foo = load(f)
  
load_end = time()

print 'Dump:', dump_end - dump_start
print 'Load:', load_end - dump_end
print 'Data:', len(foo)

Code: Alles auswählen

─[ArchBox][/tmp]╼ time python2 setst.py             
Dump: 0.178967952728
Load: 0.127691984177
Data: 288000
python2 setst.py  0,37s user 0,02s system 99% cpu 0,387 total
Nicht gerade lange…
the more they change the more they stay the same
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Dav1d hat geschrieben:

Code: Alles auswählen

─[ArchBox][/tmp]╼ time python2 setst.py             
Dump: 0.178967952728
Load: 0.127691984177
Data: 288000
python2 setst.py  0,37s user 0,02s system 99% cpu 0,387 total
Nicht gerade lange…

Danke für deine bemühungen, aber dein beispiel ist keinesfalls wirklich praxisnahe, da der Gameserver während dem Betrieb noch mit vielen anderen Berechnungen u.s.w. beschäftigt ist und außerdem in der Regel noch weitere Scripts auf dem Server laufen, kann der Gameserver der cPickle.dump funktion nur einen Teil der CPU zuweisen. Somit vordoppelt oder verdreifacht sich die von dir ausgerechnete Zeit ganz schnell mal. Und das würde ohne Threading ein lagg von 1-1,5 Sekunden verursachen, was nicht gerade schön ist auf einem Gameserver.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

@BlackJack:

Hey, danke das mit dem ´in´ werd ich mir merken, wusste nicht dass das schneller ist als die ´has_key´-Funktion.


So nun Back2Topic:
Die vollständige Datenbank sieht in etwa so aus:

Code: Alles auswählen

{
    'creationdate': datetime.datetime(2011, 12, 31, 10, 5, 38, 833585),
    'players': {
        'unique_id': {'spieler1': {'lastonline': datetime.datetime.today()
                                    }
                    'spieler2': 'lastonline': datetime.datetime.today()}, # !
        'unique_ip': {'IP1': {'lastonline': datetime.datetime.today()}
                    'IP2': {'lastonline': datetime.datetime.today()}}, # !
    }
}
Heißt also, dass jede IP und jeder Spieler, die auf dem Server war eine eigenes datetime objekt bekommt, wann diese(r) zuletzt auf dem Server gesehen wurde.

Das stimmt durchaus, dass es eher sinnvoll wäre ausschließlich nur die Daten zu schreiben, die auch tatsächlich geändert wurden. Allerdings ist da cPickle in diesem Falle sehr begrenzt.

Also wäre wohl tatsächlich eine andere Datenbankart eher geeignet, die nur die Daten schreibt, die die auch wirklich geändert wurden.

Hmm, dann ist wohl cPickle nicht wirklich die Optimale lösung.
Dav1d
User
Beiträge: 1437
Registriert: Donnerstag 30. Juli 2009, 12:03
Kontaktdaten:

Mir ging es darum, dir zu zeigen, dass du versuchst zu optimieren, wenn es nicht sicher ist, ob es überhaupt nötig ist. Außerdem wie BlackJack meinte, mit den falschen Mitteln, da threading dir in Verbindung mit pickle überhaupt nichts nützen könnte.
the more they change the more they stay the same
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Ich weiß aus der Praxis, dass threading zum gewünschten ergebniss führt.

Einmal hatte ich selbes problem, dass beim speichern/laden der Daten der Server gelaggt hat. Kaum waren die threads drin, hats nicht mehr gelaggt.

Allerdings hatte dies andere nachteile, da ich damals noch kein Lock() verwendet hatte.

Und nun wollte ich die sache richtig angehen und eben die sache so schreiben, dass die Nachteile nicht mehr auftreten. Desshalb hab ich hier um hilfe angefragt.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Also Leute, ich hab mich nun umentschieden eine richtige sqlite3 Datenbank anstatt dem cPickle Serialisierungsmurks zu benutzen. Somit kann das Topic hier dicht gemacht werden und ich mach dann im passenden Unterforum ein neues Topic auf wenn ich fragen zu sqlite habe.


Danke euch allen für die hilfe :)
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

gut. ;-)

Wenn du wirklich durchweg "nur" Schlüssel-Werte Paare schreibst / liest, dann sollte für dich ein Key-Value Store (wie z.B. Redis) auch interessant sein.

Zum einen sind die ziemlich schnell, zum anderen kann zumindest Redis die Daten asynchron im Hintergrund auf die HD schreiben. Und es gibt auch einen Object-Mapper für Redis.

Gruß, noisefloor
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

Hey,

ja der größteil sind natürlich schon schlüssel/werte paare in der Datenbank.

Allerdings das was mich bisher am aller meißten über sqlite 'verärgert', ist die umständliche art abzufragen ob für einen bestimmten spieler bereits eine Reihe in der Datenbank existiert und man dann je nach dem die Reihe Updaten oder Inserten muss. Etwas recht umständlich wie ich finde.

Code: Alles auswählen

# Wir gehen davon aus, dass "cur" eine Variable ist, die eine sqlite3.Connect instanz beinhaltet.
cur.execute("SELECT * FROM datenbanke WHERE spielerid=?", (spielerid,))

Bei den dicts war das deutlich einfach mit has_key bzw. wie im verlauf des topics schon beschrieben den ´in´ operator zu benutzen.
Benutzeravatar
noisefloor
User
Beiträge: 3856
Registriert: Mittwoch 17. Oktober 2007, 21:40
Wohnort: WW
Kontaktdaten:

Hallo,

na ja, was heißt umständlich. Wenn du wissen willst, ob etwas in einer (relationalen) Datenbank abfragen willst, du brauchst du einen Query. Gibt der "None" zurück, ist nichts drin.

Vom Prinzip ist das nicht anderes als das, was du mit dem Dict auch machst. :-)

Gruß, noisefloor
BlackJack

@Brainsucker: Statt ``*`` könntest Du vielleicht besser ``count(*)`` oder einfach nur ``1`` selektieren. Denn die Daten möchtest Du ja gar nicht haben.

Ausserdem könntest Du statt abzufragen auch einfach das machen was erwartungsgemäss häufiger klappen wird und testen ob es wirklich funkioniert hat, und falls nicht das andere machen.
Brainsucker
User
Beiträge: 68
Registriert: Mittwoch 16. November 2011, 23:20

BlackJack hat geschrieben:@Brainsucker: Statt ``*`` könntest Du vielleicht besser ``count(*)`` oder einfach nur ``1`` selektieren. Denn die Daten möchtest Du ja gar nicht haben.
Hey, danke wusste gar nicht dass man auch irgendetwas x-beliebiges selektieren kann. Das macht die sache schon etwas einfacher um den datenfluss so gering wie möglich zu halten.
Antworten