# Schätzfrage: Wieviel Prozessorpower brauche ich?



## metulszki (16. Nov 2006)

Hallo zusammen,

folgende Aufgabe habe ich zu lösen: 

Ein Javaprogramm soll per Netzwerk ankommende Anfragen parallel an z.Zt. 12 MySQL-Server weiterleiten, die Resultsets zusammenfassen und an den anfragenden Client zurücksenden.

Die Anfrage ist z.B. "SELECT volltextsuchbegriff". Das Programm startet einen Thread, der startet wiederum 12 Threads, die daraus richtiges SQL machen, Datenbankverbindungen aufmachen und die MySQL-Server befragen. Wenn die fertig sind, formatiert der erste Thread die Resultate in einem CSV-Format und sendet sie zurück. Jeder MySQL-Server liefert maximal 750 Zeilen.  

Zur Zeit schaffe ich auf einem DUAL-XEON mit 3,2 GHz und 2GB RAM gerade mal 4 Anfragen / Sekunde.  Danach kapituliert der GC und es hagelt outofMemory-Exceptions.

Ich habe schon viel mit -XX:+UseAgressiveHeap und Konsorten, verschiedenen VM's und Umstellung auf Thread-Pools rumprobiert. Keine Änderung. 

Meine Frage an euch ist nun: Liegts an meinem Code oder ist die Aufgabe so anspruchsvoll, dass man soviel Rechenpower braucht (kann ich mir eigentlich nicht vorstellen.).

Danke...


----------



## Murray (17. Nov 2006)

Ich vermute hier eher ein Speicher- als ein CPU-Problem.

Das folgende Beispiel kann vielleicht als Vereinfachung deines Szenarios durchgehen:

```
import java.util.ArrayList;
import java.util.List;

public class ThrdTest {

	class Dispatcher  {
		
		private int working = 0;
		public Dispatcher( final int workers) {
			final Dispatcher _this = this;
			new Thread( new Runnable(){
				public void run() {
					
					for ( int i=0; i<workers; i++) {
						new Worker( _this);
					}
					
					System.out.println( "Dispatcher " + this  + " is waiting for " + workers + " worker-threads...");
					waitForWorkers();
					System.out.println( "Dispatcher " + this  + " has finished");
					
				}
			}, "Dispatcher-" + hashCode()).start();
		}
		
		public synchronized void workerStarted() {
			working++;
			this.notify();
		}
		 
		public synchronized void workerFinished() {
			working--;
			this.notify();
		}
		
		private void waitForWorkers() {
			
			try {
				synchronized ( this) {
					while( working >0) {
						wait();
					}
				}
			} catch ( Exception e) {
				e.printStackTrace();
			}
			
		}	
	}
	
	class Worker {
		
		private Dispatcher dispatcher; 
		
		public Worker( Dispatcher disp) {
			
			dispatcher = disp;
			
			dispatcher.workerStarted();
			
			new Thread( new Runnable(){
				public void run() {
						
					try {
						System.out.println( "  Worker " + this  + " is working...");
						List<Long> l = new ArrayList<Long>();
						for ( int i=0; i<10000; i++) {
							l.add( new Long( System.currentTimeMillis()));
							//l.clear(); //--- !!!
							Thread.yield();
						}
						System.out.println( "  Worker " + this  + " has finished");

						dispatcher.workerFinished();
					} catch ( Exception e) {
						e.printStackTrace();
					}
				}
			}, "Worker-" + hashCode()).start();
							
		}
	}
	
	public ThrdTest( int count) {
		for ( int i=0; i<count; i++) {
			new Dispatcher( 12);
		}
	}

	public static void main(String[] args) {
		
		new ThrdTest( 50);
		
	}

}
```

Es werde also soviele Dispatcher-Objekte erzeugt, wie im ThrdTest-Konstruktor angegeben. Diese Objekte entsprechen den parallel zu verarbeitenden Requests. Jeder Dispatcher erzeugt wiederumg 12 Worker-Threads, die dann mehr oder weniger sinnvolle Arbeit verrichten und dabei temporär Speicher brauchen (hier die ArrayList, in deinem Fall vermutlich ResultSets etc.). Dieser Speicher wird zwar am Ende der Bearbeitung wieder freigegeben, aber eben von allen parallel verarbeiteten Threads zwischenzeitlich gebraucht; je mehr Threads parallel laufen, desto mehr Speicher wird also gebraucht.

