I tuoi rebuild richiedono un'eternità con milioni di upload? Prova ad aggiungere questo template!

parallel-fs-ops.template.yml|allegato (2,8 KB)

Parliamo di un problema di scalabilità che diventa dolorosamente evidente una volta che un sito Discourse accumula una grande libreria di upload.

Ciò che prima richiedeva minuti per eseguire un comando chown su un enorme directory di upload ora richiede solo secondi!

Contesto

I rebuild di Discourse possono eseguire operazioni ricorsive come:

chown -R ...
chmod -R ...

Più specificamente, questa riga in templates/web.template.yml:

- chown -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

Questi comandi percorrono il file system in modo seriale, uno alla volta.

Questo è perfettamente ragionevole per un’installazione piccola. Ma quando shared/uploads contiene centinaia di migliaia — o milioni — di file, le operazioni ricorsive di proprietà e permessi possono dominare il deploy. La capacità di CPU, archiviazione e rete potrebbe essere disponibile, ma un singolo processo percorre l’intero albero un inode alla volta.

Per le community con molti upload, il risultato può essere:

  • Rebuild estremamente lunghi
  • Finestre di manutenzione più lunghe
  • Deploy e aggiornamenti di sicurezza ritardati
  • Scarsa utilizzazione di archiviazione veloce o distribuita
  • Un deploy che sembra bloccato mentre elabora un albero di file enorme
  • Prestazioni particolarmente problematiche su NFS, JuiceFS, CephFS e altri file system remoti

La parte frustrante è che molti di questi file sono indipendenti. I loro permessi possono essere elaborati in modo concorrente.

La Soluzione: Template per Operazioni Parallele sul File System

Ho creato un template pups che sostituisce in modo trasparente le operazioni ricorsive di chmod e chown con pipeline parallele di find e xargs.

I wrapper si annunciano ogni volta che intercettano un’operazione ricorsiva:

echo "[parallel-fs-ops] chmod -R override active: $*" >&2

e:

echo "[parallel-fs-ops] chown -R override active: $*" >&2

Quel prefisso tra parentesi quadre rende facile individuare l’ottimizzazione in un log di deploy.

Come appare un deploy

Vicino all’inizio del deploy, il template conferma quali binari gestiranno le successive operazioni sul file system:

[parallel-fs-ops] chmod -> /usr/local/bin/chmod
[parallel-fs-ops] chown -> /usr/local/bin/chown

Quando un template upstream esegue successivamente una modifica ricorsiva dei permessi, l’output del deploy include una riga simile a:

[parallel-fs-ops] chmod -R override active: -R 0755 /var/www/discourse/public

Una modifica ricorsiva della proprietà produce:

