<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr" xml:base="https://vincent.bernat.ch">
  <title>Vincent Bernat — Réseau</title>
  <link href="https://vincent.bernat.ch/fr/blog/atom-network.xml" rel="self"/>
  <link href="https://vincent.bernat.ch/fr/blog#tag-network" rel="alternate"/>
  <id>http://www.luffy.cx/fr/blog/atom-network.xml/</id>
  <updated>2026-09-05T11:44:12Z</updated>

  <entry>
    <title type="html">Un guide non interactif du Spanning Tree Protocol</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-spanning-tree-video" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-spanning-tree-video#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-08-30T20:54:32Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-spanning-tree-video.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Imaginez que vous louiez des bureaux pour un événement de trois jours. Vous
installez à la hâte quelques commutateurs Ethernet et scotchez des câbles au sol
pour connecter tout le monde. Malheureusement, Gaston, votre collègue le plus
maladroit, trébuche sur un câble à chaque fois qu’il se lève pour aller chercher
un café. Des câbles supplémentaires régleraient le problème, mais une boucle se
transforme en tempête de diffusion : les paquets Ethernet se multiplient jusqu’à
saturation. C’est là qu’intervient le <em>spanning tree protocol</em> : il bloque juste
assez de câbles pour ne laisser qu’un arbre sans boucle, et le reconstruit en
une seconde à chaque nouvelle bévue de Gaston<sup id="fnref:credits"><a class="footnote-ref" href="#fn:credits">1</a></sup>.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1920px"><span class="lf-media-inner" style="padding-bottom: 56.250%"><video width="1920" height="1080" controls="" preload="none" crossorigin="anonymous" poster="https://media-bernat-ch.b-cdn.net/images/posters/2026-spanning-tree.f0cfb7dd350fbe.jpg" class="lf-media lf-opaque"><source src="https://media.bernat.ch/videos/2026-spanning-tree.m3u8" type="application/vnd.apple.mpegurl"/><source src="https://media.bernat.ch/videos/2026-spanning-tree/progressive.mp4" type="video/mp4; codecs=&quot;mp4a.40.2,avc1.4d401f&quot;"/><track src="https://media.bernat.ch/videos/2026-spanning-tree.chapters.vtt" kind="chapters" label="Chapitres"/><track src="https://media.bernat.ch/videos/2026-spanning-tree.en.vtt" kind="subtitles" srclang="en" label="English"/><track src="https://media.bernat.ch/videos/2026-spanning-tree.fr.vtt" kind="subtitles" srclang="fr" default="" label="français"/></video></span></div></figure>
<p>Ce contenu est aussi disponible en <a href="/fr/blog/2026-spanning-tree" title="Un guide interactif du Spanning Tree Protocol">version texte</a>, avec des
démonstrations interactives qui font tourner une véritable implémentation
directement dans votre navigateur !</p>
<hr/>
<p>Cette vidéo est une expérimentation<sup id="fnref:time"><a class="footnote-ref" href="#fn:time">2</a></sup>. Honnêtement, hormis pour Radia
Perlman <a href="#video:seek-130">lisant son poème</a><sup id="fnref:radia"><a class="footnote-ref" href="#fn:radia">3</a></sup>, vous devriez plutôt lire
l’<a href="/fr/blog/2026-spanning-tree" title="Un guide interactif du Spanning Tree Protocol">article original</a>. Il présente le même contenu, mais vous
pouvez jouer avec les exemples interactifs, qui en sont la principale
contribution. En revanche, si la vidéo vous plaît, dites-le moi en commentaire !</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:credits">
<p>Les sprites de Gaston et Nono proviennent de <a href="https://craftpix.net/" title="Craftpix: 2D game assets">Craftpix</a>. La
musique est « <a href="https://www.chosic.com/download-audio/25044/" title="Sonatina No. 2 In G Major – III. Allegro by Aaron Dunn">Sonatina No. 2 in G Major – III. Allegro</a> », d’Aaron
Dunn. <a class="footnote-backref" href="#fnref:credits" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:time">
<p>Je pensais pouvoir produire cette vidéo en deux coups de cuiller à pot
avec quelques outils. Au final, c’était encore un puits sans fond et il m’a
fallu plus de 12 heures. <a class="footnote-backref" href="#fnref:time" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:radia">
<p>Sa voix provient d’une <a href="https://www.youtube.com/watch?v=jOqzLcM2Hbo">vidéo YouTube</a> et a été
nettoyée. <a class="footnote-backref" href="#fnref:radia" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Un guide interactif du Spanning Tree Protocol</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-spanning-tree" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-spanning-tree#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-08-24T15:00:00Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-spanning-tree.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<div class="admonition when-rss">
<p class="admonition-title">Avertissement</p>
<p>Cet article contient des exemples interactifs. Pour les
visualiser et interagir avec eux, vous devez <a href="/fr/blog/2026-spanning-tree" title="Un guide interactif du Spanning Tree Protocol">quitter votre lecteur de
flux</a>.</p>
</div>

<p>Imaginez que vous louiez des bureaux pour un événement de trois jours. Vous
installez à la hâte quelques commutateurs Ethernet et scotchez des câbles au sol
pour connecter tout le monde. Malheureusement, Gaston, votre collègue le plus
maladroit, trébuche sur un câble à chaque fois qu’il se lève pour aller chercher
un café. Vous pourriez ajouter des câbles supplémentaires, mais vous
provoqueriez alors une tempête de diffusion : des paquets Ethernet qui tournent
en boucle et se multiplient jusqu’à saturation.</p>
<p>C’est là qu’intervient le <em>spanning tree protocol</em> (<abbr title="Spanning Tree Protocol">STP</abbr>). <abbr title="Spanning Tree Protocol">STP</abbr> bloque le trafic
sur un sous-ensemble des câbles pour ne laisser qu’un arbre sans boucle. Quand
Gaston récidive, <abbr title="Spanning Tree Protocol">STP</abbr> reconstruit l’arbre en une seconde, ce qui laisse à Nono,
votre unique renfort technique, le temps de rebrancher le câble<sup id="fnref:sprites"><a class="footnote-ref" href="#fn:sprites">1</a></sup>. Jugez
par vous-même : l’illustration ci-dessous fait tourner une véritable
implémentation de <abbr title="Spanning Tree Protocol">STP</abbr> dans votre navigateur !</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:demo

A1 @0,0 prio=4096
A2 @0,1
A3 @0,2
A4 @0,3

B1 @1,0 prio=8192
B2 @1,1
B3 @1,2
B4 @1,3

C1 @2,0 prio=8192
C2 @2,1
C3 @2,2
C4 @2,3

A1 -- A2 hazard=0
A2 -- A3 hazard=0
A3 -- A4 hazard=0
B1 -- B2
B2 -- B3
B3 -- B4
C1 -- C2 hazard=0
C2 -- C3 hazard=0
C3 -- C4 hazard=0

A1 -- B1 cost=10
B1 -- C1 cost=10
A4 -- B4 cost=20
B4 -- C4 cost=20

Léo @-0.3,0.7 proto=none icon=👦🏻
Zoé @-0.3,1.3 proto=none icon=👧🏽
Eva @0.3,0.7  proto=none icon=👱🏻‍♀️
Luc @0.3,1.3  proto=none icon=👨🏾
A2 -- Léo hazard=0 A2:edge
A2 -- Zoé hazard=0 A2:edge
A2 -- Eva hazard=0 A2:edge
A2 -- Luc hazard=0 A2:edge

Max @-0.3,1.7 proto=none icon=👨🏽
Ana @-0.3,2.3 proto=none icon=👩🏾
Ida @0.3,1.7  proto=none icon=👵🏾
Léa @0.3,2.3  proto=none icon=👩🏼
A3 -- Max hazard=0 A3:edge
A3 -- Ana hazard=0 A3:edge
A3 -- Ida hazard=0 A3:edge
A3 -- Léa hazard=0 A3:edge

Tom @0.7,0.7 proto=none icon=👦🏼
Zac @0.7,1.3 proto=none icon=👨🏻
Sam @1.3,0.7  proto=none icon=🧑🏽
Noa @1.3,1.3  proto=none icon=👱🏼
B2 -- Tom hazard=0.2 B2:edge
B2 -- Zac hazard=0.2 B2:edge
B2 -- Sam hazard=0.2 B2:edge
B2 -- Noa hazard=0.2 B2:edge

Isa @0.7,1.7 proto=none icon=👩🏻
Cam @0.7,2.3 proto=none icon=🧑🏾‍🦱
Aya @1.3,1.7  proto=none icon=🧕🏽
Guy @1.3,2.3  proto=none icon=👴🏿
B3 -- Isa hazard=0.2 B3:edge
B3 -- Cam hazard=0.2 B3:edge
B3 -- Aya hazard=0.2 B3:edge
B3 -- Guy hazard=0.2 B3:edge

Awa @1.7,0.7 proto=none icon=👩🏿
Ève @1.7,1.3 proto=none icon=👧🏼
Aël @2.3,0.7  proto=none icon=🧓🏿
Ali @2.3,1.3  proto=none icon=🧔🏾
C2 -- Awa hazard=0 C2:edge
C2 -- Ève hazard=0 C2:edge
C2 -- Aël hazard=0 C2:edge
C2 -- Ali hazard=0 C2:edge

Gil @1.7,1.7 proto=none icon=👨🏼‍🦳
Lou @1.7,2.3 proto=none icon=🧑🏿
Lia @2.3,1.7  proto=none icon=👧🏻
Mia @2.3,2.3  proto=none icon=👩🏽‍🦰
C3 -- Gil hazard=0 C3:edge
C3 -- Lou hazard=0 C3:edge
C3 -- Lia hazard=0 C3:edge
C3 -- Mia hazard=0 C3:edge
</code></pre></div>

<div class="admonition">
<p class="admonition-title">Note</p>
<p>Cet article est aussi disponible en <a href="/fr/blog/2026-spanning-tree-video" title="Un guide non interactif du Spanning Tree Protocol">vidéo</a>, mais je vous
conseille de continuer ici afin d’explorer les démonstrations interactives.</p>
</div>
<div class="toc">
<ul>
<li><a href="#les-bases">Les bases</a><ul>
<li><a href="#interlude-historique">Interlude historique</a></li>
<li><a href="#election-de-la-racine">Élection de la racine</a></li>
<li><a href="#attribution-des-roles-aux-ports">Attribution des rôles aux ports</a></li>
<li><a href="#transition-detat-des-ports">Transition d’état des ports</a></li>
<li><a href="#notification-de-changement-de-topologie">Notification de changement de topologie</a></li>
<li><a href="#securite">Sécurité</a></li>
</ul>
</li>
<li><a href="#pourquoi-rstp-aujourdhui">Pourquoi RSTP aujourd’hui ?</a><ul>
<li><a href="#quelle-taille-pour-un-reseau">Quelle taille pour un réseau ?</a></li>
<li><a href="#rstp-est-il-rapide">RSTP est-il rapide ?</a></li>
</ul>
</li>
<li><a href="#a-propos-de-mstp">À propos de MSTP</a></li>
<li><a href="#a-propos-des-exemples-interactifs">À propos des exemples interactifs</a></li>
</ul>
</div>
<h1 id="les-bases">Les bases</h1>
<p>Conçu dans les années 80, le <em>spanning tree protocol</em> a donné naissance à une
déclinaison « rapide » (<abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>) et à une variante « compatible VLAN »
(<abbr title="Multiple Spanning Tree Protocol">MSTP</abbr>)<sup id="fnref:history"><a class="footnote-ref" href="#fn:history">2</a></sup>. Tout ingénieur réseau sensé sait qu’il existe de meilleures
solutions, comme <a href="/fr/blog/2017-vxlan-bgp-evpn" title="VXLAN: BGP EVPN avec FRR">BGP EVPN VXLAN</a>. Pourtant, puisque n’importe quel
commutateur le parle, le vénérable <em>spanning tree protocol</em> n’a pas dit son
dernier mot.</p>
<p>Nous nous concentrons sur <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> : il a remplacé le protocole d’origine en 2004.
Pour éliminer les boucles réseau, <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> met en œuvre une machine à états
complexe. Les temporisateurs, les changements d’état des liens et les trames de
contrôle qu’un pont reçoit de ses voisins directs pilotent ses transitions. Ces
trames Ethernet sont les <em>Bridge Protocol Data Units</em> (<abbr title="Bridge Protocol Data Unit">BPDU</abbr>). Vous pouvez les
voir à l’œuvre ci-dessous : appuyez sur le bouton « <em>Start</em> ».</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp
:tx-hold 10

A1 @0,1
C11 @1,0 prio=4096 icon=🌳
C12 @1,2 prio=4096 icon=🌳
C21 @2,0 prio=4096 icon=🌳
C22 @2,2 prio=4096 icon=🌳
A2 @3,1

H1 @0,0.2 proto=none icon=💻
H2 @0,1.8 proto=none icon=🖨️
H3 @3,0.2 proto=none icon=📠
H4 @3,1.8 proto=none icon=📺

A1 -- C11
A1 -- C12
A2 -- C21
A2 -- C22
C11 -- C12
C11 -- C21
C11 -- C21
C11 -- C22
C12 -- C21
C12 -- C22
C21 -- C22
A1 -- H1 A1:edge
A1 -- H2 A1:edge
A2 -- H3 A2:edge
A2 -- H4 A2:edge
</code></pre></div>

<p>Au bout de <a href="#mstp:5,...">quelques instants</a>, la topologie converge vers un
arbre : depuis la racine C11, il existe un chemin vers chaque pont<sup id="fnref:bridge"><a class="footnote-ref" href="#fn:bridge">3</a></sup> et
aucune boucle. En haut à droite, l’interface affiche une icône d’arbre 🌳 suivie
du temps qu’il a fallu pour atteindre cet état. <a href="#mstp:9,C11--C12,...">Coupez un
lien</a> et observez comment le protocole trouve en moins
d’une seconde un autre chemin pour joindre C12. Vous pouvez arrêter la
simulation, l’avancer pas à pas, la réinitialiser ou la ralentir avec le mode « escargot » 🐌. Ne vous inquiétez pas de toutes les informations affichées : je
les explique plus loin.</p>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Pendant que vous faites défiler la page, la simulation en cours reste
visible pour que vous puissiez la garder en vue au fil de votre lecture.
Décochez cette case pour désactiver ce comportement : <input type="checkbox" id="lf-mstp-sticky" checked="checked"/>.</p>
</div>
<p>Tous les exemples s’exécutent dans votre navigateur grâce à <a href="https://github.com/mstpd/mstpd" title="Multiple Spanning Tree Protocol Daemon">MSTPD</a>, une
implémentation libre de <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr><sup id="fnref:incomplete"><a class="footnote-ref" href="#fn:incomplete">4</a></sup> fonctionnant en espace
utilisateur<sup id="fnref:kernel"><a class="footnote-ref" href="#fn:kernel">5</a></sup>.</p>
<h2 id="interlude-historique">Interlude historique</h2>
<p><a href="https://hiddenheroes.netguru.com/radia-perlman" title="The Magic Box and the Spanning Tree: How Radia Perlman made the Internet work">Radia Perlman</a>, intronisée à l’<a href="https://www.internethalloffame.org/inductee/radia-perlman/" title="Radia Perlman profile on the Internet Hall of Fame">Internet Hall of Fame</a> en 2014, a résumé
dans ce poème l’ancêtre de <abbr title="Spanning Tree Protocol">STP</abbr> qu’elle a inventé chez <abbr title="Digital Equipment Corporation">DEC</abbr>. Il a été repris plus
tard dans un <a href="https://patents.google.com/patent/US7339900B2/en" title="Method and apparatus for preventing spanning tree loops during traffic overload conditions">brevet américain</a> :</p>
<blockquote>
<p>I think that I shall never see<br/>
A graph more lovely than a tree.<br/>
A tree whose crucial property<br/>
Is loop-free connectivity.<br/>
A tree which must be sure to span<br/>
So packets can reach every LAN.<br/>
First, the root must be selected.<br/>
By ID, it is elected.<br/>
Least cost paths from root are traced.<br/>
In the tree, these paths are placed.<br/>
A mesh is made by folks like me,<br/>
Then bridges find a spanning tree.</p>
<p>― <em>Radia Perlman</em>, <a href="https://hiddenheroes.netguru.com/radia-perlman" title="The Magic Box and the Spanning Tree: How Radia Perlman made the Internet work">Algorhyme</a>.</p>
</blockquote>
<h2 id="election-de-la-racine">Élection de la racine</h2>
<p>Pour construire un arbre, <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> commence par élire comme <strong>pont racine</strong> celui
qui a l’<strong>identifiant de pont le plus faible</strong>. Cet identifiant combine la
priorité et l’adresse MAC : <code>8192.6e:2b:10:a0:5f:29</code>.</p>
<p>Dans l’exemple ci-dessous, S1 et S2 ont des priorités de 4 096 et 8 192 : S1
devient racine. S4 a une priorité de 12 288, tandis que S3 conserve la priorité
par défaut de 32 768<sup id="fnref:priority"><a class="footnote-ref" href="#fn:priority">6</a></sup> : S4 devient racine. S5 et S6 n’ont pas de
priorité particulière : l’adresse MAC la plus faible l’emporte et S5 devient
racine.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @0,0 prio=4096
S2 @0,1 prio=8192
S1 -- S2

S3 @1,0
S4 @1,1 prio=12288
S3 -- S4

S5 @2,0
S6 @2,1
S5 -- S6
</code></pre></div>

<p><a href="#mstp:2,S2-&gt;S1,@">Au départ</a>, chaque pont s’annonce comme racine<sup id="fnref:wireshark"><a class="footnote-ref" href="#fn:wireshark">7</a></sup> :</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="hll"><span class="ss">    Root Identifier</span>: 8192.02:00:00:01:00:01
</span><span class="hll"><span class="ss">    Bridge Identifier</span>: 8192.02:00:00:01:00:01
</span></code></pre></div>

<p>Dès qu’un pont reçoit une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> annonçant une meilleure racine, il
<a href="#mstp:3,S2-&gt;S1,@">propage</a> cette nouvelle information à ses voisins.</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="hll"><span class="ss">    Bridge Identifier</span>: 8192.02:00:00:00:00:01
</span></code></pre></div>

<h2 id="attribution-des-roles-aux-ports">Attribution des rôles aux ports</h2>
<p>La deuxième étape consiste à attribuer un rôle à chaque port. <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> définit cinq
rôles, chacun représenté par une lettre :</p>
<ul>
<li>racine (R, <em>root</em>),</li>
<li>désigné (D, <em>designated</em>),</li>
<li>alternatif (A, <em>alternate</em>),</li>
<li>désactivé (X, <em>disabled</em>),</li>
<li>de secours (B, <em>backup</em>)<sup id="fnref:backup"><a class="footnote-ref" href="#fn:backup">8</a></sup>.</li>
</ul>
<p>Chaque pont qui n’est pas racine choisit comme <strong>port racine</strong> celui dont le
chemin vers la racine a le coût le plus faible. Sauf configuration explicite,
chaque pont déduit le coût d’un lien à partir de son débit : 20 000 pour
1 Gbit/s. En cas d’égalité, l’identifiant de port le plus faible l’emporte.</p>
<p>Chacun des ports restants devient un <strong>port désigné</strong> si la <abbr title="Bridge Protocol Data Unit">BPDU</abbr> qu’il émet est
« meilleure » que celle qu’il reçoit. Sinon, il devient un <strong>port alternatif</strong>.
Plus tard, si le port racine tombe, le « meilleur » port alternatif devient le
port racine. Les critères pour choisir la meilleure <abbr title="Bridge Protocol Data Unit">BPDU</abbr> sont :</p>
<ol>
<li>l’identifiant de pont racine le plus faible,</li>
<li>le coût cumulé jusqu’à la racine le plus faible,</li>
<li>l’identifiant de pont le plus faible,</li>
<li>l’identifiant de port le plus faible.</li>
</ol>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @1,0  prio=4096 icon=🌳
S2 @0,1
S3 @2,1

S1 -- S2
S1 -- S3
S1 -- S3
S2 -- S3
</code></pre></div>

<p>Dans l’exemple ci-dessus, <a href="#mstp:13">après convergence</a>, S1 est la racine car sa
priorité est de 4 096, alors que les autres ponts ont une priorité de 32 768.
Tous ses ports sont des ports désignés puisque le coût cumulé jusqu’à la racine
est nul.</p>
<p>Le port de S2 face à S1 devient un port racine car il présente le coût cumulé le
plus faible vers la racine : 20 000 contre 40 000. S3 possède deux ports face à
S1 et celui dont l’identifiant de port est le plus faible devient le port
racine : <code>0x8000</code> contre <code>0x8001</code>. L’autre candidat est un port alternatif car
le port distant sur ce lien émet une meilleure <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, avec un coût cumulé nul.
Sur le segment entre S2 et S3, c’est le port de S2 qui l’emporte : les deux
ponts ont le même coût cumulé jusqu’à la racine (20 000), mais l’identifiant de
pont de S2 est plus petit : <code>32768.02:00:00:00:00:01</code> contre
<code>32768.02:00:00:00:00:02</code>.</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="hll"><span class="ss">    Root Path Cost</span>: 20000
</span><span class="hll"><span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:01
</span><span class="ss">    Port identifier</span>: 0x8002
</code></pre></div>

<p>Si vous <a href="#mstp:13,S1--S3:1">coupez le lien actif entre S1 et S3</a>, S3 promeut le
« meilleur » port alternatif en port racine. Si vous <a href="#mstp:13,S1--S3:1,S1--S3:2">désactivez aussi le second
lien</a>, S3 retient le port alternatif restant comme
port racine. En revanche, si vous <a href="#mstp:13,S1--S2,@,...">désactivez le lien entre S1 et
S2</a>, S2 doit travailler un peu plus pour élire un nouveau
port racine, car il ne dispose d’aucun port alternatif.</p>
<p>Sauf événement particulier, les ports désignés émettent une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> <a href="#mstp:13,S1-&gt;S3:2,S2-&gt;S3,S1-&gt;S2,@">toutes les 2
secondes</a><sup id="fnref:hello"><a class="footnote-ref" href="#fn:hello">9</a></sup>. Si un pont ne reçoit plus de
<abbr title="Bridge Protocol Data Unit">BPDU</abbr> de son voisin pendant 3 périodes « <em>hello</em> » consécutives, il le considère
comme mort et efface les informations associées au port.</p>
<h2 id="transition-detat-des-ports">Transition d’état des ports</h2>
<p>Chaque port se trouve dans l’un des trois états suivants. Le schéma représente
chaque état par une couleur de fond :</p>
<ul>
<li>rejet (<em>discarding</em>, rouge),</li>
<li>apprentissage (<em>learning</em>, jaune),</li>
<li>transmission (<em>forwarding</em>, vert).</li>
</ul>
<p>Un <em>port racine</em> passe automatiquement à l’état de transmission. Un <em>port
alternatif</em> reste à l’état de rejet. Un <em>port désigné</em> dispose de deux moyens
pour passer de l’état de rejet à l’état de transmission :</p>
<ul>
<li>Si le port est un <strong>port d’extrémité</strong> (<em>edge port</em>), soit par configuration,
  soit parce que l’équipement distant ne parle aucune variante de <abbr title="Spanning Tree Protocol">STP</abbr>, le pont
  suppose que cet équipement ne participe pas au protocole et ne peut donc pas
  créer de boucle. Dans ce cas, le port désigné passe immédiatement à l’état de
  transmission.</li>
