Libseccomp: RFE: seccomp_rule_add ficou muito lento desde a v2.4.0

Criado em 1 mai. 2019  ·  23Comentários  ·  Fonte: seccomp/libseccomp

Olá,
o problema foi introduzido pelo commit ce3dda9a1747cc6a4c044eafe5a2eb653c974919 entre v2.3.3 e v2.4.0. Considere o seguinte exemplo: foo.c.zip .
Ele adiciona um número muito grande de regras. E funciona cerca de 100 vezes mais devagar após o commit mencionado acima.

foo.c tempo de execução usando v2.4.1: 0.448
foo.c tempo de execução usando v2.3.3: 0.077

Eu cavei um pouco e descobri que db_col_transaction_start() copia a coleção de filtros já existente e usa arch_filter_rule_add() para duplicar as regras de filtro. Mas arch_filter_rule_add() chama arch_syscall_translate() que chama arch_syscall_resolve_name() que funciona em O(número de syscalls na arquitetura dada). Portanto, adicionar uma regra funciona pelo menos em O (número de regras já adicionadas * número de syscalls em arquiteturas usadas), o que IMO é muito ruim.
Eu contei o número de chamadas para arch_filter_rule_add() no exemplo acima e é igual a 201152 .

Antes desse commit, o número de chamadas para arch_filter_rule_add() era 896 . E pelo que entendi do código, também db_col_transaction_start() copia a coleção de filtros já existente e não usa arch_filter_rule_add(). O que nos dá uma estimativa: tempo de adicionar uma regra em torno de O(número de regras já adicionadas + número de syscalls na arquitetura fornecida), o que é muito melhor.

No entanto, IMO não deve estar relacionado ao número de regras já adicionadas, porque adicionar n regras funciona em O(n^2) então. Mas isso é um tópico para uma discussão diferente, então não deve ser um problema para filtros pequenos ou filtros gerados com pouca frequência.

Por que esse problema importa?
Alguns filtros precisam executar programas PID (por exemplo, permitindo que a thread envie sinais apenas para ela mesma). Portanto, se o programa restrito precisa ser executado um número considerável de vezes, torna-se uma sobrecarga muito visível. Eu tenho um filtro de cerca de 300 regras e a sobrecarga de libseccomp é de cerca de 0,16s por execução do processo em área restrita (eu executo o processo dezenas de vezes).

Agradeço antecipadamente por sua ajuda!

enhancement prioritlow

Comentários muito úteis

Estamos vendo os tempos limite dos usuários devido a essa alteração. Isso realmente desacelerou as coisas em uma ordem de magnitude.

Todos 23 comentários

Olá @varqox.

Sim, as funções do resolvedor syscall podem usar algumas melhorias, de fato, se você observar o código, verá vários comentários como os seguintes:

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

Se você quiser melhorar esse código, podemos usar a ajuda!

Como o @pcmoore mencionou, existem amplas oportunidades para acelerar a _criação_ de um filtro seccomp usando libseccomp. Sua pesquisa acima delineou uma das várias áreas que poderiam ser melhoradas. Isso não tem sido uma preocupação para meus usuários, então não me concentrei nisso.

Com relação ao desempenho do _runtime_, atualmente estou trabalhando no uso de uma árvore binária para filtros grandes como o que você forneceu em foo.c. Os resultados iniciais com meus clientes internos parecem promissores, mas eu adoraria ter outra visão das mudanças. Veja solicitação de pull https://github.com/seccomp/libseccomp/pull/152

OK, vejo que a resolução de syscall pode ser melhorada, mas não é a causa raiz do problema. Que é, a meu ver, a criação de um snapshot no db_col_transaction_start() . Lá arch_filter_rule_add() é chamado, o que é lento devido à resolução da syscall, que é resolvida na regra original.

Eu vejo da seguinte forma: Queremos duplicar todo o conjunto de filtros atuais (aka struct db_filter) com todas as suas regras, então _construímos_ todos os filtros do zero ao invés de aproveitar o que já temos e apenas _copiar_ todos os filtros. Não temos que construir do zero, temos um filtro de construção completo do qual queremos apenas uma cópia. Talvez eu tenha perdido alguma coisa, mas parece que muitas melhorias podem ser feitas na função db_col_transaction_start().

Com todo o estado na coleção libseccomp db interna, duplicá-la não é uma tarefa trivial, regenerar a coleção a partir das regras originais é muito mais fácil (de uma perspectiva de código). Manter o controle das regras originais também nos permite oferecer a capacidade de "remover" uma regra existente (possível recurso futuro).

Isso não quer dizer que o código de transação não possa ser melhorado - definitivamente pode - mas o código atual é do jeito que é por um motivo, principalmente simplicidade.

