Quelle est l'efficacité du système de fichiers virtuel procfs et est-il possible de l'optimiser

Le système de fichiers proc (ci-après simplement procfs) est un système de fichiers virtuel qui fournit des informations sur les processus. Elle est un «bel» exemple d'interfaces suivant le paradigme «tout est un fichier». Procfs a été développé il y a très longtemps: à une époque où les serveurs desservaient en moyenne des dizaines de processus, l'ouverture d'un fichier et la lecture des informations de processus ne posaient pas de problème. Cependant, le temps ne s'arrête pas, et maintenant les serveurs servent des centaines de milliers, voire plus de processus en même temps. Dans ce contexte, l'idée «d'ouvrir un fichier pour chaque processus afin de soustraire les données d'intérêt» ne semble plus aussi attrayante, et la première chose qui vient à l'esprit pour accélérer la lecture est d'obtenir des informations sur un groupe de processus en une seule itération. Dans cet article, nous allons essayer de trouver des éléments procfs pouvant être optimisés.


image


L'idée même d'améliorer procfs est née lorsque nous avons découvert que CRIU passe beaucoup de temps à lire les fichiers procfs. Nous avons vu comment un problème similaire a été résolu pour les sockets et avons décidé de faire quelque chose de similaire à l'interface sock-diag, mais uniquement pour procfs. Bien sûr, nous avons supposé combien il serait difficile de changer l'interface de longue date et bien établie dans le noyau, pour convaincre la communauté que le jeu en valait la chandelle ... et nous avons été agréablement surpris par le nombre de personnes qui ont soutenu la création de la nouvelle interface. À strictement parler, personne ne savait à quoi devrait ressembler la nouvelle interface, mais il ne fait aucun doute que procfs ne répond pas aux exigences de performances actuelles. Par exemple, ce scénario: le serveur répond aux demandes depuis trop longtemps, vmstat montre que la mémoire est passée en permutation et "ps ax" est démarré à partir de 10 secondes ou plus, top n'affiche rien du tout. Dans cet article, nous ne considérerons aucune nouvelle interface spécifique; nous essaierons plutôt de décrire les problèmes et leurs solutions.


Chaque processus procfs en cours d'exécution est représenté par le répertoire / proc / <pid> .
Dans chacun de ces répertoires, il existe de nombreux fichiers et sous-répertoires qui donnent accès à certaines informations sur le processus. Les sous-répertoires regroupent les données par fonctionnalité. Par exemple ( $$ est une variable shell spéciale qui est développée en pid - l'identifiant du processus en cours):


 $ 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 

Tous ces fichiers produisent des données dans différents formats. La plupart sont en texte au format ASCII facilement perceptible par l'homme. Eh bien, presque facile:


 $ 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 

Pour comprendre ce que signifie chaque élément de cet ensemble, le lecteur devra ouvrir man proc (5), ou la documentation du noyau. Par exemple, le deuxième élément est le nom du fichier exécutable entre crochets, et le dix-neuvième élément est la valeur actuelle de la priorité d'exécution (sympa).


Certains fichiers sont assez lisibles par eux-mêmes:


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

Mais à quelle fréquence les utilisateurs lisent-ils les informations directement à partir des fichiers procfs? De combien de temps le noyau a-t-il besoin pour convertir des données binaires au format texte? Quelle est la surcharge de procfs? Dans quelle mesure cette interface est-elle pratique pour les programmes de surveillance d'état et combien de temps passent-ils pour traiter ces données de texte? Quelle est l'importance d'une mise en œuvre si lente dans les situations d'urgence?


Très probablement, ce ne sera pas une erreur de dire que les utilisateurs préfèrent des programmes comme top ou ps, au lieu de lire directement les données de procfs.


Pour répondre aux questions restantes, nous allons mener plusieurs expériences. Tout d'abord, trouvez où le noyau passe du temps à générer des fichiers procfs.


Pour obtenir certaines informations de tous les processus du système, nous devrons passer par le répertoire / proc / et sélectionner tous les sous-répertoires dont le nom est représenté par des chiffres décimaux. Ensuite, dans chacun d'eux, nous devons ouvrir le fichier, le lire et le fermer.


Au total, nous ferons trois appels système, dont l'un créera un descripteur de fichier (dans le noyau, un descripteur de fichier est associé à un ensemble d'objets internes pour lesquels de la mémoire supplémentaire est allouée). Les appels système open () et close () eux-mêmes ne nous donnent aucune information, ils peuvent donc être attribués à la surcharge de l'interface procfs.


Essayons simplement d'ouvrir () et de fermer () pour chaque processus du système, mais nous ne lirons pas le contenu des fichiers:


 $ 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 - un petit utilitaire, dont le code peut être trouvé sur le lien ci-dessous


Peu importe le fichier à ouvrir, car les données réelles ne sont générées qu'au moment de la lecture ().


Regardez maintenant la sortie du profileur perf core:


 - 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 

Le noyau passe presque 75% du temps juste pour créer et supprimer le descripteur de fichier, et environ 16% pour lister les processus.


Bien que nous sachions combien de temps il faut pour les appels open () et close () pour chaque processus, nous ne pouvons toujours pas estimer son importance. Nous devons comparer les valeurs obtenues avec quelque chose. Essayons de faire de même avec les fichiers les plus connus. Habituellement, lorsque vous devez répertorier les processus, l'utilitaire ps ou top est utilisé. Ils lisent / proc / <pid> / stat et / proc / <pid> / status pour chaque processus du système.


