Qual a eficiência do sistema de arquivos virtual procfs e é possível otimizá-lo

O sistema de arquivos proc (a seguir simplesmente procfs) é um sistema de arquivos virtual que fornece informações sobre processos. Ela é um exemplo "fino" de interfaces, seguindo o paradigma "tudo é um arquivo". O Procfs foi desenvolvido há muito tempo: em um momento em que os servidores atendiam dezenas de processos em média, ao abrir um arquivo e ler as informações do processo não era um problema. No entanto, o tempo não pára e agora os servidores atendem a centenas de milhares ou mais processos ao mesmo tempo. Nesse contexto, a idéia de “abrir um arquivo para cada processo para subtrair os dados de interesse” não parece mais tão atraente, e a primeira coisa que vem à mente para acelerar a leitura é obter informações sobre um grupo de processos em uma iteração. Neste artigo, tentaremos encontrar elementos procfs que podem ser otimizados.


imagem


A própria idéia de melhorar o procfs surgiu quando descobrimos que o CRIU gasta uma quantidade considerável de tempo apenas lendo os arquivos procfs. Vimos como um problema semelhante foi resolvido para soquetes e decidimos fazer algo semelhante à interface sock-diag, mas apenas para procfs. É claro que assumimos o quão difícil seria mudar a interface antiga e bem estabelecida no kernel, convencer a comunidade de que o jogo valia a pena ... e ficamos agradavelmente surpreendidos com o número de pessoas que apoiaram a criação da nova interface. A rigor, ninguém sabia como deveria ser a nova interface, mas não há dúvida de que o procfs não atende aos requisitos de desempenho atuais. Por exemplo, este cenário: o servidor responde a solicitações por muito tempo, vmstat mostra que a memória entrou em uma troca e "ps ax" é iniciado a partir de 10 segundos ou mais, top não mostra nada. Neste artigo, não consideraremos nenhuma nova interface específica; tentaremos descrever os problemas e suas soluções.


Cada processo procfs em execução é representado pelo diretório / proc / <pid> .
Em cada diretório, existem muitos arquivos e subdiretórios que fornecem acesso a determinadas informações sobre o processo. Os subdiretórios agrupam dados por recurso. Por exemplo ( $$ é uma variável de shell especial que é expandida em pid - o identificador do processo atual):


 $ 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 esses arquivos geram dados em diferentes formatos. A maioria está no formato de texto ASCII que é facilmente percebido pelos seres humanos. Bem, quase 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 entender o que cada elemento deste conjunto significa, o leitor terá que abrir o man proc (5) ou a documentação do kernel. Por exemplo, o segundo elemento é o nome do arquivo executável entre colchetes, e o décimo nono elemento é o valor atual da prioridade de execução (nice).


Alguns arquivos são bastante legíveis por eles mesmos:


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

Mas com que frequência os usuários leem informações diretamente dos arquivos procfs? Quanto tempo o kernel precisa converter dados binários em formato de texto? Qual é a sobrecarga dos procfs? Qual é a conveniência dessa interface para os programas de monitoramento de status e quanto tempo eles gastam para processar esses dados de texto? Quão crítica é uma implementação tão lenta em situações de emergência?


Provavelmente, não será um erro dizer que os usuários preferem programas como top ou ps, em vez de ler dados diretamente do procfs.


Para responder às perguntas restantes, realizaremos várias experiências. Primeiro, encontre onde o kernel gasta tempo para gerar arquivos procfs.


Para obter certas informações de todos os processos do sistema, teremos que passar pelo diretório / proc / e selecionar todos os subdiretórios cujo nome é representado por dígitos decimais. Então, em cada um deles, precisamos abrir o arquivo, lê-lo e fechá-lo.


No total, faremos três chamadas de sistema, uma das quais criará um descritor de arquivo (no kernel, um descritor de arquivo está associado a um conjunto de objetos internos para os quais a memória adicional é alocada). As chamadas de sistema open () e close () não fornecem nenhuma informação, portanto podem ser atribuídas à sobrecarga da interface procfs.


Vamos apenas tentar abrir () e fechar () para cada processo no sistema, mas não leremos o conteúdo dos arquivos:


 $ 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 - um pequeno utilitário, cujo código pode ser encontrado no link abaixo


Não importa qual arquivo abrir, pois dados reais são gerados somente no momento da leitura ().


Agora observe a saída do perf core profiler:


 - 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 

O kernel passa quase 75% do tempo apenas para criar e excluir o descritor de arquivo e cerca de 16% para listar os processos.


Embora saibamos quanto tempo leva para as chamadas de abertura () e fechamento () de cada processo, ainda não podemos estimar quão significativo é. Precisamos comparar os valores obtidos com alguma coisa. Vamos tentar fazer o mesmo com os arquivos mais famosos. Geralmente, quando você precisa listar os processos, o utilitário ps ou top é usado. Ambos lêem / proc / <pid> / stat e / proc / <pid> / status para cada processo no sistema.


Vamos começar com / proc / <pid> / status - este é um arquivo enorme com um número fixo 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 