Je nach VM-Einstellungen läuft dieses Testprogramm bei einer bestimmten Anzahl paraller Dispatcher auf OutOfMemoryErrors. Wenn man im Worker das Kommentarzeichen vor dem  l.clear() wegnimmt (und damit also den Speicherbedarf jedes Workes auf ein Minimum reduziert), gibt es auch bei wesentlich mehr Requests keine Probleme.

Was kann man also tun? Ich würde den Speicherbedarf der einzelnen Dispatcher- und Worker-Threads abschätzen (besser: messen) und ggfs. optimieren. Dann lässt sich abschätzen, wieviele Requests mit einer bestimmten Speichergröße überhaupt parallel zu verarbeiten sind. Wenn das für den Anwendungsfall nicht reicht, dann muss mehr Speicher her (vielleicht sollte man dann aber auch eher an den Einsatz eines lastverteilten System mit mehreren VMs denken).

Die Anwendung müsste auf jeden Fall beim Überschreiten der maximalen Zahl paraller Request die Notbremse ziehen und den Request entweder ablehnen oder (besser) solange parken, bis ein anderer Reqeuest abgearbeitet worden ist.


----------



## Guest (17. Nov 2006)

Hallo Murray,

vielen Dank für deine Antwort. Ich sehe mir am WE nochmal den Code nach Speicherfressern durch. Wenns nichts hilft, muss halt mal ein Profi ran.

Das schlimme ist, dass bei uns zur Zeit 3 Dual-Xeons dafür draufgehen, die paar Daten hin- und herzuschaufeln.


Grüße


----------



## metulszki (17. Nov 2006)

Hallo,


ich nochmal. Das oben war ich. Login vergessen... :autsch:


----------



## Wildcard (17. Nov 2006)

Benutz mal einen Profiler und schau dir einen Heap-Dump an.
Wieviel Speicher hast du der VM eigentlich zugewiesen?


----------



## metulszki (17. Nov 2006)

Hi,
zunächst 512M, dann 1024M; z.Zt -XX:+UseAgressiveHeap. 
Speichererhöhung hat dass Problem nur verzögert. Bis zu einer gewissen Last läuft das Programm tagelang durch. Steigt die Last über einen bestimmten Bereich, kommt der GC scheinbar mit aufräumen nicht mehr hinterher.

Vielleicht mache ich es durch meinen Code dem GC auch nur zu schwer. 

Am zielführendsten wäre sicher wenn ein Profi mit Erfahrung in solchen Sachen mal über meinen Code schaut. Ist vielleicht auch was für die Jobecke...

Grüße


----------



## Wildcard (17. Nov 2006)

Ich kann dir jetzt echt nicht sagen wieviel RAM du dafür brauchst, aber 1024 sollte IMO wirklich genug sein.
Ein HeapDump sollte dir weiterhelfen, damit kannst du die echten Speicherfresser identifizieren und zielgerichtet optimieren.


----------



## metulszki (17. Nov 2006)

Denke ich eben auch.

Ich schau am Montag mal genau mit dem Profiler.

Hilft es dem GC eigentlich, wenn man Variablen explizit auf NULL setzt, z.B. bevor der Thread stirbt?


----------



## Wildcard (17. Nov 2006)

Unter Umständen ja.
Wenn beide Ereignisse jedoch sehr Zeitnahe erfolgen bringt es nichts.
Falls es bei dir ins Konzept passt kannst du auch versuchen über SoftReferences deinen Speicherbedarf zu verringern.


----------



## Murray (17. Nov 2006)

Kannst du Code posten? Interessant wären besonders die eigentlichen "Worker-Threads", also die Threads, die die DB-Anfragen machen.


----------



## metulszki (17. Nov 2006)

Ja, kann ich. Aber erst morgen. (Von der Firma aus...)


----------



## Murray (17. Nov 2006)

Von der Firma aus? Am heiligen Samstag?? Ist es denn sooo eilig?


----------



## metulszki (17. Nov 2006)

Bin eh drin.

