Journal [Btrfs et openSUSE] Épisode 5 : les quotas

Posté par  (site web personnel) . Licence CC By‑SA.
30
23
sept.
2017

Sommaire

Btrfs is hard

« Btrfs et openSUSE » est une série de journaux sur le système de fichiers Btrfs, basée sur ma propre expérience d'utilisateur d'openSUSE. Au menu :

  • des généralités sur Btrfs
  • des noms qui pètent : sous-volumes, snapshots, rollbacks
  • du snapper
  • du grub
  • de la mise à jour incrémentale
  • des quotas
  • de la maintenance
  • des trucs spécifiques à openSUSE
  • des tentatives désespérées pour rester applicable à d'autres distributions
  • des erreurs (pas taper)
  • des bugs
  • des cris
  • des larmes
  • et bien plus ! ou bien moins

Aujourd'hui, l'épisode 5 : les quotas.

Btrfs et les quotas : intro

Les quotas, qu'est-ce que c'est ?

Un quota, dans le contexte d'un système de fichiers, c'est une limite imposée à un ou plusieurs utilisateurs sur la quantité de données qu'il(s) peu(ven)t conserver dans un ou plusieurs emplacements de l'arborescence.

Typiquement, cela est utile dans le cas d'un système multi-utilisateurs, afin de garantir un partage équitable des ressources.

Les qgroups

Btrfs n'utilise pas la définition traditionnelle des quotas. Pour lui, un quota ne s'applique pas directement à un utilisateur mais plutôt à un sous-volume (par exemple @/home/toto) ou à un groupe de sous-volumes (par exemple @/home/toto/ + @/home/toto/.snapshots).

On parle alors de qgroup pour exprimer ces ensembles limités en taille. Ou plutôt en taille​s.

En effet, l'utilisation du Copy-on-Write fait que des fichiers situés dans des sous-volumes différents – et donc comptabilisés dans des qgroups différents – peuvent partager la même donnée au niveau bloc, par exemple suite à la création d'un instantané.

Dans ce cas, à quel sous-volume appliquer une limite ? Au sous-volume d'origine ? À tous les sous-volumes ?

Btrfs résout ce problème en permettant de fixer des limites pour deux types de taille :

  • la taille totale des données référencées par ce qgroup, potentiellement partagées avec d'autres qgroups ;
  • la taille des données exclusives à ce qgroup, non partagées avec d'autres qgroups.

Jouons avec les quotas

Activer les quotas : btrfs quota

wake up

Sur un système de fichiers fraîchement créé, les quotas sont désactivés.

La commande btrfs quota permet de les activer :

~# btrfs quota
usage: btrfs quota <command> [options] <path>

    btrfs quota enable <path>
        Enable subvolume quota support for a filesystem.
    btrfs quota disable <path>
        Disable subvolume quota support for a filesystem.
    btrfs quota rescan [-sw] <path>
        Trash all qgroup numbers and scan the metadata again with the current config.

manage filesystem quota settings
~# btrfs quota enable /
~#

Lors de la première activation, Btrfs va automatiquement lancer une réanalyse des quotas et créer des qgroups pour les sous-volumes existants.

Gérer les qgroups : btrfs qgroup

Afficher les qgroups : show

Tout simplement :

~# btrfs qgroup show /
qgroupid         rfer         excl 
--------         ----         ---- 
0/5          16.00KiB     16.00KiB 
0/257         2.36MiB      2.36MiB 
0/258        16.00KiB     16.00KiB 
0/259        16.00KiB     16.00KiB 
0/260         1.23MiB      1.23MiB 
0/261        12.58MiB     12.58MiB 
0/262        16.00KiB     16.00KiB 
0/263        16.00KiB     16.00KiB 
0/264        16.00KiB     16.00KiB 
0/265        16.00KiB     16.00KiB 
0/266        16.00KiB     16.00KiB 
0/267       403.54MiB    403.54MiB 
0/268        16.00KiB     16.00KiB 
0/269       340.00KiB    340.00KiB 
0/270         2.16GiB      2.16GiB 
0/275        16.00KiB     16.00KiB 
0/2779       16.00KiB     16.00KiB 
0/5692        1.23GiB      1.23GiB 
0/6046       16.00KiB     16.00KiB 
0/6047        8.03GiB    248.00KiB 
0/6048        8.03GiB    276.00KiB 
0/6049        8.03GiB    160.33MiB 
0/6050        8.03GiB    424.00KiB 
0/6051        8.03GiB     30.09MiB 
~#