<li>Sinon, il envoie une <strong>proposition</strong> à son voisin en aval. Si le pont distant
  estime que la <abbr title="Bridge Protocol Data Unit">BPDU</abbr> reçue est « meilleure » que toutes celles mémorisées pour
  ses autres ports, il élit le port de réception comme port racine et démarre le
  processus de <strong>synchronisation</strong> : pour éviter une boucle, il fait passer à
  l’état de rejet tous les ports désignés qui ne sont ni des ports d’extrémité
  ni déjà synchronisés. Il renvoie ensuite un <strong>accord</strong>. À la réception de cet
  accord, le port désigné du pair passe à l’état de transmission<sup id="fnref:learning"><a class="footnote-ref" href="#fn:learning">10</a></sup>.</li>
</ul>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @1,0 prio=4096 icon=🌳
S2 @1,1
S3 @0,2
S4 @2,2
S5 @0,3 prio=8192 icon=🪾
S6 @2,3
H1 @0,1.2   proto=none icon=🖨️
H2 @2,1.2   proto=none icon=📠
H3 @2.5,1.3 proto=none icon=📺
H4 @2.5,2.3 proto=none icon=💻

S1 -- S2
S2 -- S3
S2 -- S4
S3 -- S5
S4 -- S6
S4 -- S3
S5 -- S6

S3 -- H1 S3:edge
S4 -- H2 S4:edge
S4 -- H3 S4:edge
S6 -- H4 S6:edge
</code></pre></div>

<p>Dans la topologie ci-dessus, H1, H2, H3 et H4 sont des équipements terminaux qui
ne participent pas au protocole. Nous configurons les ports auxquels ils sont
raccordés comme des ports d’extrémité : ces ports passent donc immédiatement à
l’état de transmission.</p>
<p>Utilisez le bouton « <em>step</em> » pour faire avancer la simulation. L’horloge passe
à 1 seconde. <a href="#mstp:2,S1-&gt;S2,S2-&gt;S1,@">Avancez encore d’un cran</a> : S1 et S2
s’envoient mutuellement une proposition. Voici celle de S2 :</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x4e, Agreement, Port Role: Designated, Proposal
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
        <span class="no">..0. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: No
        <span class="no">...0 .... </span><span class="o">= </span><span class="ss">Learning</span>: No
<span class="hll">        <span class="no">.... 11.. </span><span class="o">= </span><span class="ss">Port Role</span>: Designated <span class="p">(</span><span class="m">3</span><span class="p">)</span>
</span><span class="hll">        <span class="no">.... ..1. </span><span class="o">= </span><span class="ss">Proposal</span>: Yes
</span>        <span class="no">.... ...0 </span><span class="o">= </span><span class="ss">Topology Change</span>: No
<span class="hll"><span class="ss">    Root Identifier</span>: 32768.02:00:00:00:00:01
</span><span class="ss">    Root Path Cost</span>: 0
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:01
<span class="ss">    Port identifier</span>: 0x8001
</code></pre></div>

<p>S1 ignore cette proposition : son propre identifiant de racine est plus faible.
Quand S2 reçoit une proposition similaire de S1, il accepte S1 comme racine. Il
élit également le port vers S1 comme port racine et démarre le processus de
synchronisation. Ses deux ports désignés sont déjà à l’état de rejet : rien ne
change de ce côté. <a href="#mstp:3,S2-&gt;S1#2,@">Avancez encore d’un cran</a> : S2 envoie
deux <abbr title="Bridge Protocol Data Unit">BPDU</abbr> à S1. Dans l’une d’elles, le bit d’accord vaut 1 et le bit de
proposition vaut 0. Elle montre aussi que S2 a accepté S1 comme racine et que
son port racine est désormais à l’état de transmission. À la réception de cette
<abbr title="Bridge Protocol Data Unit">BPDU</abbr>, S1 fait passer son propre port désigné à l’état de transmission. À partir
de cet instant, le lien entre S1 et S2 achemine le trafic utilisateur.</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x79, Agreement, Forwarding, Learning, Port Role: Root, Topology Change
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
<span class="hll">        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
</span><span class="hll">        <span class="no">..1. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: Yes
</span>        <span class="no">...1 .... </span><span class="o">= </span><span class="ss">Learning</span>: Yes
<span class="hll">        <span class="no">.... 10.. </span><span class="o">= </span><span class="ss">Port Role</span>: Root <span class="p">(</span><span class="m">2</span><span class="p">)</span>
</span><span class="hll">        <span class="no">.... ..0. </span><span class="o">= </span><span class="ss">Proposal</span>: No
</span>        <span class="no">.... ...1 </span><span class="o">= </span><span class="ss">Topology Change</span>: Yes
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="ss">    Root Path Cost</span>: 20000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:01
<span class="ss">    Port identifier</span>: 0x8001
</code></pre></div>

<p>Voyons maintenant ce qui est arrivé à S5. <a href="#mstp:2,S5-&gt;S3,S3-&gt;S5,S5-&gt;S6,S6-&gt;S5,@">Réinitialisez la simulation et
avancez de deux pas</a>. S5 échange des <abbr title="Bridge Protocol Data Unit">BPDU</abbr>
avec S3 et S6. Comme S5 possède un identifiant de racine plus faible que S3 et
S6, il reste la racine, tandis que S3 et S6 acceptent la proposition et élisent
leurs ports racines. S3 et S6 démarrent le processus de synchronisation. Le port
de S6 vers H4 reste actif car il s’agit d’un port d’extrémité. <a href="#mstp:3,S3-&gt;S5#1,S6-&gt;S5#1,@">Avancez d’un
cran</a> : S3 et S6 renvoient tous deux un accord à
S5, qui fait passer ses deux ports désignés à l’état de transmission. Pourtant,
le lien entre S5 et S3 continue de rejeter le trafic utilisateur ! Si vous
regardez attentivement, le port de S3 vers S5 est maintenant un port désigné, et
non un port racine. Lors de la <a href="#mstp:3,S2-&gt;S3,@">même étape</a>, S3 reçoit aussi
une meilleure <abbr title="Bridge Protocol Data Unit">BPDU</abbr> de S2, avec S1 comme racine. Il élit son port vers S2 comme
port racine et rétrograde le port vers S5 en port désigné, qui reste à l’état de
rejet.</p>
<p>À l’<a href="#mstp:4,S3-&gt;S5,@">étape suivante</a>, les choses se corsent un peu. S3 envoie
une proposition à S5<sup id="fnref:agreement"><a class="footnote-ref" href="#fn:agreement">11</a></sup> :</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x4f, Agreement, Port Role: Designated, Proposal, Topology Change
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
        <span class="no">..0. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: No
        <span class="no">...0 .... </span><span class="o">= </span><span class="ss">Learning</span>: No
        <span class="no">.... 11.. </span><span class="o">= </span><span class="ss">Port Role</span>: Designated <span class="p">(</span><span class="m">3</span><span class="p">)</span>
<span class="hll">        <span class="no">.... ..1. </span><span class="o">= </span><span class="ss">Proposal</span>: Yes
</span>        <span class="no">.... ...1 </span><span class="o">= </span><span class="ss">Topology Change</span>: Yes
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
</span><span class="ss">    Root Path Cost</span>: 40000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:02
<span class="ss">    Port identifier</span>: 0x8002
</code></pre></div>

<p>S5 élit S1 comme racine et le port vers S3 comme port racine. Il démarre son
processus de synchronisation, mais le port désigné vers S6 ne passe <em>pas</em> à
l’état de rejet. Pourquoi ? Ce port reste un port désigné et son voisin S6 a
déjà envoyé un accord sur ce lien : il conserve donc son statut de port
synchronisé.</p>
<p>Revenons maintenant <a href="#mstp:3,@">un pas en arrière</a> pour observer ce qui arrive à
S6. À cet instant, S6 croit que S5 est la racine. <a href="#mstp:4,S4-&gt;S6#1,@">Avancez d’un
cran</a> : S4 envoie une nouvelle proposition à S6. S6
l’accepte, élit S1 comme racine et le port vers S4 comme port racine. Le rôle du
port face à S5 change : de port racine, il devient port désigné. Comme son pair
continue d’annoncer une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> inférieure sur le lien, ce port devient contesté
(<em>disputed</em>) et passe à l’état de rejet. Le port racine passe à l’état de
transmission et le lien devient immédiatement opérationnel, car le port désigné
de S4 est déjà à l’état de transmission. Si nous <a href="#mstp:5,S5-&gt;S6,S6-&gt;S5,@">avançons d’un
cran</a>, S5 et S6 échangent deux <abbr title="Bridge Protocol Data Unit">BPDU</abbr>. Celle de S5 est
meilleure grâce à son identifiant de pont plus faible. Le port de S5 reste un
port désigné, tandis que S6 rétrograde le sien en port alternatif.</p>
<p>Reprenons une dernière fois depuis le début : coupez le lien entre S1 et S2,
<a href="#mstp:S1--S2,9">laissez tourner la simulation jusqu’à ce que la topologie soit
stable</a>, arrêtez-la, puis rétablissez le lien entre S1 et S2.
Lors du <a href="#mstp:S1--S2,9,S1--S2,1,S1-&gt;S2,S2-&gt;S1,@">premier pas</a>, S1 et S2
échangent des propositions. S2 élit S1 comme racine à la place de S5, et le port
vers S1 comme port racine. Il rétrograde son ancien port racine en port désigné
et le place à l’état de rejet. L’autre port désigné reste synchronisé et
conserve son état de transmission. À l’<a href="#mstp:S1--S2,9,S1--S2,2,S2-&gt;S1,@">étape
suivante</a>, S2 envoie un accord à S1 et le lien
entre eux commence à acheminer le trafic utilisateur. S2 <a href="#mstp:S1--S2,9,S1--S2,2,S2-&gt;S3,@">envoie également une
proposition à S3</a>, mais pas à S4 : <a href="#mstp:S1--S2,9,S1--S2,2,S2-&gt;S4,@">il lui
envoie une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> ordinaire</a>. S4 élit malgré
tout S1 comme racine et le port vers S2 comme port racine. Il rétrograde son
ancien port racine, celui vers S3, en port désigné, qui passe à l’état de rejet
à cause du changement de port racine. L’autre port alternatif, celui vers S6,
devient lui aussi un port désigné et reste à l’état de rejet. Le nouveau port
racine passe à l’état de transmission. À l’<a href="#mstp:S1--S2,9,S1--S2,3,S3-&gt;S4#1,@">étape
suivante</a>, le port de S4 vers S3 se
stabilise comme port alternatif après avoir reçu une « meilleure » <abbr title="Bridge Protocol Data Unit">BPDU</abbr> de S3.</p>
<p><abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> est une gigantesque machine à états découpée en machines plus petites :
<em>Bridge Detection</em>, <em>Port Information</em>, <em>Port Protocol Migration</em>, <em>Port Role
Selection</em>, <em>Port Role Transitions</em>, <em>Port Receive</em>, <em>Port State Transitions</em>,
<em>Port Timers</em>, <em>Port Transmit</em> et <em>Topology Change</em>. Certaines s’appliquent à
l’ensemble du pont, d’autres à chaque port. Chaque pont en exécute une instance.
Le temps, les changements d’état opérationnel des ports et les <abbr title="Bridge Protocol Data Unit">BPDU</abbr> reçues des
autres instances en pilotent les transitions. Ce fonctionnement par événements
rend <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> plus efficace, mais aussi plus difficile à appréhender.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 900px"><span class="lf-media-inner" style="padding-bottom: 43.333%"><img alt="Locomotive à vapeur articulée Garratt de classe Msa des Western Australian&#10;Government Railways : élévation et plan" src="https://media-bernat-ch.b-cdn.net/images/msa-garratt@1x.23f40c8ca9d5b5.jpg" srcset="https://media-bernat-ch.b-cdn.net/images/msa-garratt@1x.23f40c8ca9d5b5.jpg 1131w,https://media-bernat-ch.b-cdn.net/images/msa-garratt@2x.ea938133978ead.jpg 1800w" sizes="auto, (max-width: 900px) 100vw, 900px" width="900" height="390" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAANAQMAAACEvEzUAAAAA1BMVEX///+nxBvIAAAADElEQVR42mNgoBAAAABBAAEodTPOAAAAAElFTkSuQmCC)"/></span></div><figcaption>Image provisoire de la machine à états <em>Port Information</em> extraite d'IEEE 802.1Q-2005, page 182. Dans l'attente de l'autorisation de reproduction de l'IEEE, voici le plan de la locomotive à vapeur articulée Garratt de classe Msa des Western Australian Government Railways.</figcaption></figure>
<h2 id="notification-de-changement-de-topologie">Notification de changement de topologie</h2>
<p>Un pont maintient automatiquement une table d’adresses MAC : il associe chaque
adresse MAC source au port qui l’a reçue en dernier. Pour commuter une trame
Ethernet, il consulte cette table afin de choisir le bon port<sup id="fnref:bum"><a class="footnote-ref" href="#fn:bum">12</a></sup>. Quand un
lien tombe, un frigo connecté joignable par un port peut le devenir par un
autre. Les ponts concernés doivent alors purger les adresses MAC apprises :
elles ne sont peut-être plus valables.</p>
<p>Pour cela, <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> met en œuvre des <em>notifications de changement de topologie</em> à
l’aide d’un mécanisme d’inondation. Lorsqu’un port qui n’est pas un port
d’extrémité passe à l’état de transmission, un pont génère des <abbr title="Bridge Protocol Data Unit">BPDU</abbr> dont le bit
<em>topology change</em> (<abbr title="Topology Change">TC</abbr>) est activé. Il les envoie à tous ses ports désignés, à
l’exception des ports d’extrémité, ainsi qu’à son port racine. Il purge
également la table d’adresses MAC sur ces ports. Quand un pont reçoit une telle
<abbr title="Bridge Protocol Data Unit">BPDU</abbr>, il propage la notification sur ses ports désignés hors ports d’extrémité
et sur son port racine, sauf celui par lequel elle est arrivée. Il purge lui
aussi la table d’adresses MAC sur ces ports. Dans les exemples, les <abbr title="Bridge Protocol Data Unit">BPDU</abbr> dont le
bit <abbr title="Topology Change">TC</abbr> vaut 1 sont entourées d’un cercle rouge.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

S1 @1,0 prio=4096 icon=🌳
S2 @0,1
S3 @1,1
S4 @2,1
S5 @1,2
LPT @0.1,2 proto=none icon=🖨️

S1 -- S2
S1 -- S3
S1 -- S4
S2 -- S3
S2 -- S5
S4 -- S5
S5 -- LPT S5:edge
</code></pre></div>

<p>Lancez la simulation et attendez <a href="#mstp:10">quelques secondes</a> que la topologie
se stabilise. Arrêtez la simulation et <a href="#mstp:10,S2--S5">désactivez le lien entre S2 et
S5</a>. S5 élit le port face à S4 comme port racine, lequel passe
immédiatement à l’état de transmission. <a href="#mstp:10,S2--S5,1,S5-&gt;S4,@">Avancez d’un
cran</a> : S5 émet une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> avec le bit <abbr title="Topology Change">TC</abbr> à 1 :</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x79, Agreement, Forwarding, Learning, Port Role: Root, Topology Change
        <span class="no">0... .... </span><span class="o">= </span><span class="ss">Topology Change Acknowledgment</span>: No
        <span class="no">.1.. .... </span><span class="o">= </span><span class="ss">Agreement</span>: Yes
        <span class="no">..1. .... </span><span class="o">= </span><span class="ss">Forwarding</span>: Yes
        <span class="no">...1 .... </span><span class="o">= </span><span class="ss">Learning</span>: Yes
        <span class="no">.... 10.. </span><span class="o">= </span><span class="ss">Port Role</span>: Root <span class="p">(</span><span class="m">2</span><span class="p">)</span>
        <span class="no">.... ..0. </span><span class="o">= </span><span class="ss">Proposal</span>: No
<span class="hll">        <span class="no">.... ...1 </span><span class="o">= </span><span class="ss">Topology Change</span>: Yes
</span><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
<span class="ss">    Root Path Cost</span>: 40000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:04
<span class="ss">    Port identifier</span>: 0x8002
</code></pre></div>

<p>S4 reçoit cette <abbr title="Bridge Protocol Data Unit">BPDU</abbr>. Il purge la table d’adresses MAC sur le port face à S1 :
LPT était auparavant joignable par ce port, mais il l’est désormais par S5.
<a href="#mstp:10,S2--S5,2,S4-&gt;S1,@">Avancez d’un cran</a> : S4 envoie à S1
une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> avec le bit <abbr title="Topology Change">TC</abbr> à 1. À la réception de cette <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, S1 purge la table
d’adresses MAC sur les ports face à S2 et S3. <a href="#mstp:10,S2--S5,3,S1-&gt;S2,S1-&gt;S3,@">Avancez d’un
cran</a> : S1 envoie une notification à S2 et à
S3. <a href="#mstp:10,S2--S5,4,S2-&gt;S3,@">Avancez encore d’un cran</a> : S2 envoie une
notification à S3, tandis que S3 ne fait rien car son port vers S2 est un <em>port
alternatif</em>. S3 ne purge aucune table d’adresses MAC : LPT reste joignable par
son port vers S1.</p>
<p>Si vous <a href="#mstp:10,S2--S5,6,@">avancez encore un peu</a>, vous verrez que certaines
<abbr title="Bridge Protocol Data Unit">BPDU</abbr> périodiques conservent le bit <abbr title="Topology Change">TC</abbr> à 1. Chaque port dispose d’un
temporisateur égal au temporisateur « <em>hello</em> » plus une seconde<sup id="fnref:timer"><a class="footnote-ref" href="#fn:timer">13</a></sup>. Ce
temporisateur démarre quand le port émet une notification. Jusqu’à son
expiration, le port positionne le bit <abbr title="Topology Change">TC</abbr> à 1 dans toutes les <abbr title="Bridge Protocol Data Unit">BPDU</abbr> qu’il envoie.
Vous pouvez aussi voir <a href="#mstp:10,S2--S5,7,S1-&gt;S4,S4-&gt;S5,@">certaines <abbr title="Bridge Protocol Data Unit">BPDU</abbr>
périodiques</a> sans le bit <abbr title="Topology Change">TC</abbr> : elles
proviennent d’un port qui n’a fait que recevoir une notification et qui n’a donc
pas armé son temporisateur.</p>
<h2 id="securite">Sécurité</h2>
<p><abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> est sensible aux erreurs de configuration et peu résistant face aux acteurs
malveillants. Un pont qui ne parle pas <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> peut créer une boucle. Une personne
malveillante peut s’insérer dans la topologie pour perturber le service,
espionner le trafic ou le modifier.</p>
<p>Pour limiter ces problèmes, vous devez identifier les ports d’extrémité. Un port
d’extrémité est raccordé à un équipement tel qu’un PC ou une imprimante. Ces
équipements ne génèrent pas de <abbr title="Bridge Protocol Data Unit">BPDU</abbr> et ne peuvent pas créer de boucle. <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>
propose deux options liées :</p>
<ul>
<li>Quand elle est vraie, <strong>AdminEdge</strong> initialise un port comme port d’extrémité.</li>
<li>Quand elle est vraie, <strong>AutoEdge</strong> permet à un port de devenir un port
  d’extrémité s’il ne reçoit aucune <abbr title="Bridge Protocol Data Unit">BPDU</abbr> pendant 3 secondes. Cette option est
  activée par défaut.</li>
</ul>
<p>Si un port d’extrémité reçoit une <abbr title="Bridge Protocol Data Unit">BPDU</abbr>, quelles que soient les valeurs de ces
deux options, il redevient un port ordinaire.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>R0 @1.5,1.5 prio=8192

# AutoEdge=true, AdminEdge=false, bridge
S1 @3,1.58
R0 -- S1

# AutoEdge=true, AdminEdge=false, end device
H1 @2.84,2.18 icon=🖨️ proto=none
R0 -- H1

# AutoEdge=true, AdminEdge=true, bridge
S2 @2.18,2.84
R0 -- S2 R0:edge

# AutoEdge=true, AdminEdge=true, end device
H2 @1.58,3 icon=💻 proto=none
R0 -- H2 R0:edge

# AutoEdge=false, AdminEdge=true, bridge
S3 @0.68,2.76
R0 -- S3 R0:edge R0:no-auto-edge

# AutoEdge=false, AdminEdge=true, end device
H3 @0.24,2.32 icon=📠 proto=none
R0 -- H3 R0:edge R0:no-auto-edge

# AutoEdge=false, AdminEdge=false, bridge
S4 @0,1.42
R0 -- S4 R0:no-auto-edge

# AutoEdge=false, AdminEdge=false, end device
H4 @0.16,0.82 icon=📺 proto=none
R0 -- H4 R0:no-auto-edge

# Network port, bridge
S5 @0.82,0.16
R0 -- S5 R0:network S5:network

# Network port, end device
H5 @1.42,0 icon=☕ proto=none
R0 -- H5 R0:network

# AdminEdge=true, bpdu-guard=true, bridge
S6 @2.32,0.24
R0 -- S6 R0:bpdu-guard R0:edge

# AdminEdge=true, bpdu-guard=true, end device
H6 @2.76,0.68 icon=💡 proto=none
R0 -- H6 R0:bpdu-guard R0:edge
</code></pre></div>

<p>Dans la topologie ci-dessus, S1, S2, S3, S4, S5 et S6 se comportent comme des
ponts, tandis que H1, H2, H3, H4, H5 et H6 se comportent comme des équipements
terminaux :</p>
<ul>
<li>S1 et H1 sont sur un port sans configuration : <em>AutoEdge</em> est vraie,
  <em>AdminEdge</em> est fausse,</li>