Will ja auch geholfen kriegen :roll:


----------



## metulszki (18. Nov 2006)

So sieht er aus:


```
package  db_threader;

import  java.sql.*;
import  java.util.*;


/**
 * put your documentation comment here
 */
public class DBThread extends Thread {
    Statement stmt;
    ResultSet rs;
    Connection con;
    String url, user, pass, term;
    Vector data;
    int mode;
    float min, max;

    /**
     * put your documentation comment here
     * @param     String url  ;JDBC-URL
     * @param     String user ;DB-user
     * @param     String pass ;DB-pass
     * @param     Vector data ;returned data
     * @param     String term ;searchterm
     * @param     int mode ;querymode
     * @param     float min ;for mode 1 and 4
     * @param     float max ;for mode 1 and 4
     */
    public DBThread (String url, String user, String pass, Vector data, String term, 
            int mode, float min, float max) {
        this.url = url;
        this.user = user;
        this.pass = pass;
        this.data = data;
        this.term = term;
        this.mode = mode;
        if (min > max) {
            min = 0;
            max = 99999;
        }
        this.min = min;
        this.max = max;
    }

    /**
     * put your documentation comment here
     */
    public void run () {
        String query;
        query = "SELECT dba.DB_SHOPS.FACTOR AS FACTOR, dba.DB_SHOPS.FACTOR_DESC AS FACTOR_DESC, "
                + "DB_PRODUCTS.ID AS ID, " + "DB_PRODUCTS.EAN AS EAN_NR, " + 
                "DB_PRODUCTS.VERSANDKOSTENFREI AS VERSANDKOSTENFREI, " + "DB_PRODUCTS.PROD_NAME AS PROD_NAME, "
                + "(DB_PRODUCTS.PROD_PREIS*((100+dba.DB_SHOPS.FACTOR)/100)) AS PROD_PREIS, "
                + "DB_PRODUCTS.PROD_BESCHR AS PROD_BESCHR, " + "DB_PRODUCTS.DELIVERY_TIME AS DELIVERY_TIME, "
                + "DB_PRODUCTS.URL AS PURL, " + "DB_PRODUCTS.PROD_IMG  AS PROD_IMG, "
                + "dba.DB_SHOPS.NAME AS SHOPNAME, " + "dba.DB_SHOPS.VERSANDKOSTEN AS VERSANDKOSTEN, "
                + "dba.DB_SHOPS.LAST_SCAN AS LAST_SCAN, " + "dba.DB_SHOPS.SHOP_URL AS SSURL, "
                + "dba.DB_SHOPS.URL AS SURL, " + "DB_PRODUCTS.SHOP_ID AS SHOP_ID,"
                + "DB_PRODUCTS.PROD_ID AS PROD_ID, DB_PRODUCTS.DELIVERY_TIME_TEXT AS DELIVERY_TIME_TEXT "
                + "FROM DB_PRODUCTS ,dba.DB_SHOPS  ";
        if (mode == 1) {
            query += "WHERE PASSIV=0 AND PAUSED=0 " + "AND (DB_PRODUCTS.SHOP_ID=dba.DB_SHOPS.ID) "
                    + "AND (DB_PRODUCTS.PROD_PREIS >= 0) " + "AND (MATCH(DB_PRODUCTS.PROD_KEYWORDS) AGAINST ('"
                    + term + "' IN BOOLEAN MODE)) AND PROD_PREIS >=" + this.min
                    + " AND PROD_PREIS <=" + this.max + " ORDER BY PROD_PREIS LIMIT 751;";
        } 
        else if (mode == 2) {
            query = "SHOW TABLE STATUS LIKE 'DB_PRODUCTS'";
        } 
        else if (mode == 3) {
            query += "WHERE SHOP_ID=" + term + ";";
        } 
        else if (mode == 4) {
            query += "WHERE DB_PRODUCTS.EAN=" + term + " AND dba.DB_SHOPS.PASSIV=0 AND dba.DB_SHOPS.PAUSED=0 AND dba.DB_SHOPS.ID=DB_PRODUCTS.SHOP_ID AND PROD_PREIS >="
                    + this.min + " AND PROD_PREIS <=" + this.max + " LIMIT 751;";
        }
        try {
            DriverManager.setLoginTimeout(Server.maxconnecttime);
            con = DriverManager.getConnection(url, user, pass);
            stmt = con.createStatement();
            stmt.setQueryTimeout(Server.maxquerytime);
            rs = stmt.executeQuery(query);
            if (mode == 1 || mode == 3 || mode == 4) {
                int columncount = rs.getMetaData().getColumnCount();
                while (rs.next()) {
                    Vector b = new Vector();
                    for (int i = 1; i <= columncount; i++) {
                        b.add(rs.getString(i));
                    }
                    b.add(url);
                    synchronized (this.data) {
                        this.data.add(b);
                    }
                }
            } 
            else if (mode == 2) {
                if (rs.next()) {
                    synchronized (this.data) {
                        this.data.add(rs.getString("Rows"));
                    }
                }
            }
            rs.close();
            stmt.close();
            con.close();
        } catch (SQLException e) {
            Tools.outwrite(e.toString());
            Tools.errorDB(url, e.toString());
            Tools.outwrite(query);
        }
    }
}
```


