Tutoriales

¿Cómo puedo reducir el tiempo para crear y destruir un contenedor OCI a 5 milisegundos?

Cuando comencé a trabajar en crun, una alternativa runc compatible con Open Container Initiative (OCI) para tiempos de ejecución de contenedores de Linux, en 2017, estaba buscando una forma más rápida de iniciar y detener contenedores. Estoy trabajando para mejorar el tiempo de ejecución de OCI, que es el componente de la pila de OCI que se comunica con el kernel y configura el entorno de tiempo de ejecución del contenedor.

[ Getting started with containers? Check out Deploying containerized applications: A technical overview. ]

El tiempo de ejecución de OCI tiene un tiempo de ejecución muy limitado y su trabajo consiste principalmente en ejecutar una serie de llamadas al sistema que se asignan directamente al archivo de configuración de OCI.

Me sorprendió que una tarea tan trivial llevara tanto tiempo.

Descargo de responsabilidad: utilicé el kernel predeterminado disponible en la instalación de Fedora y todas las bibliotecas que probé. Además de las correcciones descritas en esta publicación de blog, existen otras correcciones que pueden afectar el rendimiento general.

Usé la misma versión de crun utilizada para todas las pruebas a continuación.

Para comparar las pruebas, utilicé súper bien al instalar bienes.

Publicaciones relacionadas

Cómo fue en 2017

Para comprobar hasta dónde hemos llegado, debe volver a 2017 (o simplemente instalar una imagen antigua de Fedora). Para las pruebas a continuación, utilicé Fedora 24 basado en Linux kernel 4.5.5.

En una instalación nueva de Fedora 24, construyendo crun desde la rama maestra, observé estos puntos de referencia:

# hyperfine 'crun run foo'
Benchmark 1: 'crun run foo'
  Time (mean ± σ):     159.2 ms ±  21.8 ms    [User: 43.0 ms, System: 16.3 ms]
  Range (min … max):    73.9 ms … 194.9 ms    39 runs

160 milisegundos es mucho, y recuerdo que es similar a lo que observé hace cinco años.

Hice un perfil del tiempo de ejecución de OCI e inmediatamente mostró que la mayor parte del tiempo del usuario se pasaba en libseccomp compilar seccomp filtrar.

Para verificar esto, ejecuté una ejecución con la misma configuración pero sin seccomp contorno:

# hyperfine 'crun run foo'
Benchmark 1: 'crun run foo'
  Time (mean ± σ):     139.6 ms ±  20.8 ms    [User: 4.1 ms, System: 22.0 ms]
  Range (min … max):    61.8 ms … 177.0 ms    47 runs

Tomó una décima parte del tiempo que les tomó a los usuarios anteriores, ¡y el tiempo total también ha mejorado!

Entonces hay dos problemas: 1) el tiempo del sistema es bastante largo, 2) libseccomp Domina el tiempo del usuario. Necesito resolver los dos.

reducir el tiempo del sistema

Hay muy pocos culpables de la mayor parte del tiempo perdido en el kernel.Primero procesaría la hora del sistema y regresaría seccomp después.

Crear y destruir espacios de nombres de red

Crear y destruir espacios de nombres de red solía ser costoso.Puedo reproducir el problema usando unshare herramienta. En Fedora 24, obtengo los siguientes resultados:

# hyperfine 'unshare -n true'
Benchmark 1: 'unshare -n true'
  Time (mean ± σ):      47.7 ms ±  51.4 ms    [User: 0.6 ms, System: 3.2 ms]
  Range (min … max):     0.0 ms … 190.5 ms    365 runs

¡Eso es mucho tiempo!

Traté de arreglarlo en el kernel y sugerí reparar.Florian Westphal reescribió esto como una serie de una mejor manera y lo fusionó con el kernel de Linux:

commit 8c873e2199700c2de7dbd5eedb9d90d5f109462b
Author: Florian Westphal 
Date:   Fri Dec 1 00:21:04 2017 +0100

    netfilter: core: free hooks with call_rcu
    
    Giuseppe Scrivano says:
      "SELinux, if enabled, registers for each new network namespace 6
        netfilter hooks."
    
    Cost for this is high.  With synchronize_net() removed:
       "The net benefit on an SMP machine with two cores is that creating a
       new network namespace takes -40% of the original time."
    
    This patch replaces synchronize_net+kvfree with call_rcu().
    We store rcu_head at the tail of a structure that has no fixed layout,
    i.e. we cannot use offsetof() to compute the start of the original
    allocation.  Thus store this information right after the rcu head.
    
    We could simplify this by just placing the rcu_head at the start
    of struct nf_hook_entries.  However, this structure is used in
    packet processing hotpath, so only place what is needed for that
    at the beginning of the struct.
    
    Reported-by: Giuseppe Scrivano 
    Signed-off-by: Florian Westphal 
    Signed-off-by: Pablo Neira Ayuso 

