Déboguer une Application Spring Boot en Production : Outils et Méthodologie

Guide expert débogage Spring Boot production 2026 : thread dumps, heap dumps, profiling, distributed tracing. Méthodologie complète + outils.

Déboguer une Application Spring Boot en Production : Outils et Méthodologie

3h du matin. Alerte PagerDuty. Votre application Spring Boot en production :

  • Répond en 10 secondes au lieu de 200ms
  • Consomme 8 GB de RAM au lieu de 2 GB
  • CPU à 100% constant

Vous devez déboguer... MAINTENANT. Mais comment ?

  • ❌ Pas de debugger attaché (performances)
  • ❌ Pas de logs suffisants
  • ❌ Impossible de redémarrer (perte de contexte)

La réalité du débogage production : 90% de pression, 10% de données. Ce guide vous donne la méthodologie et les outils pour résoudre n'importe quel incident Spring Boot en production.

Nous couvrons :

1. Diagnostic rapide (5 minutes)

2. Thread dumps (deadlocks, threads bloquées)

3. Heap dumps (fuites mémoire)

4. Profiling live (CPU, allocations)

5. Distributed tracing (latences inter-services)

6. Méthodologie complète (checklist)

Phase 1 : Diagnostic Rapide (5 Minutes)

1.1 Métriques Spring Boot Actuator

Activer Actuator (si pas déjà fait) :


    org.springframework.boot
    spring-boot-starter-actuator
# application.yml
management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus,env,threaddump,heapdump
  endpoint:
    health:
      show-details: always
  metrics:
    export:
      prometheus:
        enabled: true

Endpoints critiques :

# 1. Santé applicative
curl http://localhost:8080/actuator/health

# 2. Métriques JVM
curl http://localhost:8080/actuator/metrics/jvm.memory.used
curl http://localhost:8080/actuator/metrics/jvm.threads.live
curl http://localhost:8080/actuator/metrics/process.cpu.usage

# 3. Métriques HTTP
curl http://localhost:8080/actuator/metrics/http.server.requests

# 4. Métriques custom
curl http://localhost:8080/actuator/metrics/order.processing.time

1.2 Dashboard Grafana Express

Requêtes Prometheus essentielles :

# CPU usage
process_cpu_usage{job="spring-boot-app"}

# Memory usage
sum(jvm_memory_used_bytes{job="spring-boot-app"}) by (area)

# GC time
rate(jvm_gc_pause_seconds_sum[5m])

# Thread count
jvm_threads_live{job="spring-boot-app"}

# HTTP latency P99
histogram_quantile(0.99,
  sum(rate(http_server_requests_seconds_bucket[5m])) by (le, uri)
)

# Error rate
sum(rate(http_server_requests_seconds_count{status=~"5.."}[5m]))
/
sum(rate(http_server_requests_seconds_count[5m]))

1.3 Checklist Diagnostic Initial

SymptômeMétrique à VérifierCause Probable
Lenteurhttp_server_requests_seconds{quantile="0.99"}DB query, external API
CPU 100%process_cpu_usage, thread countBoucle infinie, regex coûteux
RAM croissantejvm_memory_used_bytes, GC timeFuite mémoire, cache non borné
Erreurs 5xxLogs, tracesException non catchée
Timeoutshttp_server_requests_secondsDeadlock, DB connection pool saturé

Phase 2 : Thread Dump Analysis

2.1 Capturer un Thread Dump

Méthode 1 : Actuator (recommandé en prod)

curl http://localhost:8080/actuator/threaddump > threaddump_$(date +%s).json

Méthode 2 : jstack (si accès direct au pod)

# Récupérer PID
kubectl exec -it spring-app-pod -- jps

# Capturer thread dump
kubectl exec -it spring-app-pod -- jstack  > threaddump.txt

Méthode 3 : kill -3 (génère dump dans logs)

kubectl exec -it spring-app-pod -- kill -3 
kubectl logs spring-app-pod | tail -1000 > threaddump.txt

2.2 Analyser avec FastThread.io

# Upload sur https://fastthread.io/ pour analyse automatique
# Ou analyse manuelle :

# Trouver threads en BLOCKED
grep -A 20 "java.lang.Thread.State: BLOCKED" threaddump.txt

# Trouver threads en WAITING
grep -A 20 "java.lang.Thread.State: WAITING" threaddump.txt

2.3 Cas d'Usage : Deadlock Détection

Exemple de deadlock :

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007f8a9c004e00 (object 0x00000007ef600000, a java.lang.Object),
  which is held by "Thread-2"

"Thread-2":
  waiting to lock monitor 0x00007f8a9c002b00 (object 0x00000007ef600010, a java.lang.Object),
  which is held by "Thread-1"

Résolution :