Pode-se observar que apenas cerca de 60% do tempo gasto na chamada do sistema read (). Se você observar o perfil mais de perto, verifica-se que 45% do tempo é usado dentro das funções do kernel seq_printf, seq_put_decimal_ull. Portanto, a conversão do formato binário para o texto é uma operação bastante cara. O que suscita a pergunta bem fundamentada: realmente precisamos de uma interface de texto para extrair dados do kernel? Com que frequência os usuários desejam trabalhar com dados brutos? E por que os utilitários top e ps têm que converter esses dados de texto novamente em binários?


Provavelmente seria interessante saber quanto mais rápido a saída seria se os dados binários fossem usados ​​diretamente e se três chamadas do sistema não fossem necessárias.


Já houve tentativas de criar essa interface. Em 2004, tentamos usar o mecanismo 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). 

Infelizmente, a comunidade não demonstrou muito interesse neste trabalho. Uma das últimas tentativas de corrigir a situação ocorreu há dois anos.


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

A interface task-diag é baseada nos seguintes princípios:


  • Transação: enviou uma solicitação, recebeu uma resposta;
  • O formato das mensagens está na forma de netlink (o mesmo que a interface sock_diag: binária e extensível);
  • A capacidade de solicitar informações sobre muitos processos em uma chamada;
  • Agrupamento otimizado de atributos (qualquer atributo do grupo não deve aumentar o tempo de resposta).

Essa interface foi apresentada em várias conferências. Foi integrado ao pstools, aos utilitários CRIU e David Ahern integrou task_diag ao perf como um experimento.


A comunidade de desenvolvimento do kernel se interessou pela interface task_diag. O assunto principal da discussão foi a escolha do transporte entre o kernel e o espaço do usuário. A idéia inicial de usar soquetes netlink foi rejeitada. Em parte devido a problemas não resolvidos no código do mecanismo netlink e em parte porque muitas pessoas pensam que a interface netlink foi projetada exclusivamente para o subsistema de rede. Em seguida, foi proposto o uso de arquivos transacionais dentro do procfs, ou seja, o usuário abre o arquivo, grava a solicitação nele e, em seguida, simplesmente lê a resposta. Como sempre, havia oponentes dessa abordagem. Uma solução que todos gostariam até que fosse encontrada.


Vamos comparar o desempenho de task_diag com procfs.


O mecanismo task_diag possui um utilitário de teste adequado para nossos experimentos. Suponha que desejemos solicitar identificadores de processo e seus direitos. Abaixo está a saída para um processo:


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

E agora, para todos os processos no sistema, ou seja, a mesma coisa que fizemos no experimento procfs quando lemos o arquivo / proc / pid / status:


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

Foram necessários apenas 0,05 segundos para obter os dados para construir a árvore do processo. E com o procfs, foram necessários 0,177 segundos para abrir um arquivo para cada processo e sem a leitura dos dados.


Saída Perf para a 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 

Não há nada interessante na listagem em si, exceto pelo fato de não haver funções óbvias adequadas para otimização.


Vejamos a saída perf ao ler informações sobre todos os processos no 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, precisamos fazer mais de 150.000 chamadas de sistema para obter informações sobre todos os processos e para task_diag - um pouco mais de 50.


Vamos olhar para situações da vida real. Por exemplo, queremos exibir uma árvore de processos junto com argumentos de linha de comando para cada um. Para fazer isso, precisamos extrair o pid do processo, o pid de seu pai e os argumentos da linha de comando.


Para a interface task_diag, o programa envia uma solicitação para obter todos os parâmetros de uma vez:


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

Para os procfs originais, precisamos ler / proc // status e / proc // cmdline para cada processo:

 $ 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 

É fácil notar que task_diag é 7 vezes mais rápido que procfs (0,096 contra 0,27 + 0,46). Normalmente, uma melhoria de desempenho de vários por cento já é um bom resultado, mas aqui a velocidade aumentou quase uma ordem de magnitude.


Também vale a pena mencionar que a criação de objetos internos do kernel também afeta muito o desempenho. Especialmente quando o subsistema de memória está sob carga pesada. Compare o número de objetos criados para procfs e 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 

E também é necessário descobrir quantos objetos são criados ao iniciar um processo simples, por exemplo, o verdadeiro utilitário:


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

O Procfs cria 600 vezes mais objetos que task_diag. Essa é uma das razões pelas quais o procfs funciona tão mal quando a carga da memória é pesada. Pelo menos, portanto, vale a pena otimizá-lo.


Esperamos que o artigo atraia mais desenvolvedores para otimizar o estado procfs do subsistema do kernel.


Muito obrigado a David Ahern, Andy Lutomirski, Stephen Hemming, Oleg Nesterov, W. Trevor King, Arnd Bergmann, Eric W. Biederman e muitos outros que ajudaram a desenvolver e melhorar a interface task_diag.


Agradecemos a cromer , k001 e Stanislav Kinsbursky pela ajuda na redação deste artigo.


Referências


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


All Articles