Libseccomp: RFE: seccomp_rule_add se volvió muy lento desde v2.4.0

Creado en 1 may. 2019  ·  23Comentarios  ·  Fuente: seccomp/libseccomp

Hola,
el problema fue introducido por el compromiso ce3dda9a1747cc6a4c044efe5a2eb653c974919 entre v2.3.3 y v2.4.0. Considere el siguiente ejemplo: foo.c.zip .
Añade una gran cantidad de reglas. Y funciona alrededor de 100 veces más lento después de la confirmación mencionada anteriormente.

foo.c tiempo de ejecución usando v2.4.1: 0.448
foo.c tiempo de ejecución usando v2.3.3: 0.077

Investigué un poco y descubrí que db_col_transaction_start() copia la colección de filtros ya existente y usa arch_filter_rule_add() para duplicar las reglas de filtro. Pero arch_filter_rule_add() llama a arch_syscall_translate() que llama a arch_syscall_resolve_name() que funciona en O (número de llamadas al sistema en la arquitectura dada). Por lo tanto, agregar una regla funciona al menos en O (número de reglas ya agregadas * número de llamadas al sistema en arquitecturas usadas) que, en mi opinión, es realmente malo.
Conté la cantidad de llamadas a arch_filter_rule_add() en el ejemplo anterior y es igual a 201152 .

Antes de esa confirmación, el número de llamadas a arch_filter_rule_add() era 896 . Y por lo que entiendo del código, también db_col_transaction_start() copia la colección de filtros ya existente y no usa arch_filter_rule_add(). Lo que nos da una estimación: tiempo de agregar una regla alrededor de O (número de reglas ya agregadas + número de llamadas al sistema en la arquitectura dada), que es mucho mejor.

Sin embargo, en mi opinión, no debería estar relacionado con la cantidad de reglas ya agregadas, porque agregar n reglas funciona en O (n ^ 2) entonces. Pero ese es un tema para otra discusión, por lo que no debería ser un problema para los filtros pequeños o los filtros que se generan con poca frecuencia.

¿Por qué importa este problema?
Algunos filtros necesitan ejecutar programas PID (por ejemplo, permitir que el subproceso envíe señales solo a sí mismo). Entonces, si el programa restringido debe ejecutarse una cantidad considerable de veces, se convierte en una sobrecarga muy visible. Tengo un filtro de alrededor de 300 reglas y la sobrecarga de libseccomp es de alrededor de 0,16 s por ejecución del proceso de espacio aislado (ejecuto el proceso docenas de veces).

¡Gracias de antemano por su ayuda!

enhancement prioritlow

Comentario más útil

Estamos viendo tiempos de espera de los usuarios debido a este cambio. Realmente ralentizó las cosas en un orden de magnitud.

Todos 23 comentarios

Hola @varqox.

Sí, las funciones de resolución de syscall podrían mejorar, de hecho, si observa el código, verá varios comentarios como los siguientes:

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

Si desea ver cómo mejorar ese código, ¡podríamos usar la ayuda!

Como mencionó @pcmoore , existen amplias oportunidades para acelerar la _creación_ de un filtro seccomp usando libseccomp. Su investigación anterior describió una de varias áreas que podrían mejorar. Esto no ha sido una preocupación para mis usuarios, por lo que no me he centrado en ello.

Con respecto al rendimiento de _runtime_, actualmente estoy trabajando en el uso de un árbol binario para filtros grandes como el que proporcionaste en foo.c. Los resultados iniciales con mis clientes internos parecen prometedores, pero me encantaría tener otro par de ojos en los cambios. Ver solicitud de extracción https://github.com/seccomp/libseccomp/pull/152

Bien, veo que la resolución de syscall podría mejorarse, pero no es la causa raíz del problema. Que es, como yo lo veo, la creación de una instantánea en db_col_transaction_start() . Allí se llama a arch_filter_rule_add() , que es lento debido a la resolución de la llamada al sistema, que se resuelve en la regla original.

Lo veo de la siguiente manera: queremos duplicar todo el conjunto de filtros actuales (también conocido como struct db_filter) con todas sus reglas, por lo que _construimos_ todos los filtros desde cero en lugar de aprovechar lo que ya tenemos y simplemente _copiar_ todos los filtros. No tenemos que construir desde cero, tenemos un filtro de construcción completo del que solo queremos una copia. Tal vez me perdí algo, pero parece que se puede mejorar mucho la función db_col_transaction_start().

Con todo el estado en la colección de base de datos libseccomp interna, duplicarla no es una tarea trivial, regenerar la colección a partir de las reglas originales es mucho más fácil (desde la perspectiva del código). Hacer un seguimiento de las reglas originales también nos permite ofrecer la posibilidad de "eliminar" una regla existente (posible característica futura).

Esto no quiere decir que el código de transacción no se pueda mejorar, definitivamente se puede, pero el código actual es así por una razón, principalmente la simplicidad.

