De nombreuses applications web font appel aux bases de données pour leur fonctionnement, et pour certaines opérations celles-ci peuvent se révéler lentes en particulier quand elles mettent en jeu de nombreuses données. Dans cet article nous voyons comment limiter ces effets.
Débloque les + belles offres tech en 10 mins
Les causes des ralentissements
Les ralentissements dans les fonctionnalités peuvent venir de deux causes différentes :
- Des requêtes très lentes à s’exécuter.
- De très nombreuses requêtes envoyées à la base dans l’implémentation d’une fonctionnalité.
Dans le premier cas, il peut être de bon aloi d’être accompagné d’un DBA pour améliorer les performances. En effet des requêtes peuvent être lentes à cause d’une mauvaise configuration de la base, ou parce que la requête en elle-même n’est pas efficace. Pour ce dernier cas, la fonctionnalité EXPLAIN de MySQL ou le plan d’exécution Oracle qu’on trouve dans SQL Developer peuvent être d’une grande aide.
Dans l’autre cas, il faut voir si ce n’est pas possible d’améliorer la situation. C’est ce que nous allons voir dans la suite de l’article.
Le pire du pire : les requêtes dans des boucles
La cinétique d’une requête à la base de données est la suivante :
- Etablissement de la connexion réseau
- Exécution de la requête par la base de données
- Retour de la requête
Le souci, c’est que tout ceci prend plusieurs millisecondes. Dans le cas d’une requête simple, le temps passé dans les connexions réseau occupe la majeure partie du temps d’exécution de la requête du point de vue de votre serveur d’application. On voit d’ailleurs ça très bien avec un profiler d’applications.
Dès lors, si on prend une requête en base exécutée plusieurs milliers de fois au travers d’une boucle, je vous laisse imaginer toutes les précieuses secondes perdues en connexions réseau. La solution : batcher vos requêtes !
Le batch de requêtes
Le batch de requêtes consiste à créer des requêtes qui vont contenir un nombre relativement important de données qu’on va récupérer en une fois. Ensuite, du côté applicatif, on va stocker le résultat dans un objet qui sera interrogé côté applicatif pour donner chacun des résultats demandés. Côté base de données, les opérateurs qui sont utilisés pour le batch sont IN
pour les cas simples et UNION
pour les cas plus complexes.
Par contre, n’essayez pas de tout remonter d’un coup sous peine soit d’avoir des erreurs de base de données parce que votre requête est trop longue ou renvoie trop de résultats, ou des ralentissements côté applicatif parce que vous sollicitez trop le garbage collector. Dans bien des cas des batchs traitant entre cent et mille lignes d’un coup permettent d’éviter ces effets de bord.
Les gains de performance obtenus
Dans une application chez un de mes clients, il m’a été demandé à un moment de créer une fonctionnalité avec un traitement en base impliquant de nombreuses requêtes, en fait il en fallait dix par enregistrement à remonter environ. Ainsi, dans le pire des cas où il y avait plus de mille enregistrements une requête au serveur web mettait plus d’une minute à répondre. Un audit du code au profileur a permis de faire ressortir que l’application passait son temps à attendre la base.
Dès lors, en optimisant le processus et en passant par un système de batch, on est tombé à dix requêtes en base pour mille enregistrements à remonter, en jouant avec les opérateurs IN
et UNION
pour les requêtes SQL. Dès lors dans le cas le pire évoqué précédemment on n’avait plus qu’une vingtaine de requêtes à exécuter, toutes très rapides. Et on est descendu de plus d’une minute à… cinq secondes pour un même traitement !
Ne faites pas de requêtes inutiles en base
Dans certains cas, il se peut qu’un ensemble de requêtes ne soit valide que pour des objets qui satisfont certaines conditions, et cet ensemble sera inopérant ou inutile dans tous les autres cas. Dès lors, rien ne sert d’effectuer ces requêtes pour tous les objets qui ne satisfont pas lesdits prérequis !
Au niveau du code, il suffit d’effectuer l’opération suivante :
if (maCondition.estSatisfaite(monObject)) {
faireTraitementsEnBase()
}
Une telle optimisation m’a permis là encore de gagner plusieurs secondes par requête dans le cadre d’une autre application.
Autre cas : une requête qui retourne le même objet dans 90% des cas.
Il m’est arrivé également de travailler sur une application qui contenait un algorithme comme suit :
for (obj: maListe) {
// obj contient ici une méthode getAccountId()
Account account = dao.getAccount(obj);
if (account.getId() == currentAccountId) {
doSomething(obj);
} else {
doSomethingElse(account, obj);
}
}
Le souci est dans cet algorithme est que dans l’immense majorité des cas le DAO retournait toujours la même valeur pour account
quelle que soit la situation dans laquelle il était appelé. Une optimisation était alors :
Map<Integer, Account> map = new HashMap<Integer, Account>();
for (obj: maListe) {
Integer accountId = obj.getAccountId();
if (accountId.intValue() == currentAccountId) {
doSomething(obj);
} else {
Account account = map.getAccount(accountId);
if (account == null) {
account = dao.getAccount(obj);
map.put(accountId, account);
}
doSomethingElse(account, obj);
}
}
Vu l’endroit où ce code était placé, l’optimisation en question a ramené encore 95% d’amélioration du temps de réponse sur les nombreuses fonctionnalités qui dépendaient de cet algorithme.
En conclusion
Les quelques exemples ici montrent ce qu’on peut faire avec les bases de données, mais plus généralement ça s’applique à de nombreuses ressources pour lesquelles les accès sont lents. Ainsi un jour j’ai eu à faire une application qui créait des fichiers Excel en se connectant à un OpenOffice fonctionnant en mode serveur. C’était à l’époque où Apache POI n’était pas encore très stable, donc pas taper d’accord ? 😉 Au début là encore j’avais commencé par remplir mon document cellule par cellule, sauf qu’il y avait 50000 lignes à faire et qu’au bout d’une heure ce n’était toujours pas fini. En batchant là encore, de façon à remplir le document par paquet de mille lignes, l’affaire était pliée en quelques secondes !
Débloque les + belles offres tech en 10 mins
Cet article vous a plu ? Vous aimerez sûrement aussi :
- Les transactions en Java/JEE
- Trucs et astuces pour l’optimisation des performances en Java/JEE
- Quelques principes d’architectures d’applis Web en Java/JEE
- La sécurisation des applications web
- Si vous mélangez Hibernate avec du SQL
Julien
Moi c’est Julien, ingénieur en informatique avec quelques années d’expérience. Je suis tombé dans la marmite étant petit, mon père avait acheté un Apple – avant même ma naissance (oui ça date !). Et maintenant je me passionne essentiellement pour tout ce qui est du monde Java et du système, les OS open source en particulier.
Au quotidien, je suis devops, bref je fais du dév, je discute avec les opérationnels, et je fais du conseil auprès des clients.
Un petit article sur le traitement en batch est il envisageable ?