----------



## metulszki (21. Nov 2006)

was'n los auf einmal? Hat sich mein Kopfkratzen auf euch ausgeweitet?

Grüße


----------



## DP (21. Nov 2006)

die unendliche stringkonkatenation mit += braucht schon ressourcen ohne ende.


----------



## Murray (21. Nov 2006)

Besonders Strings, die hier nur der Lesbarkeit wegen (?) zusammengebaut werden, eigentlich aber völlig konstant sind, sollte man besser umbauen. Die Query bis zum WHERE ist doch völlig konstant; diese Konstante würde ich static final deklarieren.
Du könntest auch ausprobieren, ob die Umstellung auf Prepared Statements etwas bringt.

DBConnections zu holen ist eine ziemlich teuere Operation; hier kann ein Connection Pool helfen.

Bei den Vektoren für die Rows kennt man die Länge vorher. In solchen Fällen bietet es sich an, den Vector mit genau dieser Größe zu instanziieren; er muss dann später nicht mehr vergrößert werden, braucht aber auch nicht mehr Platz als wirklich nötig.

```
while (rs.next()) {
                    Vector b = new Vector( columncount+1); //--- Vector soll alle Spalten und die URL aufnehmen
                    for (int i = 1; i <= columncount; i++) {
                        b.add(rs.getString(i));
                    }
                    b.add(url);
                    synchronized (this.data) {
                        this.data.add(b);
                    }
                }
```


Und dann noch eine Kleinigkeit: man kann dem Garbage-Collector unter die Arme greifen, indem man Variablen auf null setzt, wenn man sie nicht mehr braucht. In diesem Fall ist das der String, aus dem das Statement konstruiert wird. Wenn man diese Variable direkt nach der Erzeugung des Statements auf null setzt, kann der Garbage-Collector den Speicher schon freigeben. Setzt man sie nicht auf null, kann der Speicher erst dann wieder freigegeben werden, wenn der Sichtbarkeitsbereich der Variablen (hier also die komplette run-Methode) verlassen wird.


----------



## metulszki (21. Nov 2006)

Danke!

Ich bau das morgen gleich mal um...

Grüße


----------



## Guest (24. Nov 2006)

So,

läuft jetzt mit "persistenten" DB-Verbindungen; die DB-Verbindungen werden bei Programmstart aufgebaut und wiederverwendet. Ein paar Codeoptimierungen (Strings u.s.w.) habe ich auch noch gemacht. 

Endgültige Erfolgsmeldung kann ich erst nach ein paar Tagen geben.

Danke schonmal an alle, die sich den Kopf für mich angestrengt haben...

Bis dann...


----------



## Gast (24. Nov 2006)

Hallo,
mit DB-Zugriffen kenn ich mich zwar nicht so aus, aber hab da einen Tip zum Thread.
Man sollte Klassen die als Threads laufen sollen nicht von Thread erben lassen, das kostet zuviel Performance. Implementiere besser das Interface Runable.


Gruß, Michael


----------



## Murray (24. Nov 2006)

Gast hat gesagt.:
			
		

> Man sollte Klassen die als Threads laufen sollen nicht von Thread erben lassen, das kostet zuviel Performance. Implementiere besser das Interface Runable.



