Java Threadpool vs. neuer Thread im Hochanforderungsszenario

8

Ich habe einen alten Java-Code für einen REST-Service, der für jede eingehende Anfrage einen separaten Thread verwendet. I.e. Die Hauptschleife würde socket.accept () durchlaufen und den Socket an eine Runnable übergeben, die dann ihren eigenen Hintergrundthread startet und den Run auf sich selbst aufruft. Dies hat eine Weile gut bewundert, bis mir aufgefallen ist, dass die Verzögerung von accept zur Verarbeitung der Anfrage unter hoher Last inakzeptabel werden würde. Wenn ich bewundernd gut sage, meine ich, dass es 100-200 Anfragen pro Sekunde ohne signifikante CPU-Auslastung handhabt. Die Performance verschlechterte sich nur dann, wenn andere Daemons auch Last hinzufügten und dann nur einmal die Last 5 überstiegen. Wenn die Maschine unter einer hohen Last (5-8) stand, würde die Zeit von der Annahme bis zur Verarbeitung lächerlich hoch werden ( 500ms bis 3000ms) während die eigentliche Verarbeitung unter 10ms blieb. Das ist alles auf Dual-Core-Centos 5-Systemen.

Da ich an Threadpools auf .NET gewöhnt war, nahm ich an, dass die Thread-Erstellung der Schuldige war, und ich dachte, ich würde das gleiche Muster in Java anwenden. Jetzt wird meine Runnable mit ThreadPool.Executor ausgeführt (und der Pool verwendet und ArrayBlockingQueue). Wiederum funktioniert es in den meisten Szenarien hervorragend, es sei denn, die Maschinenlast wird hoch, dann zeigt die Zeit vom Erstellen des Runnable, bis run () aufgerufen wird, ungefähr das gleiche lächerliche Timing. Schlimmer noch, die Systemlast hat sich mit der Threadpool-Logik fast verdoppelt (10-16). So, jetzt bekomme ich die gleichen Latenzprobleme mit der doppelten Last.

Mein Verdacht ist, dass die Sperrkonkurrenz der Warteschlange schlechter ist als die vorherigen Kosten für das Neustarten von Threads, die keine Sperren hatten. Kann jemand seine Erfahrung von neuem Thread vs. threadpool teilen. Und wenn mein Verdacht richtig ist, hat jemand einen alternativen Ansatz, um mit einem Threadpool ohne Sperrkonflikt umzugehen?

Ich wäre versucht, das ganze System nur single-threaded zu machen, da ich nicht weiß, wie viel mein Threading hilft und IO scheint kein Problem zu sein, aber ich bekomme einige Anfragen, die langlebig sind das würde dann alles blockieren.

danke, arne

UPDATE: Ich wechselte zu Executors.newFixedThreadPool(100); und während es die gleiche Verarbeitungskapazität beibehielt, lud die Ladung ziemlich schnell gleich und das Laufen für 12 Stunden zeigte, dass die Last konstant bei 2x blieb. Ich denke in meinem Fall ist ein neuer Thread pro Anfrage günstiger.

    
Arne Claassen 01.03.2009, 22:58
quelle

4 Antworten

12

Mit der Konfiguration von:

%Vor%

Wenn dann 10 Threads gleichzeitig Anfragen verarbeiten, werden weitere Anfragen zur Warteschlange hinzugefügt, es sei denn, sie erreichen 100 Anfragen in der Warteschlange, zu diesem Zeitpunkt werden neue Threads erstellt, es sei denn, es sind bereits 100 Threads bei der Verarbeitung von Der Befehl wird abgelehnt.

Der Abschnitt der Javadocs von ThreadPoolExecutor (kopiert unten) kann einen anderen lesen wert sein.

Basierend auf ihnen und Ihrer offensichtlichen Bereitschaft, 100 Threads laufen zu lassen, und Ihr Wunsch, alle Anfragen zu akzeptieren und schließlich zu verarbeiten. Ich würde empfehlen, Variationen zu versuchen wie:

%Vor%

Was übrigens aus Executors.newFixedThreadPool(100);

stammt
  

Warteschlangen

     

Jede BlockingQueue kann verwendet werden, um gesendete Aufgaben zu übertragen und zu halten. Die Verwendung dieser Warteschlange interagiert mit der Poolgröße:

     
  • Wenn weniger als corePoolSize-Threads ausgeführt werden, fügt der Executor immer lieber einen neuen Thread hinzu als eine Warteschlange.
  •   
  • Wenn corePoolSize oder mehrere Threads ausgeführt werden, zieht es der Executor immer vor, eine Anfrage in die Warteschlange zu stellen, anstatt einen neuen Thread hinzuzufügen.
  •   
  • Wenn eine Anfrage nicht in die Warteschlange gestellt werden kann, wird ein neuer Thread erstellt, es sei denn, dies würde maximumPoolSize überschreiten. In diesem Fall wird die Aufgabe zurückgewiesen.
  •   

