Hallo,
Das Problem wurde durch den Commit ce3dda9a1747cc6a4c044eafe5a2eb653c974919 zwischen v2.3.3 und v2.4.0 eingeführt. Betrachten Sie das folgende Beispiel: foo.c.zip .
Es fügt eine sehr große Anzahl von Regeln hinzu. Und arbeitet nach dem oben genannten Commit rund 100-mal langsamer.
foo.c
Ausführungszeit mit v2.4.1: 0.448
foo.c
Ausführungszeit mit v2.3.3: 0.077
Ich habe ein wenig gegraben und herausgefunden, dass db_col_transaction_start() bereits vorhandene Filtersammlungen kopiert und arch_filter_rule_add() verwendet, um Filterregeln zu duplizieren. Aber arch_filter_rule_add() ruft arch_syscall_translate() auf, das arch_syscall_resolve_name() aufruft, was in O (Anzahl der Systemaufrufe auf der angegebenen Architektur) funktioniert. Das Hinzufügen einer Regel funktioniert also zumindest in O (Anzahl der bereits hinzugefügten Regeln * Anzahl der Syscalls auf verwendeten Architekturen), was meiner Meinung nach wirklich schlecht ist.
Ich habe die Anzahl der Aufrufe von arch_filter_rule_add() im obigen Beispiel gezählt und es ist gleich 201152
.
Vor diesem Commit betrug die Anzahl der Aufrufe von arch_filter_rule_add() 896
. Und nach dem, was ich aus dem Code verstehe, kopiert auch db_col_transaction_start() bereits vorhandene Filtersammlungen und verwendet arch_filter_rule_add() nicht. Was uns eine Schätzung gibt: Zeit zum Hinzufügen einer Regel um O (Anzahl bereits hinzugefügter Regeln + Anzahl Systemaufrufe auf der gegebenen Architektur), was viel besser ist.
Allerdings sollte es IMO nicht mit der Anzahl der bereits hinzugefügten Regeln zusammenhängen, da das Hinzufügen von n Regeln dann in O (n ^ 2) funktioniert. Aber das ist ein Thema für eine andere Diskussion, also sollte es kein Problem für kleine Filter oder selten erzeugte Filter sein.
Warum ist dieses Problem wichtig?
Einige Filter benötigen ausführende Programme PID (z. B. dem Thread erlauben, Signale nur an sich selbst zu senden). Wenn also das eingeschränkte Programm eine beträchtliche Anzahl von Malen ausgeführt werden muss, wird es zu einem sehr sichtbaren Overhead. Ich habe einen Filter mit ungefähr 300 Regeln und der Overhead von libseccomp beträgt ungefähr 0,16 s pro Ausführung des Sandbox-Prozesses (ich führe den Prozess dutzende Male aus).
Vielen Dank im Voraus für Ihre Hilfe!
Hallo @varqox.
Ja, die Syscall-Resolver-Funktionen könnten einige Verbesserungen gebrauchen. Wenn Sie sich den Code ansehen, werden Sie tatsächlich mehrere Kommentare wie die folgenden sehen:
/* XXX - plenty of room for future improvement here */
Wenn Sie diesen Code verbessern möchten, könnten wir die Hilfe gebrauchen!
Wie @pcmoore erwähnte, gibt es zahlreiche Möglichkeiten, die _Erstellung_ eines seccomp-Filters mit libseccomp zu beschleunigen. Ihre obige Recherche hat einen von mehreren Bereichen beschrieben, die verbessert werden könnten. Dies war für meine Benutzer kein Problem, daher habe ich mich nicht darauf konzentriert.
Bezüglich der _Laufzeit_-Leistung arbeite ich derzeit daran, einen Binärbaum für große Filter zu verwenden, wie den, den Sie in foo.c bereitgestellt haben. Die ersten Ergebnisse mit meinen internen Kunden sehen vielversprechend aus, aber ich würde gerne ein anderes Auge auf die Änderungen werfen. Siehe Pull-Request https://github.com/seccomp/libseccomp/pull/152
OK, ich sehe, dass die Systemaufrufauflösung verbessert werden könnte, aber das ist nicht die Hauptursache des Problems. Das ist, wie ich es sehe, die Erstellung eines Snapshots in db_col_transaction_start() . Dort wird arch_filter_rule_add() aufgerufen, was langsam ist, da der Syscall aufgelöst wird, der in der ursprünglichen Regel aufgelöst wird.
Ich sehe das so: Wir wollen den gesamten Satz aktueller Filter (auch bekannt als struct db_filter) mit all ihren Regeln duplizieren, also _konstruieren_ wir alle Filter von Grund auf neu, anstatt Vorteile aus dem zu ziehen, was wir bereits haben, und _kopieren_ einfach alle Filter. Wir müssen nicht von Grund auf neu bauen, wir haben einen vollständig gebauten Filter, von dem wir nur eine Kopie wollen. Vielleicht habe ich etwas übersehen, aber es sieht so aus, als ob eine Menge Verbesserungen an der Funktion db_col_transaction_start() vorgenommen werden könnten.
Mit dem gesamten Status in der internen libseccomp-DB-Sammlung ist das Duplizieren keine triviale Aufgabe, das Regenerieren der Sammlung aus den ursprünglichen Regeln ist viel einfacher (aus Code-Perspektive). Die Verfolgung der ursprünglichen Regeln ermöglicht es uns auch, die Möglichkeit anzubieten, eine bestehende Regel zu "entfernen" (mögliche zukünftige Funktion).
Das soll nicht heißen, dass der Transaktionscode nicht verbessert werden könnte – das ist definitiv möglich –, aber der aktuelle Code ist aus einem bestimmten Grund so, wie er ist, hauptsächlich aus Gründen der Einfachheit.
Aufgrund dieser Änderung sehen wir Zeitüberschreitungen von Benutzern. Es hat die Dinge wirklich um eine Größenordnung verlangsamt.
Ein weiterer Gedanke, wir können dies wahrscheinlich so ändern, dass wir die Regeln nur bei einem Transaktionsstart duplizieren, nicht den gesamten Baum, und den Baum nur bei einer fehlgeschlagenen Transaktion neu erstellen. Es ist nicht perfekt, aber das sollte einen guten Teil der Zeit zurückgewinnen.
Wir müssen etwas unternehmen, da die Startzeiten für Container und Exec-Prozesse einen enormen Leistungsrückgang erfahren und dazu führen, dass die Leute auf 2,3x pinnen
Ich werde die _"große"_ Natur des Problems nicht weiter kommentieren, diese Perspektive wurde bereits mehrfach gemacht und ich betrachte sie sowohl als relativ als auch als abhängig vom Anwendungsfall. Ich möchte jedoch alle daran erinnern, dass libseccomp-Releases vor v2.4 anfällig für eine potenzielle Schwachstelle sind, die öffentlich gemacht wurde (Problem Nr. 139).
Für diejenigen, die sich Sorgen um dieses Problem machen, es ist derzeit für eine Version 2.5 gekennzeichnet.
Sie haben ein Refactoring durchgeführt und es hat "enorme" Auswirkungen auf die Leistung in einer Nebenversion, und Sie sind nicht hilfreich, indem Sie dies wegblasen und sagen, dass es vom Anwendungsfall abhängt. Bitte nehmen Sie dies ernst, da die Leute bemerken werden, wie die Distributionen auf 2.4 aktualisiert werden
@crosbymichael die Änderung war nicht einfach ein Refactoring, es war notwendig, Probleme zu beheben und die Änderungen im Kernel zu unterstützen (insbesondere die Notwendigkeit, sowohl Multiplex- als auch Direktaufruf-Systemaufrufe zu unterstützen, z. B. Socket-Systemaufrufe auf 32-Bit-x86).
Ich blase das _nicht_ weg, ich habe weiter über Möglichkeiten nachgedacht, dieses Problem zu lösen (siehe meine Kommentare oben), und die Tatsache, dass ich dies als etwas für die nächste Nebenversion markiert habe. An diesem Punkt fällt es mir schwer, Ihre Kommentare nicht als aufrührerisch zu empfinden. Wenn dies nicht Ihre Absicht ist, schlage ich vor, in Zukunft bei Kommentaren vorsichtiger zu sein. Wenn Sie mit dem Fortschritt bei diesem Problem unzufrieden sind, können Sie jederzeit helfen, indem Sie einen Patch/eine PR zur Überprüfung einreichen.
Notiz an mich selbst und alle anderen, die darüber nachdenken, dieses Problem zu lösen ...
Ich wurde kürzlich daran erinnert, warum wir in Bezug auf Transaktionen tun, was wir tun (alles im Voraus kopieren); Wir tun dies, weil wir in der Lage sein müssen, eine Transaktion ohne Fehler rückgängig zu machen. Wieso den?
Eine normale Operation seccomp_rule_add() muss den Filter auch im Fehlerfall intakt halten; Wenn wir eine mehrteilige Transaktion (z. B. Socket/IPC-Systemaufrufe auf x86/s390/s390x/etc.) als Teil einer normalen Regelhinzufügung scheitern, MÜSSEN wir in der Lage sein, zu Beginn der Transaktion ohne Fehler zum Filter zurückzukehren ( ungeachtet des Speicherdrucks usw.).
Das Duplizieren des Baums ohne die Regeln wird aufgrund der Art des Baums und der Verknüpfung innerhalb des Baums weiterhin eine Herausforderung sein, aber wir können möglicherweise selektiv auswählen, wann wir eine interne Transaktion erstellen müssen, und sie für die vielen überspringen Fälle, in denen es nicht benötigt wird.
Ich habe mir das etwas länger angesehen, und aufgrund der Art und Weise, wie wir den Entscheidungsbaum während einer Regelhinzufügung destruktiv ändern, bin ich mir nicht sicher, ob wir es vermeiden können, Regelhinzufügungen mit einer Transaktion zu verpacken. Das heißt, anstatt Wege zu finden, unsere Verwendung von Transaktionen intern einzuschränken, müssen wir einen Weg finden, sie zu beschleunigen, zum Glück denke ich, dass ich eine Lösung gefunden habe: Schattenbäume.
Derzeit erstellen wir jedes Mal, wenn wir eine neue Transaktion erstellen, einen neuen Baum und verwerfen ihn bei Erfolg, was, wie wir gesehen haben, in einigen Anwendungsfällen unerschwinglich langsam sein kann. Mein Gedanke ist, dass wir, anstatt den doppelten Baum beim Festschreiben zu verwerfen, versuchen, die gerade hinzugefügte Regel dem doppelten Baum hinzuzufügen (wodurch sie eine Kopie des aktuellen Filters wird) und sie als "Schattentransaktion" zu behalten, um die nächste zu beschleunigen Momentaufnahme der Transaktion. Einige Notizen:
db_col_transaction_start()
sollte versuchen, die Schattentransaktion zu verwenden, falls vorhanden, aber wenn nicht, sollte es auf das aktuelle Verhalten zurückgreifen.db_col_transaction_abort()
sollte sich genauso verhalten wie jetzt; Dies bedeutet, dass eine fehlgeschlagene Transaktion die Schattentransaktion löscht (es muss ein Baum erstellt werden, um den Filter wiederherzustellen), aber die nächste erfolgreiche Transaktion wird den Schatten wiederherstellen. Eine fehlgeschlagene Transaktion sollte so selten sein, dass dies kein großes Problem darstellt.Ich hatte heute Abend nach dem Abendessen etwas Zeit, also habe ich die obige Schattentransaktionsidee schnell umgesetzt. Der Code ist immer noch grob und meine Tests (unten) noch grober, aber es sieht so aus, als würden wir mit diesem Ansatz einige Leistungssteigerungen sehen:
# time for i in {0..20000}; do /bin/true; done
real 0m10.479s
user 0m7.641s
sys 0m3.924s
# time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done
real 0m16.303s
user 0m12.584s
sys 0m4.501s
time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done
real 0m15.021s
user 0m11.540s
sys 0m4.387s
Wenn wir den Test-Overhead abziehen, sehen wir eine Leistungssteigerung von etwa 20 % bei diesem "Test", aber ich erwarte, dass der Nutzen für komplexe Filtersätze besser (viel besser?) Als dieser ist.
@varqox und/oder @crosbymichael , sobald ich die Patches ein wenig aufgeräumt und eine PR erstellt habe, könnten Sie dies in Ihrer Umgebung testen?
Mein Beispiel-Testfall ist bereits hier:
Hallo,
Das Problem wurde durch den Commit ce3dda9 zwischen v2.3.3 und v2.4.0 eingeführt. Betrachten Sie das folgende Beispiel: foo.c.zip .
Es fügt eine sehr große Anzahl von Regeln hinzu. Und arbeitet nach dem oben genannten Commit rund 100-mal langsamer.
foo.c
Ausführungszeit mit v2.4.1:0.448
foo.c
Ausführungszeit mit v2.3.3:0.077
Aber sobald die PR fertig ist, kann ich sie in meiner Umgebung testen.
Hallo @varqox , ja, ich habe gesehen, dass Sie einen Testfall in den ursprünglichen Bericht aufgenommen haben, aber ich bin mehr daran interessiert zu hören, wie er sich im realen Gebrauch verhält. Wenn Sie PR #180 ausprobieren und berichten könnten, wäre ich Ihnen sehr dankbar - danke!
Hallo @pcmoore ,
Danke für diese PR.
Ich habe Ihre PR #180 gebaut und getestet, das Ergebnis ist für meinen Testfall vielversprechend. Ich beobachte dieses Problem, weil Kunden die Docker-Gesundheitsprüfung verwenden und unter dem Leistungsproblem in libseccomp 2.4.x
litten.
In meinem Testfall ist die Leistung dieses PR vergleichbar mit libseccomp 2.3.3
. Details sind wie folgt:
Ubuntu 19.04 VM (2 CPU, 2 GB Speicher) auf MacBook Pro (15 Zoll, Mitte 2015)
Kernel 5.0.0-32-generisch
Docker CE 19.03.2
Bereiten Sie 20 Behälter vor:
for i in $(seq 1 20)
do
docker run -d --name bb$i busybox sleep 3d
done
Führen Sie den Test durch, indem Sie docker exec
gleichzeitig auf alle Container feuern
for i in $(seq 1 20)
do
/usr/bin/time -f "%E real" docker exec bb$i true &
done
0:01.05 real
0:01.12 real
0:01.16 real
0:01.20 real
0:01.23 real
0:01.27 real
0:01.31 real
0:01.35 real
0:01.37 real
0:01.38 real
0:01.40 real
0:01.41 real
0:01.40 real
0:01.40 real
0:01.45 real
0:01.46 real
0:01.47 real
0:01.48 real
0:01.48 real
0:01.49 real
0:00.98 real
0:01.63 real
0:01.67 real
0:01.95 real
0:02.55 real
0:02.70 real
0:02.70 real
0:02.96 real
0:03.04 real
0:03.16 real
0:03.17 real
0:03.21 real
0:03.23 real
0:03.27 real
0:03.24 real
0:03.29 real
0:03.27 real
0:03.29 real
0:03.28 real
0:03.27 real
0:00.95 real
0:01.12 real
0:01.20 real
0:01.23 real
0:01.28 real
0:01.29 real
0:01.31 real
0:01.37 real
0:01.38 real
0:01.40 real
0:01.43 real
0:01.43 real
0:01.44 real
0:01.45 real
0:01.42 real
0:01.47 real
0:01.48 real
0:01.48 real
0:01.48 real
0:01.50 real
2.4.1
in AC_INIT
in configure.ac
gesetzt, bevor ich diesen PR erstellt habe./usr/local/lib
installiert. Ich habe ihn überprüft, indem ich ldd /usr/bin/runc
ausgeführt habe, um sicherzustellen, dass der benutzerdefinierte Build während des Tests verwendet wird.Das ist großartig, danke für die Hilfe @xinfengliu!
Hallo @pcmoore ,
Danke für diese PN.
In meinem Fall stellt es die Leistung von libseccomp auf ein Niveau wieder her, das mit v2.3.3 vergleichbar ist.
g++ foo.c -lseccomp -o foo -O3
for ((i=0; i<10; ++i)); do time ./foo; done
./foo 0.01s user 0.00s system 98% cpu 0.018 total
./foo 0.02s user 0.00s system 98% cpu 0.020 total
./foo 0.02s user 0.00s system 98% cpu 0.019 total
./foo 0.02s user 0.00s system 98% cpu 0.018 total
./foo 0.02s user 0.00s system 98% cpu 0.019 total
./foo 0.02s user 0.00s system 98% cpu 0.019 total
./foo 0.02s user 0.00s system 98% cpu 0.019 total
./foo 0.02s user 0.00s system 98% cpu 0.019 total
./foo 0.02s user 0.00s system 98% cpu 0.018 total
./foo 0.02s user 0.00s system 98% cpu 0.019 total
Durchschnitt: 0.0188 s
./foo 0.19s user 0.00s system 99% cpu 0.195 total
./foo 0.19s user 0.00s system 99% cpu 0.194 total
./foo 0.19s user 0.00s system 99% cpu 0.193 total
./foo 0.19s user 0.00s system 99% cpu 0.196 total
./foo 0.19s user 0.00s system 99% cpu 0.195 total
./foo 0.20s user 0.00s system 99% cpu 0.196 total
./foo 0.19s user 0.00s system 99% cpu 0.194 total
./foo 0.20s user 0.00s system 99% cpu 0.197 total
./foo 0.19s user 0.00s system 99% cpu 0.195 total
./foo 0.19s user 0.00s system 99% cpu 0.194 total
Durchschnitt: 0.1949 s
./foo 0.01s user 0.01s system 98% cpu 0.012 total
./foo 0.01s user 0.00s system 97% cpu 0.013 total
./foo 0.01s user 0.00s system 96% cpu 0.013 total
./foo 0.01s user 0.01s system 97% cpu 0.014 total
./foo 0.01s user 0.00s system 97% cpu 0.012 total
./foo 0.01s user 0.00s system 98% cpu 0.013 total
./foo 0.01s user 0.00s system 98% cpu 0.012 total
./foo 0.01s user 0.00s system 98% cpu 0.013 total
./foo 0.01s user 0.00s system 97% cpu 0.013 total
./foo 0.01s user 0.00s system 97% cpu 0.011 total
Durchschnitt: 0.0126 s
Es scheint, als ob diese PR in diesem synthetischen Test eine gewisse Beschleunigung gegenüber v2.3.3 bringt.
Measured: 0.0052 s
Measured: 0.0040 s
Measured: 0.0046 s
Measured: 0.0042 s
Measured: 0.0038 s
Measured: 0.0038 s
Measured: 0.0039 s
Measured: 0.0036 s
Measured: 0.0042 s
Measured: 0.0044 s
Measured: 0.0036 s
Measured: 0.0037 s
Measured: 0.0044 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0040 s
Measured: 0.0037 s
Measured: 0.0043 s
Measured: 0.0042 s
Measured: 0.0035 s
Measured: 0.0034 s
Measured: 0.0038 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0037 s
Measured: 0.0038 s
Durchschnitt: 0.0039 s
Measured: 0.0496 s
Measured: 0.0480 s
Measured: 0.0474 s
Measured: 0.0475 s
Measured: 0.0479 s
Measured: 0.0479 s
Measured: 0.0492 s
Measured: 0.0485 s
Measured: 0.0491 s
Measured: 0.0490 s
Measured: 0.0484 s
Measured: 0.0483 s
Measured: 0.0480 s
Measured: 0.0482 s
Measured: 0.0474 s
Measured: 0.0483 s
Measured: 0.0507 s
Measured: 0.0472 s
Measured: 0.0482 s
Measured: 0.0471 s
Measured: 0.0498 s
Measured: 0.0489 s
Measured: 0.0474 s
Measured: 0.0494 s
Measured: 0.0483 s
Measured: 0.0498 s
Measured: 0.0492 s
Durchschnitt: 0.0466 s
Measured: 0.0058 s
Measured: 0.0059 s
Measured: 0.0054 s
Measured: 0.0046 s
Measured: 0.0059 s
Measured: 0.0048 s
Measured: 0.0045 s
Measured: 0.0051 s
Measured: 0.0052 s
Measured: 0.0053 s
Measured: 0.0048 s
Measured: 0.0048 s
Measured: 0.0045 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0059 s
Measured: 0.0044 s
Measured: 0.0046 s
Measured: 0.0046 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0062 s
Measured: 0.0047 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0044 s
Durchschnitt: 0.0049 s
Obwohl PR im synthetischen Test bessere Zeiten als v2.3.3 liefert, ist es in der realen Welt etwas langsamer (möglicherweise wegen komplizierterer Regeln und der Ausführung von seccomp_merge(), um zwei große Filter zusammenzuführen). Es bietet jedoch immer noch eine etwa zehnfache Beschleunigung gegenüber v2.4.2.
Vielen Dank für die Überprüfung der Leistung @varqox! Sobald @drakenclimber auf die letzte Runde der Kommentare antwortet (und ich alle verbleibenden Probleme behebe, die er möglicherweise anspricht), werden wir dies zusammenführen.
Ah, egal, mir ist gerade aufgefallen, dass @drakenclimber die PR als genehmigt markiert hat. Ich werde weitermachen und das jetzt zusammenführen.
Ich habe gerade PR Nr. 180 zusammengeführt, also denke ich, dass wir dies als geschlossen markieren können. Wenn jemand verbleibende Leistungsprobleme bemerkt, können Sie dies gerne kommentieren und / oder erneut öffnen. Danke an alle für eure Geduld und Hilfe!
@pcmoore planen Sie eine baldige Veröffentlichung mit diesen Änderungen?
Dies ist derzeit Teil des Meilensteins der Veröffentlichung von libseccomp v2.5. Sie können unseren Fortschritt in Richtung der Veröffentlichung von v2.5 über den folgenden Link verfolgen:
Hilfreichster Kommentar
Aufgrund dieser Änderung sehen wir Zeitüberschreitungen von Benutzern. Es hat die Dinge wirklich um eine Größenordnung verlangsamt.