<li>S2 et H2 sont sur un port où <em>AdminEdge</em> est vraie,</li>
<li>S3 et H3 sont sur un port où <em>AutoEdge</em> est fausse et <em>AdminEdge</em> est vraie,</li>
<li>S4 et H4 sont sur un port où <em>AutoEdge</em> est fausse.</li>
</ul>
<p>Si vous lancez la simulation et <a href="#mstp:24">attendez une vingtaine de secondes</a>,
les liens vers S1, S2, S3, S4, H1, H2, H3 et H4 finissent par acheminer le
trafic utilisateur : aucune de ces options n’a d’importance.</p>
<p>Mais qu’en est-il des deux dernières paires ? S5 et H5 sont raccordés à un port
de type <em>network</em>. Un tel port active une fonctionnalité propriétaire : le
<strong>bridge assurance</strong>. Le port émet des <abbr title="Bridge Protocol Data Unit">BPDU</abbr> quel que soit son rôle. S’il n’en
reçoit aucune pendant 3 périodes « <em>hello</em> » consécutives, il passe à l’état de
rejet. Sur le lien entre R0 et S5, vous pouvez voir des <a href="#mstp:24,R0-&gt;S5,S5-&gt;R0,@"><abbr title="Bridge Protocol Data Unit">BPDU</abbr> circuler dans les
deux sens</a>, contrairement aux autres liens, où seuls
les ports désignés en émettent.</p>
<p>S6 et H6 sont raccordés à un port où <em>AdminEdge</em> est vraie et où le <strong><abbr title="Bridge Protocol Data Unit">BPDU</abbr>
guard</strong> est activé. Il s’agit d’une autre fonctionnalité propriétaire, qui
désactive un port <a href="#mstp:2,S6-&gt;R0,@">s’il reçoit une <abbr title="Bridge Protocol Data Unit">BPDU</abbr></a>.</p>
<p>En résumé, si vous attendez d’un port qu’il soit un port d’extrémité,
positionnez <em>AdminEdge</em> à vrai et activez le <em><abbr title="Bridge Protocol Data Unit">BPDU</abbr> guard</em>. Sinon, déclarez-le
comme port <em>network</em>.</p>
<h1 id="pourquoi-rstp-aujourdhui">Pourquoi <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> aujourd’hui ?</h1>
<p>Un cas d’usage solide pour <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> aujourd’hui est le réseau d’administration hors
bande (<abbr title="Out-of-band">OOB</abbr>) d’un centre de données, où quelques secondes d’indisponibilité sont
tolérables. La configuration est minimale et vous pouvez utiliser des
commutateurs bon marché, comme un Cisco 2960X<sup id="fnref:price"><a class="footnote-ref" href="#fn:price">14</a></sup>. Deux commutateurs jouent
le rôle de ponts racines et plusieurs boucles raccordent les commutateurs
présents dans chaque baie. Cette conception simple survit à une panne sur chaque
boucle<sup id="fnref:erps"><a class="footnote-ref" href="#fn:erps">15</a></sup>.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp
:tx-hold 10

# Root bridges
R1 @0,1 prio=0
R2 @0,2 prio=4096
R1 -- R2 cost=200 R1:network R2:network
R1 -- R2 cost=200 R1:network R2:network

# First loop
C1  @1,0 icon=🗄️
C4  @2,0 icon=🗄️
C7  @3,0 icon=🗄️
C10 @4,0 icon=🗄️
C12 @5,0 icon=🗄️
C13 @5,3 icon=🗄️
C15 @4,3 icon=🗄️
C18 @3,3 icon=🗄️
C21 @2,3 icon=🗄️
C24 @1,3 icon=🗄️
R1  -- C1  R1:network C1:network
C1  -- C4  C1:network C4:network
C4  -- C7  C4:network C7:network
C7  -- C10 C7:network C10:network
C10 -- C12 C10:network C12:network
C12 -- C13 C12:network C13:network
C13 -- C15 C13:network C15:network
C15 -- C18 C15:network C18:network
C18 -- C21 C18:network C21:network
C21 -- C24 C21:network C24:network
C24 -- R2  C24:network R2:network

# Second loop
C2  @1,0.5 icon=🗄️
C5  @2,0.5 icon=🗄️
C8  @3,0.5 icon=🗄️
C11 @4,0.5 icon=🗄️
C14 @4,2.5 icon=🗄️
C17 @3,2.5 icon=🗄️
C20 @2,2.5 icon=🗄️
C23 @1,2.5 icon=🗄️
R1  -- C2  R1:network C2:network
C2  -- C5  C2:network C5:network
C5  -- C8  C5:network C8:network
C8  -- C11 C8:network C11:network
C11 -- C14 C11:network C14:network
C14 -- C17 C14:network C17:network
C17 -- C20 C17:network C20:network
C20 -- C23 C20:network C23:network
C23 -- R2  C23:network R2:network

# Third loop
C3  @1,1 icon=🗄️
C6  @2,1 icon=🗄️
C9  @3,1 icon=🗄️
C16 @3,2 icon=🗄️
C19 @2,2 icon=🗄️
C22 @1,2 icon=🗄️
R1  -- C3  R1:network C3:network
C3  -- C6  C3:network C6:network
C6  -- C9  C6:network C9:network
C9  -- C16 C9:network C16:network
C16 -- C19 C16:network C19:network
C19 -- C22 C19:network C22:network
C22 -- R2  C22:network R2:network
</code></pre></div>

<p>Cette topologie converge en environ <a href="#mstp:6">6 secondes</a>. Chaque boucle doit
rester petite (environ 16 ponts) pour réduire la probabilité d’une double panne
et éviter de partager trop de bande passante. Cette conception peut évoluer un
peu sans devenir trop complexe : un VLAN par boucle ou un domaine de pont par
boucle.</p>
<h2 id="quelle-taille-pour-un-reseau">Quelle taille pour un réseau ?</h2>
<p>L’âge maximal, dont la valeur par défaut est 20, détermine la distance maximale
entre un pont et la racine. La topologie ci-dessous est trop grande : les <abbr title="Bridge Protocol Data Unit">BPDU</abbr>
issues de R1 ne parviennent pas au-delà de S20<sup id="fnref:root-mac"><a class="footnote-ref" href="#fn:root-mac">16</a></sup>.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp
:tx-hold 10
:max-age 20

R1 @0,0 prio=4096 icon=🌳
R2 @0,5 prio=4096 icon=🪾

S1  @1,0
S2  @2,0
S3  @3,0
S4  @4,0
S5  @5,0
S6  @6,0

S7  @6,1
S8  @5,1
S9  @4,1
S10 @3,1
S11 @2,1
S12 @1,1

S13 @1,2
S14 @2,2
S15 @3,2
S16 @4,2
S17 @5,2
S18 @6,2

S19 @6,3
S20 @5,3
S21 @4,3
S22 @3,3
S23 @2,3
S24 @1,3

S25 @1,4
S26 @2,4
S27 @3,4
S28 @4,4
S29 @5,4
S30 @6,4

S31 @6,5
S32 @5,5
S33 @4,5
S34 @3,5
S35 @2,5
S36 @1,5

R1  -- S1
S1  -- S2
S2  -- S3
S3  -- S4
S4  -- S5
S5  -- S6
S6  -- S7
S7  -- S8
S8  -- S9
S9  -- S10
S10 -- S11
S11 -- S12
S12 -- S13
S13 -- S14
S14 -- S15
S15 -- S16
S16 -- S17
S17 -- S18
S18 -- S19
S19 -- S20
S20 -- S21
S21 -- S22
S22 -- S23
S23 -- S24
S24 -- S25
S25 -- S26
S26 -- S27
S27 -- S28
S28 -- S29
S29 -- S30
S30 -- S31
S31 -- S32
S32 -- S33
S33 -- S34
S34 -- S35
S35 -- S36
S36 -- R2
R1  -- R2 cost=200 down
</code></pre></div>

<p>Une fois la <a href="#mstp:40">topologie stabilisée</a>, une partie du réseau considère R1
comme racine et l’autre partie vote pour R2. À la frontière, S20 tente de
démarrer une synchronisation avec S21 pour faire passer son <em>port désigné</em> à
l’état de transmission. Sa <abbr title="Bridge Protocol Data Unit">BPDU</abbr> ressemble à ceci :</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x4e, Agreement, Port Role: Designated, Proposal
<span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:00
<span class="ss">    Root Path Cost</span>: 400000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:15
<span class="ss">    Port identifier</span>: 0x8002
<span class="hll"><span class="ss">    Message Age</span>: 20
</span><span class="hll"><span class="ss">    Max Age</span>: 20
</span></code></pre></div>

<p>S21 la rejette car l’âge du message est égal à l’âge maximal. De son côté, la
<abbr title="Bridge Protocol Data Unit">BPDU</abbr> que S21 envoie à S20 ressemble à ceci :</p>
<div class="language-wireshark codehilite"><pre><span/><code><span class="nc">Spanning Tree Protocol</span>
<span class="ss">    Protocol Identifier</span>: Spanning Tree Protocol <span class="p">(</span><span class="m">0x0000</span><span class="p">)</span>
<span class="ss">    Protocol Version Identifier</span>: Rapid Spanning Tree <span class="p">(</span><span class="m">2</span><span class="p">)</span>
<span class="ss">    BPDU Type</span>: Rapid/Multiple Spanning Tree <span class="p">(</span><span class="m">0x02</span><span class="p">)</span>
<span class="ss">    BPDU flags</span>: 0x7c, Agreement, Forwarding, Learning, Port Role: Designated
<span class="hll"><span class="ss">    Root Identifier</span>: 4096.02:00:00:00:00:01
</span><span class="ss">    Root Path Cost</span>: 320000
<span class="ss">    Bridge Identifier</span>: 32768.02:00:00:00:00:16
<span class="ss">    Port identifier</span>: 0x8001
<span class="hll"><span class="ss">    Message Age</span>: 16
</span><span class="hll"><span class="ss">    Max Age</span>: 20
</span></code></pre></div>

<p>Cela ne suffit pas à changer le <em>port racine</em> de S20, car S20 dispose d’un
<em>identifiant de racine</em> plus faible : <code>4096.02:00:00:00:00:00</code> contre
<code>4096.02:00:00:00:00:01</code>.</p>
<p><a href="#mstp:40,R1--R2,...">Réparer le lien entre R1 et R2</a> résout le problème. L’âge
de message maximal transporté par un paquet est désormais de 18, en dessous de
l’âge maximal configuré. Mais cela ne fonctionne que jusqu’à la rupture d’un
autre lien. Une correction possible consiste à porter l’âge maximal à
40<sup id="fnref:forward-delay"><a class="footnote-ref" href="#fn:forward-delay">17</a></sup>.</p>
<h2 id="rstp-est-il-rapide"><abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> est-il rapide ?</h2>
<p><abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> converge en général en quelques secondes au démarrage. Il répare souvent un
arbre en moins d’une seconde. Même la <a href="/fr/blog/2026-spanning-tree#quelle-taille-pour-un-reseau">topologie à 38 ponts</a>
converge en moins de 10 secondes<sup id="fnref:time"><a class="footnote-ref" href="#fn:time">18</a></sup>. Certaines topologies mettent un peu
plus de temps à se rétablir quand la racine devient indisponible<sup id="fnref:slow"><a class="footnote-ref" href="#fn:slow">19</a></sup>.</p>
<div class="language-text-only mstp-topology codehilite"><pre><span/><code>:protocol rstp

R0 @1,0 prio=0
S1 @1,1 prio=4096
S2 @0,2 prio=8192
S3 @2,2

R0 -- S1
S1 -- S2
S2 -- S3
S3 -- S1
</code></pre></div>

<p>Dans la topologie ci-dessus, lancez la simulation, <a href="#mstp:10">attendez la
convergence</a>, arrêtez-la, puis <a href="#mstp:10,R0--S1">coupez le lien entre R0 et
S1</a>. La topologie est déjà optimale, mais <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> peine à
converger de nouveau.</p>
<p>D’abord, S1 perd son port racine. Il n’a plus aucune information sur R0 et se
proclame racine. Il conserve ses ports vers S2 et S3 comme ports désignés à
l’état de transmission. <a href="#mstp:10,R0--S1,1,S1-&gt;S2,S1-&gt;S3,@">Avancez d’un cran</a> :
il envoie une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> à S2 et à S3 pour les informer du changement de racine. À sa
réception, S2 accepte S1 comme racine, car il ne connaît pas de meilleure racine
sur un autre port. Il élit le port vers S1 comme port racine. L’autre port reste
un port désigné. Aucun des deux ports ne change d’état.</p>
<p>À la réception de la <abbr title="Bridge Protocol Data Unit">BPDU</abbr> de S1, S3 se comporte différemment : il connaît R0
comme une meilleure racine que S1 grâce à son port alternatif vers S2. Il
promeut ce port en port racine et rétrograde le port vers S1 en port désigné, ce
qui nécessite un nouvel accord. <a href="#mstp:10,R0--S1,2,S3-&gt;S1#1,@">Avancez d’un
cran</a> : S3 envoie une proposition à S1 avec R0
comme racine. S1 élit R0 comme racine et promeut son port vers S3 en port
racine.</p>
<p>Lors de la <a href="#mstp:10,R0--S1,2,S2-&gt;S3,@">même séquence</a>, S3 reçoit aussi une <abbr title="Bridge Protocol Data Unit">BPDU</abbr>
de S2 affirmant que S1 est la racine. S3 n’a donc plus aucun port annonçant R0
comme racine : il élit S1 comme racine et son port vers S2 comme port racine.
<a href="#mstp:10,R0--S1,3,S3-&gt;S1,@">Avancez d’un cran</a> : sa <abbr title="Bridge Protocol Data Unit">BPDU</abbr> suivante vers S1
contient cette information et S1 s’élit de nouveau racine. Mais lors de la <a href="#mstp:10,R0--S1,3,S1-&gt;S2#1,@">même
vague</a>, S1 envoie une proposition à S2 avec R0
comme racine. Alors que S1 et S3 s’accordent sur le fait que S1 est la racine,
S2 croit désormais que c’est R0 ! <a href="#mstp:10,R0--S1,4,@,...">À son tour</a>, S2
convainc de nouveau S3 que R0 est la racine, S3 convainc S1, S1 convainc S2 et
S2 convainc S3.</p>
<p>Cela pourrait durer indéfiniment, mais ce n’est pas le cas. Les <abbr title="Bridge Protocol Data Unit">BPDU</abbr> affirmant « R0 est la racine » finissent par se périmer lorsque l’âge du message dépasse
l’âge maximal. Dans l’exemple ci-dessus, à la <a href="#mstp:10,R0--S1,24,S2-&gt;S3,@">onzième
seconde</a>, S2 envoie une <abbr title="Bridge Protocol Data Unit">BPDU</abbr> à S3 avec R0 comme
racine, mais S3 la jette car elle a atteint l’âge maximal. Avec un peu de
chance, la convergence peut aussi être plus rapide si un port cesse de
transmettre des <abbr title="Bridge Protocol Data Unit">BPDU</abbr> après avoir atteint le nombre maximal autorisé par
seconde : il s’agit du <em>transmit hold count</em>, dont la valeur par défaut est 6.</p>
<h1 id="a-propos-de-mstp">À propos de <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr></h1>
<p><abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> est la version « compatible VLAN » de <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> : il exécute plusieurs instances
de <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> et permet d’associer chaque VLAN à une instance donnée. Par exemple,
vous pouvez rattacher les VLAN 100 à 200 à une première instance et les VLAN 300
à 400 à une seconde. Les VLAN restants sont rattachés à une instance spéciale
appelée <em>Internal Spanning Tree</em> (<abbr title="Internal Spanning Tree">IST</abbr>). <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> apporte sa propre complexité, mais
l’idée est de disposer de plusieurs topologies logiques indépendantes. Pour
creuser le sujet, jetez un œil à « <a href="https://ine.com/blog/2008-07-27-mstp-tutorial-part-i-inside-a-region" title="MSTP Tutorial Part I: Inside a Region"><abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> Tutorial Part I: Inside a Region</a> ».</p>
<h1 id="a-propos-des-exemples-interactifs">À propos des exemples interactifs</h1>
<p>Les exemples interactifs exécutent <a href="https://github.com/mstpd/mstpd" title="Multiple Spanning Tree Protocol Daemon">MSTPD</a> directement dans votre navigateur,
compilé en <a href="https://developer.mozilla.org/en-US/docs/WebAssembly" title="WebAssembly on MDN">WebAssembly</a> avec <a href="https://emscripten.org/" title="Emscripten documentation">emscripten</a>. Une API C remplace le code qui
dialogue avec le noyau Linux : elle gère les ponts et les ports, exporte l’état
en JSON et fait avancer le temps de manière déterministe. Une surcouche
JavaScript la rend plus agréable à utiliser :</p>
<div class="language-javascript codehilite"><pre><span/><code><span class="k">import</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">loadMSTPD</span><span class="w"> </span><span class="p">}</span><span class="w"> </span><span class="kr">from</span><span class="w"> </span><span class="s2">"./dist/mstpd.mjs"</span><span class="p">;</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">mstp</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="k">await</span><span class="w"> </span><span class="nx">loadMSTPD</span><span class="p">();</span>

<span class="c1">// Crée 3 ponts</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">a</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">createBridge</span><span class="p">(</span><span class="s2">"A"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">priority</span><span class="o">:</span><span class="w"> </span><span class="mf">4096</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">b</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">createBridge</span><span class="p">(</span><span class="s2">"B"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">priority</span><span class="o">:</span><span class="w"> </span><span class="mf">8192</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">createBridge</span><span class="p">(</span><span class="s2">"C"</span><span class="p">);</span>

<span class="c1">// Chaque pont a deux ports</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">a1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">a</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"a-b"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">a2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">a</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"a-c"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">b1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"b-a"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">b2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">b</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"b-c"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">c1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"c-a"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">1</span><span class="w"> </span><span class="p">});</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">c2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">c</span><span class="p">.</span><span class="nx">addPort</span><span class="p">(</span><span class="s2">"c-b"</span><span class="p">,</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">portno</span><span class="o">:</span><span class="w"> </span><span class="mf">2</span><span class="w"> </span><span class="p">});</span>

<span class="c1">// Construit une topologie en triangle</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">link</span><span class="p">(</span><span class="nx">a1</span><span class="p">,</span><span class="w"> </span><span class="nx">b1</span><span class="p">);</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">link</span><span class="p">(</span><span class="nx">a2</span><span class="p">,</span><span class="w"> </span><span class="nx">c1</span><span class="p">);</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">link</span><span class="p">(</span><span class="nx">b2</span><span class="p">,</span><span class="w"> </span><span class="nx">c2</span><span class="p">);</span>

<span class="c1">// Active tous les ponts et tous les ports</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">br</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">[</span><span class="nx">a</span><span class="p">,</span><span class="w"> </span><span class="nx">b</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="p">])</span><span class="w"> </span><span class="nx">br</span><span class="p">.</span><span class="nx">enable</span><span class="p">();</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="kd">const</span><span class="w"> </span><span class="nx">p</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="p">[</span><span class="nx">a1</span><span class="p">,</span><span class="w"> </span><span class="nx">a2</span><span class="p">,</span><span class="w"> </span><span class="nx">b1</span><span class="p">,</span><span class="w"> </span><span class="nx">b2</span><span class="p">,</span><span class="w"> </span><span class="nx">c1</span><span class="p">,</span><span class="w"> </span><span class="nx">c2</span><span class="p">])</span><span class="w"> </span><span class="nx">p</span><span class="p">.</span><span class="nx">enable</span><span class="p">();</span>

<span class="c1">// Exécute 40 secondes de temps réel et affiche la topologie</span>
<span class="nx">mstp</span><span class="p">.</span><span class="nx">step</span><span class="p">(</span><span class="mf">40</span><span class="p">);</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="s2">"Topology:"</span><span class="p">,</span><span class="w"> </span><span class="nx">mstp</span><span class="p">.</span><span class="nx">topology</span><span class="p">());</span>
</code></pre></div>

<p>Plusieurs dizaines de tests unitaires explorent les fonctionnalités de MSTPD et
vérifient qu’elles se comportent correctement dans cet environnement :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>node<span class="w"> </span>--test<span class="w"> </span>*.test.mjs
<span class="go">✔ two bridges: lower priority becomes root (41.657342ms)</span>
<span class="go">✔ triangle loop: exactly one port blocks and all agree on the root (5.832ms)</span>
<span class="go">✔ breaking the active link reconverges and restoring recovers (18.730753ms)</span>
<span class="go">[…]</span>
<span class="go">ℹ tests 40</span>
<span class="go">ℹ pass 40</span>
<span class="go">ℹ fail 0</span>
<span class="go">[…]</span>
<span class="go">ℹ duration_ms 396.190897</span>
</code></pre></div>

