Libseccomp: RFE : seccomp_rule_add est devenu très lent depuis la v2.4.0

Créé le 1 mai 2019  ·  23Commentaires  ·  Source: seccomp/libseccomp

Bonjour,
le problème a été introduit par le commit ce3dda9a1747cc6a4c044eafe5a2eb653c974919 entre la v2.3.3 et la v2.4.0. Prenons l'exemple suivant : foo.c.zip .
Il ajoute un très grand nombre de règles. Et fonctionne environ 100 fois plus lentement après le commit mentionné ci-dessus.

foo.c temps d'exécution avec v2.4.1 : 0.448
foo.c temps d'exécution avec v2.3.3 : 0.077

J'ai creusé un peu et découvert que db_col_transaction_start() copie la collection de filtres déjà existante et utilise arch_filter_rule_add() pour dupliquer les règles de filtrage. Mais arch_filter_rule_add() appelle arch_syscall_translate() qui appelle arch_syscall_resolve_name() qui fonctionne en O (nombre d'appels système sur l'architecture donnée). Ainsi, l'ajout d'une règle fonctionne au moins en O (nombre de règles déjà ajoutées * nombre d'appels système sur les architectures utilisées), ce qui, selon l'OMI, est vraiment mauvais.
J'ai compté le nombre d'appels à arch_filter_rule_add() dans l'exemple ci-dessus et il est égal 201152 .

Avant cette validation, le nombre d'appels à arch_filter_rule_add() était 896 . Et d'après ce que je comprends du code, db_col_transaction_start() copie également la collection de filtres déjà existante et n'utilise pas arch_filter_rule_add(). Ce qui nous donne une estimation : temps d'ajout d'une règle autour de O (nombre de règles déjà ajoutées + nombre d'appels système sur l'architecture donnée), ce qui est bien meilleur.

Cependant, selon l'OMI, cela ne devrait pas être lié au nombre de règles déjà ajoutées, car l'ajout de n règles fonctionne alors en O (n ^ 2). Mais c'est un sujet de discussion différent, donc cela ne devrait pas être un problème pour les petits filtres ou les filtres générés rarement.

Pourquoi ce problème est-il important ?
Certains filtres nécessitent l'exécution de programmes PID (permettant par exemple au thread de s'envoyer des signaux uniquement à lui-même). Ainsi, si le programme restreint doit être exécuté un nombre considérable de fois, cela devient une surcharge très visible. J'ai un filtre d'environ 300 règles et la surcharge de libseccomp est d'environ 0,16 s par exécution du processus en bac à sable (j'exécute le processus des dizaines de fois).

Merci d'avance pour votre aide!

enhancement prioritlow

Commentaire le plus utile

Nous constatons des délais d'expiration des utilisateurs à cause de ce changement. Cela a vraiment ralenti les choses d'un ordre de grandeur.

Tous les 23 commentaires

Salut @varqox.

Oui, les fonctions de résolution des appels système pourraient être améliorées. En fait, si vous regardez le code, vous verrez plusieurs commentaires comme celui-ci :

/* XXX - plenty of room for future improvement here */

Si vous souhaitez améliorer ce code, nous devrions utiliser l'aide !

Comme @pcmoore l'a mentionné, il existe de nombreuses possibilités d'accélérer la _création_ d'un filtre seccomp à l'aide de libseccomp. Votre recherche ci-dessus a décrit l'un des nombreux domaines qui pourraient être améliorés. Cela n'a pas été une préoccupation pour mes utilisateurs, donc je ne me suis pas concentré là-dessus.

En ce qui concerne les performances _runtime_, je travaille actuellement sur l'utilisation d'un arbre binaire pour les grands filtres comme celui que vous avez fourni dans foo.c. Les premiers résultats avec mes clients internes semblent prometteurs, mais j'aimerais avoir un autre regard sur les changements. Voir la demande d'extraction https://github.com/seccomp/libseccomp/pull/152

