# Serialisierung: Wie macht RMI das so schnell?



## tuxedo (18. Apr 2008)

Hallo zusammen,

ich hab mal wieder ein eher spezielles Thema...

Wie meiner Signatur ja zu entnehmen ist, bastle ich an einer RMI-Alternative die keine Probleme mit Routern und Co. macht.

Die Socket-Implementierung funktioniert soweit. Mittlerweile hab ich auch eine Java NIO implementierung um mit seeeeehr vielen Clients umgehen zu können (theoretisch müsste RMI da vor SIMON in die Knie gehen...Stichwort "1 Thread pro Client").

Nun ja. Mein Problem ist leider immer noch, dass die Serialisierung und Deserialisierung, welche ja Grundlegendes Prinzip von RMI und SIMON ist, sehr langsam ist. Okay, ich muss die Aussage relativieren. SIMON ist langsamer als RMI. Und dank dem "Yourkit Java Profiler" (ein Dank geht an Vladimir von YourKit der mir eine kostenlose OpenSource Lizenz gegeben hat) wieß ich auch, dass die Serialisierung dran schuld ist. 

Ein Beispiel:

RMI schafft es ein Transportobjekt, welches einen String plus ein byte[] mit 65kbyte Größe beinhaltet, 1000 zum Server und zurück zu kommunizieren, und das in etwa 1,3ms pro Durchlauf.

SIMON braucht da etwa 2,5-2,8ms und ist demnach rund doppelt so langsam (getestet auf einem Core2 Duo 2,13Ghz mit 2GB RAM und WinXP SP2, test lief via localhost)

Der Profiler sagt mir, dass in meinem Test, der 1byte - 65kbyte mit je 1000 Durchläufen testet, auf der Serverseite für die De-Serialisierung rund 1,2 Sekunden drauf gehen und dass das der Flaschenhals ist. 

Jetzt steh ich etwas ratlos da. RMI benutzt wie SIMON auch ObjectInputStream und ObjectOutputStream. Damit werden Objekte in bytes verwandelt und umgekehrt. Primitive Datentypen wie Integer oder Float werden gesondert behandelt und in ihre byte-Repräsentation zerlegt (Integer besteht ja aus 4 Byte. Diese werden "einfach so" gesendet statt den Integer als Objekt zu serialisieren etc.). Diese Idee hab ich RMI schon abgekupfert und bei mir nachgebildet. Und dennoch hinkt SIMON RMI dermaßen hinterher. 

Irgendwo in den Tiefen des RMI Sourcecodes muss noch etwas stecken das dem ganzen einen Performance-Schub gibt. Nur find ich's um's verrecken nicht. Den einzigsten Unterschied den ich jetzt noch entdeckt habe ist, dass RMI ThreadGroups verwendet. Aber ich hab gesagt bekommen dass man das lieber sein lassen sollte.

Nächster Knackpunkt war die umstellung auf NIO. Da gibts keine Streams mehr, weshalb das serialisierung via ObjectOutputStream darauf hinaus läuft, dass ich in einen ByteArrayOutputStream serialisiere, mir danach die Bytes aus dem BAOS hole und mit NIO verschicke. 
Dazu hab ich im Netz gelesen dass es geschickter sei nicht den Umweg über byte[]s zu gehen, sondern einen eigene ByteOutputStream zu schaffen, welcher intern gleich ByteBuffer benutzt, was NIO zugute kommt da hier nur mit ByteBuffern gearbeitet wird und die lästige Konvertierung von byte[] nach ByteBuffer wegfällt.

Aber auch damit ist es nicht schneller geworden (aber auch nicht langsamer).

Ich weiß dass Externalisierung statt Serialisierung schneller ist, weil auch damit Object*Stream umgangen wird. Auch das hab ich versucht. Wird aber auch nicht schneller. Ich bin schier am verzweifeln. 

Vielleicht kennt sich jemand etwas tiefer mit der Geschichte aus und kann mir den einen oder anderen Tipp geben.

Wie gesagt. Laut Profiler ist der HotSpot eindeutig das Serialisierung und Deserialisierung mittels der ObjectInputStrean ud ObjectOutputStream Methode. 

Gruß
Alex