commit 26888dfd7e7454686b8d3ea9ba5045d5f236e4d7
Author: Florian Westphal 
Date:   Fri Dec 1 00:21:03 2017 +0100

    netfilter: core: remove synchronize_net call if nfqueue is used
    
    since commit 960632ece6949b ("netfilter: convert hook list to an array")
    nfqueue no longer stores a pointer to the hook that caused the packet
    to be queued.  Therefore no extra synchronize_net() call is needed after
    dropping the packets enqueued by the old rule blob.
    
    Signed-off-by: Florian Westphal 
    Signed-off-by: Pablo Neira Ayuso 

commit 4e645b47c4f000a503b9c90163ad905786b9bc1d
Author: Florian Westphal 
Date:   Fri Dec 1 00:21:02 2017 +0100

    netfilter: core: make nf_unregister_net_hooks simple wrapper again
    
    This reverts commit d3ad2c17b4047
    ("netfilter: core: batch nf_unregister_net_hooks synchronize_net calls").
    
    Nothing wrong with it.  However, followup patch will delay freeing of hooks
    with call_rcu, so all synchronize_net() calls become obsolete and there
    is no need anymore for this batching.
    
    This revert causes a temporary performance degradation when destroying
    network namespace, but its resolved with the upcoming call_rcu conversion.
    
    Signed-off-by: Florian Westphal 
    Signed-off-by: Pablo Neira Ayuso 

Estos parches son muy diferentes. El tiempo para crear y destruir un espacio de nombres de red se reduce a una cantidad ridícula en un kernel 5.19.15 moderno:

# hyperfine 'unshare -n true'
Benchmark 1: 'unshare -n true'
  Time (mean ± σ):       1.5 ms ±   0.5 ms    [User: 0.3 ms, System: 1.3 ms]
  Range (min … max):     0.8 ms …   6.7 ms    1907 runs

[ Modernize your IT with managed cloud services. ]

cola de montaje

Instalar mqueue También solía ser una operación relativamente costosa.

En Fedora 24, solía verse así:

# mkdir /tmp/mqueue; hyperfine 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'; rmdir /tmp/mqueue
Benchmark 1: 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'
  Time (mean ± σ):      16.8 ms ±   3.1 ms    [User: 2.6 ms, System: 5.0 ms]
  Range (min … max):     9.3 ms …  26.8 ms    261 runs

También traté de resolver este problema y se me ocurrió una repararNo fue aceptado, pero a Al Viro se le ocurrió una versión mejor para solucionar este problema:

commit 36735a6a2b5e042db1af956ce4bcc13f3ff99e21
Author: Al Viro 
Date:   Mon Dec 25 19:43:35 2017 -0500

    mqueue: switch to on-demand creation of internal mount
    
    Instead of doing that upon each ipcns creation, we do that the first
    time mq_open(2) or mqueue mount is done in an ipcns.  What's more,
    doing that allows to get rid of mount_ns() use - we can go with
    considerably cheaper mount_nodev(), avoiding the loop over all
    mqueue superblock instances; ipcns->mq_mnt is used to locate preexisting
    instance in O(1) time instead of O(instances) mount_ns() would've
    cost us.
    
    Based upon the version by Giuseppe Scrivano ; I've
    added handling of userland mqueue mounts (original had been broken in
    that area) and added a switch to mount_nodev().
    
    Signed-off-by: Al Viro 

Después de este parche, el costo de crear un mqueue Las monturas también están fuera de línea:

# mkdir /tmp/mqueue; hyperfine 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'; rmdir /tmp/mqueue
Benchmark 1: 'unshare --propagation=private -m mount -t mqueue mqueue /tmp/mqueue'
  Time (mean ± σ):       0.7 ms ±   0.5 ms    [User: 0.5 ms, System: 0.6 ms]
  Range (min … max):     0.0 ms …   3.1 ms    772 runs

Crear y destruir espacios de nombres IPC

Dejé de trabajar en los tiempos de inicio de contenedores durante algunos años y comencé de nuevo a principios de 2020. Otro problema es cuando se crea y se destruye el espacio de nombres de IPC.