OK, je vois que la résolution des appels système pourrait être améliorée, mais ce n'est pas la cause première du problème. Qui est, comme je le vois, la création d'un instantané dans le db_col_transaction_start() . Là , arch_filter_rule_add() est appelé, ce qui est lent en raison de la résolution de l'appel système, qui est résolu dans la règle d'origine.

Je le vois comme suit : nous voulons dupliquer l'ensemble des filtres actuels (alias struct db_filter) avec toutes leurs règles, donc nous _construisons_ tous les filtres à partir de zéro au lieu de profiter de ce que nous avons déjà et juste _copier_ tous les filtres. Nous n'avons pas besoin de construire à partir de zéro, nous avons un filtre de construction complet dont nous voulons juste une copie. J'ai peut-être raté quelque chose, mais il semble que beaucoup d'améliorations puissent être apportées à la fonction db_col_transaction_start().

Avec tout l'état de la collection interne libseccomp db, la dupliquer n'est pas une tâche triviale, régénérer la collection à partir des règles d'origine est beaucoup plus facile (du point de vue du code). Garder une trace des règles d'origine nous permet également d'offrir la possibilité de "supprimer" une règle existante (fonctionnalité future possible).

Cela ne veut pas dire que le code de transaction ne pourrait pas être amélioré - il le peut certainement - mais le code actuel est tel qu'il est pour une raison, principalement la simplicité.

Nous constatons des délais d'expiration des utilisateurs à cause de ce changement. Cela a vraiment ralenti les choses d'un ordre de grandeur.

Une autre pensée, nous pouvons probablement changer cela afin de ne dupliquer les règles qu'au début d'une transaction, pas l'arbre entier, et de ne recréer l'arbre que sur une transaction ayant échoué. Ce n'est pas parfait, mais cela devrait revenir une bonne partie du temps.

Nous devons faire quelque chose car les temps de démarrage des conteneurs et des processus d'exécution connaissent une énorme régression des performances et obligent les gens à épingler à 2,3x

Je ne vais pas commenter davantage la nature _ "énorme" _ du problème, cette perspective a déjà été faite plusieurs fois et je la considère à la fois relative et dépendante du cas d'utilisation. Cependant, je voulais rappeler à tout le monde que les versions de libseccomp antérieures à la v2.4 sont vulnérables à une vulnérabilité potentielle qui a été rendue publique (numéro 139).

Pour ceux qui sont préoccupés par ce problème, il est actuellement marqué pour une version v2.5.

Vous avez fait une refactorisation et elle a des impacts "énormes" sur les performances dans une version mineure et vous n'êtes pas utile en supprimant cela en disant que cela dépend du cas d'utilisation. Veuillez prendre cela au sérieux car les gens vont commencer à remarquer la mise à jour des distributions vers la version 2.4

@crosbymichael le changement n'était pas simplement une refactorisation, il était nécessaire de résoudre les problèmes et de prendre en charge les changements dans le noyau (notamment la nécessité de prendre en charge les appels système multiplexés et les appels directs, par exemple les appels système de socket sur 32 bits x86).

Je ne souffle pas cela, j'ai continué à réfléchir aux moyens de résoudre ce problème (voir mes commentaires ci-dessus), et au fait que j'ai marqué cela comme quelque chose pour la prochaine version mineure. À ce stade, il m'est difficile de ne pas percevoir vos commentaires comme incendiaires, si ce n'est pas votre intention, je suggère de faire plus attention lorsque vous commentez à l'avenir. Si vous n'êtes pas satisfait des progrès sur ce problème, vous êtes toujours le bienvenu pour aider en soumettant un correctif/PR pour examen.

Note à moi-même et à quiconque envisage d'essayer de résoudre ce problème ...

On m'a récemment rappelé pourquoi nous faisons ce que nous faisons en ce qui concerne les transactions (copier tout à l'avance) ; nous le faisons parce que nous devons pouvoir annuler une transaction sans échouer. Pourquoi?
Une opération normale seccomp_rule_add() doit garder le filtre intact même en cas d'échec ; si nous échouons une transaction en plusieurs parties (par exemple socket/ipc syscalls sur x86/s390/s390x/etc.) dans le cadre d'un ajout de règle normal, nous DEVONS pouvoir revenir au filtre au début de la transaction sans échec ( indépendamment de la pression de la mémoire, etc.).

