parallel-fs-ops.template.yml (2,8 KB)
Vamos falar sobre um problema de escala que se torna dolorosamente óbvio assim que um site do Discourse acumula uma grande biblioteca de uploads.
O que antes levava minutos para o comando chown ser executado em um diretório de uploads enorme agora leva segundos!
Contexto
Reconstruções do Discourse podem executar operações recursivas como:
chown -R ...
chmod -R ...
Mais especificamente, esta linha em templates/web.template.yml:
- chown -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Estes comandos percorrem o sistema de arquivos de forma serial, um por um.
Isso é perfeitamente razoável para uma instalação pequena. Mas quando shared/uploads contém centenas de milhares — ou milhões — de arquivos, operações recursivas de propriedade e permissão podem dominar o tempo de implantação. Pode haver capacidade disponível de CPU, armazenamento e rede, mas um único processo percorre toda a árvore um inode de cada vez.
Para comunidades com muitos uploads, o resultado pode ser:
- Reconstruções extremamente longas
- Janelas de manutenção mais longas
- Atrasos em implantações e atualizações de segurança
- Pobre utilização de armazenamento rápido ou distribuído
- Uma implantação parecendo travada enquanto processa uma árvore de arquivos enorme
- Desempenho particularmente ruim em NFS, JuiceFS, CephFS e outros sistemas de arquivos remotos
A parte frustrante é que muitos desses arquivos são independentes. Suas permissões podem ser processadas em paralelo.
A Solução: Modelo de Operações Paralelas de Sistema de Arquivos
Criei um modelo de pups que substitui transparentemente as operações recursivas de chmod e chown por pipelines paralelos de find e xargs.
Os wrappers se anunciam sempre que interceptam uma operação recursiva:
echo "[parallel-fs-ops] chmod -R override active: $*" >&2
e:
echo "[parallel-fs-ops] chown -R override active: $*" >&2
Esse prefixo entre colchetes torna a otimização fácil de identificar em um log de implantação.
Como uma implantação parece
No início da implantação, o modelo confirma quais binários lidarão com as operações de sistema de arquivos posteriores:
[parallel-fs-ops] chmod -> /usr/local/bin/chmod
[parallel-fs-ops] chown -> /usr/local/bin/chown
Quando um modelo upstream executa posteriormente uma mudança de permissão recursiva, a saída da implantação inclui uma linha semelhante a:
[parallel-fs-ops] chmod -R override active: -R 0755 /var/www/discourse/public
Uma mudança de propriedade recursiva produz:
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Para uma instalação com muitos uploads, você pode ver algo semelhante a:
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Os caminhos exatos e os argumentos dependem dos modelos em uso, mas a parte importante é o marcador visível:
[parallel-fs-ops]
Sem o modelo, a implantação pode parecer pausada por um longo tempo durante uma operação recursiva de sistema de arquivos. Com o modelo, o log informa que:
- O wrapper foi instalado corretamente.
- Uma operação recursiva foi detectada.
- A implementação paralela está ativa.
- Os argumentos originais sendo processados são visíveis.
Isso é particularmente valioso durante a solução de problemas, pois distingue uma travessia paralela lenta do sistema de arquivos de uma construção travada.
Após a conclusão da operação, a implantação continua com sua saída normal do pups. O próprio wrapper não imprime uma linha por arquivo, portanto, mesmo uma árvore contendo milhões de uploads não inunda o log de implantação.
O modelo
run:
- file:
path: /usr/local/bin/chmod
chmod: "+x"
contents: |
#!/bin/bash
if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
echo "[parallel-fs-ops] chmod -R override active: $*" >&2
args=()
for arg in "$@"; do
[[ "$arg" != "-R" ]] && args+=("$arg")
done
mode="${args[0]}"
targets=("${args[@]:1}")
[[ ${#targets[@]} -eq 0 ]] && targets=(".")
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
else
exec /bin/chmod "$@"
fi
- file:
path: /usr/local/bin/chown
chmod: "+x"
contents: |
#!/bin/bash
if [[ "$*" =~ (^|[[:space:]])-R([[:space:]]|$) ]]; then
echo "[parallel-fs-ops] chown -R override active: $*" >&2
args=()
for arg in "$@"; do
[[ "$arg" != "-R" ]] && args+=("$arg")
done
owner="${args[0]}"
targets=("${args[@]:1}")
[[ ${#targets[@]} -eq 0 ]] && targets=(".")
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chown "$owner"
else
exec /bin/chown "$@"
fi
- exec:
cmd: |
echo "[parallel-fs-ops] chmod -> $(command -v chmod)"
echo "[parallel-fs-ops] chown -> $(command -v chown)"
O modelo instala wrappers em /usr/local/bin, que normalmente aparece antes de /bin no PATH.
Quando uma operação normal, não recursiva, é solicitada, o wrapper delega diretamente para a utilidade padrão:
exec /bin/chmod "$@"
Quando -R está presente, ele remove a flag recursiva, enumera os alvos com segurança usando delimitadores nulos e processa lotes em paralelo:
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
Isso também funciona quando o pups invoca comandos através de /bin/sh. O shebang Bash do wrapper é respeitado quando o executável é iniciado, mesmo que o shell chamador seja Dash.
Por que isso é mais importante quando você tem muitos uploads
Comunidades com muitos uploads são exatamente onde o comportamento da implantação precisa escalar de forma graciosa.
Um fórum de longa duração pode conter:
- Imagens incorporadas em anos de publicações
- Avatares e fundos de perfil
- Variantes de imagem originais e otimizadas
- Anexos de vídeo e áudio
- Documentos e arquivos compactados
- Uploads seguros
- Mídia gerenciada por plugins
- Árvores de upload de multisite
A quantidade de código da aplicação pode permanecer relativamente estável, enquanto o número de objetos de sistema de arquivos enviados continua a crescer. A travessia do sistema de arquivos — e não a compilação ou a criação de contêineres — pode eventualmente se tornar o custo dominante da implantação.
Este é um problema de escala incomum: quanto mais bem-sucedida e rica em conteúdo a comunidade se torna, mais caro o trabalho operacional de rotina pode se tornar.
Por que um modelo é necessário
Alterar o .bashrc ou definir BASH_ENV não resolve isso de forma confiável. O pups executa comandos run através de /bin/sh, e o Dash nem carrega a configuração do Bash nem entende funções específicas do Bash.
Um modelo fornece uma maneira repetível de instalar os wrappers cedo o suficiente para que as operações recursivas subsequentes — incluindo aquelas de modelos upstream — sejam resolvidas através da implementação paralela:
templates:
- "templates/postgres.template.yml"
- "templates/redis.template.yml"
- "templates/web.template.yml"
- "containers/parallel-fs-ops.template.yml"
Opções Configuráveis
Opções de processamento paralelo
O modelo usa:
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
Os parâmetros relevantes do xargs são:
| Opção | Propósito |
|---|---|
-0 |
Lê caminhos delimitados por nulo produzidos por find -print0. Isso lida com segurança com nomes de arquivos contendo espaços, aspas, abas ou quebras de linha. |
-n 32 |
Passa no máximo 32 caminhos para cada invocação de chmod ou chown. Este é o tamanho do lote. |
-P 128 |
Permite que até 128 processos de chmod ou chown sejam executados em paralelo. Este é o nível de paralelismo. |
Juntos, -n 32 -P 128 significam que até 128 processos podem ser executados simultaneamente, com cada processo processando um lote de até 32 caminhos. Aproximadamente 4.096 caminhos podem, portanto, estar ativamente distribuídos entre lotes de comandos de uma só vez.
Escolhendo -n
-n controla quanta trabalho é atribuído a cada comando:
- Valores menores fornecem uma distribuição de trabalho mais fina, mas iniciam mais processos.
- Valores maiores reduzem a sobrecarga de lançamento de processos, mas criam lotes maiores e menos uniformemente distribuídos.
-n 1executa um comandochmodouchownpor caminho.-n 32é um ponto de partida razoável para equilibrar lote e paralelismo.- Valores muito grandes podem reduzir a eficácia de
-Pporque menos lotes totais são criados.
Escolhendo -P
-P controla quantos comandos podem ser executados ao mesmo tempo:
- Valores menores reduzem a carga na CPU e no sistema de arquivos.
- Valores maiores podem melhorar o desempenho em armazenamento rápido ou distribuído.
- Paralelismo excessivo pode sobrecarregar discos, saturar um servidor de metadados ou piorar o desempenho.
-P 1é efetivamente execução serial.-P 8ou-P 16é um ponto de partida conservador.-P 32pode ser adequado para armazenamento baseado em SSD rápido.-P 128deve ser usado apenas quando o sistema de arquivos e o host podem sustentar essa concorrência.
Os melhores valores dependem da latência do sistema de arquivos, do desempenho de metadados, da capacidade da CPU e do número de arquivos. Idealmente, ambos devem ser configuráveis e benchmarkados para a instalação específica.
Paralelismo excessivo pode sobrecarregar um sistema de arquivos, saturar servidores de metadados ou degradar o desempenho da implantação. O tamanho do lote e a concorrência devem, portanto, ser configuráveis.
Este modelo é uma solução prática, mas a proposta maior é mais ampla:
O Discourse poderia suportar oficialmente paralelismo configurável para grandes operações recursivas de sistema de arquivos durante implantações?
Uma implementação upstream poderia:
- Paralelizar apenas árvores de diretórios grandes conhecidas
- Evitar percorrer árvores de uploads inalteradas desnecessariamente
- Tornar a concorrência configurável
- Detectar sistemas de arquivos locais versus baseados em rede
- Preservar a semântica completa dos argumentos de
chmodechown - Emitir progresso periódico para árvores muito grandes
- Registrar tempos para que administradores possam identificar gargalos de implantação
Importante advertência
O wrapper acima está focado nas formas recursivas de comando usadas pelo nosso processo de build. Ele não é uma reimplementação completa de todas as possíveis combinações de opções de chmod ou chown.
Ele deve ser testado contra os comandos exatos gerados pelos modelos de um site antes do uso em produção. Operadores devem começar com paralelismo conservador e medir o efeito em seu armazenamento.
Mas o problema subjacente é real: operações recursivas de metadados serializadas não escalam bem quando uma comunidade acumulou uma árvore de uploads massiva.
Boa sorte, e agradeço qualquer comentário ou sugestão (mesmo que eu tenha duplicado os esforços de outra pessoa, apreciaria ponteiros para isso também)!
Abraços!