Puede reproducir el problema del espacio de nombres de la red con unshare herramienta:

# hyperfine 'unshare -i true'
Benchmark 1: 'unshare -i true'
  Time (mean ± σ):      10.9 ms ±   2.1 ms    [User: 0.5 ms, System: 1.0 ms]
  Range (min … max):     4.2 ms …  17.2 ms    310 runs

Esta vez, la versión del parche que envié fue aceptada aguas arriba:

commit e1eb26fa62d04ec0955432be1aa8722a97cb52e7
Author: Giuseppe Scrivano 
Date:   Sun Jun 7 21:40:10 2020 -0700

    ipc/namespace.c: use a work queue to free_ipc
    
    the reason is to avoid a delay caused by the synchronize_rcu() call in
    kern_umount() when the mqueue mount is freed.
    
    the code:
    
        #define _GNU_SOURCE
        #include 
        #include 
        #include 
        #include 
    
        int main()
        {
            int i;
    
            for (i = 0; i < 1000; i++)
                if (unshare(CLONE_NEWIPC) < 0)
                    error(EXIT_FAILURE, errno, "unshare");
        }
    
    goes from
    
            Command being timed: "./ipc-namespace"
            User time (seconds): 0.00
            System time (seconds): 0.06
            Percent of CPU this job got: 0%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:08.05
    
    to
    
            Command being timed: "./ipc-namespace"
            User time (seconds): 0.00
            System time (seconds): 0.02
            Percent of CPU this job got: 96%
            Elapsed (wall clock) time (h:mm:ss or m:ss): 0:00.03
    
    Signed-off-by: Giuseppe Scrivano 
    Signed-off-by: Andrew Morton 
    Reviewed-by: Paul E. McKenney 
    Reviewed-by: Waiman Long 
    Cc: Davidlohr Bueso 
    Cc: Manfred Spraul 
    Link: 
    Signed-off-by: Linus Torvalds 

Con este parche, el tiempo para crear y destruir IPC se ha reducido considerablemente, como se informa en el mensaje de confirmación. En un kernel 5.19.15 moderno, ahora obtengo:

# hyperfine 'unshare -i true'
Benchmark 1: 'unshare -i true'
  Time (mean ± σ):       0.1 ms ±   0.2 ms    [User: 0.2 ms, System: 0.4 ms]
  Range (min … max):     0.0 ms …   1.5 ms    1966 runs

reducir el tiempo del usuario

El tiempo del kernel parece estar bajo control ahora. ¿Qué puedo hacer para reducir el tiempo de usuario?

me enteré antes libseccomp fue el culpable aquí, así que lo arreglé tan pronto como arreglé IPC en el kernel.

La mayoría de los costos están relacionados con libseccomp se debe al código de búsqueda de llamada del sistema. Los archivos de configuración de OCI contienen una lista de nombres de llamadas al sistema.Este seccomp_syscall_resolve_name La llamada de función busca cada llamada del sistema y devuelve el número de llamada del sistema según el nombre de la llamada del sistema.

Libseccomp solía realizar una búsqueda lineal a través de la tabla de llamadas al sistema para cada nombre de llamada al sistema. Por ejemplo, x86_64 se ve así:

/* NOTE: based on Linux v5.4-rc4 */
const struct arch_syscall_def x86_64_syscall_table[] = { \
	{ "_llseek", __PNR__llseek },
	{ "_newselect", __PNR__newselect },
	{ "_sysctl", 156 },
	{ "accept", 43 },
	{ "accept4", 288 },
	{ "access", 21 },
	{ "acct", 163 },
.....
    };

int x86_64_syscall_resolve_name(const char *name)
{
	unsigned int iter;
	const struct arch_syscall_def *table = x86_64_syscall_table;

	/* XXX - plenty of room for future improvement here */
	for (iter = 0; table[iter].name != NULL; iter++) {
		if (strcmp(name, table[iter].name) == 0)
			return table[iter].num;
	}

	return __NR_SCMP_ERROR;
}

Establecer seccomp a través del perfil libseccomp hay un complejo O(n*metro)Dónde no es el número de llamadas al sistema en el perfil, y Metro es el número de llamadas al sistema conocidas libseccomp.

Seguí los consejos en los comentarios del código y pasé un tiempo tratando de solucionarlo. En enero de 2020, estaba en un reparar por libseccomp Use una función hash perfecta para encontrar el nombre de llamada al sistema para resolver el problema.

aquí está libseccomp reparar:

