Scaling Java Application
Vers l’infini et au delà

Julien Roy @vanr0y

Architecte Java

Présentation

  • Moteur de calcul de prix d'assurances
  • Backoffice de configuration des produits
  • Exposition d'API à déstination de partenaires

Objectifs

  • Temps de réponses < 50ms
  • Nombre de req/s = ∞

Conception

  • Stateless
  • Réduction / Suppréssion des IO
  • Utilisation cache
  • Autoscaling ( basé sur CPU )

Architecture

Architecture

Architecture

Tests de charge

  • 15 minutes
  • 250 utilisateurs en //
  • Ramp-Up de 10 mins

Résultats attendus

  • Temps de réponse constant < 80 ms
  • Courbe de req/s croissante
  • Taux d'erreur proche de 0

Résultats attendu

Résultats du premier run

Round 1 : Analyse

Timeline des événements ECS

Round 1 : Analyse

Liste des tâches

Round 1 : Analyse

Détail d'une tâche

Round 1 : Analyse

Statistiques JVM

Round 1 : Debug

Round 1 : Debug

Dépendances


version: '3'

services:

  postgresql:
    image: postgres:10.1
    ports:
      - 5432:5432

  hydra:
    image: oryd/hydra:v1.0.0-beta.9
    ports:
      - 4444:4444
      - 4445:4445
    command:
      serve all --dangerous-force-http

Round 1 : Debug

Lancement application


$ docker run -it -p 8080:8080 -p 1099:1099 \
  -e JAVA_OPTS='
    -Xms512m
    -Xmx512m
    -XX:MaxMetaspaceSize=128m
    -XX:+UseG1GC
    -XX:NativeMemoryTracking=summary
    -Djava.rmi.server.hostname=localhost
    -Dcom.sun.management.jmxremote
    -Dcom.sun.management.jmxremote.port=1099 ' \
   --memory 896m  \
  --memory-swap 896m \
  --name quoteengine_service \
  quoteengine_service
                        

Round 1 : Debug

Simulation latences


$ docker run --name quoteengine_pumba_hydra_1 -d \
    gaiaadm/pumba:master netem \
    --duration 3600m delay --time 500 quoteengine_hydra_1

$ docker run --name quoteengine_pumba_postgresql_1 -d \
    gaiaadm/pumba:master netem \
    --duration 3600m --time 200 quoteengine_postgresql_1
                        

Round 1 : Debug

Lancement des tests

Round 1 : Debug

Stats docker au lancement

Round 1 : Debug

Stats docker après 30s

Round 1 : Debug

Stats docker après 1min

Round 1 : Debug

Etat de l'application

Round 1 : Debug

Analyse mémoire

Round 1 : Debug

Recherche Bug JVM

Round 1 : Debug

Résolution


private static byte[] compress(byte[] data) throws IOException {
    Deflater deflater = new Deflater();
    deflater.setInput(data);
    try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream(data.length)) {
        deflater.finish();
        byte[] buffer = new byte[1024];
        while (!deflater.finished()) {
            int count = deflater.deflate(buffer);
            outputStream.write(buffer, 0, count);
        }
        return outputStream.toByteArray();
    } finally {
        deflater.end();
    }
}
                        

Round 1 : Tests

Round 1 : Résultats

Fuite mémoire résolue

Round 2 : Analyse

Timeline des événements ECS

Round 2 : Analyse

Graph CPU

Round 2 : Debug

Round 2 : Debug

Lancement des tests

Round 2 : Debug

Visualisation des threads

Round 2 : Debug

Threads dump

Round 2 : Debug

Code source


JwkDefinitionHolder getDefinitionLoadIfNecessary(String keyId) {
    JwkDefinitionHolder result = this.getDefinition(keyId);
    if (result != null) {
        return result;
    }
    synchronized (this.jwkDefinitions) {
        this.jwkDefinitions.clear();
        for (URL jwkSetUrl : jwkSetUrls) {
            this.jwkDefinitions.putAll(loadJwkDefinitions(jwkSetUrl));
        }
        return this.getDefinition(keyId);
    }
}
                        

Round 2 : Debug

Résolution


JwkDefinitionHolder getDefinitionLoadIfNecessary(String keyId) {
    JwkDefinitionHolder result = this.getDefinition(keyId);
    if (result != null) {
        return result;
    }
    synchronized (this.jwkDefinitions) {

        // Double-checked locking pattern
        result = this.getDefinition(keyId);
        if (result != null) {
            return result;
        }

        this.jwkDefinitions.clear();
        for (URL jwkSetUrl : jwkSetUrls) {
            this.jwkDefinitions.putAll(loadJwkDefinitions(jwkSetUrl));
        }
        return this.getDefinition(keyId);
    }
}
                        

Round 2 : Tests

Round 2 : Résultats

Race condition résolue

Round 3 : Analyse

Connection DB

Round 3 : Analyse

Requetes DB

Round 3 : Debug

Round 3 : Debug

Activation logs


logging:
  level:
    org.springframework.jdbc.core: TRACE
                        

Round 3 : Debug

Logs Spring JDBC

Round 3 : Debug

Activation logs Driver


logging:
  level:
    org.postgresql.core.v3.QueryExecutorImpl: TRACE
                        

Round 3 : Debug

Logs Driver Postgresql

Round 3 : Debug

Break point driver Posgresql

Round 3 : Debug

Résolution


@Service
@Transactional
@RequiredArgsConstructor
public class TaxService {

  private final CacheManager cacheManager;

  public List<GlobalTax> getGlobalTaxes(String countryCode) {
    return Optional.ofNullable(getGlobalTaxCache().get(countryCode)).map(Cache.ValueWrapper::get).orElse(null);
  }

  private Cache getGlobalTaxCache() {
    return cacheManager.getCache(CacheConfiguration.GLOBAL_TAX_CACHE_NAME);
  }
                        

Round 3 : Résultats

Gestion du cache améliorée

La suite

  • Tests de performance intégré au pipeline de déploiment
  • Amélioration autoscaling ( basé sur les temps de réponses ? )

Points clés

  • Environnment stable ( ISO prod )
  • Mesures reproductibles ( jeux de données )
  • Metriques complétes
  • Une seule modification à la fois
  • Reproduction en local ( Si possible )

Questions