Zurück zum Blog

Folgen und abonnieren

Nur auf Englisch verfügbar

Diese Seite ist momentan nur auf Englisch verfügbar. Wir entschuldigen uns für die Unannehmlichkeiten. Bitte besuchen Sie diese Seite später noch einmal.

QUIC oder TCP: Was ist besser?

Kazuho Oku

Principal OSS Engineer

Jana Iyengar

VP of Product, Infrastructure Services, Fastly

Wir haben schon häufig darüber gesprochen, wie sehr wir QUIC schätzen (und warum wir an einer eigenen Implementierung namens quicly arbeiten). QUIC verspricht geringere Latenzzeiten, einen verbesserten Durchsatz, Netzwerkstabilität bei Nutzung verschiedener Clients sowie mehr Datenschutz und Sicherheit. Spannenderweise steht die QUIC-Arbeitsgruppe der IETF kurz davor, die erste Version von QUIC fertigzustellen, die für den Einsatz im gesamten Internet gedacht ist. Zwar freuen sich viele Developer und Teams, die QUIC bereits nutzen auf einen breiten Einsatz des Protokolls, doch es kommen immer wieder Bedenken auf: Wie hoch sind die Compute-Kosten für die zusätzlichen Funktionen und den Schutz von QUIC? Wie kann man QUIC als Ersatz für TCP anpreisen, wenn es deutlich mehr Rechenleistung benötigt?

Fastly hat versucht, diese Fragen für Sie zu beantworten. Wir haben einige Tests durchgeführt und können grundsätzlich bestätigen, dass QUIC genauso recheneffizient sein kann wie TCP!

Bevor Sie aber die Korken knallen lassen, müssen wir eines schon vorab zugeben: Wir haben einen einfachen Versuchsaufbau und eine einfache Benchmark verwendet. Es sind also weitere Tests mit realistischerer und repräsentativer Hardware und entsprechenden Traffic-Szenarien notwendig. Wichtig ist, dass wir keine Hardware-Offloads für TCP oder QUIC aktiviert hatten. Unser Ziel war es, ein einfaches Szenario mit synthetischem Traffic zu verwenden, um einige der offensichtlicheren Rechenengpässe zu vermeiden und Erkenntnisse darüber zu gewinnen, wie wir die Kosten von QUIC senken können.

Dennoch waren wir überrascht, dass QUIC selbst in unserem einfachen Versuchsszenario genauso gut abschnitt wie TCP.

Sie können sich unseren Versuch so vorstellen, als hätten wir unser Auto auf einer Rennstrecke gegen einen Ferrari antreten lassen. Eine Rennstrecke ist eine hochgradig künstliche Umgebung, in der das Fahrerlebnis nicht repräsentativ für den Alltag ist (außer vielleicht für Rennfahrer). Indem wir uns aber damit beschäftigen, wie wir das Erlebnis auf unserer Rennstrecke so gut wie möglich optimieren können, fällt es uns leichter, Engpässe aufzudecken. Außerdem lassen sich aus den auf den Alltag übertragbaren Erkenntnissen, die wir dabei gewinnen, Maßnahmen ableiten, um diese Engpässe zu beseitigen. Und genau darum geht es in diesem Blogpost.

Hintergrund

TCP ist seit langem das Arbeitspferd des Internets. Im Laufe der Jahre wurden viele Anstrengungen unternommen, seine Implementierungen zu optimieren, um es recheneffizienter zu machen. Die Entwicklung von QUIC hingegen steckt noch in den Kinderschuhen. Es ist noch nicht weit verbreitet und muss auf Recheneffizienz getrimmt werden. Wie schneidet also ein derartig neues Protokoll im Vergleich zum altehrwürdigen TCP ab? Und vor allem: Kann QUIC in naher Zukunft so effizient sein wie TCP?

Wir sind bei den Compute-Kosten für QUIC von zwei wesentlichen Quellen ausgegangen:

  • Verarbeitung von Bestätigungen: Ein großer Teil der Pakete in einer typischen TCP-Verbindung enthält nur Bestätigungen. TCP-Bestätigungen werden im Kernel verarbeitet, sowohl beim Sender als auch beim Empfänger. QUIC verarbeitet diese im User Space, was zu mehr Datenkopien über die User-Kernel-Grenze hinweg und zu mehr Kontextwechseln führt. Außerdem werden TCP-Bestätigungen im Klartext übermittelt, während QUIC-Bestätigungen verschlüsselt sind, was die Kosten für das Senden und Empfangen von Bestätigungen in QUIC erhöht.

  • Kostenaufwand pro Paket für den Sender: Der Kernel weiß über TCP-Verbindungen Bescheid und kann einen Status speichern und wiederverwenden, der für alle in einer Verbindung gesendeten Pakete unverändert bleiben soll. So muss der Kernel beispielsweise nur einmal zu Beginn der Verbindung die Route für die Zieladresse nachschlagen oder Firewall-Regeln anwenden. Weil der Kernel keinen Verbindungsstatus für QUIC-Verbindungen hat, werden diese Kernel-Operations bei jedem ausgehenden QUIC-Paket durchgeführt.