<p>Du code JavaScript supplémentaire recherche les blocs <code>&lt;pre&gt;</code> contenant une
définition de topologie et les transforme en composants interactifs. Vous pouvez
inspecter et modifier la définition en cliquant sur le bouton « <em>edit</em> ».</p>
<p>Il y a aussi une astuce pour déterminer si la topologie a convergé. Après chaque
pas, nous enregistrons un instantané de la mémoire de la simulation, jouons 50
secondes en accéléré pour vérifier que la topologie est stable, puis remontons
le temps en restaurant cet instantané. 🕰️</p>
<p>Le code complet se trouve sur <a href="https://github.com/vincentbernat/mstpd/tree/feature/wasm/wasm">GitHub</a>. Je suis très satisfait du résultat. Il
peut être difficile de suivre tout ce qui se passe à chaque étape, mais la
possibilité d’avancer et reculer aide beaucoup. Je compte réutiliser cette
approche dans de prochains articles.</p>
<div class="admonition">
<p class="admonition-title">Note</p>
<p><a href="https://mtlynch.io/">Michael Lynch</a> a relu une première version de la version anglaise
de cet article. Il est l’auteur de « <a href="https://refactoringenglish.com/" title="Refactoring English: Effective Writing for Software Developers">Refactoring English</a> », un livre pour
améliorer votre écriture en anglais : articles de blog, documentation, messages
de commit et tutoriels. Les erreurs restantes sont les miennes !</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2026)</p>
<p>Cet article a été brièvement discuté sur
<a href="https://news.ycombinator.com/item?id=49610596">Hacker News</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:sprites">
<p>Les sprites de Gaston et Nono proviennent de <a href="https://craftpix.net/" title="Craftpix: 2D game assets">Craftpix</a>. La
tasse de café est dessinée par <a href="https://yaninyunus.itch.io/16x16-specialtycoffee" title="Yanin: 16x16 specialty coffee assets">Yanin</a>. <a class="footnote-backref" href="#fnref:sprites" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:history">
<p><abbr title="Spanning Tree Protocol">STP</abbr> a été introduit dans <a href="https://sci-hub.fr/10.1109/IEEESTD.1991.101050" title="IEEE Standards for Local and Metropolitan Area Networks: Media Access Control (MAC) Bridges">IEEE 802.1D-1990</a>. Il est encore présent
dans <a href="https://sci-hub.fr/10.1109/IEEESTD.1998.95619" title="IEEE Standard for Local and Metropolitan Area Networks: Media Access Control (MAC) Bridges">IEEE 802.1D-1998</a>, mais il a été retiré d’<a href="https://sci-hub.fr/10.1109/IEEESTD.2004.94569" title="IEEE Standard for Local and Metropolitan Area Networks: Media Access Control (MAC) Bridges">IEEE 802.1D-2004</a> au
profit de <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>, introduit dans <a href="https://sci-hub.fr/10.1109/IEEESTD.2001.93287" title="IEEE Standard for Local and Metropolitan Area Networks: Rapid Reconfiguration of Spanning Tree">IEEE 802.1w-2001</a>. <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr> est apparu dans
<a href="https://sci-hub.fr/10.1109/IEEESTD.2002.94223" title="IEEE Standards for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks — Amendment: Multiple Spanning Trees">IEEE 802.1s-2002</a> avant d’être intégré à <a href="https://sci-hub.fr/10.1109/IEEESTD.2003.94280" title="IEEE Standards for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks">IEEE 802.1Q-2003</a>. Tous deux
font partie d’<a href="ahttps://oadoi.org/10.1109/IEEESTD.2022.10004498" title="IEEE Standard for Local and Metropolitan Area Networks: Bridges and Bridged Networks">IEEE 802.1Q-2022</a>, aux côtés de <a href="https://sci-hub.fr/10.1109/IEEESTD.2012.6231597" title="IEEE Standard for Local and Metropolitan Area Networks: Shortest Path Bridging"><abbr title="Shortest Path Bridging">SPB</abbr></a>,
<del>un protocole dont je n’avais jamais entendu parler avant d’écrire cet
article</del>. <a class="footnote-backref" href="#fnref:history" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:bridge">
<p>À partir d’ici, j’emploie le terme « pont » plutôt que le mot plus
courant « commutateur ». <a class="footnote-backref" href="#fnref:bridge" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:incomplete">
<p>MSTPD implémente la machine à états d’<a href="https://sci-hub.fr/10.1109/IEEESTD.2006.216285" title="IEEE Standard for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks">IEEE 802.1Q-2005</a>, mais
sous Linux, il ne fait tourner que <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr>. Linux 5.18 a ajouté la <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=ec7328b59176227216c461601c6bd0e922232a9b" title="net: bridge: mst: Multiple Spanning Tree (MST) mode">prise en
charge de la commutation pour plusieurs arbres recouvrants</a>, mais MSTPD ne l’utilise pas encore.
Consultez la <a href="https://github.com/mstpd/mstpd/pull/150" title="[RFC/RFT] use kernel MST support if available">PR #150</a> pour suivre les avancées sur ce point. <a class="footnote-backref" href="#fnref:incomplete" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:kernel">
<p>Le noyau Linux ne gère que <abbr title="Spanning Tree Protocol">STP</abbr>. Il délègue les autres protocoles à
l’espace utilisateur. <a class="footnote-backref" href="#fnref:kernel" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:priority">
<p>La priorité est un multiple de 4 096 : avec <abbr title="Multiple Spanning Tree Protocol">MSTP</abbr>, les 12 bits de
poids faible de la priorité du pont encodent l’identifiant de l’instance
<abbr title="Multiple Spanning Tree">MST</abbr>, ne laissant que les 4 bits de poids fort pour la priorité configurée. <a class="footnote-backref" href="#fnref:priority" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:wireshark">
<p>Pour inspecter les <abbr title="Bridge Protocol Data Unit">BPDU</abbr> qui circulent sur un lien, sélectionnez-le,
cliquez sur le bouton « <em>Download packets</em> », puis ouvrez le fichier avec
<a href="https://www.wireshark.org/" title="Wireshark: network protocol analyzer">Wireshark</a>. <a class="footnote-backref" href="#fnref:wireshark" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:backup">
<p>Un port de secours n’existe que si le pont possède plusieurs ports
sur le même domaine de collision. Cela ne devrait pas se produire dans un
réseau commuté. <a class="footnote-backref" href="#fnref:backup" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:hello">
<p>C’est la valeur du temporisateur « <em>hello</em> ». Elle était autrefois
configurable, mais <a href="https://sci-hub.fr/10.1109/IEEESTD.2006.216285" title="IEEE Standard for Local and Metropolitan Area Networks: Virtual Bridged Local Area Networks">IEEE 802.1Q-2005</a> la fixe à 2. MSTPD n’autorise pas
d’autre valeur. <a class="footnote-backref" href="#fnref:hello" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:learning">
<p>Si un port ne reçoit pas d’accord à l’expiration du temporisateur
« <em>hello</em> » (ou de l’âge maximal si le port vient tout juste d’être activé),
il se rabat sur la méthode à base de temporisateurs, par compatibilité avec
<abbr title="Spanning Tree Protocol">STP</abbr> : il passe à l’état d’apprentissage, attend de nouveau l’expiration du
temporisateur « <em>hello</em> », puis passe à l’état de transmission. <a class="footnote-backref" href="#fnref:learning" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:agreement">
<p>Comme dans beaucoup de propositions, S3 positionne aussi le bit
d’accord à 1. Le bit de proposition signifie « je suis le port désigné sur
ce lien et je veux passer à l’état de transmission ». Le bit d’accord
signifie « je suis déjà synchronisé avec le reste de mon pont sur cette
information de racine ». Les deux peuvent être vrais en même temps. <a class="footnote-backref" href="#fnref:agreement" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
<li id="fn:bum">
<p>S’il ne trouve aucune entrée, le pont duplique la trame Ethernet sur
tous les ports, sauf celui d’entrée. Il en va de même si l’adresse MAC de
destination est l’adresse de diffusion (<code>ff:ff:ff:ff:ff:ff</code>). Ce
comportement amorce le processus d’apprentissage. <a class="footnote-backref" href="#fnref:bum" title="Jump back to footnote 12 in the text">↩</a></p>
</li>
<li id="fn:timer">
<p>Ce temporisateur rend <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> résistant à la perte de paquets. <a class="footnote-backref" href="#fnref:timer" title="Jump back to footnote 13 in the text">↩</a></p>
</li>
<li id="fn:price">
<p>Vous pouvez en trouver d’occasion pour moins de 100 €. Tous les ports
utilisent <abbr title="Per-VLAN Spanning Tree">PVST</abbr>+ par défaut et basculent automatiquement vers <abbr title="Rapid Spanning Tree Protocol">RSTP</abbr> classique. <a class="footnote-backref" href="#fnref:price" title="Jump back to footnote 14 in the text">↩</a></p>
</li>
<li id="fn:erps">
<p>Une solution de rechange serait l’<a href="https://www.itu.int/rec/T-REC-G.8032" title="ITU-T G.8032">Ethernet Ring Protection
Switching</a> (<abbr title="Ethernet Ring Protection Switching">ERPS</abbr>), un autre protocole dont je n’avais jamais
entendu parler avant de me documenter pour cet article. <a class="footnote-backref" href="#fnref:erps" title="Jump back to footnote 15 in the text">↩</a></p>
</li>
<li id="fn:root-mac">
<p>Si vous observez attentivement ce qui se passe à t=2s, vous verrez
que R2 gagne en popularité comme racine : S17 à S36 croient que R2 est la
racine. S16 ne suit pas car nous atteignons l’âge maximal. Plus tard, S17 à
S20 changent d’avis. Je vous laisse explorer l’état des différents ponts
pour en comprendre la cause. <a class="footnote-backref" href="#fnref:root-mac" title="Jump back to footnote 16 in the text">↩</a></p>
</li>
<li id="fn:forward-delay">
<p>En portant l’âge maximal à 40, vous devez aussi augmenter le
délai de transmission à 21 (<code>:forward-delay 21</code>), car le standard impose la
condition suivante : 2 × (<em>Forward Delay</em> − 1) ≥ <em>Max Age</em>. Pour cette
topologie précise, vous pourriez aussi porter l’âge maximal à 37 et le délai
de transmission à 20. <a class="footnote-backref" href="#fnref:forward-delay" title="Jump back to footnote 17 in the text">↩</a></p>
</li>
<li id="fn:time">
<p>La simulation peut sembler lente, mais elle ne tourne pas en temps
réel. Regardez l’horodatage dans le coin supérieur droit pour connaître le
temps écoulé, par exemple « t=8s ». Une fois la topologie stabilisée, ce
même coin affiche le temps de convergence, par exemple « 🌳 2s ». <a class="footnote-backref" href="#fnref:time" title="Jump back to footnote 18 in the text">↩</a></p>
</li>
<li id="fn:slow">
<p>Khaled Elmeleegy, Alan Cox et Eugene Ng ont formalisé ce phénomène dans
« <a href="https://sci-hub.fr/10.1109/INFOCOM.2006.229" title="On Count-to-Infinity Induced Forwarding Loops in Ethernet Networks">On Count-to-Infinity Induced Forwarding Loops in Ethernet Networks</a> »,
puis dans « <a href="https://sci-hub.fr/10.1109/TNET.2008.920874" title="Understanding and Mitigating the Effects of Count to Infinity in Ethernet Networks">Understanding and Mitigating the Effects of Count to Infinity
in Ethernet Networks</a> ». Ils proposent une correction qui n’a jamais
trouvé sa place dans un standard. <a class="footnote-backref" href="#fnref:slow" title="Jump back to footnote 19 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Partitionner une table de routage pour des lectures sans verrou en Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-akvorado-rib-sharding" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-akvorado-rib-sharding#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-05-24T19:00:00Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-akvorado-rib-sharding.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Pour associer des informations de routage, telles que les chemins d’AS ou les
communautés <abbr title="Border Gateway Protocol">BGP</abbr>, aux flux, <a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> peut importer des routes via le <a href="https://www.rfc-editor.org/rfc/rfc7854" title="RFC 7854: BGP Monitoring Protocol (BMP)"><abbr title="Border Gateway Protocol">BGP</abbr>
Monitoring Protocol</a> (<abbr title="BGP Monitoring Protocol">BMP</abbr>). Comme la table de routage Internet contient plus
d’<a href="https://bgp.potaroo.net" title="BGP Routing Table Analysis Reports">un million de routes</a>, Akvorado doit <strong>gérer des dizaines
de millions de routes</strong><sup id="fnref:optimize"><a class="footnote-ref" href="#fn:optimize">1</a></sup>. C’est un défi de longue date<sup id="fnref:past"><a class="footnote-ref" href="#fn:past">2</a></sup>, mais je
pense que ce problème est désormais résolu grâce au <strong>sharding de la <abbr title="Routing Information Base">RIB</abbr></strong>, une
méthode qui découpe la base de routage en plusieurs parties afin de permettre
les mises à jour concurrentes.</p>
<div class="toc">
<ul>
<li><a href="#implementation-precedente">Implémentation précédente</a><ul>
<li><a href="#stocker-les-routes-dans-une-table-associative">Stocker les routes dans une table associative</a></li>
<li><a href="#canonisation-des-routes">Canonisation des routes</a></li>
<li><a href="#quest-ce-qui-pose-probleme">Qu’est-ce qui pose problème ?</a></li>
</ul>
</li>
<li><a href="#sharding-de-la-rib">Sharding de la RIB</a><ul>
<li><a href="#premiere-etape">Première étape</a></li>
<li><a href="#deuxieme-etape">Deuxième étape</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="implementation-precedente">Implémentation précédente</h1>
<p>Akvorado associe deux éléments pour construire sa <abbr title="Routing Information Base">RIB</abbr> :</p>
<ol>
<li>un <strong>arbre de préfixes</strong> et</li>
<li>une <strong>liste de routes</strong> attachée à chaque préfixe.</li>
</ol>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 711px"><span class="lf-media-inner" style="padding-bottom: 105.626%"><img alt="Implémentation de la RIB BMP d'Akvorado avant le sharding, avec la disposition&#10;mémoire de chaque structure et un unique verrou." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-before.b60946a339a187.svg" width="711" height="751" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado sans sharding. Un unique verrou en lecture/écriture.</figcaption></figure>
<p>Dans le diagramme ci-dessus, la <abbr title="Routing Information Base">RIB</abbr> stocke cinq préfixes IPv4 et deux préfixes
IPv6. L’un d’eux, <code>2001:db8:1::/48</code>, contient trois routes :</p>
<ul>
<li>depuis le pair 3, via <code>2001:db8::3:1</code>, AS 65402, chemin d’AS <code>65402</code>, communauté
  <code>65402:31</code> ;</li>
<li>depuis le pair 4, via <code>2001:db8::4:1</code>, mêmes <abbr title="Autonomous System Number">ASN</abbr>, chemin d’AS et communauté ;</li>
<li>depuis le pair 5, via <code>2001:db8::5:1</code>, AS 65402, chemin d’AS <code>65401 65402</code>,
  communauté <code>65402:31</code>.</li>
</ul>
<p>La structure <code>rib</code> est définie en Go de la manière suivante :</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">          </span><span class="o">*</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixIndex</span><span class="p">]</span>
<span class="w">    </span><span class="nx">routes</span><span class="w">        </span><span class="kt">map</span><span class="p">[</span><span class="nx">routeKey</span><span class="p">]</span><span class="nx">route</span>
<span class="w">    </span><span class="nx">nlris</span><span class="w">         </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHops</span><span class="w">      </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">rtas</span><span class="w">          </span><span class="o">*</span><span class="nx">intern</span><span class="p">.</span><span class="nx">Pool</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextPrefixID</span><span class="w">  </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">freePrefixIDs</span><span class="w"> </span><span class="p">[]</span><span class="nx">prefixIndex</span>
<span class="p">}</span>
</code></pre></div>

<p>L’arbre de préfixes s’appuie sur le paquet <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a>, une adaptation de
l’<a href="https://www.hariguchi.org/art/art.pdf" title="ART: A Fast Free Multibit Trie Based Routing Table">algorithme ART</a> de Donald Knuth. Les <a href="https://github.com/gaissmai/iprbench">benchmarks</a> montrent
qu’il surpasse les autres implémentations pour les recherches, les insertions et
la consommation mémoire<sup id="fnref:performance"><a class="footnote-ref" href="#fn:performance">3</a></sup>. De plus, l’auteur est d’une grande aide.</p>
<h2 id="stocker-les-routes-dans-une-table-associative">Stocker les routes dans une table associative</h2>
<p>La liste des routes de chaque préfixe n’est pas stockée directement dans
l’arbre de préfixes : cela mettrait trop de pression sur le ramasse-miettes en
allouant un tableau par préfixe.</p>
<p>À la place, la <abbr title="Routing Information Base">RIB</abbr> attribue un identifiant de 32 bits unique à chaque préfixe,
soit en prenant le dernier identifiant disponible dans le tableau
<code>freePrefixIDs</code>, soit en utilisant la valeur <code>nextPrefixID</code> avant de
l’incrémenter. Les routes sont ensuite stockées dans la table associative
<code>routes</code>, qui s’appuie en Go sur une <a href="https://go.dev/blog/swisstable" title="Faster Go maps with Swiss Tables">structure particulièrement
optimisée</a>. Pour récupérer les routes attachées à un préfixe, nous les
cherchons un par un dans la table <code>routes</code> via une clef de 64 bits qui combine
l’index de 32 bits du préfixe avec l’index de 32 bits représentant la position
de la route dans la liste. Akvorado parcourt les routes de la première à la
dernière pour trouver la meilleure<sup id="fnref:scan"><a class="footnote-ref" href="#fn:scan">4</a></sup>. Il sait qu’il n’y a plus de route
lorsque la clé ne renvoie aucun résultat.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">prefixIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeIndex</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeKey</span><span class="w"> </span><span class="kt">uint64</span>
</code></pre></div>

<h2 id="canonisation-des-routes">Canonisation des routes</h2>
<p>Une route contient un identifiant de pair <abbr title="Border Gateway Protocol">BGP</abbr>, un <abbr title="Network Layer Reachability Information">NLRI</abbr> partiel<sup id="fnref:nlri"><a class="footnote-ref" href="#fn:nlri">5</a></sup>, le prochain
saut et les attributs.</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">route</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">peer</span><span class="w">       </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">nlri</span><span class="w">       </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nlri</span><span class="p">]</span>
<span class="w">    </span><span class="nx">nextHop</span><span class="w">    </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">nextHop</span><span class="p">]</span>
<span class="w">    </span><span class="nx">attributes</span><span class="w"> </span><span class="nx">intern</span><span class="p">.</span><span class="nx">Reference</span><span class="p">[</span><span class="nx">routeAttributes</span><span class="p">]</span>
<span class="w">    </span><span class="nx">prefixLen</span><span class="w">  </span><span class="kt">uint8</span>
<span class="p">}</span>

<span class="kd">type</span><span class="w"> </span><span class="nx">nlri</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">family</span><span class="w"> </span><span class="nx">bgp</span><span class="p">.</span><span class="nx">Family</span>
<span class="w">    </span><span class="nx">path</span><span class="w">   </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">rd</span><span class="w">     </span><span class="nx">RD</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">nextHop</span><span class="w"> </span><span class="nx">netip</span><span class="p">.</span><span class="nx">Addr</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">routeAttributes</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">asn</span><span class="w">              </span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">asPath</span><span class="w">           </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">communities</span><span class="w">      </span><span class="p">[]</span><span class="kt">uint32</span>
<span class="w">    </span><span class="nx">largeCommunities</span><span class="w"> </span><span class="p">[]</span><span class="nx">bgp</span><span class="p">.</span><span class="nx">LargeCommunity</span>
<span class="p">}</span>
</code></pre></div>

<p>Pour économiser la mémoire et les allocations, les <abbr title="Network Layer Reachability Information">NLRI</abbr>, les prochains sauts et
les attributs de route sont canonisés : un entier de 32 bits remplace la valeur
réelle. Ce mécanisme est antérieur au <a href="https://go.dev/blog/unique" title="New unique package">paquet <code>unique</code></a> introduit dans
Go 1.23. Nous le conservons car ses compromis sont différents :</p>
<ul>
<li>il utilise un <strong>comptage de références explicite</strong> plutôt que des pointeurs
  faibles ;</li>
<li>il fonctionne avec des <strong>valeurs non comparables</strong> implémentant les méthodes
  <code>Hash()</code> et <code>Equal()</code><sup id="fnref:hash"><a class="footnote-ref" href="#fn:hash">6</a></sup> ;</li>
<li>il utilise des <strong>instances explicites pour la canonisation</strong>, ce qui se révèlera utile pour
  le sharding ;</li>
<li>il offre de <strong>meilleures performances</strong>. Voir par exemple ce <a href="https://github.com/akvorado/akvorado/pull/2244/changes/682b6063af50780dcd64e46b39d5e66c5074d9ab" title="outlet/routing: use Go's unique instead of intern">benchmark</a> ;</li>
<li>il consomme <strong>deux fois moins de mémoire</strong> grâce à des références 32 bits
  plutôt que des pointeurs ;</li>
<li>mais il <strong>n’est pas utilisable de manière concurrente</strong>.</li>
</ul>
<h2 id="quest-ce-qui-pose-probleme">Qu’est-ce qui pose problème ?</h2>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Pour l’<a href="https://www.free.fr/freebox">AS 12322</a>, nous n’utilisons pas encore <abbr title="BGP Monitoring Protocol">BMP</abbr><sup id="fnref:cisco"><a class="footnote-ref" href="#fn:cisco">7</a></sup>. Mais
<a href="https://github.com/bogi788" title="bogi788 on GitHub">Gerhard Bogner</a> a eu la patience, la disponibilité et les compétences
techniques pour m’aider à <a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">déboguer ce problème</a>.</p>
</div>
<p>Le verrou global en lecture/écriture est le goulot d’étranglement de cette
implémentation. Mais pourquoi ? Plusieurs composants utilisent la <abbr title="Routing Information Base">RIB</abbr>, chacun
avec ses propres contraintes :</p>
<ul>
<li>
<p>Les <strong>consommateurs Kafka</strong> consultent la <abbr title="Routing Information Base">RIB</abbr> pour enrichir les flux avec des
  informations de routage. Leur nombre est bornée par la somme des partitions
  Kafka<sup id="fnref:KIP-932"><a class="footnote-ref" href="#fn:KIP-932">8</a></sup>. Akvorado ajuste également ce nombre afin de garantir un
  export groupé efficace des flux vers ClickHouse. Sur notre installation, la
  quantité de consommateurs oscille entre 8 et 16. Comme nous voulons observer les
  données récentes le plus tôt possible, nous ne pouvons pas nous permettre que
  les consommateurs Kafka prennent trop de retard.</p>
</li>
<li>
<p>Les <strong>routeurs surveillés</strong> envoient leurs routes via le protocole <abbr title="BGP Monitoring Protocol">BMP</abbr>. À la
  connexion, ils peuvent envoyer des millions de routes<sup id="fnref:bmpconfig"><a class="footnote-ref" href="#fn:bmpconfig">9</a></sup>. Après la
  synchronisation initiale, les mises à jour arrivent en continu et peuvent
  connaître des pics ponctuels. Le routeur détecte une station <abbr title="BGP Monitoring Protocol">BMP</abbr> bloquée
  lorsque sa fenêtre TCP est pleine. Dans ce cas, il réinitialise la session.
  Bien qu’Akvorado dispose d’un important tampon en entrée, il doit appliquer
  les routes reçues suffisamment vite pour ne pas être considéré comme bloqué.</p>
</li>
<li>
<p>Quand un <strong>pair <abbr title="Border Gateway Protocol">BGP</abbr> distant tombe</strong>, Akvorado purge les routes associées en
  parcourant la <abbr title="Routing Information Base">RIB</abbr>. Quand un <strong>routeur surveillé tombe</strong>, Akvorado patiente un
  peu, mais finit par purger toutes les routes associées.</p>