P.S. Okay, man könnte meinen dass bei einer Applikation die über das Internet kommuniziert es egal ist wenn ein Aufruf 1,3 oder 2,8ms braucht da die Latenz im Internet sowieso höher ist. Aber man muss auch berücksichtigen dass diese Messung indirekt wiedergibt wie schnell der Server antworten kann. Und das wiederum deutet darauf hin, dass der Server wohl mehr Antworten raushauen könnte wenn er nicht so viel mit Serialisierung und Deserialisieurng zu tun hätte. Für den einzelnen Client ist es also ertmal wurscht. Aber bei vielen Clients kann sich das negativ auf den Server und die Antwortzeiten auswirken.


----------



## byte (18. Apr 2008)

alex0801 hat gesagt.:
			
		

> Wie meiner Signatur ja zu entnehmen ist, bastle ich an einer RMI-Alternative die keine Probleme mit Routern und Co. macht.


Hab jetzt nicht alles durchgelesen (bin auch kein RMI-Experte). Ich wollte nur mal dezent darauf hinweisen, dass es dafür längst Lösungen gibt. Hessian und Burlap gehen beide über das HTTP-Protokoll. Am komfortabelsten gehts aber imo mit dem HTTP-Invoker von Spring.
Ich sehe daher wenig Sinn in Deiner SIMON-Variante, möchte Deine Arbeit aber auch nicht schmälern.


----------



## tuxedo (18. Apr 2008)

Naja. Ich weiß dass es weitere Techniken gibt Remote-Methoden aufzurufen. Was mir nur an den bisherigen Techniken nicht gefällt sind die verwendeten Protokolle und die Skalierbarkeit. HTML und Co. produziert mir zu viel Overhead. Auch RMI kann man über HTTP tunneln. 

Mein Augenmerk bei der Entwicklung von SIMON lag nicht nur auf der Routertauglichkeit. Mit der Java NIO Implementierung ist das ganze im Vergleich zu den anderen Techniken auch noch extrem skalierbar und, wenn man SUNs Aussagen trauen darf, noch extrem performant. Einzigstes Nadelöhr ist und bleibt (bis jetzt) die Serialisierung. Und wenn man so eine Abstraktion wie bei RMI haben will, wird man nicht um Proxy-Objekte rum kommen. Und dann steht man, egal welches Protokoll man verwendet, wieder vor dem gleichen Problem.

Naja, wie dem auch sei. Bei all dem wollte/will ich ja auch noch was lernen. Hätt ich zu einer fertigen Lösung gegriffen, wüsste ich bis heute nicht wie das mit NIO funktioniert und wüsste auch nicht was ein ByteBuffer ist und was ihn so viel besser macht als ein byte[].

Und noch was: Nutzt eine der schon bestehenden Alternativen NIO oder arbeiten die alle noch mit den starndard Java Sockets basierend auf Streams? Wenn ja, dann braucht man Mörder-Server-Kisten um 2500 und mehr Clients zu bedienen (pro Client ein Thread?)....

Aber zurück zum eigentlichen Problem: Die Serialisierung...bzw. deserialisierung.

- Alex


----------



## byte (18. Apr 2008)

alex0801 hat gesagt.:
			
		

> Und noch was: Nutzt eine der schon bestehenden Alternativen NIO oder arbeiten die alle noch mit den starndard Java Sockets basierend auf Streams? Wenn ja, dann braucht man Mörder-Server-Kisten um 2500 und mehr Clients zu bedienen (pro Client ein Thread?)....


Wie gesagt: die von mir angesprochenen Lösungen benutzen keine Sockets sondern serialisieren die Daten in HTTP-Pakete. Die werden dann über das übliche Transportprotokoll verschickt und auf der anderen Seite wieder ausgepackt. Port 80 ist für gewöhnlich immer offen, also gibts keine Routerprobleme. Und man kann bestehende Vorteile von HTTP nutzen, z.B. Sicherheit durch HTTPS oder Load Balancing durch bestehende Webserver wie den von Apache.


----------



## sliwalker (18. Apr 2008)

Hoi,

ich bin da auch kein Experte, aber mich würde mal interessieren, was Du denn in den ObjectOutputStream steckst.
Vielleicht liegts ja nicht am ObjectOutputStream sondern an der Anpassung der Daten davor.

greetz
SLi


----------



