Mehrere TCP/IP Verbindungen



  • Gast3 schrieb:

    wobei du das ja auch "...so lange nicht mehr gleichzeitig laufen wollen als man physikalische CPUs hat merkt man keinen Unterschied..." so siehst

    Nein, das sehe ich nicht so, das hast du falsch verstanden.
    Ich hab auch in meiner letzten Antwort an dich versucht dieses Misverständnis aufzuklären.
    Ich meinte: wenn es mehr zu tun gibt als CPU Leistung da ist, dann wird alles langsam, egal welche IO Strategie man verwendet. Die Frage ist nur: wie schnell wird es langsamer mit synchronem IO und wie schnell mit diversen async. IO Varianten.

    Und was den Rest angeht: klar kostet ein Context-Switch Zeit. Das Aufrufen des für die jeweilige Connection zuständigen Handlers aus einem gemeinsamen Thread-Pool heraus ist aber nichts grundsätzlich anderes. Hier müssen bei einem Server der gerade viele, viele Verbindungen bedient auch die Daten für die jeweilige Connection aus dem 2nd level Cache, 3rd level Cache oder sogar aus dem Hauptspeicher nachgeladen werden.
    Was beim Context-Switch noch dazukommt sind die CPU-Register sowie dass der Scheduler anlaufen muss. Was die CPU Register angeht, so hat die "1 Thread pro Connection" Lösung da keinen echten Nachteil, da in die async Variante weniger Daten in Registern halten kann sondern vor dem nächsten IO Call alles ins RAM zurückschreiben muss, zu Fuss diverse State Variablen updaten muss etc.

    Und was den Scheduler angeht so haben da nur Lösungen die mehrere IOs mit einem einzigen Kernel-Call absetzen können bzw. die "completion notification" für mehrere IOs mit einem einzigen Kernel-Call abholen können einen echten Vorteil. Denn ob der Kernel-Call jetzt ein synchroner send/recv Aufruf ist + Thread schlafen legen im Scheduler etc., oder ein asynchroner send/recv Aufruf + wieder beim Completion-Port nachfragen was es als nächstes zu tun gibt, das macht nicht wirklich den grossen Unterschied.

    Bei APIs wie epoll - die das oben erwähnte können, also mehrere Requests/Notifications mit einem Kernel-Call abschicken/abholen (wobei epoll nur den Abholen Teil unterstützt wenn ich das richtig verstanden habe) - lasse ich mir dagegen gerne einreden dass es einen Vorteil bringt. Wobei mich auch da interessieren würde wie gross der Vorteil wirklich ist. Also auf wie viele "Null-Requests/Second" (=Requests wo der Server so-gut-wie nix tut ausser ne Antwort zurückzuschicken) man mit einem gut implementierten epoll Server kommt, und auf wie viele man mit einem "1 Thread pro Connection" Server kommt.

    Sinnvollerweise vielleicht mit einem "1 Thread pro Connection" Server der nicht für jede Connection nen eigenen Thread erzeugt + zerstört, sondern einen "dynamically sized Thread-Pool" verwendet -- damit man nicht pro Connection den Overhead des Thread-Erstellens + Thread-Aufräumens bezahlt. (Bzw. alternativ mit einem "einfachen" Server, aber mit Clients die 10+ Requests pro Connection absetzen.)

    Dann hätte man mal Zahlen die man sich konkret ansehen kann. Und wo man konkret den geschätzten (oder idealerweise gemessenen) Bearbeitungsaufwand pro Request gegenüberstellen kann.

    Und dann kann man sinnvoll abschätzen ob es sich lohnt die deutlich höhere Komplexität eines asynchronen Servers in Kauf zu nehmen -- oder eben doch nicht.

    Und was diverse Zitate aus u.A. der ASIO Dokumentation angeht: das ist auch relativ unspannend. Der ASIO Entwickler kommt mir auch eher wie ein Evangelist vor, ich kann mich nicht erinnern irgendwo echte Vergleiche gesehen zu haben. Also so wirklich A vs. B, schön mit Zahlen und Source-Code um das alles nachzuvollziehen.



  • ps: Wie könnte man das deiner Meinung nach sinnvoll testen? Ich überleg mir das nämlich gerade, und komm da nicht wirklich auf nen grünen Zweig.

    Es haben ja die wenigsten 3+ Rechner zu hause. Und Clients + Server auf dem selben Rechner laufen lassen ist ... sicher nicht optimal. VMs scheiden mMn. auch aus. Natürlich könnte man den VMs jeweils nur 1-2 CPUs geben, so dass auch wenn alle VMs alle ihre virtuellen Cores auslasten der Host noch Cores "übrig" hat. Blöderweise teilen sich aber auch alle eine phys. CPU, und damit auch die Caches dieser CPU. Und auch andere Engstellen wie z.B. den Netzwerkkartentreiber des Hosts.

    Die beste Möglichkeit die mir einfällt wäre einen sehr starken PC zu verwenden wo man die Clients laufen lässt, und einen vergleichsweise schwachen PC auf dem man den Server laufen lässt. Dann sollte man davon ausgehen können dass der Client PC genug "Zupf" haben wird ausreichend Requests zu schicken, so dass der Server, ganz egal wie schnell er dann ist, ausreichend was zu tun bekommt.

    Wobei da natürlich auch noch das Bottleneck Netzwerk dazwischenkommt - denn kaum jmd. hat was schnelleres als Gigabit Ethernet zuhause.



  • hustbaer schrieb:

    Und was den Scheduler angeht so haben da nur Lösungen die mehrere IOs mit einem einzigen Kernel-Call absetzen können bzw. die "completion notification" für mehrere IOs mit einem einzigen Kernel-Call abholen können einen echten Vorteil. Denn ob der Kernel-Call jetzt ein synchroner send/recv Aufruf ist + Thread schlafen legen im Scheduler etc., oder ein asynchroner send/recv Aufruf + wieder beim Completion-Port nachfragen was es als nächstes zu tun gibt, das macht nicht wirklich den grossen Unterschied.

    Welche API bietet das denn nicht? Es klingt unterschwellig so, als ob du das den IOCPs unterstellen könntest, aber die haben auch GetQueuedCompletionStatusEx .



  • @Jodocus
    Danke für die Info.
    Die Funktion gibt's erst ab Vista bzw. Windows Server 2008, und daher kannte ich die einfach noch nicht.

    Wobei man auch hier zum "Erzeugen" der IOs pro IO einen Kernel-Call braucht.
    Ist also auch bloss die halbe Miete.
    Oder gibt's auch dazu 'was was ich bloss nicht kenne?

    Oder haben die Damen & Herren OS-Programmierer das etwa sogar hingebracht dass die Aufrufe zum Absetzen von IOs effizient sind trotz dem man pro IO eine OS-Funktion aufrufen muss?

    Würde mich wundern, aber möglich wär's natürlich. Ich kenne halt nur die Architektur von "normalen" Windows-Treibern, und da fangen bei jedem noch so einfachen IO-Request (egal wie man ihn absetzt) viele viele Heinzelmännchen im Kernel zu laufen an die viele viele Zahnräder drehen. Wenn das bei Netzwerk-Treibern anders aussieht, dann OK.



  • Also ich kenne den Leitfaden, wo man zum Compileren Kerneanzahl(incl HT)+1 nehmen soll. Mehr würde schaden. Nur kann ich den Rechner beim Compilieren nicht mit zu vielen Thread in die Knie zwingen, außer ich lass ihn dann doch mehr so viel RAM essen, daß kein Plattencache mehr da ist.
    Durch welchen Mechanismus skalieren Netzwerkverbindungen so unglaublich viel schlechter als Compile-Jobs?



  • ps: Wie könnte man das deiner Meinung nach sinnvoll testen? Ich überleg mir das nämlich gerade, und komm da nicht wirklich auf nen grünen Zweig.

    das stört mich auch sehr - Benchmarks oder Architektur/Technologie-Tests in solchen Größenordnungen laufen dabei fast immer direkt beim Kunden - ich hätte gerne 250 RASPBERRY PIs direkt auf meinem Tisch in einem grossen Rack mit ordentlich Kabel dazwischen, dann koennte man schoen spielen 🙂

    ...sehr starken PC zu verwenden wo man die Clients laufen lässt, und einen vergleichsweise schwachen PC auf dem man den Server laufen lässt...

    auch eine gute Idee

    denn kaum jmd. hat was schnelleres als Gigabit Ethernet zuhause

    ja da wirds schnell eng



  • Ich werfe einfach mal diesen Artikel in die Runde *g*duck'n'run* 🕶



  • Naja...
    Wenn ich wirklich und ernsthaftig sowas entwickeln würde (also nicht bloss interessehalber nen Benchmark schreiben, sondern so wirklich fürn Produktiveinsatz), dann würde ich mir vermutlich ne billige Celeron/Pentium ULV Kiste kaufen damit ich die als Server zum Benchmarken verwenden kann.
    Falls möglich noch zusätzlich runtertakten.
    Oder sogar irgendeine superlangsame AMD-Kröte (Sempron oder so).

    So stehen die Kosten aber in keinem sinnvollen Verhältnis zum Nutzen, und damit wäre meine einzige Testplattform ein ~2x2.5 GHz Athlon (genaue Modellbezeichnung hab' ich jetzt nicht im Kopf). Der ist zwar lange nicht so schnell wie mein Desktop (Haswell Xeon), aber sicher bin ich nicht dass er "langsam genug" ist 🙂

    ps: Wobei es vermutlich auch OK wäre Clients und Server auf dem selben PC laufen zu lassen. Dadurch würde das Bottleneck Netzwerk vermieden, und da die Clients ja immer mehr oder weniger das selbe machen, wäre ein solcher Test zumindest nicht komplett uninteressant. Klar, je nachdem wie das Timing des Servers ist wird es Unterschiede geben bezüglich wann ein IO-Aufruf des Clients ohne Warten abgeschlossen werden kann bzw. eben nicht. Was dann die für die Clients nötige Rechenleistung ändert (da unterschiedlich viel Overhead), und dadurch u.U. das Resultat verfälscht.
    Wenn man die Clients und den Server allerdings so programmiert dass es im Client so-gut-wie keine IO Calls gibt die "sofort" abgeschlossen werden können, und dann Clients und Server noch auf unterschiedliche Cores einschränkt... dann könnte da sogar ein Schuh draus werden.



  • Skym0sh0 schrieb:

    Ich werfe einfach mal diesen Artikel in die Runde *g*duck'n'run* 🕶

    Wenn du schon weisst dass der Artikel Mist ist, wieso postest du ihn dann?
    Und der Artikel ist Mist.

    Denn...

    Der Author hat wohl offenbar das C10K Problem nicht ganz verstanden - oder es ist ihm einfach schnuppe. Bei C10K geht es - wenn ich mich richtig erinnere - darum dass pro Sekunde 1000 neue Connections daherkommen, und jeden Connection für 10 Sekunden "verbleibt" bevor sie dann wieder geschlossen wird. Und der Server das packen muss.
    Dass es kein echtes Problem ist einfach nur 10.000 Threads zu starten die auf eingehende Verbindungen warten (ohne dass 'was daherkommt), sollte klar sein.

    Und das sich das Erzeugen von 10K Threads auf heutiger Hardware in etwa einer Sekunde ausgeht (was das einzige ist was man mit nem zugekniffenen Auge und viel Vertrauen aus dem Artikel entnehmen kann)... hmja, toll. Keine grosse Überraschung. Wobei ich mich frage woher er das wissen will, denn er gibt ja nichtmal eine Meldung aus nachdem alle Threads erzeugt wurden (bzw. genaugenommen müsste man tracken wann die Threads wirklich zu laufen angefangen haben, also wann sie soweit sind dass sie "accept" aufrufen).

    => Ziemlich uninteressant.

    Und das mal ganz abgesehen von der Tatsache dass das "C10K Problem" schon einen ziemlichen Bart hat. Das war aktuell zu Zeiten wo ne schnelle CPU einen Core und ca. 1 GHz hatte und 2 GB RAM "so richtig viel" waren.

    Schade, denn was er sonst in dem Artikel schreibt deckt sich so ziemlich mit meiner Ansicht. Also dass es grösstenteils ein Scheinproblem ist das von Evangelisten am (Schein-)Leben erhalten wird. Nur blöd wenn man sich dabei so unprofessionell darstellt, dass man damit bloss besagten Evangelisten in die Hände spielt.



  • volkard schrieb:

    Also ich kenne den Leitfaden, wo man zum Compileren Kerneanzahl(incl HT)+1 nehmen soll. Mehr würde schaden. Nur kann ich den Rechner beim Compilieren nicht mit zu vielen Thread in die Knie zwingen, außer ich lass ihn dann doch mehr so viel RAM essen, daß kein Plattencache mehr da ist.
    Durch welchen Mechanismus skalieren Netzwerkverbindungen so unglaublich viel schlechter als Compile-Jobs?

    Jo, das Thema hatten wir schon.
    Dabei können die Compiler-Threads aber grösstenteils ne volle Zeitscheibe rechnen bevor sie unterbrochen werden. Also auf nem PC mit 8 Hardwarethreads auch bloss 8 Context-Switches alle z.B. 16ms.
    Das ist nicht so schlimm.

    Wenn du 100K Threads hast, die jeweils immer nur für < 1ms laufen bis sie sich wieder heia legen, sieht die Sache aber möglicherweise anders aus.

    Die Erfahrung mit dem Compilieren ist also nicht unbedingt 1:1 übertragbar.



  • Das klingt hier alles viel zu viel nach einer Hasstirade gegen die Technologien, die für hoch-skalierbare Server gemacht wurden.

    Im Hinblick auf modernes O(1)-Scheduling und immer kleinere Zeiten für Context-switches mag der Abstand zu einer Lösung mit thread-per-connection oder Threadpool durchaus sehr klein geworden sein, nur ist das ja nicht die einzige Stelle, an der ein Server skalierbar sein muss.

    Mit IOCPs kann man halt die Responsiveness der Servers um ein paar ms steigern (in Regionen von 10k natürlich, unterhalb ist's eh egal).

    Was aber nun, wenn der Server auch mal arbeiten, d.h. I/O-Calls betätigen muss?

    Dann nämlich gibt es noch einen anderen empfindlichen Teil, der Zeit fressen kann neben dem Scheduling/Swapping: Das kopieren der Puffer zwischen User- und Kernel-Land (und das ist viel teurer als Sachen aus dem L3-Cache nachzuladen). Alle Puffer müssen dauernd hin- und herkopiert werden, und zwar mehrmals (User-Mode <--> WinSock Kernel/TCP Stack <--> NIC-Buffer). Die gesamte Zeit, die der Server mit Kopieren verbrät, ist an sich konstant, aber grob proportional zur Anzahl der gemachten Calls/Clients, also ziemlich O(N). Deswegen gibt es für Windows nun z.B. Registered I/O, also es wird der Speicher von vornherein für den Stack "registriert" und in den Kernel-Mode geswappt, um wenigstens eine Kopie zu sparen. (Anmerkung: ist aber eher eine UDP-Technik, da UDP-Server gewöhnlich einen viel höheren Throughput haben als TCP-Server).

    Zumindest auf Windows erfordern I/O-Calls allesamt Speicher vom non-paged Pool (und das nicht zu knapp). Zur Erinnerung: Wenn der ausgeht, und zwar viel viel schneller als der RAM, wird gleich das gesamte System instabil (mit lustigen Konsequenzen wie Datenverlust), denn Treiber brauchen diesen Speicher ja auch.

    Wenn man einen Server für Clients schreibt, die richtig Daten streamen (z.B. Game Server oder Wettersimulatoren), dann muss man nicht nur einen Read-Call machen, sondern am besten gleich mehrere (und damit NOCH mehr non-paged Pool einsetzen).
    Warum? Wenn Daten anfallen, der Server aber noch nicht fertig ist mit Bearbeiten und noch keinen neuen recv-Call gestartet hat, wo landen die Daten dann? Normalerweise im Zwischenspeicher vom TCP-Stack (einzustellen mit SO_RCVBUF). Der ist aber in der Regel nicht so groß und ja auch nur für Notfälle gedacht, damit das TCP-Window nicht beeinträchtigt wird und der Sender gar anfangen muss, langsamer zu senden (im UDP-Fall werden die Pakete ansonsten bekannter weise einfach weggeschmissen).
    Wenn man jetzt aber multithreadet, dann geht einem die Reihenfolge der Packets durcheinander, also muss man sich auch noch effizient und skalierbar darum kümmern, dass die Reihenfolge erhalten bleibt. Noch so eine Baustelle.

    Das gleiche gilt für AcceptEx: Wenn nicht genug Calls laufen, aber ein Ansturm auf den Server losgeht, landen alle Clients, die kein Accept bekommen, in der Backlog-Queue (und die ist von der Größe systembedingt begrenzt, egal wie viel RAM man sich kauft). Ist die Queue voll --> Connection refused. Ist man zu lange in der Queue --> Connection timed out. Deswegen muss ein Server auch messen, ob und wie viele Clients in der Backlog-Queue sind, um ggf. mehr AcceptEx zu posten (aber eben nicht zu viel, wegen non-paged).

    Das Problem ist nicht so trivial, wie es hier dargestellt wird. Wir spekulieren hier doch alle nur rum, aber wenn es so trivial wäre, dann würde nicht so immens viel Zeit investiert werden in die Entwicklung der APIs. Das alles als Machwerk von Blendern/Evangelisten abzustempeln klingt für mich nach Verschwörungsblabla, sorry.

    Fazit: Thread-per-connection mag jetzt vielleicht schon etwas besser skalieren, ein skalierbarer Server hat aber noch ganz woanders Baustellen. Und nicht vergessen: diese Technik ist für richtig schwere Server, mit zigtausenden Verbindungen die allesamt richtig Daten senden und nicht nur á la Chatserver rumdümpeln und Verbindungen halten. Nur, weil man diese Technik nicht unbedingt braucht, heißt das halt nicht, dass sie unnötig wäre oder zu viel Zeit hinein investiert werden würde.
    Ist halt wie mit C++. Kannst dir mächtig dabei in den Fuß schießen und brauchst du auch nicht unbedingt, wenn du keine besonderen Anforderungen hast.



  • Jodocus schrieb:

    mit zigtausenden Verbindungen die allesamt richtig Daten senden und nicht nur á la Chatserver rumdümpeln und Verbindungen halten.

    Nene, die nehmen ja eigene Treiber, schon wieder vergessen? 😃


Anmelden zum Antworten