Les normes que personne ne demande
Sur Le Chantier, le maître d'ouvrage sait décrire ce qu'il veut. Une surface, un nombre de bureaux, un hall d'accueil, un parking. Il ne dira jamais qu'il souhaite une structure capable de résister au feu pendant une heure, ni des circulations assez larges pour un fauteuil roulant.
Les normes s'appliquent quand même. Incendie, sismique, accessibilité, thermique. Elles n'apparaissent dans aucune demande et pourtant elles fixent l'épaisseur des murs, le nombre de cages d'escalier et la position des gaines.
En logiciel, ces normes s'appellent les qualités de service, ou par leur suffixe anglais, les *-ilities. Personne ne les demande, tout le monde les subit.
Le billet précédent a posé les exigences non-fonctionnelles : la charge, la volumétrie, les utilisateurs concurrents, le SLA. Ce sont des chiffres arrachés au client. Ils décrivent ce que le système va devoir encaisser.
Une qualité de service, c'est autre chose. C'est la capacité technique qu'il faut posséder pour tenir ce chiffre.
L'exigence non-fonctionnelle dit à quoi le système doit faire face. La qualité de service dit ce qu'il doit savoir faire pour y arriver. La première se négocie avec le client, la seconde relève de l'architecte.
La liste complète des attributs de qualité en recense des dizaines, du confinement de panne à la localisation. Sur un projet réel, cinq d'entre elles portent l'essentiel des décisions. Ce billet traite celles-là.
De l'exigence à la décision
Une qualité de service n'existe pas en soi. Elle apparaît toujours au bout d'une chaîne qui part d'un chiffre et se termine par un choix technique vérifiable.
Prends une exigence banale : dix mille utilisateurs simultanés en pointe le lundi matin. Elle ne dit rien sur l'architecture. Traduite en qualité de service, elle devient un besoin de scalabilité horizontale. La décision qui en découle, ce sont des services sans état derrière un répartiteur de charge. Et la vérification qui va avec, un test de charge sur trois instances.
10 000 utilisateurs en pointe"] qs["Qualité de service
scalabilité horizontale"] dec["Décision d'architecture
services sans état, répartiteur"] ver["Vérification
test de charge sur 3 instances"] ex --> qs --> dec --> ver ver -- "objectif non tenu" --> qs classDef exigence fill:#f5f2ec,stroke:#8a4d16,color:#1a1a1a classDef qualite fill:#d89253,stroke:#8a4d16,color:#1a1a1a classDef decision fill:#b5651d,stroke:#5f5e5a,color:#f5f2ec classDef controle fill:#a0413e,stroke:#5f5e5a,color:#f5f2ec class ex exigence class qs qualite class dec decision class ver controle
La flèche de retour compte autant que le reste. Une qualité de service qu'on ne mesure jamais est un vœu, pas une propriété du système.
Si tu ne sais pas dire comment tu vérifieras qu'une qualité est atteinte, tu ne l'as pas conçue, tu l'as espérée.
La norme ISO/IEC 25010 formalise ce vocabulaire dans un modèle de qualité du produit logiciel, et range d'ailleurs la modularité et la testabilité comme sous-caractéristiques de la maintenabilité. Utile pour se comprendre entre architectes, moins utile devant un client. Sur le terrain, cinq questions suffisent.
| Qualité | La question qu'elle pose | Le signe qu'elle manque | La décision qu'elle déclenche |
|---|---|---|---|
| Scalabilité | Que se passe-t-il si la charge double lundi ? | On ajoute de la RAM et on croise les doigts | Sans état, répartiteur, dimensionnement horizontal |
| Manageabilité | Qui te signale les problèmes en premier ? | L'utilisateur appelle avant que l'alerte parte | Sondes, journaux centralisés, leviers d'action à chaud |
| Modularité | Combien de fichiers touche-t-on pour changer de fournisseur d'envoi de mails ? | Le nom du fournisseur apparaît dans quarante fichiers | Interfaces, adaptateurs, une implémentation par frontière |
| Extensibilité | Que faut-il modifier pour ajouter un format ? | Un switch qui grossit à chaque livraison |
Point d'extension, fabrique, enregistrement |
| Testabilité | Peut-on prouver ce comportement sans démarrer tout le système ? | Aucun test ne tourne sans un accès à la base réelle | Responsabilité unique, dépendances injectées |
Ces cinq lignes sont le plan de ce qui suit.
Scalabilité : encaisser plus sans réécrire
Un immeuble calculé pour six niveaux ne prend pas un septième étage parce que le maître d'ouvrage a de nouveaux besoins. Les fondations ont été dimensionnées une fois, avec les charges admissibles connues à l'époque. Pour accueillir plus de monde, on ouvre un second bâtiment.
Le logiciel offre les deux options, et c'est tout l'enjeu du choix.
Le scale up consiste à grossir la machine : plus de cœurs, plus de mémoire, un disque plus rapide. Le scale out consiste à ajouter des machines identiques derrière un répartiteur.
| Critère | Scale up (vertical) | Scale out (horizontal) |
|---|---|---|
| Geste | Redimensionner l'instance existante | Ajouter une instance de plus |
| Limite | Le plus gros gabarit disponible chez ton hébergeur | Pratiquement aucune, tant que l'état est externalisé |
| Coût | Croissance non linéaire, les très gros gabarits coûtent cher | Croissance linéaire, unités banalisées |
| Disponibilité pendant l'opération | Redémarrage nécessaire dans la plupart des cas | Aucune interruption, on ajoute à chaud |
| Effet sur le code | Aucun, c'est de l'infrastructure | Structurant, le code doit être prévu pour |
| Tolérance à la panne | La machine reste un point de défaillance unique | La perte d'une instance est absorbée |
La dernière ligne explique pourquoi le scale out gagne presque toujours, alors même qu'il est plus contraignant. Doubler la puissance d'un serveur ne le rend pas plus fiable. Ajouter un second serveur, si.
Reste la partie que personne ne dit au client quand il demande une montée en charge « sans interruption de service ». Cette phrase se paye en code.
- Aucune session en mémoire dans le processus, sinon un utilisateur perd son panier dès que sa requête tombe sur une autre instance.
- Aucun fichier écrit sur le disque local, parce que l'instance suivante ne le verra pas.
- Aucun ordonnanceur en singleton dans l'application, sinon trois instances exécuteront la même tâche nocturne trois fois.
- Des opérations rejouables sans dommage, parce qu'un répartiteur qui bascule provoque des doublons.
Ces quatre règles recoupent les processus sans état des Twelve-Factor App. Elles coûtent peu si on les applique dès le premier jour, et très cher deux ans plus tard.
Un système scalable n'est pas un système rapide. C'est un système dont on connaît le geste à faire quand la charge monte, et dont le code ne s'y oppose pas.
Manageabilité : savoir ce qui se passe, et pouvoir agir
Tout bâtiment sérieux a son local technique. Une GTB, la gestion technique du bâtiment, y affiche l'état des centrales de traitement d'air, la température par zone, les défauts en cours. Le gardien ne découvre pas la panne de chauffage parce qu'un occupant du troisième étage est descendu se plaindre.
La manageabilité recouvre deux capacités distinctes, et la seconde est souvent oubliée.
Savoir ce qui se passe : journaux exploitables, métriques d'usage, sondes de santé, traçabilité d'une requête de bout en bout. Pouvoir agir sans redéployer : basculer un indicateur de fonctionnalité, purger un cache, vider une file, relancer un traitement bloqué.
Un test suffit à situer une équipe. Pose-lui la question : qui vous signale les problèmes en premier, vos sondes ou vos utilisateurs ?
Si la réponse est « les utilisateurs », le système n'est pas manageable, quel que soit le nombre de tableaux de bord installés. Le chapitre Monitoring Distributed Systems du livre SRE de Google formule la même idée autrement : une alerte doit correspondre à une action, sinon elle produit du bruit et finit ignorée.
C'est le point le plus contre-intuitif du sujet. Une supervision trop bavarde est aussi dangereuse qu'une supervision absente, parce qu'elle enseigne à ne pas regarder.
Un message automatique qui part dans une boîte mail que personne ne surveille n'est pas une alerte. C'est une trace, utile après coup, inutile pendant l'incident.
La manageabilité se conçoit, elle ne s'ajoute pas. Un identifiant de corrélation propagé entre les services ne coûte presque rien au démarrage du projet, et devient un chantier à part entière une fois quinze composants livrés.
Modularité : remplacer une brique sans toucher au reste
Dans un immeuble, la chaufferie se remplace sans démonter les murs porteurs. Pas par magie : parce que les raccords sont normalisés et que le local a été prévu comme un volume séparé, avec ses arrivées et ses départs à des positions connues.
La modularité, c'est cette même discipline appliquée au code. Le système est découpé en modules dont chacun expose une frontière stable et cache son intérieur.
Le test est simple et brutal. Combien de fichiers faut-il modifier pour changer de fournisseur d'envoi de mails ?
Dans un système modulaire, la réponse tient en un fichier : l'adaptateur qui implémente l'interface ServiceDeNotification. Dans un système qui ne l'est pas, le nom du fournisseur, son SDK et son format de gabarit se retrouvent dans le contrôleur, dans le service métier et dans deux tâches planifiées.
Trois signes de non-modularité, faciles à repérer en revue de code.
- Un type technique issu d'une bibliothèque tierce traverse trois couches. La dépendance a fui hors de sa frontière.
- Un module lit directement la table d'un autre module. Il n'y a plus de frontière du tout, seulement une convention orale.
- Un changement de version d'une bibliothèque déclenche des compilations en cascade dans des modules qui n'ont rien à voir avec elle.
La modularité a un coût réel qu'il faut assumer devant l'équipe : une interface de plus, un adaptateur de plus, une indirection à lire quand on cherche le code qui fait vraiment le travail. Ce coût se justifie sur les frontières qui changent, pas partout.
Une interface se pose là où tu anticipes un remplacement ou un test. En poser partout produit une architecture qu'on ne sait plus parcourir, pour un bénéfice nul.
Extensibilité : ajouter sans modifier
Sur Le Chantier, on réserve des trémies. Un percement laissé dans les planchers, une gaine technique vide qui monte sur toute la hauteur du bâtiment. Rien ne passe dedans le jour de la livraison. Elles existent pour le jour où un ascenseur ou une colonne supplémentaire deviendra nécessaire : ce jour-là, personne n'aura de plancher à casser.
L'extensibilité est la version logicielle de cette réservation : ajouter une fonction sans rouvrir le code existant.
Le contre-exemple type est le switch sur un format. À la première livraison il compte deux branches. Au bout d'un an il en compte onze, et chaque ajout impose de retester une méthode que quatre équipes appellent.
// Avant : chaque nouveau format oblige à rouvrir cette méthode
public Rapport exporter(String format, Donnees donnees) {
switch (format) {
case "pdf": return new ExportPdf().generer(donnees);
case "csv": return new ExportCsv().generer(donnees);
case "xlsx": return new ExportExcel().generer(donnees);
default: throw new IllegalArgumentException(format);
}
}
La version extensible inverse la responsabilité. Le point d'extension est une interface, et chaque format s'enregistre lui-même auprès d'une fabrique.
public interface Exporteur {
String format();
Rapport generer(Donnees donnees);
}
public class FabriqueExporteurs {
private final Map<String, Exporteur> parFormat;
public FabriqueExporteurs(List<Exporteur> exporteurs) {
this.parFormat = exporteurs.stream()
.collect(Collectors.toMap(Exporteur::format, e -> e));
}
public Rapport exporter(String format, Donnees donnees) {
Exporteur exporteur = parFormat.get(format);
if (exporteur == null) {
throw new FormatInconnuException(format);
}
return exporteur.generer(donnees);
}
}
Ajouter le format JSON devient une classe de plus et zéro ligne modifiée. La méthode exporter ne bouge plus, donc elle n'a plus besoin d'être retestée à chaque livraison.
Le piège est de généraliser le motif. Un point d'extension pour une famille qui n'aura jamais qu'un membre est de la complexité pure. La règle pratique : à la deuxième variante on note l'idée, à la troisième on la met en place.
L'extensibilité se paye en indirection, la non-extensibilité se paye en régressions. Le bon arbitrage dépend du nombre de variantes attendues, pas de l'élégance du code.
Testabilité : pouvoir prouver que ça marche
Avant la réception d'un ouvrage, chaque corps de métier éprouve son lot séparément. L'électricien mesure ses circuits, le plombier met le réseau en pression. Personne n'attend l'inauguration pour découvrir qu'une canalisation fuit derrière une cloison finie.
La testabilité est la propriété qui rend cette vérification lot par lot possible dans le code. Elle se joue au moment où l'on écrit le code testé, bien avant le premier test.
Deux conditions suffisent à la plupart des cas.
Responsabilité unique. Une fonction qui lit une configuration, calcule un montant et écrit en base ne peut pas être testée sur son calcul sans configuration ni base de données. Découpée en trois, la partie qui porte la règle métier se teste avec deux lignes.
Dépendances injectées. Une classe qui construit elle-même son client HTTP avec new impose au test d'avoir un serveur en face. La même classe qui reçoit ce client par son constructeur accepte un double en test.
| Critère | Test unitaire | Test d'intégration |
|---|---|---|
| Périmètre | Une classe, une fonction, une règle | Plusieurs composants assemblés |
| Dépendances | Remplacées par des doubles | Réelles ou proches du réel |
| Durée typique | Millisecondes | Secondes à minutes |
| Ce qu'il prouve | La règle métier est correcte | Le câblage entre les briques tient |
| Ce qu'il ne voit pas | Les erreurs de branchement et de configuration | Les cas limites de calcul, trop coûteux à couvrir ici |
| Quand il casse | La cause est locale et évidente | La cause peut être n'importe où dans la chaîne |
Les deux familles ne se remplacent pas. Un projet couvert à 90 % en unitaire et sans aucun test d'intégration livre du code juste, mal branché. L'inverse livre un système qui démarre et qui calcule faux.
Un dernier critère, souvent absent des discussions sur les tests : la déployabilité. Savoir livrer de façon reproductible, vérifier ce qui tourne réellement sur chaque nœud, et revenir en arrière sans improviser. Le scénario qui suit montre ce que coûte son absence.
Un code non testable produit quand même des tests : lents, fragiles, et que l'équipe finit par désactiver pour débloquer la chaîne.
Scénario B : Knight Capital, 45 minutes le 1er août 2012
Knight Capital Americas était l'un des plus gros teneurs de marché des actions américaines. Le 1er août 2012, l'entreprise a perdu en trois quarts d'heure de quoi disparaître.
Les faits qui suivent viennent de l'ordre de la SEC contre Knight Capital, Exchange Act Release n° 70694 du 16 octobre 2013, complétés par la fiche publique sur Knight Capital Group.
Knight préparait le lancement du Retail Liquidity Program du NYSE, qui ouvrait ce jour-là. Le nouveau code de routage a été déployé manuellement sur les serveurs concernés dans les jours précédents. Sur huit serveurs, sept ont reçu le nouveau code. Le huitième a été oublié.
Le nouveau code réutilisait un indicateur de configuration qui servait auparavant à activer une fonctionnalité nommée Power Peg, inutilisée depuis 2003 et jamais retirée du dépôt. Sur les sept serveurs à jour, l'indicateur déclenchait le comportement voulu. Sur le huitième, il a réveillé le code mort.
du nouveau code de routage"] s7["7 serveurs
code à jour"] s8["1 serveur oublié
ancien code"] flag{"Indicateur de configuration
réutilisé"} ok["Routage attendu"] ko["Code mort de 2003
réactivé"] mails["97 messages automatiques
jamais traités comme des alertes"] dep --> s7 --> flag dep --> s8 --> flag flag -- "sur les 7 serveurs" --> ok flag -- "sur le serveur oublié" --> ko ko --> mails classDef neutre fill:#f5f2ec,stroke:#8a4d16,color:#1a1a1a classDef attention fill:#d89253,stroke:#8a4d16,color:#1a1a1a classDef grave fill:#a0413e,stroke:#5f5e5a,color:#f5f2ec class dep,s7,ok neutre class s8,flag attention class ko,mails grave
Le détail qui transforme l'incident en catastrophe tient à une modification de 2005 : la fonction qui comptait les quantités déjà exécutées avait été déplacée ailleurs dans la chaîne de traitement, sans que le code Power Peg soit revalidé. Réactivé sept ans plus tard, il envoyait donc des ordres enfants sans jamais s'arrêter.
Avant l'ouverture, le système a émis 97 messages automatiques signalant une erreur liée à Power Peg. Ces messages n'étaient pas conçus comme des alertes de supervision et personne n'y a réagi.
À l'ouverture, le serveur oublié a exécuté plus de quatre millions de transactions sur 154 titres, pour plus de 397 millions d'actions, en quarante-cinq minutes environ. En cherchant l'origine du problème, les équipes ont retiré le nouveau code des sept serveurs correctement déployés, ce qui a aggravé la situation au lieu de la corriger.
Knight a annoncé une perte avant impôt d'environ 440 millions de dollars, chiffrée à plus de 460 millions dans l'ordre de la SEC. L'entreprise a accepté une sanction de 12 millions de dollars pour manquement à la règle d'accès au marché, a dû être recapitalisée en urgence et a été absorbée l'année suivante.
Reprends les qualités passées en revue, déployabilité comprise, et coche ce qui manquait ce matin-là.
| Qualité | Ce qui manquait ce jour-là |
|---|---|
| Déployabilité | Copie manuelle sur huit serveurs, aucune vérification automatique de l'état livré, aucun retour arrière répété |
| Manageabilité | 97 messages émis, aucune alerte, aucun levier pour arrêter le flux d'ordres pendant l'incident |
| Testabilité | Du code mort laissé neuf ans dans le dépôt, dont les hypothèses avaient été invalidées dès 2005 par une modification faite ailleurs |
Aucune de ces trois qualités n'apparaissait dans un cahier des charges. Aucune n'aurait été facturée au client. Les trois manquaient le même matin.
Aucune bibliothèque défaillante, aucun serveur en panne. Trois qualités de service absentes, découvertes le jour où elles auraient servi.
Points clés
- Une exigence non-fonctionnelle est un chiffre, une qualité de service est une capacité. Le travail de l'architecte est la traduction de l'un vers l'autre, puis vers une décision vérifiable.
- Cinq questions couvrent l'essentiel : que se passe-t-il si la charge double, qui te signale les pannes, combien de code touche un remplacement, que faut-il modifier pour ajouter une variante, peut-on prouver un comportement isolément.
- Le scale out se prépare dans le code, pas dans l'infrastructure. Sans état en mémoire, sans fichier local, sans ordonnanceur en singleton, avec des opérations rejouables.
- Une alerte qui ne déclenche aucune action n'est pas de la manageabilité. Knight Capital a reçu 97 messages avant l'ouverture des marchés et n'a rien vu.
- La modularité et l'extensibilité se posent aux frontières qui bougent. Généralisées partout, elles produisent une complexité qui ne sert personne.
Dans la série
Palier 1 : Lire le terrain. Domaine : Process.
- Précédent : Le processus d'architecture : six étapes, des exigences au support
- Suivant : Types d'applications : web, API, mobile, service, desktop
- Vue d'ensemble : Architecture logicielle : par où commencer
Pour aller plus loin
- Le processus d'architecture : six étapes, des exigences au support, pour les chiffres qui alimentent ces qualités de service
- Couplage lâche, stateless et cache : tenir la charge, où la scalabilité devient des décisions concrètes d'architecture système
- List of system quality attributes, le catalogue complet, bien au-delà des cinq retenues ici
- Monitoring Distributed Systems, le chapitre du livre SRE de Google sur ce qui mérite une alerte
- Les processus sans état des Twelve-Factor App, la formulation la plus courte des contraintes du scale out
- Knight Capital Group pour le contexte de l'incident ; les faits détaillés proviennent de l'ordre de la SEC, Exchange Act Release n° 70694 du 16 octobre 2013, consultable sur sec.gov