Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Performance de backend Spring : les techniques ...

Sponsored · Your Podcast. Everywhere. Effortlessly. Share. Educate. Inspire. Entertain. You do you. We'll handle the rest.

Performance de backend Spring : les techniques que tout dev devrait connaître

Découvrez les techniques essentielles pour booster les performances de vos backends Spring : bon paramétrage des entités JPA, lectures de données efficaces et N+1 Select, insertions par blocs, modifications par lot, logs et métriques d'Hibernate, pool de connexion, gestion des transactions, cache, virtual threads, mesures avec Gatling et autres.

Avatar for Florian Beaufumé

Florian Beaufumé

April 27, 2026

More Decks by Florian Beaufumé

Other Decks in Programming

Transcript

  1. • Architecte logiciel et développeur senior • Expert Java/Spring/backend •

    Freelance depuis 15+ ans • https://beaufume.fr/articles/ • @fbeaufume Florian Beaufumé
  2. • Identifier • L'anecdote strcat • Logs • Métriques Hibernate

    • Traquer une requête SQL • Optimiser • Persistance : lectures, insertions, mises à jour • Pool de connexion à la BD • Transactions • Cache • Virtual threads • Tests • Mesurer • Gatling Sommaire @fbeaufume beaufume.fr
  3. • Application cliente C++ • Un traitement est long (plus

    de 10 secondes) • Boucle qui concatène de très nombreuses petites chaines via strcat : Strcat @fbeaufume char* inputs[] = { "foo", "bar", "acme", ... }; char result[100000] = ""; for (int i = 0; i < inputs_size; i++) { strcat(result, inputs[i]); // Append inputs[i] to result } return result; beaufume.fr
  4. • Implémentation simplifiée de strcat : • Comportement pour plusieurs

    appels : Strcat char* strcat(char* destination, char* source) { char* pos = destination; // The current position while (*pos != '\0') { pos++; } // Find the end of destination while (*source != '\0') { *pos = *source; pos++; source++; // Copy the characters } *pos = '\0'; return destination; } Itération 1 Itération 2 Itération 3 Find Find Find Copy Copy Copy 1 2 ✘ ✘ @fbeaufume beaufume.fr
  5. • Alternative optimisée : • Comportement pour plusieurs appels :

    Strcat char* strcat2(char* destination, char* source) { char* pos = destination; // The current position while (*pos != '\0') { pos++; } // Find the end of destination while (*source != '\0') { *pos = *source; pos++; source++; // Copy the characters } *pos = '\0'; return destination pos; } Itération 1 Itération 2 Itération 3 Find Copy Copy Copy @fbeaufume beaufume.fr
  6. Logs web INFO c.a.s.AccessLogFilter : Served GET '/foo' to 'john.doe'

    as 200 in 16 ms @Component public class AccessLogFilter implements Filter { @Override public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { long duration = -System.currentTimeMillis(); ... try { chain.doFilter(req, res); } finally { duration += System.currentTimeMillis(); String url = ... // From request object String username = ... // From Spring Security LOGGER.info("Served {} '{}' to '{}' as {} in {} ms", httpReq.getMethod(), url, username, httpRes.getStatus(), duration); } } } Simplifié @fbeaufume
  7. • Logs SQL : • Log des requêtes SQL lentes

    : Logs Hibernate spring.jpa.show-sql=true INFO org.hibernate.SQL_SLOW : SlowQuery: 137 milliseconds. SQL: select b.id (...) spring.jpa.properties.hibernate.log_slow_query=100 Hibernate: select b.id, b.name from book b Configuration Spring Extrait de log @fbeaufume Extrait de log tronqué beaufume.fr
  8. • Activation : • API : Métriques Hibernate spring.jpa.properties.hibernate.generate_statistics=true //

    To get the SessionFactory from the EntityManagerFactory, if needed SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class); Statistics stats = sessionFactory.getStatistics(); // The number of sessions opened by this session factory so far long openedSessionCount = stats.getSessionOpenCount(); // The number of inserts for the book entity EntityStatistics bookEntityStats = stats.getEntityStatistics(Book.class.getName()); long bookInsertCount = bookEntityStats.getInsertCount(); // The average duration of a query String[] queries = stats.getQueries(); QueryStatistics queryStats = stats.getQueryStatistics(queries[0]); long averageDuration = queryStats.getExecutionAvgTime();
  9. 1. Identifier la requête dans les logs 2. Ajouter breakpoint

    conditionnel dans SqlStatementLogger.logStatement : 3. Exécuter le traitement 4. Remonter la pile d'appel Trouver le code exécutant une requête SQL @fbeaufume beaufume.fr
  10. JPA - Relations Fetch type Mode de chargement Valeur par

    défaut pour LAZY Différé A la demande @OneToMany et @ManyToMany EAGER Immédiat Systématique @OneToOne et @ManyToOne @fbeaufume beaufume.fr
  11. • "Configurer en lazy, mais charger en eager." 1. Configurer

    ses relations en lazy : 2. Cas par cas, charger en eager l'ensemble des données : JPA - Chargement des relations @Entity public class Book { @ManyToOne(fetch = FetchType.LAZY) private Author author; @Query("FROM Book b JOIN FETCH b.author") List<Book> findAllWithAuthor(); @fbeaufume beaufume.fr
  12. • @OneToOne et @ManyToOne sont EAGER par défaut • C'est

    généralement mauvais pour les performances • Mesures en EAGER : • Encore plus de requêtes quand plusieurs ToOne JPA - Comportement de EAGER Book Author N 1 @ManyToOne en EAGER Cas Type de lecture Méthode Requêtes SQL 1 One bookRepository.findById 1 2 One @Query("FROM Book b WHERE b.id=:id") 2 ✘ 3 One API Criteria 2 ✘ 4 All bookRepository.findAll 1+N ✘ 5 All @Query("FROM Book b") 1+N ✘ 6 All API Criteria 1+N ✘
  13. • Avec (LEFT) JOIN FETCH : • Avec entity graph

    : • Ce qui charge les données en 1 requête plutôt que 1+N+M ✘ : JPA - Chargement des relations @Query("FROM Book b JOIN FETCH b.author a JOIN FETCH a.address") List<Book> findAllWithAuthorAndAddress(); @EntityGraph(attributePaths = {"author", "author.address"}) @Query("FROM Book b") List<Book> findAllWithAuthorAndAddress(); Book Author N 1 @ManyToOne en LAZY Address 1 N @ManyToOne en LAZY SELECT ... FROM book b JOIN author ... JOIN address ... @fbeaufume beaufume.fr
  14. • Avec entity graph nommé : • Avec API Criteria

    : JPA - Chargement des relations root.fetch("author").fetch("address"); @EntityGraph("Book.withAuthorAndAddress") @Query("FROM Book b") List<Book> findAllWithAuthorAndAddress(); @Entity @NamedEntityGraph( name = "Book.withAuthorAndAddress", attributeNodes = { @NamedAttributeNode(value = "author", subgraph = "author-graph") }, subgraphs = { @NamedSubgraph(name = "author-graph", attributeNodes = @NamedAttributeNode("address")) }) public class Book { BookRepository @fbeaufume beaufume.fr
  15. • JOIN FETCH, entity graph, etc, fonctionnent aussi avec des

    ToMany • Mais pour chaque ToMany les rows sont multipliés • De plus pour plusieurs ToMany il faut un type Set • Envisager une seule ToMany à la fois et laisser Hibernate fusionner JPA - Relations ToMany @fbeaufume beaufume.fr
  16. JPA - Relations ToMany // FROM Driver d LEFT JOIN

    FETCH d.cars driverRepository.findAllWithCars(); // FROM Driver d LEFT JOIN FETCH d.bikes return driverRepository.findAllWithBikes(); Driver Car 1 20 Bike 30 1 2 requêtes (200 rows puis 300 rows) Sans optimisation : 21 requêtes ✘ 1 (10 rows) + 10 (20 rows) + 10 (30 rows) // FROM Driver d // LEFT JOIN FETCH d.cars // LEFT JOIN FETCH d.bikes return driverRepository.findAllWithCarsAndBikes(); 1 requête (10x20x30 = 6000 rows) 10x Solution 1 : double JOIN FETCH Solution 2 : deux JOIN FETCH @fbeaufume beaufume.fr
  17. JPA - Ecritures # Active le batching des inserts et

    updates spring.jpa.properties.hibernate.jdbc.batch_size=50 # Permet de réordonner les inserts multi-types pour les batcher spring.jpa.properties.hibernate.order_inserts=true # Permet de réordonner les updates multi-types pour les batcher spring.jpa.properties.hibernate.order_updates=true @fbeaufume beaufume.fr
  18. • Par auto-incrément : • Par séquence : JPA -

    Génération d'ID @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "book-gen") @SequenceGenerator(name = "book-gen", sequenceName = "book_seq", allocationSize = 20) private Long id; @Entity public class Book { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @fbeaufume beaufume.fr
  19. JPA - Insertion d'entités insert into ... IDENTITY SEQUENCE (simplifié,

    pour INCREMENT et allocationSize à 20) 1 21 22 2 insert into ... insert into ... insert into ... select nextval ... insert into ... insert into ... select nextval ... insert into ... insert into ... Total pour 100 entités : 100 SQL Total pour 100 entités : 105 SQL 3 insert into ... insert into ... 20 40 #1 #2 #3 #21 #22 #2 #3 #22 #1 #21
  20. • Grouper les insertions : • Mesures pour 100 insertions

    : JPA - Insertions spring.jpa.properties.hibernate.jdbc.batch_size=50 spring.jpa.properties.hibernate.order_inserts=true Cas Configuration Appels (seq + insert) 1 IDENTITY 0 + 100 ✘ 2 SEQUENCE, allocSize=20 5 + 100 ✘ 3 SEQUENCE, allocSize=1, batch_size=50 100 + 2 ✘ 4 SEQUENCE, allocSize=20, batch_size=50 5 + 2 @fbeaufume beaufume.fr
  21. • Augmentation de salaire : • Solution 1 : •

    Solution 2 : JPA - Modifications par blocs @Modifying @Query("UPDATE Employee e SET e.salary = e.salary * ?2 WHERE e.department = ?1") int applyRaise(String department, float amount); @Transactional public void applyRaise(String department, float amount) { employeeRepository.findByDepartment(department) .forEach(e -> e.applyRaise(amount)); } 1 SELECT + N UPDATE ✘ 1 UPDATE @fbeaufume beaufume.fr spring.jpa.properties.hibernate.jdbc.batch_size=50 1 SELECT + N/50 BATCH UPDATE
  22. Pool de connexion à la BD # Nombre maximum de

    connexions dans le pool # 10 par défaut spring.datasource.hikari.maximum-pool-size=20 # Nombre minimum de connexions dans le pool # 'maximum-pool-size' par défaut pour un pool de taille fixe spring.datasource.hikari.minimum-idle=5 # Quand 'minimum-idle' < 'maximum-pool-size', # durée avant liberation pour une connexion inutilisée # 600000 (10 minutes) par défaut spring.datasource.hikari.idle-timeout=300000 # Durée maximale d'attente par l'appli d'une connexion avant de lever une exception # Compromis entre attendre et échouer # 30000 (30 sec) par défaut spring.datasource.hikari.connection-timeout=10000 Exemple de configuration Hikari pour Spring Boot, voir aussi https://github.com/brettwooldridge/HikariCP @fbeaufume beaufume.fr
  23. Transactions Objectif Bénéfice @Transactional @Transactional (readOnly=true) Pas de transaction Intégrité

    ACID Dirty checking Empêche les modifications Performances Cache JPA de niveau 1 Moins d'overhead Faible rétention des cnx BD @fbeaufume beaufume.fr
  24. • Consommation mémoire ou disque • Structure : • Perte

    de fraicheur des données Cache - Limitations @fbeaufume Back #1 Back #2 Back #1 Back #2 Back #1 Back #2 Cache Cache Cache Cache Incohérence Coordination Latence Cache local Cache distribué Cache serveur beaufume.fr
  25. • Cache Spring : • Caches JPA/Hibernate : • Et

    du paramétrage (taille de cache, expiration, etc) Cache - APIs @Cacheable("books") public Book getByIsbn(String isbn) { ... } @QueryHints({@QueryHint(name = "org.hibernate.cacheable", value = "true")}) List<Book> findByGenre(String genre); BookService @Entity @Cacheable @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) public class Book { BookRepository Entité Cache de requêtes Cache d'entités @fbeaufume beaufume.fr
  26. • Gains mémoire et CPU sous charge • Spring Boot

    3.2+ et Java 21+ mais préférer Java 24+ (contre problème de thread pinning sur synchronized) • Activation : Virtual threads spring.threads.virtual.enabled=true Traitement métier Appel BD Threads classiques Virtual threads Thread X Y X @fbeaufume beaufume.fr
  27. • Envisager @Transactional et @Before/AfterAll plutôt que @Before/AfterEach : Tests

    Spring - Initialisation des données @SpringBootTest @Transactional public class ItemServiceTest { @BeforeAll static void beforeAll(@Autowired ItemRepository itemRepository) { itemRepository.saveAll(...); } @AfterAll static void afterAll(@Autowired ItemRepository itemRepository) { itemRepository.deleteAll(); } @Test void deleteItem() throws Exception { ... } // Other test methods } @fbeaufume beaufume.fr
  28. • Spring Test réutilise les application contexts quand c'est possible

    • Des application contexts différents : Tests Spring - Cache des contextes @SpringBootTest class Sample1Test { @SpringBootTest(webEnvironment = RANDOM_PORT) class Sample3Test { @WebMvcTest // Or @DataJpaTest class Sample5Test { @SpringBootTest class Sample4Test { @MockitoBean FooService foo; @SpringBootTest // Or @AutoConfigureRestTestClient for SB 4+ @AutoConfigureMockMvc class Sample2Test { 1 2 3 4 5 Tests d'intégration Tests unitaires ou librairie mock-in-bean En choisir un, dans une classe mère @fbeaufume beaufume.fr
  29. Gatling - Simulation public class DemoSimulation extends Simulation { int

    userCount = Integer.getInteger("userCount", 20); int rampUpDurationSeconds = Integer.getInteger("rampUpDuration", 10); int totalDurationSeconds = Integer.getInteger("totalDuration", 60); HttpProtocolBuilder httpProtocol = http.baseUrl("http://localhost:8080") .check(status().is(200)); ScenarioBuilder scenario = CoreDsl.scenario("My Scenario") .exec(http("Page 1").get("/pause?duration=100&random=10")); { setUp(scenario.injectClosed( rampConcurrentUsers(0).to(userCount).during(rampUpDurationSeconds), constantConcurrentUsers(userCount) .during(totalDurationSeconds - rampUpDurationSeconds)) .protocols(httpProtocol)) .assertions(global().successfulRequests().percent().is(100d)); } } Temps Utilisateurs 10s 60s 20 1 2 3 4 5 5 @fbeaufume beaufume.fr
  30. Gatling - Résultats Synthèse de la simulation Temps de réponse

    des pages Temps de réponse dans le temps Distribution des temps de réponse @fbeaufume beaufume.fr