commit 9b129c41ac1f43d373742697aa2faf6040b9dfab
Author: Giuseppe Scrivano 
Date:   Thu Jan 23 17:01:39 2020 +0100

    arch: use gperf to generate a perfact hash to lookup syscall names
    
    This patch significantly improves the performance of
    seccomp_syscall_resolve_name since it replaces the expensive strcmp
    for each syscall in the database, with a lookup table.
    
    The complexity for syscall_resolve_num is not changed and it
    uses the linear search, that is anyway less expensive than
    seccomp_syscall_resolve_name as it uses an index for comparison
    instead of doing a string comparison.
    
    On my machine, calling 1000 seccomp_syscall_resolve_name_arch and
    seccomp_syscall_resolve_num_arch over the entire syscalls DB passed
    from ~0.45 sec to ~0.06s.
    
    PM: After talking with Giuseppe I made a number of additional
    changes, some substantial, the highlights include:
    * various style tweaks
    * .gitignore fixes
    * fixed subject line, tweaked the description
    * dropped the arch-syscall-validate changes as they were masking
      other problems
    * extracted the syscalls.csv and file deletions to other patches
      to keep this one more focused
    * fixed the x86, x32, arm, all the MIPS ABIs, s390, and s390x ABIs as
      the syscall offsets were not properly incorporated into this change
    * cleaned up the ABI specific headers
    * cleaned up generate_syscalls_perf.sh and renamed to
      arch-gperf-generate
    * fixed problems with automake's file packaging
    
    Signed-off-by: Giuseppe Scrivano 
    Reviewed-by: Tom Hromatka 
    [PM: see notes in the "PM" section above]
    Signed-off-by: Paul Moore 

El parche ha sido fusionado y lanzado.actualmente en construcción seccomp La complejidad del perfil es superior) y no El número de llamadas al sistema en el archivo de configuración.

Las mejoras son significativas, con suficientes nuevos libseccomp:

# hyperfine 'crun run foo'
Benchmark 1: 'crun run foo'
  Time (mean ± σ):      28.9 ms ±   5.9 ms    [User: 16.7 ms, System: 4.5 ms]
  Range (min … max):    19.1 ms …  41.6 ms    73 runs

El tiempo de usuario es de solo 16,7 ms.Solía ​​ser más de 40 ms y alrededor de 4 ms cuando seccomp No utilizado.

Entonces, usar 4.1ms como tiempo de usuario no cuesta seccompTengo:

time_used_by_seccomp_before = 43.0ms - 4.1ms = 38.9ms time_used_by_seccomp_after = 16.7ms - 4.1ms = 12.6ms

¡Más de tres veces más rápido!La búsqueda de llamadas al sistema es solo una parte de la ecuación libseccomp Hacer. Nuevamente tomó una cantidad considerable de tiempo compilar el filtro BPF.

Compilación de filtros BPF

¿Puedo hacerlo mejor?Este seccomp_export_bpf la función hace filtro de paso de banda compilación de filtros, que sigue siendo bastante cara.

Una simple observación es que la mayoría de los contenedores reutilizan el mismo seccomp Análisis repetido, casi no se produjo personalización. Por lo tanto, tiene sentido almacenar en caché el resultado de la compilación y reutilizarlo cuando sea posible.

[ Read Improving Linux container security with seccomp. ]

hay uno Nueva función de ejecución Guarde en caché el resultado de la compilación del filtro BPF. En el momento de escribir este artículo, el parche no se ha fusionado, aunque está casi terminado.

Con esto, el costo de compilar seccomp El perfil solo se pagará si el filtro BPF generado no está en el caché. Esto es lo que tengo ahora:

# hyperfine 'crun-from-the-future run foo'
Benchmark 1: 'crun-from-the-future run foo'
  Time (mean ± σ):       5.6 ms ±   3.0 ms    [User: 1.0 ms, System: 4.5 ms]
  Range (min … max):     4.2 ms …  26.8 ms    101 runs

En conclusión

Durante cinco años, el tiempo total requerido para crear y destruir un contenedor OCI ha disminuido de casi 160 milisegundos a poco más de 5 milisegundos.

¡Eso es casi una mejora de 30x!

[ Get this complimentary eBook from Red Hat: Managing your Kubernetes clusters for dummies. ]


Este artículo fue adaptado de Acelere su viaje a los contenedores OCI y republicado con permiso.

LEER  APRENDER A UTILIZAR LINUX | CURSO BÁSICO DE MICRO PARTE 1 2019

Publicaciones relacionadas

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Botón volver arriba