Loadbalanced Threadpool

Status
Nicht offen für weitere Antworten.
T

tuxedo

Gast
Hallo,

ja, der Titel ist ein wenig verzwickt..
Ich versuchs mal zu erklären was ich gerne machen will, aber nicht so recht weiß wie:

Ich hab einen Java NIO Server. Dieser kann verbindungen entgegen nehmen. Für die weitere Arbeit an der Verbindung ist ein sogenannter Selector zuständig. Jede Verbindung, repräsentiert durch einen "Channel", kann sich an so einem Selector registrieren und dem Selector mitteilen was sie gerne tun möchte: Auf Daten warten oder selbst Daten senden.

Bisher hab ich dafür _einen_ Thread der sich für _alle_ Clients um diese "ich will auf Daten warten" und "ich hab Daten die gesendet werden müssen" Events kümmert. Im Netz hab ich gelesen, dass man hierfür einen Dispatcher-Pool baut:

server.png


Links sieht man den Acceptor. Der nimmt die Verbindungen (Channels) an und übergibt sie dann einem Pool von Dispatchern die auf die kommenden und gehenden Events am Channel hören.

Und da steckt mein Problem:
Ich brauche eine Anzahl an Threads, die ich, nachdem sie gestartet wurden, mit Informationen versorge, auf welchem Channel sie für Events lauschen sollen. Und für diese Anzahl Threads brauch ich _einen_ Eingang wo ich Channels registrieren kann, und wo "blackboxartig" entschieden wird, welcher der Threads dafür benutzt wird.

Ein "ExecutorService" im Sinne des java.util.concurrent Packages ist dafür glaub ich ungeeignet:
Da kann ich doch einfach nur "Runnables" reinstecken die dann im Pool gestartet werden und laufen bis sie fertig sind. Ich brauch aber eine Anzahl an Threads die laufen und die dynamisch und möglichst gleich verteilt mit Channels registriert werden.
Okay, ich könnte einen 20 Threads großen ExecutorServer Pool definieren und mir die reingesteckten Runnables "merken". Aber das bringt mich ja nicht weiter: Die Dispatcher-Runnables haben ja keine beschränkte Laufzeit. Die laufen solange bis der Server beendet wird. Und außerdem bleibt dann noch das "Problem" mit dem verteilen der Channels auf die Threads...

Ich hab im Netz dazu (also dispatching bei NIO mit einem Pool) nix gefunden. Entweder es sind nur unvollständige Codebeispiel die lediglich das Prinzip des gesamten Systems erklären ( http://today.java.net/cs/user/print/a/350#dispatcher ), oder es wurde das Handtuch geworfen und einfach definiert: "Die Synchronisation ist zu viel Arbeit. Ich will lieber primitiven Code haben".

Naja, das mit der synchronisation haut in meinem Fall recht gut hin. Hab das schon recht gut durchdacht. Aber bei mir scheitert's irgendwie an dem Ansatz für den Dispatcher-Pool.

Any ideas?
 
T

tuxedo

Gast
Mein erster und bisher einzigster Ansatz wäre:

Eine Klasse Dispatch-Pool in der ich X Threads starte (und diese dann z.B. in eine Gruppe stecke).
Diese Klasse bekommt eine Methode "registerChannel" die den Channel in einem der Threads registriert. Und zwar immer dem, der am wenigsten Channels bisher registriert hat. Wenn es gleichviele Threads mit dem Minimum an registrierten Channels gibt, wird der erstbeste genommen.

So sollten eigentlich die Channels gleichmäßig auf meine X Threads verteilt werden.

Gegenvorschläge? Verbesserungen? Andere Ideen?

- Alex
 

byte

Top Contributor
> Die Dispatcher-Runnables haben ja keine beschränkte Laufzeit. Die laufen solange bis der Server beendet wird.

Bis der Server beendet wird? Oder bis der Client disconnected? Dem ThreadPool ist es prinzipiell wurscht, wie lange die Runnables laufen. Er kümmert sich einfach nur ums verteilen der Runnables auf die Threads. Wenn ein Runnable fertig ist, dann ist der Thread frei für das nächste Runnable.


> Und außerdem bleibt dann noch das "Problem" mit dem verteilen der Channels auf die Threads...

Genau das nimmt Dir der ExecutorService doch ab. Du brauchst Dich nicht mehr um die Threads zu kümmern. Du schmeisst einfach Runnables rein und die werden in einem Thread des Pools ausgeführt, falls einer frei ist.


Ansonsten hab ich nicht wirklich verstanden, was Du vor hast. Ich weiss nicht mal, was ein NIO Server sein soll. :roll:



Edit: OK, jetzt wo ich Dein zweites Post lese, verstehe ich etwas besser, wo das Problem liegt. Vergiß also, was oben steht. ;)
 
