
In der PHP-Ökosystem gibt es derzeit zwei Connectoren für die Arbeit mit dem Tarantool-Server – das offizielle PECL-Erweiterung , entwickelt in C, und , entwickelt in PHP. Ich bin der Autor letzterer.
In diesem Artikel möchte ich die Ergebnisse der Leistungstests beider Bibliotheken teilen und zeigen, wie man mit minimalen Änderungen im Code eine Steigerung der Leistung um 3-5 % erreichen kann (bei synthetischen Tests!).
Was werden wir testen?
Wir werden die oben genannten synchronen Connectoren testen, die asynchron, parallel und asynchron-parallel ausgeführt werden. 🙂 Außerdem wollen wir den Code der Connectoren selbst nicht anpassen. Derzeit gibt es mehrere Erweiterungen, die das gewünschte Ziel erreichen können:
- – ein hochleistungsfähiges asynchrones Framework für PHP. Es wird von Internet-Riesen wie Alibaba und Baidu verwendet. Seit Version 4.1.0 gibt es die magische Methode SwooleRuntime::enableCoroutine(), die es ermöglicht, "mit einer einzigen Codezeile synchronen PHP-Netzwerkbibliotheken in asynchrone umzuwandeln."
- Async ― bis vor kurzem ein vielversprechendes Erweiterung für die asynchrone Arbeit in PHP. Warum bis vor kurzem? Leider hat der Autor aus mir unbekannten Gründen das Repository gelöscht und die weitere Zukunft des Projekts ist unklar. Wir müssen auf der Forks zurückgreifen. Wie Swoole ermöglicht diese Erweiterung mit Leichtigkeit, die Standardimplementierung von TCP und TLS-Streams durch ihre asynchronen Versionen zu ersetzen und dadurch Asynchronität zu aktivieren. Dies geschieht über die Option „async.tcp = 1«.
- ― eine relativ neue Erweiterung des bekannten Joe Watkins, der Bibliotheken wie phpdbg, apcu, pthreads, pcov und uopz entwickelt hat. Die Erweiterung bietet ein API für Multithreading in PHP und wird als Ersatz für pthreads positioniert. Ein wesentliches Limit dieser Bibliothek ist, dass sie nur mit der ZTS (Zend Thread Safe) Version von PHP funktioniert.
Wie werden wir testen?
Wir starten eine Tarantool-Instanz ohne Journaling für die Vorspeicherung (wal_mode = none) und mit einem vergrößerten Netzwerkpuffer (readahead = 1 * 1024 * 1024). Die erste Option schließt die Arbeit mit der Festplatte aus, die zweite ermöglicht es, mehr Anfragen aus dem Puffer des Betriebssystems zu lesen und damit die Anzahl der Systemaufrufe zu minimieren.
Für Benchmarks, die mit Daten arbeiten (Einfügen, Löschen, Lesen usw.), wird vor dem Start des Benchmarks ein Memtx-Space (neu) erstellt, in dem die Werte des Primärindex von einem Generator sortierter Ganzzahlwerte (Sequenz) erstellt werden.
DDL-Space sieht folgendermaßen aus:
space = box.schema.space.create(config.space_name, {id = config.space_id, temporary = true})
space:create_index('primary', {type = 'tree', parts = {1, 'unsigned'}, sequence = true})
space:format({{name = 'id', type = 'unsigned'}, {name = 'name', type = 'string', is_nullable = false}})Nach Bedarf wird der Space vor dem Start des Benchmarks mit 10.000 Tupeln des Typs gefüllt
{id, "tuple_"}Der Zugriff auf die Tupel erfolgt über einen zufälligen Schlüsselwert.
Der Benchmark ist eine Einzelanfrage an den Server, die 10.000 Mal (Revolutions) ausgeführt wird, die wiederum in Iterationen durchgeführt werden. Die Iterationen werden wiederholt, bis alle Abweichungen in der Zeit zwischen 5 Iterationen innerhalb der zulässigen Toleranz von 3 % liegen*. Danach wird das Mittelwert-Ergebnis genommen. Zwischen den Iterationen gibt es eine Pause von 1 Sekunde, um zu verhindern, dass der Prozessor drosselt. Der Lua-Garbage-Collector wird vor jeder Iteration deaktiviert und nach Beendigung erzwungen. Der PHP-Prozess wird nur mit den für den Benchmark erforderlichen Erweiterungen gestartet, mit aktivierter Ausgabe-Pufferung und deaktiviertem Garbage Collector.
* Die Anzahl der Revolutions, Iterationen und der Toleranzschwelle kann in den Benchmark-Einstellungen geändert werden.
Testumgebung
Die unten veröffentlichten Ergebnisse wurden auf einem MacBook Pro (2015) mit dem Betriebssystem Fedora 30 (Kernelversion 5.3.8-200.fc30.x86_64) erzielt. Tarantool wurde in Docker mit der Option „--network host".
Paketversionen:
Tarantool: 2.3.0-115-g5ba5ed37e
Docker: 19.03.3, build a872fc2f86
PHP: 7.3.11 (cli) (gebaut: 22. Okt 2019 08:11:04)
tarantool/client: 0.6.0
rybakit/msgpack: 0.6.1
ext-tarantool: 0.3.2 (+ Patch für 7.3)*
ext-msgpack: 2.0.3
ext-async: 0.3.0-8c1da46
ext-swoole: 4.4.12
ext-parallel: 1.1.3
* Leider funktioniert der offizielle Connector nicht mit PHP-Versionen > 7.2. Um das Erweiterungspaket für PHP 7.3 zu kompilieren und auszuführen, musste ich auf .
Ergebnisse
Synchronmodus
Das Tarantool-Protokoll verwendet ein binäres Format zur Serialisierung von Nachrichten. In der PECL-Connector-Bibliothek ist die Serialisierung tief im Code verborgen, und es ist nicht möglich, den Kodierungsprozess aus dem Userland-Code Der rein PHP-basierte Connector hingegen bietet die Möglichkeit, den Kodierungsprozess mithilfe des Standardcodierers oder durch die Verwendung einer eigenen Implementierung anzupassen. Standardmäßig sind zwei Codierer verfügbar, einer basiert auf (das offizielle MessagePack PECL-Erweiterung), der andere auf (in reinem PHP).
Bevor wir die Connectoren vergleichen, messen wir die Leistung der MessagePack-Codierer für den PHP-Connector, und in weiteren Tests werden wir den verwenden, der die besten Ergebnisse zeigt:

