Server-Tuning durch manuelles Scheduling

07.02.2005 von ANNIE FOONG, JASON FUNG  und Donald Newell
Bei SMP-Servern skaliert der Netzwerkdurchsatz nicht besonders gut mit der Anzahl der CPUs. Doch weist man Interrupts und den TCP/IP-Stack manuell einzelnen Prozessoren zu, steigt der Durchsatz um bis zu 30 Prozent.

Ein Netzwerkdurchsatz im Bereich von mehreren Gbit/s treibt selbst aktuelle SMP-Server an ihre Grenzen. Besonders die Netzwerkprotokoll-Stacks der gängigen Betriebssysteme stellen einen limitierenden Faktor dar. Gerade die Implementierungen des TCP/IP-Stacks sind bekannt dafür, dass sie bei Multiprozessorrechnern nicht sehr gut mit der Prozessorzahl skalieren [7, 9]. Dieser Artikel erläutert, wie man durch eine manuelle Zuordnung von Threads, Prozessen und Interrupts an einzelne Prozessoren den Netzwerkdurchsatz signifikant steigern kann.

Die Versuche basieren auf dem TCP/IP-Stack von Redhat Linux 2.4.20. Dabei wird die Auswirkung der set_schedaffinity()-Befehle untersucht, die ab dem Kernel 2.6 offiziell im Mainstream-Linux enthalten sind. Mit dem damit möglichen manuellen Setzen der Prozessoraffinität umgeht man den Scheduler des Betriebssystems. Man zwingt ihn, bestimmte Aufgaben immer der gleichen physikalischen CPU zuzuteilen.

Allein die manuelle Zuweisung der Interrupts von Netzwerkkarten an eine CPU kann den Datendurchsatz um bis zu 25 Prozent steigern. In Kombination mit einer Prozesszuweisung an einzelne CPUs sind Steigerungen um bis zu 30 Prozent möglich. Dieser Artikel zeigt außerdem, wie sich die L2- und L3-Cache-Misses verringern, wenn man die manuelle Prozessoraffinität entsprechend nutzt.

Prozessverteilung unter SMP

Der TCP-Overhead bei der datenkommunikation ist gut untersucht [2, 3], den Overhead durch das Scheduling und die Interrupt-Verwaltung übersah man jedoch meist. Obwohl dabei nur wenige CPU-Takte benötigt werden, haben aber beide speziell bei Mehrprozessorsystemen einen entscheidenden Einfluss auf die Effektivität des Cache-Systems.

Der Scheduler von typischen SMP-Betriebssystemen versucht, die Last gleichmäßig auf die CPUs zu verteilen. Dazu verschiebt er Prozesse von stark ausgelasteten Prozessoren auf weniger beschäftigte. Allerdings zollt man jedes Mal Tribut, wenn das planmäßige Scheduling oder ein externer Interrupt einen Prozess auf eine andere CPU verschiebt. Der Prozess muss dann auf der neuen CPU zunächst die prozessorinternen Caches mit seinen Daten füllen, bevor er mit voller Geschwindigkeit arbeiten kann.

Speziell die Hardware-Interrupts sind dabei problematisch, da die Betriebssysteme sie nicht gleichmäßig über die Prozessoren verteilen können. Der I/O Advanced Programmable Interrupt Controller (APIC) auf dem Mainboard ist verantwortlich dafür, die Interrupts der verschiedenen Hardware-Komponenten an die prozessorinternen APICs weiterzuleiten. Je nach Konfiguration verteilt der externe APIC die Interrupts gemäß seiner Programmierung in der I/O APIC Redirection Table. Alternativ wählt er die CPU für die Verarbeitung des Interrupts, die geraden den Prozess mit der niedrigsten Priorität bearbeitet.

Sowohl Windows NT als auch Linux nutzen in der SMP-Standardkonfiguration die zweite Möglichkeit. Da der Kernel die Prioritätslisten aber nur sporadisch aktualisiert, landen alle Interrupts für längere Zeit auf einer einzigen CPU. Dieser Prozessor erreicht bei hohem Datendurchsatz der Netzwerkkarten einen Sättigungszustand, wodurch das ganze System gebremst wird.

Unerwünschte Nebeneffekte

Linux ab Kernel 2.6 nutzt einen intelligenteren Verteilungsalgorithmus. Es übergibt kurzzeitig alle Interrupts an einen Prozessor. Dann wählt es zufällig den zuständigen Prozessor für die nächste, relativ kurze Zeitspanne aus. Diese Aufteilung beseitigt zwar den Flaschenhals von langen Zuordnungszeiten, ist aber mit Aufwand verbunden und wählt nicht immer die beste CPU aus.