T

tuxedo

Gast
NIO = New IO ... Java's "besseres" IO-Konzept.

Hast du zu meinem zweiten Post vllt. eine Idee oder Anmerkung?

- Alex
 

Janus

Bekanntes Mitglied
thread groups solltest du nicht verwenden. rät sogar sun mittlerweile von ab. bringen keinen vorteil mit, aber dafür nen haufen bugs ;)

für dein problem würd ich auf gestaffelte thread pools setzen.

der dispatcher ist ein eigener einzelner thread, der zentral alle requests entgegen nimmt und geordnet in eine queue einreiht. dabei werden requests mit einer "channel thread ID" versehen, die den thread kennzeichnet, der sich um diesen request kümmern soll. diese channel threads sind eigene prozesse, die jeder einen eigenen thread pool gesetzter größe verwalten. dieser thread pool dient als worker pool und verwendet die standard pools von ExecutorService.

- dispatcher füllt channel queue
- channel threads pollen channel queue nach ihrer ID
- channel thread schnappt sich seinen request und übergibt ihn an seinen worker pool

die grundidee dabei ist, dass garantiert werden kann, dass dispatcher und channel threads immer laufen und eindeutig identifiziert werden können.
anonyme worker threads wiederrum führen kritischen code aus, der u.u. auch dazu führen darf, dass ein thread durch eine exception aussteigt und sofort durch einen neuen thread ersetzt werden kann.

spart im endeffekt imo gewaltig code für solche verwaltungsaufgaben.
 
T

tuxedo

Gast
Janus hat gesagt.:
thread groups solltest du nicht verwenden. rät sogar sun mittlerweile von ab. bringen keinen vorteil mit, aber dafür nen haufen bugs ;)
Gut dass ich gefragt hab ;-)

für dein problem würd ich auf gestaffelte thread pools setzen.

der dispatcher ist ein eigener einzelner thread, der zentral alle requests entgegen nimmt und geordnet in eine queue einreiht. dabei werden requests mit einer "channel thread ID" versehen, die den thread kennzeichnet, der sich um diesen request kümmern soll. diese channel threads sind eigene prozesse, die jeder einen eigenen thread pool gesetzter größe verwalten. dieser thread pool dient als worker pool und verwendet die standard pools von ExecutorService.

Ähm. Ne. Das ist nicht das was ich gesucht hab. Ein Channel lässt sich nicht in einem Thread benutzen. Ich paste mal n bisschen abstrakten pseudo-beispiel-code hier rein:

Code:
class Acceptor implements Runnable {
  ...
  void init() {
    ServerSocketChannel serverChannel = ServerSocketChannel.open();
    serverChannel.configureBlocking(true);
    serverChannel.socket().bind(new InetSocketAddress(serverPort));
  }

  public void run() {
    while (isRunning) {
      try {
        SocketChannel channel = serverChannel.accept(); 

        Connection con = new Connection(channel, appHandler);
// ---
// HIER wird die Verbindung an den Dispatcher weitergereicht
        dispatcherPool.nextDispatcher().register(con);  
// ---
      } catch (...) {
        ...
      }
    }
  }
}


Das ist die Acceptor-Klasse aus der Beispielgrafik in meinem ersten Post. Im Acceptor werden verbindungen entgegen genommen und an _einen_ Dispatcher aus dem Pool weitergereicht. Welcher Dispatcher-Thread das sein wird, wird irgendwo hinter "nextDispatcher" entscheiden. Ein Dispatcher ist ein Thread/Runnable, der in seiner run() Methode folgendes macht:

Code:
public void run() {
    while (isRunning) {
      ...
      int eventCount = selector.select();

// hole die am Selector anstehenden Events
      Iterator<SelectionKey> it = selector.selectedKeys().iterator();

      while (it.hasNext()) {
        SelectionKey key = it.next(); 
        it.remove();

// Wenn es ein Read-Event ist, 

        // read event?
        if (key.isValid() && key.isReadable()) {
          workerPool.execute(new Worker(key));
        }

        // write event?
        …
      }
    }
  }
}

Es gibt also X Dispatcher-Threads welche die ihnen mitgeteilten Channels überwachen und auf Events lauschen. Taucht ein solcher Event auf, wird er an einen Worker gereicht der sich um die Bearbeitung kümmert.

- dispatcher füllt channel queue
- channel threads pollen channel queue nach ihrer ID
- channel thread schnappt sich seinen request und übergibt ihn an seinen worker pool

die grundidee dabei ist, dass garantiert werden kann, dass dispatcher und channel threads immer laufen und eindeutig identifiziert werden können.
anonyme worker threads wiederrum führen kritischen code aus, der u.u. auch dazu führen darf, dass ein thread durch eine exception aussteigt und sofort durch einen neuen thread ersetzt werden kann.