Estamos vendo os tempos limite dos usuários devido a essa alteração. Isso realmente desacelerou as coisas em uma ordem de magnitude.

Outro pensamento, provavelmente podemos mudar isso para que apenas dupliquemos as regras em uma transação iniciada, não a árvore inteira, e apenas recriemos a árvore em uma transação com falha. Não é perfeito, mas isso deve voltar uma boa parte do tempo.

Precisamos fazer algo porque os tempos de início dos contêineres e processos exec estão vendo uma enorme regressão de desempenho e fazendo com que as pessoas fixem em 2,3x

Não vou comentar mais sobre a natureza _"enorme"_ do problema, essa perspectiva já foi feita várias vezes e a considero relativa e dependente do caso de uso. No entanto, eu queria lembrar a todos que as versões do libseccomp anteriores à v2.4 são vulneráveis ​​a uma potencial vulnerabilidade que se tornou pública (edição #139).

Para aqueles que estão preocupados com esse problema, ele está marcado para uma versão v2.5.

Você fez uma refatoração e isso tem impactos "enormes" no desempenho em uma versão menor e você não está sendo útil ao descartar isso dizendo que é dependente do caso de uso. Por favor, leve isso a sério, pois as pessoas vão começar a perceber que as distros serão atualizadas para 2.4

@crosbymichael a mudança não foi simplesmente uma refatoração, foi necessário corrigir problemas e suportar as alterações no kernel (mais notavelmente a necessidade de suportar syscalls multiplexadas e diretas, por exemplo, syscalls de soquete em x86 de 32 bits).

Eu _não_ estou ignorando isso, continuei pensando em maneiras de resolver esse problema (veja meus comentários acima) e o fato de ter marcado isso como algo para o próximo lançamento menor. Neste ponto é difícil para mim não perceber seus comentários como inflamatórios, se essa não for sua intenção, sugiro ter mais cuidado ao comentar no futuro. Se você estiver insatisfeito com o progresso desse problema, sempre poderá ajudar enviando um patch/PR para revisão.

Nota para si mesmo e para quem mais está pensando em tentar resolver isso ...

Recentemente, lembrei-me de por que fazemos o que fazemos em relação às transações (copiar tudo antecipadamente); fazemos isso porque precisamos ser capazes de reverter uma transação sem falhar. Por quê?
Uma operação normal de seccomp_rule_add() precisa manter o filtro intacto mesmo em caso de falha; se falharmos em uma transação de várias partes (por exemplo, soquete/ipc syscalls em x86/s390/s390x/etc.) como parte de uma adição de regra normal, DEVEMOS ser capazes de reverter para o filtro no início da transação sem falhar ( independentemente da pressão da memória, etc.).

Duplicar a árvore sem as regras continuará sendo um desafio devido à natureza da árvore e à vinculação dentro da árvore, mas podemos escolher seletivamente quando precisamos criar uma transação interna, pulando-a para os muitos casos em que não é necessário.

Passei um pouco mais de tempo analisando isso e, devido à maneira como modificamos destrutivamente a árvore de decisão durante uma adição de regra, não tenho certeza se podemos evitar envolver as adições de regra com uma transação. Isso significa que, em vez de encontrar maneiras de limitar nosso uso de transações internamente, precisamos encontrar uma maneira de acelerá-las, felizmente acho que encontrei uma solução: árvores de sombra.

Atualmente, construímos uma nova árvore cada vez que criamos uma nova transação e a descartamos com sucesso, o que, como vimos, pode ser proibitivamente lento em alguns casos de uso. Meu pensamento é que, em vez de descartar a árvore duplicada no commit, tentamos adicionar a regra que acabamos de adicionar à árvore duplicada (tornando-a uma cópia do filtro atual) e mantê-la como uma "transação sombra" para acelerar a próxima instantâneo da transação. Algumas notas:

  • db_col_transaction_start() deve tentar usar a transação de sombra se estiver presente, mas se não, deve retornar ao comportamento atual.
  • db_col_transaction_abort() deve se comportar da mesma forma que agora; isso significa que uma transação com falha limpará a transação de sombra (é necessário fazer uma árvore para restaurar o filtro), mas a próxima transação bem-sucedida restaurará a sombra. Uma transação com falha deve ser rara o suficiente para que isso não seja um problema importante.
  • Podemos precisar limpar a transação de sombra em outras operações, por exemplo, operações de arch/ABI?, mas isso é algo que precisamos verificar. Independentemente disso, limpar a transação de sombra deve ser trivial.
  • Isso tem a vantagem de não apenas acelerar a adição de regras, mas também acelerar as transações em geral. Isso pode não ser significativo agora, mas será útil quando expormos a funcionalidade da transação aos usuários (isso seria necessário se quisermos fazer um mecanismo do tipo "compromisso" do BSD).

Eu tive algum tempo depois do jantar hoje à noite, então fiz um passo rápido na implementação da ideia de transação de sombra acima. O código ainda é bruto, e meus testes (abaixo) ainda mais brutos, mas parece que estamos vendo alguns ganhos de desempenho com essa abordagem:

  • Sobrecarga do teste de linha de base
# time for i in {0..20000}; do /bin/true; done
real    0m10.479s
user    0m7.641s
sys     0m3.924s
  • Ramo mestre atual
# time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

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

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

Se subtrairmos a sobrecarga do teste, veremos um aumento de aproximadamente 20% no desempenho neste "teste", mas espero que o benefício para conjuntos de filtros complexos seja melhor (muito melhor?) do que isso.

@varqox e/ou @crosbymichael assim que eu limpar um pouco os patches e criar um PR, você poderia testar isso em seu ambiente?

Meu caso de teste de exemplo já está aqui:

Olá,
o problema foi introduzido pelo commit ce3dda9 entre v2.3.3 e v2.4.0. Considere o seguinte exemplo: foo.c.zip .
Ele adiciona um número muito grande de regras. E funciona cerca de 100 vezes mais devagar após o commit mencionado acima.

foo.c tempo de execução usando v2.4.1: 0.448
foo.c tempo de execução usando v2.3.3: 0.077

Mas assim que o PR estiver pronto, posso testá-lo em meu ambiente.

Oi @varqox , sim, vi que você incluiu um caso de teste no relatório original, mas estou mais interessado em saber como ele funciona em uso real . Se você pudesse experimentar o PR #180 e relatar eu realmente aprecio isso - obrigado!

Olá @pcmore ,

Obrigado por fazer este PR.
Eu construí e testei seu PR #180, o resultado é promissor para o meu caso de teste. Eu observo esse problema porque os clientes usam a verificação de integridade do docker e sofreram com o problema de desempenho em libseccomp 2.4.x .
No meu caso de teste, o desempenho deste PR é comparável a libseccomp 2.3.3 . Os detalhes são como abaixo:

Ambiente

Ubuntu 19.04 VM (2 CPUs, 2G de memória) no MacBook Pro (15 polegadas, meados de 2015)
Kernel 5.0.0-32-genérico
Docker CE 19.03.2

Caso de teste:

Prepare 20 recipientes:

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

Execute o teste disparando docker exec em todos os contêineres ao mesmo tempo

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

Sua construção de relações 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 diversas

  • Eu coloquei 2.4.1 em AC_INIT em configure.ac antes de construir este PR.
  • Esta compilação personalizada está instalada em /usr/local/lib , eu a verifiquei executando ldd /usr/bin/runc para garantir que a compilação personalizada esteja sendo usada durante o teste.
  • Eu executei o teste algumas vezes, existem variações muito pequenas nos resultados. Portanto, estou confiante de que não são resultados acidentais.

Isso é ótimo, obrigado pela ajuda @xinfengliu!

Olá @pcmore ,
Obrigado por este PR.
No meu caso, ele restaura o desempenho da libseccomp para um nível comparável com a 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

Média: 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

Média: 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

Média: 0.0126 s

Parece que este PR dá alguma aceleração sobre v2.3.3 neste teste sintético.

Construindo e carregando (de seccomp_init() para seccomp_load()) filtros no meu sandbox mais alguma sobrecarga de inicialização do 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

Média: 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

Média: 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

Média: 0.0049 s

Embora no teste sintético PR dê melhores tempos do que v2.3.3, no mundo real é um pouco mais lento (possivelmente por causa de regras mais complicadas e rodando seccomp_merge() para mesclar dois filtros grandes). No entanto, ainda oferece uma aceleração de aproximadamente dez vezes em relação à v2.4.2.

Obrigado por verificar o desempenho @varqox! Assim que @drakenclimber responder à última rodada de comentários (e eu corrigir quaisquer problemas restantes que ele possa trazer), vamos combinar isso.

Ah, deixa pra lá, acabei de notar que @drakenclimber marcou o PR como aprovado. Eu vou em frente e mesclar isso agora.

Acabei de mesclar o PR #180, então acho que podemos marcar isso como fechado, se alguém notar algum problema de desempenho restante, sinta-se à vontade para comentar e/ou reabrir. Obrigado a todos pela paciência e ajuda!

@pcmoore você planeja lançar em breve com essas mudanças?

Isso atualmente faz parte do marco de lançamento do libseccomp v2.5, você pode acompanhar nosso progresso em relação ao lançamento v2.5 usando o link abaixo:

Esta página foi útil?
0 / 5 - 0 avaliações