// ❌ MAUVAIS : Deadlock possible
public class BankService {
    private final Object lock1 = new Object();
    private final Object lock2 = new Object();

    public void transfer(Account from, Account to) {
        synchronized (from) {  // Thread 1 lock from
            synchronized (to) {  // Thread 1 attend to (détenu par Thread 2)
                // Transfer logic
            }
        }
    }
}

// ✅ BON : Ordre d'acquisition cohérent
public class BankService {
    public void transfer(Account from, Account to) {
        Account first = from.getId() < to.getId() ? from : to;
        Account second = from.getId() < to.getId() ? to : from;

        synchronized (first) {
            synchronized (second) {
                // Transfer logic
            }
        }
    }
}

2.4 Thread Pool Saturation

Symptôme : Toutes les threads Tomcat en RUNNABLE/WAITING

"http-nio-8080-exec-1" #25 daemon prio=5 os_prio=0 tid=0x00007f8a9c1f0800 nid=0x1a waiting on condition
   java.lang.Thread.State: WAITING (parking)
        at sun.misc.Unsafe.park(Native Method)
        at java.util.concurrent.locks.LockSupport.park(LockSupport.java:175)
        at java.util.concurrent.LinkedBlockingQueue.take(LinkedBlockingQueue.java:442)

Diagnostic :

@Configuration
public class TomcatConfig {
    @Bean
    public WebServerFactoryCustomizer tomcatCustomizer() {
        return factory -> {
            factory.addConnectorCustomizers(connector -> {
                ProtocolHandler handler = connector.getProtocolHandler();
                if (handler instanceof AbstractProtocol) {
                    AbstractProtocol protocol = (AbstractProtocol) handler;

                    // Augmenter pool threads
                    protocol.setMaxThreads(400);  // Défaut: 200
                    protocol.setMinSpareThreads(50);  // Défaut: 10

                    // Connection timeout
                    protocol.setConnectionTimeout(20000);  // 20s

                    // Accept count (backlog)
                    protocol.setAcceptCount(200);
                }
            });
        };
    }
}

Phase 3 : Heap Dump Analysis

3.1 Capturer un Heap Dump

Méthode 1 : Actuator

curl -X POST http://localhost:8080/actuator/heapdump > heapdump_$(date +%s).hprof

Méthode 2 : jmap

kubectl exec -it spring-app-pod -- jmap -dump:format=b,file=/tmp/heapdump.hprof 

# Copier localement
kubectl cp spring-app-pod:/tmp/heapdump.hprof ./heapdump.hprof

Méthode 3 : Automatique sur OutOfMemoryError

# JVM options
JAVA_OPTS: >
  -XX:+HeapDumpOnOutOfMemoryError
  -XX:HeapDumpPath=/var/log/heapdump.hprof
  -XX:+ExitOnOutOfMemoryError

3.2 Analyser avec Eclipse MAT

Installation :

___CODE_BLOCK_15___

Analyses clés :

1. Leak Suspects Report (automatique)

- Identifie objets consommant le plus de mémoire

- Détecte patterns de fuites classiques

2. Dominator Tree

- Hiérarchie des objets par taille retained

- Identifier les "gros" objets

3. Histogram

- Nombre d'instances par classe

- Détecter explosions d'objets

Exemple de fuite :

Class Name                                 | Objects | Shallow Heap | Retained Heap
-------------------------------------------|---------|--------------|---------------
java.util.HashMap$Node[]                   | 1       | 64           | 2,147,483,648
  ↳ java.util.HashMap                      | 1       | 48           | 2,147,483,616
    ↳ com.example.CacheService             | 1       | 32           | 2,147,483,584

Diagnostic : CacheService retient 2 GB via HashMap non bornée.

Fix :

// ❌ MAUVAIS : Cache sans limite
@Service
public class CacheService {
    private final Map cache = new HashMap<>();

    public void put(String key, Object value) {
        cache.put(key, value);  // Jamais évicté !
    }
}

// ✅ BON : Cache avec LRU et limite
@Service
public class CacheService {
    private final Cache cache = Caffeine.newBuilder()
            .maximumSize(10_000)  // Limite 10k entries
            .expireAfterWrite(1, TimeUnit.HOURS)  // TTL 1h
            .recordStats()  // Métriques
            .build();

    public void put(String key, Object value) {
        cache.put(key, value);
    }

    public Object get(String key) {
        return cache.getIfPresent(key);
    }
}

3.3 Cas Pratique : Connection Pool Leak

Symptôme : HikariCP connections non libérées

Class Name                                 | Objects
-------------------------------------------|--------
com.zaxxer.hikari.pool.HikariProxyConnection | 50
com.zaxxer.hikari.pool.PoolEntry             | 50

Diagnostic :

