Server: Threadverteilung
-
Hi,
ich spiele grad ein wenig mit Server-Code herum, dabei hab ich mal ne Frage. Dabei geht's mir darum, wie/wo ich Threads einsetze, dabei sehe ich im Moment zwei Möglichkeiten:
Option 1: Es gibt genau einen Netzwerkthread (für den Listener und alle Client-Sockets), dazu N Worker-Threads, welche sich aus einer vom Netzwerkthread gefüllten Job-Queue die nächste Aufgabe (also eingehende Nachricht) holen, bearbeiten und was halt so anfällt zurückschicken.
Option 2: Es gibt N Netzwerkthreads, die jeweils etwa gleichviele Client-Sockets "bearbeiten", also auf Nachrichten warten, verarbeiten und Anfallendes zurückschicken. Es gibt hier im eigentlichen Sinne also keine reinen Worker-Threads.
Ich tendiere im Moment zu Option 2, einfach weil da die Job-Queue-Synchronisation wegfällt und sowieso nicht sonderlich viel zu berechnen ist, gibt auch nicht viel Disk I/O. Hat da jemand Erfahrung, welche Option (evtl gibt's auch noch andere Vorschläge?) performanter ist oder sein könnte? Oder ist das abhängig vom Szenario (Option 1 für viel Bearbeitung und Disk I/O, Option 2 für schnelle Job-Abarbeitung)? Das Ganze auch in Hinblick auf Large-Scale, bei 20 Verbindungen spielt's ja keine Rolle.
-
*Option 3:* Ohne Threads, per select().
Ich persönlich verwende Option 1. Allerdings starte ich für jeden Client einen eigenen Thread, weil Performance-Tests ergeben haben, dass es keinerlei messbaren Unterschied zwischen Fire-And-Forget-Thread und Thread-Pool gibt.
Ich habe allerdings maximal ein paar Dutzend Anfragen/Sekunde. Keine tausenden. Ich kann also nicht beurteilen wie gut das bei tausenden Jobs/Sekunde skaliert.
-
Option 2 hat den nachteil, dass sie schlecht damit klarkommt, wenn zufällig grad alle Verbindungen die von Thread X bedient werden gleichzeitig was wollen, und alle anderen nur rumhängen und nix tun.
Für Large-Scale wird oft das "proactive pattern" verwendet, was entfernte Ähnlichkeiten zu deiner Option 1 hat. Wenn dich interessiert wie das aussieht hilft dir Google. Boost.ASIO ist z.B. eine Netzwerk-Library die das "proactive pattern" implementiert. Oder guck dir an wie IO-Completion-Ports unter Windows funktionieren -- mit denen kann man auch "proactive" arbeiten.
Wenn ich derzeit einen Server für irgendwas in C++ programmieren müsste, wo ich viele viele gleichzeitige Verbindungen erwarte, würde ich auf jeden Fall die ASIO nehmen. Vorausgesetzt natürlich es lässt sich kein passender Application-Server finden, den man verwenden könnte.
-
frenki schrieb:
*Option 3:* Ohne Threads, per select().
Ich persönlich verwende Option 1. Allerdings starte ich für jeden Client einen eigenen Thread, weil Performance-Tests ergeben haben, dass es keinerlei messbaren Unterschied zwischen Fire-And-Forget-Thread und Thread-Pool gibt.
Ich habe allerdings maximal ein paar Dutzend Anfragen/Sekunde. Keine tausenden. Ich kann also nicht beurteilen wie gut das bei tausenden Jobs/Sekunde skaliert.
Damit bekommt man nicht nur ein Problem wenn man zuviele Anfragen pro Sekunde bekommt, sondern wenn man zu viele offene Verbindungen hat. Also z.B. 100 Requests/Sekunde, und jeder Request braucht 30 Sek. bis er fertig abgearbeitet ist. Macht 3000 Threads = wahnsinns Overhead. Auf einer 32 Bit Maschine geht das nichtmal mehr, weil man die 3000 Stack-Frames nichtmal im Adressraum unterbekommt.
-
war es nicht so, daß ein böser übeltäter bei nur einem listener den listener leicht ddosen kann und deinen server zu einem großnotstand führt?
alles andere sollte egal sein, denn wenn du 100000 gleichzeitige kunden hast, kannste dir auch einen quadruple p8 mit 64G ram hinstellen und so.
die wichtige frage ist, ob du pro client einen thread nimmst. das kann bei komplexeren protokollen die programmierung stark vereinfachen und ist theoretisch(!) in keiner langsameren komplexitätsklasse als threadpooling.
-
hustbaer schrieb:
Macht 3000 Threads = wahnsinns Overhead. Auf einer 32 Bit Maschine geht das nichtmal mehr, weil man die 3000 Stack-Frames nichtmal im Adressraum unterbekommt.
unterkommen geht. man muß ja nicht das voreingestellte 1M für den stack lassen und kann verfügen, daß rekursionen nur erlaubt sind, wenn sie logarithmisch beschränkt sind (oder wie damals die bundesbahn rekursionen ganz verbieten
) und speicherallokationen über 4k auf dem stack nicht sein dürfen.
threaderstellung und -zerstörung sind trotzdem ein doller overhead, dem will ich nicht widersprochen haben.edit: wieviele threads kann man eigentlich in einer sekunde starten und löschen? hab ich nicht vor jahren schon 10000 pro sekunde gehabt? vielleicht ist das gar nicht mehr so wichtig.
-
Ich erzeuge threads immer nur mit 4KB stack und habe bis jetzt noch nie einen overflow bekommen.
-
volkard schrieb:
die wichtige frage ist, ob du pro client einen thread nimmst. das kann bei komplexeren protokollen die programmierung stark vereinfachen und ist theoretisch(!) in keiner langsameren komplexitätsklasse als threadpooling.
Huh, da hab ich noch Bedenken, viele Verbindungen sind nur sehr kurzlebig; andererseits soll der Server nur unter Linux laufen, da sollen Threads ja recht leichtgewichtig sein. Mal sehen, da es eh nur eine Spielerei ist, nehme ich erstmal das (für mich) schwerere, Pooling.
hustbaer schrieb:
Option 2 hat den nachteil, dass sie schlecht damit klarkommt, wenn zufällig grad alle Verbindungen die von Thread X bedient werden gleichzeitig was wollen, und alle anderen nur rumhängen und nix tun.
Das ist ein gutes Argument, da muss ich mal drüber nachdenken.
hustbaer schrieb:
Für Large-Scale wird oft das "proactive pattern" verwendet, was entfernte Ähnlichkeiten zu deiner Option 1 hat. Wenn dich interessiert wie das aussieht hilft dir Google. [...] Oder guck dir an wie IO-Completion-Ports unter Windows funktionieren -- mit denen kann man auch "proactive" arbeiten.
So viel hab ich bei Google leider nicht gefunden. Das was ich gefunden hab, sah für mich so aus: Reactive=Ich reagiere auf z.B. per Callback eingehende Daten; Proactive=Ich sage, wieviele Daten ich brauche und mache nix, bis ich sie hab. Stimmt das so in etwa? Wenn ja, eignet sich für mich aber, glaube ich, der reaktive Ansatz weit mehr. Ich hab keine festen Nachrichtenlängen und das Protokoll ist Ping-Pong-mäßig, vor meiner Antwort gibt's auf der Leitung auch keine neue Anfrage.
Mit den IO-Completion-Ports hatte ich mich mal befasst, das Konzept hatte mich damals schon beeindruckt. Passt aber irgendwie nicht zu dem, was ich jetzt unter "proactive" verstanden hab.edit:
volkard schrieb:
war es nicht so, daß ein böser übeltäter bei nur einem listener den listener leicht ddosen kann und deinen server zu einem großnotstand führt?
Also machen mehrere Listener auf dem selben Port Sinn? Was wäre denn eine gute Anzahl?
-
volkard schrieb:
edit: wieviele threads kann man eigentlich in einer sekunde starten und löschen? hab ich nicht vor jahren schon 10000 pro sekunde gehabt? vielleicht ist das gar nicht mehr so wichtig.
Das kann man getrost vernachlässigen.
Mein Performance-Test stellt 100 Verbindungen zum Server her, schickt einen simplen Job (SQL-Statemnt: SELECT * FORM TABELLE_MIT_CA_200_DATENSAETZEN) und trennt die Verbindung wieder.
Anschliessend wird die benötigte Zeit durch 100 geteilt um die Dauer pro Verbindung auszurechnen: 2 Millisekunden pro Verbindung. Das ist via localhost auf einem Quadcore unter Windows Vista.
Bedeutet 2 Millisekunden zum Thread erstellen, Datenbank-Zugriff, Datenübertragung, Thread zerstören.
Da 95 % der echten Jobs 0.2 bis 0.5 Sekunden dauern, mache ich mir um die lächerliche Zeit zum Erstellen des Threads keinen Kopf. Ich optimiere nur da wo es Weh tut. Und hier spüre ich nichtmal ein Kitzeln...
-
volkard schrieb:
hustbaer schrieb:
Macht 3000 Threads = wahnsinns Overhead. Auf einer 32 Bit Maschine geht das nichtmal mehr, weil man die 3000 Stack-Frames nichtmal im Adressraum unterbekommt.
unterkommen geht. man muß ja nicht das voreingestellte 1M für den stack lassen und kann verfügen, daß rekursionen nur erlaubt sind, wenn sie logarithmisch beschränkt sind (oder wie damals die bundesbahn rekursionen ganz verbieten
) und speicherallokationen über 4k auf dem stack nicht sein dürfen.
threaderstellung und -zerstörung sind trotzdem ein doller overhead, dem will ich nicht widersprochen haben.edit: wieviele threads kann man eigentlich in einer sekunde starten und löschen? hab ich nicht vor jahren schon 10000 pro sekunde gehabt? vielleicht ist das gar nicht mehr so wichtig.
Was den Stack angeht: kann man unter Windows überhaupt weniger als die Default-Size anfordern? Ich weiss man kann das STACK_SIZE_PARAM_IS_A_RESERVATION Flag mitgeben, aber ich bin mir nicht so sicher ob Windows nicht trotzdem mindestens soviel verwendet wie im PE-Image angegeben ist. Natürlich kann man das Default im PE-Image ändern...
Was Threads/Sekunde angeht: man kann ja die Connection-Threads trotzdem poolen. Wenn eine Connection geschlossen wird steckt man den Thread einfach in den Pool zurück, und wenn eine neue Connection ankommt nimmt man einen aus dem Pool.
Was man IMO aber nicht vernachlässigen sollte sind andere Dinge wie z.B. Database Connections. Der Application-Server mag 10000 Threads noch leicht mitmachen, aber ob der DB-Server so glücklich ist wenn er 10000 DB-Connections gleichzeitig hat? Natürlich *kann* man auch mit ein Thread pro Connection weniger DB-Connections als Threads verwenden, aber was einstmal so einfach und schön war, wird dadurch auch gleich komplizierter.
-
volkard schrieb:
war es nicht so, daß ein böser übeltäter bei nur einem listener den listener leicht ddosen kann und deinen server zu einem großnotstand führt?
Inwieweit helfen da mehrere Listener-Threads?
Davon abgesehen ist DDOS sowieso etwas wogegen man sich nicht wirklich schützen kann. Man kann zwar ein System bauen dass nicht unter einer DDOS zusammenbricht, aber man kann soweit ich weiss nicht verhindern dass die Attacke die Erreichbarkeit des Systems (für neue Connections) zumindest gravierend verschlechtert.
-
Badestrand schrieb:
hustbaer schrieb:
Für Large-Scale wird oft das "proactive pattern" verwendet, was entfernte Ähnlichkeiten zu deiner Option 1 hat. Wenn dich interessiert wie das aussieht hilft dir Google. [...] Oder guck dir an wie IO-Completion-Ports unter Windows funktionieren -- mit denen kann man auch "proactive" arbeiten.
So viel hab ich bei Google leider nicht gefunden. Das was ich gefunden hab, sah für mich so aus: Reactive=Ich reagiere auf z.B. per Callback eingehende Daten; Proactive=Ich sage, wieviele Daten ich brauche und mache nix, bis ich sie hab. Stimmt das so in etwa? Wenn ja, eignet sich für mich aber, glaube ich, der reaktive Ansatz weit mehr. Ich hab keine festen Nachrichtenlängen und das Protokoll ist Ping-Pong-mäßig, vor meiner Antwort gibt's auf der Leitung auch keine neue Anfrage.
Mit den IO-Completion-Ports hatte ich mich mal befasst, das Konzept hatte mich damals schon beeindruckt. Passt aber irgendwie nicht zu dem, was ich jetzt unter "proactive" verstanden hab.Reactive ist "ruf Funktion XYZ auf, wenn auf Socket X Daten angekommen sind".
Die Funktion XYZ ist dann dafür zuständig die Daten zu lesen und zu verarbeiten.Proactive ist "lies 10 Bytes von Socket X in den Puffer Y, und ruf mir die Funktion XYZ auf, sobald die 10 Byte gelesen wurden". Wenn die Funktion XYZ aufgerufen wurde, stehen die 10 Byte bereits in Puffer Y.
Der grosse Unterschied ist: das proactive Pattern kann man leicht erweitern um z.B. aus dem "lies 10 Bytes" ein "lies ein Foo-Paket" zu machen. Wobei das "Foo-Paket" etwas ist was man selbst definieren kann, und auch nicht immer gleich gross sein muss. Siehe z.B. boost::asio::async_read_until:
http://www.boost.org/doc/libs/1_37_0/doc/html/boost_asio/reference/async_read_until.htmlNatürlich lässt sich Proactive auch über eine reaktive API implementieren. Boost.ASIO macht das z.B. auch, wenn's auf dem jeweiligen System keine besser geeignete API gibt.
-
Badestrand schrieb:
...
das design hat nicht viel mit meinung und tendieren zu tun, es haengt vom anwendungsfall ab und was das ziel des ganzen ist.
dass es "gut" laufen soll ist klar, aber wie du 'gut' definierst ist dann entscheident.wenn du immer 10 verbindungen pro sekunde hast und diese lange zum abarbeiten brauchen, nimmst du wohl ein anderes system als wenn du 10000 verbindungen hast.
hast du konstant 10000 verbindungen von denen jede ab und zu was machen will, kannst du ebenfalls anders damit arbeiten als mit 16 unter hochlast.
hast du x unabhaengige verbindungen, ist die performance skalierung vermutlich auch anders als wenn sie eine knapper resource sich teilen muessen.
das haengt am ende auch von der umgebung ab, wenn das verwendete OS am ende mit der applikation um die rechenleistung kaempft oder alleine schon wegen den stacks speicher auslagert, ist multithreading einfach nur dumm und kontraproduktiv.
frenki schrieb:
weil Performance-Tests ergeben haben, dass es keinerlei messbaren Unterschied zwischen Fire-And-Forget-Thread und Thread-Pool gibt.
dann waren deine tests nicht representativ.
es gibt nicht um sonst threadpools, die vom design und implementieren her aufwendiger sind.
simples beispiel ist make, bau ein _grosses_ projekt mit weit mehr threads als du cores hast, es wird laenger dauern.
-
simples beispiel ist make, bau ein _grosses_ projekt mit weit mehr threads als du cores hast, es wird laenger dauern.
Das liegt aber daran dass man die Caches thrasht, und nicht daran dass das Erzeugen/Verwalten/Freigeben von Threads so lange dauert. Und vielleicht noch daran dass Disk-IO schneller geht, wenn nicht 100 Threads gleichzeitg 100 verschiedene Files lesen/schreiben, sondern alles hübsch hintereinander gelesen/geschrieben wird.
Bei Client-Server Anwendungen fällt das Erstellen eines Threads wirklich nicht sehr ins Gewicht. Viel schwerer wiegen da z.B. DB-Verbindungen. Diese zu poolen ist viel wichtiger als Threads zu poolen. Ich persönlich würde beides machen, kann man natürlich auch schön kombinieren (jeder Thread im Pool bekommt "seine" DB-Verbindung, die er auch nichtmehr hergibt solange er läuft).
-
hustbaer schrieb:
simples beispiel ist make, bau ein _grosses_ projekt mit weit mehr threads als du cores hast, es wird laenger dauern.
Das liegt aber daran dass man die Caches thrasht,
nein, an caches liegt es nicht, aber allgemein kann man sagen, es liegt daran das eine knappe resource geteilt wird.
und nicht daran dass das Erzeugen/Verwalten/Freigeben von Threads so lange dauert.
wann sprach ich davon dass es daran liegt?
Und vielleicht noch daran dass Disk-IO schneller geht, wenn nicht 100 Threads gleichzeitg 100 verschiedene Files lesen/schreiben, sondern alles hübsch hintereinander gelesen/geschrieben wird.
s.o.
Bei Client-Server Anwendungen fällt das Erstellen eines Threads wirklich nicht sehr ins Gewicht.
je nach anwendungsfall faellt es in gewicht.
Viel schwerer wiegen da z.B. DB-Verbindungen. Diese zu poolen ist viel wichtiger als Threads zu poolen. Ich persönlich würde beides machen, kann man natürlich auch schön kombinieren (jeder Thread im Pool bekommt "seine" DB-Verbindung, die er auch nichtmehr hergibt solange er läuft).
ja, knappe resourcen sollte man nicht ueberrennen, sonst verschlucken sie sich. das ist keine DB spezifische sache.
Wegen dieser fehlerhaften vorgehenweisen haben einige datenbanken selbst schon queues fuer requests. denen ist es entsprechend egal wieviele threads dann requesten, allerdings hat man dann auf einem system eventuell zwei threadpools, das reicht schon damit es wieder suboptimal laeuft.
-
rapso schrieb:
hustbaer schrieb:
simples beispiel ist make, bau ein _grosses_ projekt mit weit mehr threads als du cores hast, es wird laenger dauern.
Das liegt aber daran dass man die Caches thrasht,
nein, an caches liegt es nicht, aber allgemein kann man sagen, es liegt daran das eine knappe resource geteilt wird.
und nicht daran dass das Erzeugen/Verwalten/Freigeben von Threads so lange dauert.
wann sprach ich davon dass es daran liegt?
Ich hatte es so interpretiert. Wenn du was anderes gemeint hast, war auf jeden Fall nicht klar was.
Viel schwerer wiegen da z.B. DB-Verbindungen. Diese zu poolen ist viel wichtiger als Threads zu poolen. Ich persönlich würde beides machen, kann man natürlich auch schön kombinieren (jeder Thread im Pool bekommt "seine" DB-Verbindung, die er auch nichtmehr hergibt solange er läuft).
ja, knappe resourcen sollte man nicht ueberrennen, sonst verschlucken sie sich. das ist keine DB spezifische sache.
Wegen dieser fehlerhaften vorgehenweisen haben einige datenbanken selbst schon queues fuer requests. denen ist es entsprechend egal wieviele threads dann requesten, allerdings hat man dann auf einem system eventuell zwei threadpools, das reicht schon damit es wieder suboptimal laeuft.Ja logisch sind DB-Connections nicht das einzige. Aber halt sehr häufig anzutreffen. Und eine DB-Connection ist oft ziemlich "heavy". Da geht's nicht nur um Requests, sondern einfach um die Connection selbst.
Was Requests angeht: das erwarte ich mir schon von einer Datenbank, dass die selbst dafür sorgt Requests so abzuarbeiten dass es nix ausmacht wenn man 1000 gleichzeitig absetzt.