# Warum ist das Java "Select" so langsam?



## Angel4585 (13. Okt 2008)

Hallo,

wir haben gerade in unserem Berufsschulunterricht Javasockets programmiert.
Das ganze unter Linux(ubuntu 8.04).

Ein Klassenkamerad hat dabei einen Server mit ServerSocketChannels programmiert und ich einen Client mit SockeChannels.
Bei dieser Kombination war es so, das die Prozessorauslastung beim Server auf über 90% lag und der Server bei etwa 75 verbundenen Clients ohne Fehlermeldung(!!!) aus einer Endlosschleife rausgeflogen ist und sich beendet hat.

Danach hat der Kollege einen server in Python geschrieben, welcher weniger Probleme damit hatte.
Der Pythonserver hatte ne Prozessorauslastung von unter 10% bei wesentlich mehr Clients und hatte nie irgendwelche Anzeichen das er gleich abstürzen würde.

Woran könnte das liegen? Hat Java da einen Bug?


----------



## The_S (13. Okt 2008)

Ich würde eher sagen, dass eure Software da nen Bug hatte ... Code wäre hilfreich.


----------



## Angel4585 (13. Okt 2008)

ok, hier der Code des Servers(NICHT VON MIR!):

```
package asyncsocketserver;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.net.ServerSocket;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;

public class Main {

    public void serve(int port) throws IOException {
        //create channel
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        //create channel socket
        ServerSocket serverSocket = serverChannel.socket();
        //create selector
        Selector selector = Selector.open();

        //bind socket to port
        serverSocket.bind(new InetSocketAddress(port));
        //set to nonblocking
        serverChannel.configureBlocking(false);

        //register channel with select
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        //mainloop
        while (true) {
            //int number = selector.select(1000);
            int number = selector.select(1);

            if (number == 0) {
                continue; //notjhing to do

            }

            for (Iterator it = selector.selectedKeys().iterator(); it.hasNext();) {
                SelectionKey key = (SelectionKey) it.next();

                if (key.isAcceptable()) {
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();
                    SocketChannel channel = server.accept();
                    if (channel != null) {
                        channel.configureBlocking(false);
                        channel.register(selector, SelectionKey.OP_READ);
                    }

                } else if (key.isReadable()) {
                    byte[] array = new byte[100000];
                    ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
                    int count;

                    SocketChannel socketChannel = (SocketChannel) key.channel();

                    while ((count = socketChannel.read(buffer)) > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            int n = buffer.remaining();
                            buffer.get(array, 0, n);
                            
                            //System.out.println(new String(array, 0, n));
                        }

                        buffer.clear();

                        if (count < 0) {
                            socketChannel.close();
                        }
                    }

                    it.remove();
                }
            }
        }
    }

    public Main() {
        try {
            serve(10000);
        } catch (IOException ex) {
            Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    public static void main(String[] args) {
        new Main();
    }
}
```


----------



## The_S (13. Okt 2008)

Hab den Code mir jetzt nicht genauer angeschaut und kann auch nicht mit 75 Clients testen, aber allein dieser Code erzeugt bei mir keine Prozessauslastung von über 90%.

Gibts auch ncoh nen kleinen Testclient dazu?


----------



## Angel4585 (13. Okt 2008)

Hier der Code des Clients, das problem muss aber ja beim Server liegen, der client läuft a) auf nem anderen rechner und b) hat der sich nicht aufgehängt.