// ❌ MAUVAIS : Connection non fermée
@Service
public class UserService {
    @Autowired
    private DataSource dataSource;

    public User findUser(long id) throws SQLException {
        Connection conn = dataSource.getConnection();
        PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?");
        stmt.setLong(1, id);
        ResultSet rs = stmt.executeQuery();

        if (rs.next()) {
            return mapUser(rs);
        }
        return null;
        // ❌ Connection jamais fermée !
    }
}

// ✅ BON : Try-with-resources
@Service
public class UserService {
    @Autowired
    private DataSource dataSource;

    public User findUser(long id) throws SQLException {
        try (Connection conn = dataSource.getConnection();
             PreparedStatement stmt = conn.prepareStatement("SELECT * FROM users WHERE id = ?")) {

            stmt.setLong(1, id);
            try (ResultSet rs = stmt.executeQuery()) {
                if (rs.next()) {
                    return mapUser(rs);
                }
            }
        }
        return null;
        // ✅ Auto-closed
    }
}

Phase 4 : Profiling en Production

4.1 Async Profiler (Low Overhead)

Installation :

# Télécharger Async Profiler
wget https://github.com/async-profiler/async-profiler/releases/download/v2.10/async-profiler-2.10-linux-x64.tar.gz
tar -xzf async-profiler-2.10-linux-x64.tar.gz

# Copier dans pod
kubectl cp async-profiler/ spring-app-pod:/tmp/

Profiling CPU :

# Démarrer profiling (30 secondes)
kubectl exec -it spring-app-pod -- \
  /tmp/async-profiler/profiler.sh -d 30 -f /tmp/cpu-profile.html 

# Copier résultat
kubectl cp spring-app-pod:/tmp/cpu-profile.html ./cpu-profile.html

# Ouvrir dans navigateur
open cpu-profile.html

Profiling Allocations :

# Profiler allocations mémoire
kubectl exec -it spring-app-pod -- \
  /tmp/async-profiler/profiler.sh -e alloc -d 30 -f /tmp/alloc-profile.html 

FlameGraph :

OrderService.processOrder (45%)
  ├─ validateOrder (20%)
  │   ├─ checkInventory (15%)  ← Hotspot !
  │   └─ validatePayment (5%)
  └─ saveOrder (25%)
      └─ JPA persist (25%)

Diagnostic : checkInventory consomme 15% CPU → optimiser requête SQL.

4.2 Arthas (Alibaba Toolkit)

Installation :

# Télécharger Arthas
curl -O https://arthas.aliyun.com/arthas-boot.jar

# Lancer dans pod
kubectl exec -it spring-app-pod -- \
  java -jar /tmp/arthas-boot.jar

Commandes utiles :

# 1. Dashboard temps réel
dashboard

# 2. Surveiller méthode spécifique
watch com.example.OrderService processOrder '{params,returnObj,throwExp}' -x 2

# 3. Tracer exécution méthode
trace com.example.OrderService processOrder -n 5

# 4. Profiler CPU (flame graph)
profiler start
# ... attendre 30s ...
profiler stop

# 5. Heap dump ciblé
heapdump --live /tmp/dump.hprof

# 6. Thread dump
thread -n 10  # Top 10 threads CPU

Exemple trace :

`---ts=2026-02-13 10:30:15;thread_name=http-nio-8080-exec-1;id=25;
    `---[2150.5ms] com.example.OrderService:processOrder()
        +---[0.5ms] com.example.OrderService:validateOrder()
        +---[2000ms] com.example.InventoryService:checkStock()  ← Bottleneck!
        |   `---[1995ms] org.springframework.jdbc.core.JdbcTemplate:query()
        `---[150ms] com.example.OrderRepository:save()

Diagnostic : Query SQL dans checkStock prend 2 secondes → ajouter index.

4.3 VisualVM (Remote Monitoring)

Configuration JMX :

# application.yml
spring:
  jmx:
    enabled: true

management:
  endpoints:
    web:
      exposure:
        include: "*"
  jmx:
    exposure:
      include: "*"

JVM Options :

JAVA_OPTS: >
  -Dcom.sun.management.jmxremote
  -Dcom.sun.management.jmxremote.port=9010
  -Dcom.sun.management.jmxremote.authenticate=false
  -Dcom.sun.management.jmxremote.ssl=false
  -Djava.rmi.server.hostname=localhost

Port-forward et connexion :

# Forward JMX port
kubectl port-forward spring-app-pod 9010:9010

# Ouvrir VisualVM
jvisualvm

# Connexion : localhost:9010

Phase 5 : Distributed Tracing

5.1 Spring Cloud Sleuth + Zipkin

Dependencies :


    org.springframework.cloud
    spring-cloud-starter-sleuth


    org.springframework.cloud
    spring-cloud-sleuth-zipkin