Ici, on voit les qgroups qui ont été créés automatiquement lors de l'activation des quotas. Chaque sous-volume est rangé dans un qgroup, 0/x ou x est l'identifiant du sous-volume. On parle de qgroup de niveau 0.

Deux autres colonnes sont présentes :

  • rfer, qui représente la quantité de données que référence le qgroup.
  • excl, qui représente la quantité de données exclusive à ce qgroup.

Par exemple, pour le sous-volume 6051 :

~# btrfs subvolume list / | grep 6051
ID 6051 gen 447643 top level 275 path @/.snapshots/101/snapshot # c'est un instantané
~# btrfs qgroup show / | awk 'NR <= 2 || /6051/'
qgroupid         rfer         excl 
--------         ----         ---- 
0/6051        8.03GiB     30.11MiB 
~#

Le qgroup associé 0/6051 référence 8,03 Gio de données dont 30,11 Mio ne sont pas partagées avec d'autres qgroups (donc d'autres sous-volumes).

C'est beaucoup plus rapide de connaître la taille d'un sous-volume entier avec btrfs qgroup show, qui garde le compte de la quantité de données présentes, qu'avec btrfs filesystem du, qui doit faire le compte à chaque appel.

Enfin, il est possible d'utiliser des options pour détailler l'affichage :

  • Les options -r et -e permettent de rajouter deux autres colonnes pour afficher les limites de tailles appliquées. Par défaut, aucune limite n'est appliquée.
  • Les options -p et -c permettent d'afficher les liens de parenté entre les qgroups.
~# btrfs qgroup show -pcre / | awk 'NR <= 2 || /6051/'
qgroupid         rfer         excl     max_rfer     max_excl parent   child
--------         ----         ----     --------     -------- ------   -----
0/6051        8.03GiB     30.11MiB         none         none ---      ---
~#

Supprimer des qgroups : destroy

Si les quotas sont activés, un nouveau qgroup 0/x est créé automatiquement à la création d'un sous-volume/snapshot avec l'ID x. Cependant, aucun qgroup n'est détruit à la suppression d'un sous-volume. Cela se voit très bien après la suppression de quelques instantanés :

~# btrfs subvolume delete 6063 /
~# btrfs subvolume delete 6064 /
~# btrfs subvolume delete 6065 /
~# btrfs qgroup show / | awk 'NR <= 2 || /606/'
qgroupid         rfer         excl 
--------         ----         ---- 
0/6063          0.00B        0.00B 
0/6064          0.00B        0.00B 
0/6065          0.00B        0.00B 
~#

Il est possible de supprimer ces qgroups vides avec btrfs qgroup destroy :

~# btrfs qgroup destroy 0/6063 /
~# btrfs qgroup destroy 0/6064 /
~# btrfs qgroup destroy 0/6065 /
~#

La suppression automatique de ces qgroups de niveau 0 sera peut-être implémentée prochainement. En attendant, on peut utiliser snapper qui se charge de ce nettoyage.

Créer des hiérarchies de qgroups : create et assign

Un qgroup est un groupe de sous-volumes…

hiérarchie

… ou un groupe de qgroups.

En effet, on n'est pas limité à un seul niveau on peut avoir un arbre de qgroups !

                          +---+
                          |2/1|
                          +---+
                         /     \
                   +---+/       \+---+
                   |1/1|         |1/2|
                   +---+         +---+
                  /     \       /     \
            +---+/       \+---+/       \+---+