Obwohl frühere Untersuchungen zur Prozessoraffinität viel versprechend sind [11], kann das unkoordinierte Verteilen von Interrupts auf verschiedene Prozessoren zu negativen Effekten führen [1]. Die Interrupt-Routinen landen wahllos auf den verschiedenen Prozessoren, was zu einem Wettstreit um gemeinsame Ressourcen führt. Zudem wird der Cache-Inhalt - wie etwa der TCP-Kontext - nicht optimal genutzt, wenn jedes neue Netzwerkpaket auf einem anderen Prozessor landet.

Im Folgenden werden die Auswirkungen untersucht, die sich ergeben, wenn eine Anwendung die Prozessoraffinität selbst bestimmt. Man geht davon aus, dass die Anwendung die Charakteristika der eigenen Last bestens kennt. Deshalb kann sie die Verteilung auf einzelne Prozessoren vorteilhafter erledigen als der Scheduler des Betriebssystems.

Die manuelle Zuordnung einzelner Prozesse an einen Prozessor ist dabei relativ einfach. Standard-APIs unter Linux und Windows erlauben es, Anwendungen, Prozesse, Threads und auch Interrupts fest an einzelne CPUs zu binden [8,9]. Die umfangreichen Ergebnisse der Untersuchung zeigen die charakteristischen Vorteile dieses Verfahrens [4]. Details zur Durchführung und eine Zusammenfassung der Ergebnisse finden Sie auf den folgenden Seiten.

Testaufbau

Folgende Tabelle zeigt die Konfiguration des Servers und der vier Clients in der Testumgebung.

Testumgebung

Getesteter Server

Je Client

Prozessoren

2 x Intel 2 GHz Xeon MP

2 x Intel 3,06 GHz Xeon

Cache

512 KByte L2, 2 MByte L3

512 KByte L2

FSB

400 MHz

533 MHz

Speicher

DDR 200 MHz Registered ECC, 4 Channels mit je 256 MByte

2 Channels mit je 2 GByte

Netzwerkkarte

4 x Dual-port Intel Pro/1000 MT

Dual-port Intel Pro/1000 MT

Das Bild zeigt den Aufbau des kleinen Test-Clusters. Als Benchmark kam der ttcp-Micro-Benchmark zum Einsatz. Er tauscht Datenpakete zwischen je zwei Knoten in beide Richtungen aus. Der ttcp-Micro-Benchmark arbeitet dabei auf dem üblichen, so genannten Fast Path des TCP-Stacks. Im Test ist je eine Verbindung mit einer IP-Adresse an eine Instanz des Benchmarks gebunden. Diese wird jeweils von einem Port einer Netzwerkkarte bedient. Auf dem Test-Server laufen also acht Benchmark-Instanzen, die mit ihren acht IP-Adressen je einen Netzwerkanschluss belegen.

Manuelle Zuweisung unter Linux

Interrupts einer Netzwerkkarte lassen sich in Linux statisch einem Prozessor zuweisen, indem man die Bitmaske im /proc-Filesystem entsprechend setzt. Details dazu finden sich in der Linux-Source-Tree-Dokumentation [13]. Die Datei /proc/irq/<irq num >/smp_affinity enthält dabei die Interrupt-Maske für den Interrupt mit der Nummer <irq num >. Zunächst gilt es, herauszufinden, welcher Interrupt zu welchem Device gehört. Diese Information enthält die Datei /proc/interrupts, die folgendermaßen aussieht:

[root@chaos /proc] cat /proc/interrupts
CPU0 CPU1
0 : 500984 400880 IO-APIC-edge timer
.. ..
.. ..
25: 13090 0 eth0
NMI:
LOC:
ERR:

Hat man den Interrupt der Netzwerkkarte ermittelt (im Beispiel hat eth0 den IRQ 25), kann man durch folgenden Befehl alle Interrupts der Karte zumindest bis zum nächsten Reboot der CPU1 zuordnen:

[root@chaos] echo 02 > /proc/irq/25/smp_affinity

Das Setzen von Bitnummer <n> (beginnend ab 0) bindet den Interrupt fest an die CPU-Nummer <n>.

Kernel-Versionen

Seit der Kernel-Version 2.4 unterstützt Linux die Interrupt-Zuordnung an eine festgelegte CPU. Wie die Tabelle zeigt, existiert für die feste Prozesszuordnung an eine CPU erst seit kurzem ein einheitliches Verfahren. Allerdings unterscheiden sich die übergebenen Datentypen noch je nach Distribution.

Prozessoraffinität bei unterschiedlichen Linux-Kernels

Kernel.org-Distributionen

2.4.20

Kein Support

2.4.21

set_cpus_allowed()

2.6.5

set_cpus_allowed(), sched_setaffinity()

Redhat-Distributionen

2.4.18-14 (RH8)