## tuxedo (19. Apr 2008)

byto hat gesagt.:
			
		

> alex0801 hat gesagt.:
> 
> 
> 
> ...



Na dann sind doch wieder Sockets im Einsatz ;-) Ohne ists bei TCP/IP etwas schwierig. Und wie du selbst sagst: serialisiert wird hier auch. Also besteht da das gleiche Nadelöhr.

Aber es gibt halt 2 Möglichkeiten Sockets zu benutzen. Einmal den Stream-Ansatz den Java bei jeder 0815 TCP/IP Verbindung verwendet, oder nen Non-Blocking-Ansatz mit gemultiplexten Channels was im Java NIO zum Einsatz kommt. Letzteres ist durchweg performaner und Server die diesen Ansatz verfolgen können wesentlich mehr Clients bedienen.
Der "Umweg" über einen Apache-Webserver scheint mir nicht gerade der Latenz zugute zukommen. Auch ist HTTP nicht das "beste" Protokoll um schnell und performant Daten zu übermitteln. Das war und ist für mich der Hauptgrund nicht auf  HTTP zu setzen. Zumal ich auch noch die Serverseite recht "einfach" halten will. In meiner SIMON Lösung reichen eine Hand voll Zeilen Code. Alles was ich jetzt zu HTTP Invoker und Co. gelesen hab sprengt das ganze deutlich. 

@Sliwalker:

>>ich bin da auch kein Experte, aber mich würde mal interessieren, was Du denn in den ObjectOutputStream steckst.
Vielleicht liegts ja nicht am ObjectOutputStream sondern an der Anpassung der Daten davor. 

Naja, ein 0815 Java Objekt welches über einen Konstruktur verfügt mit dem ich eine String-Instanzvariable und eine byte[] Instanzvariable setzen kann. Mehr nicht. Und je nach Test variiert die göße des byte[] von 1 Byte bis 65kbyte. Der String hingegen bleibt immer gleich.

- Alex


----------



## Janus (19. Apr 2008)

> (theoretisch müsste RMI da vor SIMON in die Knie gehen...Stichwort "1 Thread pro Client")



findet deine deserialisierung statt, bevor der stream an einen thread übergeben wird oder danach? thread pooling sorgt logischerweise auch für nen gewissen overhead.


----------



## tuxedo (20. Apr 2008)

Ich habe keine Streams mehr. Java NIO benutzt einen anderen Ansatz.

Momentan ist es so, dass mein Dispatcher-Thread eine Methode kennt, der die notwenidgen Daten und Paremeter übergeben werden, welche den Remote-Aufruf darstellen. Die Methode serialisiert die Daten und macht einen ByteBuffer daraus. Dieser wird dann in eine Map gesteckt und der Thread "benachrichtig" dass es Daten gibt die gesendet werden müssen. Dies wird vom "Selektor" in der run() Methode erkannt und triggert einen Worker-Thread der den ByteBuffer aus der Map nimmt. Der Worker-Thread befindet sich in einem Thread-Pool wo er automatisch gestartet wird. Sobald er gestartet ist, sendet er an die entsprechende Gegenstelle den ByteBuffer.

Was mir aber gerade selbst aufgefallen ist:

Es ist glaube ich nicht sonderlich geschickt wenn ich das serialisieren in einer Methode des Threads/Runnables mache. Das  sollte wohl besser auch von einem Worker-Thread erledigt werden. Aber ob das was an der Perfrmance meines Tests ändert wage ich zu bezweifeln. Weil im Test wird ein Aufruf nach dem anderen getriggert. Und erst wenn das Ergebnis des Aufrufs da ist, wird der nächste ausgeführt. 

-Alex


----------



## byte (20. Apr 2008)

Anonymous hat gesagt.:
			
		

> Na dann sind doch wieder Sockets im Einsatz ;-) Ohne ists bei TCP/IP etwas schwierig. Und wie du selbst sagst: serialisiert wird hier auch. Also besteht da das gleiche Nadelöhr.


Das habe ich nie bestritten. Aber es ging Dir doch um eine Routerfreundliche Verbindung und da ist halt HTTP die einzige Möglichkeit, da Port 80 praktisch immer offen ist.



