2017-02-06 2 views
1

Wir haben unser Thread-per-Verbindung-Kommunikationsmodell auf einen asynchronen IO-basierten TCP-Server unter Verwendung von boost::asio migriert. Der Grund für diese Änderung ist, dass das alte Modell nicht gut genug skaliert hat. Wir haben permanent etwa 2k persistente Verbindungen im Durchschnitt mit der Tendenz, auf einer monatlichen Basis zu wachsen.Eine optimale Anzahl von Worker-Threads in einem asynchronen IO-TCP-Server

Meine Frage ist, was wäre die ideale Anzahl von Worker-Threads, die die io_service-Warteschlange für Completion-Handler abfragen wird - die Anzahl der virtuellen CPU-Kerne?

Die Auswahl einer kleinen Zahl kann zu Situationen führen, in denen der Server nicht schnell genug konsumiert und nicht mit der Rate umgehen kann, mit der die Clients Nachrichten senden.

Ist es sinnvoll, in solchen Situationen dynamisch Arbeitsthreads hinzuzufügen?

Update: Wahrscheinlich ist es meine Implementierung aber ich finde diese Aussage Teil des Boost Asio Doku verwirrend:

Implementierungsstrategien wie Thread-per-Verbindung (die ein synchroner-only-Ansatz würde erfordern,) kann das System Leistung verschlechtern, aufgrund der erhöhten Kontextwechsel, Synchronisation und Datenbewegung zwischen den CPUs. Bei asynchronen Operationen ist es möglich die Kosten der Kontextumschaltung zu vermeiden, indem die Anzahl der Betriebssystem-Threads - in der Regel eine begrenzte Ressource - und nur Aktivierung der logischen Threads der Steuerung, die Ereignisse auf verarbeiten.

Als ob Sie X Threads auf einer Maschine Abschluss Ereignisse Pumpen, die X-Kern hat - 1) Sie keine Garantien haben, dass jeder Thread eine eigene CPU bekommt und 2) wenn meine Verbindungen sind persistent i don Es gibt keine Garantien, dass der Thread, von dem gesagt wird, dass er einen async_read ausführt, derselbe ist wie derjenige, der den Beendigungshandler ausführt.

void Connection::read { 
    boost::asio::async_read(socket, boost::asio::buffer(buffer, 18), 
          boost::bind(&Connection::handleRead, shared_from_this(), 
          boost::asio::placeholders::error, 
          boost::asio::placeholders::bytes_transferred)); 
} 

void Connection::handleRead(const boost::system::error_code &error, 
               std::size_t bytes_transferred) { 
    // processing of the bytes 
    ... 
    // keep processing on this socket 
    read(); 
} 
+3

Dies ist eindeutig unmöglich zu beantworten, außer zu sagen "es kommt darauf an". Was ich sagen würde ist, dass Ihr ideales Design eins sein sollte, nicht mehr als zwei Kernel-Threads pro NIC installiert bis zur NIC-Bandbreitesättigung. Wenn Sie das überschreiten, müssen Sie Ihre Software umgestalten, um nicht so ineffizient zu sein. Lesen Sie, wie nginx eine NIC-Skalierbarkeit von 10 Gbit/s implementiert hat, oder bringen Sie einen ASIO-Berater mit, der Sie berät. –

+2

Leute (nicht notwendigerweise Sie) neigen dazu, die Macht eines einzelnen Threads stark zu unterschätzen. Die Menge an Arbeit, die ein einzelner Thread tun kann, ist kolossal. Die Bedenken, die Sie ausgleichen müssen, sind 1) Kontextwechsel von zu vielen Threads, die Latenz- und Durchsatzprobleme verursachen, 2) hohe Latenz und schlechter Durchsatz durch zu wenige Threads, die Abschlussereignisse pumpen, und 3) höhere als erforderliche Arbeitssatzgröße aufgrund übermäßigen Threads Stapel, die eine schlechte CPU-Cache-Leistung verursachen. Das Gleichgewicht zwischen all dem für Ihre Arbeit ist der Sweet Spot, und der einzige Weg, es zu finden, ist zu experimentieren und zu messen. – hoodaticus

+0

Ich möchte mehr darüber nachgehen. Was wir mit dem async-IO-Ansatz sehen, ist, dass die Latenzprobleme wie erwartet weg sind, aber die absolute Anzahl von ctxt-Schaltern/Sekunde ist ungefähr 10 mal größer als die, die wir in dem Thread pro Verbindungsmodell hatten. Mit anderen Worten, 24 IO-Threads, die die Abschluss-Ereignisse pumpen, erzeugen viel mehr Kontext-Switches als ~ 1 k-Worker-Threads im alten Modell. Kannst du bitte deine Gedanken dazu teilen? – ladaManiak

Antwort

1

In einer idealen Situation mit perfekt nicht blockierende E/A, eine Arbeitsseite, die vollständig in der L1-Cache passt, und keine anderen Prozesse in dem physikalischen System, wobei jeder Thread die gesamten Ressourcen eines Prozessorkerns verwenden . In einer solchen Situation ist die ideale Anzahl von Threads eine pro logischem Kern.

Wenn ein Teil Ihrer E/A blockiert, dann ist es sinnvoll, mehr Threads als die Anzahl der Kerne hinzuzufügen, so dass keine Kerne im Leerlauf sind. Wenn die Hälfte der Thread-Zeit blockiert ist, sollten Sie fast 2 Threads pro Kern haben. Wenn 75% der Threadzeit blockiert sind, solltest du 3 oder 4 pro Kern haben, und so weiter. Context Switching Overhead gilt als Blockierung für diesen Zweck.

Mir ist aufgefallen, dass Microsoft, wenn man blind darüber rät, sich für zwei oder vier Threads pro Kern entscheidet. Abhängig von Ihrem Budget, um diese Entscheidung zu treffen, würde ich entweder mit 2 oder 4 gehen oder mit einem Thread pro Kern beginnen und mich hocharbeiten, indem ich den Durchsatz (Anfragen gewartet/Sekunde) und Latenz (min, max und durchschnittliche Antwortzeit) messen würde. bis ich den Sweet Spot getroffen habe.

Die dynamische Anpassung dieses Wertes ist nur sinnvoll, wenn Sie mit radikal unterschiedlichen Programmen arbeiten. Für eine vorhersehbare Arbeitslast gibt es einen optimalen Punkt für Ihre Hardware, der sich auch mit steigendem Arbeitsaufwand kaum ändern sollte. Wenn Sie einen allgemeinen Webserver erstellen, ist wahrscheinlich eine dynamische Anpassung erforderlich.

Verwandte Themen