parallel-fs-ops.template.yml|pièce jointe (2,8 Ko)
Parlons d’un problème d’évolutivité qui devient douloureusement évident dès qu’un site Discourse accumule une grande bibliothèque de fichiers téléversés.
Ce qui prenait des minutes pour exécuter une commande chown sur un immense répertoire de fichiers téléversés ne prend désormais que quelques secondes !
Contexte
Les reconstructions (rebuilds) de Discourse peuvent exécuter des opérations récursives telles que :
chown -R ...
chmod -R ...
Plus précisément, cette ligne dans templates/web.template.yml :
- chown -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Ces commandes parcourent le système de fichiers de manière séquentielle, un par un.
C’est parfaitement raisonnable pour une petite installation. Mais lorsque shared/uploads contient des centaines de milliers — voire des millions — de fichiers, les opérations récursives de propriété et de permissions peuvent dominer le déploiement. La capacité CPU, de stockage et de réseau peut être disponible, mais un seul processus parcourt l’arborescence entière un inode à la fois.
Pour les communautés à forte intensité de téléversements, le résultat peut être :
- Des reconstructions extrêmement longues
- Des fenêtres de maintenance plus longues
- Des déploiements et mises à jour de sécurité retardés
- Une mauvaise utilisation des stockages rapides ou distribués
- L’impression qu’un déploiement est bloqué pendant qu’il traite une arborescence de fichiers énorme
- Des performances particulièrement pénibles sur NFS, JuiceFS, CephFS et d’autres systèmes de fichiers distants
L’aspect frustrant est que beaucoup de ces fichiers sont indépendants. Leurs permissions peuvent être traitées de manière concurrente.
La solution : Modèle d’opérations parallèles sur le système de fichiers
J’ai créé un modèle pups qui remplace de manière transparente les opérations récursives chmod et chown par des pipelines parallèles find et xargs.
Les wrappers (gourmands) s’annoncent chaque fois qu’ils interceptent une opération récursive :
echo "[parallel-fs-ops] chmod -R override active: $*" >&2
et :
echo "[parallel-fs-ops] chown -R override active: $*" >&2
Ce préfixe entre crochets facilite l’identification de l’optimisation dans le journal de déploiement.
À quoi ressemble un déploiement
Au début du déploiement, le modèle confirme quels binaires géreront les opérations ultérieures sur le système de fichiers :
[parallel-fs-ops] chmod -> /usr/local/bin/chmod
[parallel-fs-ops] chown -> /usr/local/bin/chown
Lorsqu’un modèle amont exécute ultérieurement un changement de permissions récursif, la sortie du déploiement inclut une ligne similaire à :
[parallel-fs-ops] chmod -R override active: -R 0755 /var/www/discourse/public
Un changement de propriété récursif produit :
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Pour une installation à forte intensité de téléversements, vous pourriez voir quelque chose de ressemblant à :
[parallel-fs-ops] chown -R override active: -R discourse:www-data /shared/log/rails /shared/uploads /shared/backups /shared/tmp
Les chemins et arguments exacts dépendent des modèles utilisés, mais la partie importante est le marqueur visible :
[parallel-fs-ops]
Sans le modèle, le déploiement peut sembler se mettre en pause longtemps pendant une opération récursive sur le système de fichiers. Avec le modèle, le journal vous indique que :
- Le wrapper a été installé correctement.
- Une opération récursive a été détectée.
- L’implémentation parallèle est active.
- Les arguments d’origine en cours de traitement sont visibles.
Ceci est particulièrement précieux lors du dépannage car cela distingue un parcours parallèle lent du système de fichiers d’une construction bloquée.
Après la fin de l’opération, le déploiement se poursuit avec sa sortie pups normale. Le wrapper lui-même n’imprime pas une ligne par fichier, donc même une arborescence contenant des millions de téléversements ne noie pas le journal de déploiement.
Le modèle
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)"
Le modèle installe des wrappers dans /usr/local/bin, qui apparaît normalement avant /bin dans le PATH.
Lorsqu’une opération normale, non récursive, est demandée, le wrapper délègue directement vers l’utilitaire standard :
exec /bin/chmod "$@"
Lorsque -R est présent, il supprime le drapeau récursif, énumère les cibles en toute sécurité avec des délimiteurs null et traite les lots de manière concurrente :
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
Ceci fonctionne également lorsque pups invoque des commandes via /bin/sh. Le shebang Bash du wrapper est respecté lorsque l’exécutable est lancé, même si le shell appelant est Dash.
Pourquoi c’est le plus important lorsque vous avez beaucoup de téléversements
Les communautés à forte intensité de téléversements sont exactement l’endroit où le comportement du déploiement doit être évolutif de manière élégante.
Un forum de longue date peut contenir :
- Des images intégrées sur des années de publications
- Des avatars et fonds d’écran de profil
- Des variantes d’images originales et optimisées
- Des pièces jointes vidéo et audio
- Des documents et archives
- Des téléversements sécurisés
- Des médias gérés par des plugins
- Des arborescences de téléversements multi-sites
La quantité de code applicatif peut rester relativement stable tandis que le nombre d’objets de système de fichiers téléversés continue de croître. Le parcours du système de fichiers — et non la compilation ou la création de conteneurs — peut finir par devenir le coût dominant du déploiement.
C’est un problème d’évolutivité inhabituel : plus la communauté devient réussie et riche en contenu, plus les travaux opérationnels de routine peuvent devenir coûteux.
Pourquoi un modèle est nécessaire
Modifier .bashrc ou définir BASH_ENV ne résout pas fiablement ce problème. pups exécute les commandes run via /bin/sh, et Dash ne charge pas la configuration Bash ni ne comprend les fonctions spécifiques à Bash.
Un modèle fournit un moyen répétable d’installer les wrappers assez tôt pour que les opérations récursives ultérieures — y compris celles provenant de modèles amont — soient résolues via l’implémentation parallèle :
templates:
- "templates/postgres.template.yml"
- "templates/redis.template.yml"
- "templates/web.template.yml"
- "containers/parallel-fs-ops.template.yml"
Options configurables
Options de traitement parallèle
Le modèle utilise :
find "${targets[@]}" -print0 |
xargs -0 -n 32 -P 128 /bin/chmod "$mode"
Les paramètres xargs pertinents sont :
| Option | But |
|---|---|
-0 |
Lit les chemins délimités par null produits par find -print0. Cela gère en toute sécurité les noms de fichiers contenant des espaces, des guillemets, des tabulations ou des sauts de ligne. |
-n 32 |
Passe au plus 32 chemins à chaque invocation de chmod ou chown. C’est la taille du lot. |
-P 128 |
Permet jusqu’à 128 processus chmod ou chown de s’exécuter simultanément. C’est le niveau de parallélisme. |
Ensemble, -n 32 -P 128 signifie que jusqu’à 128 processus peuvent s’exécuter simultanément, chaque processus traitant un lot de 32 chemins au maximum. Environ 4 096 chemins peuvent donc être activement répartis entre les lots de commandes à la fois.
Choix de -n
-n contrôle la quantité de travail attribuée à chaque commande :
- Les valeurs plus basses offrent une distribution du travail plus fine mais lancent plus de processus.
- Les valeurs plus élevées réduisent le surcoût de lancement des processus mais créent des lots plus grands et moins uniformément répartis.
-n 1exécute une commandechmodouchownpar chemin.-n 32est un point de départ raisonnable pour équilibrer le lot et le parallélisme.- Les très grandes valeurs peuvent réduire l’efficacité de
-Pcar moins de lots totaux sont créés.
Choix de -P
-P contrôle combien de commandes peuvent s’exécuter en même temps :
- Les valeurs plus basses réduisent la charge sur le CPU et le système de fichiers.
- Les valeurs plus élevées peuvent améliorer les performances sur les stockages rapides ou distribués.
- Un parallélisme excessif peut submerger les disques, saturer un serveur de métadonnées ou aggraver les performances.
-P 1est une exécution séquentielle effective.-P 8ou-P 16est un point de départ prudent.-P 32peut convenir à un stockage basé sur SSD rapide.-P 128ne devrait être utilisé que lorsque le système de fichiers et l’hôte peuvent soutenir cette concurrence.
Les meilleures valeurs dépendent de la latence du système de fichiers, des performances de métadonnées, de la capacité CPU et du nombre de fichiers. Les deux devraient idéalement être configurables et benchmarkés pour l’installation spécifique.
Trop de parallélisme peut submerger un système de fichiers, saturer les serveurs de métadonnées ou dégrader les performances du déploiement. La taille des lots et la concurrence devraient donc être configurables.
Ce modèle est une solution de contournement pratique, mais la proposition plus large est plus vaste :
Discourse pourrait-il officiellement prendre en charge un parallélisme configurable pour les grandes opérations récursives sur le système de fichiers lors des déploiements ?
Une implémentation amont pourrait :
- Paralléliser uniquement les arborescences de répertoires connues et volumineuses
- Éviter de parcourir inutilement les arborescences de téléversements inchangées
- Rendre la concurrence configurable
- Détecter les systèmes de fichiers locaux par rapport aux systèmes de fichiers basés sur le réseau
- Préserver la sémantique complète des arguments
chmodetchown - Émettre des mises à jour périodiques de progression pour les très grandes arborescences
- Enregistrer les durées pour que les administrateurs puissent identifier les goulets d’étranglement du déploiement
Avertissement important
Le wrapper ci-dessus se concentre sur les formes de commandes récursives utilisées par notre processus de construction. Ce n’est pas une réimplémentation complète de toutes les combinaisons d’options possibles de chmod ou chown.
Il doit être testé contre les commandes exactes générées par les modèles d’un site avant une utilisation en production. Les opérateurs devraient commencer avec un parallélisme prudent et mesurer l’effet sur leur stockage.
Mais le problème sous-jacent est réel : les opérations métadonnées récursives séquentielles ne sont pas évolutives lorsqu’une communauté a accumulé une arborescence de téléversements massive.
Bonne chance, et j’apprécie tout commentaire ou suggestion (même si j’ai peut-être dupliqué les efforts de quelqu’un d’autre, j’apprécierais des indications à ce sujet aussi) !
Cordialement !