Estamos viendo tiempos de espera de los usuarios debido a este cambio. Realmente ralentizó las cosas en un orden de magnitud.

Otro pensamiento, probablemente podamos cambiar esto para que solo dupliquemos las reglas en el inicio de una transacción, no en todo el árbol, y solo recreemos el árbol en una transacción fallida. No es perfecto, pero eso debería recuperar una buena parte del tiempo.

Necesitamos hacer algo porque los tiempos de inicio de los contenedores y los procesos ejecutivos están experimentando una gran regresión en el rendimiento y provocando que las personas fijen a 2.3x.

No voy a comentar más sobre la naturaleza _"enorme"_ del problema, esa perspectiva ya se ha hecho varias veces y la considero relativa y dependiente del caso de uso. Sin embargo, quería recordarles a todos que las versiones de libseccomp anteriores a v2.4 son vulnerables a una posible vulnerabilidad que se ha hecho pública (problema n.º 139).

Para aquellos que estén preocupados por este problema, actualmente está marcado para una versión v2.5.

Hizo una refactorización y tiene impactos "enormes" en el rendimiento en una versión menor y no está siendo útil al ignorar esto diciendo que depende del caso de uso. Tómese esto en serio, ya que las personas comenzarán a notarlo a medida que las distribuciones se actualicen a 2.4.

@crosbymichael , el cambio no fue simplemente una refactorización, fue necesario solucionar problemas y admitir los cambios en el kernel (sobre todo, la necesidad de admitir llamadas al sistema tanto multiplexadas como directas, por ejemplo, llamadas al sistema de socket en x86 de 32 bits).

No estoy descartando esto, he seguido pensando en formas de resolver este problema (ver mis comentarios anteriores) y el hecho de que lo he marcado como algo para la próxima versión menor. En este punto, es difícil para mí no percibir sus comentarios como incendiarios, si esa no es su intención, sugiero tener más cuidado al comentar en el futuro. Si no está satisfecho con el progreso de este problema, siempre puede ayudar enviando un parche/PR para su revisión.

Nota para mí mismo y para cualquier otra persona que esté considerando tratar de resolver esto...

Hace poco me recordaron por qué hacemos lo que hacemos con respecto a las transacciones (copiar todo por adelantado); hacemos esto porque necesitamos poder revertir una transacción sin fallar. ¿Por qué?
Una operación seccomp_rule_add() normal necesita mantener el filtro intacto incluso en caso de falla; si fallamos en una transacción de varias partes (por ejemplo, llamadas al sistema socket/ipc en x86/s390/s390x/etc.) como parte de una adición de regla normal, DEBEMOS poder volver al filtro al comienzo de la transacción sin fallar ( independientemente de la presión de la memoria, etc.).

Duplicar el árbol sin las reglas seguirá siendo un desafío debido a la naturaleza del árbol y la vinculación dentro del árbol, pero es posible que podamos elegir selectivamente cuándo necesitamos crear una transacción interna, saltándola por muchos casos en que no es necesario.

Pasé un poco más de tiempo mirando esto y debido a la forma en que modificamos destructivamente el árbol de decisiones durante la adición de una regla, no estoy seguro de que podamos evitar envolver las adiciones de reglas con una transacción. Esto significa que en lugar de encontrar formas de limitar nuestro uso de transacciones internamente, debemos encontrar una forma de acelerarlas, afortunadamente creo que pude haber encontrado una solución: árboles de sombra.

Actualmente construimos un nuevo árbol cada vez que creamos una nueva transacción y la descartamos si tiene éxito, lo que, como hemos visto, puede ser prohibitivamente lento en algunos casos de uso. Mi idea es que, en lugar de descartar el árbol duplicado en la confirmación, intentamos agregar la regla que acabamos de agregar al árbol duplicado (haciéndola una copia del filtro actual) y manteniéndola como una "transacción oculta" para acelerar la siguiente instantánea de la transacción. Algunas notas:

  • db_col_transaction_start() debería intentar usar la transacción oculta si está presente, pero si no, debería volver al comportamiento actual.
  • db_col_transaction_abort() debería comportarse de la misma manera que lo hace ahora; esto significa que una transacción fallida borrará la transacción en la sombra (necesita un árbol para restaurar el filtro), pero la próxima transacción exitosa restaurará la sombra. Una transacción fallida debe ser lo suficientemente infrecuente como para que esto no sea un problema importante.
  • Es posible que debamos borrar la transacción oculta en otras operaciones, por ejemplo, operaciones arch/ABI, pero eso es algo que debemos revisar. De todos modos, la compensación de la transacción en la sombra debería ser trivial.
  • Esto tiene la ventaja no solo de acelerar la adición de reglas, sino también de acelerar las transacciones en general. Puede que esto no sea significativo ahora, pero será útil cuando expongamos la funcionalidad de la transacción a los usuarios (esto sería necesario si alguna vez queremos hacer un mecanismo similar al "compromiso" de BSD).