Da QUIC im User Space ausgeführt wird, sind diese Kosten bei QUIC höher als bei TCP. Das liegt daran, dass jedes Paket, das von QUIC gesendet oder empfangen wird, die Grenze zwischen User und Kernel überschreitet, was als Kontextwechsel bezeichnet wird.

Das Experiment und der erste Eindruck

Um die eingangs gestellten Fragen beantworten zu können, haben wir uns für eine einfache Benchmark entschieden. Wir haben quicly undefinedals QUIC-Server und als QUIC-Client verwendet. QUIC-Pakete werden immer mit TLS 1.3 verschlüsselt, und quicly undefinedverwendet zu diesem Zweck picotls, die TLS-Bibliothek von H2O. Unser Referenz-TCP-Setup würde picotls undefinedmit nativem Linux-TCP verwenden, um die Unterschiede zwischen dem Referenz-TCP-Setup und dem QUIC-Setup zu minimieren.

Die Recheneffizienz kann auf zwei Arten gemessen werden: durch Messung der Menge an Rechenressourcen, die zur Sicherung eines Netzwerks erforderlich sind, oder durch Messung des mit der gesamten verfügbaren Rechenleistung erzielbaren Durchsatzes. Die Sättigung des Netzwerks führt zu Schwankungen aufgrund von Paketverlusten und der anschließenden Wiederherstellung von Verlusten und Staukontrollmaßnahmen. Obwohl es wichtig ist, diese bei der Messung der Performance zu berücksichtigen, wollten wir diese Schwankungen vermeiden und haben uns daher für die letztere Methode entschieden. Wir haben die Recheneffizienz als den Durchsatz gemessen, den ein Sender in einem ansonsten ungenutzten Netzwerk mit hoher Bandbreite mit all seiner Rechenleistung aufrechterhalten könnte.

Die Recheneffizienz des Servers, von dem die Daten gesendet werden, ist aus zwei weiteren Gründen wichtig. Erstens tragen die Sender in der Regel die Hauptlast der Compute-Kosten in Transportprotokollen. Dies liegt daran, dass der Sender für die meisten rechenintensiven Transportfunktionen verantwortlich ist, wie z. B. das Ausführen von Timern zur Erkennung von im Netzwerk verloren gegangenen Paketen und deren erneute Übertragung, die Überwachung der Round-Trip-Zeit des Netzwerks und das Ausführen von Bandbreitenschätzern, damit das Netzwerk nicht überlastet wird. Zweitens sind Server in der Regel Sender, und die Verbesserung der Recheneffizienz der Protokollverarbeitung ist bei Servern von größerer Bedeutung. (Das bedeutet nicht, dass die Recheneffizienz bei Clients nicht wichtig ist, aber die Protokollverarbeitung ist in der Regel nicht ihr primärer Engpass).

Zunächst ein paar Worte zu unserem Versuchsaufbau, bevor wir uns den Ergebnissen zuwenden. Unser Sender verwendete Ubuntu 19.10 (Linux Kernel Version 5.3.0) auf einem Intel Core m3-6Y30 Prozessor, der auf einen Kern und einen Thread begrenzt war. Der Sender war über einen USB-Gigabit-Ethernet-Adapter, der den ASIX AX88179 Controller verwendet, mit dem lokalen Netzwerk verbunden. Prüfsummen-Offloading war sowohl für TCP als auch für UDP aktiviert. Andere Hardware-Optimierungen für TCP, wie z. B. TCP Segmentation Offload (TSO), wurden nicht verwendet; wir planen, diese in zukünftigen Vergleichen zusammen mit ähnlichen Hardware-Optimierungen für UDP zu verwenden. Der Empfänger war ein Quad-Core MacBook Pro mit 2,5 GHz, das über einen handelsüblichen Gigabit-Ethernet-Switch mit dem Sender verbunden war. Um eine Sättigung des Netzwerks durch den Sender zu vermeiden, wenn er seine gesamte Rechenleistung einsetzt, haben wir die Taktfrequenz der CPU von 2,2 GHz auf 400 MHz begrenzt. Wir haben uns vergewissert, dass es bei der Durchführung der Experimente keine Verluste im Netzwerk gab.