> Aber es gibt halt 2 Möglichkeiten Sockets zu benutzen. Einmal den Stream-Ansatz den Java bei jeder 0815 TCP/IP Verbindung verwendet, oder nen Non-Blocking-Ansatz mit gemultiplexten Channels was im Java NIO zum Einsatz kommt. Letzteres ist durchweg performaner und Server die diesen Ansatz verfolgen können wesentlich mehr Clients bedienen.


Kann ich wenig zu sagen, ausser dass alle JEE-Applicationserver entweder über HTTP oder RMI gehen. Woran liegt das Deiner Meinung nach?



> Der "Umweg" über einen Apache-Webserver scheint mir nicht gerade der Latenz zugute zukommen. Auch ist HTTP nicht das "beste" Protokoll um schnell und performant Daten zu übermitteln.


Das hat ja auch niemand behauptet, dass HTTP am schnellsten ist. Aber wenn Du die Daten in HTTP Pakete serialisierst, dann kannst Du die Daten halt durch nahezu jeden Router schicken, ohne dass Dein Port geblockt wird. Und Du hast halt weitere Vorteile wie HTTPS, ...



> Das war und ist für mich der Hauptgrund nicht auf  HTTP zu setzen. Zumal ich auch noch die Serverseite recht "einfach" halten will. In meiner SIMON Lösung reichen eine Hand voll Zeilen Code. Alles was ich jetzt zu HTTP Invoker und Co. gelesen hab sprengt das ganze deutlich.


HTTP Invoker setzt halt auf Serverseite einen Webserver voraus, was in der JEE-Welt Gang und Gebe ist. Spring Remote bietet natürlich noch mehr. Du kannst Deine Services genauso per RMI bereitstellen. Im Java-Code findest Du dabei übrigens gar keine technischen Implementierungsdetails, weder auf Server- noch auf Clientseite. Du schreibst einfach POJOs. Einen Service hast Du dann mit 4 Zeilen Konfiguration Remote bereit gestellt.

Und was Performance angeht: in JEE-Anwendungen ist in 98% der Fälle, wo es Performanceprobleme gibt, nicht die Remote-Verbindung das Nadelöhr, sondern die Datenbank.


----------



## tuxedo (21. Apr 2008)

Moin byto ...

Die Sache mit der "routerfreundlichkeit" bezieht sich bei SIMON auf die Callbacks vom Server zum Client. RMI versucht hier NEUE Verbindungen aufzubauen, was beim Client vorraussetzt, dass er _direkt_ erreichbar ist. Ergo müsste ein Client der hinter einem Router sitzt erstmal ne Runde Portforwarding konfigurieren bevor Callbacks überhaupt möglich sind.

Mein Hauptaugenmerk bei SIMON sind nicht Firmen die eine Serveranwendung außerhalb des eigenen Netzwerks benutzen müssen und somit Probleme haben wenn die ausgehende Verbindung NICHT Port 80 benutzt weil der Firmenproxy nur 80 und 443 erlaubt. Aber auch das wäre kein Problem. Ist ja nicht verboten auf Port 80 was anderes als HTTP zu servieren. Also SIMON flink auf einen zulässigen Port umkonfiguriert und schon sollte es laufen (ich hab auf meinem Server z.B. SSH auf Port 443 lauschen und kann so mit SSH TCP-Verbindungen durch den Proxy hindurch tunneln).

Dem Router ist also das Protokoll (meistens) wurscht. 

>> Kann ich wenig zu sagen, ausser dass alle JEE-Applicationserver entweder über HTTP oder RMI gehen. Woran liegt das Deiner Meinung nach? 

Ich hab nicht mit JEE angefangen ;-) Das warst du. Spricht ja nix dagegen RMI oder sonstwas in der JEE Welt zu benutzen. SIMON hat nur ein Ziel: Eine RMI-Alternative die keine Probleme mit den Callbacks hat und zudem noch besser skalierbar - durch die verwendung von Java NIO - ist. Fertig. Mehr nicht. Wer's letztendlich wo benutzt ist mir ja egal. Zielgruppen für diese Lib sind solche Leute, die den Overhead von HTTP nicht haben wollen, die keinen Webserver betreiben wollen und/oder Callbacks benutzen und extrem viele Clients auf einmal bedienen wollen (und da ist Java NIO ungeschlagen im Vergleich zu dem blockierenden IO vom "normalen" Java IO, was unter anderem von RMI benutzt wird.). Und für diesen Fall hab ich nunmal keine Library im Netz gefunden, weshalb ich mich selbst dran gesetzt habe. 

