"Soleil de printemps
Boîte aux lettres repeinte
Dégoulinades jusqu’à terre"
Hara Sekitei (1889-1951)
Ah mais tout de même. Enfin un article dont nos amis programmeurs comprennent le titre et savent de quoi-t-on va parler, voilà qui ne serait pas du luxe. C'est que tout développeur ne connaît que ça, la base de données. Il l'appelle, la rappelle, lui envoie des petits coucous, prend de ses nouvelles, discute avec elle du temps qu'il fait, et ce toute la journée durant. Eh bien, une fois de plus, c'est malheureux de toujours en arriver là, les souris vertes doivent mettre les pieds dans le plat et mettre le holà à cette idylle apparemment parfaitement innocente. La manipulation sans précaution de notre gentille base de données est LA cause noumero uno (una ?) d'inefficacité des programmes dans le monde, et donc une alliée bien malgré elle du gaspillage informatique à tous les étages, et du réchauffement climatique qui s'ensuit.
Il va donc nous falloir renverser la tête pour arrêter de marcher sur la vapeur, et ce même si cette si cette image vous laisse aussi perplexe que moi. Essayons de comprendre pourquoi la base de données n'est pas le formidable couteau suisse du programmeur que l'on imagine souvent, mais plutôt sa version masse d'armes de vingt kilos à pommeau incrusté que l'on maniera avec la délicatesse d'Hercule, en fin de journée et un peu pressé de finir son onzième travail sur douze.
A la vitesse de l'électron, mais avec un boulet au pied
La base de données est, c'est bien connu, la meilleure manière en programmation de ranger ses petites affaires et de les avoir sous le coude instantanément. La meilleure, vraiment ? Commençons par un petit rappel salutaire directement recopié, sans reversement aux ayants droits, de notre magnifique dossier sur les grandeurs numériques, en se remémorant les ordres de grandeur de latence suivants :
- depuis le cache processeur : 0, négligeable, rien du tout, niente, le processeur travaille directement avec son petit cache qu'il garde auprès de lui
- depuis la mémoire vive (ou RAM pour les intimes) : 10 ns, soit 0,00001ms
- depuis un disque dur SSD : 0,01 à 0,1ms
- depuis un disque dur à plateau : 1 à 10ms
- à travers un appel réseau : de 0,1 à 1ms si on est sur un réseau local très performant, de l'ordre de 30ms si l'on est à travers des connexions distantes
Mais pourquoi donc rappeler tout cela, me demande une souris qui gratte ses mignonnes petites oreilles de consternation ? C'est qu'il nous faut nous poser la question : où est notre base de données là-dedans ? En fait, elle est à cheval sur trois catégories : elle travaille pour partie en mémoire vive (grâce à un cache plus ou moins performant), pour une grosse partie depuis le disque dur, mais il faut lui ajouter une petite latence réseau, car elle est en général déportée sur une autre machine et, quand ça n'est pas le cas, toute la couche des appels réseaux est utilisée quand même pour des appels en local.
Alors, nous voyons tout de suite le problème d'appeler sans arrêt notre base de données : à chaque appel, on paye quelques millisecondes, voire beaucoup plus quand on fait des requêtes maousse costaudes, et des millisecondes qui s'entassent, eh bien ça fait des secondes, puis finalement des heures, voire des jours. Alors que l'on voit tout de suite que si nos données étaient toutes en mémoire, eh bien on irait constamment à la vitesse de la mémoire vive, soit 100 000 fois plus vite, excusez du peu. L'idéal étant même de toujours s'arranger pour travailler dans le cache processeur, là on tombe carrément dans la vitesse intersidérale absolue, sauf que c'est bien difficile à réaliser en pratique si l'on ne programme pas dans un langage qui permet de gérer finement l'allocation mémoire, et désormais très peu de langages modernes l'autorisent, vu que c'est aussi une source d'erreurs fatales et de cataclysmes sans nom. Cela dit, nous ne sommes tout de même pas obligés de nous mettre les mêmes contraintes pour notre petit programme que pour la séquence de lancement d'une fusée en temps réel, donc déjà si on arrête de spammer la base de données à tout va et qu'on sollicite un peu plus la mémoire vive de notre machine on sera bien content.
Bien entendu, il n'y a pas équivalence stricte entre ces méthodes de stockage de données : les données en mémoire vive ou dans un cache processeur sont volatiles, alors que dans votre base de données, elles seront persistantes (enfin si tout va bien). Remarquons d'ailleurs la tendance bien navrante de certains programmeurs à utiliser leur base de données comme fourre-tout pour y mettre des données qui n'ont aucun intérêt à être stockées, comme des résultats de calculs intermédiaires, des caches temporaires, etc, tout ça parce que c'est bien facile de tout ranger dans le gros placard que de travailler un peu à gérer des structures de données en mémoire. Résultat des courses, c'est la double peine : vous perdez en performance à appeler sans cesse votre base de données, et en plus vous consommez du stockage inutilement pour des données qui peuvent être recalculées à la demande.
Il existe aujourd'hui des bases de données non relationnelles fringantes, ou encore des systèmes de cache clé-valeur excessivement plus performants qu'une base de données traditionnelle, comme Redis pour n'en citer qu'un, et qui seront bien utiles dans certains contextes, comme le fait de partager certaines données volatiles entre plusieurs serveurs. Cela dit, même quand ils travaillent exclusivement en mémoire, bravo à eux, ils resteront toujours nettement moins performants que l'utilisation de la mémoire vive au sein même de la zone d'exécution de votre propre code : vous évitez ainsi de payer une petite (ou grosse) latence réseau, ainsi que tout un traitement lié au protocole de transfert aller-retour à la base et son interprétation dans le langage de programmation que vous êtes en train d'utiliser, bref un tas de choses qui ne coûtent pas bien cher si on se sert de sa base de données avec parcimonie, mais qui commencent à chiffrer sévèrement dès qu'on fait tourner plusieurs milliers de fois par seconde notre petit code.
Allô, la base de données ?
Bien bien, on vous a compris, les souris, on jette la base de données à la poubelle et on ne fait plus que du code en mémoire, merci et salut. Attendez ! Ne partez pas si vite, on ne va pas se quitter sur un malentendu pareil : il n'est pas question de mettre toute notre application en mémoire partout et tout le temps, sinon on a remplacé une nuisance par une autre. De toute manière c'est tout simplement impossible, car vous avez sans doute remarqué qu'on est loin de pouvoir mettre autant de mémoire vive sur une machine qu'elle n'a de stockage disponible. Si votre application consomme 100 Go sur disque dur, vous aurez bien du mal à obtenir l'équivalent pour passer l'ensemble en mémoire vive, même à considérer que ça prendrait strictement la même place, ce qui n'est pas vrai (le stockage en mémoire consomme toujours légèrement plus de place que la donnée elle-même).
Bon, donc on ne jette pas notre base de données à la poubelle, au contraire même : c'est tout de même une technologie super robuste pour stocker des données de manière persistante et garantir leur intégrité, beaucoup mieux que s'il fallait faire ça soi-même par des bouts de fichiers, ou tout autre technique tordue qui vous viendrait à l'esprit. Simplement on va s'interdire d'appeler trop souvent notre base de données, pour ne pas payer le coût prohibitif de l'appel à chaque fois qu'on a besoin d'une donnée. Imaginez un peu un code de 100 lignes qui appelle la base de données toutes les 10 lignes pour faire un traitement. Disons qu'on perd 1ms à chaque appel, ce qui est déjà très optimiste, donc notre code s'exécute en 10ms. Si on a réussi à faire tous ces appels en une fois, on ne paye déjà plus que 1ms, on a déjà gagné un facteur 10. Mais si en plus on a anticipé, et préparé dans une autre section de code ces données et qu'elles sont déjà en mémoire, là notre bout de code s'exécute de manière quasi instantanée.
Alors évidemment, ceci supppose d'arrêter de manier à la truelle un concept de programmation particulièrement en vogue, mais qui est une ineptie en terme de performances : l'absence de contexte. Je fais ma petite fonction qui s'occupe de son petit calcul et ne sait rien du reste du monde, mieux encore, je l'appelle au travers d'une API comme ça c'est super étanche, elle ne sait rien de mon application, c'est l'insouciance bienheureuse de l'ignorance béate. Ces principes sont très sains appliqués à une certaine échelle, pour éviter ce qu'il faut bien appeler un sac de noeud géant qui constitue malheureusement la réalité sordide de bien des projets informatiques ayant dérivé dans la nuit, mais ils sont franchement délétères quand on les reporte à une échelle microscopique, puisque l'on va finir par payer un coût exhobitant à chaque micro-étape de notre programme.
Aux Souris Vertes, nous avons un Principe Majeur s'agissant de structurer correctement son application : toute partie du code qui utilise un tant soit peu intensément les données doit pouvoir disposer de la logique complète de la manière dont elles sont structurées, et même la guider : on stocke toujours ses données de la manière la plus adéquate pour les reprendre ensuite, et pas selon un schéma préétabli qui fait beau sur le papier, mais qui est totalement impraticable pour la machine. C'est quand même elle qui travaille à la fin, non mais.
Résumons en quelques savants conseils percutants notre manière de gérer la base de données en Programmeur Responsable :
- mutualiser autant que possible les appels successifs, quitte à organiser le code autrement. Par exemple, je dois récupérer la date de naissance d'un utilisateur à partir de son login à chaque fois qu'il se connecte. Soit je fais un appel à chaque connexion, soit je charge une fois pour toute en mémoire une relation
- maîtriser la logique globale de l'accès aux données dans l'application, en particulier savoir quelles parties de code écrivent ou lisent dans la base de données pour ne pas faire de traitements redondants ou des appels inutiles, pour des données que vous auriez pu vous repasser d'un bout à l'autre du code.
- stocker les données sous la forme la plus adaptée à la manière dont elles sont ensuite extraites. Le paradigme des bases de données relationnelles, je-mets-tout-à-plat-et-après-je-fais-30-jointures-si-j'ai-besoin, est en ce sens désastreux. En principe, on sait toujours comment une donnée est récupérée (toute seule ou en groupe, filtrée selon un attribut particulier, etc) et il faut en tenir compte dans la manière de structurer son stockage en base. Rappelons également que la création d'index à tout va quand on se rend compte que les performances sont abyssales, généralement lorsque l'on requête sur un ou des attributs que l'on n'avait pas anticipés, n'est pas une méthode viable : chaque nouvel index créé peut dépasser la taille des données brutes stockées, ce qui fait qu'on consomme du stockage à gogo sans vraiment corriger le problème, qui se trouve, comme presque toujours, au sein de la logique que déroule notre code.
Voilà, nous n'allons pas multiplier les conseils aujourd'hui, le principe est simple : on contrôle finement les appels à notre base de données, de manière à en faire le moins souvent possible. Et une fois qu'on a pris nos petites habitudes, on peut les appliquer même quand il n'y a pas forcément un enjeu important de performances, car il n'y a pas de raison de gaspiller des ressources machine, même quand la différence n'est pas perceptible à l'oeil nu. Bien sûr, on cherchera toujours un équilibre entre la complexité du code à écrire et à maintenir, et le gain réel en performance.
"Ciel d'hiver -
La neige répond
A l'appel du rocher"
Midoriro no Mausu (la Souris Verte)