¿Qué tan eficiente es el sistema de archivos virtuales procfs? ¿Es posible optimizarlo?

El sistema de archivos proc (en adelante, simplemente procfs) es un sistema de archivos virtual que proporciona información sobre los procesos. Ella es un "buen" ejemplo de interfaces que siguen el paradigma "todo es un archivo". Procfs se desarrolló hace mucho tiempo: en un momento en que los servidores atendían docenas de procesos en promedio, cuando abrir un archivo y leer la información del proceso no era un problema. Sin embargo, el tiempo no se detiene, y ahora los servidores sirven a cientos de miles, o incluso más procesos al mismo tiempo. En este contexto, la idea de "abrir un archivo para cada proceso con el fin de restar los datos de interés" ya no parece tan atractiva, y lo primero que viene a la mente para acelerar la lectura es obtener información sobre un grupo de procesos en una iteración. En este artículo, intentaremos encontrar elementos procfs que puedan optimizarse.


imagen


La idea misma de mejorar procfs surgió cuando descubrimos que CRIU pasa una cantidad considerable de tiempo solo leyendo archivos procfs. Vimos cómo se resolvió un problema similar para los sockets y decidimos hacer algo similar a la interfaz sock-diag, pero solo para procfs. Por supuesto, supusimos lo difícil que sería cambiar la interfaz de larga data y bien establecida en el núcleo, convencer a la comunidad de que el juego valía la pena ... y nos sorprendió gratamente la cantidad de personas que apoyaron la creación de la nueva interfaz. Hablando estrictamente, nadie sabía cómo debería ser la nueva interfaz, pero no hay duda de que procfs no cumple con los requisitos de rendimiento actuales. Por ejemplo, este escenario: el servidor responde a las solicitudes durante demasiado tiempo, vmstat muestra que la memoria se ha cambiado y "ps ax" se inicia desde 10 segundos o más, la parte superior no muestra nada en absoluto. En este artículo no consideraremos ninguna interfaz nueva específica, sino que trataremos de describir los problemas y sus soluciones.


Cada proceso de ejecución de procfs está representado por el directorio / proc / <pid> .
En cada uno de estos directorios hay muchos archivos y subdirectorios que proporcionan acceso a cierta información sobre el proceso. Los subdirectorios agrupan datos por característica. Por ejemplo ( $$ es una variable de shell especial que se expande en pid, el identificador del proceso actual):


 $ ls -F /proc/$$ attr/ exe@ mounts projid_map status autogroup fd/ mountstats root@ syscall auxv fdinfo/ net/ sched task/ cgroup gid_map ns/ schedstat timers clear_refs io numa_maps sessionid timerslack_ns cmdline limits oom_adj setgroups uid_map comm loginuid oom_score smaps wchan coredump_filter map_files/ oom_score_adj smaps_rollup cpuset maps pagemap stack cwd@ mem patch_state stat environ mountinfo personality statm 

Todos estos archivos generan datos en diferentes formatos. La mayoría están en formato de texto ASCII que los humanos perciben fácilmente. Bueno, casi fácil:


 $ cat /proc/$$/stat 24293 (bash) S 21811 24293 24293 34854 24876 4210688 6325 19702 0 10 15 7 33 35 20 0 1 0 47892016 135487488 3388 18446744073709551615 94447405350912 94447406416132 140729719486816 0 0 0 65536 3670020 1266777851 1 0 0 17 2 0 0 0 0 0 94447408516528 94447408563556 94447429677056 140729719494655 140729719494660 140729719494660 140729719496686 0 

Para comprender qué significa cada elemento de este conjunto, el lector tendrá que abrir man proc (5), o la documentación del kernel. Por ejemplo, el segundo elemento es el nombre del archivo ejecutable entre paréntesis, y el elemento decimonoveno es el valor actual de la prioridad de ejecución (agradable).


Algunos archivos son bastante legibles por sí mismos:


 $ cat /proc/$$/status | head -n 5 Name: bash Umask: 0002 State: S (sleeping) Tgid: 24293 Ngid: 0 

Pero, ¿con qué frecuencia los usuarios leen información directamente de los archivos procfs? ¿Cuánto tiempo necesita el núcleo para convertir datos binarios a formato de texto? ¿Cuál es la sobrecarga de procfs? ¿Qué tan conveniente es esta interfaz para los programas de monitoreo de estado y cuánto tiempo pasan para procesar estos datos de texto? ¿Qué tan crítica es una implementación tan lenta en situaciones de emergencia?