Obwohl die PHP-Version (Pure) in der Geschwindigkeit hinter der PECL-Erweiterung zurückbleibt, würde ich in realen Projekten dennoch empfehlen, diese zu verwenden. , da im offiziellen MessagePack-Erweiterung die Formatsspezifikation nur teilweise umgesetzt ist (zum Beispiel keine Unterstützung für benutzerdefinierte Datentypen, ohne die Sie Decimal — einen neuen Datentyp, der in Tarantool 2.3 eingeführt wurde — nicht verwenden können) und es auch noch einige andere (einschließlich Kompatibilitätsprobleme mit PHP 7.4). Zudem wirkt das Projekt insgesamt verlassen.
Nun, messen wir die Leistung der Connectoren im synchronen Modus:

Wie aus dem Diagramm hervorgeht, zeigt der PECL-Connector (Tarantool) eine bessere Leistung im Vergleich zum PHP-Connector (Client). Das ist auch nicht überraschend, da letzterer nicht nur in einer langsameren Sprache implementiert ist, sondern auch insgesamt mehr Arbeit verrichtet: Bei jedem Aufruf wird ein neues Objekt erstellt Request und Response (im Falle von Select — auch Criteria, und im Falle von Update/Upsert — Operations), separate Entitäten Verbindung, Packer und Handler fügen ebenfalls Overhead hinzu. Offensichtlich muss man für Flexibilität bezahlen. Insgesamt zeigt der PHP-Interpreter jedoch eine gute Leistung, auch wenn es Unterschiede gibt, sind diese gering und könnten mit Preloading in PHP 7.4 noch geringer ausfallen, ganz zu schweigen von JIT in PHP 8.
Lass uns weitermachen. In Tarantool 2.0 gibt es nun Unterstützung für SQL. Lassen Sie uns die Operationen Select, Insert, Update und Delete mit dem SQL-Protokoll durchführen und die Ergebnisse mit den noSQL (binären) Äquivalenten vergleichen:

Die SQL-Ergebnisse sind nicht besonders beeindruckend (ich erinnere daran, dass wir immer noch den synchronen Modus testen). Allerdings würde ich mich nicht vorzeitig darüber ärgern, denn die SQL-Unterstützung befindet sich noch in aktiver Entwicklung (vor nicht allzu langer Zeit wurde zum Beispiel die Unterstützung für ) hinzugefügt, und laut der Liste , stehen dem SQL-Engine in Zukunft einige Optimierungen bevor.
Async
Schauen wir uns nun an, wie die Async-Erweiterung uns helfen kann, die oben genannten Ergebnisse zu verbessern. Für das Schreiben asynchroner Programme bietet die Erweiterung eine API, die auf Koroutinen basiert, und genau das werden wir nutzen. Durch Versuch und Irrtum stellen wir fest, dass die optimale Anzahl von Koroutinen für unsere Umgebung 25 beträgt:

