# Load Testing avec k6 --- # Le problème * gros client signé 🎉 * tenir la charge 😱 * spoiler : non * savoir par quoi commencer --- # Le contexte  ---  --- # Le plan * quelle est notre archi ? * qu'est-ce qu'on suspecte qui va péter ? * qu'est-ce qu'on veut tester ? (fonctionnel / SLA) * quels besoins techniques pour les tests ? * VMs + script de provisionnement ? * outil de load testing ? * données réalistes ? * débit ? * 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 * 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 * 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 + sleeps sales ! * 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, ... * 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 💪🐍 --- ## Résultat final * script 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 : * mapping VU <--> device à base de "testId" * 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, ...) * 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 * simuler 500 devices ==> 500 connexions TCP * j'ai pas 500 ordinateurs à travers le monde, mais Microsoft oui * création d'une VM manuelle, template, az-cli * il faut pourvoir 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 * on re-relance, après avoir modifié quelques trucs faciles * on analyse --- ---  ---  --- ## Sources * moi * [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 ? * n'oubliez pas le ROTI à la fin