← Tous les articles

Ce que coûte vraiment un flux complet

J’ai mesuré une table complète dans le schéma de production au lieu de l’estimer, et la mesure a tué l’une de mes propres valeurs par défaut

Michel Wijnberg

« Est-ce que ça tient une table complète ? » est la question posée à tout produit BGP, et presque aucun n’y répond par un chiffre. La raison honnête, c’est que la réponse est embarrassante à produire : il faut charger une vraie table dans son vrai schéma et regarder.

C’est donc ce qu’est cet article. Une table IPv4 complète, 1 071 538 meilleurs chemins issus de 78 538 AS d’origine, chargée dans le schéma de production sur la même version majeure de PostgreSQL que celle du laboratoire. Chaque chiffre ci-dessous en est une mesure, pas un modèle.

Il contient aussi ce que la mesure fait de plus utile : vous contredire. L’un des nombres ci-dessous était déjà livré comme valeur par défaut dans Osprey, et la mesure l’a montré en dessous du trafic ordinaire. Il a depuis été relevé d’un facteur quatre.


L’unité, ce sont les sessions, pas les routeurs

L’erreur de dimensionnement de loin la plus fréquente, et autant l’évacuer d’abord.

Un routeur avec quatre transitaires en table complète remonte quatre pairs sur une seule connexion BMP. Il coûte quatre fois ce que coûte un routeur à un seul transitaire. Dimensionner au nombre d’équipements sous-compte exactement de votre facteur de multihoming, c’est-à-dire de la chose même pour laquelle vous avez construit le réseau.


Ce que coûte une table complète, par unité

Les tailles sont celles que PostgreSQL rapporte lui-même pour ses relations, en base 1024 malgré les libellés Mo et Go, et les valeurs par ligne en dérivent. Donc 412 « Mo » font 412 Mio : multipliez 1 071 538 lignes par 403 octets et vous obtenez 411,8 Mio, non pas les 432 Mo que suggérerait une lecture décimale.

UnitéCoûtPortée
bgp_rib_entry avec ses 9 index412 Mo (403 o/ligne)par session de pair supervisée
bgp_best_path avec 4 index327 Mo (319 o/ligne)par AS et domaine de routage
bgp_rib_entry_history avec 3 index376 o par intervallepar transition enregistrée
Ensemble de préfixes BMP par pair, tas Go53 Mio (52 o/préfixe)par session de pair supervisée
Tampon de debounce du RIB du moteur, plafond par défaut267 Mio (560 o/entrée)par moteur, quels que soient les flux

Deux points de ce tableau méritent d’être détachés.

Les index représentent un peu plus de la moitié de l’empreinte. Sur bgp_rib_entry, par session : clé primaire 82 Mo, peer_prefix 42 Mo, prefix_gist 32 Mo, prefix 23 Mo, origin_as 8,9 Mo, staleness 7,5 Mo, next_hop 7,3 Mo. Ce sont les sept qui valent d’être nommés ; les deux restants sont plus petits et ne changent rien. À eux seuls les sept font 203 Mo sur un total de 412 Mo, et la clé primaire à elle seule représente un cinquième du tout. Si vous vous êtes déjà demandé si un index de plus est gratuit sur une table de cette taille, le moins cher ici coûte encore 7,3 Mo par session.

Cette mesure a aussi révélé un défaut que personne n’avait remarqué. idx_bgp_rib_peer_lastseen s’est avéré identique octet pour octet à l’index staleness : 7,5 Mo et une écriture btree supplémentaire sur chaque ligne du RIB, pour rien. Il a été supprimé. Ce genre de chose est invisible tant qu’on ne met pas les tailles côte à côte.

403 octets par ligne, c’est avec les communities. Une chaîne réaliste de six communities est incluse ; les routes sans communities coûtent environ 55 octets de moins. Citez le chiffre le plus bas sans dire lequel c’est, et votre guide de dimensionnement est optimiste de 14 % avant que quiconque ait déployé quoi que ce soit.


Déploiements chiffrés