Lo más probable es que no sea un error decir que los usuarios prefieren programas como top o ps, en lugar de leer datos de procfs directamente.


Para responder las preguntas restantes, realizaremos varios experimentos. Primero, encuentre dónde el kernel pasa tiempo para generar archivos procfs.


Para obtener cierta información de todos los procesos en el sistema, tendremos que pasar por el directorio / proc / y seleccionar todos los subdirectorios cuyo nombre está representado por dígitos decimales. Luego, en cada uno de ellos, necesitamos abrir el archivo, leerlo y cerrarlo.


En total, haremos tres llamadas al sistema, una de las cuales creará un descriptor de archivo (en el núcleo, un descriptor de archivo está asociado con un conjunto de objetos internos para los que se asigna memoria adicional). Las llamadas al sistema open () y close () no nos proporcionan ninguna información, por lo que pueden atribuirse a la sobrecarga de la interfaz procfs.


Intentemos abrir () y cerrar () para cada proceso en el sistema, pero no leeremos el contenido de los archivos:


 $ time ./task_proc_all --noread stat tasks: 50290 real 0m0.177s user 0m0.012s sys 0m0.162s 

 $ time ./task_proc_all --noread loginuid tasks: 50289 real 0m0.176s user 0m0.026s sys 0m0.145 

task-proc-all: una pequeña utilidad, cuyo código se puede encontrar en el siguiente enlace


No importa qué archivo abrir, ya que los datos reales se generan solo en el momento de leer ().


Ahora mire la salida del perfilador de perf del núcleo:


 - 92.18% 0.00% task_proc_all [unknown] - 0x8000 - 64.01% __GI___libc_open - 50.71% entry_SYSCALL_64_fastpath - do_sys_open - 48.63% do_filp_open - path_openat - 19.60% link_path_walk - 14.23% walk_component - 13.87% lookup_fast - 7.55% pid_revalidate 4.13% get_pid_task + 1.58% security_task_to_inode 1.10% task_dump_owner 3.63% __d_lookup_rcu + 3.42% security_inode_permission + 14.76% proc_pident_lookup + 4.39% d_alloc_parallel + 2.93% get_empty_filp + 2.43% lookup_fast + 0.98% do_dentry_open 2.07% syscall_return_via_sysret 1.60% 0xfffffe000008a01b 0.97% kmem_cache_alloc 0.61% 0xfffffe000008a01e - 16.45% __getdents64 - 15.11% entry_SYSCALL_64_fastpath sys_getdents iterate_dir - proc_pid_readdir - 7.18% proc_fill_cache + 3.53% d_lookup 1.59% filldir + 6.82% next_tgid + 0.61% snprintf - 9.89% __close + 4.03% entry_SYSCALL_64_fastpath 0.98% syscall_return_via_sysret 0.85% 0xfffffe000008a01b 0.61% 0xfffffe000008a01e 1.10% syscall_return_via_sysret 

El kernel pasa casi el 75% del tiempo solo para crear y eliminar el descriptor de archivo, y aproximadamente el 16% para enumerar los procesos.


Aunque sabemos cuánto tiempo lleva abrir () y cerrar () las llamadas para cada proceso, aún no podemos estimar qué tan importante es. Necesitamos comparar los valores obtenidos con algo. Intentemos hacer lo mismo con los archivos más famosos. Por lo general, cuando necesita enumerar los procesos, se utiliza la utilidad ps o top. Ambos leen / proc / <pid> / stat y / proc / <pid> / status para cada proceso en el sistema.


Comencemos con / proc / <pid> / status: este es un archivo masivo con un número fijo de campos:


 $ time ./task_proc_all status tasks: 50283 real 0m0.455s user 0m0.033s sys 0m0.417s 

 - 93.84% 0.00% task_proc_all [unknown] [k] 0x0000000000008000 - 0x8000 - 61.20% read - 53.06% entry_SYSCALL_64_fastpath - sys_read - 52.80% vfs_read - 52.22% __vfs_read - seq_read - 50.43% proc_single_show - 50.38% proc_pid_status - 11.34% task_mem + seq_printf + 6.99% seq_printf - 5.77% seq_put_decimal_ull 1.94% strlen + 1.42% num_to_str - 5.73% cpuset_task_status_allowed + seq_printf - 5.37% render_cap_t + 5.31% seq_printf - 5.25% render_sigset_t 0.84% seq_putc 0.73% __task_pid_nr_ns + 0.63% __lock_task_sighand 0.53% hugetlb_report_usage + 0.68% _copy_to_user 1.10% number 1.05% seq_put_decimal_ull 0.84% vsnprintf 0.79% format_decode 0.73% syscall_return_via_sysret 0.52% 0xfffffe000003201b + 20.95% __GI___libc_open + 6.44% __getdents64 + 4.10% __close 

