# Load Testing avec k6 --- # Le problème * gros client signé 🎉 * tenir la charge 😱 * spoiler : non * savoir par quoi commencer --- # Le contexte  ---  --- # Test de charge ? * 2 visions du test de charge : * test de perf : à quelle vitesse ça va ? * test de charge : est-ce que ça va assez vite ? * spike : survivre à un pic de charge * stress : survivre à une charge élevée pendant un temps moyen * soak/endurance : survivre à une très longue durée --- # Comment produire de la charge ? * applicatif Windows installé sur les devices * n'existe pas (encore) pour Linux * pas vraiment pilotables (il faut configurer un soft annexe ...) * pas de reporting joli pour les erreurs * on ne souhaite pas tester les devices, juste notre plateforme * donc seulement le HTTP qui est envoyé, pas besoin de s'encombrer --- # Le plan * objectif = "mesurer l'absorption des données" * quelle est notre archi ? * qu'est-ce qu'on veut vérifier ? (fonctionnel / SLA) * qu'est-ce qu'on suspecte qui va casser ? * quels besoins techniques pour les tests ? * vrai ou fake device ? * outil de load testing ? * VMs + script de provisionnement ? * données réalistes ? * débit réaliste ? * let's go ! --- # k6 * "Grafana k6 is an open-source, developer-friendly, and extensible load testing tool." * "k6 allows you to prevent performance issues and proactively improve reliability." * orienté HTTP (ou browser) * juste un binaire (Go) == installation facile * v1.0 toute récente * Javascript 😱 (ou TypeScript 🥸) * exécuter une fonction en parallèle + en boucle * stats pendant et à la fin * pourquoi pas JMeter ? Java 🤮 * pourquoi pas Locust ? Python + perf + stats 😥 * ça semble OK --- ## Script minimal ```javascript import http from 'k6/http'; import { sleep } from 'k6'; export const options = { iterations: 10, }; export default function () { http.get('https://quickpizza.grafana.com'); sleep(1); } ``` --- ```shell $ k6 run script_minimal.js ``` [...] ``` data_received..................: 32 kB 2.8 kB/s data_sent......................: 1.0 kB 92 B/s http_req_blocked...............: avg=26.35ms min=400ns med=651ns max=263.49ms p(90)=26.35ms p(95)=144.92ms http_req_connecting............: avg=10.96ms min=0s med=0s max=109.69ms p(90)=10.96ms p(95)=60.33ms http_req_duration..............: avg=108.97ms min=107.43ms med=108.62ms max=111.04ms p(90)=110.91ms p(95)=110.98ms { expected_response:true }...: avg=108.97ms min=107.43ms med=108.62ms max=111.04ms p(90)=110.91ms p(95)=110.98ms http_req_failed................: 0.00% 0 out of 10 http_req_receiving.............: avg=157.2µs min=82.8µs med=123.01µs max=294.46µs p(90)=274.06µs p(95)=284.26µs http_req_sending...............: avg=376.97µs min=56.42µs med=182.76µs max=2.03ms p(90)=753.35µs p(95)=1.39ms http_req_tls_handshaking.......: avg=11.14ms min=0s med=0s max=111.43ms p(90)=11.14ms p(95)=61.28ms http_req_waiting...............: avg=108.44ms min=107.06ms med=108.27ms max=110.52ms p(90)=109.19ms p(95)=109.86ms http_reqs......................: 10 0.879973/s iteration_duration.............: avg=1.13s min=1.1s med=1.1s max=1.37s p(90)=1.13s p(95)=1.25s iterations.....................: 10 0.879973/s vus............................: 1 min=1 max=1 vus_max........................: 1 min=1 max=1 ``` --- ## Cycle de vie d'un test k6 * init (js) * setup (JSON) * run * teardown  --- ## Virtual Users (VU) * "The simulated users that run separate and concurrent iterations of your test script." * exécuté en parallèle (go routines) * jouent tous le même scénario qu'on leur donne --- ## Scénarios, executors, iterations * scénario : modélisation d'une workload (nombre de VUs au cours du temps) * un executor se charge de scheduler les VUs pour le scenario * open/closed model : lier durée d'itération avec durée/débit du test ? * disponibles : * Shared iterations * Per-VU iterations * Constant VUs * Ramping VUs * Constant arrival rate * Ramping arrival rate * Externally controlled (REST / CLI) * stages --- ## Ramping VUs  --- ## Ramping arrival rate  --- ## Metrics, checks, thresholds * built-in (vus précédemment) * custom : * counter * gauge * rate * trend * real-time ou summary à la fin --- # Mise en application * globalement OK ✅ * quelques problèmes 😬 --- ## Problème d'Executor * notre besoin : allumage progressif des devices, chacun à charge constante * Ramping VUs : simule le démarrage progressif des devices * mais chaque VU tabasse, ce qu'on ne veut pas * Ramping Arrival rate : simule qu'il y a de plus en plus de requêtes * mais ne nous renseigne pas directement sur le nombre de devices * solution : ramping VUs + des sleep ! * résultat : constant arrival rate par VU --- ## Problème de dashboard * k6 par Grafana, référence de l'Observabilité * plein d'intégrations real-time, mais pas dans Grafana (non-Cloud) ! * bricolage d'un dashboard, malaxage des données (Prometheus vers Grafana), ... * on s'attendait à mieux --- ## Problème de Counter * le code incrémente des compteurs (métriques) * la summary à la fin est bon * le CSV real-time contient des lignes : * la valeur de l'incrément, pas son cumul * ce n'est pas un bug ("c'est une feature") : #1340 * Grafana n'est pas fait pour faire ça * mais Pandas 🐼 si ! * merci Python 💪🐍 --- ## setup terminé * script k6 de ~330 lignes * config par type de device à simuler * calcul du débit utile qui sera simulé * setup des données de test en JSON * chaque VU simule un device edge : * assignation d'identités (devices) aux VUs (k6 pas conçu pour) * authentification (config) * préparation de la requête (config) et envoi * sleep pour attendre un peu (config) * update des métriques custom (429, erreurs, charge utile totale, ...) * côté back : * script pour seeder la base de données * dashboards pour mesurer côté back ce qu'il se passe * procédures de cleanup à la fin * super résultat au final ! --- ## On veut voir du code ! ```javascript export function setup() { "use strict"; const payloadSendRateBySecondByDevice = config.singleLogLineSize * config.numberLogsLineByPush / config.secondsBetweenEachPush; const payloadSendRateBySecondTotal = payloadSendRateBySecondByDevice * config.numberDevicesToSimulate; console.log("estimated payload rate = " + payloadSendRateBySecondTotal/1000 + "Ko/s total"); console.log("estimated payload rate = " + payloadSendRateBySecondTotal/1000000*3600 + "Mo/h total"); let vusState = {}; for (let i = 0; i < config.numberDevicesToSimulate; i++) { vusState[(i+1).toString()] = { // MUST BE STRICTLY JSON-SERIALIZABLE, SO ONLY NUMBERS/STRINGS/NULLS last_log_line_count: -1, deviceId: generateDeviceId(), lastSuccessfulAuthentication: null, authenticationDetails: null, }; } return vusState; } ``` --- ```javascript export default function (vusState) { "use strict"; let vuId = exec.vu.idInTest; let vuState = vusState[vuId.toString()]; if (!vuState.lastSuccessfulAuthentication) { tryToAuthenticate(vuState); } else { sendLogs(vuState); } } ``` --- ```javascript let authenticationSuccessfulCounter = new Counter("authentication_successful"); let authenticationFailedCounter = new Counter("authentication_failed"); let pushSuccessfulCounter = new Counter("push_successful"); let pushFailed429Counter = new Counter("collector_429"); let pushFailedOtherErrorsCounter = new Counter("collector_other_errors"); let totalLogsSizePushedCounter = new Counter("total_logs_size_pushed"); let durationPushToCollectorTrend = new Trend("duration_push_collector"); let durationWaitingCollectorTrend = new Trend("waiting_push_collector"); ``` --- ```javascript function sendLogs(vuState) { "use strict"; if (vuState.lastPushDatetime) { const currentDatetime = new Date(); const elapsedTimeSinceLastPush = (currentDatetime - new Date(vuState.lastPushDatetime)) / 1000; // re-hydrate the date if (elapsedTimeSinceLastPush < config.secondsBetweenEachPush) { sleep(0.1); } } let body = JSON.stringify({...}); let headers = {...}; let response = http.post(pushUrl, body, headers); durationWaitingCollectorTrend.add(response.timings.waiting); durationPushToCollectorTrend.add(response.timings.duration); // ... ``` --- ```javascript // ... if (response.status === 429) { pushFailed429Counter.add(1); } else if (response.status === 200) { pushSuccessfulCounter.add(1); totalLogsSizePushedCounter.add(config.singleLogLineSize * config.numberLogsLineByPush); } else { pushFailedOtherErrorsCounter.add(1); } vuState.lastPushDatetime = new Date(); } ``` --- ## Déploiment * toute une aventure ! * simuler 500 devices ==> 500 connexions TCP * j'ai pas 500 ordinateurs à travers le monde, mais Microsoft oui * création d'une VM Linux manuelle, template, az-cli * il faut pouvoir se SSH dessus ==> network + bastion * network group sec pénibles * bastion cher + pas d'outbound * IPv4 en nombre limité ==> NAT requis * IPv6 pas supporté au niveau du NAT * finalement on a payé pour avoir + d'IPv4 * mais on n'aura pas 500 connections TCP --- # Résultats * on lance, plein d'erreurs de config * on relance, on commence à avoir des résultats, et on ajuste le débit * on re-relance, après avoir modifié quelques trucs faciles * on analyse --- ---  ---  --- # Conclusions * on n'est pas prêts, mais on le sait * et on sait mesurer si on le sera * l'outillage n'a pas été si bloquant * le plus compliqué à été de savoir ce qu'on voulait simuler * pour savoir ce qu'il fallait mesurer * "qui peut le + peut le - !" --- ## Sources * Guillaume CHALONS et Julien LENORMAND * [la doc bien foutue de Grafana k6](https://grafana.com/docs/k6/) * [breakpoint testing](https://grafana.com/docs/k6/latest/testing-guides/test-types/breakpoint-testing/) * [JavaScript compatibility mode](https://grafana.com/docs/k6/latest/using-k6/javascript-typescript-compatibility-mode/) --- # Questions ? * (ou remarques, ou doutes, ou ...)