spart im endeffekt imo gewaltig code für solche verwaltungsaufgaben.

Eine Queue ist doch etwas was abgearbeitet wird. Ein Channel ist aber nix was man "abarbeiten" kann. Das was man abarbeiten kann, ist das was bei einem ausgelösten Event heraus kommt. Wenn du dir die Grafik weiter oben anschaust, ist das aber das was ein Dispatcher "weiterreicht".

Wenn ich mir deine Vorschlag so ansehe, dann hast du eine Stufe zu weit rechts angesetzt. Klar, man könnte auch nur einen Dispatcher nehmen und "n" Worker dahinter. Aber das wäre dann eine 1:n Dispatcher-Worker Konfiguration. Währen der eine Dispatcher ein Event entgegen nimmt, kann er kein zweites Event entgegen nehmen, aber es kann viele Worker geben die noch mit dem Bearbeiten der Pakete beschäftigt sind.

Was ich aber haben will ist eine n:m Dispatcher-Worker Konfiguration:
n Dispatcher die auf Events warten und diese an
m Worker (in einem ExecutorService Thread Pool) zur bearbeitung (also ein Task) weitergeben.

So. Für die Worker kann ich einen stinknormalen Pool nehmen. Weil da kann ich für die zu bearbeitenden Pakete ein Runnable nehmen und dieses in den Pool werfen.

Für den Dispatcher geht das aber nicht. Ich hab keinen Task, nix ausführbares. Ich hab nur ein Channel auf dem ein Event ausgelöst werden kann. Der Dispatcher ist quasi nix anderes als ein ge'thread'eter ActionListener dem ich Channels registrieren kann auf denen er "hört". Und mir reicht einer davon nicht. Ich brauch mehrere. Mit einer Queue hat das nicht so viel zu tun.

Ich hoffe es ist rüber gekommen ... Wenn nicht: Nochmal meinen zweiten Post mit meinem ersten Ansatz lesen ;-)

- Alex
 

Janus

Bekanntes Mitglied
naja, was heisst zu weit rechts. das, was du in acceptor und dispatcher aufteilst, kann man meiner ansicht nach als dispatcher zusammenfassen. und da braucht man nicht mehr als einen. zumindest pro server port. im dispatcher sollte kaum last erzeugt werden. da kann man auch paar tausend anfragen pro sekunde durchdrücken.
 
T

tuxedo

Gast
Ja, stimmt. Acceptor und Dispatcher lassen sich zusammenfassen. Mir war/ist bisher nur nicht klar, warum so viele Totorial und Dokuseiten was von einem Dispatcher-Pool schreiben. Auch der Link zu java.net den ich gepostet habe macht sowas.

Hab mich jetzt ein bisschen genauer in das Thema eingelesen. Ron Hitchen's "Java NIO" war da eine große Hilfe. Er schreibt hier im "Selector" Kapitel, dass das fangen von Events auf den Channels nahezu komplett vom OS gehandhabt wird und keinen Flaschenhals darstellt. Man kann zwar mehrere Selectoren benutzen, allerdings, so Hitchens, würde man damit das "Problem" im gesamten nicht mindern, nur weiter aufteilen.

Mein Ansatz war, im Dispatcher die Pakete schon zu lesen, und diese dann fertig gelesen an die Worker-Threads weiter zu geben.

Mehrere Dispatcher wären hier deshlab sinnvoll gewesen, weil das lesen der Pakete Zeit kostet.

Hitchens Ansatz ist aber, nur einen Dispatcher zu haben, der keine Pakete liest, sondern das "Pakete sind zum lesen bereit" Event an einen Worker-Thread in einen Pool weiterleitet und dieser dann liest. Der Dispatcher kann so ungebremst zum nächsten Event übergehen.

Werde meine Architektur dahingehend wohl anpassen. Aber "seltsam" ist es schon, dass so viele Beispiele von mehreren Dispatchern ausgehen und den Acceptor aus dem Dispatcher ausgegliedert haben. Kann sich das jemand erklären?

- Alex
 

Janus

Bekanntes Mitglied
ist wahrscheinlich eher als theoretische aufteilung gedacht. gibt viele systeme, die auf architekturebene in mehrere subsysteme zerlegt werden, um deutlich zu machen, wie die interaktion aussieht, in der praxis dann aber zusammengelegt werden, um unnötige komplexität zu vermeiden.
 
T

tuxedo

Gast
Das mag sein. Aber das sollte man, wenn man schon eine Doku schreibt, am besten auch erwähnen ;-)

- Alex
 
Status
Nicht offen für weitere Antworten.

Ähnliche Java Themen


Oben