Se puede ver que solo alrededor del 60% del tiempo pasado dentro de la llamada al sistema read (). Si observa el perfil más de cerca, resulta que el 45% del tiempo se usa dentro de las funciones del núcleo seq_printf, seq_put_decimal_ull. Por lo tanto, la conversión de formato binario a texto es una operación bastante costosa. Lo que plantea la pregunta bien fundada: ¿realmente necesitamos una interfaz de texto para extraer datos del núcleo? ¿Con qué frecuencia los usuarios desean trabajar con datos sin procesar? ¿Y por qué las utilidades superior y ps tienen que convertir estos datos de texto a binario?


Probablemente sería interesante saber qué tan rápido sería la salida si los datos binarios se usaran directamente, y si no se requieren tres llamadas al sistema.


Ya ha habido intentos de crear dicha interfaz. En 2004 intentamos usar el motor netlink.


 [0/2][ANNOUNCE] nproc: netlink access to /proc information (https://lwn.net/Articles/99600/) nproc is an attempt to address the current problems with /proc. In short, it exposes the same information via netlink (implemented for a small subset). 

Desafortunadamente, la comunidad no ha mostrado mucho interés en este trabajo. Uno de los últimos intentos de rectificar la situación ocurrió hace dos años.


 [PATCH 0/15] task_diag: add a new interface to get information about processes (https://lwn.net/Articles/683371/) 

La interfaz task-diag se basa en los siguientes principios:


  • Transacción: envió una solicitud, recibió una respuesta;
  • El formato de los mensajes es en forma de netlink (lo mismo que la interfaz sock_diag: binario y extensible);
  • La capacidad de solicitar información sobre muchos procesos en una sola llamada;
  • Agrupación optimizada de atributos (cualquier atributo en el grupo no debe aumentar el tiempo de respuesta).

Esta interfaz ha sido presentada en varias conferencias. Se integró en pstools, utilidades CRIU y David Ahern integró task_diag en perf como experimento.


La comunidad de desarrollo del kernel se ha interesado en la interfaz task_diag. El tema principal de discusión fue la elección del transporte entre el núcleo y el espacio del usuario. La idea inicial de usar sockets de netlink fue rechazada. En parte debido a problemas no resueltos en el código del motor de netlink en sí, y en parte porque muchas personas piensan que la interfaz de netlink fue diseñada exclusivamente para el subsistema de red. Luego se propuso utilizar archivos transaccionales dentro de procfs, es decir, el usuario abre el archivo, escribe la solicitud en él y luego simplemente lee la respuesta. Como de costumbre, hubo opositores a este enfoque. Una solución que a todos les gustaría hasta encontrarla.


Comparemos el rendimiento de task_diag con procfs.


El motor task_diag tiene una utilidad de prueba que se adapta bien a nuestros experimentos. Supongamos que queremos solicitar identificadores de proceso y sus derechos. A continuación se muestra la salida para un proceso:


 $ ./task_diag_all one -c -p $$ pid 2305 tgid 2305 ppid 2299 sid 2305 pgid 2305 comm bash uid: 1000 1000 1000 1000 gid: 1000 1000 1000 1000 CapInh: 0000000000000000 CapPrm: 0000000000000000 CapEff: 0000000000000000 CapBnd: 0000003fffffffff 

Y ahora para todos los procesos en el sistema, es decir, lo mismo que hicimos para el experimento procfs cuando leemos el archivo / proc / pid / status:


 $ time ./task_diag_all all -c real 0m0.048s user 0m0.001s sys 0m0.046s 

Tomó solo 0.05 segundos obtener los datos para construir el árbol de procesos. Y con procfs, tomó 0.177 segundos solo abrir un archivo para cada proceso, y sin leer datos.


Salida de rendimiento para la interfaz task_diag:


 - 82.24% 0.00% task_diag_all [kernel.vmlinux] [k] entry_SYSCALL_64_fastpath - entry_SYSCALL_64_fastpath - 81.84% sys_read vfs_read __vfs_read proc_reg_read task_diag_read - taskdiag_dumpit + 33.84% next_tgid 13.06% __task_pid_nr_ns + 6.63% ptrace_may_access + 5.68% from_kuid_munged - 4.19% __get_task_comm 2.90% strncpy 1.29% _raw_spin_lock 3.03% __nla_reserve 1.73% nla_reserve + 1.30% skb_copy_datagram_iter + 1.21% from_kgid_munged 1.12% strncpy 

No hay nada interesante en el listado en sí, excepto por el hecho de que no hay funciones obvias adecuadas para la optimización.


Miremos la salida de rendimiento cuando leamos información sobre todos los procesos en el sistema:


  $ perf trace -s ./task_diag_all all -c -q Summary of events: task_diag_all (54326), 185 events, 95.4% syscall calls total min avg max stddev (msec) (msec) (msec) (msec) (%) --------------- -------- --------- --------- --------- --------- ------ read 49 40.209 0.002 0.821 4.126 9.50% mmap 11 0.051 0.003 0.005 0.007 9.94% mprotect 8 0.047 0.003 0.006 0.009 10.42% openat 5 0.042 0.005 0.008 0.020 34.86% munmap 1 0.014 0.014 0.014 0.014 0.00% fstat 4 0.006 0.001 0.002 0.002 10.47% access 1 0.006 0.006 0.006 0.006 0.00% close 4 0.004 0.001 0.001 0.001 2.11% write 1 0.003 0.003 0.003 0.003 0.00% rt_sigaction 2 0.003 0.001 0.001 0.002 15.43% brk 1 0.002 0.002 0.002 0.002 0.00% prlimit64 1 0.001 0.001 0.001 0.001 0.00% arch_prctl 1 0.001 0.001 0.001 0.001 0.00% rt_sigprocmask 1 0.001 0.001 0.001 0.001 0.00% set_robust_list 1 0.001 0.001 0.001 0.001 0.00% set_tid_address 1 0.001 0.001 0.001 0.001 0.00% 

Para procfs, necesitamos hacer más de 150,000 llamadas al sistema para obtener información sobre todos los procesos, y para task_diag, un poco más de 50.


Veamos situaciones de la vida real. Por ejemplo, queremos mostrar un árbol de procesos junto con argumentos de línea de comando para cada uno. Para hacer esto, necesitamos extraer el pid del proceso, el pid de su padre y los argumentos de la línea de comando.


Para la interfaz task_diag, el programa envía una solicitud para obtener todos los parámetros a la vez:


 $ time ./task_diag_all all --cmdline -q real 0m0.096s user 0m0.006s sys 0m0.090s 

Para los procesos originales, necesitamos leer / proc // status y / proc // cmdline para cada proceso:

 $ time ./task_proc_all status tasks: 50278 real 0m0.463s user 0m0.030s sys 0m0.427s 

 $ time ./task_proc_all cmdline tasks: 50281 real 0m0.270s user 0m0.028s sys 0m0.237s 

Es fácil notar que task_diag es 7 veces más rápido que procfs (0.096 contra 0.27 + 0.46). Por lo general, una mejora del rendimiento de varios por ciento ya es un buen resultado, pero aquí la velocidad ha aumentado en casi un orden de magnitud.


También vale la pena mencionar que la creación de objetos internos del núcleo también afecta en gran medida el rendimiento. Especialmente cuando el subsistema de memoria está bajo una gran carga. Compare el número de objetos creados para procfs y task_diag:


 $ perf trace --event 'kmem:*alloc*' ./task_proc_all status 2>&1 | grep kmem | wc -l 58184 $ perf trace --event 'kmem:*alloc*' ./task_diag_all all -q 2>&1 | grep kmem | wc -l 188 

Y también necesita saber cuántos objetos se crean al iniciar un proceso simple, por ejemplo, la verdadera utilidad:


 $ perf trace --event 'kmem:*alloc*' true 2>&1 | wc -l 94 

Procfs crea 600 veces más objetos que task_diag. Esta es una de las razones por las que procfs funciona tan mal cuando la carga de memoria es pesada. Al menos, por lo tanto, vale la pena optimizarlo.


Esperamos que el artículo atraiga a más desarrolladores para optimizar el estado de procfs del subsistema del kernel.


Muchas gracias a David Ahern, Andy Lutomirski, Stephen Hemming, Oleg Nesterov, W. Trevor King, Arnd Bergmann, Eric W. Biederman y muchos otros que ayudaron a desarrollar y mejorar la interfaz task_diag.


Gracias a cromer , k001 y Stanislav Kinsbursky por su ayuda en la redacción de este artículo.


Referencias


Source: https://habr.com/ru/post/es418715/


All Articles