qgroups     |0/1|         |0/2|         |0/3|
            +-+-+         +---+         +---+
              |          /     \       /     \
              |         /       \     /       \
              |        /         \   /         \
extents       1       2            3            4

La commande pour assigner un qgroup à un qgroup parent est la suivante :

~# btrfs qgroup assign --help
usage: btrfs qgroup assign [options] <src> <dst> <path>

    Assign SRC as the child qgroup of DST

    --rescan       schedule qutoa rescan if needed
    --no-rescan    don't schedule quota rescan

Du coup, pour créer un arbre similaire à celui représenté plus haut :

~# # qgroups de niveau 1
~# btrfs qgroup create 1/1 /
~# btrfs qgroup create 1/2 /
~#
~# # qgroup de niveau 2
~# btrfs qgroup create 2/1 /
~#
~# # Assignation
~# btrfs qgroup assign 1/1 2/1 /
~# btrfs qgroup assign 1/2 2/1 /

Cela n'a pas de sens de créer les qgroups de niveau 0 à la main, vu qu'ils correspondent directement à un sous-volume et que Btrfs les crée pour nous.

Il est cependant possible de combiner la création d'un sous-volume à l'assignation à un qgroup parent :

~# # L'option -i ("inherit") est aussi disponible sur btrfs subvolume snapshot
~# btrfs subvolume create -i 1/1 /a
~# btrfs subvolume create -i 1/1 -i 1/2 /b
~# btrfs subvolume create -i 1/2 /c

Et voilà le résultat !

~# btrfs qgroup show -pc / | awk 'NR <= 2 || /1\/[12]/'
qgroupid         rfer         excl parent   child
--------         ----         ---- ------   -----
0/6119       16.00KiB     16.00KiB 1/1      ---
0/6120       16.00KiB     16.00KiB 1/1,1/2  ---
0/6121       16.00KiB     16.00KiB 1/2      ---
1/1          32.00KiB     32.00KiB 2/1      0/6119,0/6120
1/2          32.00KiB     32.00KiB 2/1      0/6120,0/6121
2/1          48.00KiB     48.00KiB ---      1/1,1/2
~#

Fixer des limites à un qgroup : limit

limites

Voilà, voilà, on y arrive : il est possible de mettre des limites aux tailles d'un qgroup. Et c'est heureux parce que c'est un peu le but de toute cette affaire.

~# btrfs qgroup limit --help
usage: btrfs qgroup limit [options] <size>|none [<qgroupid>] <path>

    Set the limits a subvolume quota group.

    -c   limit amount of data after compression. This is the default,
         it is currently not possible to turn off this option.
    -e   limit space exclusively assigned to this qgroup

~#

Exemple :

~# # Créons les sous-volumes '/test', '/test/a' et '/test/b'
~# btrfs subvolume create /test/
Create subvolume '//test'
~# btrfs subvolume create /test/a
Create subvolume '//test/a'
~# btrfs subvolume create /test/b
Create subvolume '//test/b'
~# btrfs qgroup show -erF /test/a/
qgroupid         rfer         excl     max_rfer     max_excl 
--------         ----         ----     --------     -------- 
0/6067       16.00KiB     16.00KiB         none         none
~#
~# # Assignons des limites à '/test/a'
~# btrfs qgroup limit 10m 0/6067 /
~# btrfs qgroup limit -e 5m 0/6067 /
~# btrfs qgroup show -erF /test/a/
qgroupid         rfer         excl     max_rfer     max_excl 
--------         ----         ----     --------     -------- 
0/6067       16.00KiB     16.00KiB     10.00MiB      5.00MiB
~#

On a créé nos limites. Maintenant, testons si elles marchent :