Configuration :

spring:
  sleuth:
    sampler:
      probability: 1.0  # 100% en dev, 0.1 (10%) en prod

  zipkin:
    base-url: http://zipkin:9411
    sender:
      type: web

Custom Spans :

@Service
public class OrderService {
    @Autowired
    private Tracer tracer;

    public Order processOrder(OrderRequest request) {
        Span span = tracer.nextSpan().name("process-order").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {

            span.tag("order.id", request.getId());
            span.tag("customer.id", request.getCustomerId());

            // Business logic
            Order order = createOrder(request);

            // Custom event
            span.event("order.created");

            return order;

        } finally {
            span.end();
        }
    }

    private Order createOrder(OrderRequest request) {
        Span span = tracer.nextSpan().name("create-order").start();
        try (Tracer.SpanInScope ws = tracer.withSpan(span)) {
            // DB operations
            return orderRepository.save(new Order(request));
        } finally {
            span.end();
        }
    }
}

Trace Visualization dans Zipkin :

Trace: a1b2c3d4e5f6
Total Duration: 2.35s

[Frontend] GET /api/orders (2.35s)
  └─ [API Gateway] route-request (2.30s)
      └─ [Order Service] process-order (2.25s)
          ├─ [Order Service] validate-order (0.05s)
          ├─ [Inventory Service] check-stock (2.00s)  ← Bottleneck!
          │   └─ [Database] SELECT inventory (1.95s)
          └─ [Payment Service] process-payment (0.20s)

5.2 OpenTelemetry (Standard 2026)

Migration vers OpenTelemetry :


    io.opentelemetry.instrumentation
    opentelemetry-spring-boot-starter
    2.0.0
otel:
  traces:
    exporter: otlp
  metrics:
    exporter: otlp
  exporter:
    otlp:
      endpoint: http://otel-collector:4317

Méthodologie Complète : Checklist

1. Triage Initial (< 5 min)

  • [ ] Vérifier dashboards Grafana (CPU, RAM, latency, error rate)
  • [ ] Consulter logs récents (kubectl logs ou Loki)
  • [ ] Vérifier health endpoint (/actuator/health)
  • [ ] Identifier pattern temporel (croissance linéaire ? spikes ?)

2. Catégorisation du Problème

SymptômeAction Prioritaire
CPU élevéThread dump → Async Profiler
RAM croissanteHeap dump → MAT
LatenceDistributed tracing → Zipkin
Erreurs 5xxLogs → Stack traces
DeadlockThread dump → FastThread

3. Investigation Approfondie

  • [ ] Capturer 3 thread dumps espacés de 10s
  • [ ] Capturer heap dump si RAM > 80%
  • [ ] Lancer profiler (Async Profiler ou Arthas)
  • [ ] Analyser traces distribuées (Zipkin/Jaeger)
  • [ ] Vérifier métriques DB (slow queries, connection pool)

4. Hypothèses et Tests

  • [ ] Formuler 2-3 hypothèses basées sur données
  • [ ] Tester hypothèses (ajouter logs, métriques)
  • [ ] Reproduire localement si possible
  • [ ] Rollback si dégradation critique

5. Résolution et Post-Mortem

  • [ ] Appliquer fix (code, config, infra)
  • [ ] Valider amélioration (métriques before/after)
  • [ ] Documenter incident (cause racine, timeline)
  • [ ] Ajouter monitoring préventif

Outils Recommandés (2026)

CatégorieOutilUse Case
MonitoringPrometheus + GrafanaMétriques temps réel
LoggingLoki + GrafanaLogs agrégés
TracingTempo + GrafanaTraces distribuées
ProfilingAsync ProfilerCPU, allocations (low overhead)
JVM DebugArthasInspection live JVM
Heap AnalysisEclipse MATFuites mémoire
Thread AnalysisFastThread.ioDeadlocks, threads bloquées
APMDatadog / New RelicAll-in-one (payant)

Conclusion

Le débogage en production nécessite :

  • Observabilité (métriques, logs, traces)
  • Outils non intrusifs (profilers low-overhead)
  • Méthodologie (triage → investigation → résolution)
  • Préparation (heap dump on OOM, JMX activé)

Checklist préparation production :

  • [ ] Actuator activé avec endpoints essentiels
  • [ ] Métriques exportées vers Prometheus
  • [ ] Distributed tracing configuré
  • [ ] Heap dump automatique sur OOM
  • [ ] Dashboards Grafana opérationnels
  • [ ] Runbooks d'incidents documentés

Maillage interne :

Le débogage production n'est plus de la magie noire. Avec les bons outils et la bonne méthodologie, vous résolvez 90% des incidents en moins de 30 minutes.

Êtes-vous prêt pour votre prochain incident ?