</li>
</ul>
<p>En résumé : sur une installation chargée, la contention sur le verrou est
forte autant pour les lecteurs que pour les écrivains, et aucun des deux ne
peut prendre trop de retard.</p>
<h1 id="sharding-de-la-rib">Sharding de la <abbr title="Routing Information Base">RIB</abbr></h1>
<h2 id="premiere-etape">Première étape</h2>
<p>Pour supprimer le verrou global, la <abbr title="Routing Information Base">RIB</abbr> est découpée en plusieurs « shards »,
chacun gérant un sous-ensemble des préfixes :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Implémentation de la RIB BMP d'Akvorado après le sharding, avec la disposition&#10;mémoire de chaque structure et un verrou par partition." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-step1.71c5ecf8b46b57.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado avec sharding.</figcaption></figure>
<p>L’arbre de préfixes reste global et protégé par un unique verrou. Chaque
partition dispose de son propre verrou en lecture/écriture, de sa table de
routes et de ses blocs de canonisation pour stocker les <abbr title="Network Layer Reachability Information">NLRI</abbr>, les prochains
sauts et les attributs de route, ce qui n’aurait pas été possible avec le
<a href="https://go.dev/blog/unique" title="New unique package">paquet <code>unique</code> de Go</a>. Les index de préfixes sont eux aussi
partitionnés : les 8 bits de poids fort correspondent à l’index de la partition
et les 24 bits de poids faible à l’index local du préfixe.</p>
<p>Gerhard <a href="https://github.com/akvorado/akvorado/discussions/2287#discussioncomment-16020731">a confirmé</a> que suite à <a href="https://github.com/akvorado/akvorado/commit/7e6bbf2210fdf7116d2ee168b307b9906cc223c0" title="outlet/routing: implement RIB sharding for BMP">ce changement à
l’aveugle</a>, le composant <abbr title="BGP Monitoring Protocol">BMP</abbr> a commencé à ronronner. 🎉</p>
<p>J’ai ensuite écrit un <a href="https://github.com/akvorado/akvorado/blob/0811c40cc2065380e3a6230c2796312838e57850/outlet/routing/provider/bmp/concurrent_test.go">benchmark</a> portant sur un
demi-million de routes synthétiques mais plausibles<sup id="fnref:plausible"><a class="footnote-ref" href="#fn:plausible">10</a></sup>, réparties entre
0 et 8 écrivains qui modifient les routes aussi vite que possible, tandis que 1
à 16 lecteurs consultent en boucle un ensemble de 10 000 routes. J’ignore si ce
benchmark est réaliste, mais il confirme les améliorations à la fois sur les
latences en lecture et en écriture :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 970px"><span class="lf-media-inner" style="padding-bottom: 40.928%"><img alt="Deux cartes de chaleur. L'une pour le ratio de latence en lecture, l'autre&#10;pour celui de la latence en écriture. Toutes deux comparent l'accélération&#10;entre le code avant et après le sharding via des cases colorées. La plupart des&#10;cases sont vertes." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap.0ed40d587fea78.svg" width="970" height="397" class="lf-media"/></span></div><figcaption>Amélioration des performances de latence en lecture et en écriture après le sharding.</figcaption></figure>
<p>Il montre également qu’un grand nombre d’écrivains dégrade la latence en
lecture.</p>
<h2 id="deuxieme-etape">Deuxième étape</h2>
<p>L’unique verrou en lecture/écriture protégeant l’arbre de préfixes constitue la
cible suivante. Le paquet <a href="https://github.com/gaissmai/bart/" title="Balanced Routing Tables (BART) in Go">bart</a> propose des méthodes alternatives pour les
mutations. Elles renvoient un arbre mis à jour via une copie partielle. Les
lecteurs n’ont plus besoin du verrou global, qui ne sert plus qu’à synchroniser
les écrivains. L’arbre de préfixes est encapsulé dans un pointeur atomique.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 876px"><span class="lf-media-inner" style="padding-bottom: 72.603%"><img alt="Implémentation de la RIB BMP d'Akvorado pour le sharding avec lectures sans&#10;verrou, avec la disposition mémoire de chaque structure." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-step2.2996a7f0868135.svg" width="876" height="636" class="lf-media"/></span></div><figcaption>Implémentation de la RIB BMP d'Akvorado avec sharding et lectures sans verrou.</figcaption></figure>
<p>Sans verrou, un lecteur peut désormais récupérer un index de préfixe périmé en
parcourant sa copie de l’arbre, lorsqu’un écrivain concurrent supprime la dernière
route attachée à cet index et le recycle pour un autre préfixe. Pour éviter ce
problème, nous combinons l’index de préfixe avec un numéro de génération que
nous stockons dans l’arbre :</p>
<div class="language-go codehilite"><pre><span/><code><span class="kd">type</span><span class="w"> </span><span class="nx">generation</span><span class="w"> </span><span class="kt">uint32</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">prefixRef</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">idx</span><span class="w"> </span><span class="nx">prefixIndex</span>
<span class="w">    </span><span class="nx">gen</span><span class="w"> </span><span class="nx">generation</span>
<span class="p">}</span>
<span class="kd">type</span><span class="w"> </span><span class="nx">rib</span><span class="w"> </span><span class="kd">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">mu</span><span class="w">     </span><span class="nx">sync</span><span class="p">.</span><span class="nx">Mutex</span>
<span class="w">    </span><span class="nx">tree</span><span class="w">   </span><span class="nx">atomic</span><span class="p">.</span><span class="nx">Pointer</span><span class="p">[</span><span class="nx">bart</span><span class="p">.</span><span class="nx">Table</span><span class="p">[</span><span class="nx">prefixRef</span><span class="p">]]</span>
<span class="w">    </span><span class="nx">shards</span><span class="w"> </span><span class="p">[]</span><span class="o">*</span><span class="nx">ribShard</span>
<span class="p">}</span>
</code></pre></div>

<p>Chaque partition stocke le numéro de génération de chaque index de préfixe
local. Ce numéro est incrémenté quand l’index de préfixe associé est libéré.
Lors de la recherche des routes attachées à un index de préfixe, le lecteur
vérifie que le numéro de génération correspond. Sinon, il considère que l’index
a été recyclé et que la liste de routes est vide<sup id="fnref:retry"><a class="footnote-ref" href="#fn:retry">11</a></sup>. Le schéma précédent
illustre ce cas pour l’index de préfixe 5, stocké avec un numéro de génération
de 3, alors que la valeur actuelle dans le tableau <code>[]generations</code> est 4. Le
numéro de génération peut boucler, mais ce n’est pas un problème car les
recherches sont rapides.</p>
<p>L’exécution du benchmark sur cette nouvelle implémentation montre les
améliorations sur la latence en lecture ainsi qu’en écriture dès que le coût de
la copie partielle de l’arbre de préfixes est amorti.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1256px"><span class="lf-media-inner" style="padding-bottom: 46.815%"><img alt="Six cartes de chaleur. Trois pour le ratio de latence en lecture, trois&#10;autres pour celui de la latence en écriture. Elles comparent par paires les&#10;valeurs sans sharding, avec sharding, et avec lectures sans verrou. Pour la&#10;latence en lecture, la plupart des cases sont vertes, ce qui traduit&#10;l'amélioration apportée par la seconde étape. Pour la latence en écriture,&#10;l'accélération est négative pour un faible nombre de&#10;lecteurs." src="https://media-bernat-ch.b-cdn.net/images/akvorado/sharding-heatmap2.8bbce411e680da.svg" width="1256" height="588" class="lf-media"/></span></div><figcaption>Amélioration des performances de latence en lecture et en écriture après les lectures sans verrou. La colonne centrale montre les améliorations cumulées des deux étapes.</figcaption></figure>
<hr/>
<p>Parmi les nombreuses tentatives d’optimisation du composant <abbr title="BGP Monitoring Protocol">BMP</abbr>, le sharding
de la <abbr title="Routing Information Base">RIB</abbr> est l’une des plus satisfaisantes. <a href="https://github.com/akvorado/akvorado/releases/tag/v2.2.0">Akvorado 2.2</a> implémente la
première étape. La <a href="https://github.com/akvorado/akvorado/pull/2433" title="More efficient RIB tree for BMP">PR #2433</a>, rédigée pendant l’écriture de ce
billet, implémente la seconde et est sortie avec <a href="https://github.com/akvorado/akvorado/releases/tag/v2.4.0">Akvorado 2.4</a>. 🪓</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:optimize">
<p>Chaque routeur exportant des flux n’a pas besoin d’envoyer ses
routes. Quand Akvorado ne trouve pas de route pour un équipement donné, il
se rabat sur une route envoyée par un autre équipement. C’est à l’opérateur
de juger si cette approximation est acceptable. <a class="footnote-backref" href="#fnref:optimize" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:past">
<p>J’ai effectué de nombreuses tentatives pour augmenter l’efficacité du
composant <abbr title="BGP Monitoring Protocol">BMP</abbr>. Voir par exemple les <a href="https://github.com/akvorado/akvorado/pull/254" title="inlet/bmp: avoid long lock times when flushing peers">PR #254</a>, <a href="https://github.com/akvorado/akvorado/pull/255" title="“Lockless” RIB design">PR #255</a>, <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>,
<a href="https://github.com/akvorado/akvorado/pull/2244" title="outlet/routing: read from RIB without any locks">PR #2244</a> et <a href="https://github.com/akvorado/akvorado/pull/2245" title="outlet/routing: buffer BMP messages to avoid being flagged as “stuck”">PR #2245</a>. Malgré ces efforts, ce composant est resté
problématique pour certains utilisateurs. Voir la
<a href="https://github.com/akvorado/akvorado/discussions/2287" title="Using go's unique package">discussion #2287</a> comme exemple le plus récent. <a class="footnote-backref" href="#fnref:past" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:performance">
<p>Cela continue de s’améliorer : <a href="https://github.com/gaissmai/bart/releases/tag/v0.28.0" title="bart release v0.28.0: New FastNode implementation">bart 0.28.0</a> introduit une
implémentation alternative qui troque un peu de mémoire contre de meilleures
performances en recherche. Je ne l’ai pas encore essayée car cela fait déjà
quelques mois que je prépare ce billet. <a class="footnote-backref" href="#fnref:performance" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:scan">
<p>Akvorado préfère la route dont le prochain saut correspond exactement.
Sinon, il se rabat sur n’importe quelle autre route. C’est une
approximation. Une alternative consisterait à maintenir un arbre de
préfixes par pair <abbr title="Border Gateway Protocol">BGP</abbr>, mais il faudrait alors configurer tous les routeurs pour
qu’ils exportent leurs routes. Le démon <abbr title="BGP Monitoring Protocol">BMP</abbr> de <a href="http://www.pmacct.net/">pmacct</a> adopte cette
approche. <a class="footnote-backref" href="#fnref:scan" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:nlri">
<p>Si nous considérons la <abbr title="Routing Information Base">RIB</abbr> <abbr title="Border Gateway Protocol">BGP</abbr> comme une base de données, le <abbr title="Network Layer Reachability Information">NLRI</abbr>
(<em>Network Layer Reachability Information</em>) en constitue la clé primaire.
Son contenu dépend de la famille <abbr title="Border Gateway Protocol">BGP</abbr>. Pour IPv4 ou IPv6 unicast, il s’agit
du préfixe. Pour les familles VPNv4 et VPNv6, il inclut le <em>route
distinguisher</em>. Si l’extension <a href="https://www.rfc-editor.org/rfc/rfc7911" title="RFC 7911: Advertisement of Multiple Paths in BGP">ADD-PATH</a> est activée, le <abbr title="Network Layer Reachability Information">NLRI</abbr> contient
également un identifiant de chemin.</p>
<p>Dans notre implémentation, nous ne stockons pas le préfixe : nous le
reconstituons à partir de l’adresse IP recherchée et de la longueur de
préfixe stockée séparément. <a class="footnote-backref" href="#fnref:nlri" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:hash">
<p>Les méthodes <code>Hash()</code> s’appuient sur les paquets
<a href="https://pkg.go.dev/hash/maphash" title="hash/maphash sur Go Packages"><code>hash/maphash</code></a> et <a href="https://pkg.go.dev/unsafe" title="unsafe sur Go Packages"><code>unsafe</code></a> pour une meilleure
performance. Voir par exemple la <a href="https://github.com/akvorado/akvorado/blob/97fced5e855ac220e96148e132f643dc180cd097/outlet/routing/provider/bmp/rib.go#L89-L96">fonction <code>Hash()</code> de la structure
<code>nlri</code></a>. <a class="footnote-backref" href="#fnref:hash" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:cisco">
<p>Bien qu’auteur ou co-auteur des premières RFC liées à <abbr title="BGP Monitoring Protocol">BMP</abbr> depuis 2016
(<a href="https://www.rfc-editor.org/rfc/rfc7854" title="RFC 7854: BGP Monitoring Protocol (BMP)">RFC 7854</a>, <a href="https://www.rfc-editor.org/rfc/rfc8671" title="RFC 8671: Support for Adj-RIB-Out in the BGP Monitoring Protocol (BMP)">RFC 8671</a>, <a href="https://www.rfc-editor.org/rfc/rfc9069" title="RFC 9069: Support for Local RIB in the BGP Monitoring Protocol (BMP)">RFC 9069</a>), Cisco n’a pas fourni
d’implémentation utilisable dans IOS XR avant la version 24.2.1. Il nous
reste encore quelques routeurs à mettre à jour pour activer cette
fonctionnalité. <a class="footnote-backref" href="#fnref:cisco" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:KIP-932">
<p>La <a href="https://cwiki.apache.org/confluence/display/KAFKA/KIP-932%3A+Queues+for+Kafka" title="KIP-932: Queues for Kafka">KIP-932</a> introduit, dans Kafka 4.2, la notion de <em>groupes
partagés</em> pour permettre la consommation coopérative d’une même partition.
Akvorado ne la prend pas encore en charge. <a class="footnote-backref" href="#fnref:KIP-932" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:bmpconfig">
<p><abbr title="BGP Monitoring Protocol">BMP</abbr> peut être configuré pour envoyer les routes de chaque pair <abbr title="Border Gateway Protocol">BGP</abbr>
avant ou après l’application des politiques entrantes. Dans ce dernier cas,
plus d’un million de routes peuvent arriver par transit. <abbr title="BGP Monitoring Protocol">BMP</abbr> peut aussi être
configuré pour envoyer la <abbr title="Routing Information Base">RIB</abbr> locale, qui ne contient que le meilleur chemin
pour chaque préfixe. <a class="footnote-backref" href="#fnref:bmpconfig" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:plausible">
<p>Les préfixes sont aléatoires, mais la distribution de leurs tailles
et celle des longueurs de chemin AS suivent les <a href="https://bgp.potaroo.net/as2.0/" title="AS65000 BGP Routing Table Analysis Report">données fournies par
Geoff Huston</a>. <a class="footnote-backref" href="#fnref:plausible" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:retry">
<p>Une alternative aurait été de réessayer la recherche, mais cela
n’aurait pas de sens : la <abbr title="Routing Information Base">RIB</abbr> est une structure <a href="https://fr.wikipedia.org/wiki/Mod%C3%A8le_de_coh%C3%A9rence#Coh%C3%A9rence_%C3%A0_terme" title="Modèle de coherence sur Wikipedia">cohérente à terme</a> et
une liste de routes vide est bien un état qui s’est produit récemment. <a class="footnote-backref" href="#fnref:retry" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Découverte automatique de métriques Prometheus avec les labels Docker</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-decouverte-metriques-prometheus-labels-docker" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-decouverte-metriques-prometheus-labels-docker#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-03-05T15:40:24Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-decouverte-metriques-prometheus-labels-docker.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="/fr/blog/2025-akvorado-2.0" title="Akvorado release 2.0">Akvorado</a>, un collecteur de flux réseau, s’appuie sur <a href="https://traefik.io/traefik" title="Traefik: modern HTTP reverse proxy and load-balancer">Traefik</a>, un proxy
HTTP, pour exposer les services HTTP d’un environnement <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a>. Des
<a href="https://docs.docker.com/engine/manage-resources/labels/" title="Docker object labels">labels Docker</a> attachés à chaque service définissent les règles
de routage. Traefik les prend en compte automatiquement au démarrage d’un
conteneur. Plutôt que de maintenir un fichier de configuration statique pour
collecter les <a href="https://prometheus.io/docs/concepts/data_model/" title="Prometheus Data Model">métriques Prometheus</a>, la même approche
s’applique avec <a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>, simplifiant sa configuration.</p>
<div class="toc">
<ul>
<li><a href="#traefik-docker">Traefik &amp; Docker</a></li>
<li><a href="#decouverte-de-metriques-avec-alloy">Découverte de métriques avec Alloy</a><ul>
<li><a href="#decouverte-des-conteneurs-docker">Découverte des conteneurs Docker</a></li>
<li><a href="#reetiquetage-des-cibles">Réétiquetage des cibles</a></li>
<li><a href="#collecte-et-transmission">Collecte et transmission</a></li>
</ul>
</li>
<li><a href="#exporteurs-integres">Exporteurs intégrés</a></li>
</ul>
</div>
<h1 id="traefik-docker">Traefik &amp; Docker</h1>
<p>Traefik <a href="https://doc.traefik.io/traefik/reference/install-configuration/providers/docker/" title="Traefik: Docker provider">écoute les événements sur la socket Docker</a>. Chaque service annonce sa configuration via des labels. Par
exemple, voici le service Loki dans Akvorado :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">services</span><span class="p">:</span>
<span class="w">  </span><span class="nt">loki</span><span class="p">:</span>
<span class="w">    </span><span class="c1"># …</span>
<span class="w">    </span><span class="nt">expose</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">3100/tcp</span>
<span class="w">    </span><span class="nt">labels</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.enable=true</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.http.routers.loki.rule=PathPrefix(`/loki`)</span>
</code></pre></div>

<p>Dès que le conteneur est opérationnel, Traefik crée un routeur qui redirige les
requêtes correspondant à <code>/loki</code> vers le premier port exposé. Placer la
configuration Traefik au sein même de la définition du service est attirant.
Comment obtenir la même chose pour les métriques Prometheus ?</p>
<h1 id="decouverte-de-metriques-avec-alloy">Découverte de métriques avec Alloy</h1>
<p><a href="https://grafana.com/docs/alloy/latest/" title="Grafana Alloy documentation">Grafana Alloy</a>, un collecteur de métriques capable de collecter des métriques
Prometheus, inclut un composant <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.docker/" title="Alloy: discovery.docker"><code>discovery.docker</code></a>. Tout
comme Traefik, il se connecte à la socket Docker<sup id="fnref:socket"><a class="footnote-ref" href="#fn:socket">1</a></sup>. Avec quelques règles
de réétiquetage, on peut lui apprendre à utiliser les labels Docker pour
localiser et collecter les métriques.</p>
<p>On définit trois labels sur chaque service :</p>
<ul>
<li><code>metrics.enable</code> active la collecte de métriques,</li>
<li><code>metrics.port</code> indique le port exposant les métriques Prometheus,</li>
<li><code>metrics.path</code> indique le chemin vers les métriques.</li>
</ul>
<p>Si le service expose plus d’un port, <code>metrics.port</code> est obligatoire, sinon il
prend par défaut la valeur de l’unique port exposé. La valeur par défaut de
<code>metrics.path</code> est <code>/metrics</code>. Le service Loki devient :</p>
<div class="language-yaml codehilite"><pre><span/><code><span class="nt">services</span><span class="p">:</span>
<span class="w">  </span><span class="nt">loki</span><span class="p">:</span>
<span class="w">    </span><span class="c1"># …</span>
<span class="w">    </span><span class="nt">expose</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">3100/tcp</span>
<span class="w">    </span><span class="nt">labels</span><span class="p">:</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.enable=true</span>
<span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">traefik.http.routers.loki.rule=PathPrefix(`/loki`)</span>
<span class="hll"><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">metrics.enable=true</span>
</span><span class="hll"><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">metrics.path=/loki/metrics</span>
</span></code></pre></div>

<p>La configuration d’Alloy se divise en quatre parties :</p>
<ol>
<li><strong>découvrir</strong> les conteneurs via la socket Docker,</li>
<li><strong>filtrer et réétiqueter</strong> les cibles à l’aide des labels Docker,</li>
<li><strong>collecter</strong> les métriques,</li>
<li><strong>transmettre</strong> les métriques à Prometheus.</li>
</ol>
<h2 id="decouverte-des-conteneurs-docker">Découverte des conteneurs Docker</h2>
<p>Le premier bloc découvre les conteneurs en cours d’exécution :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">discovery.docker</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">host</span><span class="w">             </span><span class="o">=</span><span class="w"> </span><span class="s2">"unix:///var/run/docker.sock"</span>
<span class="w">  </span><span class="na">refresh_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="w">  </span><span class="nb">filter</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">name</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"label"</span>
<span class="w">    </span><span class="na">values</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"com.docker.compose.project=akvorado"</span><span class="p">]</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>Alloy se connecte à la socket Docker et liste les conteneurs toutes les 30
secondes<sup id="fnref:events"><a class="footnote-ref" href="#fn:events">2</a></sup>. Le bloc <code>filter</code> restreint la découverte aux conteneurs du
projet <code>akvorado</code>, évitant toute interférence avec d’autres conteneurs sur le
même hôte. Pour chaque conteneur découvert, Alloy produit une cible avec des
labels tels que <code>__meta_docker_container_label_metrics_port</code> pour le label
Docker <code>metrics.port</code>.</p>
<h2 id="reetiquetage-des-cibles">Réétiquetage des cibles</h2>
<p>L’étape de réétiquetage filtre et transforme les cibles brutes issues de la
découverte Docker en cibles exploitables. La première étape ne conserve que les
cibles dont <code>metrics.enable</code> vaut <code>true</code> :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"prometheus"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.docker.docker.targets</span>

<span class="c1">  // Keep only targets with metrics.enable=true</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_enable"</span><span class="p">]</span>
<span class="w">    </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="no">true</span><span class="err">`</span>
<span class="w">    </span><span class="na">action</span><span class="w">        </span><span class="o">=</span><span class="w"> </span><span class="s2">"keep"</span>
<span class="w">  </span><span class="p">}</span>

<span class="c1">  // …</span>
<span class="p">}</span>
</code></pre></div>

<p>La deuxième étape remplace le port découvert lorsque <code>metrics.port</code> est
défini :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="c1">// When metrics.port is set, override __address__.</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__address__", "__meta_docker_container_label_metrics_port"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">):</span><span class="err">\d+;</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__address__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"$1:$2"</span>
<span class="p">}</span>
</code></pre></div>

<p>Ensuite, on gère les conteneurs en mode réseau <code>host</code>. Quand
<code>__meta_docker_network_name</code> vaut <code>host</code>, l’adresse est réécrite en
<code>host.docker.internal</code> au lieu de <code>localhost</code><sup id="fnref:hostdocker"><a class="footnote-ref" href="#fn:hostdocker">3</a></sup> :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="c1">// When host networking, override __address__ to host.docker.internal.</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_port", "__meta_docker_network_name"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">;host`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__address__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"host.docker.internal:$1"</span>
<span class="p">}</span>
</code></pre></div>

<p>L’étape suivante dérive le label <code>job</code> à partir du nom du service, en supprimant
tout suffixe numéroté. Le label <code>instance</code> est l’adresse sans le port :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_com_docker_compose_service"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)(?:</span><span class="err">-\d+</span><span class="p">)?</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="p">}</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__address__"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">):</span><span class="err">\d+`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"instance"</span>
<span class="p">}</span>
</code></pre></div>