Tuve algo de tiempo después de la cena esta noche, así que hice un pase rápido para implementar la idea de transacción en la sombra anterior. El código aún es tosco, y mis pruebas (a continuación) aún más toscas, pero parece que estamos viendo algunas ganancias de rendimiento con este enfoque:

  • Sobrecarga de prueba de línea base
# time for i in {0..20000}; do /bin/true; done
real    0m10.479s
user    0m7.641s
sys     0m3.924s
  • Rama maestra actual
# time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

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

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

Si restamos la sobrecarga de la prueba, observamos un aumento de aproximadamente el 20 % en el rendimiento de esta "prueba", pero espero que el beneficio de los conjuntos de filtros complejos sea mejor (¿mucho mejor?) que esto.

@varqox y/o @crosbymichael una vez que limpie un poco los parches y cree un PR, ¿podría probar esto en su entorno?

Mi caso de prueba de ejemplo ya está aquí:

Hola,
el problema fue introducido por el compromiso ce3dda9 entre v2.3.3 y v2.4.0. Considere el siguiente ejemplo: foo.c.zip .
Añade una gran cantidad de reglas. Y funciona alrededor de 100 veces más lento después de la confirmación mencionada anteriormente.

foo.c tiempo de ejecución usando v2.4.1: 0.448
foo.c tiempo de ejecución usando v2.3.3: 0.077

Pero tan pronto como el PR esté listo, puedo probarlo en mi entorno.

Hola, @varqox , sí, vi que incluyeste un caso de prueba en el informe original, pero estoy más interesado en escuchar cómo funciona en el uso real . Si pudieras probar PR #180 e informarme, te lo agradecería mucho, ¡gracias!

Hola @pcmoore ,

Gracias por hacer este PR.
Construí y probé su PR #180, el resultado es prometedor para mi caso de prueba. Observo este problema porque los clientes usan el control de estado de la ventana acoplable y sufrieron el problema de rendimiento en libseccomp 2.4.x .
En mi caso de prueba, el rendimiento de este PR es comparable a libseccomp 2.3.3 . Los detalles son los siguientes:

Ambiente

Ubuntu 19.04 VM (2 CPU, 2G de memoria) en MacBook Pro (15 pulgadas, mediados de 2015)
Kernel 5.0.0-32-genérico
Docker CE 19.03.2

Caso de prueba:

Prepara 20 recipientes:

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

Ejecute la prueba disparando docker exec en todos los contenedores al mismo tiempo

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

Resultados

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

Tu construcción de relaciones públicas

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

Notas misceláneas

  • Establecí 2.4.1 en AC_INIT en configure.ac antes de construir este PR.
  • Esta compilación personalizada está instalada en /usr/local/lib , la verifiqué ejecutando ldd /usr/bin/runc para asegurarme de que la compilación personalizada se esté utilizando durante la prueba.
  • Realicé la prueba varias veces, hay variaciones muy pequeñas en los resultados. Así que estoy seguro de que no son resultados accidentales.

¡Eso es genial, gracias por la ayuda @xinfengliu!

Hola @pcmoore ,
Gracias por este PR.
En mi caso, restaura el rendimiento de libseccomp a un nivel comparable con v2.3.3.

Resultados

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

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

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

Promedio: 0.0126 s

Parece que este PR da algo de aceleración sobre v2.3.3 en esta prueba sintética.

Crear y cargar (desde seccomp_init() hasta seccomp_load()) filtros en mi sandbox más algunos gastos generales de inicialización de sandbox

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

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

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

Promedio: 0.0049 s

Aunque en PR de prueba sintética da mejores tiempos que v2.3.3, en el mundo real es un poco más lento (posiblemente debido a reglas más complicadas y al ejecutar seccomp_merge() para fusionar dos filtros grandes). Sin embargo, todavía ofrece una aceleración aproximadamente diez veces superior a la v2.4.2.

¡Gracias por verificar el desempeño @varqox! Tan pronto como @drakenclimber responda a la última ronda de comentarios (y solucione los problemas restantes que pueda plantear), fusionaremos esto.

Ah, no importa, acabo de darme cuenta de que @drakenclimber marcó el PR como aprobado. Voy a seguir adelante y combinar eso ahora.

Acabo de fusionar PR # 180, así que creo que podemos marcar esto como cerrado, si alguien nota algún problema de rendimiento restante, siéntase libre de comentar y / o volver a abrir. ¡Gracias a todos por su paciencia y ayuda!

@pcmoore , ¿planeas lanzarlo pronto con esos cambios?

Actualmente, esto es parte del hito de lanzamiento de libsecomp v2.5, puede seguir nuestro progreso hacia el lanzamiento de v2.5 utilizando el siguiente enlace:

¿Fue útil esta página
0 / 5 - 0 calificaciones