Ich würde normalerweise auch dazu raten, eher Runnable zu implementieren, weil man damit flexibler ist. Dass das aber Auswirkungen auf die Performance haben soll, leuchtet mir nicht ein. Kannst du das belegen?


----------



## Kawa-Mike (24. Nov 2006)

Hier ein Tip für die run()-Prozedur.
Besser ist es wenn du ein finally anhängst, um sicherzustellen das die Connection auf jeden Fall geschlossen wird !


```
} finally
 try{
 if(rs != null)rs.close();
 if(stmt != null)stmt.close();
 if(con != null) con.close();
 } catch(SQLException e){
   // ganz schlimm !
 }
}
```

So kannst du sicher sein das die Resultsets wirklich geschlossen werden und nicht noch irgendwo herumgeistern.
Nach dem close() würde ich auch die Variablen auf null setzten.
also z.b. rs.close();
rs = null;


----------



## metulszki (27. Nov 2006)

So,

Testlauf ist sehr erfolgreich verlaufen.

Der ConnectionPool hat eine Performance-Steigerung um ca. 200% gebracht, d.h. ich kann mir 2 Server sparen ;-)

Vielen Dank für eure Hilfe!


----------



## Gast (28. Nov 2006)

@Murray: Die Performance ergibt sich daraus, dass nicht mehr ein komplettes Objekt "Thread" benötigt wird. Sondern man von schlankeren Klassen(zumindest ja von Object) erben kann (schnellere Initialisierung z.B.) Die Auswirkungen sind aber eher im Bereich Speicherverbrauch zu sehen. Der gehört aber für mich mit zur Performance.

Gruß, Michael


----------



## Murray (28. Nov 2006)

Gast hat gesagt.:
			
		

> @Murray: Die Performance ergibt sich daraus, dass nicht mehr ein komplettes Objekt "Thread" benötigt wird. Sondern man von schlankeren Klassen(zumindest ja von Object) erben kann (schnellere Initialisierung z.B.) Die Auswirkungen sind aber eher im Bereich Speicherverbrauch zu sehen. Der gehört aber für mich mit zur Performance.



Wenn die Operation asynchron laufen soll, braucht man aber doch in jedem Fall einen Thread ?!?

Und ob ich nun schreibe

```
Thread t = new MyObject(); //--- MyObject extends Thread
t.start();
```
oder

```
Thread t = new Thread( new MyObject()); //--- MyObject implements Runnable
t.start();
```
ändern ja nichts daran, dass ein Thread-Objekt erzeugt wird (eigentlich wird im zweiten Fall ja sogar ein Objekt mehr erzeugt).


----------



## byte (28. Nov 2006)

Prinzipiell hast Du schon Recht, aber mindestens seit Java 5 kannst Du Threads auch Cachen (siehe _java.util.concurrent.Executors_). Das heisst, Java kümmert sich um die Verwaltung der Thread-Objekte und kann somit z.B. alte Threads wiederverwenden. Das spart Zeit.

Und wenn Du die Logik in einem Runnable implementierst, dann bist Du halt generell flexibler, weil der Programmcode nicht fest an einen Thread gebunden ist.


----------



## Murray (28. Nov 2006)

byto hat gesagt.:
			
		

> Und wenn Du die Logik in einem Runnable implementierst, dann bist Du halt generell flexibler, weil der Programmcode nicht fest an einen Thread gebunden ist.


Völlig unbestritten - ich leite eigentlich nie von Thread ab, aber das mache ich eben nicht aus Performance-Gründen


----------



## Azrahel (30. Nov 2006)

ich empfehl dir das ganze mal zu profilen. ich benutz dazu hyades (ist ein EclipsePlugin). Dann kannst du dir nämlich auch anzeigen lassen welche Objecte instanziiert wurden, welche der GC schon eingesammelt hat und welche noch nicht. Hat mir schon geholfen weil ich sehen konnte ob auch alle nicht mehr benötigten Objecte brav von GC eingesammelt wurden. 

www.eclipse.org/tptp/home/archives/index.html

www.eclipse.org/articles/Article-TPTP-Profiling-Tool/tptpProfilingArticle.html


----------