<p>Si un conteneur définit <code>metrics.path</code>, Alloy l’utilise comme chemin. Sinon, la
valeur par défaut est <code>/metrics</code> :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__meta_docker_container_label_metrics_path"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="err">`</span><span class="p">(.</span><span class="err">+</span><span class="p">)</span><span class="err">`</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__metrics_path__"</span>
<span class="p">}</span>
<span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">source_labels</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"__metrics_path__"</span><span class="p">]</span>
<span class="w">  </span><span class="na">regex</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="s2">""</span>
<span class="w">  </span><span class="na">target_label</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"__metrics_path__"</span>
<span class="w">  </span><span class="na">replacement</span><span class="w">   </span><span class="o">=</span><span class="w"> </span><span class="s2">"/metrics"</span>
<span class="p">}</span>
</code></pre></div>

<h2 id="collecte-et-transmission">Collecte et transmission</h2>
<p>Une fois les cibles correctement réétiquetées, la collecte et la transmission
sont sommaires :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.prometheus.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>

<span class="nv">prometheus.remote_write</span><span class="w"> </span><span class="s2">"default"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nb">endpoint</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">url</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"http://prometheus:9090/api/v1/write"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p><code>prometheus.scrape</code> récupère périodiquement les métriques des cibles
découvertes. <code>prometheus.remote_write</code> les transmet à Prometheus.</p>
<h1 id="exporteurs-integres">Exporteurs intégrés</h1>
<p>Certains services n’exposent pas de point d’accès Prometheus. Redis et Kafka en
sont des exemples courants. Alloy embarque des <a href="https://grafana.com/docs/alloy/latest/reference/components/prometheus/" title="Alloy: Prometheus components">exporteurs
Prometheus</a> capables d’interroger ces services et d’exposer
les métriques à leur place.</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.exporter.redis</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">redis_addr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"redis:6379"</span>
<span class="p">}</span>
<span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"redis"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">prometheus.exporter.redis.docker.targets</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">target_label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="w">    </span><span class="na">replacement</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"redis"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
<span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"redis"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.redis.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>
</code></pre></div>

<p>Le même schéma s’applique à Kafka :</p>
<div class="language-terraform codehilite"><pre><span/><code><span class="nv">prometheus.exporter.kafka</span><span class="w"> </span><span class="s2">"docker"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">kafka_uris</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="s2">"kafka:9092"</span><span class="p">]</span>
<span class="p">}</span>
<span class="nv">discovery.relabel</span><span class="w"> </span><span class="s2">"kafka"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nv">prometheus.exporter.kafka.docker.targets</span>
<span class="w">  </span><span class="nb">rule</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="na">target_label</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"job"</span>
<span class="w">    </span><span class="na">replacement</span><span class="w">  </span><span class="o">=</span><span class="w"> </span><span class="s2">"kafka"</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
<span class="nv">prometheus.scrape</span><span class="w"> </span><span class="s2">"kafka"</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="na">targets</span><span class="w">         </span><span class="o">=</span><span class="w"> </span><span class="nv">discovery.relabel.kafka.output</span>
<span class="w">  </span><span class="na">forward_to</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="p">[</span><span class="nv">prometheus.remote_write.default.receiver</span><span class="p">]</span>
<span class="w">  </span><span class="na">scrape_interval</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s2">"30s"</span>
<span class="p">}</span>
</code></pre></div>

<p>Chaque exporteur est un composant distinct avec sa propre configuration de
réétiquetage et de collecte. Le label <code>job</code> est défini explicitement.</p>
<hr/>
<p>Avec cette configuration, ajouter des métriques à un nouveau service disposant
d’un point d’accès Prometheus se résume à quelques labels dans
<code>docker-compose.yml</code>, tout comme l’ajout d’une route Traefik. Alloy s’en charge
automatiquement. Une approche similaire est possible avec d’autres méthodes de
découverte, comme <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.kubernetes/" title="Alloy: discovery.kubernetes"><code>discovery.kubernetes</code></a>,
<a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.scaleway/" title="Alloy: discovery.scaleway"><code>discovery.scaleway</code></a> ou <a href="https://grafana.com/docs/alloy/latest/reference/components/discovery/discovery.http/" title="Alloy: discovery.http"><code>discovery.http</code></a>. 🩺</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:socket">
<p>Traefik et Alloy nécessitent tous deux l’accès à la socket Docker,
ce qui confère un accès root à la machine hôte. Un <a href="https://github.com/Tecnativa/docker-socket-proxy" title="Docker Socket Proxy: security-enhanced proxy for Docker socket">Docker socket
proxy</a> atténue ce risque en n’exposant que les points d’accès de l’API en
lecture seule nécessaires à la découverte. <a class="footnote-backref" href="#fnref:socket" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:events">
<p>Contrairement à Traefik, qui surveille les événements, Grafana Alloy
interroge la liste des conteneurs à intervalles réguliers — un comportement
hérité de Prometheus. <a class="footnote-backref" href="#fnref:events" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:hostdocker">
<p>Le service Alloy nécessite <code>extra_hosts:
["host.docker.internal:host-gateway"]</code> dans sa définition. <a class="footnote-backref" href="#fnref:hostdocker" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Akvorado 2.0</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2025-akvorado-2.0" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2025-akvorado-2.0#isso-thread" rel="replies" type="text/html"/>
    <updated>2025-09-22T06:12:52Z</updated>
    <id>http://www.luffy.cx/fr/blog/2025-akvorado-2.0.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p><a href="https://github.com/akvorado/akvorado/releases/tag/v2.0.0">Akvorado 2.0</a> est publié aujourd’hui ! <a href="https://github.com/akvorado/akvorado" title="Akvorado: flow collector, enricher and visualizer">Akvorado</a> est un collecteur de flux
réseau <abbr title="IP Flow Information Export">IPFIX</abbr> et sFlow. Il enrichit les flux et les stocke dans une base de
données ClickHouse. Les utilisateurs peuvent parcourir les données via une
console web. Cette version introduit un changement architectural important et
d’autres améliorations mineures. Regardons plus en détails ! 🤿</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>diff<span class="w"> </span>--shortstat<span class="w"> </span>v1.11.5
<span class="go"> 493 files changed, 25015 insertions(+), 21135 deletions(-)</span>
</code></pre></div>

<div class="toc">
<ul>
<li><a href="#nouveau-service-outlet">Nouveau service « outlet »</a></li>
<li><a href="#autres-changements">Autres changements</a><ul>
<li><a href="#observabilite">Observabilité</a></li>
<li><a href="#documentation">Documentation</a></li>
<li><a href="#integration-continue">Intégration continue</a></li>
<li><a href="#docker">Docker</a></li>
<li><a href="#go">Go</a><ul>
<li><a href="#chaine-de-compilation">Chaîne de compilation</a></li>
<li><a href="#tests">Tests</a></li>
<li><a href="#un-autre-paquet-pour-kafka">Un autre paquet pour Kafka</a></li>
<li><a href="#table-de-routage-amelioree-pour-bmp">Table de routage améliorée pour BMP</a></li>
<li><a href="#divers">Divers</a></li>
</ul>
</li>
<li><a href="#javascript">JavaScript</a></li>
</ul>
</li>
<li><a href="#prochaines-etapes">Prochaines étapes</a></li>
</ul>
</div>
<h1 id="nouveau-service-outlet">Nouveau service « outlet »</h1>
<p>Le changement majeur dans Akvorado 2.0 est la <strong>séparation du service inlet</strong> en
deux parties : l’<strong>inlet</strong> et l’<strong>outlet</strong>. Auparavant, l’inlet gérait tout le
traitement des flux : réception, décodage et enrichissement. Les flux étaient
ensuite envoyés à Kafka pour stockage dans ClickHouse :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 542px"><span class="lf-media-inner" style="padding-bottom: 40.037%"><img alt="Traitement des flux Akvorado avant le changement : les flux sont reçus et&#10;traités par l'inlet, envoyés à Kafka et stockés dans ClickHouse" src="https://media-bernat-ch.b-cdn.net/images/akvorado/before-outlet.d3c1c81ca52d28.svg" width="542" height="217" class="lf-media"/></span></div><figcaption>Traitement des flux Akvorado avant l'introduction du service outlet</figcaption></figure>
<p>Les flux réseau atteignent le service inlet via des chaussettes UDP, un
protocole non fiable. L’inlet doit les traiter <strong>assez rapidement pour éviter de
perdre des paquets</strong>. Pour gérer un grand nombre de flux, l’inlet crée plusieurs
ensembles de workers pour recevoir les flux, récupérer les métadonnées et
assembler les flux enrichis pour Kafka. De nombreuses options de configuration
existaient pour la montée en charge, ce qui augmentait la complexité pour les
utilisateurs. Le code devait <strong>éviter tout blocage</strong>, rendant la chaîne de
traitement complexe et parfois peu fiable, notamment le composant <abbr title="BGP Monitoring Protocol">BMP</abbr><sup id="fnref:bmp"><a class="footnote-ref" href="#fn:bmp">1</a></sup>.
L’ajout de nouvelles fonctionnalités sans aggraver le problème devenait
difficile<sup id="fnref:features"><a class="footnote-ref" href="#fn:features">2</a></sup>.</p>
<p>Dans Akvorado 2.0, l’inlet reçoit les flux et les pousse vers Kafka sans les
décoder. Le nouveau service outlet gère les tâches restantes :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 732px"><span class="lf-media-inner" style="padding-bottom: 29.645%"><img alt="Traitement des flux Akvorado après le changement : les flux sont reçus par&#10;l'inlet, envoyés à Kafka, traités par l'outlet et insérés dans ClickHouse" src="https://media-bernat-ch.b-cdn.net/images/akvorado/after-outlet.9e9a485126fdf3.svg" width="732" height="217" class="lf-media"/></span></div><figcaption>Traitement des flux Akvorado après l'introduction du service outlet</figcaption></figure>
<p>Ce changement va au-delà d’un simple découpage<sup id="fnref:size"><a class="footnote-ref" href="#fn:size">3</a></sup> : l’outlet lit maintenant
les flux depuis Kafka et les pousse vers ClickHouse, deux tâches qu’Akvorado ne
gérait pas auparavant. Les flux sont <strong>groupés par lots pour augmenter
l’efficacité</strong> et réduire la charge sur ClickHouse en utilisant <a href="https://github.com/ClickHouse/ch-go/" title="Low-level Go Client for ClickHouse">ch-go</a>, un
client Go bas niveau pour ClickHouse. Quand les lots sont trop petits, des
insertions asynchrones sont utilisées (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). Le nombre de <strong>workers de
l’outlet s’ajuste dynamiquement</strong> (<a href="https://github.com/akvorado/akvorado/commit/e5a625aecf9e" title="outlet: make the number of Kafka workers dynamic">e5a625</a>) en fonction de la cible sur la
taille des lots et de la latence (50 000 flux et 5 secondes par défaut).</p>
<p>Cette nouvelle architecture nous permet également de <strong>simplifier et optimiser</strong>
le code. L’outlet récupère les métadonnées de façon synchrone (<a href="https://github.com/akvorado/akvorado/commit/e20645c92e38" title="outlet/metadata: synchronous fetching of metadata">e20645</a>). Le
composant <abbr title="BGP Monitoring Protocol">BMP</abbr> devient plus simple en supprimant le multitâche coopératif
(<a href="https://github.com/akvorado/akvorado/commit/3b9486b000c0" title="outlet/routing: remove batching of peer removal">3b9486</a>). Réutiliser le même objet <code>RawFlow</code> pour décoder les flux Protobuf
depuis Kafka réduit la pression sur le ramasse-miettes (<a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).</p>
<p>L’effet sur les performances globales d’Akvorado était quelque peu incertain,
mais un <a href="https://github.com/akvorado/akvorado/issues/1908#issuecomment-3233143146">utilisateur a indiqué</a> gagner <strong>35 % d’utilisation CPU
en moins</strong> après migration depuis la version précédente, ainsi que la résolution
du problème de longue date du composant <abbr title="BGP Monitoring Protocol">BMP</abbr>. 🥳</p>
<h1 id="autres-changements">Autres changements</h1>
<p>Cette nouvelle version inclut de nombreux changements divers. On y trouve par
exemple la complétion pour les ports sources et destinations (<a href="https://github.com/akvorado/akvorado/commit/f92d2ef9e2bd" title="console/filter: add autocomplete for src and dst ports">f92d2e</a>), et le
redémarrage automatique de l’orchestrateur (<a href="https://github.com/akvorado/akvorado/commit/0f72ffd6761b" title="cmd: automatic restart of orchestrator on configuration change">0f72ff</a>) lors de changements de
configuration pour éviter un piège courant pour les nouveaux utilisateurs.</p>
<p>Concentrons-nous sur quelques domaines clés pour cette version :
<a href="#observabilite">observabilité</a>, <a href="#documentation">documentation</a>,
<a href="#integration-continue"><abbr title="Continuous integration">CI</abbr></a>, <a href="#docker">Docker</a>, <a href="#go">Go</a> et
<a href="#javascript">JavaScript</a>.</p>
<h2 id="observabilite">Observabilité</h2>
<p>Akvorado expose des métriques pour fournir une visibilité sur la chaîne de
traitement et aider à diagnostiquer les problèmes. Elles sont disponibles <strong>au
format Prometheus</strong>, via par exemple <a href="https://demo.akvorado.net/api/v0/inlet/metrics"><code>/api/v0/inlet/metrics</code></a>. Avec
l’introduction de l’outlet, de nombreuses métriques ont été déplacées. Certaines
ont également été renommées (<a href="https://github.com/akvorado/akvorado/commit/4c0b15e1cd4e" title="inlet/outlet: rename a few metrics">4c0b15</a>) pour correspondre aux <a href="https://prometheus.io/docs/practices/naming/">bonnes pratiques
Prometheus</a>. Une métrique pour mesurer le décalage
des consommateurs Kafka a été ajoutée (<a href="https://github.com/akvorado/akvorado/commit/e3a778552d6e" title="outlet/kafka: expose consumer lag as a prometheus metric">e3a778</a>).</p>
<p>Si vous n’avez pas votre propre pile d’observabilité, la configuration Docker
Compose fournie avec Akvorado en propose une. Vous pouvez l’activer avec les
profils introduits à cette fin (<a href="https://github.com/akvorado/akvorado/commit/529a8f69bebc" title="docker: use profiles to optionally enable Prometheus, Loki and Grafana">529a8f</a>).</p>
<p>Le profil <code>prometheus</code> fournit <a href="https://prometheus.io/" title="Prometheus: Open source metrics and monitoring">Prometheus</a> pour <strong>stocker les métriques</strong> et
<a href="https://grafana.com/docs/alloy/latest/" title="Documentation de Grafana Alloy">Alloy</a> pour <strong>les collecter</strong> (<a href="https://github.com/akvorado/akvorado/commit/2b3c46372985" title="docker: switch from prometheus to alloy for scrapping metrics">2b3c46</a>, <a href="https://github.com/akvorado/akvorado/commit/f81299ff2816" title="docker: group services together in alloy configuration">f81299</a> et <a href="https://github.com/akvorado/akvorado/commit/8eb7cd63b1c0" title="docker: make Alloy configuration use only Docker labels">8eb7cd</a>). Les
métriques Redis et Kafka sont collectées via l’exporteur intégré à Alloy
(<a href="https://github.com/akvorado/akvorado/commit/560113e3af24" title="docker: monitor Redis and Kafka using Alloy internal exporter">560113</a>). Les autres métriques sont exposées au format Prometheus et sont
automatiquement récupérées par Alloy à l’aide d’annotations Docker. C’est
similaire à ce qui est fait pour configurer Traefik. <a href="https://github.com/google/cadvisor/" title="Analyzes resource usage and performance characteristics of running containers">cAdvisor</a> a également
été ajouté (<a href="https://github.com/akvorado/akvorado/commit/83d8556d6ddb" title="docker: add cAdvisor for container monitoring">83d855</a>) pour fournir des métriques liées aux conteneurs.</p>
<p>Le profil <code>loki</code> fournit <a href="https://grafana.com/docs/loki/latest/" title="Documentation de Grafana Loki">Loki</a> pour <strong>stocker les logs</strong> (<a href="https://github.com/akvorado/akvorado/commit/45c684dc6737" title="docker: add Loki to the observability stack">45c684</a>). Bien
qu’Alloy puisse collecter et envoyer les logs vers Loki, ses capacités d’analyse
sont limitées : je n’ai pas trouvé de moyen de préserver toutes les métadonnées
associées aux logs structurés produits par de nombreuses applications, y compris
Akvorado. <a href="https://vector.dev/" title="Vector: lightweight, ultra-fast tool for building observability pipelines">Vector</a> remplace Alloy (<a href="https://github.com/akvorado/akvorado/commit/95e2011d0c67" title="docker: replace Alloy by Vector for parsing logs">95e201</a>) et propose un langage, <abbr title="Vector Remap Language">VRL</abbr>,
pour transformer les logs. Malheureusement, il ne peut actuellement <a href="https://github.com/vectordotdev/vector/issues/7358" title="Support checkpointing and reading old logs with docker_logs source">pas
récupérer les logs Docker précédant son démarrage</a>.</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (11.2025)</p>
<p>Alloy peut désormais préserver les champs des logs
structurés dans les métadonnées en utilisant une expression régulière. Voir <a href="https://github.com/grafana/alloy/pull/4629" title="feat(loki.process): add regex field to structured_metadata stage">PR #4629</a>.</p>
</div>
<p>Enfin, le profil <code>grafana</code> fournit <a href="https://grafana.com/docs/grafana/latest/" title="Documentation de Grafana">Grafana</a>, mais les tableaux de bord
fournis sont pour le moment cassés. Ce sera corrigé dans une prochaine version.</p>
<h2 id="documentation">Documentation</h2>
<p>La configuration Docker Compose fournie par Akvorado facilite la mise en route
rapide. Cependant, Akvorado nécessite quelques étapes obligatoires pour être
fonctionnel. Il est livré avec une <a href="https://demo.akvorado.net/docs">documentation complète</a>, y compris un chapitre sur le <a href="https://demo.akvorado.net/docs/troubleshooting">dépannage des
problèmes</a>. J’espérais que cette documentation
réduirait la charge de support. Il est difficile de savoir si cela fonctionne.
Les utilisateurs satisfaits rapportent rarement leurs succès, tandis qu’une
poignée d’utilisateurs ouvrent des discussions pour demander de l’aide sans
avoir vraiment lu la documentation.</p>
<p>Dans cette version, la <strong>documentation a été considérablement améliorée</strong>.</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>diff<span class="w"> </span>--shortstat<span class="w"> </span>v1.11.5<span class="w"> </span>--<span class="w"> </span>console/data/docs
<span class="go"> 10 files changed, 1873 insertions(+), 1203 deletions(-)</span>
</code></pre></div>

<p>La documentation a été mise à jour (<a href="https://github.com/akvorado/akvorado/commit/fc1028bfcdc5" title="doc: documentation update for inlet/outlet separation">fc1028</a>) pour correspondre à la nouvelle
architecture d’Akvorado. La section de dépannage a été réécrite (<a href="https://github.com/akvorado/akvorado/commit/17a272d0ba18" title="docs: update troubleshooting documentation">17a272</a>).
Une documentation sur comment améliorer les performances de ClickHouse lors de
la mise à niveau depuis des versions antérieures à 1.10.0 a été ajoutée
(<a href="https://github.com/akvorado/akvorado/commit/5f1e9a49c7d5" title="docs: document schema update for installations before 1.10.0">5f1e9a</a>). Un <abbr title="Large Language Model">LLM</abbr> a relu l’ensemble (<a href="https://github.com/akvorado/akvorado/commit/06e3f334fdb3" title="docs: proofread the whole documentation">06e3f3</a>). La documentation destinée
aux développeurs a également été améliorée (<a href="https://github.com/akvorado/akvorado/commit/548bbbfca161" title="docs: explain more how to develop on the frontend">548bbb</a>, <a href="https://github.com/akvorado/akvorado/commit/e41bae008b68" title="docs: make it a bit simpler to hack Akvorado">e41bae</a> et
<a href="https://github.com/akvorado/akvorado/commit/871fc5981498" title="docs: make it even easier to hack on the console">871fc5</a>).</p>
<p>Du point de vue de l’utilisabilité, les sections de la table des matières
peuvent maintenant se replier (<a href="https://github.com/akvorado/akvorado/commit/c142e58110b3" title="console/frontend: make TOC collapsable in documentation">c142e5</a>). Les avertissements aident à
attirer l’attention des utilisateurs sur les points importants (<a href="https://github.com/akvorado/akvorado/commit/8ac89407fd49" title="console: add admonitions to documentation markdown">8ac894</a>).</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 693px"><span class="lf-media-inner" style="padding-bottom: 67.677%"><img alt="Avertissement dans la documentation d'Akvorado demandant à un utilisateur de&#10;ne pas ouvrir un ticket ou démarrer une discussion avant d'avoir lu la&#10;documentation" src="https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@1x.1322ddc0bd4b5b.png" srcset="https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@1x.1322ddc0bd4b5b.png 871w,https://media-bernat-ch.b-cdn.net/images/akvorado/admonitions@2x.2ad1061da0399d.png 1386w" sizes="auto, (max-width: 693px) 100vw, 693px" width="693" height="469" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGMAAABDAQMAAACyWSngAAAAA1BMVEX5+PvrZTWkAAAAEUlEQVR42mNgGAWjYBQMfgAAA6oAASjPUCUAAAAASUVORK5CYII=)"/></span></div><figcaption>Exemple d'utilisation des avertissements dans la documentation d'Akvorado</figcaption></figure>
<h2 id="integration-continue">Intégration continue</h2>
<p>Cette version inclut des efforts pour <strong>accélérer</strong> l’intégration continue sur
GitHub. Les tests de couverture et de concurrence s’exécutent en parallèle
(<a href="https://github.com/akvorado/akvorado/commit/6af2163066aa" title="build: speed up CI by running coverage and race tests in parallel">6af216</a> et <a href="https://github.com/akvorado/akvorado/commit/fa9e48d16a90" title="build: run a third job for misc tests">fa9e48</a>). L’image Docker se construit pendant les tests mais
ne reçoit son étiquette qu’après leurs succès (<a href="https://github.com/akvorado/akvorado/commit/8b0dcee14f60" title="build: build Docker image earlier but tag later">8b0dce</a>).</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 1358px"><span class="lf-media-inner" style="padding-bottom: 36.672%"><img alt="Flux GitHub pour la CI avec de nombreuses tâches, certaines s'exécutant&#10;en parallèle, d'autres non" src="https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@1x.d3576897765cd8.png" srcset="https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@1x.d3576897765cd8.png 1707w,https://media-bernat-ch.b-cdn.net/images/akvorado/github-actions@2x.085acee6fbcf28.png 2716w" sizes="auto, (max-width: 972px) 100vw, 900px" width="1358" height="498" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqcAAAD5AQMAAAAgMA7pAAAAA1BMVEX19/mL6bEjAAAAK0lEQVR42u3BAQEAAACCIP+vbkhAAQAAAAAAAAAAAAAAAAAAAAAAAADAuwFTpgABD31LnAAAAABJRU5ErkJggg==)"/></span></div><figcaption>Flux GitHub pour tester et construire Akvorado</figcaption></figure>
<p>Les tests de bout en bout (<a href="https://github.com/akvorado/akvorado/commit/883e19922eee" title="build: add end-to-end testing">883e19</a>) s’assurent que la configuration Docker
Compose fournie fonctionne comme attendu. <a href="https://hurl.dev/">Hurl</a> exécute des tests sur
diverses URL, notamment pour vérifier les métriques (<a href="https://github.com/akvorado/akvorado/commit/42679b1f2db3" title="build: use hurl for end-to-end tests">42679b</a> et <a href="https://github.com/akvorado/akvorado/commit/169fa933af64" title="build: add more e2e tests by testing Prometheus metrics">169fa9</a>).
Par exemple :</p>
<div class="language-hurl codehilite"><pre><span/><code><span class="c1">## Test inlet has received NetFlow flows</span>
<span class="kr">GET</span><span class="w"> </span><span class="sx">http://127.0.0.1:8080/prometheus/api/v1/query</span>
<span class="nn">[Query]</span>
<span class="na">query</span><span class="p">:</span><span class="w"> </span>sum(akvorado_inlet_flow_input_udp_packets_total{job="akvorado-inlet",listener=":2055"})
<span class="k">HTTP</span><span class="w"> </span><span class="mi">200</span>
<span class="nn">[Captures]</span>
<span class="na">inlet_receivedflows</span><span class="p">:</span><span class="w"> </span>jsonpath "$.data.result[0].value[1]" toInt
<span class="nn">[Asserts]</span>
<span class="k">variable</span><span class="w"> </span><span class="s2">"inlet_receivedflows"</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="mi">10</span>