DéploiementBase de données vivanteTas BMP
1 routeur de bordure, 2 transitaires1,15 Go107 Mio
2 routeurs de bordure, 2 transitaires chacun1,98 Go213 Mio
4 routeurs × 2 transitaires3,6 Go426 Mio
Flux de route reflector, 20 sessions en table complète8,6 Go1,04 Gio

Ce sont des chiffres d’état vivant. L’historique est à part, et c’est là que ça devient intéressant.


Le churn ne se lit pas sur un instantané

Une table vous dit ce qu’elle coûte à détenir. Elle ne vous dit rien de ce qu’elle coûte à enregistrer, car cela dépend de la fréquence à laquelle elle change, et un instantané n’a pas d’axe temporel.

Le churn a donc été mesuré à partir d’archives MRT publiques : collecteur RIPE RIS rrc00, 2026-08-10, quatre fenêtres d’une heure réparties sur la journée, 6,8 millions de messages UPDATE sur 98 sessions de pairs distinctes. Le debounce de 30 secondes d’Osprey est appliqué à l’échantillon. La suppression par hachage d’attributs n’est délibérément pas modélisée, ce sont donc des bornes supérieures de ce qui serait réellement enregistré.

Un intervalle est ici une transition d’état persistée après debounce. Ce n’est pas un UPDATE BGP brut, qui peut porter de nombreux préfixes à la fois, et ce n’est pas un événement de préfixe, que le debounce peut replier dans un intervalle existant.

Par sessionIntervalles enregistrés par jourStockage d’historique
Médiane413 000155 Mo/jour → 14 Go pour 90 jours
p90840 000316 Mo/jour → 28 Go pour 90 jours
La plus lourde observée8 300 0003,1 Go/jour → 281 Go pour 90 jours

Le debounce fait un vrai travail : il replie chaque fenêtre de 30 secondes en un enregistrement par (session, préfixe), ce qui supprime 45,6 % des événements de préfixe bruts. Le désactiver sur une cible en table complète multiplie la facture d’historique par environ 1,8, et ce que vous conservez en échange est une visibilité plus fine des transitions survenant à l’intérieur de la fenêtre. Sur un flux DFZ, c’est presque exclusivement le genre de détail que vous espériez ne pas avoir à stocker ligne par ligne.

Deux réserves que je voudrais si je lisais ceci. rrc00 est le collecteur multihop de RIPE et ses pairs sont plus bruyants qu’une session de transit locale à un IXP : traitez donc ces chiffres comme pessimistes. Et la dispersion compte bien plus que la médiane : la session la plus lourde vaut 20 fois la médiane. Dimensionnez sur le p90, alertez sur la queue, et ne laissez personne vous vendre la médiane comme plan de capacité.


Le chiffre qui était faux

Osprey dispose d’un disjoncteur sur l’enregistrement de l’historique. Si une cible produit plus de storm_ceiling_per_hour intervalles, l’enregistrement est supprimé pour le reste de l’heure, afin qu’une session qui bat ne remplisse pas un disque sans surveillance.

La valeur par défaut livrée était de 250 000 par heure.

Mettez-la maintenant à côté de la mesure. La session réelle la plus lourde de l’échantillon rrc00 tourne à environ 346 000 intervalles par heure, soit les 8,3 millions par jour du tableau ci-dessus. Pas un emballement. Pas une boucle de battement. Un vrai pair, sur un vrai collecteur, un lundi ordinaire.

La valeur par défaut était déclenchée par le fonctionnement normal, toute seule. Et le mode de défaillance d’un disjoncteur de suppression est du genre silencieux : rien ne plante, rien n’alerte fort, l’historique cesse simplement d’être complet et le trou n’est visible que si l’on va le chercher.

La valeur par défaut est désormais de 1 000 000 par heure, dérivée de la même mesure plutôt que de l’intuition. Le churn p90 tourne autour de 35 000 intervalles par heure et par session, donc le plus gros profil chiffré ci-dessus, un flux de route reflector à 20 sessions en table complète, tourne autour de 700 000 par heure au p90. Le nouveau plafond se situe au-dessus avec de la marge, et au-dessus de la session réelle la plus lourde, tout en restant très en dessous de ce que produit un véritable emballement : une session qui rejoue sa table en boucle de battement en génère des multiples, le disjoncteur disjoncte donc toujours.