Wir verwenden hier eine relativ einfache Hardware für unseren Sender. Und das ist auch gut so. Wie bereits erwähnt, kümmern wir uns in diesem ersten Schritt um die Maßnahmen, die wir ergreifen würden, um die auftretenden Engpässe zu beseitigen, und um deren Übertragbarkeit auf andere Umgebungen. In unseren weiteren Schritten werden wir uns mit Server-Hardware beschäftigen.

Als erste Referenz-Benchmark haben wir mit iperf den maximal erreichbaren unverschlüsselten TCP-Durchsatz gemessen. Dieser betrug 708 Mbit/s.

Als zweite Referenz-Benchmark haben wir den nachhaltigen Durchsatz gemessen, der mit TLS 1.3 über TCP unter Verwendung von picotls undefinedmit AES128-GCM als Verschlüsselung erreicht werden kann. Dieser Durchsatz betrug 466 Mbit/s – etwa 66 % dessen, was wir mit unverschlüsseltem TCP gemessen haben. Diese Verringerung der Performance beruht auf den Kryptographiekosten, der Verwendung eines nicht blockierenden Sockets und den Kosten für die Unterbrechung der Ausführung im User Space bei der Verarbeitung eingehender Bestätigungen. Wichtig ist jedoch, dass sich dieser Kostenaufwand nicht wirklich auf die Frage auswirkt, die wir hier untersuchen, da diese Kosten sowohl für TLS über TCP als auch für QUIC gelten.

Schließlich haben wir den nachhaltigen Durchsatz mit handelsüblichem quicly gemessen. Dieser Durchsatz betrug 196 Mbit/s. QUIC war in der Lage, etwa 40 % von TLS 1.3 über TCP zu erreichen und verursachte dabei die erwarteten Kosten, die zunächst ernüchternd ausfielen.

Wir wollten nicht nur die Betriebskosten von QUIC messen, sondern auch sehen, was wir tun können, um diese Kosten zu senken. Außerdem wollten wir uns nicht nur auf unsere Implementierung beschränken, sondern auch Änderungen oder Optimierungen des Protokolls berücksichtigen. Unser Vorgehen bestand aus drei Schritten, die wir im Folgenden näher erläutern werden.

Verringerung der Bestätigungshäufigkeit

Wie bei TCP empfiehlt die QUIC-Spezifikation, dass ein Empfänger für jeweils zwei empfangene Pakete eine Bestätigung sendet. Dies ist zwar eine vernünftige Vorgabe, aber das Empfangen und Verarbeiten von Bestätigungen ist für einen Datensender mit Compute-Kosten verbunden. Ein Empfänger könnte einfach weniger Bestätigungen senden, das kann allerdings den Durchsatz der Verbindung verringern, insbesondere zu Beginn.

Einfacher ausgedrückt, können Sie sich vorstellen, dass jede Bestätigung des Senders es ihm ermöglicht, seine Übertragungsrate zu erhöhen. Je eher er eine Bestätigung erhält, desto eher erhöht er seine Senderate. Wenn diese Rate niedrig ist, gehen pro Round Trip weniger Pakete an den Empfänger und daher kommen auch weniger Bestätigungen von ihm zurück. Eine Verringerung der Anzahl der Bestätigungen bei einer solchen Verbindung kann die Senderate und die gesamte Performance des Senders messbar verringern. 

Bei einer Verbindung mit hohem Durchsatz könnte eine geringere Anzahl der Bestätigungen dennoch bedeuten, dass genug davon zurückkommen, um die Rate des Senders nicht messbar zu beeinflussen. (Dies ist eine Vereinfachung, und die anderen Überlegungen werden hier etwas ausführlicher erläutert). In unserem Testaufbau können wir die Häufigkeit der Bestätigungen verringern, ohne dass sich dies auf den Durchsatz des Senders auswirkt.

 Wir haben die Häufigkeit der Bestätigungen von einmal alle zwei Pakete auf einmal alle zehn Pakete reduziert, wodurch quicly undefinedeinen Durchsatz von 240 Mbit/s aufrechterhalten konnte. Dies kommt dem Netzwerk zugute, da die Anzahl der Bestätigungspakete reduziert wird. Das Experiment beweist, dass auch der Sender davon profitiert, da sich der Compute-Kostenaufwand verringert. Dieses Ergebnis hat uns davon überzeugt, die vorgeschlagene QUIC Delayed ACK-Erweiterung zu implementieren – mehr dazu später.