```
package textclient;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
import java.util.Random;

public class Main {

    
    private static SocketChannel sc;
    /**
     * @param args the command line arguments
     */
    private static void initSocket(String address, int port) throws IOException {
        sc = SocketChannel.open();
        sc.connect(new InetSocketAddress(address, port));
        sc.configureBlocking(false);
        
    }

    private static void closeSocket() throws IOException {
        sc.close();
        sc = null;
    }

    private static void write(byte value) throws IOException {
        ByteBuffer bb = ByteBuffer.allocate(1);
        bb.put(value);
        bb.flip();
        System.out.println(bb.get());
        bb.flip();
        sc.write(bb);
        bb = null;
    }

    public static void positionSenden(byte x, byte y) throws IOException {
        if (x < 20 && x >= 0) {
            write(x);
        }else{
            throw new IOException("Falsche Eingabe fuer x!");
        }
        if (y < 20 && y >= 0) {
            write(y);
        }else{
            throw new IOException("Falsche Eingabe fuer y!");
        }
    }

    public static void main(String[] args) {
        try {
            for(int i = 0; i < 100000; i++){
                initSocket("10.1.25.111", 10000);
                Random rand = new Random();
                positionSenden((byte)rand.nextInt(8), (byte)rand.nextInt(8));            
                //read() oder write(value)
                closeSocket();
                try{
                    Thread.sleep(1);
                }catch(InterruptedException ex){
                    ex.printStackTrace();
                }
            }
        } catch (IOException ex) {
            ex.printStackTrace();
        }
    }
}
```


----------



## The_S (13. Okt 2008)

Also ich kenne mich mit SocketChannels nicht aus, da ich bis jetzt immer nur mit ServerSocket/Socket gearbeitet habe. Aber, was mir beim Testen auffällt:

Sobald sich erstmalig ein Client registriert hat, blockiert der Aufruf 


```
SocketChannel channel = server.accept();
```

nicht mehr (egal ob sich weitere Clients registrieren oder nicht und ob der erste Client schon lange nicht mehr ausgeführt wird), sondern liefert ständig ein SocketChannel Objekt zurück. Dadurch steigt die Auslastung natürlich stark an. Versuch mal in dieser Richtung den Fehler zu suchen.


----------



## tuxedo (13. Okt 2008)

Der Sinn von dem Select ist es, viele Clients auf einmal bedienen zu können, ohne für jeden Client einen Thread spendieren zu müssen. Das ist der Hauptgrund für die Wahl von NIO im Bezug auf Socketkommunikation: Die skalierbarkeit für seeeehr große Systeme.

In besagtem Servercode ist mir spontan das hier aufgefallen:


```
//mainloop
        while (true) {
            //int number = selector.select(1000);
            int number = selector.select(1);
```

select(1) wartet AFAIK 1 Millisekunde bevor es weiterspringt und den Rest der while-Schleife bedient. Das ist nicht sehr sinnig. Was gibt es denn im Rest der Schleiße zu tun wenn keinder der Clients ein READ auslöst? Der Server verbrät hier unnötig Zeit. 
Wenn du da einen Wert einträgst, dann vielleicht irgendwas >100. Aber 1ms macht dort keinen Sinn.

Zweiter Punkt:

Wieso benutzt man NIO, reduziert dann aber den Server auf einen einzigen Thread der dann _alle_ Clients bedienen soll?! Ist der Server mit dem Lesen von Daten eines Clients beschäftigt, kann kein anderer Client bedient werden. Lagere das Lesen in einem ThreadPool aus und lass die main-loop gleich wieder selektieren und somit gleich den nächsten Client bedienen. Die main loop mit dem lesen blockieren ist nicht der Sinn von NIO und auch nicht der Sinn eines Servers der mehr als einen Client gleichzeitig bedienen können soll.

Tipp: 
Wenn man mit NIO nicht umgehen kann (sorry, soll jetzt nicht herablassend wirken) sollte man, sofern man weniger als einige hundert Clients bedienen muss, bei den Standard IO Funktionen bleiben und auf NIO verzichten.
So mächtig wie NIO auch ist, so viel Fallstricke und Fehlerquellen bietet es Anfängern. 

- Alex

P.S.

>> Hat Java da einen Bug?
P.I.C.N.I.J. (Problem in chair, not in Java)


----------



## Kr0e (20. Okt 2008)

@tuxedo

Aber in diesem Beispiel wird doch "nur" gelesen. Also ich meine, er liest vom Channel und kopiert den Inhalt in ein Bytearray, was danach passiert, gehört nicht in die Mainschleife, aber das kopieren durchaus oder etwa nicht ?

Gruß Chris


----------



## tuxedo (20. Okt 2008)