Une chose que la même mesure met en évidence et que je préfère énoncer plutôt que laisser implicite : une cible agrégeant de nombreuses sessions bruyantes peut légitimement franchir le plafond. Les 98 sessions rrc00 réunies produisent environ 50 millions d’intervalles par jour, à peu près 2 millions par heure, confortablement au-delà du nouveau plafond. Si vous pointez Osprey sur quelque chose de ce genre, le disjoncteur se déclenchera, et il a raison.

Cela mérite d’être dit franchement, car les deux nombres ont l’air de se contredire : le plafond est une garde contre l’emballement, pas un plan de capacité. Il est fixé par cible, au-dessus de ce que produit un fonctionnement normal sur le plus gros profil que nous dimensionnons, et en dessous de ce que produit une session rejouant sa table en boucle. Une cible agrégeante qui le dépasse légitimement a dépassé une cible, pas le produit.

C’est pourquoi son déclenchement est enregistré et non simplement journalisé. Quand le plafond s’engage, l’historique est incomplet pour le reste de l’heure, et ce trou est écrit dans un registre d’angles morts que vous pouvez interroger. Un système de supervision qui cesse silencieusement de superviser est pire qu’un système qui cesse bruyamment, et la seule chose qui les distingue est de savoir si le trou est un enregistrement de premier rang.


Ce que cela ne vous dit pas

Tout ce qui précède, ce sont des octets : ce qu’occupe une table complète dans PostgreSQL, ce que le serveur BMP retient dans le tas Go, et à quelle vitesse l’historique s’accumule. C’est un axe, et une question de dimensionnement en compte plusieurs.

Non mesuré ici, et donc non affirmé :

  • Le CPU, à l’ingestion comme à la requête.
  • Le délai avant la première réponse utile après qu’une session est montée, qui est le chiffre qu’un exploitant ressent réellement.
  • Le chemin des messages entre le serveur BMP et la base : ce que coûtent le bus et l’état du websocket en direct pendant qu’une table converge.
  • La latence des requêtes sur une table de cette taille, distincte de son empreinte.

Ce sont des mesures distinctes et je ne les ai pas faites. Un guide de dimensionnement qui sous-entend discrètement le contraire, en vous donnant un chiffre de stockage assuré et en vous laissant supposer que le reste passe à l’échelle de la même façon, fait exactement ce contre quoi cet article existe.


Pourquoi publier une valeur par défaut qu’on avait fausse

Parce que l’alternative est pire d’une façon précise.

Un guide de dimensionnement bâti sur des estimations est infalsifiable. Personne ne peut le vérifier, donc personne ne trouve l’erreur, donc l’erreur part en production. Si celle-ci a été trouvée, c’est parce que les chiffres ont été produits en chargeant une vraie table et en rejouant de vraies mises à jour, et qu’un vrai flux de mises à jour se moque de ce que le fichier de configuration juge raisonnable.

La mesure a coûté environ une journée. Elle a trouvé un index en double, une valeur par défaut fausse d’un facteur quatre, et elle a produit la première réponse honnête de ce produit à « est-ce que ça tient une table complète ». À savoir : oui, à 412 Mo et 53 Mio de tas par session pour l’état vivant, et quelque part entre 14 Go et 281 Go par 90 jours pour l’historique, selon entièrement le bruit de vos pairs. Cette fourchette est la vraie réponse, pas son milieu.


L’essentiel

Chaque chiffre de cet article est une mesure du schéma livré, et l’un d’eux a contredit le code livré.

C’est l’argument pour mesurer son propre produit plutôt que de raisonner dessus. Non parce que raisonner serait négligent, mais parce qu’une valeur par défaut choisie par raisonnement n’a aucun moyen de vous dire qu’elle était fausse. Une vraie table le fait en une journée.


C’est le dernier article de cette série. Le suivant repart de dessous tout cela : comment un enregistreur rejoint le protocole d’autrui sans en devenir partie, une porte d’entrée à la fois.