[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

Per un’installazione con molti upload, potresti vedere qualcosa di simile a:

[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp

I percorsi e gli argomenti esatti dipendono dai template utilizzati, ma la parte importante è il marcatore visibile:

[parallel-fs-ops]

Senza il template, il deploy potrebbe sembrare in pausa per un lungo periodo durante un’operazione ricorsiva sul file system. Con il template, il log ti dice che:

  1. Il wrapper è stato installato correttamente.
  2. È stata rilevata un’operazione ricorsiva.
  3. L’implementazione parallela è attiva.
  4. Gli argomenti originali in elaborazione sono visibili.

Questo è particolarmente prezioso durante la risoluzione dei problemi, poiché distingue una traversata parallela lenta del file system da un build bloccato.

Dopo la fine dell’operazione, la deployment continua con il suo normale output pups. Il wrapper stesso non stampa una riga per file, quindi anche un albero contenente milioni di upload non inonda il log di deploy.

Il template

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)"

Il template installa i wrapper in /usr/local/bin, che normalmente appare prima di /bin in PATH.

Quando viene richiesta un’operazione normale, non ricorsiva, il wrapper delega direttamente all’utilità standard:

exec /bin/chmod "$@"

Quando è presente -R, rimuove il flag ricorsivo, elenca i target in modo sicuro con delimitatori null e elabora i batch in modo concorrente:

find "${targets[@]}" -print0 |
  xargs -0 -n 32 -P 128 /bin/chmod "$mode"

Questo funziona anche quando pups invoca i comandi tramite /bin/sh. Lo shebang Bash del wrapper viene rispettato quando l’eseguibile viene avviato, anche se la shell chiamante è Dash.

Perché questo è più importante quando hai molti upload

Le community con molti upload sono esattamente il luogo in cui il comportamento del deploy deve scalare con grazia.

Un forum a lungo termine può contenere:

  • Immagini incorporate in anni di post
  • Avatar e sfondi del profilo
  • Varianti di immagini originali e ottimizzate
  • Allegati video e audio
  • Documenti e archivi
  • Upload sicuri
  • Media gestiti da plugin
  • Alberi di upload multi-sito

La quantità di codice applicativo può rimanere relativamente stabile mentre il numero di oggetti di file system caricati continua a crescere. La traversata del file system — non la compilazione o la creazione dei container — può diventare in definitiva il costo dominante del deploy.

Questo è un problema di scalabilità insolito: più la community diventa di successo e ricca di contenuti, più il lavoro operativo di routine può diventare costoso.

Perché è necessario un template

Cambiare .bashrc o impostare BASH_ENV non risolve affidabilmente questo problema. pups esegue i comandi run tramite /bin/sh, e Dash non carica la configurazione Bash né comprende le funzioni specifiche di Bash.

Un template fornisce un modo ripetibile per installare i wrapper abbastanza presto da garantire che le successive operazioni ricorsive — incluso quelle da template upstream — vengano risolte tramite l’implementazione parallela:

templates:
  - "templates/postgres.template.yml"
  - "templates/redis.template.yml"
  - "templates/web.template.yml"
  - "containers/parallel-fs-ops.template.yml"

Opzioni Configurabili

Opzioni di elaborazione parallela

Il template usa:

find "${targets[@]}" -print0 |
  xargs -0 -n 32 -P 128 /bin/chmod "$mode"

I parametri xargs rilevanti sono:

Opzione Scopo
-0 Legge percorsi delimitati da null prodotti da find -print0. Questo gestisce in modo sicuro nomi di file contenenti spazi, virgolette, tab o a capo.
-n 32 Passa al massimo 32 percorsi a ciascuna invocazione di chmod o chown. Questa è la dimensione del batch.
-P 128 Consente fino a 128 processi chmod o chown da eseguire in modo concorrente. Questo è il livello di parallelismo.

Insieme, -n 32 -P 128 significa che fino a 128 processi possono essere eseguiti simultaneamente, con ciascun processo che gestisce un batch di fino a 32 percorsi. Pertanto, circa 4.096 percorsi potrebbero essere attivamente distribuiti tra i batch di comandi in una volta.

Scelta di -n

-n controlla quanta lavoro viene assegnato a ciascun comando:

  • Valori più bassi forniscono una distribuzione del lavoro più fine ma avviano più processi.
  • Valori più alti riducono l’overhead di avvio dei processi ma creano batch più grandi e meno uniformemente distribuiti.
  • -n 1 esegue un comando chmod o chown per percorso.
  • -n 32 è un punto di partenza ragionevole per bilanciare batch e parallelismo.
  • Valori molto grandi potrebbero ridurre l’efficacia di -P perché vengono creati meno batch totali.

Scelta di -P

-P controlla quanti comandi possono essere eseguiti contemporaneamente:

  • Valori più bassi riducono il carico sulla CPU e sul file system.
  • Valori più alti possono migliorare le prestazioni su archiviazione veloce o distribuita.
  • Un parallelismo eccessivo può sovraccaricare i dischi, saturare un server di metadati o peggiorare le prestazioni.
  • -P 1 è effettivamente un’esecuzione seriale.
  • -P 8 o -P 16 è un punto di partenza conservativo.
  • -P 32 potrebbe adattarsi a un’archiviazione basata su SSD veloci.
  • -P 128 dovrebbe essere usato solo quando il file system e l’host possono sostenere quella concorrenza.

I valori migliori dipendono dalla latenza del file system, dalle prestazioni dei metadati, dalla capacità della CPU e dal numero di file. Entrambi dovrebbero idealmente essere configurabili e benchmarkati per l’installazione specifica.

Troppa parallelismo può sovraccaricare un file system, saturare i server di metadati o degradare le prestazioni del deploy. La dimensione del batch e la concorrenza dovrebbero quindi essere configurabili.

Questo template è un workaround pratico, ma la proposta più ampia è più vasta:

Discourse potrebbe supportare ufficialmente un parallelismo configurabile per grandi operazioni ricorsive sul file system durante i deploy?

Un’implementazione upstream potrebbe:

  • Parallelizzare solo noti alberi di directory grandi
  • Evitare di attraversare inutilmente alberi di upload invariati
  • Rendere la concorrenza configurabile
  • Rilevare file system locali rispetto a quelli basati su rete
  • Preservare la semantica completa degli argomenti di chmod e chown
  • Emettere progressi periodici per alberi molto grandi
  • Registrare i tempi in modo che gli amministratori possano identificare i colli di bottiglia del deploy

Importante avvertenza

Il wrapper sopra è focalizzato sulle forme ricorsive dei comandi utilizzate dal nostro processo di build. Non è una completa reimplementazione di ogni possibile combinazione di opzioni di chmod o chown.

Dovrebbe essere testato contro i comandi esatti generati dai template di un sito prima dell’uso in produzione. Gli operatori dovrebbero iniziare con un parallelismo conservativo e misurare l’effetto sulla loro archiviazione.

Ma il problema sottostante è reale: le operazioni ricorsive seriali dei metadati non scalano bene quando una community ha accumulato un albero di upload massiccio.

In bocca al lupo, e apprezzo qualsiasi commento o suggerimento (anche se forse ho duplicato gli sforzi di qualcun altro, apprezzerei anche dei riferimenti a quello)!

Ciao!

2 Mi Piace