Optimisez vos applications java (JDBC) sous oracle

La démarche d’optimisation des performances

Dans un contexte d’optimisation applicative sous Oracle, on considère souvent trois axes (niveaux) d’optimisation :

 

– Hardware : Optimisation de l’infrastructure (passer les redos, les tablespaces les plus sollicités sur des disques plus performants de type SSD par exemple, jouer sur la RAM ou la CPU disponible mais aussi considérer les débits réseaux, …

– Middleware : Optimisation de l’instance (ce qu’on appelle plus couramment le tuning BDD, inutile pour autant de tenter d’ajouter un spoiler ou un cône d’admission d’air à votre base pour qu’elle aille plus vite, quelques paramètres bien ciblés font parfois des miracles ;))

– Software : Optimisation du code applicatif (avec des index ciblés, du partitionning ou même encore quand cela est possible des réécritures de requêtes)

Idéalement, une optimisation efficace se fait conjointement selon ces trois axes.

Cependant, ce qui le plus souvent définit l’axe que l’on favorise est le ratio investissement/gain.
L’investissement peut être temporel (temps passé) ou financier (coût d’acquisition du matériel).
Si un code inefficace peut être compensé par une infrastructure performante, l’investissement matériel (selon le cycle de vie de l’infrastructure en question) peut être retardé en favorisant l’optimisation du code applicatif souvent plus chronophage (car elle nécessite la coopération des équipes applicatives, des développeurs ou éditeurs).

Quant au gain attendu, il doit être défini avant de commencer l’optimisation car, selon ce sur quoi on agit, le gain obtenu peut être global (meilleurs performances sur toute l’application) ou ciblé (tel écran, requête ou batch répond plus rapidement).

Optimisation applicative Java, un piège à éviter

Comme évoqué précédemment, l’optimisation applicative reste souvent la plus délicate parce qu’elle implique souvent des acteurs variés.
De plus, l’utilisation de plus en plus commune de Framework rend parfois la tâche encore moins évidente. En effet, la structure des requêtes ainsi que le nombre de résultats renvoyés par la base à l’applicatif (En rapport à l’information réellement attendue par l’utilisateur) ne sont pas toujours efficients ni maîtrisables.
Parfois, de grandes quantités de données transitent entre la BDD et l’applicatif afin d’alimenter un cache applicatif. Si l’on ne peut pas toujours remettre ce fonctionnement en cause, il est en revanche possible de l’améliorer.

Quand les requêtes à optimiser ne sont pas directement identifiées par les équipes applicatives, l’utilisation de statpack (en SE) ou de rapport AWR (en EE) permet de cibler, par les TOPs, les requêtes les plus consommatrices. Cependant, il faut bien comprendre que seuls les temps de Parse et d’Execute sont pris en considération.

Ainsi, dans plusieurs contextes, même après optimisation (par création d’index par exemple) d’une requête renvoyant un grand nombre de résultats, une incohérence entre le temps d’exécution constaté dans un plan d’exécution généré sous SQL*Net (avec autotrace), et le temps constaté côté applicatif pour exploiter le résultat de la requête peut être le signe d’un problème de Fetch par le driver JDBC.

En effet, l’utilisation de SQL*Net masque le problème inhérent à l’utilisation du Driver JDBC. Il est cependant possible d’identifier le problème en générant une trace de la requête avec l’utilitaire tkprof (On peut alors constater que le plus gros de l’elapsed est lié au Fetch).

Explication du phénomène

Par défaut, JDBC transite une quantité limitée de lignes par paquet (10 pour être précis).
Ainsi, une requête qui renvoi 2000 lignes via JDBC à l’application java devra envoyer 200 paquets de 10 lignes. Ce fonctionnement impose beaucoup d’allers-retours entre la database et l’applicatif java et peut engendrer de très mauvaises performances.
A l’inverse, si l’on fait le choix de transiter les lignes par paquets de 100, il ne faudra alors plus que 20 paquets … et si l’on fait maintenant le choix de transiter 1000 lignes par paquet, il ne faudra donc plus que 2 paquets.

Il n’y a pas de valeur idéale et seuls des tests permettent d’identifier la situation la plus adéquate. Cependant, dans plusieurs contextes, une valeur entre 500 et 1000 permettait des gains très significatifs (Temps de fetch divisé par 10 à 20). Au-delà, le gain devenait peu perceptible et la capacité de l’applicatif Java à traiter plus de lignes simultanément aurait pu être mis à l’épreuve.

Mise en application

Si l’utilisation de l’API JDBC permet de définir le nombre de lignes par paquet pour une requête (voir setFetchSize), la plupart des framework java (Spring, Hibernate, …) proposent aussi une fonction permettant de définir le nombre de lignes par paquet sur toutes les connexions initialisées.

Conclusion

Si cette optimisation nécessite une modification du code applicatif, sa mise en œuvre est relativement simple et rapide. Elle a l’avantage de cibler l’ensemble des requêtes renvoyant un grand nombre de lignes. Elle s’accompagne idéalement d’une optimisation ciblée sur les requêtes dont le temps d’exécution peut être amélioré.

Pour aller plus loin