Observations sur rsync

Récemment, je me suis demandé comment la commande rsync faisait pour ne renvoyer que les parties modifiées des fichiers traités. Cela a donné lieu à une expérience intéressante dont les résultats sont partagés ici.

Introduction en 10 secondes

rsync est un programme très commun utilisé pour synchroniser des fichiers entre deux machines. Son principal avantage face à des programmes comme scp est qu’il ne renvoie pas un fichier entier si seulement une partie de ce fichier a changé depuis la dernière invocation de rsync. Mais comment cela est-il implémenté?

Pour faire simple, un fichier est découpé en blocs. À chaque bloc est associé une somme de contrôle. Ce processus se produit à la fois sur la machine expéditrice et la machine destinataire. Lorsque les sommes de contrôle diffèrent, cela indique à rsync que le bloc associé doit être mis à jour.

La question

Maintenant, la question est la suivante: Est-ce que rsync a besoin de recalculer la somme de contrôle de chaque bloc, à chaque invocation? Ou bien est-ce que ces sommes de contrôle sont stockés et réutilisés ultérieurement?

Je me suis posé cette question dans un contexte précis. Imaginons un ficher de plus de 1 Go, dont un seul octet diffère entre deux machines. Je m’attends à ce qu’un seul bloc soit envoyé sur le réseau, ce point est acquis.

Mais qu’est-ce que cela donne en termes d’I/O disque? Est-ce que le fichier est entièrement chargé en mémoire, des deux côtés, pour détecter cette différence d’un octet? Ou bien existe-t-il une optimisation qui gère ce cas.

Cette page contient la réponse, mais ce n’était pas 100% clair pour moi à la première lecture.

Design d’expérience

Stand back, I’m going to try science.

— XKCD

Dans ces situations, je pose d’abord mes hypothèses et je décris les résultats que j’attends. Seulement après, je conçois une expérience pour vérifier ces hypothèses. Ici, l’expérience est la suivante:

  1. Monitorer deux machines dont le débit du disque est limité à 50 Mo/s

  2. Sur le nœud 0, créer un fichier de 3 Go *

  3. L’envoyer via rsync sur le nœud 1

  4. Ajouter un octet au fichier sur le nœud 0

  5. Le renvoyer via rsync sur le nœud 1

*: La taille arbitraire de 3 Go est choisie car cela prendra précisément 60 secondes pour l’écrire et le lire depuis le disque, à raison de 50 Mo/s. J’aime les chiffres ronds.

Les métriques système, et plus particulièrement la bande passante du disque, nous diront si le fichier est intégralement monté en RAM. Cela suppose qu’après les étapes 3, 4, 5 et 6, le cache du système de fichier soit purgé. Faute de quoi, le fichier serait servi directement depuis la RAM, faussant les résultats.

Le script de test est donné ci-dessous. Il est exécuté sur le nœud 0.

input_file=~/big-file

dd if=/dev/zero of=$input_file bs=1M count=3000
sleep 1m

sudo sync ; echo 1 | sudo tee /proc/sys/vm/drop_caches
ssh node1 'sudo sync ; echo 1 | sudo tee /proc/sys/vm/drop_caches'
sleep 1m

rsync -a $input_file node1:~/
sleep 1m

sudo sync ; echo 1 | sudo tee /proc/sys/vm/drop_caches
ssh node1 'sudo sync ; echo 1 | sudo tee /proc/sys/vm/drop_caches'
sleep 1m

echo 0 >> $input_file
rsync -a $input_file node1:~/

Phases du test

Dans les graphiques ci-dessous, les trois lignes verticales représentent respectivement le début de la génération du fichier de test, et le lancement de la première et de la seconde commande rsync.

Bande passante disque sur le nœud 0

