Go to Hackademy website

Réduisez la durée de vos tests grâce à votre RAM

Nicolas Zermati

Posté par Nicolas Zermati dans les catégories back

Il y a quelque temps je me suis offert un SSD pour redonner un coup de jeune à ma configuration. J’essaye d’en prendre soin. J’ai suivi les recommandations que j’ai trouvé sur internet afin de maximiser la durée de vie du disque. Minimiser les écritures est un des conseils qui revient le plus souvent, j’ai donc essayé de faire la chasse aux accès disque inutile…

Un des moments où j’entendais vraiment gratter mon disque mécanique était lorsque je lançais des tests d’intégration. Beaucoup de lectures / écritures de la base de données étaient faites à ce moment là. L’intégralité de mes tests (230 scénarios et 1814 steps Cucumber) se lançait en plus ou moins 15 minutes. Aujourd’hui la même suite de tests se lance en plus ou moins 35 secondes. Dans cet article, je vous explique comment s’est passé la transition.

Contexte

Mon projet utilise PostgeSQL 9 et ma machine tourne sous la dernière Ubuntu. En conséquence, les infos que vous trouverez ici s’appliquent principalement à cette configuration. Le principe peut à mon avis s’appliquer avec d’autres SGBD.

Pour cet article je suppose que vous avez déjà une installation de PostgreSQL fonctionnelle. Dans mon cas il s’agit de l’installation par défaut qui est réalisée lors de l’installation du paquet Ubuntu. Je suppose aussi que vous avez quelques centaines de méga-octets de mémoire vive qui ne sont jamais utilisés.

Création d’un cluster de test

Avant toute chose, comprenez bien qu’après cette étape votre machine hébergera deux instances de PostgreSQL. Votre instance habituelle, probablement nommée main, ainsi qu’une nouvelle instance nommée test que nous allons créer maintenant.

L’intérêt d’avoir deux instances est que l’on va pouvoir configurer l’une indifféremment de l’autre. Il ne faut pas confondre ces instances avec les bases de données en elles même. Plusieurs bases sont souvent gérées par une même instance, dans ces cas là toutes les bases partagent des options communes.

Les commandes suivantes vous permettront de créer une nouvelle instance :

root     # su postgres
postgres $ pg_createcluster --port=5434 9.3 test

Une instance de PostgreSQL sera créée, les fichiers relatifs à la configuration de l’instance seront présents dans le répertoire /etc/postgresql/9.3/test et les données seront dans le répertoire /var/lib/postgresql/9.3/test.

Étant un peu laxiste sur la sécurité d’un environnement de développement et encore plus lorsqu’il s’agit d’une base utilisée pour les tests, je configure les permissions d’accès à PostgreSQL via le fichier /etc/postgresql/9.3/pg_hba.conf comme ceci :

local   all             postgres                                trust
local   all             all                                     trust
host    all             all             127.0.0.1/32            trust
host    all             all             ::1/128                 md5

Cette configuration, on le verra plus tard, va nous permettre de nous connecter à PostgreSQL en tant qu’utilisateur postgres et sans mot de passe.

Configuration du système de fichiers

L’astuce repose entièrement sur la capacité du système d’exploitation à créer un système de fichiers résidant en mémoire vive. Pour créer un tel système de fichiers, on modifie le fichiers /etc/fstab en ajoutant la ligne suivante :

tmpfs	/media/tmpfs	tmpfs	defaults,size=1g					0	0

Cette ligne aura pour effet de réserver 1Go de mémoire vive qui sera mise à disposition sous forme de système de fichiers sur le point de montage /media/tmps. Il faudra créer ce répertoire à la main avec la commande : sudo mkdir -p /media/tmpfs. Le système de fichiers sera disponible au prochain redémarrage. Pour le rendre disponible dès maintenant, utilisez la commande sudo mount /media/tmpfs.

Ce système de fichiers sera vidé à chaque redémarrage de la machine. À chaque démarrage de la machine, on va donc charger les données de notre instance test dans le système de fichiers en RAM. Pour cela on peut utiliser le fichiers /etc/rc.local qui est exécuté au lancement du système.