~# dd if=/dev/urandom of=/test/a/bigfile bs=1M count=50
dd: erreur d´écriture de '/test/a/bigfile': Débordement du quota d´espace disque
5+0 enregistrements lus
4+0 enregistrements écrits
5046272 bytes (5,0 MB, 4,8 MiB) copied, 1,04639 s, 4,8 MB/s
~# btrfs qgroup show --sync -erF /test/a/
qgroupid         rfer         excl     max_rfer     max_excl 
--------         ----         ----     --------     -------- 
0/6067        4.83MiB      4.83MiB     10.00MiB      5.00MiB
~#

Pour la taille exclusive, cela semble marcher. On a essayé de créer un fichier de 50 Mio en faisant 50 blocs de 1 Mio, mais une erreur quota est survenue au moment de l'écriture du 5e Mio.

Mais essayons d'aller plus loin, pour la taille rfer :

~# rm /test/a
~# dd if=/dev/urandom of=/test/b/bigfile bs=1M count=50
50+0 enregistrements lus
50+0 enregistrements écrits
52428800 bytes (52 MB, 50 MiB) copied, 0,251852 s, 208 MB/s
~# cp --reflink /test/b/bigfile /test/a/
~# btrfs qgroup show --sync -erF /test/a/
qgroupid         rfer         excl     max_rfer     max_excl 
--------         ----         ----     --------     -------- 
0/6067       50.02MiB     16.00KiB     10.00MiB      5.00MiB
~# touch /test/b/nothing
~# cp --reflink /test/b/nothing /test/a/
cp: impossible de créer le fichier standard '/test/a/nothing': Débordement du quota d´espace disque
~#

On remarque truc bizarre : sur un premier cp --reflink, le quota est allègrement dépassé, sans message d'erreur. Une fois le quota dépassé, une erreur est effectivement levée sur le second cp --reflink. Mais pourquoi ?

tl;dr C'est un bug (boo#1057962).

estimation

Chaque opération sur le système de fichiers est encapsulée dans une transaction. La transaction n'est pas écrite immédiatement : elle est d'abord construire en mémoire. L'espace disque est d'abord réservé.