Wir „verteilen“ 10.000 Operationen auf 25 Koroutinen und sehen, was dabei herauskommt:

Die Anzahl der Operationen pro Sekunde hat sich um mehr als das Dreifache erhöht für !
Es ist bedauerlich, aber der PECL-Connector ist mit ext-async nicht gestartet.
Und was ist mit SQL?

Wie Sie sehen, liegt im asynchronen Modus der Unterschied zwischen dem binären Protokoll und SQL im Bereich der Messgenauigkeit.
Swoole
Wir bestimmen erneut die optimale Anzahl an Koroutinen, diesmal für Swoole:

Wir entscheiden uns für 25. Wir wiederholen den gleichen Trick wie mit der Async-Erweiterung – wir verteilen 10.000 Operationen auf 25 Koroutinen. Zusätzlich fügen wir einen weiteren Test hinzu, bei dem die gesamte Arbeit auf 2 Prozesse aufgeteilt wird (das heißt, jeder Prozess wird 5.000 Operationen in 25 Koroutinen ausführen). Die Prozesse werden mithilfe von SwooleProcess.
Ergebnisse:

Swole zeigt ein etwas niedrigeres Ergebnis im Vergleich zu Async bei der Ausführung in einem Prozess, aber mit 2 Prozessen ändert sich das Bild drastisch (die Zahl 2 wurde nicht zufällig gewählt, auf meinem Rechner haben genau 2 Prozesse die besten Ergebnisse geliefert).
Übrigens bietet die Async-Erweiterung ebenfalls eine API zur Arbeit mit Prozessen, allerdings habe ich da keinen Unterschied bei der Ausführung von Benchmarks in einem oder mehreren Prozessen bemerkt (es ist möglich, dass ich irgendwo einen Fehler gemacht habe).
SQL vs. binäres Protokoll:

Wie bei Async wird der Unterschied zwischen binären und SQL-Operationen im asynchronen Modus nivelliert.
Parallel
Da die Parallel-Erweiterung nicht für Koroutinen, sondern für Threads gedacht ist, messen wir die optimale Anzahl paralleler Threads:

Diese beträgt 16 auf meiner Maschine. Lassen Sie uns die Benchmarks der Connectoren mit 16 parallelen Threads durchführen:

Wie Sie sehen, ist das Ergebnis sogar besser als bei den asynchronen Erweiterungen (außer Swoole, das auf 2 Prozessen läuft). Beachten Sie, dass beim PECL-Connector bei den Update- und Upsert-Operationen nichts steht. Das hängt damit zusammen, dass diese Operationen mit einem Fehler ausgefallen sind – ich weiß nicht, ob es am ext-parallel, ext-tarantool oder an beiden liegt.
Lassen Sie uns nun die SQL-Leistung vergleichen:

Haben Sie die Ähnlichkeit mit dem Diagramm für die synchron gestarteten Connectoren bemerkt?
Alles zusammen
Und schließlich fassen wir alle Ergebnisse in einem Diagramm zusammen, um das Gesamtbild der getesteten Erweiterungen zu sehen. Wir fügen nur einen neuen Test zum Diagramm hinzu, den wir noch nicht durchgeführt haben – wir werden die Async-Koroutinen parallel mit Parallel* starten. Die Idee, die oben genannten Erweiterungen zu integrieren, wurde bereits von den Autoren, jedoch wurde kein Konsens erzielt, sodass wir das selbst tun müssen.
* Die Koroutinen Swoole mit Parallel zu starten, war nicht möglich, anscheinend sind diese Erweiterungen inkompatibel.
Also, die finalen Ergebnisse:

Abschließend
Die Ergebnisse erscheinen mir sehr vielversprechend, und ich bin mir irgendwie sicher, dass dies noch nicht das Ende ist! Ob Sie das in Ihrem tatsächlichen Projekt benötigen, müssen Sie selbst entscheiden. Ich möchte nur sagen, dass es für mich ein spannendes Experiment war, bei dem ich herausfinden konnte, wie viel man aus einem synchronen TCP-Connector mit minimalem Aufwand „herausziehen“ kann. Wenn Sie Ideen zur Verbesserung der Benchmarks haben, schaue ich mir gerne Ihren Pull-Request an. Der gesamte Code mit Anweisungen zur Ausführung und den Ergebnissen ist in einem separaten .
Quelle: habr.com