Zusammenführen von Paketen mit Generic Segmentation Offload (GSO)

Dem Rat von Willem de Bruijn und Eric Dumazet in „Optimizing UDP for content delivery: GSO, pacing and zerocopy“ folgend, haben wir als Nächstes untersucht, ob UDP Generic Segmentation Offload dazu beitragen kann, den Kostenaufwand zu verringern, der durch das Schreiben einzelner Pakete und den Kontextwechsel pro Paket entsteht. Generic Segmentation Offload (GSO) ist eine Linux Funktion, die es Nutzern ermöglicht, dem Kernel eine Reihe von Paketen als eine einzige Einheit zur Verfügung zu stellen. Diese Einheit wird vom Kernel als ein großes virtuelles Paket weitergeleitet. So muss der Kernel weniger Entscheidungen über große virtuelle Pakete treffen als bei kleinen Paketen. Im Gegensatz zum TCP Segmentation Offload ist GSO in Linux nicht unbedingt ein Hardware-Offload. Wenn die Hardware keine Segmentation Offloads von UDP-Paketen unterstützt, wird das große virtuelle Paket in mehrere kleine UDP-Pakete aufgeteilt, wenn es den Netzwerkkartentreiber erreicht. Das war bei diesem Experiment der Fall.

Durch das Zusammenfassen von maximal zehn UDP-Pakete zu einem Objekt und Senden mit GSO, stieg der Durchsatz von QUIC von 240 Mbit/s auf 348 Mbit/s – eine Steigerung um 45 %! Um zu sehen, ob die Performance durch die Zusammenführung noch weiter gesteigert werden kann, haben wir versucht, bis zu 20 UDP-Pakete mit GSO zusammenzufassen. Dies führte zu einer weiteren Steigerung des Durchsatzes um 45 %, und QUIC erreichte nun eine Geschwindigkeit von 431 Mbit/s.

Das war enorm. Die Kosten pro Paket von QUIC waren eindeutig ein signifikanter Engpass und die Lösung dieses Problems mit GSO eine enorme Hilfe. Wir mussten eine GSO-Größe wählen, auf die wir später genauer eingehen werden. Dann haben wir uns einem anderen Parameter zugewandt, dem nicht so viel Aufmerksamkeit geschenkt wird, wie er eigentlich verdient, nämlich der Paketgröße.

Erhöhung der Paketgröße

Die QUIC-Spezifikation empfiehlt eine konservative standardmäßige Mindestgröße für QUIC-Pakete von 1.200 Byte, und quicly undefinedverwendete 1.280 Byte. Die Implementierungen dürfen die Paketgröße erhöhen, wenn sie Grund zu der Annahme haben, dass der Pfad größere Pakete unterstützen könnte. Da der Pfad in der Lage war, 1.472 Byte große QUIC-Pakete zu unterstützen, und TCP auf diesem Pfad 1.460 Byte große Pakete verwendete, war es sinnvoll, dass auch QUIC größere Pakete verwendete. Die Erhöhung dieser maximalen Paketgröße führt zu einer Senkung der Compute-Kosten, da die Anzahl der Pakete, die für die Übertragung einer bestimmten Datenmenge erforderlich sind, verringert wird. Dadurch steigt die Recheneffizienz sowohl beim Sender als auch beim Empfänger, da auf beiden Seiten feste Verarbeitungskosten pro Paket anfallen.

Also änderten wir die QUIC-Paketgröße von 1.280 Byte auf 1.460 Byte, um eine Parität mit der TCP-Payload-Größe zu erreichen. Mit dieser Änderung war quicly undefinedin der Lage, einen Durchsatz von 466 Mbit/s aufrechtzuerhalten – eine Steigerung um 8 %.

QUIC war jetzt schneller als TLS über TCP!

Anwendung der Ergebnisse in der Praxis

Dieses Experiment zeigt einen klaren Weg auf, wie die Effizienz von quicly verbessert werden kann: Reduzierung der Bestätigungshäufigkeit, Zusammenführung von Paketen mit GSO und Verwendung möglichst großer Pakete. Überlegen wir nun, wie wir diese Optimierungen verallgemeinern und so anpassen können, dass sie in den unterschiedlichen Umgebungen gut funktionieren, um so das Risiko von Nebeneffekten zu minimieren.

Verringerung der Bestätigungshäufigkeit

Die Reduzierung der Bestätigungshäufigkeit auf eine feste Rate von einmal pro zehn Pakete ist mit einer Reihe von Problemen verbunden. Erstens kann dies, wie oben erwähnt, den Durchsatz messbar beeinträchtigen, wenn dieser bei der Verbindung von vornherein niedrig ist. Zweitens ist der Client in diesen Experimenten quicly, während der Client in der Produktivumgebung ein Browser sein wird, den wir nicht kontrollieren können. 