set_cpus_allowed()

2.4.20-13 (RH9)

set_cpus_allowed(), sched_setaffinity()

Für den Test wurde der ttcp-Benchmark so modifiziert, dass er über das entsprechende API die Prozess- oder Thread-Affinität verändern kann. Im User Mode ist die Funktion sched_setaffinity() dafür zuständig. Folgendes Beispiel demonstriert die Zuordnung des aktuellen Prozesses an die CPU1:

#include <sched.h>
extern int sched_setaffinity(pid_t __pid, unsigned int __len, unsigned long int *__mask) __THROW;

unsigned long cpumask = 0x02; // bit 1 in mask refers to CPU1

// PID (1st arg)=0 denotes current process
sched_setaffinity(0, sizeof(cpumask), &cpumask);

Im Kernel Mode lautet der Aufruf folgendermaßen:

#include <linux/sched.h>

unsigned long cpumask = 0x02; // bit 1 in mask refers to CPU1
set_cpus_allowed(current, cpumask);

Ergebnisse

Folgende Diagramme zeigen den TCP-Durchsatz und die Prozessorbelastung für vier Affinitätsszenarien:

Die Prozessaffinität allein hat kaum einen Einfluss auf die Leistung. In diesem Modus muss die CPU0 nicht nur alle Interrupts, sondern auch noch vier ttcp-Prozesse abarbeiten. Alle möglichen Vorteile frisst das Ungleichgewicht der Lastverteilung wieder auf.

Dagegen kann die alleinige Interrupt-Affinität den Netzwerkdurchsatz um bis zu 25 Prozent steigern. Dies beruht auf einer Eigenart des Schedulers. Um den lokalen Cache einer CPU besser auszunutzen, versucht der Scheduler, einen Prozess immer auf die CPU zu legen, auf der er zuletzt gelaufen ist. Ebenso laufen die höheren Ebenen des Interrupt-Handlers auf der CPU, die zuvor den ersten Teil des Interrupts bearbeitet hat. Deshalb erreicht die Interrupt-Affinität in gewissem Maße auch eine Prozessaffinität.

Allerdings landen Interrupt und zugehöriger Prozess manchmal auch auf verschiedenen Prozessoren. Deshalb bringt die volle Affinität mit bis zu 29 Prozent Durchsatzsteigerung den größten Effekt. Einen anderen Blickwinkel liefern die nötigen CPU-Takte pro übertragenem Bit. Dieses Kostenmaß berücksichtigt den Einsatz von beiden Prozessoren und den Durchsatzgewinn gleichzeitig. Mit voller Affinität sinkt der Aufwand pro Bit demnach um 24 Prozent.

Analyse

Um die Gründe der Beschleunigung zu verstehen, konzentrieren wir uns im Folgenden auf die zwei Extrema, die folgendes Bild nochmals grafisch darstellt.

In beiden Fällen wurden die Gesamtzahl der benötigten CPU-Takte und die Zahl der Cache-Misses gezählt. Diese Daten liefern die prozessorinternen Hardware Event Counter. Die Auswertung der Zyklen und Cache-Misses kann mit Programmen wie VTune [12] oder Oprofie [10] erfolgen.

Die Prozessaffinität verhindert, dass ein Prozess unnötig von einer CPU zur anderen wandert. Die Interrupt-Affinität sorgt dafür, dass alle Ebenen einer Interrupt-Verarbeitung auf demselben Prozessor ablaufen. Wählt man wie im Beispiel die volle Affinität, läuft die Interrupt-Verarbeitung der Netzwerkkarte zudem auf derselben CPU, die später auch die höheren Ebenen des TCP-Stacks und die Anwendung selbst ausführt.

Mit anderen Worten: Es existiert also ein direkter Ausführungsweg innerhalb der CPU. Da die Codeausführung und somit der Datenzugriff immer auf derselben CPU stattfindet, erreicht man eine sehr hohe Rate an Cache-Hits. Die höheren Schichten im Protokoll-Stack können auf Daten zurückgreifen, die die niedrigeren Schichten bereits in den lokalen Cache geladen haben.

Die folgende Tabelle zeigt die Zeiteinsparungen (Taktzykleneinsparung), die in den beiden betrachteten Fällen aus der besseren Cache-Ausnutzung entstehen. Alle Zahlen sind normiert auf die geleistet Arbeit. Zu beachten ist, dass die Verzögerungen bei einem L3-Cache-Miss deutlich höher ausfallen als bei einem L2-Cache-Miss. Deshalb dürften L3-Misses eine wesentlich größere Auswirkung auf die Leistung des Gesamtsystems haben.

Verbesserung beim Übergang zur vollen Affinität

Netzwerktransfer und Blockgröße

Taktzyklen