Ich würde sagen wenn hier noch weitere Diskussionbedarf besteht damm entweder per PM oder in einem extra Thread.

Und deshalb zurück zum eigentlichen Thema....


----------



## sliwalker (21. Apr 2008)

Hoi,

war am WE weg...deshalb erst heute:

Du schriebst:
Naja, ein 0815 Java Objekt welches über einen Konstruktur verfügt mit dem ich eine String-Instanzvariable und eine byte[] Instanzvariable setzen kann. Mehr nicht. Und je nach Test variiert die göße des byte[] von 1 Byte bis 65kbyte. Der String hingegen bleibt immer gleich. 


Schonmal probiert über einen ByteArrayOutputStream zu gehen?
Wie gesagt: kein Experte und die Implementierung von OutputStream und ByteArrayOutputStream kenne ich nicht.
Aber wenn Du, wie Du glaube ich oben mal gesagt hast, bei manchen Typen sowie so die byte-Repräsentation verwendest um es durchzuschicken, könnte doch vielleicht der ByteArrayOutputStream vorteile bringen.

greetz
SLi


----------



## tuxedo (21. Apr 2008)

Hallo SLi ...

Hmm, hast du schonmal einen Objekt serialisiert und hast davon die bytes benötigt? AFAIK gibt es keinen anderen weg als einen ByteArrayOutputStream zu erstellen und diesen mit dem ObjectOutputStream zu koppeln.
Nach dem serialisieren kann man dann den ByteArrayOutputStream nach den nun darin enthaltenen Bytes fragen.

Alles kein Thema. Funktioniert ja. Und wie ich weiter oben schon geschrieben habe, ist im Netz der eine oder andere Hinweis zu finden, dass es "unperformant" sein soll, das Objekt erst in ein byte[] zu verfrachten und anschließend in einen ByteBuffer zu stecken (denn diesen braucht man bei der Kommunikation mit NIO). Also wurde der Vorschlag gemacht einen eigenen ByteArrayOutputStream zu bauen, nur eben als ByteBuffer. Also einen ByteBufferOutputStream.

Hab ich gemacht. War ja nicht sonderlich schwer. Nur dummerweise hat das an der Performance nix verändert. Der Profiler meckert ja am "readObject" und "writeObject" rum und sagt, dass das die Hotspots sind.

Kennt jemand vielleicht noch einen Weg wie man beim Profilen mit dem YOurkitJavaProfiler noch mit in den Source des JDKs reinschauen kann? Dann könnte ich da noch weiter rein "profilen" und schauen wo wirklich der Hund begraben ist. 

- Alex


----------



## Quurks (27. Apr 2009)

Meine Idee währe, sich den Code uas dem src Verzeichnis des JDK zu ziehen & ihn als eingene Klasse einbinden, dh einfach in ne Klasse kopiern(Falls ich richtig liege und der Profiler einfach dem "Mitgelieferten" Src-Code nicht darstellt


----------



## Spacerat (27. Apr 2009)

Rein intuitiv würd' ich mal sagen, das es eben an der begrentzten Anzahl der Threads liegt. Multithreading bedeutet ja gerade Parallelisierung von Aufgaben und je mehr Threads parallel laufen können, desto weniger Wartezeit hat man. Begrentzt man dagegen die Anzahl der Threads z.B. auf 50, müssen bei 100 parallelen Anfragen 50 Anfragen zurück gestellt werden. Ferner muss man bei einer begrenzten Anzahl an Threads diese am "laufen" (d.h. in Wartemodus stellen) halten währen sie bei 1 Thread pro Anfrage nach ihrer Ausführung zerstört weden können.


----------



## Marco13 (22. Dez 2010)

Bei diesem Thread http://www.java-forum.org/netzwerkprogrammierung/110914-bilduebertragung.html#post712730 war ein Link auf [JavaSpecialists 088] - Resetting ObjectOutputStream - kann das was mit diesem Thema hier zu tun haben? Wenn nicht, kann man diesen Thread ja wieder re-humieren


----------