Es gibt drei allgemeine Strategien für die Warteschlange:

     
  1. Direkte Übergaben. Eine gute Standardauswahl für eine Arbeitswarteschlange ist eine SynchronousQueue, die Tasks an Threads weitergibt, ohne sie anderweitig zu halten. Hier schlägt ein Versuch fehl, eine Aufgabe in die Warteschlange zu stellen, wenn keine Threads sofort verfügbar sind, um sie auszuführen, so dass ein neuer Thread erstellt wird. Diese Richtlinie vermeidet Sperren beim Verarbeiten von Anforderungen, die möglicherweise interne Abhängigkeiten aufweisen. Direkte Weiterreichungen erfordern in der Regel unbegrenzte maximumPoolSizes, um die Ablehnung neu eingereichter Aufgaben zu vermeiden. Dies wiederum ermöglicht die Möglichkeit eines unbegrenzten Thread-Wachstums, wenn Befehle im Durchschnitt schneller ankommen als sie verarbeitet werden können.
  2.   
  3. Unbegrenzte Warteschlangen. Wenn Sie eine unbegrenzte Warteschlange verwenden (z. B. eine LinkedBlockingQueue ohne eine vordefinierte Kapazität), warten neue Aufgaben in der Warteschlange, wenn alle corePoolSize-Threads beschäftigt sind. Daher werden niemals mehr als corePoolSize-Threads erstellt. (Und der Wert von maximumPoolSize hat daher keine Auswirkungen.) Dies kann sinnvoll sein, wenn jeder Task vollständig unabhängig von anderen ist, sodass sich die Ausführung von Tasks nicht gegenseitig beeinträchtigen kann. zum Beispiel in einem Webseitenserver. Während diese Warteschlangendarstellung nützlich sein kann, um vorübergehende Bursts von Anforderungen zu glätten, erlaubt sie die Möglichkeit eines unbegrenzten Wachstums der Arbeitswarteschlange, wenn Befehle im Durchschnitt schneller ankommen als sie verarbeitet werden können.
  4.   
  5. Begrenzte Warteschlangen. Eine beschränkte Warteschlange (z. B. eine ArrayBlockingQueue) verhindert die Erschöpfung von Ressourcen, wenn sie mit endlichen maximumPoolSizes verwendet wird, kann jedoch schwieriger zu optimieren und zu steuern sein. Warteschlangengrößen und maximale Poolgrößen können gegeneinander abgewogen werden: Die Verwendung großer Warteschlangen und kleiner Pools minimiert die CPU-Auslastung, Betriebssystemressourcen und den Kontext-Switching-Overhead, kann jedoch zu einem künstlich niedrigen Durchsatz führen. Wenn Aufgaben häufig blockieren (z. B. wenn sie I / O-gebunden sind), kann ein System möglicherweise Zeit für mehr Threads einplanen, als Sie andernfalls zulassen. Die Verwendung von kleinen Warteschlangen erfordert im Allgemeinen größere Poolgrößen, wodurch die CPUs beschäftigt bleiben, es kann jedoch zu einem inakzeptablen Planungsaufwand kommen, was ebenfalls den Durchsatz verringert.
  6.   
    
Stephen Denne 04.03.2009, 07:34
quelle
4

Messung, Messung, Messung! Wo verbringt es die Zeit? Was muss passieren, wenn du dein Runnable erstellst? Hat das Runnable irgendetwas, das die Instanziierung blockieren oder verzögern könnte? Was passiert während dieser Verzögerung?

Ich bin eigentlich ein großer Gläubiger, Dinge allgemein zu denken, aber diese Art von Fall, mit unerwartetem Verhalten wie diesem, muss nur einige Messungen haben.

Was ist die Laufzeitumgebung, die JVM-Version und die Architektur?

    
Charlie Martin 01.03.2009 23:05
quelle
1

Suns Implementierung von Thread , obwohl sehr viel schneller als früher, hat Locking. IIRC, ArrayBlockingQueue sollte bei Besetzt überhaupt nicht gesperrt werden. Daher ist es Profiler Zeit (oder auch nur ein paar ctrl-\ s oder jstack s).

Die Systemlast sagt Ihnen nur, wie viele Threads in der Warteschlange sind. Es ist nicht unbedingt sehr nützlich.

    
Tom Hawtin - tackline 02.03.2009 12:56
quelle
0

Ich habe das mit meinem eigenen Code gemacht. Ich habe den Netbeans-Profiler verwendet, um die Thread-Pool-Implementierung zu ändern, die ich verwendet habe. Sie sollten dasselbe mit der Visual VM machen können, aber ich habe das noch nicht ausprobiert.

    
TofuBeer 01.03.2009 23:43
quelle

Tags und Links