La duplication de l'arborescence sans les règles va continuer à être difficile en raison de la nature de l'arborescence et de la liaison à l'intérieur de l'arborescence, mais nous pourrions être en mesure de choisir de manière sélective quand nous devons créer une transaction interne, en la sautant pour les nombreux cas où cela n'est pas nécessaire.

J'ai passé un peu plus de temps à regarder cela et à cause de la façon dont nous modifions de manière destructive l'arbre de décision lors d'un ajout de règle, je ne suis pas sûr que nous puissions éviter d'envelopper les ajouts de règle avec une transaction. Cela signifie qu'au lieu de trouver des moyens de limiter notre utilisation des transactions en interne, nous devons trouver un moyen de les accélérer, heureusement, je pense avoir trouvé une solution : les arbres fantômes.

Actuellement, nous construisons une nouvelle arborescence chaque fois que nous créons une nouvelle transaction et la supprimons en cas de succès, ce qui, comme nous l'avons vu, peut être extrêmement lent dans certains cas d'utilisation. Ma pensée est qu'au lieu de supprimer l'arborescence dupliquée lors de la validation, nous essayons d'ajouter la règle que nous venons d'ajouter à l'arborescence dupliquée (ce qui en fait une copie du filtre actuel) et de la conserver en tant que "transaction fantôme" pour accélérer la prochaine instantané de la transaction. Quelques notes:

  • db_col_transaction_start() devrait tenter d'utiliser la transaction fantôme si elle est présente, mais sinon, elle devrait revenir au comportement actuel.
  • db_col_transaction_abort() devrait se comporter de la même manière que maintenant ; cela signifie qu'une transaction qui a échoué effacera la transaction shadow (elle doit arborer pour restaurer le filtre), mais la prochaine transaction réussie restaurera le shadow. Une transaction échouée doit être suffisamment peu fréquente pour que cela ne soit pas un problème majeur.
  • Nous devrons peut-être effacer la transaction fantôme sur d'autres opérations, par exemple arch/ABI ops?, mais c'est quelque chose que nous devrons vérifier. Quoi qu'il en soit, la compensation de la transaction fictive devrait être triviale.
  • Cela a l'avantage non seulement d'accélérer l'ajout de règles, mais aussi d'accélérer les transactions en général. Ce n'est peut-être pas significatif maintenant, mais ce sera utile lorsque nous exposerons la fonctionnalité de transaction aux utilisateurs (cela serait nécessaire si nous voulons un jour faire un mécanisme de type "pledge" BSD).

J'ai eu un peu de temps après le dîner ce soir, alors j'ai rapidement mis en œuvre l'idée de transaction fantôme ci-dessus. Le code est encore brut, et mes tests (ci-dessous) encore plus bruts, mais il semble que nous constatons des gains de performances avec cette approche :

  • Frais généraux de test de base
# time for i in {0..20000}; do /bin/true; done
real    0m10.479s
user    0m7.641s
sys     0m3.924s
  • Branche principale actuelle
# time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

real    0m16.303s
user    0m12.584s
sys     0m4.501s
  • Patché
time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

real    0m15.021s
user    0m11.540s
sys     0m4.387s

Si nous soustrayons la surcharge de test, nous recherchons une augmentation d'environ 20% des performances sur ce "test", mais je m'attends à ce que l'avantage pour les ensembles de filtres complexes soit meilleur (bien meilleur?) Que cela.

@varqox et/ou @crosbymichael une fois que j'aurai un peu nettoyé les correctifs et créé un PR, seriez-vous en mesure de tester cela dans votre environnement ?

Mon exemple de cas de test est déjà ici :

Bonjour,
le problème a été introduit par le commit ce3dda9 entre la v2.3.3 et la v2.4.0. Prenons l'exemple suivant : foo.c.zip .
Il ajoute un très grand nombre de règles. Et fonctionne environ 100 fois plus lentement après le commit mentionné ci-dessus.

