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.
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: trueEndpoints 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.time1.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ôme | Métrique à Vérifier | Cause Probable |
|---|---|---|
| Lenteur | http_server_requests_seconds{quantile="0.99"} | DB query, external API |
| CPU 100% | process_cpu_usage, thread count | Boucle infinie, regex coûteux |
| RAM croissante | jvm_memory_used_bytes, GC time | Fuite mémoire, cache non borné |
| Erreurs 5xx | Logs, traces | Exception non catchée |
| Timeouts | http_server_requests_seconds | Deadlock, 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).jsonMé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.txtMé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.txt2.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.txt2.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).hprofMé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.hprofMéthode 3 : Automatique sur OutOfMemoryError
# JVM options
JAVA_OPTS: >
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/var/log/heapdump.hprof
-XX:+ExitOnOutOfMemoryError3.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,584Diagnostic : 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 | 50Diagnostic :
// ❌ 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.htmlProfiling 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.jarCommandes 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 CPUExemple 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=localhostPort-forward et connexion :
# Forward JMX port
kubectl port-forward spring-app-pod 9010:9010
# Ouvrir VisualVM
jvisualvm
# Connexion : localhost:9010Phase 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: webCustom 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:4317Méthodologie Complète : Checklist
1. Triage Initial (< 5 min)
- [ ] Vérifier dashboards Grafana (CPU, RAM, latency, error rate)
- [ ] Consulter logs récents (
kubectl logsou Loki) - [ ] Vérifier health endpoint (
/actuator/health) - [ ] Identifier pattern temporel (croissance linéaire ? spikes ?)
2. Catégorisation du Problème
| Symptôme | Action Prioritaire |
|---|---|
| CPU élevé | Thread dump → Async Profiler |
| RAM croissante | Heap dump → MAT |
| Latence | Distributed tracing → Zipkin |
| Erreurs 5xx | Logs → Stack traces |
| Deadlock | Thread 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égorie | Outil | Use Case |
|---|---|---|
| Monitoring | Prometheus + Grafana | Métriques temps réel |
| Logging | Loki + Grafana | Logs agrégés |
| Tracing | Tempo + Grafana | Traces distribuées |
| Profiling | Async Profiler | CPU, allocations (low overhead) |
| JVM Debug | Arthas | Inspection live JVM |
| Heap Analysis | Eclipse MAT | Fuites mémoire |
| Thread Analysis | FastThread.io | Deadlocks, threads bloquées |
| APM | Datadog / New Relic | All-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 :
- Observabilité avec OpenTelemetry
- Tests E2E Spring Boot Testcontainers
- WebFlux et Programmation Réactive
- Erreurs Monitoring Production : Top 10
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 ?