IN_MEMORY_PG=/media/tmpfs/postgres/9.3/
mkdir -p $IN_MEMORY_PG
cp -R /var/lib/postgresql/9.3/test $IN_MEMORY_PG
chmod -R 700 $IN_MEMORY_PG
chown -R postgres $IN_MEMORY_PG

exit 0

À ce moment, vous pouvez redémarrer votre machine où bien simplement exécuter les commandes ci-dessus. Ces commandes auront pour résultat de copier les données du cluster test vers notre système de fichiers en mémoire vive. Vous aurez peut être à adapter la version de PostgreSQL, le nom du cluster ou autre en fonction de votre distribution.

Une fois la copie des fichiers faite, on modifie la configuration de PostgreSQL pour utiliser le système de fichiers. Chez moi, cela se passe dans le fichier /etc/postgresql/9.3/test/postgresql.conf où il faut modifier la valeur de l’option data_directory.

data_directory = '/media/tmpfs/postgres/9.3/test'

Une fois cette option modifiée, vous pouvez redémarrer PostgreSQL avec la commande sudo service postgresql restart. À présent, votre base est prête et devrait être beaucoup plus rapide ! À chaque démarrage, la base sera vide.

Préparation de l’application

La configuration de votre application, dans son environnement de test doit être légèrement différente… L’utilisateur utilisé doit être postgres, le port doit être le 5434 et il faut s’assurer qu’avant chaque lancement, la base de données existe et qu’elle contient bien le schéma requis.

Dans mon application, j’ai utilisé le code suivant pour qu’en environnement de test, on tente de créer la base, de charger les extensions nécessaire (ici hstore) et de migrer le schema.

# In test mode try the connection differs
if ENV['RACK_ENV'] == 'test'
  # Create
  %x{createdb -U #{user} -p #{port} #{name}}

  DB = Sequel.connect(url)

  # Load DB extensions
  %w(hstore).each do |ext|
    DB.run "create extension if not exists #{ext};"
  end

  # Migrate
  Sequel.extension :migration
  Sequel::Migrator.run(DB, SEQUEL_MIGRATION_DIR)
else
  DB = Sequel.connect(url)
end

C’est le seul changement que j’ai apporté à mon application !

Remarques

On peut très bien décider de persister le données de l’instance test avant l’extinction de la machine en utilisant un script à l’extinction. Pour faire ça on va copier les données depuis le système de fichiers en RAM vers le disque à l’exact inverse de ce qui se fait au démarrage.

Une architecture logicielle ingénieuse permet de diminuer les échanges avec la base de données. La couche de persistance doit idéalement être indépendante du reste de la logique de l’application.

On est donc en droit de se dire que cette optimisation n’est pas une nécessité. Si vous utilisez Rails et ActiveRecord, il y a de forte chance pour que la persistance soit au cœur votre application et que cette dernière en dépende totalement…

Dans le cas des tests d’intégration, on provoque des échanges avec la base de données de manière volontaire, c’est tout le principe.

Conclusion

Cette astuce ne s’appliquera pas forcément à tous vos projets, ni à toutes les configurations matérielles. Toutefois, si vous avez l’occasion de l’appliquer, je vous le recommande très vivement. Dans mon cas mes tests ont été 25 fois plus rapides, c’est tout ce que je vous souhaite.

L’équipe Synbioz.

Libres d’être ensemble.

Articles connexes

Ruby, Sidekiq et Crystal

25/07/2019

Dans le cadre d’une application web il nous est tous déjà arrivé de devoir effectuer une tâche assez longue en asynchrone pour ne pas gêner le flux de notre application. En Ruby la solution la plus...

Du dosage à la rouille

13/06/2019

Parlons de la rouille, cette délicieuse sauce qui accompagne nos soupes de poisson. Le bon dosage des ingrédients ravira le palais vos convives ! Pour faire une portion de rouille les ingrédients...

Une brève histoire d'Elixir et Erlang/OTP

31/01/2019

Je développe depuis plusieurs années en Ruby. Depuis mon arrivée chez Synbioz, j’expérimente en plus avec Elixir de façon assez naturelle. En quoi Elixir est-il différent, me demanderez-vous ? Pour...

Écrire une thread pool en Ruby

10/01/2019

Pouvoir exécuter plusieurs tâches en parallèle, que ce soit dans un script ou une application, peut être vraiment très utile, surtout dans le cas où le traitement de ces tâches peut être très long....