Ja, das sagst du... "nur" lesen. Generell ist es halt keine gute Idee mit "ich lese ja nur mal eben geschwind" andere Verbindungen wieder zu blockieren...

Hier mal der "kritische" Abschnitt des Servers mit KOmmentaren von mir:


```
} else if (key.isReadable()) {
                    byte[] array = new byte[100000]; // rund 100kbyte in einer Schleife immer und immer wieder allokieren? Keine gute Idee ...
                    ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // ein "nicht direct" buffer wäre schneller. Hinzu kommt: Fehlendes Buffer-Recycling: Wieso immer wieder neu erzeugen?
                    int count;

                    SocketChannel socketChannel = (SocketChannel) key.channel();

                    while ((count = socketChannel.read(buffer)) > 0) {
                        buffer.flip();
                        while (buffer.hasRemaining()) {
                            int n = buffer.remaining();
                            buffer.get(array, 0, n); // Wieso von ByteBuffer auf ein Byte[] umbiegen? -> nicht sonderlich performant.
                           
                            //System.out.println(new String(array, 0, n));
                        }

                        buffer.clear();

                        if (count < 0) {
                            socketChannel.close();
                        }
                    }

                    it.remove();
                }
```

Tipp:

Wenn die einzigste Aufgabe des Servers im "lesen" besteht: Wieso muss dann alles in Reihe ablaufen?

Lagere die Aufgabe des "isReadable()" Falls in ein "Runnable" aus und steck das ganze in einen Thread-Pool (Stichwort ExecutorService). 

Ich hör schon die Stimmen im Hintergrund: "Java ist so langsam, da ist Python ja um Ellen besser ...". Mag sein. Aber in diesem trifft das nur deshalb zu, weil der Entwickler mit Python vertrauter ist als mit Java.

Und frei nach Olaf Schubert: "Soviel dazu. Macht was draus. Was ist eure Sache, ich kann mich schließlich nicht um alles kümmern ;-) ".

Gruß
Alex

P.S. 

noch ein Tipp:

http://xsocket.sourceforge.net/
http://mina.apache.org/


----------



## Kr0e (20. Okt 2008)

Hmm, könntest du vlt. mal in eine paar Worten erörtern, was genau bei deinem Threadpool jetzt anders wär ?
Also ich könnte mir als "Lösung" auch noch eine Recvsperre denken... Z.b., dass jeder Clienten nur maximal 4096 bytes senden kann, und der Rest eben beim nächsten Select gelesen wird...

Gruß Chris

[EDIT:]

Da ich zur Zeit an einer NEtzwerklib sitze, zur Erstellung von Servern und Clienten, interesiert mich dieses Thema und wäre offen für neue Ideen! Meine Readanhabung ist zur Zeit so:


```
private void read(SelectionKey key) throws IOException
	{
		ByteChannel readChannel = (ByteChannel) key.channel();
		readBuffer.clear();
		int numRead = 0;
		
		try 
		{
			numRead = read(readChannel, readBuffer);
		} 
		catch(IOException e)
		{				
			e.printStackTrace();
			
			synchronized(channelBufferMap)
			{
				channelBufferMap.remove(readChannel);
			}
			
			readChannel.close();
			key.cancel();
			
			lostChannel(readChannel); //EventThread
			
			return;
		}

		if (numRead == -1) 
		{
			synchronized(channelBufferMap)
			{
				channelBufferMap.remove(readChannel);
			}
			
			readChannel.close();
			key.cancel();
			
			lostChannel(readChannel); //EventThread
			
			return;
		}

		if(numRead == 0)
			return;
		
		byte[] buffer = readBuffer.array();
		byte[] bufferCopy = new byte[numRead];
		System.arraycopy(buffer, 0, bufferCopy, 0, numRead);
		
		readSuccesfully(readChannel, bufferCopy); //EventThread
	}
```