foo.c temps d'exécution avec v2.4.1 : 0.448
foo.c temps d'exécution avec v2.3.3 : 0.077

Mais dès que le PR est prêt, je peux le tester dans mon environnement.

Salut @varqox , oui, j'ai vu que vous aviez inclus un cas de test dans le rapport d'origine, mais je suis plus intéressé à savoir comment il fonctionne en utilisation réelle . Si vous pouviez essayer PR #180 et faire un rapport, j'apprécierais vraiment - merci !

Salut @pcmoore ,

Merci d'avoir fait ce PR.
J'ai construit et testé votre PR #180, le résultat est prometteur pour mon cas de test. Je surveille ce problème parce que les clients utilisent Docker Health Check et souffrent du problème de performances dans libseccomp 2.4.x .
Dans mon cas de test, les performances de ce PR sont comparables à libseccomp 2.3.3 . Les détails sont comme ci-dessous :

Environnement

Machine virtuelle Ubuntu 19.04 (2 processeurs, 2 Go de mémoire) sur MacBook Pro (15 pouces, mi-2015)
Noyau 5.0.0-32-générique
DockerCE 19.03.2

Cas de test:

Préparez 20 contenants :

for i in $(seq 1 20)
do
  docker run -d --name bb$i busybox sleep 3d
done

Exécutez le test en lançant docker exec sur tous les conteneurs en même temps

for i in $(seq 1 20)
do 
  /usr/bin/time -f "%E real" docker exec bb$i true & 
done

Résultats

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

Votre construction de relations publiques

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

Notes diverses

  • J'ai mis 2.4.1 en AC_INIT en configure.ac avant de construire ce PR.
  • Cette version personnalisée est installée dans /usr/local/lib , je l'ai vérifiée en exécutant ldd /usr/bin/runc pour m'assurer que la version personnalisée est utilisée pendant le test.
  • J'ai fait le test plusieurs fois, il y a de très petites variations dans les résultats. Je suis donc convaincu qu'il ne s'agit pas de résultats accidentels.

C'est super, merci pour l'aide @xinfengliu !

Salut @pcmoore ,
Merci pour ce PR.
Dans mon cas, il restaure les performances de libseccomp à un niveau comparable à la v2.3.3.

Résultats

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

Moyenne : 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

Moyenne : 0.1949 s

PR #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

Moyenne : 0.0126 s

Il semble que ce PR donne une certaine accélération par rapport à la v2.3.3 dans ce test synthétique.

Construire et charger (de seccomp_init() à seccomp_load()) des filtres dans mon bac à sable plus une surcharge d'initialisation du bac à sable

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

Moyenne : 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

Moyenne : 0.0466 s

PR #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

Moyenne : 0.0049 s

Bien que dans le test synthétique, PR donne de meilleurs temps que la v2.3.3, dans le monde réel, il est légèrement plus lent (peut-être à cause de règles plus compliquées et de l'exécution de seccomp_merge() pour fusionner deux gros filtres). Cependant, il offre toujours une accélération environ dix fois supérieure à la v2.4.2.

Merci d'avoir vérifié les performances @varqox ! Dès que @drakenclimber répondra à la dernière série de commentaires (et que je résoudrai tous les problèmes restants qu'il pourrait soulever), nous fusionnerons cela.

Ah, peu importe, je viens de remarquer que @drakenclimber a marqué le PR comme approuvé. Je vais aller de l'avant et fusionner cela maintenant.

Je viens de fusionner PR # 180, donc je pense que nous pouvons marquer cela comme fermé, si quelqu'un remarque des problèmes de performances restants, n'hésitez pas à commenter et / ou à rouvrir. Merci à tous pour votre patience et votre aide !

@pcmoore prévoyez -vous de sortir bientôt avec ces changements ?

Cela fait actuellement partie du jalon de la version libseccomp v2.5, vous pouvez suivre notre progression vers la version v2.5 en utilisant le lien ci-dessous :

Cette page vous a été utile?
0 / 5 - 0 notes