Sur le noeud 0, l’utilisation de la bande passante du disque montre clairement qu’il a fallu une minute (60 secondes * 50 Mo/s) pour générer le fichier de test. Elle montre ensuite que le fichier a été intégralement lu depuis le disque pendant la première invocation de rsync. Cependant, durant la seconde invocation de rsync, elle montre qu’il a bel et bien été relu intégralement, mais pas immédiatement. Il y a un délait d’une minute avant que cette lecture commence.

Disk bandwidth usage on node 0

Bande passante disque sur le nœud 1

Sur le noeud 1, on peut voir que la bande passante disque utilisée est également de 50 Mo/s durant le premier rsync, ce qui est normal. Cependant, durant le second rsync, on voit un comportement intéressant. Le fichier de 3 Go est intégralement lu depuis le risque quand rsync démarre, et cela prend précisément une minute. En d’autres termes, cela montre que le noeud 0 attend que le noeud 1 ait fini de construire les sommes de contrôle avant de faire sa part du travail.

J’ai trouvé ce point assez surprenant. Je m’attendais à ce que les deux machines calculent leurs sommes de contrôle en même temps, pour diminuer le temps total d’exécution du programme.

Une autre curiosité (moins importante) est que le fichier de test est entièrement réécrit sur le disque pendant le second rsync.

Disk bandwidth usage on node 1

Sommes de contrôle mobiles

L’explication de ce délai d’une minute entre le calcul des sommes de contrôle est connue sous le terme "rolling checksum computation".

Pour bien comprendre, considérons le cas où un octet est ajouté au début d’un fichier. Si les limites des blocs sont fixes (par exemple tous les 4 Ko), alors les fichiers seront considérés comme 100% différents, car le contenu du fichier source a été décalé d’un octet. Et c’est pour cette raison que les limites des blocs peuvent évoluer dynamiquement durant le processus.

Lorsque rsync a terminé de répliquer les changements d’un bloc, le programme recalcule la somme de contrôle à partir de l’octet situé juste après le dernier changement. De fait, si le reste du fichier est identique, rsync le détectera et ne l’enverra pas au destinataire.

Durant le test, le noeud 1 calcule les sommes de contrôle de chaque bloc et les envoie au noeud 0. Ensuite, le noeud 0 les calcule également de son côté. Après chaque calcul, il compare la somme de contrôle avec celle envoyée par le noeud 1. Si elles sont identique, il informe le noeud 1 que les blocs associés sont synchronisés et passe au bloc suivant.

Initialement, je m’attendais à ce que les deux noeuds calculent leurs sommes de contrôle simultanément. Cela aurait pu fonctionner, mais seulement pour ce cas de test précis, car seul le dernier bloc diffère entre les deux fichiers. Si un octet avait été ajouté en début de fichier, tous les blocs auraient été marqués comme différents. Et pour calculer les sommes de contrôle mobiles, le noeud 0 aurait du relire intégralement le fichier une fois de plus. En d’autres termes, le destinataire aurait lu le fichier une fois, et l’expéditeur l’aurait lu deux fois.

Conclusion

Par défaut, rsync détecte si un fichier a changé à l’aide de sa taille et de sa date de dernière modification. Chaque fichier ainsi détecté est chargé intégralement depuis le disque pour le calcule des sommes de contrôle.

Quelques graphiques supplémentaires

Les deux graphiques ci-dessous montrent respectivement la bande passante réseau utilisée par le noeud 0 et le noeud 1. Ils montrent que le contenu de 3 Go a été envoyé durant le premier rsync uniquement. Cela correspond au comportement standard.

Et cela confirme que lorsque le noeud 1 a réécrit le fichier entier, durant le second rsync, cela n’a pas impliqué de transfert réseau additionnel.

Network bandwidth usage on node 0
Network bandwidth usage on node 1

Si vous avez des questions ou voulez me faire part de vos commentaires, envoyez moi un tweet (@pingtimeout). Et si vous avez apprécié cet article et voulez soutenir mon travail, vous pouvez toujours m’offrir un café sur BuyMeACoffee ☕️.