<span class="c1">## Test inlet has sent them to Kafka</span>
<span class="kr">GET</span><span class="w"> </span><span class="sx">http://127.0.0.1:8080/prometheus/api/v1/query</span>
<span class="nn">[Query]</span>
<span class="na">query</span><span class="p">:</span><span class="w"> </span>sum(akvorado_inlet_kafka_sent_messages_total{job="akvorado-inlet"})
<span class="k">HTTP</span><span class="w"> </span><span class="mi">200</span>
<span class="nn">[Captures]</span>
<span class="na">inlet_sentflows</span><span class="p">:</span><span class="w"> </span>jsonpath "$.data.result[0].value[1]" toInt
<span class="nn">[Asserts]</span>
<span class="k">variable</span><span class="w"> </span><span class="s2">"inlet_sentflows"</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="p">{{</span><span class="w"> </span><span class="nv">inlet_receivedflows</span><span class="w"> </span><span class="p">}}</span>
</code></pre></div>

<h2 id="docker">Docker</h2>
<p>Akvorado est fourni avec une configuration <a href="https://docs.docker.com/compose/" title="Docker Compose documentation">Docker Compose</a> complète pour
aider les utilisateurs à <strong>démarrer rapidement</strong>. Elle assure un déploiement
cohérent, éliminant de nombreux problèmes liés à l’environnement et la
configuration. Elle sert également de documentation.</p>
<p>Cette version apporte quelques petites améliorations autour de Docker :</p>
<ul>
<li>un exemple pour configurer Traefik pour TLS avec <a href="https://letsencrypt.org/">Let’s Encrypt</a>
  (<a href="https://github.com/akvorado/akvorado/commit/1a27bb1bc2a3" title="docker: add examples to enable authentication and TLS">1a27bb</a>),</li>
<li>la compression HTTP (<a href="https://github.com/akvorado/akvorado/commit/bee9a5de6771" title="docker: enable HTTP compression for Traefik">bee9a5</a>), et</li>
<li>le support d’IPv6 (<a href="https://github.com/akvorado/akvorado/commit/a74a41a6a049" title="docker: enable IPv6 networking, use a specific IPv4 subnet">a74a41</a>).</li>
</ul>
<p>Auparavant, de nombreuses images Docker provenaient de la <a href="https://github.com/bitnami/containers/">bibliothèque de
conteneurs Bitnami</a>. Cependant, Bitnami a été
racheté par VMWare en 2019 et VMWare a été racheté par Broadcom en 2023. En
conséquence, les <a href="https://github.com/bitnami/containers/issues/83267" title="Upcoming changes to the Bitnami catalog (effective August 28th, 2025)">images Bitnami ont été dépréciées</a> en moins d’un mois. Ce n’était pas vraiment une surprise<sup id="fnref:broadcom"><a class="footnote-ref" href="#fn:broadcom">4</a></sup>.
Les versions précédentes d’Akvorado avaient déjà commencé à s’en éloigner. Dans
cette version, l’image Kafka du projet Apache remplace celle de Bitnami
(<a href="https://github.com/akvorado/akvorado/commit/1eb38226fc23" title="docker: switch to Apache Kafka">1eb382</a>). Grâce au passage au mode KRaft, Zookeeper n’est plus nécessaire
(<a href="https://github.com/akvorado/akvorado/commit/0a2ea1b1a189" title="docker: remove zookeeper for ClickHouse cluster as well">0a2ea1</a>, <a href="https://github.com/akvorado/akvorado/commit/8a49ca40d31b" title="docker: remove Zookeeper from default setup">8a49ca</a> et <a href="https://github.com/akvorado/akvorado/commit/f65d20443622" title="docker: remove use of Zookeeper for Kafka">f65d20</a>).</p>
<p>Les images Docker d’Akvorado étaient auparavant compilées avec Nix. Cependant,
construire des images AArch64 sur x86-64 est lent car cela repose sur
l’émulation de QEMU. Le <code>Dockerfile</code> mis à jour utilise des constructions
<a href="https://docs.docker.com/build/building/multi-stage/" title="Multi-stage builds">multi-étapes</a> et <a href="https://docs.docker.com/build/building/multi-platform/" title="Multi-platform builds">multi-plateformes</a> : une étape
construit la partie JavaScript sur la plateforme hôte, une étape construit la
partie Go en compilation croisée sur la plateforme hôte, et la dernière étape
assemble l’image en se basant sur <a href="https://github.com/GoogleContainerTools/distroless" title="Language focused docker images, minus the operating system">une image légère</a>
(<a href="https://github.com/akvorado/akvorado/commit/268e95ee1891" title="docker: switch away from Nix to build Docker images">268e95</a> et <a href="https://github.com/akvorado/akvorado/commit/d526ca451ce1" title="build: add a platform independant step into Makefile">d526ca</a>).</p>
<div class="language-docker codehilite"><pre><span/><code><span class="c"># Ceci est une version simplifiée</span>
<span class="k">FROM</span><span class="w"> </span><span class="s">--platform=$BUILDPLATFORM</span><span class="w"> </span>node:20-alpine<span class="w"> </span>AS<span class="w"> </span>build-js
<span class="k">RUN</span><span class="w"> </span>apk<span class="w"> </span>add<span class="w"> </span>--no-cache<span class="w"> </span>make
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">/build</span>
<span class="k">COPY</span><span class="w"> </span>console/frontend<span class="w"> </span>console/frontend
<span class="k">COPY</span><span class="w"> </span>Makefile<span class="w"> </span>.
<span class="k">RUN</span><span class="w"> </span>make<span class="w"> </span>console/data/frontend

<span class="k">FROM</span><span class="w"> </span><span class="s">--platform=$BUILDPLATFORM</span><span class="w"> </span>golang:alpine<span class="w"> </span>AS<span class="w"> </span>build-go
<span class="k">RUN</span><span class="w"> </span>apk<span class="w"> </span>add<span class="w"> </span>--no-cache<span class="w"> </span>make<span class="w"> </span>curl<span class="w"> </span>zip
<span class="k">WORKDIR</span><span class="w"> </span><span class="s">/build</span>
<span class="k">COPY</span><span class="w"> </span>.<span class="w"> </span>.
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>build-js<span class="w"> </span>/build/console/data/frontend<span class="w"> </span>console/data/frontend
<span class="k">RUN</span><span class="w"> </span>go<span class="w"> </span>mod<span class="w"> </span>download
<span class="k">RUN</span><span class="w"> </span>make<span class="w"> </span>all-indep
<span class="hll"><span class="k">ARG</span><span class="w"> </span>TARGETOS<span class="w"> </span>TARGETARCH<span class="w"> </span>TARGETVARIANT<span class="w"> </span>VERSION
</span><span class="k">RUN</span><span class="w"> </span>make

<span class="k">FROM</span><span class="w"> </span><span class="s">gcr.io/distroless/static:latest</span>
<span class="k">COPY</span><span class="w"> </span>--from<span class="o">=</span>build-go<span class="w"> </span>/build/bin/akvorado<span class="w"> </span>/usr/local/bin/akvorado
<span class="k">ENTRYPOINT</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"/usr/local/bin/akvorado"</span><span class="w"> </span><span class="p">]</span>
</code></pre></div>

<p>Lors de la construction pour plusieurs plateformes avec <code>--platform
linux/amd64,linux/arm64,linux/arm/v7</code>, les étapes de construction jusqu’à la
ligne surlignée ne s’exécutent qu’une seule fois pour toutes les plateformes.
Cela accélère considérablement la compilation. 🚅</p>
<p>Akvorado livre maintenant des images Docker pour ces plateformes :
<code>linux/amd64</code>, <code>linux/amd64/v3</code>, <code>linux/arm64</code> et <code>linux/arm/v7</code>. Lors du
téléchargement de <code>ghcr.io/akvorado/akvorado</code>, Docker sélectionne la meilleure
image pour le CPU actuel. Sur x86-64, il y a deux choix. Si votre <a href="https://en.wikipedia.org/wiki/X86-64#Microarchitecture_levels" title="X86-64: Microarchitecture levels">CPU est assez
récent</a>, Docker télécharge <code>linux/amd64/v3</code>. Cette version
contient des optimisations supplémentaires et devrait s’exécuter plus rapidement
que la version <code>linux/amd64</code>. Il serait intéressant de livrer une image pour
<code>linux/arm64/v8.2</code>, mais Docker ne supporte pas encore le même mécanisme pour
AArch64 (<a href="https://github.com/akvorado/akvorado/commit/7928084fdef0" title="build: add some pointers to understand architecture variants">792808</a>).</p>
<h2 id="go">Go</h2>
<p>Cette version inclut de nombreux changements liés à Go mais non visibles pour
les utilisateurs.</p>
<h3 id="chaine-de-compilation">Chaîne de compilation</h3>
<p>Par le passé, Akvorado supportait les deux dernières versions de Go, empêchant
l’utilisation immédiate des dernières améliorations. L’objectif était de
permettre aux utilisateurs de distributions stables d’utiliser les versions de
Go fournies avec leur distribution pour compiler Akvorado. Cependant, cela
pouvait être frustrant quand des fonctionnalités intéressantes, comme <code>go tool</code>,
étaient publiées. Akvorado 2.0 nécessite <strong>Go 1.25</strong> (<a href="https://github.com/akvorado/akvorado/commit/77306ddceee6" title="build: bump go.mod to Go 1.25 to use wg.Go()">77306d</a>) mais peut être
compilé avec des chaînes de compilation plus anciennes en téléchargeant
automatiquement une version plus récente (<a href="https://github.com/akvorado/akvorado/commit/94fb1c64837d" title="build: accept building with a not up-to-date toolchain">94fb1c</a>)<sup id="fnref:debian"><a class="footnote-ref" href="#fn:debian">5</a></sup>. Les utilisateurs
peuvent toujours modifier <code>GOTOOLCHAIN</code> pour annuler cette décision. La chaîne
de compilation recommandée est mise à jour hebdomadairement via la <abbr title="Continuous integration">CI</abbr> pour
s’assurer d’obtenir la dernière version mineure (<a href="https://github.com/akvorado/akvorado/commit/5b11ecf29e4c" title="build: try updating Go toolchain weekly">5b11ec</a>). Ce changement
simplifie aussi les mises à jour vers des versions plus récentes : seul <code>go.mod</code>
doit être mis à jour.</p>
<p>Grâce à ce changement, Akvorado utilise maintenant <code>wg.Go()</code> (<a href="https://github.com/akvorado/akvorado/commit/77306ddceee6" title="build: bump go.mod to Go 1.25 to use wg.Go()">77306d</a>) et
j’ai commencé à convertir certains tests unitaires vers le nouveau paquet
<code>test/synctest</code> (<a href="https://github.com/akvorado/akvorado/commit/bd787e654f93" title="common/daemon: experiment with the test/synctest package">bd787e</a>, <a href="https://github.com/akvorado/akvorado/commit/7016d899690b" title="demoexporter/flows: switch to testing/synctest package for clock">7016d8</a> et <a href="https://github.com/akvorado/akvorado/commit/159085a0d3ac" title="outlet/metadata: switch to testing/synctest package for clock">159085</a>).</p>
<h3 id="tests">Tests</h3>
<p>Pour <strong>tester l’égalité</strong>, j’utilise une fonction <code>Diff()</code> qui affiche les
différences en cas d’échec :</p>
<div class="language-go codehilite"><pre><span/><code><span class="nx">got</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">input</span><span class="p">.</span><span class="nx">Keys</span><span class="p">()</span>
<span class="nx">expected</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="p">[]</span><span class="kt">int</span><span class="p">{</span><span class="mi">1</span><span class="p">,</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="mi">3</span><span class="p">}</span>
<span class="k">if</span><span class="w"> </span><span class="nx">diff</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">helpers</span><span class="p">.</span><span class="nx">Diff</span><span class="p">(</span><span class="nx">got</span><span class="p">,</span><span class="w"> </span><span class="nx">expected</span><span class="p">);</span><span class="w"> </span><span class="nx">diff</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="s">""</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">t</span><span class="p">.</span><span class="nx">Fatalf</span><span class="p">(</span><span class="s">"Keys() (-got, +want):\n%s"</span><span class="p">,</span><span class="w"> </span><span class="nx">diff</span><span class="p">)</span>
<span class="p">}</span>
</code></pre></div>

<p>Cette fonction utilise <a href="https://github.com/kylelemons/godebug"><code>kylelemons/godebug</code></a>. Ce paquet
n’est plus maintenu et présente des lacunes : par exemple, par défaut, elle ne
compare pas les champs privés des structures, ce qui peut causer des tests qui
réussissent de façon inattendue. Je l’ai remplacé par
<a href="https://github.com/google/go-cmp"><code>google/go-cmp</code></a>, qui est plus strict et formatte la différence
de manière plus lisible (<a href="https://github.com/akvorado/akvorado/commit/e2f1df9add0d" title="tests: replace godebug by go-cmp for structure diffs">e2f1df</a>).</p>
<h3 id="un-autre-paquet-pour-kafka">Un autre paquet pour Kafka</h3>
<p>Un autre changement est le passage de <a href="https://github.com/IBM/sarama">Sarama</a> à <a href="https://github.com/twmb/franz-go">franz-go</a> pour <strong>interagir
avec Kafka</strong> (<a href="https://github.com/akvorado/akvorado/commit/756e4a8fbd7a" title="/kafka: switch to franz-go">756e4a</a> et <a href="https://github.com/akvorado/akvorado/commit/2d26c5b9cab1" title="inlet/kafka: use kfake for functional tests with Kafka">2d26c5</a>). La principale motivation pour ce
changement est de travailler avec un meilleur modèle de concurrence. Sarama
repose fortement sur les canaux et il est difficile de savoir quand un objet
n’est plus utilisé. franz-go utilise une approche plus moderne<sup id="fnref:channels"><a class="footnote-ref" href="#fn:channels">6</a></sup> et
performante avec des fonctions de rappel. Il est également livré avec un paquet
pour lancer de faux clusters Kafka, ce qui est plus pratique que les fonctions
de simulation fournies par Sarama.</p>
<h3 id="table-de-routage-amelioree-pour-bmp">Table de routage améliorée pour <abbr title="BGP Monitoring Protocol">BMP</abbr></h3>
<p>Pour stocker les routes, le composant <abbr title="BGP Monitoring Protocol">BMP</abbr> utilisait
<a href="https://github.com/kentik/patricia"><code>kentik/patricia</code></a>, une implémentation d’un arbre patricia
axée sur la réduction de la pression sur le ramasse-miettes.
<a href="https://github.com/gaissmai/bart"><code>gaissmai/bart</code></a> est une alternative plus récente utilisant une
adaptation de <a href="https://www.hariguchi.org/art/art.pdf" title="ART: Allotment Routing Table. A Fast Free Multibit Trie Based Routing Table">l’algorithme ART de Donald Knuth</a>
qui promet de <a href="https://github.com/gaissmai/iprbench" title="Benchmarks for Golang IP routing table">meilleures performances</a> et les fournit :
<strong>recherches 90 % plus rapides</strong> et <strong>insertions 27 % plus rapides</strong> (<a href="https://github.com/akvorado/akvorado/commit/92ee2e05b7c4" title="outlet/routing: use gaissmai/bart instead of kentik/patricia">92ee2e</a>
et <a href="https://github.com/akvorado/akvorado/commit/fdb65c93a5fe" title="outlet/routing: store v4 routes into a v4 tree">fdb65c</a>).</p>
<p>Contrairement à <code>kentik/patricia</code>, <code>gaissmai/bart</code> n’aide pas à stocker
efficacement les valeurs attachées à chaque préfixe. J’ai adapté la même
approche utilisée par <code>kentik/patricia</code> pour stocker les listes de routes pour
chaque préfixe : stocker un index 32 bits pour chaque préfixe, et l’utiliser
pour construire un index 64 bits pour rechercher les routes dans un tableau
associatif, qui est une structure particulièrement efficace de Go.</p>
<p><code>gaissmai/bart</code> supporte également une version sans verrous de la table de
routage, mais ce n’est pas aussi simple car nous aurions besoin d’étendre cela
au tableau stockant les routes et au mécanisme d’internement. J’ai également
tenté d’utiliser le nouveau paquet <code>unique</code> de Go pour remplacer le paquet
<code>intern</code> inclus dans Akvorado, mais les performances étaient pires<sup id="fnref:worse"><a class="footnote-ref" href="#fn:worse">7</a></sup>.</p>
<h3 id="divers">Divers</h3>
<p>Les versions précédentes d’Akvorado utilisaient un <a href="/fr/blog/2023-protobuf-dynamique-golang" title="Encodage rapide et dynamique des Protocol Buffers en Go">encodeur Protobuf
personnalisé</a> pour les performances et la flexibilité.
Avec l’introduction du service outlet, Akvorado n’a besoin que d’un schéma
statique simple, donc ce code a été supprimé. Cependant, il est possible
d’améliorer les performances avec
<a href="https://github.com/planetscale/vtprotobuf"><code>planetscale/vtprotobuf</code></a> (<a href="https://github.com/akvorado/akvorado/commit/e49a744a6d98" title="build: use vtprotobuf to speedup protobuf marshal/unmarshal">e49a74</a> et <a href="https://github.com/akvorado/akvorado/commit/8b580fd26bfc" title="outlet/core: reuse the same RawFlow object when decoding">8b580f</a>).
De plus, la dépendance à <code>protoc</code>, un programme C++, était quelque peu
ennuyeuse. Par conséquent, Akvorado utilise maintenant <a href="https://buf.build/">buf</a>, écrit en Go,
pour convertir un schéma Protobuf en code Go (<a href="https://github.com/akvorado/akvorado/commit/f4c879e525a5" title="build: switch from protoc to buf to build protobuf">f4c879</a>).</p>
<p>Une autre petite optimisation pour <strong>réduire la taille du binaire d’Akvorado</strong>
de 10 MB a consisté à compresser les ressources statiques intégrées dans
Akvorado dans un fichier ZIP. Cela inclut la base des numéros d’AS, ainsi que
les images SVG pour la documentation. Un bout de code rend ce changement
transparent (<a href="https://github.com/akvorado/akvorado/commit/b1d6382585dd" title="common/embed: replace all go:embed use by an embedded archive">b1d638</a> et <a href="https://github.com/akvorado/akvorado/commit/e69b919d1c78" title="build: remove embedded drawio diagrams from embedded SVG">e69b91</a>).</p>
<h2 id="javascript">JavaScript</h2>
<p>Récemment, deux grandes <strong>attaques de la chaîne d’approvisionnement</strong> ont frappé
l’écosystème JavaScript : <a href="https://www.aikido.dev/blog/npm-debug-and-chalk-packages-compromised" title="npm debug and chalk packages compromised">une affectant les paquets populaires <code>chalk</code> et
<code>debug</code></a> et <a href="https://sigh.dev/posts/ctrl-tinycolor-post-mortem/" title="@ctrl/tinycolor Supply Chain Attack Post-mortem">une autre impactant le paquet populaire
<code>@ctrl/tinycolor</code></a>. Ces attaques existent aussi dans d’autres
écosystèmes, mais JavaScript est une cible privilégiée en raison de l’usage
intensif de petites dépendances. La version précédente d’Akvorado reposait sur
<strong>653 dépendances</strong>.</p>
<p><code>npm-run-all</code> a été supprimé (<a href="https://github.com/akvorado/akvorado/commit/3424e8b8c9f2" title="console/frontend: don't use npm-run-all">3424e8</a>, 132 dépendances). <code>patch-package</code> a
été supprimé (<a href="https://github.com/akvorado/akvorado/commit/6258050251a3" title="console/frontend: remove patch for sugar-date">625805</a> et <a href="https://github.com/akvorado/akvorado/commit/e85ff0ae31fa" title="console/frontend: remove patch for echarts">e85ff0</a>, 69 dépendances) en déplaçant les
définitions TypeScript manquantes vers <code>env.d.ts</code>. <code>eslint</code> a été remplacé par
<a href="https://oxc.rs/docs/guide/usage/linter">oxlint</a>, un linter écrit en Rust (<a href="https://github.com/akvorado/akvorado/commit/97fd8ca2e479" title="console/frontend: replace eslint by oxlint">97fd8c</a>, 125 dépendances, plugins
inclus).</p>
<p>J’ai basculé de <code>npm</code> vers <a href="https://pnpm.io/" title="Fast, disk space efficient package manager">Pnpm</a>, un gestionnaire de paquets alternatif
(<a href="https://github.com/akvorado/akvorado/commit/fce383dbf46b" title="build: switch to pnpm">fce383</a>). Pnpm n’exécute pas les scripts d’installation par défaut<sup id="fnref:install"><a class="footnote-ref" href="#fn:install">8</a></sup>
et empêche l’installation de paquets trop récents. Il est également
significativement plus rapide<sup id="fnref:fast"><a class="footnote-ref" href="#fn:fast">9</a></sup>. Contrairement à <code>npm</code>, il n’est pas livré
avec Node.js. Cependant, Node.js contient <a href="https://github.com/nodejs/corepack" title="Zero-runtime-dependency package acting as bridge between Node projects and their package managers">Corepack</a>, qui nous permet
d’utiliser Pnpm sans l’installer. Pnpm peut également lister les licences
utilisées par chaque dépendance, supprimant le besoin d’utiliser
<code>license-compliance</code> (<a href="https://github.com/akvorado/akvorado/commit/a35ca8bc07e0" title="build: replace license-compliance with pnpm">a35ca8</a>, 42 dépendances).</p>
<p>Pour des améliorations de vitesse supplémentaires, au-delà du passage à Pnpm et
Oxlint, <a href="https://vite.dev/">Vite</a> a été remplacé par sa <a href="https://voidzero.dev/posts/announcing-rolldown-vite" title="Announcing Rolldown-Vite">version Rolldown</a> plus rapide
(<a href="https://github.com/akvorado/akvorado/commit/4638272b7b6d" title="console/frontend: switch to rolldown-vite">463827</a>).</p>
<p>Après ces changements, Akvorado ne nécessite plus « que » 225 dépendances. 😱</p>
<h1 id="prochaines-etapes">Prochaines étapes</h1>
<p>J’aimerais intégrer trois fonctionnalités dans la prochaine version
d’Akvorado :</p>
<ul>
<li>
<p>Ajouter des tableaux de bord Grafana pour compléter la pile d’observabilité.
  Voir le <a href="https://github.com/akvorado/akvorado/issues/1906" title="Ship some Grafana dashboards in docker compose setup">ticket #1906</a> pour les détails.</p>