Kurz zur Erläuterung... readBuffer ist eine private Variable der klasse, in die bei jedem Read gelesen wird, und anschliessend in ein Byte[] kopiert wird. Der Grund ist folgender: Ich kann ja schlecht dem EventThread die Instanz des ReadBuffers übergeben, da dann ja beim nächsten Lesen der Inhalt modifiziert wird. Klar, man könnte natürlich bei jedem Lesen einen neuen ByteBuffer erstellen, und diesen dann an den EventThread übergeben... Ich dachte nur, dass es nicht viel ausmacht...


----------



## tuxedo (20. Okt 2008)

Bzgl. deines Edits:

Hast du dir MINA, NETTY und xSocket schon angesehen?

Verwende für meine SIMON Implementierung, nachdem ich erst eine eigene in Benutzung hatte, MINA. Das spart Zeit und bringt eine Reihe von Features mit die es in sich haben (ich sag nur freie wahl zwischen TCP/UDP, einfacher Austausch des Protokoll-Codecs, ...)

Wenn du nicht weißt worin der Vorteil eines ThreadPools liegt, dann rate ich dir wirklich zu einer der genannten Implementierungen, sonst wirst du keinen Spass dran haben.

Aber zu deiner Frage:

Du kannst Aufgaben in Form von "Runnables" formulieren und diese dann von einem ThreadPool asynchron (oder synchron, je nachdem welche Art von Pool du nimmst) erledigen lassen, ohne dass du dich um das erzeugen, starten und aufräumen von Threads kümmern musst.

Mit der Verwendung eines ThreadPools (oder allgemein bei Threads) in deinem Fall, können mehrere Pakete gleichzeitig von mehreren Clients gelesen werden. Stell dir ein System mit 10 Clients vor, und alle senden Daten an den Server: Was geht wohl schneller: Ein Server der nur "eine Hand" hat um Pakete entgegen zu nehmen und zu verarbeiten (da geht dann nur immer ein Paket zu einer Zeit), oder ein Server der 1 Hand hat um Pakete anzunehmen und 10 weitere Hände, die sich um das auspacken und interpretieren der Pakete kümmern?

Wohl eindeutig letzteres. Deine Implementierung umfasst aber die erste Variante.

- Alex


----------



## Kr0e (20. Okt 2008)

Ahh, ok.. Also das interpretieren und verarbeiten nehme ich auch in einem anderen Thread vor, keine Sorge Aber in dem thread wird dann praktisch eine Queue angelegt die abgearbeitet wird...  Also das einzige was ich bei isReadable() tuhe, ist wirklich lediglich das kopieren des Buffers von maximal 4096 bytes in ein neues array, dieses wird dann weitergereicht, und der server macht wieder sein ding... Nun zu dem Threadpool. Also legst du praktisch für jede Anfrage einen neuen thread an Richtig ? Ist das bei unglaublich vielen Clienten, die alle "zur selben" Zeit senden nicht ganz schön "thread-lastig"? Oder hab ich das Prinzip falsch verstanden ?  

Gruß Chris


----------



## tuxedo (20. Okt 2008)

>>  Also das einzige was ich bei isReadable() tuhe, ist wirklich lediglich das kopieren des Buffers von maximal 4096 bytes in ein neues array, dieses wird dann weitergereicht, und der server macht wieder sein ding... 

Naja, 

1) du hast Overhead beim benutzen von DirectbyteBuffern
2) Das anlegen von großen Byte[]'s bringt auch unnötig Overhead mit sich
3) Während all des Overheads kann dein Main-Thread nix anderes machen

Und der gesamte Pverhead bremst deine Main-Loop in der Select() läuft aus...
Deshalb: Alles was man auslagern kann, auslagern...

>>Nun zu dem Threadpool. Also legst du praktisch für jede Anfrage einen neuen thread an Richtig ? Ist das bei unglaublich vielen Clienten, die alle "zur selben" Zeit senden nicht ganz schön "thread-lastig"? Oder hab ich das Prinzip falsch verstanden ?