Commençons par / proc / <pid> / status - il s'agit d'un fichier massif avec un nombre fixe de champs:


 $ 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 

On peut voir que seulement environ 60% du temps passé à l'intérieur de l'appel système read (). Si vous regardez le profil de plus près, il s'avère que 45% du temps est utilisé à l'intérieur des fonctions du noyau seq_printf, seq_put_decimal_ull. Ainsi, la conversion du format binaire au format texte est une opération assez coûteuse. Ce qui pose la question bien fondée: avons-nous vraiment besoin d'une interface texte pour extraire les données du noyau? À quelle fréquence les utilisateurs souhaitent-ils travailler avec des données brutes? Et pourquoi les utilitaires top et ps doivent-ils reconvertir ces données texte en binaire?


Il serait probablement intéressant de savoir à quel point la sortie serait plus rapide si les données binaires étaient utilisées directement et si trois appels système n'étaient pas nécessaires.


Il y a déjà eu des tentatives de création d'une telle interface. En 2004, nous avons essayé d'utiliser le moteur 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). 

Malheureusement, la communauté n'a pas montré beaucoup d'intérêt pour ce travail. L'une des dernières tentatives pour corriger la situation a eu lieu il y a deux ans.


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

L'interface task-diag est basée sur les principes suivants:


  • Transaction: envoyé une demande, reçu une réponse;
  • Le format des messages est sous forme de netlink (le même que l'interface sock_diag: binaire et extensible);
  • La possibilité de demander des informations sur de nombreux processus en un seul appel;
  • Regroupement optimisé des attributs (tout attribut du groupe ne doit pas augmenter le temps de réponse).

Cette interface a été présentée lors de plusieurs conférences. Il a été intégré dans pstools, les utilitaires CRIU et David Ahern a intégré task_diag dans perf comme expérience.


La communauté de développement du noyau s'est intéressée à l'interface task_diag. Le principal sujet de discussion a été le choix du transport entre le noyau et l'espace utilisateur. L'idée initiale d'utiliser des sockets netlink a été rejetée. En partie à cause de problèmes non résolus dans le code du moteur netlink lui-même, et en partie parce que beaucoup de gens pensent que l'interface netlink a été conçue exclusivement pour le sous-système réseau. Ensuite, il a été proposé d'utiliser des fichiers transactionnels dans procfs, c'est-à-dire que l'utilisateur ouvre le fichier, y écrit la requête, puis lit simplement la réponse. Comme d'habitude, il y avait des opposants à cette approche. Une solution que tout le monde souhaiterait jusqu'à ce qu'elle soit trouvée.


Comparons les performances de task_diag avec procfs.


Le moteur task_diag dispose d'un utilitaire de test qui convient bien à nos expériences. Supposons que nous voulons demander des identificateurs de processus et leurs droits. Voici la sortie pour un processus:


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

Et maintenant, pour tous les processus du système, c'est-à-dire la même chose que nous avons faite pour l'expérience procfs lorsque nous lisons le fichier / proc / pid / status:


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

Il n'a fallu que 0,05 seconde pour obtenir les données pour construire l'arborescence des processus. Et avec procfs, il n'a fallu que 0.177 secondes pour ouvrir un fichier pour chaque processus, et sans lire les données.


Sortie Perf pour l'interface 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 

Il n'y a rien d'intéressant dans la liste elle-même, sauf qu'il n'y a pas de fonctions évidentes adaptées à l'optimisation.


Examinons la sortie perf lors de la lecture des informations sur tous les processus du système:


  $ 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% 

Pour procfs, nous devons effectuer plus de 150 000 appels système pour obtenir des informations sur tous les processus, et pour task_diag - un peu plus de 50.


Regardons les situations réelles. Par exemple, nous voulons afficher un arbre de processus avec des arguments de ligne de commande pour chacun. Pour ce faire, nous devons extraire le pid du processus, le pid de son parent et les arguments de ligne de commande eux-mêmes.


Pour l'interface task_diag, le programme envoie une requête pour obtenir tous les paramètres à la fois:


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

Pour les procfs d'origine, nous devons lire / proc // status et / proc // cmdline pour chaque processus:

 $ 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 

Il est facile de remarquer que task_diag est 7 fois plus rapide que procfs (0,096 contre 0,27 + 0,46). Habituellement, une amélioration des performances de plusieurs pour cent est déjà un bon résultat, mais ici, la vitesse a augmenté de près d'un ordre de grandeur.


Il convient également de mentionner que la création d'objets de noyau internes affecte également considérablement les performances. Surtout lorsque le sous-système de mémoire est sous forte charge. Comparez le nombre d'objets créés pour procfs et 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 

Et vous devez également savoir combien d'objets sont créés lors du démarrage d'un processus simple, par exemple, le véritable utilitaire:


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

Procfs crée 600 fois plus d'objets que task_diag. C'est l'une des raisons pour lesquelles procfs fonctionne si mal lorsque la charge mémoire est lourde. Il convient donc au moins de l'optimiser.


Nous espérons que l'article attirera plus de développeurs pour optimiser l'état procfs du sous-système du noyau.


Un grand merci à David Ahern, Andy Lutomirski, Stephen Hemming, Oleg Nesterov, W. Trevor King, Arnd Bergmann, Eric W. Biederman et bien d'autres qui ont aidé à développer et à améliorer l'interface task_diag.


Merci à cromer , k001 et Stanislav Kinsbursky pour l'aide à la rédaction de cet article.


Les références


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


All Articles