New article on whiteout files
This commit is contained in:
parent
4e5d7b2a4c
commit
075b635446
256
content/en/post/unveiling-whiteout-files/index.md
Normal file
256
content/en/post/unveiling-whiteout-files/index.md
Normal file
@ -0,0 +1,256 @@
|
||||
---
|
||||
title: "Unveiling Whiteout Files: Do you know how file deletions are handled between layers of a Docker image?"
|
||||
date: !!timestamp '2023-11-09 15:35:00'
|
||||
image: /post/unveiling-whiteout-files/og.webp
|
||||
tags:
|
||||
- kernel
|
||||
- container
|
||||
- docker
|
||||
- linux
|
||||
---
|
||||
|
||||
Union file systems are a mechanism for merging two or more file systems, to present them unified, under a single mount point for the user.
|
||||
|
||||
The main idea behind this mechanism is to be able to alter the contents of the first file system (e.g. the contents of a CD-ROM) by writing all changes (additions, deletions, modifications) to the second (which could be a disk partition, a USB stick, ...).
|
||||
|
||||
While adding and modifying may seem trivial, deleting is not.
|
||||
So let's explore in this article what *whiteout files* are and how they can simulate the deletion of a file.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Another common use of filesystem unions is in containers: container images are made up of layers.
|
||||
If you launch a `php` container and then a `nginx` container, both images based on `debian`, you will only download the underlying `debian` image once.
|
||||
Files from the `debian` image may be modified or deleted by an image such as `php` or `nginx`.
|
||||
Thanks to union file system!
|
||||
|
||||
|
||||
## Understanding Union File System
|
||||
|
||||
Unions file system share a number of concepts, which we will illustrate with the following diagram:
|
||||
|
||||
![File access by layer](overlayfs.png)
|
||||
|
||||
Here we see a two-layer file system, referred to in the jargon as two *branches*.
|
||||
They are denoted *Lower* for the lowest layer and *upper* for the layer that is inserted on top of the *lower* layer; and finally *Merged* for the resulting view.
|
||||
Some implementations support more than 2 branches, with sometimes complex access and modification policies.
|
||||
|
||||
When a file is deleted from the union, a so-called *whiteout file* is placed in the *upper* layer to indicate that this file should no longer be displayed in the *merged* layer.
|
||||
The same concept applies to folders, which are referred to as *opaque directory*.
|
||||
|
||||
When accessing a file in the *lower* branch that has not been modified in *upper*, the *lower* file is accessed directly.
|
||||
|
||||
When a file is modified, its entire contents are copied from the *lower* branch to the *upper* branch.
|
||||
A file that is added, overwritten or modified will therefore have its entire contents in the *upper* layer.
|
||||
|
||||
|
||||
## History
|
||||
|
||||
The concept of *whiteout file* has its origins in the early development of file system unions.
|
||||
|
||||
|
||||
[**Translucent File System**](http://mcvoy.com/lm/papers/SunOS.tfs.pdf) is undoubtedly the first implementation of the *whiteout file* concept.
|
||||
Developed by David Hendricks in the 1980s for SunOS 3, the idea was to allow users of a machine to take advantage of the base system, making modifications without impacting other users, and without having access to other users' files.
|
||||
|
||||
The first *union mounts* were implemented with BSD 4.4, in the 90s.
|
||||
|
||||
The best-known implementation today is *UnionFS*, by Erez Zadok.
|
||||
It was to be the implementation used for the Linux kernel, but like *aufs*, their code and solution didn't convince to be fully integrated.
|
||||
|
||||
It wasn't until 2014 that a *union mount* was integrated into the Linux kernel.
|
||||
This is OverlayFS.
|
||||
It arrived in kernel 3.18, after more than 4 years of rewrites and structural improvements, to reach the demanding and uncompromising level required for its integration into the official kernel.
|
||||
|
||||
{{% card color="info" title="What issues complicate the implementation of an union file system?" %}}
|
||||
|
||||
One of the trickiest problems is finding a way to represent file and folder deletions: it has to be a valid file (with or without metadata), as the information needs to be stored in a concrete way.
|
||||
In many implementations, a `.wh.<filename>` file serves as a *whiteout file*, which can create conflicts with the user's own file names (or reduce the user's choice of file names).
|
||||
|
||||
A similar problem applies to folders: should you delete every file contained in the folder, or does the mere presence of an *opaque directory* prevents discovery?
|
||||
|
||||
Memory usage can quickly get out of hand, especially if the implementation allows a lot of branches, because if you want the system to perform well you'll need to have the topologies of each file system in memory.
|
||||
|
||||
Implementing `mmap(2)` is necessarily a nightmare: when a file is modified by two processes that `mmap(2)`, we normally expect to see the modifications in both processes, but the first to make a modification creates a new file in the writeable branch.
|
||||
This makes it difficult to reconcile the pointers of the two processes.
|
||||
|
||||
Similarly, think about *hard links* management: all pointers to updated content should be modified in the write layer, but there is no pointer index, so it's not easy to find the files to be updated.
|
||||
|
||||
And let's not forget that the underlying file systems of each branch don't necessarily have the same constraints (file name sizes, extended attributes, metadata, accent encoding, etc.), so you have to juggle between them, while returning consistent errors where appropriate.
|
||||
|
||||
And many more besides.
|
||||
Not least `readdir(2)`, which needs to be stable despite the turbulence that can occur between two calls, ...
|
||||
|
||||
See this series of articles summarizing the different implementations, their choices and differences:
|
||||
<https://lwn.net/Articles/325369/>, <https://lwn.net/Articles/327738/>.
|
||||
|
||||
{{% /card %}}
|
||||
|
||||
In what follows, we'll be concentrating mainly on the operation of this file system, trying as far as possible to draw parallels with the others.
|
||||
|
||||
## Whiteout Files in Practice
|
||||
|
||||
First of all, you need to know how to set up such a file system.
|
||||
Here's a general example of how to create a simple union between a read-only and a read/write file system:
|
||||
|
||||
```
|
||||
mount -t overlay -olowerdir=/lower,upperdir=/upper,workdir=/work ignored /merged
|
||||
```
|
||||
|
||||
The type to use is `overlay`, with the `lowerdir` options indicating the location of the folder(s) to be combined in read-only mode (separated by `:` when there are several), the directory containing the read/write system in the `upperdir` option, and don't forget the `workdir` option, a path on the same partition as the `upperdir`, which must be empty.
|
||||
|
||||
We end the call by giving the source device, which is useless in our case (`ignored` or any other string will do), and finally the folder to which our union will be mounted: `/merged` in the example.
|
||||
|
||||
### Usage in Containerization
|
||||
|
||||
Let's analyze a running Docker container to learn more.
|
||||
|
||||
First, we check that we're using the `overlay2` *storage driver*:
|
||||
|
||||
```
|
||||
42sh$ docker info | grep "Storage Driver"
|
||||
Storage Driver: overlay2
|
||||
```
|
||||
|
||||
This is the case (depending on your kernel configuration, Docker may have chosen a different *driver*), so let's start the analysis:
|
||||
|
||||
```
|
||||
42sh$ docker container run --rm -it debian
|
||||
incntr$ mount | grep "on / "
|
||||
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/B62UNV3UB3X4TBWQMM6XCMM6W5:/var/lib/docker/overlay2/l/V6HGFN3C3PEW6CZ6XWRSHHDKJH,upperdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/diff,workdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/work)
|
||||
```
|
||||
|
||||
Note that 2 `lowerdir` are used. These are symbolic links pointing to the folders identifying the layers (the names of the links are random, the aim being to have a shortened path to the layer's file system, as the number of characters that can be passed to the `mount(2)` system call is limited).
|
||||
|
||||
The lowest branch (furthest to the right of the `lowerdir` parameter) contains the single layer of our `debian` image, while the branch furthest to the left overlays a number of configuration files required to run the container (`/etc/hosts`, `resolv.conf`, ...).
|
||||
|
||||
The read/write branch is also registered in the `/var/lib/docker/overlay2` folder, and its identifier can be seen. The `upperdir` is in the `diff` folder, while the `workdir` is in the `work` folder, under the same layer ID.
|
||||
|
||||
We can also see the folders used by inspecting our:
|
||||
|
||||
```
|
||||
42sh$ docker container inspect youthful_wilbur | jq .[0].GraphDriver.Data
|
||||
```
|
||||
```json
|
||||
{
|
||||
"LowerDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31-init/diff:/var/lib/docker/overlay2/2cc3656c06...c0fb91d6/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/work"
|
||||
}
|
||||
```
|
||||
|
||||
If you test with an image with more layers, you'll get more `lowerdir`, one per layer. Feel free to run the same series of commands with the `python` image, for example.
|
||||
|
||||
### Adding files
|
||||
|
||||
At this point, if we look at the contents of our `upperdir` folder, we can see that it's empty. This is normal, since we haven't made any changes.
|
||||
|
||||
In our previously launched container, let's make a modification, by adding a:
|
||||
|
||||
```
|
||||
incntr$ echo "newfile" > /root/foobar
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
└── root
|
||||
└── foobar
|
||||
```
|
||||
|
||||
Our new file, which is not the only one in the tree structure shown in the container, has been added, as you'd expect, to the read/write branch.
|
||||
|
||||
|
||||
### Modifying files
|
||||
|
||||
If we make a change to a file, for example by adding a line, it's not just the difference that is stored in the write branch, but the whole file, as it has been modified:
|
||||
|
||||
```
|
||||
incntr$ echo "Bienvenue dans le conteneur" >> /etc/issue
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
└── etc
|
||||
└── issue
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/issue
|
||||
Debian GNU/Linux 11 \n \l
|
||||
Bienvenue dans le conteneur
|
||||
```
|
||||
|
||||
|
||||
### Deleting files
|
||||
|
||||
When you want to delete a file you've just added, there's not much you can do, since deleting the file from the write branch will make the file disappear from the mounted tree.
|
||||
|
||||
When it comes to deleting a file from a read-only branch, you need to be able to hide the file using a marker.
|
||||
Depending on the *storage driver*, this marker is different: in `OverlayFS`, a deletion is materialized by a special character file of the same name.
|
||||
|
||||
```
|
||||
incntr$ rm /etc/adduser.conf
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/1531651afa872006a4b2b9b913d5d8ee317cf12be7883517ba77f3d094f871b4/diff
|
||||
└── etc
|
||||
└── adduser.conf
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
cat: No such device or address
|
||||
|
||||
42sh$ stat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
File: /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
Size: 0 Blocks: 0 IO Block: 4096 character special file
|
||||
Device: fe0bh/65035d Inode: 515773 Links: 2 Device type: 0,0
|
||||
```
|
||||
|
||||
Note here `Device type: 0,0`.
|
||||
|
||||
To create a similar file ourselves, we would need to use:
|
||||
|
||||
```
|
||||
42sh$ mkdir /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin
|
||||
42sh$ mknod /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin/sh c 0 0
|
||||
```
|
||||
|
||||
{{% card color="danger" title="Caution, undefined behavior!" %}}
|
||||
|
||||
Running this `mknod` command while the file system union is mounted elsewhere will not make the `/bin/sh` file disappear, as any modifications modifications that could be made to the branches outside the mounted system lead to explicitly undefined results.
|
||||
|
||||
{{% /card %}}
|
||||
|
||||
|
||||
### Deletion on `unionfs` and AuFS
|
||||
|
||||
The concept of *whiteout file*, as we have seen, differs depending on the file system. It turns out that, although OverlayFS was integrated into the Linux kernel after many ups and downs, when specifying the format of the archives used to distribute layers, Docker now uses the AuFS format to represent deletions.
|
||||
It is therefore important to know it too.
|
||||
|
||||
Instead of using a special file, AuFS creates a standard file `.wh.<filename>`, where `<filename>` is the name of the file to be hidden.
|
||||
|
||||
In order to adapt to the *storage driver*, when the archive is decompressed, Docker converts[^MOBYWHITEOUT] the *whiteout files* it encounters into the expected expected format.
|
||||
|
||||
[^MOBYWHITEOUT]: See the source code
|
||||
<https://github.com/moby/moby/blob/master/pkg/archive/archive_linux.go#L27>
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
Just when you thought you didn't want to know what *whiteout files* were all about, I'm sure that reading this article has given you a glimpse into the complexity of both *union mounts* and software that takes advantages of different implementations.
|
||||
|
||||
Now you know why, in particular, it's pointless to delete a large file in a layer other than the one that contributed it, for example:
|
||||
|
||||
```dockerfile
|
||||
RUN wget https://dumps.wikimedia.org/enwiki/enwiki-pages-articles-multistream.xml.bz2
|
||||
|
||||
RUN ... # some other stuff
|
||||
|
||||
RUN rm enwiki-pages-articles-multistream.xml.bz2
|
||||
```
|
||||
|
||||
Each `RUN` creates a separate layer, so our `enwiki-pages-articles-multistream.xml.bz2` file will be distributed with the first layer of our image, then a *whiteout file* will be inserted in the layer corresponding to the third `RUN`.
|
BIN
content/en/post/unveiling-whiteout-files/og.webp
Normal file
BIN
content/en/post/unveiling-whiteout-files/og.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 153 KiB |
BIN
content/en/post/unveiling-whiteout-files/overlayfs.png
Normal file
BIN
content/en/post/unveiling-whiteout-files/overlayfs.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 68 KiB |
324
content/fr/post/unveiling-whiteout-files/index.md
Normal file
324
content/fr/post/unveiling-whiteout-files/index.md
Normal file
@ -0,0 +1,324 @@
|
||||
---
|
||||
title: "Explorons les Whiteout files : tout savoir sur la suppression de fichiers dans nos conteneurs Docker"
|
||||
date: !!timestamp '2023-11-09 15:35:00'
|
||||
image: /post/unveiling-whiteout-files/og.webp
|
||||
tags:
|
||||
- kernel
|
||||
- container
|
||||
- docker
|
||||
- linux
|
||||
---
|
||||
|
||||
Les unions de systèmes de fichiers sont un mécanisme permettant de fusionner deux ou plusieurs systèmes de fichiers, pour les présenter unifiés, sous un seul point d'accès à l'utilisateur.
|
||||
|
||||
L'idée principale derrière ce mécanisme est de pouvoir permettre d'altérer le contenu du premier système de fichiers (par exemple le contenu d'un CD-ROM) en inscrivant toutes les modifications (ajouts, suppressions, modifications) dans le second (qui pourra être une partition d'un disque).
|
||||
|
||||
Si l'ajout et la modification peuvent sembler triviales, ce n'est pas le cas de la suppression. Alors explorons dans cet article ce que sont les *whiteout files* et comment ils permettent de simuler la suppression d'un fichier.
|
||||
|
||||
<!-- more -->
|
||||
|
||||
Un autre usage courant des unions de systèmes de fichiers se retrouve dans les conteneurs : les images des conteneurs sont composées de couches. Si vous lancez un conteneur `php` puis un conteneur `nginx`, deux images basées sur `debian`, vous ne téléchargerez qu'une seule fois l'image `debian` sous-jacente.
|
||||
Des fichiers issus de l'image `debian` peuvent se trouver modifié ou supprimé par une image telle que `php` ou `nginx`.
|
||||
Grâce à une union de système de fichiers!
|
||||
|
||||
|
||||
## Comprendre les unions de systèmes de fichiers
|
||||
|
||||
Les unions de systèmes de fichiers partagent un certain nombre de concepts que
|
||||
nous allons illustrer au travers du schéma suivant :
|
||||
|
||||
![Accès aux fichiers en fonction des couches](overlayfs.png)
|
||||
|
||||
On voit ici un système de fichiers à deux couches, on parle de deux *branches* dans
|
||||
le jargon. Elles sont notées *Lower* pour la couche la plus basse et *upper* la
|
||||
couche qui s'insère par dessus la couche *lower* ; et enfin *Merged* le
|
||||
résultat. Certaines implémentations supportent plus que 2 branches, avec des
|
||||
politiques d'accès et de modifications parfois complexes.
|
||||
|
||||
Lorsque l'on supprime un fichier de l'union, un fichier dit *whiteout file* est
|
||||
placé dans la couche en écriture (*upper*) pour indiquer que ce fichier ne doit plus être
|
||||
affiché dans la couche *merged*. Le même concept existe pour les dossiers,
|
||||
on parle alors d'*opaque directory*.
|
||||
|
||||
Lorsqu'il s'agit d'accéder à un fichier présent dans la branche *lower* et qui
|
||||
n'a pas été modifié dans *upper*, on accède directement au fichier de *lower*.
|
||||
|
||||
Lorsqu'un fichier est modifié, on recopie son contenu intégralement dans la
|
||||
branche *upper*, depuis la branche *lower*. Un fichier qui est ajouté, écrasé
|
||||
ou modifié aura donc son contenu intégralement dans la couche *upper*.
|
||||
|
||||
|
||||
## Historique
|
||||
|
||||
Le concept de *whiteout file* trouve son origine dans les premiers développements des unions de systèmes de fichiers.
|
||||
|
||||
|
||||
[**Translucent File System**](http://mcvoy.com/lm/papers/SunOS.tfs.pdf) est sans doute la première mise en œuvre du concept de *whiteout file*.
|
||||
Développé par David Hendricks dans les années 1980 pour SunOS 3, il s'agissait de permettre aux utilisateurs d'une machine de profiter du système de base, en y apportant des modifications sans impacter les autres utilisateurs, et sans avoir accès aux fichiers des autres utilisateurs.
|
||||
|
||||
Vint ensuite les premières implémentation d'*union mounts* avec BSD 4.4, dans les années 90.
|
||||
|
||||
L'implémentation la plus connue aujourd'hui est l'*UnionFS*, de Erez Zadok.
|
||||
Elle devait être l'implémentation utilisée pour le noyau Linux, mais tout comme *aufs*, leur code et leur solution ne convainct pas pour être pleinement intégré.
|
||||
|
||||
Il faudra attendre 2014, pour qu'un *union mount* soit intégré dans le noyau Linux.
|
||||
Il s'agit d'OverlayFS.
|
||||
Il est arrivé dans le noyau 3.18, après de plus de 4 années de réécritures et d'améliorations structurelles, pour atteindre le niveau d'exigence et sans compromis nécessaire à son intégration dans le noyau officiel.
|
||||
|
||||
{{% card color="info" title="Quelles problématiques rendent l'implémentation d'une union de systèmes de fichier compliquée ?" %}}
|
||||
|
||||
L'un des problèmes les plus délicats est de trouver une manière de représenter
|
||||
les suppressions de fichiers et de dossiers : cela doit être un fichier valide
|
||||
(avec ou sans métadonnée) car il faut pouvoir stocker l'information
|
||||
concrètement. Dans de nombreuses implémentations, un fichier `.wh.<filename>`
|
||||
sert de *whiteout file*, ce qui peut créer des conflits avec les noms des
|
||||
fichiers de l'utilisateur (ou réduire ses choix de noms de fichiers).
|
||||
|
||||
Un problème similaire s'applique aux dossiers : est-ce qu'il faut supprimer
|
||||
chaque fichier contenu dans le dossier ou la simple présence d'un *opaque
|
||||
directory* empêche toute découverte ?
|
||||
|
||||
L'usage de la mémoire peut vite devenir incontrôlable, surtout si
|
||||
l'implémentation autorise beaucoup de branches, car si on veut que le système
|
||||
soit performant il faudra avoir en mémoire les topologies de chaque système de
|
||||
fichiers.
|
||||
|
||||
L'implémentation de `mmap(2)` est nécessairement un cauchemar : lorsqu'un
|
||||
fichier est modifié par deux processus qui le `mmap(2)`, on s'attend
|
||||
normalement à voir les modifications dans les deux processus, or le premier à
|
||||
faire une modification crée un nouveau fichier dans la branche accessible en
|
||||
écriture. Il est alors ardu de réconcilier les pointeurs des deux processus.
|
||||
|
||||
D'une manière similaire, il faut penser à la gestion des *hard links* : tous
|
||||
les pointeurs d'un contenu mis à jour devraient être modifiés dans la couche en
|
||||
écriture, cependant il n'y a pas d'index des pointeurs, il n'est donc pas
|
||||
facile de retrouver les fichiers à mettre à jour.
|
||||
|
||||
Ajoutons aussi que les systèmes de fichiers sous-jacents de chacune des
|
||||
branches n'ont pas forcément les mêmes contraintes (tailles des noms de
|
||||
fichiers, attributs étendus, métadonnées, encodage des accents, ...) et qu'il
|
||||
faut réussir à jongler entre chaque, tout en retournant des erreurs cohérentes
|
||||
le cas échéant.
|
||||
|
||||
Et bien d'autres encore. Notamment `readdir(2)` qui doit être stable malgré
|
||||
les turbulences qui pourraient arriver entre deux appels, ...
|
||||
|
||||
Voir cette série d'articles résumant les différentes implémentations, leurs
|
||||
choix et différences : <https://lwn.net/Articles/325369/>,
|
||||
<https://lwn.net/Articles/327738/>.
|
||||
|
||||
{{% /card %}}
|
||||
|
||||
Par la suite, nous allons donc surtout nous concentrer sur le fonctionnement de ce système de fichier, en tentant autant que possible de faire le parallèle avec les autres.
|
||||
|
||||
|
||||
## Les *whiteout files* en pratique
|
||||
|
||||
Avant tout, il faut savoir monter un tel système.
|
||||
Voici un exemple général de création d'une union simple entre un système de fichiers en lecture seule et un en lecture/écriture :
|
||||
|
||||
```
|
||||
mount -t overlay -olowerdir=/lower,upperdir=/upper,workdir=/work ignored /merged
|
||||
```
|
||||
|
||||
Le type à utiliser est `overlay`, avec les options `lowerdir` qui indique
|
||||
l'emplacement du/des dossiers à combiner en lecture seule (on les sépare par
|
||||
des `:` lorsqu'il y en a plusieurs), on indique également le répertoire
|
||||
contenant le système en lecture/écriture dans l'option `upperdir`, et il ne
|
||||
faut pas oublier l'option `workdir` un chemin sur la même partition que
|
||||
l'`upperdir`, qui doit être vide.
|
||||
|
||||
On termine l'appel par donner le périphérique source, qui est inutile dans
|
||||
notre cas (`ignored` ou tout autre chaîne fera l'affaire), et enfin le dossier
|
||||
vers lequel sera monté notre union : `/merged` dans l'exemple.
|
||||
|
||||
### Usage dans les conteneurs
|
||||
|
||||
Analysons un conteneur Docker en cours d'exécution pour en apprendre davantage.
|
||||
|
||||
D'abord, on vérifie que l'on utilise bien le *storage driver* `overlay2` :
|
||||
|
||||
```
|
||||
42sh$ docker info | grep "Storage Driver"
|
||||
Storage Driver: overlay2
|
||||
```
|
||||
|
||||
C'est le cas (en fonction de la configuration de votre noyau, Docker aura
|
||||
peut-être choisi un *driver* différent), commençons donc l'analyse :
|
||||
|
||||
```
|
||||
42sh$ docker container run --rm -it debian
|
||||
incntr$ mount | grep "on / "
|
||||
overlay on / type overlay (rw,relatime,lowerdir=/var/lib/docker/overlay2/l/B62UNV3UB3X4TBWQMM6XCMM6W5:/var/lib/docker/overlay2/l/V6HGFN3C3PEW6CZ6XWRSHHDKJH,upperdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/diff,workdir=/var/lib/docker/overlay2/2a353708e5b16ea7775cf1a33dd23ce31430faaa504bcde5508691b230f9d700/work)
|
||||
```
|
||||
|
||||
On remarque que 2 `lowerdir` sont utilisés. Il s'agit de liens symboliques
|
||||
pointant vers les dossiers identifiant les couches (les noms des liens sont
|
||||
aléatoires, il s'agit en fait d'avoir un chemin raccourci par rapport au chemin
|
||||
complet vers le système de fichiers de la couche, car le nombre de caractères
|
||||
que l'on peut passer à l'appel système `mount(2)` est limité).
|
||||
|
||||
La branche la plus basse (le plus à droite du paramètre `lowerdir`) contient
|
||||
l'unique couche de notre image `debian`, celle un peu plus à gauche superpose
|
||||
un certain nombre de fichiers de configuration nécessaire à l'exécution du
|
||||
conteneur (`/etc/hosts`, `resolv.conf`, ...).
|
||||
|
||||
La branche en lecture/écriture est également enregistrée dans le dossier
|
||||
`/var/lib/docker/overlay2` et l'on peut voir son identifiant. L'`upperdir` se
|
||||
trouve dans le dossier `diff`, tandis que le `workdir` est dans le dossier
|
||||
`work`, sous le même identifiant de couche.
|
||||
|
||||
On peut également voir les dossiers utilisés en inspectant notre conteneur :
|
||||
|
||||
```
|
||||
42sh$ docker container inspect youthful_wilbur | jq .[0].GraphDriver.Data
|
||||
```
|
||||
```json
|
||||
{
|
||||
"LowerDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31-init/diff:/var/lib/docker/overlay2/2cc3656c06...c0fb91d6/diff",
|
||||
"MergedDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/merged",
|
||||
"UpperDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/diff",
|
||||
"WorkDir": "/var/lib/docker/overlay2/22753d0d81...8706f1a31/work"
|
||||
}
|
||||
```
|
||||
|
||||
Si on teste avec une image ayant plus de couches, on obtient davantage de
|
||||
`lowerdir`, un par couche. N'hésitez pas à faire la même série de commandes
|
||||
avec l'image `python` par exemple.
|
||||
|
||||
### Ajout de fichiers
|
||||
|
||||
À ce stade, si nous regardons le contenu de notre dossier `upperdir`, nous
|
||||
pouvons remarquer que celui-ci est vide. C'est normal puisque nous n'avons apporté
|
||||
aucune modification.
|
||||
|
||||
Dans notre conteneur précédemment lancé, apportons une modification, en
|
||||
ajoutant un fichier :
|
||||
|
||||
```
|
||||
incntr$ echo "newfile" > /root/foobar
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
└── root
|
||||
└── foobar
|
||||
```
|
||||
|
||||
Notre nouveau fichier, qui n'est pourtant pas le seul dans l'arborescence que
|
||||
l'on voit dans le conteneur, a été ajouté comme on pouvait s'y attendre, dans
|
||||
la branche en lecture/écriture.
|
||||
|
||||
|
||||
### Modification de fichiers
|
||||
|
||||
Si nous apportons une modification à un fichier, par exemple en ajoutant une
|
||||
ligne, ce n'est pas seulement la différence qui est stockée dans la branche en
|
||||
écriture, mais bien tout le fichier, tel qu'il a été modifié :
|
||||
|
||||
```
|
||||
incntr$ echo "Bienvenue dans le conteneur" >> /etc/issue
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
└── etc
|
||||
└── issue
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/issue
|
||||
Debian GNU/Linux 11 \n \l
|
||||
Bienvenue dans le conteneur
|
||||
```
|
||||
|
||||
|
||||
### Suppression de fichiers
|
||||
|
||||
Lorsque l'on souhaite supprimer un fichier que l'on vient d'ajouter, il n'y a
|
||||
pas grand chose à faire puisque supprimer ce fichier de la branche en écriture
|
||||
fera bien disparaître le fichier de l'arborescence montée.
|
||||
|
||||
Lorsqu'il s'agit de supprimer un fichier présent dans une branche en lecture
|
||||
seule, il faut réussir à faire en sorte de masquer ce fichier au moyen d'un
|
||||
marqueur. En fonction du *storage driver*, ce marqueur est différent : dans
|
||||
`OverlayFS`, une suppression est matérialisée par un fichier spécial de type
|
||||
caractère du même nom.
|
||||
|
||||
```
|
||||
incntr$ rm /etc/adduser.conf
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ tree /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff
|
||||
/var/lib/docker/overlay2/1531651afa872006a4b2b9b913d5d8ee317cf12be7883517ba77f3d094f871b4/diff
|
||||
└── etc
|
||||
└── adduser.conf
|
||||
```
|
||||
|
||||
```
|
||||
42sh$ cat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
cat: No such device or address
|
||||
|
||||
42sh$ stat /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
File: /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/etc/adduser.conf
|
||||
Size: 0 Blocks: 0 IO Block: 4096 character special file
|
||||
Device: fe0bh/65035d Inode: 515773 Links: 2 Device type: 0,0
|
||||
```
|
||||
|
||||
Notons ici `Device type: 0,0`.
|
||||
|
||||
Pour créer nous-mêmes un fichier similaire, il faudrait utiliser :
|
||||
|
||||
```
|
||||
42sh$ mkdir /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin
|
||||
42sh$ mknod /var/lib/docker/overlay2/2a353708e5...91b230f9d700/diff/bin/sh c 0 0
|
||||
```
|
||||
|
||||
{{% card color="danger" title="Attention, comportement indéterminé !" %}}
|
||||
|
||||
Faire cette commande `mknod` alors que l'union de système de fichiers est
|
||||
montée par ailleurs ne va pas faire disparaître le fichier `/bin/sh` car les
|
||||
modifications qui pourraient être apportées aux branches en dehors du système
|
||||
monté conduisent à des résultats explicitement indéfinis.
|
||||
|
||||
{{% /card %}}
|
||||
|
||||
|
||||
### Suppressions pour `unionfs` et AuFS
|
||||
|
||||
Le concept de *whiteout file*, comme on a pu le voir, diffère en fonction du
|
||||
système de fichiers. Il s'avère que même si l'OverlayFS a été intégré dans le
|
||||
noyau Linux après maintes péripéties, Docker, lorsqu'a été spécifié le format
|
||||
des archives utilisées pour distribuer les couches, utilise aujourd'hui le
|
||||
format d'AuFS pour représenter les suppressions. Il est donc important de le
|
||||
voir également.
|
||||
|
||||
Au lieu d'utiliser un fichier spécial, AuFS crée un fichier standard
|
||||
`.wh.<filename>`, où `<filename>` est le nom du fichier à masquer.
|
||||
|
||||
Afin de s'adapter au *storage driver*, lors de la décompression de l'archive,
|
||||
Docker s'emploie à convertir[^MOBYWHITEOUT] les *whiteout files* qu'il rencontre dans le
|
||||
format attendu.
|
||||
|
||||
[^MOBYWHITEOUT]: Voir le code
|
||||
<https://github.com/moby/moby/blob/master/pkg/archive/archive_linux.go#L27>
|
||||
|
||||
|
||||
## Conclusion
|
||||
|
||||
Alors que vous pensiez peut-être ne pas vouloir savoir de quoi s'agissait les *whiteout files*, je suis sûr qu'à la lecture de cet article vous avez entrevu la complexité que revêtent tant les *union mounts* que les logiciels tirant parti de différentes implémentations.
|
||||
|
||||
Vous savez maintenant pourquoi il est notamment inutile de supprimer un gros fichier dans une autre couche que celle qui l'a apportée, par exemple :
|
||||
|
||||
```dockerfile
|
||||
RUN wget https://dumps.wikimedia.org/enwiki/enwiki-pages-articles-multistream.xml.bz2
|
||||
|
||||
RUN ... # some other stuff
|
||||
|
||||
RUN rm enwiki-pages-articles-multistream.xml.bz2
|
||||
```
|
||||
|
||||
Chaque `RUN` créant une couche distincte, notre fichier `enwiki-pages-articles-multistream.xml.bz2` sera distribué avec la première couche de notre image, puis un *whiteout file* sera inséré dans la couche correspondante au troisième `RUN`.
|
Loading…
Reference in New Issue
Block a user