Avec Btrfs, les données et métadonnées sont mises à des adresses distinctes des données et métadonnées précédentes (Copy-on-Write) ; des arbres (B) distincts les référençant sont créés. Au bout de 30 secondes (durée réglable via l'option de montage commit) ou sur une synchronisation demandée (par exemple avec la commande btrfs filesystem sync), ce qui était en mémoire est écrit sur le disque. En tout dernier, le superblock est réécrit de façon atomique pour pointer vers les nouveaux arbres. Ce mécanisme garantit que le système de fichiers est toujours consistant à un instant t.

Bref, toute cette digression inutile pour dire que pour les quotas, c'est pareil : l'espace par rapport aux quotas est d'abord réservé. Le seul moment où une erreur de dépassement de quota peut être levée, c'est au début de l'opération, pas au milieu. Le système de fichiers – ce n'est pas spécifique à Btrfs – doit donc estimer l'impact que va avoir l'opération sur la quantité de données référencées par les sous-volumes.

Et c'est là que le bât blesse.

Pour l'opération cp --reflink, c'est-à-dire l'opération de clonage de fichier clone_file_range implémentée par btrfs_clone_file_range, l'estimation n'est pas faite. Plus exactement : elle semble faite pour les métadonnées mais pas pour les données. D'où le comportement observé. Pour le moment du moins ☺

Cas pratique avec snapper

Note : pour cette partie, je réutilise des bouts d'un article que j'ai écrit sur le forum Alionet.

Un problème de taille

antoine

On en avait parlé dans l'épisode 2 : la configuration par défaut de snapper est aussi agressive sur la prise d'instantanés qu'elle est laxiste sur leur nettoyage. D'où des problèmes de systèmes de fichiers saturés par des instantanés.

Depuis la version 0.3, snapper permet d'utiliser les quotas pour nettoyer les instantanés en fonction de l'espace disque utilisé.

Il est à noter qu'il n'y a pas d'utilisation des limites telles qu'on peut les définir avec btrfs qgroup limit : snapper n'utilise les quotas que pour récupérer rapidement la taille des snapshots.

En outre, snapper peut avoir avoir plusieurs configurations, chacune gérant les instantané d'un sous-volume. Par exemple, on peut imaginer la configuration toto pour @/home/toto, tata pour @/home/tata, etc. Chaque configuration aurait un qgroup distinct (1/1, 1/2, …) dans le rôle de parent, permettant de fixer à chaque utilisateur des quotas pour ses snapshots.

La commande magique : snapper setup-quota

snapper propose une commande pour activer les quotas comme il faut : snapper setup-quota. Le principe est simple :

  1. Créer un qgroup de niveau 1, 1/0 (btrfs qgroup create 1/0 /).
  2. Définir le qgroup de tout nouvel instantané comme enfant de 1/0 (btrfs subvolume snapshot -i 1/0 /.snapshots/<id>/snapshot).
  3. Supprimer les instantanés les plus anciens quand la taille exclusive de 1/0 dépasse un seuil prédéfini.

La commande va donc créer les paramètres QGROUP et SPACE_LIMIT dans la configuration de snapper (/etc/snapper/configs/root) :

# QGROUP est l'identifiant de votre groupe d'instantanés ; par défaut 1/0
# Les instantanés seront enregistrés comme des qgroups enfants de $QGROUP
QGROUP="1/0"

# Fraction maximale d’espace disque que snapper peut utiliser pour stocker les instantanés
# 50 % ici, soit tout de même 20 Go pour une partition racine de 40 Go ; vous pouvez l'ajuster comme vous le voulez
SPACE_LIMIT="0.5"

50% c'est beaucoup quand même… mettons 10% plutôt :

~# snapper set-config SPACE_LIMIT=0.1
~#

Pour le reste de la configuration, soyons fous et laissons les valeurs par défaut.

On laisse tourner quelques jours :

~# btrfs subvolume list -s /
ID 258 gen 12588 top level 257 path .snapshots/1/snapshot
ID 366 gen 12102 top level 257 path .snapshots/3/snapshot
ID 369 gen 12102 top level 257 path .snapshots/6/snapshot
ID 370 gen 12102 top level 257 path .snapshots/7/snapshot
ID 371 gen 12102 top level 257 path .snapshots/8/snapshot
ID 372 gen 12102 top level 257 path .snapshots/9/snapshot
ID 373 gen 12102 top level 257 path .snapshots/10/snapshot
ID 379 gen 12102 top level 257 path .snapshots/15/snapshot
ID 382 gen 12102 top level 257 path .snapshots/18/snapshot
ID 383 gen 12102 top level 257 path .snapshots/19/snapshot
ID 389 gen 12102 top level 257 path .snapshots/25/snapshot
ID 394 gen 12102 top level 257 path .snapshots/30/snapshot
ID 396 gen 12102 top level 257 path .snapshots/31/snapshot
ID 401 gen 12102 top level 257 path .snapshots/35/snapshot
ID 402 gen 12102 top level 257 path .snapshots/36/snapshot
ID 403 gen 12102 top level 257 path .snapshots/37/snapshot
ID 404 gen 12102 top level 257 path .snapshots/38/snapshot
ID 406 gen 12102 top level 257 path .snapshots/39/snapshot
ID 411 gen 12102 top level 257 path .snapshots/44/snapshot
ID 416 gen 12102 top level 257 path .snapshots/49/snapshot
ID 417 gen 12102 top level 257 path .snapshots/50/snapshot
ID 418 gen 12102 top level 257 path .snapshots/51/snapshot
ID 419 gen 12102 top level 257 path .snapshots/52/snapshot
ID 421 gen 12102 top level 257 path .snapshots/54/snapshot
ID 426 gen 12102 top level 257 path .snapshots/59/snapshot
ID 428 gen 12120 top level 257 path .snapshots/60/snapshot
ID 429 gen 12120 top level 257 path .snapshots/61/snapshot
ID 430 gen 12120 top level 257 path .snapshots/62/snapshot
ID 431 gen 12120 top level 257 path .snapshots/63/snapshot
ID 432 gen 12120 top level 257 path .snapshots/64/snapshot
ID 433 gen 12120 top level 257 path .snapshots/65/snapshot
ID 434 gen 12120 top level 257 path .snapshots/66/snapshot
ID 435 gen 12120 top level 257 path .snapshots/67/snapshot
ID 436 gen 12120 top level 257 path .snapshots/68/snapshot
ID 437 gen 12120 top level 257 path .snapshots/69/snapshot
ID 438 gen 12120 top level 257 path .snapshots/70/snapshot
ID 439 gen 12120 top level 257 path .snapshots/71/snapshot
ID 441 gen 12161 top level 257 path .snapshots/72/snapshot
ID 442 gen 12243 top level 257 path .snapshots/73/snapshot
ID 443 gen 12319 top level 257 path .snapshots/74/snapshot
ID 444 gen 12394 top level 257 path .snapshots/75/snapshot
ID 445 gen 12495 top level 257 path .snapshots/76/snapshot
ID 446 gen 12587 top level 257 path .snapshots/77/snapshot
~#

Je vous l'avais dit, les réglages de snapper sont barbares ! Et là les quotas sont activés, je vous laisse imaginer sans ! Bref, ça passe :

~# btrfs qgroup show / | awk 'NR <= 2 || /1\/0/'
qgroupid         rfer         excl
--------         ----         ---- 
1/0          10.20GiB      4.16GiB
~#

1/0 est le qgroup parent de tous les instantanés pris par snapper. Il référence 10,20 Gio de données mais seulement 4,16 Gio ne sont pas partagés avec d'autres qgroups (hors enfants), c'est-à-dire avec le sous-volume / actuel.

Ainsi, tous les instantanés « pèsent » 4,16 Gio, soit environ 10% de mon espace disque

Btrfs et les quotas : outro

Statut de la fonctionnalité : Mostly OK

Le wiki Btrfs indique que la fonctionnalité des quotas est mostly ok. Des bugs et des limitations subsistent.

En voici une liste non-exhaustive, en plus de ce que l'on a vu plus haut :

  • La commande btrfs qgroup show n'affiche pas certaines informations : par exemple, il n'est pas possible pour un qgroup parent de voir les sous-volumes des qgroups enfants.
  • Créer un qgroup à partir d'un dossier existant affichera une utilisation de 0 jusqu'à une réanalyse complète ne soit effectué (avec btrfs quota rescan).
  • Combiner les quotas avec beaucoup d'instantanés peut causer des problèmes de performances, notamment au moment de supprimer des instantanés (boo#1017461, a priori résolu maintenant).
  • Il est possible que le flag indiquant une réanalyse des quotas en cours se bloque (boo#1047152) ; le correctif appliqué sur openSUSE Leap a été mergé dans le noyau 4.14.

Sur openSUSE

openSUSE

Malgré ces soucis et limitations, openSUSE active les quotas Btrfs par défaut depuis la sortie de Leap 42.2.

À cette époque, la fonctionnalité avait une implémentation encore plus fragile. Son activation – avant tout pour exploiter les fonctionnalités de snapper 0.3 – avait alors entraîné de vives discussions sur la liste de diffusion opensuse-factory, alors que la distribution n'était qu'en beta. Cette dernière est tout de même sortie comme prévue, moyennant des backports et même quelques correctifs en avance de l'upstream.

Au final, malgré quelques bugs gênants, notamment ces cas de forts ralentissements en cas de btrfs balance ou de suppression d'instantanés, j'ai l'impression que cela aurait pu être pire.

openSUSE continue d'utiliser la fonctionnalité, uniquement pour snapper et les instantanés de / et toujours avec quelques patchs en avance sur l'upstream.

That's all folks!

Liens

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.