Schätzfrage: Wieviel Prozessorpower brauche ich?

Status
Nicht offen für weitere Antworten.

metulszki

Mitglied
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

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

Das folgende Beispiel kann vielleicht als Vereinfachung deines Szenarios durchgehen:
Code:
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.
 
G

Guest

Gast
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
 

Wildcard

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

metulszki

Mitglied
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

Top Contributor
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

Mitglied
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

Top Contributor
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

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

metulszki

Mitglied
So sieht er aus:

Code:
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);
        }
    }
}
 

Murray

Top Contributor
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.
Code:
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.
 
G

Guest

Gast
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...
 
G

Gast

Gast
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

Top Contributor
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

Mitglied
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 !

Code:
} 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

Mitglied
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!
 
G

Gast

Gast
@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

Top Contributor
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
Code:
 Thread t = new MyObject(); //--- MyObject extends Thread
t.start();
oder
Code:
 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

Top Contributor
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

Top Contributor
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

Bekanntes Mitglied
Status
Nicht offen für weitere Antworten.
Ähnliche Java Themen

Ähnliche Java Themen

Neue Themen


Oben