Libseccomp: RFE: seccomp_rule_add wurde seit v2.4.0 sehr langsam

Erstellt am 1. Mai 2019  ·  23Kommentare  ·  Quelle: seccomp/libseccomp

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!

enhancement prioritlow

Hilfreichster Kommentar

Aufgrund dieser Änderung sehen wir Zeitüberschreitungen von Benutzern. Es hat die Dinge wirklich um eine Größenordnung verlangsamt.

Alle 23 Kommentare

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.
  • Möglicherweise müssen wir die Schattentransaktion für andere Operationen löschen, z. B. arch/ABI ops?, aber das müssen wir überprüfen. Unabhängig davon sollte das Löschen der Schattentransaktion trivial sein.
  • Dies hat den Vorteil, dass nicht nur das Hinzufügen von Regeln beschleunigt wird, sondern Transaktionen im Allgemeinen beschleunigt werden. Dies ist jetzt vielleicht nicht von Bedeutung, aber es wird nützlich sein, wenn wir die Transaktionsfunktionalität den Benutzern zur Verfügung stellen (dies wäre notwendig, wenn wir jemals einen BSD-Mechanismus ähnlich einem "Versprechen" machen wollen).

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:

  • Basistest-Overhead
# time for i in {0..20000}; do /bin/true; done
real    0m10.479s
user    0m7.641s
sys     0m3.924s
  • Aktueller Master-Zweig
# time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

real    0m16.303s
user    0m12.584s
sys     0m4.501s
  • Gepatcht
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:

Umfeld

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

Testfall:

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

Ergebnisse

libseccomp 2.3.3

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

libseccomp 2.4.1

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

Ihr PR-Aufbau

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

Verschiedene Notizen

  • Ich habe 2.4.1 in AC_INIT in configure.ac gesetzt, bevor ich diesen PR erstellt habe.
  • Dieser benutzerdefinierte Build ist in /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.
  • Ich habe den Test einige Male durchgeführt, es gibt sehr kleine Abweichungen in den Ergebnissen. Daher bin ich zuversichtlich, dass es sich nicht um zufällige Ergebnisse handelt.

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.

Ergebnisse

foo.c

g++ foo.c -lseccomp -o foo -O3
for ((i=0; i<10; ++i)); do time ./foo; done 

libseccomp 2.3.3

./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

libseccomp 2.4.2

./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

PR-Nr. 180

./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.

Erstellen und Laden (von seccomp_init() bis seccomp_load()) von Filtern in meiner Sandbox plus etwas Sandbox-Initialisierungsaufwand

libseccomp 2.3.3

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

libseccomp 2.4.2

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

PR-Nr. 180

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:

War diese Seite hilfreich?
0 / 5 - 0 Bewertungen