L2-Misses

L3-Misses

TX 128B

9.4%

30.3%

25.6%

TX 1024B

20.7%

38.5%

35.9%

TX 65536B

15.8%

50.0%

38.3%

RX 128B 1

3.7%

26.0%

27.2%

RX 1024B

20.3%

35.1%

34.5%

RX 65536B

21.0%

18.9%

32.5%

Fazit

Der Netzwerkdurchsatz auf SMP-Servern lässt sich schon mit relativ einfachen Mitteln steigern. Dafür ist weder eine zusätzliche Hardware noch ein größerer Eingriff in den Programmcode nötig. In der Studie wurden jedoch nur der reine Datenverkehr und die Prozessorzuordnung im bestmöglichen Fall untersucht.

Die vollstatische Zuordnung bringt bei ständig wechselnden Bedingungen und variablen Anwendungen nicht unbedingt Vorteile. Einen dedizierten Server wie etwa einen Webserver mit einer festgelegten Anzahl an Arbeits-Threads und Netzwerkkarten kann sie aber gehörig beschleunigen. Zwei weit verbreitete Webserver (Redhat TUX und Microsoft IIS) erlauben daher bereits, die Arbeits-Threads und Prozesse einzelnen CPUs zuzuordnen [15].

Kommende Netzwerkkarten werden die Pakete tiefer analysieren und eine Flussanalyse zulassen (Receive Side Scalling) [9]. Anhand dieser Informationen wählen sie dynamisch den Prozessor zur Interrupt-Verarbeitung aus, der die meisten zum Netzwerkpaket passenden Daten im Cache hält.

Mit den in der Untersuchung gewonnenen Ergebnissen lassen sich neue Verfahren zur Affinitätsverteilung erstellen. Die grundlegenden Messmethoden können dabei auch auf andere Anwendungen übertragen werden. Dies wird in Zukunft zunehmend der Fall sein, denn die aktuellen Prozessoren mit mehreren Kernels auf einem Die verwandeln jeden einfachen Single-Prozessor-Desktop-Rechner in ein SMP-System. Daher wird die Affinitätsvergabe zukünftig eine zentrale Rolle in jedem Betriebssystem spielen. (ala)

Über die Autoren: Annie Foong und Jason Fung sind Senior Research Engineer und Donald Newell Principal Engineer im Intel Communications Technology Laboratory. Dieser Artikel wurde erstmals im Intel Developer Service veröffentlicht. Die deutsche Zweitverwertung erfolgt mit Genehmigung von Intel.

Literaturverzeichnis

1. V. Anand and B. Hartner. TCPIP Network Stack Performance in Linux Kernel 2.4. and 2.5. In Proc. of the Linux Symposium, Ottawa June 2002.

2. J. Chase, A. Gallatin and K. Yocum. End-System Optimizations for High-Speed TCP. IEEE Comms, 39:4, April 2001.

3. A. Foong, T. Huff, H. Hum, J. Patwardhan and G. Regnier. TCP performance re-visited. In Proc. of the IEEE Intl. Symposium on Performance of Systems & Software, Austin, Mar 2003.

4. A. Foong, J. Fung and D. Newell. An In-depth Analysis of the Impact of Processor Affinity on Network Performance. To appear in IEEE Intl. Conference on Networks, Nov 2004.

5. IA-32 Intel(r) Architecture Software Developer's Manual: Systems Programming Guide, Vol 3, Intel Corporation, 2002.

6. J. Kay and J. Pasquale. The importance of non-data touching processing overheads in TCP/IP. In Proc. of ACM SIGCOMM, San Francisco, 1993.

7. P. Leroux. Meeting the bandwidth challenge: Building Scalable Networking Equipment Using SMP. Dedicated Systems Magazine, 2001.

8. R. Love. Linux Kernel Development, Sams Publishing, 2004.

9. Scalable Networking: Eliminating the Receive Processing Bottleneck-Introducing RSS. Microsoft whitepaper, available at http://www.microsoft.com/whdc/.

10. Profile: A system-wide profiling tool for Linux. Available at http://oprofile.sourceforge.net

11. J. Salehi, J. Kurose and D. Towsley, "The effectiveness of affinity-based scheduling in multiprocessor network protocol processing", IEEE/ACM Trans. on Networking, vol 4:4, pp 516530, 1996.

12. VTune performance Analysis Tool. Available at http://www.intel.com/software/products/vtune/

13. IRQ affinity Documentation. Linux source tree>/Documentation/IRQ-affinity.txt

14. Cross-referencing Linux, available at http://lxr.linux.no

15. What's New in Internet Information Services 6.0. Microsoft product information, http://www.microsoft.com/windowsserver2003/evaluation/ overview/technologies/iis.mspx, 2003.