Die Lösung für diese beiden Probleme ist die Delayed ACK-Erweiterung für QUIC, mit der ein Sender die Bestätigungshäufigkeit des Empfängers auf Grundlage seiner aktuellen Senderate dynamisch steuern kann.

Seit der Durchführung dieses Experiments haben wir die Delayed ACK-Erweiterung implementiert, wobei der Sender die Bestätigungshäufigkeit so steuert, dass sie einmal pro Achtel seines Überlastungsfensters erfolgt, also etwa einmal pro Achtel seiner Round-Trip-Zeit. In unserem obigen Versuchsaufbau reduziert dies die Anzahl der Bestätigungen auf einmal alle sechzig Pakete, was eine deutlich größere Reduzierung als in unserem Experiment darstellt.

Zusammenführen von Paketen mit GSO

Das GSO-Experiment hat gezeigt, dass das Zusammenführen von mehr Paketen QUIC effizienter macht. GSO hat jedoch seinen Preis. Das Zusammenführen mit GSO bedeutet, dass der Sender all diese Pakete in das Netzwerk schickt, was zu einem erhöhten kurzfristigen Druck auf die Puffer des Netzwerks und zu einer erhöhten Wahrscheinlichkeit von Paketverlusten führt.

Wie viele Pakete sollte quicly undefinedalso angesichts dieses Kompromisses zusammenfassen? Die angegebene akzeptable Burst-Größe für einen QUIC-Sender ist zehn – eine Empfehlung, die sich in unserem Fall bewährt hat. undefinedquicly hat jetzt eine Option implementiert, um GSO-Bursts von zehn Paketen zu senden.

Wir stellen fest, dass der Kernel eines Senders derzeit nicht in der Lage ist, diese Pakete zu beschleunigen, also die einzelnen Pakete eines GSO-Bursts mit einer Rate zu senden, die verhindert, dass das Netzwerk den Burst absorbieren muss. Wir wären daran interessiert, eine solche Möglichkeit zu nutzen, wenn sie im Linux Kernel implementiert würde.

Auswahl der Paketgröße

Je größer die Pakete sind, desto besser ist die Performance. Allerdings besteht bei größeren Paketen die Gefahr, dass sie auf einigen Netzwerkpfaden verloren gehen. Wir könnten Mechanismen wie Path MTU Discovery implementieren, um die größte Paketgröße zu ermitteln, die für eine Verbindung verwendet werden kann, aber das ist praktisch nur für langlebige Verbindungen sinnvoll. Für die meisten Verbindungen und zu Beginn aller Verbindungen muss ein Sender eine gute Paketgröße ermitteln.

Anstatt eine feste Paketgröße zu verwenden, bestimmt der Server von quicly undefinednun seine eigene Paketgröße auf Grundlage der Größe der ersten Pakete, die er vom Client erhält. Da diese Pakete das Netzwerk bis zum Server erfolgreich durchquert haben, kann man davon ausgehen, dass Pakete derselben Größe eine gute Chance haben, das Netzwerk zurück zum Client zu durchqueren.

Fazit und nächste Schritte

Mit diesen Änderungen erreicht quicly jetzt 464 Mbit/s (1 % schneller als TLS 1.3 über TCP), wenn die ersten vom Client gesendeten QUIC-Pakete 1.460 Bytes groß sind, und 425 Mbit/s (nur 8 % langsamer als TLS 1.3 über TCP), wenn die ersten vom Client gesendeten QUIC-Pakete 1.350 Bytes groß sind – die von Chrome verwendete standardmäßige Paketgröße.

Die Workload und Umgebung in diesem Experiment sind nicht repräsentativ für die Gesamtheit der möglichen Workloads und Umgebungen. Unser Ziel war es, zu sehen, ob QUIC den TCP-Durchsatz in dieser sehr spezifischen Mikrobenchmark erreichen könnte. Mit weiteren Tests und Experimenten werden wir die Performance von QUIC in anderen repräsentativen Umgebungen und Szenarien untersuchen und verbessern.

Dieses Experiment zeigt uns jedoch, dass QUIC bei vernünftigem Einsatz von Systemoptimierungen und Protokollmechanismen eine recht gute Chance hat, genauso recheneffizient zu sein wie TCP.

Wir werden die Performance unserer Implementierung in weiteren Tests und Bereitstellungen optimieren und halten Sie hier gerne auf dem Laufenden. Bleiben Sie dran!