</li>
<li>
<p>Intégrer le <a href="https://github.com/ovh/grafana-akvorado">plugin Grafana d’OVH</a> en fournissant une
  API stable pour de telles intégrations. La console web d’Akvorado resterait
  utile pour parcourir les résultats, mais si vous voulez créer et partager des
  tableaux de bord, vous devrez passer à Grafana. Voir le <a href="https://github.com/akvorado/akvorado/issues/1895" title="Integration of OVH Grafana plugin">ticket #1895</a>.</p>
</li>
<li>
<p>Déplacer une partie du travail actuellement fait dans ClickHouse
  (dictionnaires personnalisés, GeoIP et ajout d’informations liées aux IP) vers
  le service outlet. Cela devrait donner plus de flexibilité pour ajouter des
  fonctionnalités comme celle demandée dans le <a href="https://github.com/akvorado/akvorado/issues/1030" title="RegExp for custom dimension">ticket #1030</a>. Voir le <a href="https://github.com/akvorado/akvorado/issues/2006" title="Move back GeoIP and custom dictionaries into outlet">ticket #2006</a>.</p>
</li>
</ul>
<hr/>
<p>J’ai commencé à travailler sur la division de l’inlet il y a plus d’un an. J’ai
retrouvé de la motivation ces derniers mois, en partie grâce à <a href="https://code.claude.com/docs/en/overview" title="Claude Code overview">Claude Code</a>,
que j’ai utilisé comme <a href="https://fr.wikipedia.org/wiki/M%C3%A9thode_du_canard_en_plastique" title="Méthode du canard en plastique">canard en plastique</a>. Presque aucun code
produit n’a été conservé<sup id="fnref:exceptions"><a class="footnote-ref" href="#fn:exceptions">10</a></sup> : c’est comme un stagiaire qui n’apprend
pas. D’ailleurs, il est aussi flemmard. Pour la traduction en français, il a
fallu insister de nombreuses fois pour traduire l’intégralité de l’article. 🦆</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:bmp">
<p>De nombreuses tentatives ont été faites pour rendre le composant <abbr title="BGP Monitoring Protocol">BMP</abbr> à
la fois performant et non bloquant. Voir par exemple <a href="https://github.com/akvorado/akvorado/pull/254" title="inlet/bmp: avoid long lock times when flushing peers">PR #254</a>,
<a href="https://github.com/akvorado/akvorado/pull/255" title="“Lockless” RIB design">PR #255</a> et <a href="https://github.com/akvorado/akvorado/pull/278" title="inlet/bmp: new lockless design">PR #278</a>. Malgré ces efforts, ce composant restait
problématique pour la plupart des utilisateurs. Voir le 
<a href="https://github.com/akvorado/akvorado/issues/1461" title="BMP crash: cannot read BMP body">ticket #1461</a> comme exemple. <a class="footnote-backref" href="#fnref:bmp" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:features">
<p>Certaines fonctionnalités ont été déplacées vers ClickHouse pour
éviter le coût de traitement dans l’inlet. Voir par exemple <a href="https://github.com/akvorado/akvorado/pull/1059" title="Do geoip enrich in clickhouse instead of inlet">PR #1059</a>. <a class="footnote-backref" href="#fnref:features" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:size">
<p>C’est le <a href="https://github.com/akvorado/akvorado/commit/ac68c5970e2c" title="inlet: split inlet into new inlet and outlet">plus gros changement</a> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>git<span class="w"> </span>show<span class="w"> </span>--shortstat<span class="w"> </span>ac68c5970e2c<span class="w"> </span><span class="p">|</span><span class="w"> </span>tail<span class="w"> </span>-1
<span class="go">231 files changed, 6474 insertions(+), 3877 deletions(-)</span>
</code></pre></div>

<p><a class="footnote-backref" href="#fnref:size" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:broadcom">
<p>Broadcom est connu pour ses actions hostiles aux utilisateurs.
Regardez ce qui s’est passé avec VMWare. <a class="footnote-backref" href="#fnref:broadcom" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:debian">
<p>En tant que développeur Debian, je n’aime pas ces mécanismes qui
contournent le gestionnaire de paquets de la distribution. J’ai finalement
changé d’avis quand Go 1.25 a passé un mois dans la <a href="https://ftp-master.debian.org/new.html">file NEW de
Debian</a>, un mécanisme arbitraire que je n’aime pas du
tout. <a class="footnote-backref" href="#fnref:debian" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:channels">
<p>Aux débuts de Go, l’utilisation des canaux était fortement
encouragée. Sarama a été conçu pendant cette période. Quelques années plus
tard, une approche plus nuancée a émergé. Voir notamment « <a href="https://www.jtolio.com/2016/03/go-channels-are-bad-and-you-should-feel-bad/" title="Go channels are bad and you should feel bad">Go channels are
bad and you should feel bad</a> ». <a class="footnote-backref" href="#fnref:channels" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:worse">
<p>Cela devrait être étudié davantage, mais ma théorie est que le
paquet <code>intern</code> utilise des entiers 32 bits, tandis que <code>unique</code> utilise des
pointeurs 64 bits. Voir le commit <a href="https://github.com/akvorado/akvorado/commit/74e5aca41b71" title="outlet/routing: replace intern pool by unique.Hash">74e5ac</a>. <a class="footnote-backref" href="#fnref:worse" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:install">
<p>C’est aussi possible avec <code>npm</code>. Voir le commit <a href="https://github.com/akvorado/akvorado/commit/dab2f7875171" title="build: do not run install scripts with npm">dab2f7</a>. <a class="footnote-backref" href="#fnref:install" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:fast">
<p>Une alternative encore plus rapide est <a href="https://bun.sh/" title="Fast all-in-one JavaScript runtime">Bun</a>, mais elle est moins
disponible. <a class="footnote-backref" href="#fnref:fast" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:exceptions">
<p>Les exceptions sont une partie du code pour les avertissements,
le code pour replier la table des matières et une partie de la
documentation. <a class="footnote-backref" href="#fnref:exceptions" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Création de chemins d'AS infinis dans BGP</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2024-bgp-aspath-infini" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2024-bgp-aspath-infini#isso-thread" rel="replies" type="text/html"/>
    <updated>2024-07-28T20:32:36Z</updated>
    <id>http://www.luffy.cx/fr/blog/2024-bgp-aspath-infini.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>La combinaison des <a href="https://www.rfc-editor.org/rfc/rfc3065" title="RFC 3065: Autonomous System Confederations for BGP">confédérations <abbr title="Border Gateway Protocol">BGP</abbr></a> et du <a href="https://networklessons.com/bgp/mpls-layer-3-vpn-bgp-override" title="MPLS Layer 3 VPN BGP AS Override">remplacement d’<abbr title="Autonomous System">AS</abbr></a> peut potentiellement créer une boucle de routage <abbr title="Border Gateway Protocol">BGP</abbr>, résultant en un
chemin d’<abbr title="Autonomous System">AS</abbr> qui grossit indéfiniment.</p>
<p>La <strong>confédération <abbr title="Border Gateway Protocol">BGP</abbr></strong> est une technique utilisée pour réduire le nombre de
sessions <abbr title="Internal BGP">iBGP</abbr> et améliorer le passage à l’échelle dans les grands systèmes
autonomes (<abbr title="Autonomous System">AS</abbr>). Elle divise un <abbr title="Autonomous System">AS</abbr> en sous-<abbr title="Autonomous System">AS</abbr>. La plupart des règles <abbr title="External BGP">eBGP</abbr>
s’appliquent entre les sous-<abbr title="Autonomous System">AS</abbr>, sauf que le saut suivant, la <abbr title="Multi-Exit Discriminator">MED</abbr> et les
préférences locales restent inchangés. La longueur du chemin d’<abbr title="Autonomous System">AS</abbr> ignore les
contributions des sous-<abbr title="Autonomous System">AS</abbr> de la confédération. La confédération <abbr title="Border Gateway Protocol">BGP</abbr> est rarement
utilisée et la <a href="https://www.rfc-editor.org/rfc/rfc4456" title="RFC 4456: BGP Route Reflection: An Alternative to Full Mesh Internal BGP (IBGP)">réflexion des routes <abbr title="Border Gateway Protocol">BGP</abbr></a> lui est généralement
préférée.</p>
<p>Le <strong>remplacement d’<abbr title="Autonomous System">AS</abbr></strong> est une fonctionnalité qui permet à un routeur de
remplacer l’<abbr title="Autonomous System Number">ASN</abbr> d’un voisin dans le chemin d’<abbr title="Autonomous System">AS</abbr> des routes <abbr title="Border Gateway Protocol">BGP</abbr> sortantes par le
sien. C’est utile lorsque deux systèmes autonomes distincts partagent le même
<abbr title="Autonomous System Number">ASN</abbr>. Cependant, cela interfère avec le mécanisme de prévention des boucles de
<abbr title="Border Gateway Protocol">BGP</abbr> et doit être utilisé avec prudence. Une alternative plus sûre est la
directive <code>allowas-in</code><sup id="fnref:allowconfedas-in"><a class="footnote-ref" href="#fn:allowconfedas-in">1</a></sup>.</p>
<p>Dans l’exemple ci-dessous, nous avons quatre routeurs dans une seule
confédération, chacun dans son propre sous-<abbr title="Autonomous System">AS</abbr>. <code>R0</code> est à l’origine du préfixe
<code>2001:db8::1/128</code>. <code>R1</code>, <code>R2</code>, et <code>R3</code> transmettent ce préfixe au routeur
suivant dans la boucle.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 371px"><span class="lf-media-inner" style="padding-bottom: 108.086%"><img alt="Boucle de routage BGP impliquant 4 routeurs : R0 est à l'origine du préfixe,&#10;R1, R2 et R3 le font tourner en boucle en utilisant next-hop-self et&#10;as-override" src="https://media-bernat-ch.b-cdn.net/images/bgp-loop.daef0f4df09813.svg" width="371" height="401" class="lf-media"/></span></div><figcaption>Boucle de routage BGP utilisant une confédération</figcaption></figure>
<p>Les configurations des routeurs sont disponibles dans un <a href="https://github.com/vincentbernat/network-lab/tree/master/lab-bgp-infinite-aspath">dépôt Git</a>. Ils tournent sous Cisco IOS XR. <code>R2</code> utilise la configuration <abbr title="Border Gateway Protocol">BGP</abbr>
suivante :</p>
<div class="language-ios codehilite"><pre><span/><code><span class="nf">router</span> bgp <span class="m">64502</span>
 <span class="nf">bgp</span> confederation peers
  <span class="m">64500</span>
  <span class="m">64501</span>
  <span class="m">64503</span>
<span class="c c-Singleline"> !</span>
 <span class="nf">bgp</span> confederation identifier <span class="m">64496</span>
 <span class="nf">bgp</span> router-id <span class="m">1.0.0.2</span>
 <span class="nf">address-family</span> ipv6 unicast
<span class="c c-Singleline"> !</span>
 <span class="nf">neighbor</span> <span class="m">2001:db8::2:0</span>
  <span class="nf">remote-as</span> <span class="m">64501</span>
  <span class="nf">description</span> R1
  <span class="nf">address-family</span> ipv6 unicast
<span class="c c-Singleline">  !</span>
<span class="c c-Singleline"> !</span>
 <span class="nf">neighbor</span> <span class="m">2001:db8::3:1</span>
  <span class="nf">remote-as</span> <span class="m">64503</span>
  <span class="nf">advertisement-interval</span> <span class="m">0</span>
  <span class="nf">description</span> R3
<span class="hll">  <span class="nf">address-family</span> ipv6 unicast
</span><span class="hll">   <span class="nf">next-hop-self</span>
</span><span class="hll">   <span class="nf">as-override</span>
</span><span class="c c-Singleline">  !</span>
<span class="c c-Singleline"> !</span>
<span class="c c-Singleline">!</span>
</code></pre></div>

<p>La session avec <code>R3</code> utilise à la fois les directives <code>as-override</code> et
<code>next-hop-self</code>. Cette dernière est seulement nécessaire pour rendre le préfixe
annoncé valide, car il n’y a pas d’<abbr title="Internal Gateway Protocol">IGP</abbr> dans cet exemple<sup id="fnref:igp"><a class="footnote-ref" href="#fn:igp">2</a></sup>.</p>
<p>Voici la séquence d’événements menant à un chemin d’<abbr title="Autonomous System">AS</abbr> infini :</p>
<ol>
<li>
<p><code>R0</code> envoie le préfixe à <code>R1</code> avec le chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64500)</code><sup id="fnref:confed"><a class="footnote-ref" href="#fn:confed">3</a></sup>.</p>
</li>
<li>
<p><code>R1</code> le sélectionne comme meilleur chemin et le transmet à <code>R2</code> avec le
   chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64501 64500)</code>.</p>
</li>
<li>
<p><code>R2</code> le sélectionne comme meilleur chemin et le transmet à <code>R3</code> avec le
   chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64502 64501 64500)</code>.</p>
</li>
<li>
<p><code>R3</code> le sélectionne comme meilleur chemin. Il devrait le transmettre à <code>R1</code>
   avec le chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64503 64502 64501 64500)</code>, mais en raison du
   remplacement d’<abbr title="Autonomous System">AS</abbr>, il substitue l’<abbr title="Autonomous System Number">ASN</abbr> de <code>R1</code> par le sien, le transmettant
   avec le chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64503 64502 64503 64500)</code>.</p>
</li>
<li>
<p><code>R1</code> accepte le préfixe, car son propre <abbr title="Autonomous System Number">ASN</abbr> n’est pas dans le chemin d’<abbr title="Autonomous System">AS</abbr>. Il
   compare ce nouveau préfixe avec celui de <code>R0</code>. <code>(64500)</code> et <code>(64503 64502
   64503 64500)</code> ont la même longueur car les sous-<abbr title="Autonomous System">AS</abbr> de la confédération ne
   contribuent pas à la longueur du chemin d’<abbr title="Autonomous System">AS</abbr>. <strong>Le premier critère pour
   départager les deux routes est alors l’ID du routeur</strong>. L’ID du routeur de
   <code>R0</code> (<code>1.0.0.4</code>) est plus élevé que celui de <code>R3</code> (<code>1.0.0.3</code>). Le nouveau
   préfixe devient le meilleur chemin et est transmis à <code>R2</code> avec le chemin d’<abbr title="Autonomous System">AS</abbr>
   <code>(64501 64503 64501 64503 64500)</code>.</p>
</li>
<li>
<p><code>R2</code> reçoit le nouveau préfixe, remplaçant l’ancien. Il le sélectionne comme
   meilleur chemin et le transmet à <code>R3</code> avec le chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64502 64501 64502
   64501 64502 64500)</code>.</p>
</li>
<li>
<p><code>R3</code> reçoit le nouveau préfixe, remplaçant l’ancien. Il le sélectionne comme
   meilleur chemin et le transmet à <code>R0</code> avec le chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64503 64502 64503
   64502 64503 64502 64500)</code>.</p>
</li>
<li>
<p><code>R1</code> reçoit le nouveau préfixe, remplaçant l’ancien. Encore une fois, il est
   en concurrence avec le préfixe de <code>R0</code>, et encore une fois le nouveau préfixe
   gagne en raison de l’ID de routeur inférieur. Le préfixe est transmis à <code>R2</code>
   avec le chemin d’<abbr title="Autonomous System">AS</abbr> <code>(64501 64503 64501 64503 64501 64503 64501 64500)</code>.</p>
</li>
</ol>
<p>Quelques itérations plus tard, <code>R1</code> voit le préfixe en boucle comme suit<sup id="fnref:pacing"><a class="footnote-ref" href="#fn:pacing">4</a></sup> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">RP/0/RP0/CPU0:R1#</span>show<span class="w"> </span>bgp<span class="w"> </span>ipv6<span class="w"> </span>u<span class="w"> </span><span class="m">2001</span>:db8::1/128<span class="w"> </span>bestpath-compare
<span class="go">BGP routing table entry for 2001:db8::1/128</span>
<span class="go">Last Modified: Jul 28 10:23:05.560 for 00:00:00</span>
<span class="go">Paths: (2 available, best #2)</span>
<span class="go">  Path #1: Received by speaker 0</span>
<span class="go">  Not advertised to any peer</span>
<span class="go">  (64500)</span>
<span class="go">    2001:db8::1:0 from 2001:db8::1:0 (1.0.0.4), if-handle 0x00000000</span>
<span class="go">      Origin IGP, metric 0, localpref 100, valid, confed-external</span>
<span class="go">      Received Path ID 0, Local Path ID 0, version 0</span>
<span class="hll"><span class="go">      Higher router ID than best path (path #2)</span>
</span><span class="go">  Path #2: Received by speaker 0</span>
<span class="go">  Advertised IPv6 Unicast paths to peers (in unique update groups):</span>
<span class="go">    2001:db8::2:1</span>
<span class="go">  (64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64503 64502 64500)</span>
<span class="go">    2001:db8::4:0 from 2001:db8::4:0 (1.0.0.3), if-handle 0x00000000</span>
<span class="go">      Origin IGP, metric 0, localpref 100, valid, confed-external, best, group-best</span>
<span class="go">      Received Path ID 0, Local Path ID 1, version 37</span>
<span class="go">      best of AS 64503, Overall best</span>
</code></pre></div>

<p>Il n’y a pas de limite supérieure pour un chemin d’<abbr title="Autonomous System">AS</abbr>, mais les messages <abbr title="Border Gateway Protocol">BGP</abbr> ont
des limites de taille (4096 octets selon <a href="https://www.rfc-editor.org/rfc/rfc4271" title="RFC 4271: A Border Gateway Protocol 4 (BGP-4)">RFC 4271</a> ou 65535 octets
selon <a href="https://www.rfc-editor.org/rfc/rfc8654" title="RFC 8654: Extended Message Support for BGP">RFC 8654</a>). Passé un certain nombre d’itérations, les mises à
jour <abbr title="Border Gateway Protocol">BGP</abbr> ne peuvent plus être générées. Sur Cisco IOS XR, le processus <abbr title="Border Gateway Protocol">BGP</abbr>
plante bien avant d’atteindre cette limite<sup id="fnref:ddts"><a class="footnote-ref" href="#fn:ddts">5</a></sup>.</p>
<hr/>
<p>Les principales leçons de cette histoire sont de n’utiliser en aucune
circonstance les confédérations <abbr title="Border Gateway Protocol">BGP</abbr> et d’être prudent avec les fonctionnalités
qui affaiblissent la détection de boucles de routage <abbr title="Border Gateway Protocol">BGP</abbr>. 🌀</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2026)</p>
<p>Cet article a été brièvement discuté sur
<a href="https://news.ycombinator.com/item?id=41101700">Hacker News</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:allowconfedas-in">
<p>Lors de l’utilisation des confédérations <abbr title="Border Gateway Protocol">BGP</abbr> avec Cisco IOS
XR, utilisez <code>allowconfedas-in</code> à la place. C’est disponible depuis <a href="https://www.cisco.com/c/en/us/td/docs/iosxr/ncs5500/bgp/711x/configuration/guide/b-bgp-cg-ncs5500-711x/implementing-bgp.html#feat-3439-overview-bgp-allowas-in-9k">IOS XR 7.11</a>. <a class="footnote-backref" href="#fnref:allowconfedas-in" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:igp">
<p>L’utilisation des confédérations <abbr title="Border Gateway Protocol">BGP</abbr> est déjà une mauvaise idée. Si en
plus vous n’utilisez pas le même <abbr title="Internal Gateway Protocol">IGP</abbr> pour tous les sous-<abbr title="Autonomous System">AS</abbr>, vous vous
exposez à bien des problèmes ! Toutefois, le scénario exposé ici est aussi
possible avec un <abbr title="Internal Gateway Protocol">IGP</abbr>. <a class="footnote-backref" href="#fnref:igp" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:confed">
<p>Lorsqu’un segment de chemin d’<abbr title="Autonomous System">AS</abbr> est composé d’<abbr title="Autonomous System Number">ASN</abbr> d’une
confédération, il est représenté entre parenthèses. <a class="footnote-backref" href="#fnref:confed" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:pacing">
<p>Par défaut, IOS XR ralentit les mises à jour <abbr title="External BGP">eBGP</abbr>. Ceci est contrôlé
par la directive <code>advertisement-interval</code>. Sa valeur par défaut est de 30
secondes pour les sessions <abbr title="External BGP">eBGP</abbr> (y compris dans les confédérations). <code>R1</code> et
<code>R2</code> passent cette valeur à 0, tandis que <code>R3</code> la définit à 2 secondes. Cela
donne un peu de temps pour observer la croissance du chemin d’<abbr title="Autonomous System">AS</abbr>. <a class="footnote-backref" href="#fnref:pacing" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:ddts">
<p>Il s’agit de la <a href="https://sec.cloudapps.cisco.com/security/center/content/CiscoSecurityAdvisory/cisco-sa-iosxr-bgp-dos-O7stePhX" title="Cisco IOS XR Software Border Gateway Protocol Confederation Denial of Service Vulnerability">CVE-2025-20115</a>. Cela ne se produit qu’avec
<code>as-override</code> sur un chemin d’<abbr title="Autonomous System">AS</abbr> contenant un segment <code>AS_CONFED_SEQUENCE</code>
trop long. C’est corrigé dans les versions 24.2.21 et 24.3.1. <a class="footnote-backref" href="#fnref:ddts" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
</feed>