Observabilité et Circuit Breaker avec Spring
Il y a quelques mois déjà, je discutais avec un collègue d’ observabilité, opentracing, … avec Quarkus. On est tombé sur un super exemple réalisé par Antonio Concalves. Ce projet démontre les capacités de Quarkus sur les sujets suivants:
- Circuit Breaker
- Observabilité
- OpenTracing
- Tests
- …
Et la on peut se demander quid de Spring? Je me doutais que ces fonctionnalités étaient soient disponibles par défaut soient facilement intégrables vu la richesse de l’écosystème.
J’ai donc réalisé un clone de ce projet basé sur Spring Boot/Cloud. Je ne vais pas détailler plus que ça les différentes fonctionnalités, vous pouvez vous référer au fichier README. Il est suffisamment détaillé pour que vous puissiez exécuter et les mettre en œuvre.
Architecture de l’application
Vous trouverez ci-dessous un schéma d’architecture de l’application au format C4.
Circuit Breaker
Lors des appels entre le bookstore et le booknumberservice, il peut être intéressant d’ implémenter un circuit breaker pour pallier aux indisponibilités de ce dernier.
Avec Spring, on peut utiliser Resilience4J au travers de Spring Cloud. Tout ceci se fait de manière programmatique
Il faut tout d’abord configurer les circuit breakers au travers d’une classe Configuration.
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> createDefaultCustomizer() {
return factory -> factory.configureDefault(id -> new Resilience4JConfigBuilder(id)
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(timeoutInSec)).build())
.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
.build());
}
/**
* Creates a circuit breaker customizer applying a timeout specified by the <code>booknumbers.api.timeout_sec</code> property.
* This customizer could be reached using this id: <code>slowNumbers</code>
* @return the circuit breaker customizer to apply when calling to numbers api
*/
@Bean
public Customizer<Resilience4JCircuitBreakerFactory> createSlowNumbersAPICallCustomizer() {
return factory -> factory.configure(builder -> builder.circuitBreakerConfig(CircuitBreakerConfig.ofDefaults())
.timeLimiterConfig(TimeLimiterConfig.custom().timeoutDuration(Duration.ofSeconds(timeoutInSec)).build()), "slowNumbers");
}
Grâce à ces instanciations, on référence les différents circuit breakers.
Maintenant, on peut les utiliser dans le code de la manière suivante:
public Book registerBook(@Valid Book book) {
circuitBreakerFactory.create("slowNumbers").run(
() -> persistBook(book),
throwable -> fallbackPersistBook(book)
);
return bookRepository.save(book);
}
Maintenant, il ne reste plus qu’à créer une méthode de « fallback » utilisée si un service est indisponible. Cette dernière nous permettra, par exemple, de mettre le payload dans un fichier pour futur traitement batch.
Observabilité
L’observabilité est sans contexte la pierre angulaire (oui, rien que ça…) de toute application cloud native. Sans ça, pas de scalabilité, de redémarrage automatique,etc.
Les architectures de ce type d’applications sont idempotentes. On a donc besoin d’avoir toutes les informations à notre disposition. Heureusement, Spring fournit par le biais d’ Actuator toutes les informations nécessaires. Ces dernières pourront soit être utilisées par Kubernetes (ex. le livenessProbe) ou agrégées dans une base de données Prometheus.
Pour activer certaines métriques d’actuator, il suffit de :
Ajouter la/les dépendance(s)
dependencies {
[...]
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
[...]
}
Spécifier la configuration adéquate:
management:
endpoints:
enabled-by-default: true
web:
exposure:
include: '*'
jmx:
exposure:
include: '*'
endpoint:
health:
show-details: always
enabled: true
probes:
enabled: true
shutdown:
enabled: true
prometheus:
enabled: true
metrics:
enabled: true
health:
livenessstate:
enabled: true
readinessstate:
enabled: true
datasource:
enabled: true
metrics:
web:
client:
request:
autotime:
enabled: true
OpenTracing
Sur les applications distribuées, il peut s’avérer compliqué de concentrer les logs et de les corréler. Certes, avec un ID de corrélation, on peut avoir certaines informations. Cependant, il faut que les logs soient bien positionnées dans le code. On peut également passer à travers de certaines informations (ex. connexion aux bases de données, temps d’exécution des APIS,…). Je ne vous parle pas des soucis de volumétrie engendrées par des index Elasticsearch/Splunk sur des applications à forte volumétrie.
Depuis quelques temps, le CNCF propose un projet (encore en incubation) : OpenTracing. Ce dernier fait désormais partie d’OpenTelemetry.
Grâce à cet librairie, nous allons pouvoir tracer toutes les transactions de notre application microservices et pouvoir réaliser une corrélation « out of the box » grâce à l’intégration avec Jaeger.
Pour activer la fonctionnalité il suffit d’ajouter la dépendance au classpath:
implementation 'io.opentracing.contrib:opentracing-spring-jaeger-cloud-starter:3.3.1'
et de configurer l’URL de Jaeger dans l’application
# Default values
opentracing:
jaeger:
udp-sender:
host: localhost
port: 6831
enabled: true
Une fois l’application reconstruite et redémarrée, vous pourrez visualiser les transactions dans JAEGER:
Conclusion
Je ne vais pas exposer l’implémentation des tests unitaires et d’intégration. Si vous voulez voir comment j’ai réussi à mocker simplement les appels REST à une API distante, vous pouvez regarder cette classe pour voir une utilisation du MockServer.
Aussi, n’hésitez pas à cloner, tester ce projet et me donner votre retour. J’essaierai de le mettre à jour au fur et à mesure de mes découvertes (par ex. OpenTelemetry).