Nope. Es wird nicht immer ein neuer Thread erzeugt. ThreadPools können Threads wiederverwenden. Bei einem Cached ThreadPool werden Threads dynamisch erzeugt und verwendet. Liegt ein Thread tatsächlich mal >1min brach da ohne Arbeit zu haben, wird er entsorgt. Alles in allem: Wenn du viel hast was von einem Thread bearbeitet werden soll: Nimm einen ThreadPool.

Allg. zur Performance: Möglichst wenig zwischen ByteBuffer und anderem hin und her konvertieren. Am besten gar nicht konvertieren und gleich nur mit ByteBuffern arbeiten.
Und nach Möglichkeit, bei vielen ByteBuffern, keine Direct byte Buffer verwenden, sondern Heap Byte Buffer.

Mit diesen einfachen Regeln hab ich mit meinem NIO-Server zwischen 10.000 und 11.000 Methodenaufrufe pro Sekunde geschafft (500 Test-Clients verbunden mit einem Server via Localhost. Server-ThreadPool war da auf max. 50 Threads eingestellt! Siehe auch: http://www.root1.de/node/20 für den vollen Text ist eine Anmeldung nötig...). Ein Methodenaufruf bestand auf rund 64 byte Transportdaten + nochmal einige Bytes Protokolldaten. Das ganze dann als Request und Return Paket (also 2 mal).

Bin gespannt wie viel ich mit MINA rausholen kann. 

- Alex


----------



## Kr0e (20. Okt 2008)

Ich benutze kein DirectBuffer, ich nehme an, das war an den Threadersteller gerichtet...

Ok, also ThreadPools sind mir nun klarer geworden. Ich benutze aus folgendem Grund nicht "nur" ByteBuffer:

Dazu nochmal meine Methode + Kommentare:


```
private void read(SelectionKey key) throws IOException
   {
      ByteChannel readChannel = (ByteChannel) key.channel();
      readBuffer.clear();
      int numRead = 0;
      
      try
      {
         numRead = read(readChannel, readBuffer); //Hier wird soviel gelesen, wie es bytes zu lesen gibt, bzw so
                                                                         //groß der Buffer ist... D.h. jemand schickt mir 10 bytes und ich
                                                                         //erstelle jedes Mal immer einen ByteBuffer mit max. 4096 bytes,
                                                                        //dann hab ich IMMER diesen großen buffer, unabhängig, wieviel 
                                                                        //tatsächlich gesendet wurde.... Deshalb ein MaxBuffer und dann
                                                                        //kopieren in ein entsprechend großes Array...  (Denkfehler ??)

      }
      catch(IOException e)
      {            
         e.printStackTrace();
         
         synchronized(channelBufferMap)
         {
            channelBufferMap.remove(readChannel);
         }
         
         readChannel.close();
         key.cancel();
         
         lostChannel(readChannel); //EventThread
         
         return;
      }

      if (numRead == -1)
      {
         synchronized(channelBufferMap)
         {
            channelBufferMap.remove(readChannel);
         }
         
         readChannel.close();
         key.cancel();
         
         lostChannel(readChannel); //EventThread
         
         return;
      }

      if(numRead == 0)
         return;
      
      byte[] buffer = readBuffer.array();
      byte[] bufferCopy = new byte[numRead];
      System.arraycopy(buffer, 0, bufferCopy, 0, numRead);
      
      readSuccesfully(readChannel, bufferCopy); //EventThread
   }
```

Also, die Sache mit dem Threadpool werde ich mir nochmal überlegen, aber das weiterkopieren des bytebuffers
sehe ich als unerlässlich, muss ja kein byte[] sein. Man kan ja auch einen neuen ByteBuffer erstellen, aber hauptsache, es ist eine Kopie. Wie heißt es so schön in der Doc. "Vice Versa" ?? oder so ähnlich xD

PS: Die ganzen Libs sagen mir erschreckender Weise garnichts o0. Ich werds mir mal ansehen, aber ich
versuche gerne immer selbst sowas zu machen, um den Aufbau zuverstehen... Das macht doch viel mehr Spaß 
:###  :?: 

Gruß Chris


PPS: Natürlich sind deine Libs viel viel besser xD Aber den Aufbau will ich ja verstehen xD


----------

