<?xml version='1.0' encoding='UTF-8'?>
<?xml-stylesheet type="text/xsl" href="/fr/atom.xslt"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr" xml:base="https://vincent.bernat.ch">
  <title>Vincent Bernat</title>
  <link href="https://vincent.bernat.ch/fr/blog/atom.xml" rel="self"/>
  <link href="https://vincent.bernat.ch/fr" rel="alternate"/>
  <id>http://www.luffy.cx/fr/blog/atom.xml/</id>
  <updated>2026-05-12T06:08:17Z</updated>

  <entry>
    <title type="html">CSS &amp; rythme vertical pour le texte, les images et les tableaux</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-css-rythme-vertical" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-css-rythme-vertical#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-04-22T19:48:10Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-css-rythme-vertical.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>Le rythme vertical structure les lignes selon un espacement régulier de haut en
bas. Il permet de guider l’œil de manière fluide et prévisible. Grâce à l’unité
CSS <code>rlh</code>, ce rythme est simple à mettre en place pour le texte<sup id="fnref-pawel"><a class="footnote-ref" href="#fn-pawel">1</a></sup>. Mais
les illustrations et les tableaux peuvent perturber la mise en page. Le
typographe amateur en moi aspire à suivre la sagesse de Bringhurst :</p>
<blockquote>
<p>Les titres, intertitres, citations, notes de bas de page, illustrations,
légendes et autres intrusions dans le texte créent des syncopes et des
variations par rapport au rythme de base d’un interlignage régulier. Ces
variations peuvent et doivent animer la page, mais le texte principal doit
également reprendre, après chaque variation, précisément la mesure et la
phase.</p>
<p>― <em>Robert Bringhurst</em>, <a href="https://en.wikipedia.org/wiki/The_Elements_of_Typographic_Style" title="The Elements of Typographic Style sur Wikipédia">The Elements of Typographic Style</a></p>
</blockquote>
<h1 id="pour-le-texte">Pour le texte</h1>
<p>Trois facteurs régissent le rythme vertical : la <strong>taille de police</strong>, la
<strong>hauteur de ligne</strong> et la <strong>marge ou le remplissage</strong>. Posons la base avec une
police de 18 pixels et une hauteur de ligne de 1,5 :</p>
<div class="language-css codehilite"><pre><span/><span class="nt">html</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">112.5</span><span class="kt">%</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mf">1.5</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h1</span><span class="o">,</span><span class="w"> </span><span class="nt">h2</span><span class="o">,</span><span class="w"> </span><span class="nt">h3</span><span class="o">,</span><span class="w"> </span><span class="nt">h4</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mi">100</span><span class="kt">%</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">html</span><span class="o">,</span><span class="w"> </span><span class="nt">body</span><span class="o">,</span>
<span class="nt">h1</span><span class="o">,</span><span class="w"> </span><span class="nt">h2</span><span class="o">,</span><span class="w"> </span><span class="nt">h3</span><span class="o">,</span><span class="w"> </span><span class="nt">h4</span><span class="o">,</span>
<span class="nt">p</span><span class="o">,</span><span class="w"> </span><span class="nt">blockquote</span><span class="o">,</span>
<span class="nt">dl</span><span class="o">,</span><span class="w"> </span><span class="nt">dt</span><span class="o">,</span><span class="w"> </span><span class="nt">dd</span><span class="o">,</span><span class="w"> </span><span class="nt">ol</span><span class="o">,</span><span class="w"> </span><span class="nt">ul</span><span class="o">,</span><span class="w"> </span><span class="nt">li</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">margin</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">  </span><span class="k">padding</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>


<p>Le <a href="https://www.w3.org/TR/css-values-4/" title="CSS Values and Units Module Level 4, W3C Working Draft">CSS Values and Units Module Level 4</a> définit l’unité <code>rlh</code>, égale à la
hauteur de ligne de l’élément racine. Tous les navigateurs la gèrent <a href="https://webstatus.dev/features/rlh" title="L'unité rlh sur Web Platform Status">depuis
2023</a><sup id="fnref-postcss"><a class="footnote-ref" href="#fn-postcss">2</a></sup>. Utilisez-la pour insérer des espaces verticaux ou
corriger la hauteur de ligne quand la taille de police change<sup id="fnref-calc"><a class="footnote-ref" href="#fn-calc">3</a></sup> :</p>
<div class="language-css codehilite"><pre><span/><span class="nt">h1</span><span class="o">,</span><span class="w"> </span><span class="nt">h2</span><span class="o">,</span><span class="w"> </span><span class="nt">h3</span><span class="o">,</span><span class="w"> </span><span class="nt">h4</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">margin-top</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="n">rlh</span><span class="p">;</span>
<span class="w">  </span><span class="k">margin-bottom</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h1</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">2.4</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h2</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">1.5</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">h3</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">1.2</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">p</span><span class="o">,</span><span class="w"> </span><span class="nt">blockquote</span><span class="o">,</span><span class="w"> </span><span class="nt">pre</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">margin-top</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">aside</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">font-size</span><span class="p">:</span><span class="w"> </span><span class="mf">0.875</span><span class="kt">rem</span><span class="p">;</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>


<p>Vérifions le résultat en superposant une grille<sup id="fnref-grid"><a class="footnote-ref" href="#fn-grid">4</a></sup> au-dessus du texte :</p>
<figure><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 104.853%"><img alt="Capture d'écran de mon site avec une grille en superposition et chaque ligne&#10;de texte alignée sur la grille" src="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/text@1x.17b2e8512acd85.png" srcset="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/text@1x.17b2e8512acd85.png 855w,https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/text@2x.93207136439be4.png 1360w" sizes="auto, (max-width: 680px) 100vw, 680px" width="680" height="713" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqgAAALJAQMAAABC3kPgAAAABlBMVEX59O0AAAAOW55mAAAAUklEQVR42u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+DHvhgABu8TltwAAAABJRU5ErkJggg==)"/></span></div><figcaption>L'unité CSS <code>rlh</code> fonctionne bien pour régler les espaces verticaux du texte. Vous pouvez afficher la grille avec <kbd>Ctrl</kbd>+<kbd>Shift</kbd>+<kbd>G</kbd>.</figcaption></figure>
<p>Si un sous-élément utilise une police aux métriques intrinsèques plus hautes, il
peut étirer la boîte de la ligne au-delà de la hauteur configurée<sup id="fnref-line-height"><a class="footnote-ref" href="#fn-line-height">5</a></sup>.
Une astuce consiste à ramener la hauteur de ligne à 1. Les glyphes débordent
mais ne poussent pas la ligne plus haut.</p>
<div class="language-css codehilite"><pre><span/><span class="nt">code</span><span class="o">,</span><span class="w"> </span><span class="nt">kbd</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">line-height</span><span class="p">:</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>


<h1 id="pour-les-images-adaptatives">Pour les images adaptatives</h1>
<p>Les images adaptatives, dont la taille s’ajuste à la largeur de l’affichage,
sont difficiles à aligner sur la grille car leur hauteur nous est inconnue. Le
<a href="https://www.w3.org/TR/css-rhythm-1/" title="CSS Rhythmic Sizing Module Level 1">CSS Rhythmic Sizing Module Level 1</a> introduit la propriété <code>block-step</code> pour
ajuster la hauteur d’un élément à un multiple d’une unité de pas. Cependant, la
plupart des navigateurs ne la gèrent pas encore.</p>
<p>Avec JavaScript, nous pouvons ajouter un remplissage autour de l’image pour
qu’elle ne perturbe pas le rythme vertical :</p>
<div class="language-javascript codehilite"><pre><span/><span class="kd">const</span><span class="w"> </span><span class="nx">targets</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">document</span><span class="p">.</span><span class="nx">querySelectorAll</span><span class="p">(</span><span class="s2">".lf-media-outer"</span><span class="p">);</span>
<span class="kd">const</span><span class="w"> </span><span class="nx">adjust</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">(</span><span class="nx">el</span><span class="p">,</span><span class="w"> </span><span class="nx">height</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">rlh</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">parseFloat</span><span class="p">(</span><span class="nx">getComputedStyle</span><span class="p">(</span><span class="nb">document</span><span class="p">.</span><span class="nx">documentElement</span><span class="p">).</span><span class="nx">lineHeight</span><span class="p">);</span>
<span class="w">  </span><span class="kd">const</span><span class="w"> </span><span class="nx">padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nb">Math</span><span class="p">.</span><span class="nx">ceil</span><span class="p">(</span><span class="nx">height</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="nx">rlh</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">rlh</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="nx">height</span><span class="p">;</span>
<span class="w">  </span><span class="nx">el</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">padding</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="sb">`</span><span class="si">${</span><span class="nx">padding</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mf">2</span><span class="si">}</span><span class="sb">px 0`</span><span class="p">;</span>
<span class="p">};</span>

<span class="nx">targets</span><span class="p">.</span><span class="nx">forEach</span><span class="p">((</span><span class="nx">el</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="nx">adjust</span><span class="p">(</span><span class="nx">el</span><span class="p">,</span><span class="w"> </span><span class="nx">el</span><span class="p">.</span><span class="nx">clientHeight</span><span class="p">));</span>
</pre></div>


<figure><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 104.853%"><img alt="Capture d'écran de mon site avec une grille en superposition et une image qui&#10;ne rompt pas le rythme vertical. Un remplissage supplémentaire est visible avant&#10;et après l'image. La hauteur de l'image avec son remplissage est de&#10;216." src="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/images@1x.08d84fe73e020f.png" srcset="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/images@1x.08d84fe73e020f.png 855w,https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/images@2x.5e994bde851c27.png 1360w" sizes="auto, (max-width: 680px) 100vw, 680px" width="680" height="713" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAqgAAALJAQMAAABC3kPgAAAABlBMVEX38usAAACeelVqAAAAUklEQVR42u3BAQ0AAADCoPdPbQ43oAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+DHvhgABu8TltwAAAABJRU5ErkJggg==)"/></span></div><figcaption>L'image s'aligne sur la grille grâce au remplissage calculé par JavaScript. 216 est divisible par 27, notre hauteur de ligne dans cet exemple.</figcaption></figure>
<p>Comme l’image est adaptative, sa hauteur peut varier. Il faut appeler la
fonction <code>adjust()</code> dans un observateur :</p>
<div class="language-javascript codehilite"><pre><span/><span class="kd">const</span><span class="w"> </span><span class="nx">ro</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="ow">new</span><span class="w"> </span><span class="nx">ResizeObserver</span><span class="p">((</span><span class="nx">entries</span><span class="p">)</span><span class="w"> </span><span class="p">=&gt;</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </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">entry</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">entries</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kd">const</span><span class="w"> </span><span class="nx">height</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="nx">entry</span><span class="p">.</span><span class="nx">contentBoxSize</span><span class="p">[</span><span class="mf">0</span><span class="p">].</span><span class="nx">blockSize</span><span class="p">;</span>
<span class="w">    </span><span class="nx">adjust</span><span class="p">(</span><span class="nx">entry</span><span class="p">.</span><span class="nx">target</span><span class="p">,</span><span class="w"> </span><span class="nx">height</span><span class="p">);</span>
<span class="w">  </span><span class="p">}</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">target</span><span class="w"> </span><span class="k">of</span><span class="w"> </span><span class="nx">targets</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="nx">ro</span><span class="p">.</span><span class="nx">observe</span><span class="p">(</span><span class="nx">target</span><span class="p">);</span>
<span class="p">}</span>
</pre></div>


<h1 id="pour-les-tableaux">Pour les tableaux</h1>
<p>Les cellules d’un tableau pourraient fixer leur hauteur à <code>1rlh</code> mais
paraîtraient à l’étroit. Passer à <code>2rlh</code> gaspille trop d’espace. À la place,
nous utilisons l’<a href="https://markboulton.co.uk/journal/incremental-leading/" title="Incremental leading">interligne incrémental</a> : une ligne sur
cinq est alignée sur la grille.</p>
<div class="language-css codehilite"><pre><span/><span class="nt">table</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">border-spacing</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="kt">px</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">  </span><span class="k">border-collapse</span><span class="p">:</span><span class="w"> </span><span class="kc">separate</span><span class="p">;</span>
<span class="w">  </span><span class="err">th</span><span class="w"> </span><span class="err">{</span>
<span class="w">    </span><span class="k">padding</span><span class="p">:</span><span class="w"> </span><span class="mf">0.4</span><span class="n">rlh</span><span class="w"> </span><span class="mi">1</span><span class="kt">em</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span>
<span class="w">  </span><span class="nt">td</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">padding</span><span class="p">:</span><span class="w"> </span><span class="mf">0.2</span><span class="n">rlh</span><span class="w"> </span><span class="mf">0.5</span><span class="kt">em</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span>
<span class="err">}</span>
</pre></div>


<p>Pour aligner les éléments après le tableau, il faut ajouter un peu de
remplissage. Nous pouvons soit réutiliser le code JavaScript des images, soit
utiliser quelques lignes de CSS qui comptent les lignes régulières et calculent
le remplissage vertical manquant :</p>
<div class="language-css codehilite"><pre><span/><span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w">   </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.2</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">1</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.8</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">2</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.4</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">3</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="w"> </span><span class="p">}</span>
<span class="nt">table</span><span class="p">:</span><span class="nd">has</span><span class="o">(</span><span class="nt">tbody</span><span class="w"> </span><span class="nt">tr</span><span class="p">:</span><span class="nd">nth-child</span><span class="o">(</span><span class="nt">5n</span><span class="o">+</span><span class="nt">4</span><span class="o">)</span><span class="p">:</span><span class="nd">last-child</span><span class="o">)</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="k">padding-bottom</span><span class="p">:</span><span class="w"> </span><span class="mf">0.6</span><span class="n">rlh</span><span class="p">;</span><span class="w"> </span><span class="p">}</span>
</pre></div>


<p>Une cellule d’en-tête a deux fois le remplissage d’une cellule régulière. Avec
deux lignes régulières, le remplissage total vaut 2×2×0,2+2×0,4=1,6. Il faut
ajouter <code>0.4rlh</code> pour atteindre <code>2rlh</code> de remplissage vertical supplémentaire
autour du tableau.</p>
<figure><div class="lf-media-outer" style="width: 680px"><span class="lf-media-inner" style="padding-bottom: 86.765%"><img alt="Capture d'écran de mon site avec une grille en superposition et un tableau&#10;suivant le rythme vertical. Un remplissage supplémentaire est visible après le&#10;tableau. La hauteur du tableau avec son remplissage est de&#10;405." src="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/tables@1x.56e678195427d0.png" srcset="https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/tables@1x.56e678195427d0.png 855w,https://d2pzklc15kok91.cloudfront.net/images/vertical-rhythm/tables@2x.19b9c475806bf5.png 1360w" sizes="auto, (max-width: 680px) 100vw, 680px" width="680" height="590" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEQAAAA7AQMAAAD4qb7lAAAABlBMVEWx3O0AAAAc4XKiAAAAD0lEQVR42mNgGAWjgFoAAAJOAAFGvjzoAAAAAElFTkSuQmCC)"/></span></div><figcaption>Une ligne sur cinq est alignée sur la grille. Un remplissage supplémentaire est ajouté après le tableau pour ne pas rompre le rythme vertical. 405 est divisible par 27, notre hauteur de ligne dans cet exemple.</figcaption></figure>
<hr/>
<p>Rien de tout cela n’est indispensable, mais une fois l’œil entraîné, il devient
difficile de ne plus le remarquer. En attendant que les navigateurs implémentent
le <a href="https://www.w3.org/TR/css-rhythm-1/" title="CSS Rhythmic Sizing Module Level 1">CSS Rhythmic Sizing</a>, un mélange de de
bidouilles CSS et d’un peu de JavaScript suffit. Le texte principal reprend
désormais après chaque intrusion « précisément la mesure et la phase ». 🎼</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-pawel">
<p>Voir « <a href="https://pawelgrzybek.com/vertical-rhythm-using-css-lh-and-rlh-units/" title="Vertical rhythm using CSS lh and rlh units">Vertical rhythm using CSS <code>lh</code> and <code>rlh</code> units</a> » par
Paweł Grzybek. <a class="footnote-backref" href="#fnref-pawel" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-postcss">
<p>Pour une meilleure compatibilité, remplacez <code>2rlh</code> par
<code>calc(var(--line-height) * 2rem)</code> et définissez la propriété <code>--line-height</code>
dans la pseudo-classe <code>:root</code>. J’ai écrit un <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/95f29793faad50a94fe4199a1774d59d409d3755/extensions/css.js#L69-L79">petit greffon
PostCSS</a> à cet effet. <a class="footnote-backref" href="#fnref-postcss" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn-calc">
<p>Il aurait été plus élégant de calculer la hauteur de ligne avec
<code>calc(round(up, calc(2.4rem / 1rlh), 0) * 1rlh)</code>. Cependant, l’arithmétique
typée n’est <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=1264520" title="Support mixed number, unit value, and percent for calc() parser including type checking">pas encore implémentée par Firefox</a>. De plus, les navigateurs ne gèrent <code>round()</code> que <a href="https://webstatus.dev/features/round-mod-rem" title="La fonction round() sur Web Platform Status">depuis 2024</a>. J’ai donc codé un <a href="https://github.com/vincentbernat/vincent.bernat.ch/blob/95f29793faad50a94fe4199a1774d59d409d3755/extensions/css.js#L31-L67">autre greffon PostCSS</a> pour cela. <a class="footnote-backref" href="#fnref-calc" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn-grid">
<p>Ce code CSS définit une grille qui suit la hauteur de ligne :</p>
<div class="language-css codehilite"><pre><span/><span class="nt">body</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">position</span><span class="p">:</span><span class="w"> </span><span class="kc">relative</span><span class="p">;</span>
<span class="p">}</span>
<span class="nt">body</span><span class="p">::</span><span class="nd">after</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="k">content</span><span class="p">:</span><span class="w"> </span><span class="s2">""</span><span class="p">;</span>
<span class="w">  </span><span class="k">position</span><span class="p">:</span><span class="w"> </span><span class="kc">absolute</span><span class="p">;</span>
<span class="w">  </span><span class="k">inset</span><span class="p">:</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">  </span><span class="k">z-index</span><span class="p">:</span><span class="w"> </span><span class="mi">9999</span><span class="p">;</span>
<span class="w">  </span><span class="k">background</span><span class="p">:</span><span class="w"> </span><span class="nb">linear-gradient</span><span class="p">(</span><span class="mi">180</span><span class="kt">deg</span><span class="p">,</span><span class="w"> </span><span class="mh">#c8e1ff</span><span class="mi">99</span><span class="w"> </span><span class="mi">1</span><span class="kt">px</span><span class="p">,</span><span class="w"> </span><span class="kc">transparent</span><span class="w"> </span><span class="mi">1</span><span class="kt">px</span><span class="p">);</span>
<span class="w">  </span><span class="k">background-size</span><span class="p">:</span><span class="w"> </span><span class="mi">20</span><span class="kt">px</span><span class="w"> </span><span class="mi">1</span><span class="n">rlh</span><span class="p">;</span>
<span class="w">  </span><span class="k">pointer-events</span><span class="p">:</span><span class="w"> </span><span class="kc">none</span><span class="p">;</span>
<span class="p">}</span>
</pre></div>


<p><a class="footnote-backref" href="#fnref-grid" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn-line-height">
<p>Voir « <a href="https://iamvdo.me/en/blog/css-font-metrics-line-height-and-vertical-align">Deep dive CSS: font metrics, line-height and vertical-align</a> » par Vincent De Oliveira. <a class="footnote-backref" href="#fnref-line-height" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Calculer « 1/(40rods/hogshead) to L/100km » depuis l'invite Zsh</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-calculatrice-zsh" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-calculatrice-zsh#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-03-22T13:37:09Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-calculatrice-zsh.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>J’ai souvent besoin d’un calcul rapide ou de convertir une unité en une autre.
Plutôt que de recourir à un outil séparé, quelques lignes de configuration <em>Zsh</em>
transforment <code>=</code> en calculatrice. En tapant <code>= 660km / (2/3)c * 2 -&gt; ms</code>,
j’obtiens <code>6.60457 ms</code><sup id="fnref-marseille"><a class="footnote-ref" href="#fn-marseille">1</a></sup> sans quitter mon terminal, grâce à l’éditeur
de ligne de Zsh.</p>
<div class="toc">
<ul>
<li><a href="#lalias">L’alias =</a></li>
<li><a href="#le-probleme-des-caracteres-speciaux">Le problème des caractères spéciaux</a><ul>
<li><a href="#proteger-lexpression-automatiquement-avec-zle">Protéger l’expression automatiquement avec ZLE</a></li>
<li><a href="#conserver-lexpression-originale-dans-lhistorique">Conserver l’expression originale dans l’historique</a></li>
</ul>
</li>
</ul>
</div>
<h1 id="lalias">L’alias <code>=</code></h1>
<p>L’idée de base est simple : définir <code>=</code> comme alias vers une calculatrice en
ligne de commande. Je préfère <a href="https://numbat.dev/" title="Numbat: a statically typed programming language for scientific computations">Numbat</a>, une calculatrice scientifique gérant
les conversions d’unités. <a href="https://qalculate.github.io/" title="Qalculate!: the ultimate desktop calculator">Qalculate</a> est une autre option<sup id="fnref-qalc"><a class="footnote-ref" href="#fn-qalc">2</a></sup>. Si aucun
des deux n’est disponible, nous nous rabattons sur le module <em>zcalc</em> intégré à
Zsh.</p>
<p>Comme la commande <code>alias</code> utilise <code>=</code> comme séparateur entre nom et valeur, nous
modifions directement le tableau associatif <code>aliases</code> :</p>
<div class="language-bash codehilite"><pre><span/><span class="k">if</span><span class="w"> </span><span class="o">((</span><span class="w"> </span>$+commands<span class="o">[</span>numbat<span class="o">]</span><span class="w"> </span><span class="o">))</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">  </span>aliases<span class="o">[=]=</span><span class="s1">'numbat -e'</span>
<span class="k">elif</span><span class="w"> </span><span class="o">((</span><span class="w"> </span>$+commands<span class="o">[</span>qalc<span class="o">]</span><span class="w"> </span><span class="o">))</span><span class="p">;</span><span class="w"> </span><span class="k">then</span>
<span class="w">  </span>aliases<span class="o">[=]=</span><span class="s1">'qalc'</span>
<span class="k">else</span>
<span class="w">  </span>autoload<span class="w"> </span>-Uz<span class="w"> </span>zcalc
<span class="w">  </span>aliases<span class="o">[=]=</span><span class="s1">'zcalc -f -e'</span>
<span class="k">fi</span>
</pre></div>


<p>Ainsi, <code>= 847/11</code> devient <code>numbat -e 847/11</code>.</p>
<h1 id="le-probleme-des-caracteres-speciaux">Le problème des caractères spéciaux</h1>
<p>Le premier problème apparaît vite. Taper <code>= 5 * 3</code> échoue : Zsh interprète <code>*</code>
comme un motif pour fichiers avant de le transmettre à la calculatrice. Le même
souci se pose avec d’autres caractères spéciaux pour Zsh, comme <code>&gt;</code> ou <code>|</code>. Il
faut protéger l’expression :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'5 * 3'</span>
<span class="go">15</span>
</pre></div>


<p>Nous corrigeons cela en nous appuyant sur l’éditeur de ligne de Zsh pour <strong>placer
l’expression entre guillemets</strong> avant son exécution.</p>
<h2 id="proteger-lexpression-automatiquement-avec-zle">Protéger l’expression automatiquement avec <abbr title="Zsh Line Editor">ZLE</abbr></h2>
<p>Zsh appelle le widget <code>line-finish</code> avant de valider une commande. Nous y
attachons une fonction qui détecte le préfixe <code>=</code> et protège l’expression :</p>
<div class="language-bash codehilite"><pre><span/>_vbe_calc_quote<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w">  </span><span class="k">case</span><span class="w"> </span><span class="nv">$BUFFER</span><span class="w"> </span><span class="k">in</span>
<span class="w">    </span><span class="s2">"="</span>*<span class="o">)</span>
<span class="w">      </span><span class="nb">typeset</span><span class="w"> </span>-g<span class="w"> </span><span class="nv">_vbe_calc_expr</span><span class="o">=</span><span class="nv">$BUFFER</span><span class="w"> </span><span class="c1"># not used yet</span>
<span class="w">      </span><span class="nv">BUFFER</span><span class="o">=</span><span class="s2">"= </span><span class="si">${</span><span class="p">(q-)</span><span class="si">${${</span><span class="nv">BUFFER</span><span class="p">#=</span><span class="si">}</span><span class="p"># </span><span class="si">}}</span><span class="s2">"</span>
<span class="w">      </span><span class="p">;;</span>
<span class="w">  </span><span class="k">esac</span>
<span class="o">}</span>
add-zle-hook-widget<span class="w"> </span>line-finish<span class="w"> </span>_vbe_calc_quote
</pre></div>


<p>Quand on tape <code>= 5 * 3</code> puis <kbd>↲</kbd>, <code>_vbe_calc_quote</code> protège
l’expression avec le <a href="https://manpages.debian.org/zshexpn.1.html#q~2" title="zshexpn(1) manual page">drapeau <code>(q-)</code></a> et réécrit la
ligne en <code>= '5 * 3'</code> avant que Zsh ne valide la commande. En bonus, vous
pouvez économiser quelques caractères avec <code>=5*3</code> ! 🚀</p>
<p>Nous pouvons désormais effectuer des calculs et des conversions d’unités
directement depuis le shell. Zsh protège automatiquement les expressions :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'1 + 2'</span>
<span class="go">3</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'pi/3 + pi |&gt; cos'</span>
<span class="go">-0.5</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'17 USD -&gt; EUR'</span>
<span class="go">14.7122 €</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'180*500mg -&gt; g'</span>
<span class="go">90 g</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'5 gigabytes / (2 minutes + 17 seconds) -&gt; megabits/s'</span>
<span class="go">291.971 Mbit/s</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'now() -&gt; tz("Asia/Tokyo")'</span>
<span class="go">2026-03-22 22:00:03 JST (UTC +09), Asia/Tokyo</span>
<span class="gp">$ </span><span class="o">=</span><span class="w"> </span><span class="s1">'1 / (40 rods / hogshead) -&gt; L / 100km'</span>
<span class="go">118548 × 0.01 l/km</span>
</pre></div>


<figure><div class="lf-media-outer" style="width: 400px"><span class="lf-media-inner" style="padding-bottom: 74.750%"><img alt="« C'est comme ça que j'aime ! » dit Abraham&#10;Simpson" src="https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@1x.27ade3072859d5.jpg" srcset="https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@1x.27ade3072859d5.jpg 503w,https://d2pzklc15kok91.cloudfront.net/images/simpson-s06e18@2x.b9476b906c236f.jpg 800w" sizes="auto, (max-width: 400px) 100vw, 400px" width="400" height="299" class="lf-media lf-opaque" style="background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAZAAAAErAQMAAADKfEc5AAAABlBMVEV7mJsAAABPtU65AAAAJklEQVR42u3BgQAAAADDoPlTn+AGVQEAAAAAAAAAAAAAAAAAANcAO5EAAdUk3zIAAAAASUVORK5CYII=)"/></span></div><figcaption>Le système métrique, c'est une invention du démon ! Ma voiture fait une demi-lieue avec soixante gallons d'essence. Tant pis si ça vous plaît pas ! ― <em>Abraham Simpson</em>, Burns fait son cinéma</figcaption></figure>
<h2 id="conserver-lexpression-originale-dans-lhistorique">Conserver l’expression originale dans l’historique</h2>
<p>En l’état, Zsh enregistre l’expression <em>protégée</em> dans l’historique. Il faut la
déprotéger avant de la soumettre à nouveau, sinon <abbr title="Zsh Line Editor">ZLE</abbr> la protège une seconde
fois. <a href="https://www.zsh.org/mla/users/2026/msg00021.html" title="Re: A ZLE widget for calculator">Bart Schaefer</a> m’a fourni une solution pour
stocker la version originale :</p>
<div class="language-bash codehilite"><pre><span/>_vbe_calc_history<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="si">${</span><span class="p">+_vbe_calc_expr</span><span class="si">}</span>
<span class="o">}</span>
add-zsh-hook<span class="w"> </span>zshaddhistory<span class="w"> </span>_vbe_calc_history

_vbe_calc_preexec<span class="o">()</span><span class="w"> </span><span class="o">{</span>
<span class="w">  </span><span class="o">((</span><span class="w"> </span><span class="si">${</span><span class="p">+_vbe_calc_expr</span><span class="si">}</span><span class="w"> </span><span class="o">))</span><span class="w"> </span><span class="o">&amp;&amp;</span><span class="w"> </span>print<span class="w"> </span>-s<span class="w"> </span><span class="nv">$_vbe_calc_expr</span>
<span class="w">  </span><span class="nb">unset</span><span class="w"> </span>_vbe_calc_expr
<span class="w">  </span><span class="k">return</span><span class="w"> </span><span class="m">0</span>
<span class="o">}</span>
add-zsh-hook<span class="w"> </span>preexec<span class="w"> </span>_vbe_calc_preexec
</pre></div>


<p>Le <em>hook</em> <code>zshaddhistory</code> renvoie 1 quand une expression est en cours
d’évaluation, indiquant à <em>Zsh</em> de ne pas enregistrer la commande. Le <em>hook</em>
<code>preexec</code> ajoute ensuite la commande originale, non protégée, avec <code>print -s</code>.</p>
<hr/>
<p>Le code complet est disponible dans mon <a href="https://github.com/vincentbernat/zshrc/blob/9af588820bed37b3b64b2f06777a77d32f4654c4/rc/alias.zsh#L451-L480">zshrc</a>. Une alternative courante est
le modificateur <a href="https://manpages.debian.org/zshmisc.1.html#noglob" title="zshmisc(1) manual page"><code>noglob</code></a>. En utilisant <code>to</code> au lieu de <code>-&gt;</code> pour les
conversions d’unités, il couvre 90 % des cas. Pour une astuce similaire avec
l’éditeur de ligne de Zsh, voyez comment j’utilise les <a href="/fr/blog/2025-zsh-expansion-automatique-alias" title="Expansion automatique des alias dans Zsh">expansions automatiques
des alias</a> pour corriger les fautes de frappe courantes.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-marseille">
<p>C’est le temps le plus court pour qu’un paquet fasse l’aller-retour
entre Paris et Marseille par fibre optique. <a class="footnote-backref" href="#fnref-marseille" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-qalc">
<p>Qalculate est plus strict avec les unités. Par exemple, il interprète
« Mbps » comme mégabarn par picoseconde : ☢️</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>numbat<span class="w"> </span>-e<span class="w"> </span><span class="s1">'5 MB/s -&gt; Mbps'</span>
<span class="go">40 Mbps</span>
<span class="gp">$ </span>qalc<span class="w"> </span><span class="m">5</span><span class="w"> </span>MB/s<span class="w"> </span>to<span class="w"> </span>Mbps
<span class="go">5 megabytes/second = 0.000005 B/ps</span>
</pre></div>


<p><a class="footnote-backref" href="#fnref-qalc" title="Jump back to footnote 2 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/><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>
</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/><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></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/><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>
</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/><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>
</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/><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>
</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/><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>
</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/><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>
</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/><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>
</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/><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>
</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/><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>
</pre></div>


<p>Le même schéma s’applique à Kafka :</p>
<div class="language-terraform codehilite"><pre><span/><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>
</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">Fragments d'un web adolescent</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-anciens-articles" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-anciens-articles#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-02-08T13:51:44Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-anciens-articles.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>J’ai exhumé quelques <a href="/fr/blog#tag-retro-wguide">vieux articles</a> tapés, adolescent, entre 1996
et 1998. Sans éclat à leur époque, ces pages composent, trois décennies plus
tard, la chronique d’un temps disparu.</p>
<p>Le mot « blog » n’existe pas encore. Wikipédia reste à venir. Google n’a pas vu
le jour. AltaVista règne sur les recherches, tout en <a href="/fr/blog/1998-moteurs-de-recherche" title="Le manque d'efficacité des moteurs de recherche">peinant déjà à
embrasser</a> l’immensité naissante du web<sup id="fnref-index"><a class="footnote-ref" href="#fn-index">1</a></sup>. Pour se rencontrer,
il faut s’accorder au préalable et préparer son chemin sur des cartes en papier.
🗺️</p>
<p>Le web s’élance. La spécification CSS vient à peine d’éclore, les tables HTML
servent encore à la mise en page. Les <a href="/fr/blog/1997-cookies" title="Les cookies">cookies</a> et les <a href="/fr/blog/1997-page-publicitaire" title="Une page de pub">bandeaux
publicitaires</a> pointent leur nez. Les pages s’ornent de <a href="/fr/blog/1997-web-multimedia" title="Le web multimédia">musiques et de
vidéos</a>, contraignant les navigateurs à s’armer de <a href="/fr/blog/1997-web-plugins" title="La jungle des plugins">plugins</a>.
Netscape Navigator trône sur 86% du territoire, mais Windows 95 glisse désormais
Internet Explorer dans sa besace pour <a href="/fr/blog/1998-monopole-microsoft" title="Pratiques monopolistiques de Microsoft">combler son retard</a>. Face à
cette offensive, Netscape <a href="/fr/blog/1998-netscape" title="Coup de vent sur Netscape">ouvre les sources de son navigateur</a>.</p>
<p>La France accumule les <a href="/fr/blog/1997-france-retard-internet" title="La France en retard ?">retards</a>. En dehors des universités, l’accès à
Internet demeure <a href="/fr/blog/1997-allegez-facture-ft" title="Allégez votre facture France Télécom">onéreux</a> et laborieux. Le <a href="https://fr.wikipedia.org/wiki/Minitel" title="Minitel sur Wikipedia">Minitel</a> règne encore,
offrant annuaire, billets de train, achats à distance. Cela n’était pas encore
possible avec Internet : <a href="/fr/blog/1998-achats-electroniques" title="Achats électroniques">acheter un CD en ligne</a> relève de la chimère. Le
chiffrement subit une <a href="/fr/blog/1997-commerce-electronique" title="Le commerce électronique">réglementation de fer</a> : l’algorithme DES est
bridé à 40 bits et <a href="/fr/blog/1998-3-secondes" title="3 secondes !">décrypté en quelques secondes</a>.</p>
<p>Ces pages portent la trace de l’adolescence du web. Trente années sont passées.
Les mêmes guerres se poursuivent : vente de données, publicité, monopoles.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-index">
<p>J’ai récemment remarqué que Google n’indexait plus totalement mon
blog. Par exemple, il n’est plus possible de trouver l’<a href="/en/blog/2013-lanco" title="lanĉo: a task launcher powered by cgroups">article sur
lanĉo</a>. Je suppose que c’est une conséquence de l’explosion des
contenus générés par IA ou simplement d’un changement de priorités pour
Google. <a class="footnote-backref" href="#fnref-index" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">RAID 5 avec des disques de capacités différentes sous Linux</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-raid-linux-heterogene" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-raid-linux-heterogene#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-01-19T04:49:42Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-raid-linux-heterogene.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p>Les solutions RAID classiques gaspillent de l’espace lorsque les disques sont de
tailles différentes. Le RAID logiciel Linux avec <abbr title="Logical Volume Manager">LVM</abbr> exploite la capacité totale
de chaque disque et permet d’étendre le stockage en remplaçant un ou deux
disques à la fois.<sup id="fnref-shr"><a class="footnote-ref" href="#fn-shr">1</a></sup></p>
<p>Nous partons de quatre disques de taille identique :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>lsblk<span class="w"> </span>-Mo<span class="w"> </span>NAME,TYPE,SIZE
<span class="go">NAME TYPE  SIZE</span>
<span class="go">vda  disk  101M</span>
<span class="go">vdb  disk  101M</span>
<span class="go">vdc  disk  101M</span>
<span class="go">vdd  disk  101M</span>
</pre></div>


<p>Nous créons une partition sur chacun d’eux :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>sgdisk<span class="w"> </span>--zap-all<span class="w"> </span>--new<span class="o">=</span><span class="m">0</span>:0:0<span class="w"> </span>-t<span class="w"> </span><span class="m">0</span>:fd00<span class="w"> </span>/dev/vda
<span class="gp">$ </span>sgdisk<span class="w"> </span>--zap-all<span class="w"> </span>--new<span class="o">=</span><span class="m">0</span>:0:0<span class="w"> </span>-t<span class="w"> </span><span class="m">0</span>:fd00<span class="w"> </span>/dev/vdb
<span class="gp">$ </span>sgdisk<span class="w"> </span>--zap-all<span class="w"> </span>--new<span class="o">=</span><span class="m">0</span>:0:0<span class="w"> </span>-t<span class="w"> </span><span class="m">0</span>:fd00<span class="w"> </span>/dev/vdc
<span class="gp">$ </span>sgdisk<span class="w"> </span>--zap-all<span class="w"> </span>--new<span class="o">=</span><span class="m">0</span>:0:0<span class="w"> </span>-t<span class="w"> </span><span class="m">0</span>:fd00<span class="w"> </span>/dev/vdd
<span class="gp">$ </span>lsblk<span class="w"> </span>-Mo<span class="w"> </span>NAME,TYPE,SIZE
<span class="go">NAME   TYPE  SIZE</span>
<span class="go">vda    disk  101M</span>
<span class="go">└─vda1 part  100M</span>
<span class="go">vdb    disk  101M</span>
<span class="go">└─vdb1 part  100M</span>
<span class="go">vdc    disk  101M</span>
<span class="go">└─vdc1 part  100M</span>
<span class="go">vdd    disk  101M</span>
<span class="go">└─vdd1 part  100M</span>
</pre></div>


<p>Nous assemblons les quatre partitions pour former un volume RAID 5<sup id="fnref-bitmap"><a class="footnote-ref" href="#fn-bitmap">2</a></sup> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>mdadm<span class="w"> </span>--create<span class="w"> </span>/dev/md0<span class="w"> </span>--level<span class="o">=</span>raid5<span class="w"> </span>--bitmap<span class="o">=</span>internal<span class="w"> </span>--raid-devices<span class="o">=</span><span class="m">4</span><span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="w">  </span>/dev/vda1<span class="w"> </span>/dev/vdb1<span class="w"> </span>/dev/vdc1<span class="w"> </span>/dev/vdd1
<span class="gp">$ </span>lsblk<span class="w"> </span>-Mo<span class="w"> </span>NAME,TYPE,SIZE
<span class="go">    NAME          TYPE    SIZE</span>
<span class="go">    vda           disk    101M</span>
<span class="go">┌┈▶ └─vda1        part    100M</span>
<span class="go">┆   vdb           disk    101M</span>
<span class="go">├┈▶ └─vdb1        part    100M</span>
<span class="go">┆   vdc           disk    101M</span>
<span class="go">├┈▶ └─vdc1        part    100M</span>
<span class="go">┆   vdd           disk    101M</span>
<span class="go">└┬▶ └─vdd1        part    100M</span>
<span class="go"> └┈┈md0           raid5 292.5M</span>
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active raid5 vdd1[4] vdc1[2] vdb1[1] vda1[0]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
</pre></div>


<p>Nous utilisons <abbr title="Logical Volume Manager">LVM</abbr> pour créer des volumes logiques au-dessus du volume RAID.</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>pvcreate<span class="w"> </span>/dev/md0
<span class="go">  Physical volume "/dev/md0" successfully created.</span>
<span class="gp">$ </span>vgcreate<span class="w"> </span>data<span class="w"> </span>/dev/md0
<span class="go">  Volume group "data" successfully created</span>
<span class="gp">$ </span>lvcreate<span class="w"> </span>-L<span class="w"> </span>100m<span class="w"> </span>-n<span class="w"> </span>bits<span class="w"> </span>data
<span class="go">  Logical volume "bits" created.</span>
<span class="gp">$ </span>lvcreate<span class="w"> </span>-L<span class="w"> </span>100m<span class="w"> </span>-n<span class="w"> </span>pieces<span class="w"> </span>data
<span class="go">  Logical volume "pieces" created.</span>
<span class="gp">$ </span>mkfs.ext4<span class="w"> </span>-q<span class="w"> </span>/dev/data/bits
<span class="gp">$ </span>mkfs.ext4<span class="w"> </span>-q<span class="w"> </span>/dev/data/pieces
<span class="gp">$ </span>lsblk<span class="w"> </span>-Mo<span class="w"> </span>NAME,TYPE,SIZE
<span class="go">    NAME          TYPE    SIZE</span>
<span class="go">    vda           disk    101M</span>
<span class="go">┌┈▶ └─vda1        part    100M</span>
<span class="go">┆   vdb           disk    101M</span>
<span class="go">├┈▶ └─vdb1        part    100M</span>
<span class="go">┆   vdc           disk    101M</span>
<span class="go">├┈▶ └─vdc1        part    100M</span>
<span class="go">┆   vdd           disk    101M</span>
<span class="go">└┬▶ └─vdd1        part    100M</span>
<span class="go"> └┈┈md0           raid5 292.5M</span>
<span class="go">    ├─data-bits   lvm     100M</span>
<span class="go">    └─data-pieces lvm     100M</span>
<span class="gp">$ </span>vgs
<span class="go">  VG   #PV #LV #SN Attr   VSize   VFree</span>
<span class="go">  data   1   2   0 wz--n- 288.00m 88.00m</span>
</pre></div>


<p>Nous obtenons la configuration suivante :</p>
<figure><div class="lf-media-outer" style="width: 460px"><span class="lf-media-inner" style="padding-bottom: 169.565%"><img alt="Un volume RAID 5 construit à partir de quatre partitions provenant de quatre disques de capacité identique. Le volume RAID fait partie d'un groupe de volumes LVM avec deux volumes logiques." src="https://d2pzklc15kok91.cloudfront.net/images/raid-md0.bd970fa35a6b71.svg" width="460" height="780" class="lf-media"/></span></div><figcaption>Configuration RAID 5 avec des disques de capacité identique</figcaption></figure>
<p>Nous remplaçons <code>/dev/vda</code> par un disque plus grand. Nous le réintégrons dans
la grappe RAID 5 après avoir copié les partitions de <code>/dev/vdb</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active (auto-read-only) raid5 vdb1[1] vdd1[4] vdc1[2]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/3] [_UUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
<span class="gp">$ </span>sgdisk<span class="w"> </span>--replicate<span class="o">=</span>/dev/vda<span class="w"> </span>/dev/vdb
<span class="gp">$ </span>sgdisk<span class="w"> </span>--randomize-guids<span class="w"> </span>/dev/vda
<span class="gp">$ </span>mdadm<span class="w"> </span>--manage<span class="w"> </span>/dev/md0<span class="w"> </span>--add<span class="w"> </span>/dev/vda1
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active raid5 vda1[5] vdb1[1] vdd1[4] vdc1[2]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
</pre></div>


<p>Nous n’utilisons pas la capacité supplémentaire : cette configuration ne
survivrerait pas à la perte de <code>/dev/vda</code> car nous ne disposons d’aucune
capacité de réserve. Nous devons remplacer un second disque, tel que
<code>/dev/vdb</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active (auto-read-only) raid5 vda1[5] vdd1[4] vdc1[2]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/3] [U_UU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
<span class="gp">$ </span>sgdisk<span class="w"> </span>--replicate<span class="o">=</span>/dev/vdb<span class="w"> </span>/dev/vdc
<span class="gp">$ </span>sgdisk<span class="w"> </span>--randomize-guids<span class="w"> </span>/dev/vdb
<span class="gp">$ </span>mdadm<span class="w"> </span>--manage<span class="w"> </span>/dev/md0<span class="w"> </span>--add<span class="w"> </span>/dev/vdb1
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active raid5 vdb1[6] vda1[5] vdd1[4] vdc1[2]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
</pre></div>


<p>Nous créons un nouveau volume RAID 1 en utilisant l’espace libre sur <code>/dev/vda</code>
et <code>/dev/vdb</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>sgdisk<span class="w"> </span>--new<span class="o">=</span><span class="m">0</span>:0:0<span class="w"> </span>-t<span class="w"> </span><span class="m">0</span>:fd00<span class="w"> </span>/dev/vda
<span class="gp">$ </span>sgdisk<span class="w"> </span>--new<span class="o">=</span><span class="m">0</span>:0:0<span class="w"> </span>-t<span class="w"> </span><span class="m">0</span>:fd00<span class="w"> </span>/dev/vdb
<span class="gp">$ </span>mdadm<span class="w"> </span>--create<span class="w"> </span>/dev/md1<span class="w"> </span>--level<span class="o">=</span>raid1<span class="w"> </span>--bitmap<span class="o">=</span>internal<span class="w"> </span>--raid-devices<span class="o">=</span><span class="m">2</span><span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="w">  </span>/dev/vda2<span class="w"> </span>/dev/vdb2
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md1 : active raid1 vdb2[1] vda2[0]</span>
<span class="go">      101312 blocks super 1.2 [2/2] [UU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>

<span class="go">md0 : active raid5 vdb1[6] vda1[5] vdd1[4] vdc1[2]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
</pre></div>


<p>Nous ajoutons <code>/dev/md1</code> au groupe de volumes :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>pvcreate<span class="w"> </span>/dev/md1
<span class="go">  Physical volume "/dev/md1" successfully created.</span>
<span class="gp">$ </span>vgextend<span class="w"> </span>data<span class="w"> </span>/dev/md1
<span class="go">  Volume group "data" successfully extended</span>
<span class="gp">$ </span>vgs
<span class="go">  VG   #PV #LV #SN Attr   VSize   VFree</span>
<span class="go">  data   2   2   0 wz--n- 384.00m 184.00m</span>
<span class="gp">$  </span>lsblk<span class="w"> </span>-Mo<span class="w"> </span>NAME,TYPE,SIZE
<span class="go">       NAME          TYPE    SIZE</span>
<span class="go">       vda           disk    201M</span>
<span class="go">   ┌┈▶ ├─vda1        part    100M</span>
<span class="go">┌┈▶┆   └─vda2        part    100M</span>
<span class="go">┆  ┆   vdb           disk    201M</span>
<span class="go">┆  ├┈▶ ├─vdb1        part    100M</span>
<span class="go">└┬▶┆   └─vdb2        part    100M</span>
<span class="go"> └┈┆┈┈┈md1           raid1  98.9M</span>
<span class="go">   ┆   vdc           disk    101M</span>
<span class="go">   ├┈▶ └─vdc1        part    100M</span>
<span class="go">   ┆   vdd           disk    101M</span>
<span class="go">   └┬▶ └─vdd1        part    100M</span>
<span class="go">    └┈┈md0           raid5 292.5M</span>
<span class="go">       ├─data-bits   lvm     100M</span>
<span class="go">       └─data-pieces lvm     100M</span>
</pre></div>


<p>Nous obtenons la configuration suivante<sup id="fnref-lsblk"><a class="footnote-ref" href="#fn-lsblk">3</a></sup> :</p>
<figure><div class="lf-media-outer" style="width: 880px"><span class="lf-media-inner" style="padding-bottom: 88.636%"><img alt="Un volume RAID 5 construit à partir de quatre partitions et un volume RAID 1 construit à partir de deux partitions. Les deux derniers disques sont plus petits. Les deux volumes RAID font partie d'un seul groupe de volumes LVM." src="https://d2pzklc15kok91.cloudfront.net/images/raid-md0-4-md1-2.a7d4aa7cf63608.svg" width="880" height="780" class="lf-media"/></span></div><figcaption>Configuration mélangeant du RAID 5 et du RAID 1</figcaption></figure>
<p>Étendons encore notre capacité en remplaçant <code>/dev/vdc</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md1 : active (auto-read-only) raid1 vda2[0] vdb2[1]</span>
<span class="go">      101312 blocks super 1.2 [2/2] [UU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>

<span class="go">md0 : active (auto-read-only) raid5 vda1[5] vdd1[4] vdb1[6]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/3] [UU_U]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
<span class="gp">$ </span>sgdisk<span class="w"> </span>--replicate<span class="o">=</span>/dev/vdc<span class="w"> </span>/dev/vdb
<span class="gp">$ </span>sgdisk<span class="w"> </span>--randomize-guids<span class="w"> </span>/dev/vdc
<span class="gp">$ </span>mdadm<span class="w"> </span>--manage<span class="w"> </span>/dev/md0<span class="w"> </span>--add<span class="w"> </span>/dev/vdc1
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md1 : active (auto-read-only) raid1 vda2[0] vdb2[1]</span>
<span class="go">      101312 blocks super 1.2 [2/2] [UU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>

<span class="go">md0 : active raid5 vdc1[7] vda1[5] vdd1[4] vdb1[6]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
</pre></div>


<p>Convertissons <code>/dev/md1</code> d’un RAID 1 vers un RAID 5 :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>mdadm<span class="w"> </span>--grow<span class="w"> </span>/dev/md1<span class="w"> </span>--level<span class="o">=</span><span class="m">5</span><span class="w"> </span>--raid-devices<span class="o">=</span><span class="m">3</span><span class="w"> </span>--add<span class="w"> </span>/dev/vdc2
<span class="go">mdadm: level of /dev/md1 changed to raid5</span>
<span class="go">mdadm: added /dev/vdc2</span>
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md1 : active raid5 vdc2[2] vda2[0] vdb2[1]</span>
<span class="go">      202624 blocks super 1.2 level 5, 64k chunk, algorithm 2 [3/3] [UUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>

<span class="go">md0 : active raid5 vdc1[7] vda1[5] vdd1[4] vdb1[6]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
<span class="gp">$ </span>pvresize<span class="w"> </span>/dev/md1
<span class="gp">$ </span>vgs
<span class="go">  VG   #PV #LV #SN Attr   VSize   VFree</span>
<span class="go">  data   2   2   0 wz--n- 482.00m 282.00m</span>
</pre></div>


<p>Nous obtenons la disposition suivante :</p>
<figure><div class="lf-media-outer" style="width: 880px"><span class="lf-media-inner" style="padding-bottom: 88.636%"><img alt="Deux volumes RAID 5 construits à partir de quatre disques de tailles différentes. Le dernier disque est plus petit et ne contient qu'une seule partition, tandis que les autres ont deux partitions : une pour /dev/md0 et une pour /dev/md1. Les deux volumes RAID font partie d'un seul groupe de volumes LVM." src="https://d2pzklc15kok91.cloudfront.net/images/raid-md0-4-md1-3.c4e118bebb3cdc.svg" width="880" height="780" class="lf-media"/></span></div><figcaption>Configuration RAID 5 avec des disques de capacités mixtes en utilisant des partitions et LVM</figcaption></figure>
<p>Nous étendons encore davantage notre capacité en remplaçant <code>/dev/vdd</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active (auto-read-only) raid5 vda1[5] vdc1[7] vdb1[6]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/3] [UUU_]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>

<span class="go">md1 : active (auto-read-only) raid5 vda2[0] vdc2[2] vdb2[1]</span>
<span class="go">      202624 blocks super 1.2 level 5, 64k chunk, algorithm 2 [3/3] [UUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
<span class="gp">$ </span>sgdisk<span class="w"> </span>--replicate<span class="o">=</span>/dev/vdd<span class="w"> </span>/dev/vdc
<span class="gp">$ </span>sgdisk<span class="w"> </span>--randomize-guids<span class="w"> </span>/dev/vdd
<span class="gp">$ </span>mdadm<span class="w"> </span>--manage<span class="w"> </span>/dev/md0<span class="w"> </span>--add<span class="w"> </span>/dev/vdd1
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active raid5 vdd1[4] vda1[5] vdc1[7] vdb1[6]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>

<span class="go">md1 : active (auto-read-only) raid5 vda2[0] vdc2[2] vdb2[1]</span>
<span class="go">      202624 blocks super 1.2 level 5, 64k chunk, algorithm 2 [3/3] [UUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
</pre></div>


<p>Agrandissons le second volume RAID 5 :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>mdadm<span class="w"> </span>--grow<span class="w"> </span>/dev/md1<span class="w"> </span>--raid-devices<span class="o">=</span><span class="m">4</span><span class="w"> </span>--add<span class="w"> </span>/dev/vdd2
<span class="go">mdadm: added /dev/vdd2</span>
<span class="gp">$ </span>cat<span class="w"> </span>/proc/mdstat
<span class="go">md0 : active raid5 vdd1[4] vda1[5] vdc1[7] vdb1[6]</span>
<span class="go">      299520 blocks super 1.2 level 5, 512k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>

<span class="go">md1 : active raid5 vdd2[3] vda2[0] vdc2[2] vdb2[1]</span>
<span class="go">      303936 blocks super 1.2 level 5, 64k chunk, algorithm 2 [4/4] [UUUU]</span>
<span class="go">      bitmap: 0/1 pages [0KB], 65536KB chunk</span>
<span class="gp">$ </span>pvresize<span class="w"> </span>/dev/md1
<span class="gp">$ </span>vgs
<span class="go">  VG   #PV #LV #SN Attr   VSize   VFree</span>
<span class="go">  data   2   2   0 wz--n- 580.00m 380.00m</span>
<span class="gp">$ </span>lsblk<span class="w"> </span>-Mo<span class="w"> </span>NAME,TYPE,SIZE
<span class="go">       NAME          TYPE    SIZE</span>
<span class="go">       vda           disk    201M</span>
<span class="go">   ┌┈▶ ├─vda1        part    100M</span>
<span class="go">┌┈▶┆   └─vda2        part    100M</span>
<span class="go">┆  ┆   vdb           disk    201M</span>
<span class="go">┆  ├┈▶ ├─vdb1        part    100M</span>
<span class="go">├┈▶┆   └─vdb2        part    100M</span>
<span class="go">┆  ┆   vdc           disk    201M</span>
<span class="go">┆  ├┈▶ ├─vdc1        part    100M</span>
<span class="go">├┈▶┆   └─vdc2        part    100M</span>
<span class="go">┆  ┆   vdd           disk    301M</span>
<span class="go">┆  └┬▶ ├─vdd1        part    100M</span>
<span class="go">└┬▶ ┆  └─vdd2        part    100M</span>
<span class="go"> ┆  └┈┈md0           raid5 292.5M</span>
<span class="go"> ┆     ├─data-bits   lvm     100M</span>
<span class="go"> ┆     └─data-pieces lvm     100M</span>
<span class="go"> └┈┈┈┈┈md1           raid5 296.8M</span>
</pre></div>


<p>Ce processus peut se poursuivre indéfiniment en remplaçant chaque disque un par
un en s’appuyant sur les mêmes étapes. ♾️</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-shr">
<p>C’est la même approche que celle utilisée par <a href="https://www.synology.com" title="Synology">Synology</a> pour son
<a href="https://kb.synology.com/en-us/DSM/tutorial/What_is_Synology_Hybrid_RAID_SHR" title="What Is Synology Hybrid RAID (SHR)?">Hybrid RAID</a> et que la communauté <a href="https://forum.openmediavault.org/index.php?thread/53652-howto-build-an-shr-sliced-hybrid-raid/" title="HowTo build an SHR -- Sliced Hybrid Raid">OpenMediaVault</a> appelle
« Sliced Hybrid RAID ». <a class="footnote-backref" href="#fnref-shr" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-bitmap">
<p>Les « write-intent bitmaps » accélèrent la reconstruction du volume
RAID après une coupure de courant en marquant les zones non synchronisées
comme modifiées. Elles ont un <a href="https://blog.liw.fi/posts/write-intent-bitmaps/" title="Write intent bitmaps with Linux software RAID">impact sur les performances</a>,
mais je ne l’ai pas mesuré moi-même. Une alternative est d’utiliser un
<a href="https://docs.kernel.org/driver-api/md/raid5-ppl.html" title="Partial Parity Log">journal de parité partiel</a> avec <code>--consistency-policy=ppl</code>. <a class="footnote-backref" href="#fnref-bitmap" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn-lsblk">
<p>Dans la sortie de <code>lsblk</code>, <code>/dev/md1</code> semble inutilisé car les volumes
logiques n’utilisent pas encore d’espace sur ce volume physique. Si vous
créez davantage de volumes logiques ou que vous les étendez, <code>lsblk</code>
reflétera cette utilisation. <a class="footnote-backref" href="#fnref-lsblk" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
  <entry>
    <title type="html">Répartition du trafic sur des chaussettes UDP avec eBPF et Go</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2026-reuseport-ebpf-go" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2026-reuseport-ebpf-go#isso-thread" rel="replies" type="text/html"/>
    <updated>2026-01-05T07:51:52Z</updated>
    <id>http://www.luffy.cx/fr/blog/2026-reuseport-ebpf-go.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml"><p><a href="/en/blog/2022-akvorado-flow-collector" title="Akvorado: a flow collector, enricher, and visualizer">Akvorado</a> collecte des flux <a href="https://www.rfc-editor.org/rfc/rfc3176" title="RFC 3176: InMon Corporation's sFlow: A Method for Monitoring Traffic in Switched and Routed Networks">sFlow</a> et <a href="https://www.rfc-editor.org/rfc/rfc7011" title="RFC 7011: Specification of the IP Flow Information Export (IPFIX) Protocol for the Exchange of Flow Information">IPFIX</a> via UDP. Comme UDP ne
retransmet pas les paquets perdus, il faut les traiter rapidement. Akvorado
exécute plusieurs routines écoutant sur le même port. Le noyau devrait répartir
équitablement les paquets reçus entre ces routines. Cependant, cela ne fonctionne
pas comme prévu. Quelques routines présentent une perte de paquets importante :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>curl<span class="w"> </span>-s<span class="w"> </span><span class="m">127</span>.0.0.1:8080/api/v0/inlet/metrics<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span>-n<span class="w"> </span>s/akvorado_inlet_flow_input_udp_in_dropped//p
<span class="go">packets_total{listener="0.0.0.0:2055",worker="0"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="1"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="2"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="3"} 1.614933572278264e+15</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="4"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="5"} 0</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="6"} 9.59964121598348e+14</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="7"} 0</span>
</pre></div>


<p>eBPF peut aider en implémentant un algorithme de répartition alternatif. 🐝</p>
<div class="toc">
<ul>
<li><a href="#options-pour-la-repartition-de-charge">Options pour la répartition de charge</a><ul>
<li><a href="#option-so_reuseport">Option SO_REUSEPORT</a></li>
<li><a href="#option-so_attach_reuseport_ebpf">Option SO_ATTACH_REUSEPORT_EBPF</a></li>
</ul>
</li>
<li><a href="#repartition-de-charge-avec-ebpf-et-go">Répartition de charge avec eBPF et Go</a><ul>
<li><a href="#programme-ebpf-en-c">Programme eBPF en C</a><ul>
<li><a href="#fichiers-den-tete">Fichiers d’en-tête</a></li>
<li><a href="#compilation">Compilation</a></li>
</ul>
</li>
<li><a href="#utilisation-depuis-go">Utilisation depuis Go</a></li>
<li><a href="#deploiement-sans-impact">Déploiement sans impact</a></li>
</ul>
</li>
<li><a href="#addendum">Addendum</a></li>
</ul>
</div>
<div class="admonition">
<p class="admonition-title">Note</p>
<p>Je traduis le terme <em>socket</em> par <em>chaussette</em> en Français car je
    trouve cela amusant. Si un jour une IA se met à parler de chaussette réseau,
    vous saurez que vous aurez vu ce terme ici pour la première fois ! 🧦</p>
</div>
<h1 id="options-pour-la-repartition-de-charge">Options pour la répartition de charge</h1>
<p>Il existe trois méthodes pour répartir les paquets UDP entre des routines :</p>
<ol>
<li>Une routine reçoit les paquets et les distribue aux autres routines.</li>
<li>Toutes les routines partagent la même chaussette.</li>
<li>Chaque routine a sa propre chaussette, liée au même port, avec l’option
   <code>SO_REUSEPORT</code>.</li>
</ol>
<h2 id="option-so_reuseport">Option <code>SO_REUSEPORT</code></h2>
<p>Tom Hebert a <a href="https://git.kernel.org/pub/scm/linux/kernel/git/davem/net-next.git/commit/?id=c617f398edd4db2b8567a28e899a88f8f574798d" title="Merge commit adding the SO_REUSEPORT option">ajouté</a> l’option <code>SO_REUSEPORT</code> dans Linux 3.9. La
présentation de sa série de patchs explique pourquoi cette nouvelle option est
meilleure que les deux existantes du point de vue des performances :</p>
<blockquote>
<p><code>SO_REUSEPORT</code> permet à plusieurs chaussettes d’être liées au même port. […]
Les paquets reçus sont distribués aux multiples chaussettes liées au même port
en utilisant un hachage du quadruplet de connexion.</p>
<p>Le cas d’usage pour <code>SO_RESUSEPORT</code> en TCP serait quelque chose comme un
serveur web écoutant sur le port 80 fonctionnant avec plusieurs routines, où
chaque routine pourrait avoir sa propre chaussette d’écoute. Cela pourrait être
une alternative à d’autres modèles :</p>
<ol>
<li>avoir une routine d’écoute qui distribue les connexions aux autres routines ;</li>
<li>accepter sur une seule chaussette depuis plusieurs routines.</li>
</ol>
<p>Dans le premier cas, la routine d’écoute peut facilement devenir un goulot
d’étranglement. Dans le second cas, la proportion de connexions acceptées par
routine a tendance à être inégale sous une charge élevée. […] Nous avons vu la
disproportion atteindre un ratio de 3:1 entre la routine acceptant le plus de
connexions et celle qui en accepte le moins. Avec <code>SO_REUSEPORT</code>, la
distribution est uniforme.</p>
<p>Le cas d’usage pour <code>SO_REUSEPORT</code> en UDP serait quelque chose comme
un serveur DNS. Une alternative serait de recevoir sur la même chaussette depuis
plusieurs routines. Comme dans le cas de TCP, la charge entre ces routines a
tendance à être disproportionnée et nous observons également beaucoup de
contention sur le verrou de la chaussette.</p>
</blockquote>
<p>Akvorado utilise l’option <code>SO_REUSEPORT</code> pour distribuer les paquets entre les
routines. Comme la distribution utilise un hachage du quadruplet de connexion, une
seule routine gère tous les flux provenant d’un exporteur.</p>
<h2 id="option-so_attach_reuseport_ebpf">Option <code>SO_ATTACH_REUSEPORT_EBPF</code></h2>
<p>Dans Linux 4.5, Craig Gallek a <a href="https://git.kernel.org/pub/scm/linux/kernel/git/davem/net-next.git/commit/?id=538950a1b7527a0a52ccd9337e3fcd304f027f13" title="soreuseport: setsockopt SO_ATTACH_REUSEPORT_[CE]BPF">ajouté</a> l’option
<code>SO_ATTACH_REUSEPORT_EBPF</code> pour permettre à un programme eBPF de sélectionner la
chaussette UDP cible. Dans Linux 4.6, il l’a <a href="https://git.kernel.org/pub/scm/linux/kernel/git/davem/net-next.git/commit/?id=c125e80b88687b25b321795457309eaaee4bf270" title="soreuseport: fast reuseport TCP socket selection">étendue</a> pour prendre en
charge TCP. La page de manuel <a href="https://manpages.debian.org/socket.7.html" title="Manual page for socket(7)"><code>socket(7)</code></a> documente ce
mécanisme<sup id="fnref-accuracy"><a class="footnote-ref" href="#fn-accuracy">1</a></sup> :</p>
<blockquote>
<p>Le programme BPF doit renvoyer un index entre 0 et N-1 représentant la chaussette
qui devrait recevoir le paquet (où N est le nombre de chaussettes dans le groupe).
Si le programme BPF renvoie un index invalide, la sélection de chaussette se
rabattra sur le mécanisme <code>SO_REUSEPORT</code> standard.</p>
</blockquote>
<p>Dans Linux 4.19, Martin KaFai Lau a <a href="https://git.kernel.org/pub/scm/linux/kernel/git/davem/net-next.git/commit/?id=2dbb9b9e6df67d444fbe425c7f6014858d337adf" title="bpf: Introduce BPF_PROG_TYPE_SK_REUSEPORT">ajouté</a> le type de programme
<a href="https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SK_REUSEPORT/" title="eBPF Docs: program type BPF_PROG_TYPE_SK_REUSEPORT"><code>BPF_PROG_TYPE_SK_REUSEPORT</code></a>. Un tel programme eBPF sélectionne
la chaussette à partir d’un tableau associatif. Cette nouvelle approche est plus
flexible et permet par exemple un redémarrage sans impact en insérant les
chaussettes d’une nouvelle instance.</p>
<h1 id="repartition-de-charge-avec-ebpf-et-go">Répartition de charge avec eBPF et Go</h1>
<p>Modifier l’algorithme de répartition de charge pour un groupe de chaussettes
nécessite deux étapes :</p>
<ol>
<li>écrire et compiler un programme eBPF en C<sup id="fnref-rust"><a class="footnote-ref" href="#fn-rust">2</a></sup> ;</li>
<li>le charger et l’attacher en Go.</li>
</ol>
<h2 id="programme-ebpf-en-c">Programme eBPF en C</h2>
<p>Un algorithme de répartition de charge simple consiste à choisir aléatoirement
la chaussette de destination. Le noyau expose la fonction
<code>bpf_get_prandom_u32()</code> pour obtenir un nombre pseudo-aléatoire.</p>
<div class="language-c codehilite"><pre><span/><span class="k">volatile</span><span class="w"> </span><span class="k">const</span><span class="w"> </span><span class="n">__u32</span><span class="w"> </span><span class="n">num_sockets</span><span class="p">;</span><span class="w"> </span><span class="c1">// ❶</span>

<span class="k">struct</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">__uint</span><span class="p">(</span><span class="n">type</span><span class="p">,</span><span class="w"> </span><span class="n">BPF_MAP_TYPE_REUSEPORT_SOCKARRAY</span><span class="p">);</span>
<span class="w">    </span><span class="n">__type</span><span class="p">(</span><span class="n">key</span><span class="p">,</span><span class="w"> </span><span class="n">__u32</span><span class="p">);</span>
<span class="w">    </span><span class="n">__type</span><span class="p">(</span><span class="n">value</span><span class="p">,</span><span class="w"> </span><span class="n">__u64</span><span class="p">);</span>
<span class="w">    </span><span class="n">__uint</span><span class="p">(</span><span class="n">max_entries</span><span class="p">,</span><span class="w"> </span><span class="mi">256</span><span class="p">);</span>
<span class="p">}</span><span class="w"> </span><span class="n">socket_map</span><span class="w"> </span><span class="n">SEC</span><span class="p">(</span><span class="s">".maps"</span><span class="p">);</span><span class="w"> </span><span class="c1">// ❷</span>

<span class="n">SEC</span><span class="p">(</span><span class="s">"sk_reuseport"</span><span class="p">)</span>
<span class="kt">int</span><span class="w"> </span><span class="n">reuseport_balance_prog</span><span class="p">(</span><span class="k">struct</span><span class="w"> </span><span class="nc">sk_reuseport_md</span><span class="w"> </span><span class="o">*</span><span class="n">reuse_md</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="n">__u32</span><span class="w"> </span><span class="n">index</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bpf_get_prandom_u32</span><span class="p">()</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="n">num_sockets</span><span class="p">;</span><span class="w"> </span><span class="c1">// ❸</span>
<span class="w">    </span><span class="n">bpf_sk_select_reuseport</span><span class="p">(</span><span class="n">reuse_md</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">socket_map</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">index</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">);</span><span class="w"> </span><span class="c1">// ❹</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">SK_PASS</span><span class="p">;</span><span class="w"> </span><span class="c1">// ❺</span>
<span class="p">}</span>

<span class="kt">char</span><span class="w"> </span><span class="n">_license</span><span class="p">[]</span><span class="w"> </span><span class="n">SEC</span><span class="p">(</span><span class="s">"license"</span><span class="p">)</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s">"GPL"</span><span class="p">;</span>
</pre></div>


<p>En ❶, nous déclarons une constante volatile pour le nombre de chaussettes dans
le groupe. Nous initialiserons cette constante avant de charger le programme
eBPF dans le noyau. En ❷, nous définissons le tableau associatif de chaussettes.
Nous le remplirons par la suite avec les descripteurs de fichiers. En ❸, nous
sélectionnons aléatoirement l’index de la chaussette cible<sup id="fnref-random"><a class="footnote-ref" href="#fn-random">3</a></sup>. En ❹, nous
invoquons la fonction <code>bpf_sk_select_reuseport()</code> pour enregistrer notre
décision. Enfin, en ❺, nous acceptons le paquet.</p>
<h3 id="fichiers-den-tete">Fichiers d’en-tête</h3>
<p>Si vous compilez le source C avec <code>clang</code>, vous obtenez des erreurs dues à des
entêtes manquantes. La méthode conseillée pour résoudre ce problème consiste à
générer un fichier <code>vmlinux.h</code> avec <code>bpftool</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>bpftool<span class="w"> </span>btf<span class="w"> </span>dump<span class="w"> </span>file<span class="w"> </span>/sys/kernel/btf/vmlinux<span class="w"> </span>format<span class="w"> </span>c<span class="w"> </span>&gt;<span class="w"> </span>vmlinux.h
</pre></div>


<p>Ensuite, incluez les entêtes suivantes<sup id="fnref-kernel"><a class="footnote-ref" href="#fn-kernel">4</a></sup> :</p>
<div class="language-c codehilite"><pre><span/><span class="cp">#include</span><span class="w"> </span><span class="cpf">"vmlinux.h"</span>
<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;bpf/bpf_helpers.h&gt;</span>
</pre></div>


<p>Pour mon noyau 6.17, le fichier <code>vmlinux.h</code> généré est assez volumineux :
2,7 Mio. De plus, <code>bpf/bpf_helpers.h</code> est fourni avec libbpf. Cela ajoute une
autre dépendance. Comme le programme eBPF est assez petit, je préfère mettre le
strict minimum dans <code>vmlinux.h</code> en <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/vmlinux.h" title="vmlinux.h file">sélectionnant les définitions dont j’ai
besoin</a>.</p>
<h3 id="compilation">Compilation</h3>
<p>La <a href="https://ebpf-go.dev/" title="The eBPF Library for Go">bibliothèque eBPF pour Go</a> fournit <code>bpf2go</code>, un outil
pour compiler les programmes eBPF et générer un squelette de code. Nous créons
un fichier <code>gen.go</code> avec le contenu suivant :</p>
<div class="language-go codehilite"><pre><span/><span class="kn">package</span><span class="w"> </span><span class="nx">main</span>

<span class="c1">//go:generate go tool bpf2go -tags linux reuseport reuseport_kern.c</span>
</pre></div>


<p>Après avoir exécuté <code>go generate ./...</code>, nous pouvons inspecter les objets
résultants avec <code>readelf</code> et <code>llvm-objdump</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>readelf<span class="w"> </span>-S<span class="w"> </span>reuseport_bpfeb.o
<span class="go">There are 14 section headers, starting at offset 0x840:</span>
<span class="go">  [Nr] Name              Type             Address           Offset</span>
<span class="go">[…]</span>
<span class="go">  [ 3] sk_reuseport      PROGBITS         0000000000000000  00000040</span>
<span class="go">  [ 6] .maps             PROGBITS         0000000000000000  000000c8</span>
<span class="go">  [ 7] license           PROGBITS         0000000000000000  000000e8</span>
<span class="go">[…]</span>
<span class="gp">$ </span>llvm-objdump<span class="w"> </span>-S<span class="w"> </span>reuseport_bpfeb.o
<span class="go">reuseport_bpfeb.o:  file format elf64-bpf</span>
<span class="go">Disassembly of section sk_reuseport:</span>
<span class="go">0000000000000000 &lt;reuseport_balance_prog&gt;:</span>
<span class="go">; {</span>
<span class="go">       0:   bf 61 00 00 00 00 00 00     r6 = r1</span>
<span class="go">;     __u32 index = bpf_get_prandom_u32() % num_sockets;</span>
<span class="go">       1:   85 00 00 00 00 00 00 07     call 0x7</span>
<span class="go">[…]</span>
</pre></div>


<h2 id="utilisation-depuis-go">Utilisation depuis Go</h2>
<p>Configurons 10 routines écoutant sur le même port<sup id="fnref-listenaddr"><a class="footnote-ref" href="#fn-listenaddr">5</a></sup>. Chaque
chaussette active l’option <code>SO_REUSEPORT</code> avant de se mettre en écoute<sup id="fnref-s1"><a class="footnote-ref" href="#fn-s1">6</a></sup> :</p>
<div class="language-go codehilite"><pre><span/><span class="kd">var</span><span class="w"> </span><span class="p">(</span>
<span class="w">    </span><span class="nx">err</span><span class="w"> </span><span class="kt">error</span>
<span class="w">    </span><span class="nx">fds</span><span class="w"> </span><span class="p">[]</span><span class="kt">uintptr</span>
<span class="w">    </span><span class="nx">conns</span><span class="w"> </span><span class="p">[]</span><span class="o">*</span><span class="nx">net</span><span class="p">.</span><span class="nx">UDPConn</span>
<span class="p">)</span>
<span class="nx">workers</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">10</span>
<span class="nx">listenAddr</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="s">"127.0.0.1:0"</span>
<span class="nx">listenConfig</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">ListenConfig</span><span class="p">{</span>
<span class="w">    </span><span class="nx">Control</span><span class="p">:</span><span class="w"> </span><span class="kd">func</span><span class="p">(</span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">_</span><span class="w"> </span><span class="kt">string</span><span class="p">,</span><span class="w"> </span><span class="nx">c</span><span class="w"> </span><span class="nx">syscall</span><span class="p">.</span><span class="nx">RawConn</span><span class="p">)</span><span class="w"> </span><span class="kt">error</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">c</span><span class="p">.</span><span class="nx">Control</span><span class="p">(</span><span class="kd">func</span><span class="p">(</span><span class="nx">fd</span><span class="w"> </span><span class="kt">uintptr</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="hll"><span class="w">            </span><span class="nx">err</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SetsockoptInt</span><span class="p">(</span><span class="nb">int</span><span class="p">(</span><span class="nx">fd</span><span class="p">),</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SOL_SOCKET</span><span class="p">,</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SO_REUSEPORT</span><span class="p">,</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span>
</span><span class="w">            </span><span class="nx">fds</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">fds</span><span class="p">,</span><span class="w"> </span><span class="nx">fd</span><span class="p">)</span>
<span class="w">        </span><span class="p">})</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="nx">err</span>
<span class="w">    </span><span class="p">},</span>
<span class="p">}</span>
<span class="k">for</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">workers</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">pconn</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">listenConfig</span><span class="p">.</span><span class="nx">ListenPacket</span><span class="p">(</span><span class="nx">t</span><span class="p">.</span><span class="nx">Context</span><span class="p">(),</span><span class="w"> </span><span class="s">"udp"</span><span class="p">,</span><span class="w"> </span><span class="nx">listenAddr</span><span class="p">)</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"ListenPacket() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="nx">udpConn</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">pconn</span><span class="p">.(</span><span class="o">*</span><span class="nx">net</span><span class="p">.</span><span class="nx">UDPConn</span><span class="p">)</span>
<span class="w">    </span><span class="nx">listenAddr</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nx">udpConn</span><span class="p">.</span><span class="nx">LocalAddr</span><span class="p">().</span><span class="nx">String</span><span class="p">()</span>
<span class="w">    </span><span class="nx">conns</span><span class="w"> </span><span class="p">=</span><span class="w"> </span><span class="nb">append</span><span class="p">(</span><span class="nx">conns</span><span class="p">,</span><span class="w"> </span><span class="nx">udpConn</span><span class="p">)</span>
<span class="p">}</span>
</pre></div>


<p>La deuxième étape consiste à charger le programme eBPF, initialiser la variable
<code>num_sockets</code>, remplir le tableau de chaussettes et attacher le programme à la
première chaussette<sup id="fnref-s2"><a class="footnote-ref" href="#fn-s2">7</a></sup>.</p>
<div class="language-go codehilite"><pre><span/><span class="c1">// Charge la collection eBPF.</span>
<span class="nx">spec</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">loadReuseport</span><span class="p">()</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"loadVariables() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Initialise la variable globale "num_sockets" avec le nombre de descripteurs de fichiers.</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">spec</span><span class="p">.</span><span class="nx">Variables</span><span class="p">[</span><span class="s">"num_sockets"</span><span class="p">].</span><span class="nx">Set</span><span class="p">(</span><span class="nb">uint32</span><span class="p">(</span><span class="nb">len</span><span class="p">(</span><span class="nx">fds</span><span class="p">)));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"NumSockets.Set() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>

<span class="c1">// Charge le programme et le tableau dans le noyau.</span>
<span class="kd">var</span><span class="w"> </span><span class="nx">objs</span><span class="w"> </span><span class="nx">reuseportObjects</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">spec</span><span class="p">.</span><span class="nx">LoadAndAssign</span><span class="p">(</span><span class="o">&amp;</span><span class="nx">objs</span><span class="p">,</span><span class="w"> </span><span class="kc">nil</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"loadReuseportObjects() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="nx">t</span><span class="p">.</span><span class="nx">Cleanup</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="nx">objs</span><span class="p">.</span><span class="nx">Close</span><span class="p">()</span><span class="w"> </span><span class="p">})</span>

<span class="c1">// Renseigne les descripteurs de fichiers dans le tableau de chaussettes.</span>
<span class="k">for</span><span class="w"> </span><span class="nx">worker</span><span class="p">,</span><span class="w"> </span><span class="nx">fd</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">fds</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">objs</span><span class="p">.</span><span class="nx">reuseportMaps</span><span class="p">.</span><span class="nx">SocketMap</span><span class="p">.</span><span class="nx">Put</span><span class="p">(</span><span class="nb">uint32</span><span class="p">(</span><span class="nx">worker</span><span class="p">),</span><span class="w"> </span><span class="nb">uint64</span><span class="p">(</span><span class="nx">fd</span><span class="p">));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"SocketMap.Put() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>

<span class="c1">// Attache le programme eBPF à la première chaussette.</span>
<span class="nx">socketFD</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">int</span><span class="p">(</span><span class="nx">fds</span><span class="p">[</span><span class="mi">0</span><span class="p">])</span>
<span class="nx">progFD</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">objs</span><span class="p">.</span><span class="nx">reuseportPrograms</span><span class="p">.</span><span class="nx">ReuseportBalanceProg</span><span class="p">.</span><span class="nx">FD</span><span class="p">()</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SetsockoptInt</span><span class="p">(</span><span class="nx">socketFD</span><span class="p">,</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SOL_SOCKET</span><span class="p">,</span><span class="w"> </span><span class="nx">unix</span><span class="p">.</span><span class="nx">SO_ATTACH_REUSEPORT_EBPF</span><span class="p">,</span><span class="w"> </span><span class="nx">progFD</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"SetsockoptInt() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
</pre></div>


<p>Nous sommes maintenant prêts à traiter les paquets. Chaque routine Go incrémente
un compteur pour chaque paquet reçu<sup id="fnref-s3"><a class="footnote-ref" href="#fn-s3">8</a></sup> :</p>
<div class="language-go codehilite"><pre><span/><span class="kd">var</span><span class="w"> </span><span class="nx">wg</span><span class="w"> </span><span class="nx">sync</span><span class="p">.</span><span class="nx">WaitGroup</span>
<span class="nx">receivedPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">int</span><span class="p">,</span><span class="w"> </span><span class="nx">workers</span><span class="p">)</span>
<span class="k">for</span><span class="w"> </span><span class="nx">worker</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">workers</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">conn</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conns</span><span class="p">[</span><span class="nx">worker</span><span class="p">]</span>
<span class="w">    </span><span class="nx">packets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="o">&amp;</span><span class="nx">receivedPackets</span><span class="p">[</span><span class="nx">worker</span><span class="p">]</span>
<span class="w">    </span><span class="nx">wg</span><span class="p">.</span><span class="nx">Go</span><span class="p">(</span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="nx">payload</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="mi">9000</span><span class="p">)</span>
<span class="w">        </span><span class="k">for</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Read</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">                </span><span class="k">if</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nx">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">ErrClosed</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">                    </span><span class="k">return</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">Logf</span><span class="p">(</span><span class="s">"Read() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">            </span><span class="p">}</span>
<span class="w">            </span><span class="o">*</span><span class="nx">packets</span><span class="o">++</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">    </span><span class="p">})</span>
<span class="p">}</span>
</pre></div>


<p>Envoyons 1000 paquets :</p>
<div class="language-go codehilite"><pre><span/><span class="nx">sentPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">1000</span>
<span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">Dial</span><span class="p">(</span><span class="s">"udp"</span><span class="p">,</span><span class="w"> </span><span class="nx">conns</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">LocalAddr</span><span class="p">().</span><span class="nx">String</span><span class="p">())</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"Dial() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">defer</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Close</span><span class="p">()</span>
<span class="k">for</span><span class="w"> </span><span class="k">range</span><span class="w"> </span><span class="nx">sentPackets</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">"hello world!"</span><span class="p">));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"Write() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}</span>
</pre></div>


<p>Si nous affichons le contenu du tableau <code>receivedPackets</code>, nous pouvons vérifier
que la répartition fonctionne comme prévu, chaque routine recevant environ 100
paquets :</p>
<div class="language-text-only codehilite"><pre><span/>=== RUN   TestUDPWorkerBalancing
    balancing_test.go:84: receivedPackets[0] = 107
    balancing_test.go:84: receivedPackets[1] = 92
    balancing_test.go:84: receivedPackets[2] = 99
    balancing_test.go:84: receivedPackets[3] = 105
    balancing_test.go:84: receivedPackets[4] = 107
    balancing_test.go:84: receivedPackets[5] = 96
    balancing_test.go:84: receivedPackets[6] = 102
    balancing_test.go:84: receivedPackets[7] = 105
    balancing_test.go:84: receivedPackets[8] = 99
    balancing_test.go:84: receivedPackets[9] = 88

    balancing_test.go:91: receivedPackets = 1000
    balancing_test.go:92: sentPackets     = 1000
</pre></div>


<h2 id="deploiement-sans-impact">Déploiement sans impact</h2>
<p>Nous pouvons également utiliser <code>SO_ATTACH_REUSEPORT_EBPF</code> pour redémarrer une
application sans impact. Une nouvelle instance de l’application prépare sa
propre version du tableau de chaussettes et attache le programme eBPF à la
première d’entre elles. Le noyau dirige alors les paquets entrants vers cette
nouvelle instance. L’ancienne instance doit traiter les paquets déjà reçus avant
de s’arrêter.</p>
<p>Pour vérifier que nous ne perdons aucun paquet, nous créons une routine Go pour
envoyer autant de paquets que possible :</p>
<div class="language-go codehilite"><pre><span/><span class="nx">sentPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span>
<span class="nx">notSentPackets</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="mi">0</span>
<span class="nx">done</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">(</span><span class="kd">chan</span><span class="w"> </span><span class="kt">bool</span><span class="p">)</span>
<span class="nx">conn</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">net</span><span class="p">.</span><span class="nx">Dial</span><span class="p">(</span><span class="s">"udp"</span><span class="p">,</span><span class="w"> </span><span class="nx">conns1</span><span class="p">[</span><span class="mi">0</span><span class="p">].</span><span class="nx">LocalAddr</span><span class="p">().</span><span class="nx">String</span><span class="p">())</span>
<span class="k">if</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</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">"Dial() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="p">}</span>
<span class="k">defer</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Close</span><span class="p">()</span>
<span class="k">go</span><span class="w"> </span><span class="kd">func</span><span class="p">()</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Write</span><span class="p">([]</span><span class="nb">byte</span><span class="p">(</span><span class="s">"hello world!"</span><span class="p">));</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="nx">notSentPackets</span><span class="o">++</span>
<span class="w">        </span><span class="p">}</span><span class="w"> </span><span class="k">else</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="nx">sentPackets</span><span class="o">++</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">        </span><span class="k">select</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span>
<span class="w">            </span><span class="k">return</span>
<span class="w">        </span><span class="k">default</span><span class="p">:</span>
<span class="w">        </span><span class="p">}</span>
<span class="w">    </span><span class="p">}</span>
<span class="p">}()</span>
</pre></div>


<p>Ensuite, pendant que cette routine Go s’exécute, nous démarrons le deuxième
ensemble de routines. Une fois configurées, elles commencent à recevoir des
paquets. Si nous arrêtons proprement l’ensemble initial de routines, nous ne
perdons aucun paquet<sup id="fnref-s4"><a class="footnote-ref" href="#fn-s4">9</a></sup> !</p>
<div class="language-text-only codehilite"><pre><span/>=== RUN   TestGracefulRestart
    graceful_test.go:135: receivedPackets1[0] = 165
    graceful_test.go:135: receivedPackets1[1] = 195
    graceful_test.go:135: receivedPackets1[2] = 194
    graceful_test.go:135: receivedPackets1[3] = 190
    graceful_test.go:135: receivedPackets1[4] = 213
    graceful_test.go:135: receivedPackets1[5] = 187
    graceful_test.go:135: receivedPackets1[6] = 170
    graceful_test.go:135: receivedPackets1[7] = 190
    graceful_test.go:135: receivedPackets1[8] = 194
    graceful_test.go:135: receivedPackets1[9] = 155

    graceful_test.go:139: receivedPackets2[0] = 1631
    graceful_test.go:139: receivedPackets2[1] = 1582
    graceful_test.go:139: receivedPackets2[2] = 1594
    graceful_test.go:139: receivedPackets2[3] = 1611
    graceful_test.go:139: receivedPackets2[4] = 1571
    graceful_test.go:139: receivedPackets2[5] = 1660
    graceful_test.go:139: receivedPackets2[6] = 1587
    graceful_test.go:139: receivedPackets2[7] = 1605
    graceful_test.go:139: receivedPackets2[8] = 1631
    graceful_test.go:139: receivedPackets2[9] = 1689

    graceful_test.go:147: receivedPackets = 18014
    graceful_test.go:148: sentPackets     = 18014
</pre></div>


<p>Malheureusement, arrêter correctement une chaussette UDP n’est pas trivial en
Go<sup id="fnref-c"><a class="footnote-ref" href="#fn-c">10</a></sup>. Auparavant, nous terminions les routines en fermant leurs chaussettes.
Cependant, si nous les fermons trop tôt, l’application perd les paquets qui leur
ont été attribués mais qui n’ont pas encore été traités. Avant de s’arrêter, une
routine doit appeler <code>conn.Read()</code> jusqu’à ce qu’il n’y ait plus de paquets. Une
solution consiste à définir une échéance pour <code>conn.Read()</code> et vérifier si nous
devons arrêter la routine Go lorsque l’échéance est écoulée :</p>
<div class="language-go codehilite"><pre><span/><span class="nx">payload</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nb">make</span><span class="p">([]</span><span class="kt">byte</span><span class="p">,</span><span class="w"> </span><span class="mi">9000</span><span class="p">)</span>
<span class="k">for</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="nx">conn</span><span class="p">.</span><span class="nx">SetReadDeadline</span><span class="p">(</span><span class="nx">time</span><span class="p">.</span><span class="nx">Now</span><span class="p">().</span><span class="nx">Add</span><span class="p">(</span><span class="mi">50</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span><span class="p">))</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="nx">_</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">:=</span><span class="w"> </span><span class="nx">conn</span><span class="p">.</span><span class="nx">Read</span><span class="p">(</span><span class="nx">payload</span><span class="p">);</span><span class="w"> </span><span class="nx">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">nil</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="nx">errors</span><span class="p">.</span><span class="nx">Is</span><span class="p">(</span><span class="nx">err</span><span class="p">,</span><span class="w"> </span><span class="nx">os</span><span class="p">.</span><span class="nx">ErrDeadlineExceeded</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="k">select</span><span class="w"> </span><span class="p">{</span>
<span class="w">            </span><span class="k">case</span><span class="w"> </span><span class="o">&lt;-</span><span class="nx">done</span><span class="p">:</span>
<span class="w">                </span><span class="k">return</span>
<span class="w">            </span><span class="k">default</span><span class="p">:</span>
<span class="w">                </span><span class="k">continue</span>
<span class="w">            </span><span class="p">}</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">Logf</span><span class="p">(</span><span class="s">"Read() error:\n%+v"</span><span class="p">,</span><span class="w"> </span><span class="nx">err</span><span class="p">)</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="o">*</span><span class="nx">packets</span><span class="o">++</span>
<span class="p">}</span>
</pre></div>


<p>Avec TCP, cet aspect est plus simple : après avoir activé le paramètre noyau
<code>net.ipv4.tcp_migrate_req</code>, le noyau migre automatiquement les connexions en
attentes vers une chaussette aléatoire du même groupe. Alternativement, eBPF peut
également <a href="https://docs.ebpf.io/linux/program-type/BPF_PROG_TYPE_SK_REUSEPORT/#socket-migration" title="eBPF and socket migration">contrôler cette migration</a>. Les deux
fonctionnalités sont disponibles depuis Linux 5.14.</p>
<h1 id="addendum">Addendum</h1>
<p>Après avoir implémenté cette stratégie dans Akvorado, toutes les routines
perdent désormais des paquets ! 😱</p>
<div class="language-bash-session codehilite"><pre><span/><span class="gp">$ </span>curl<span class="w"> </span>-s<span class="w"> </span><span class="m">127</span>.0.0.1:8080/api/v0/inlet/metrics<span class="w"> </span><span class="se">\</span>
<span class="gp">&gt; </span><span class="p">|</span><span class="w"> </span>sed<span class="w"> </span>-n<span class="w"> </span>s/akvorado_inlet_flow_input_udp_in_dropped//p
<span class="go">packets_total{listener="0.0.0.0:2055",worker="0"} 838673</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="1"} 843675</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="2"} 837922</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="3"} 841443</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="4"} 840668</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="5"} 850274</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="6"} 835488</span>
<span class="go">packets_total{listener="0.0.0.0:2055",worker="7"} 834479</span>
</pre></div>


<p>La raison majeure est que le module Kafka limite par défaut les lots à 32
messages. Cette limite est trop basse car les brokers ont une charge importante
lors du traitement de chaque lot : ils doivent <a href="https://www.confluent.io/blog/kafka-producer-internals-handling-producer-request/" title="Handling the Producer Request: Kafka Producer and Consumer Internals, Part 2">s’assurer de leur bonne
persistance avant d’accuser réception</a>. Augmenter la limite à <a href="https://github.com/akvorado/akvorado/commit/39cc3728261f5185ba85c5fdd8b034c4c9697a55">4096
messages</a> corrige ce problème.</p>
<p>Bien que la répartition des flux entrants avec eBPF reste utile, elle n’a pas
résolu le problème principal. Au moins, la distribution uniforme des paquets
perdus a aidé à identifier le véritable goulot d’étranglement. 😅</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn-accuracy">
<p>La version actuelle de la page de manuel est incomplète et ne
couvre pas l’évolution introduite dans Linux 4.19. Il existe un <a href="https://git.kernel.org/pub/scm/docs/man-pages/man-pages.git/commit/?id=41788bdd42312828532c4ddbadc0a4d28426d4fd" title="man/man7/socket.7: Fix documentation for SO_ATTACH_REUSEPORT_PORT">patch en
attente</a> à ce sujet. <a class="footnote-backref" href="#fnref-accuracy" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn-rust">
<p><a href="https://aya-rs.dev/book/" title="Building eBPF Programs with Aya">Rust</a> est une autre option. Cependant, le programme que nous
utilisons est si trivial qu’il est superflu d’utiliser Rust. <a class="footnote-backref" href="#fnref-rust" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn-random">
<p>Comme <code>bpf_get_prandom_u32()</code> renvoie une valeur pseudo-aléatoire de
32 bits, cette méthode présente un très léger biais vers les premiers index.
Cela ne vaut probablement pas la peine d’être corrigé. <a class="footnote-backref" href="#fnref-random" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn-kernel">
<p>Certains exemples incluent <code>&lt;linux/bpf.h&gt;</code> au lieu de <code>"vmlinux.h"</code>.
Cela rend votre programme eBPF dépendant des entêtes du noyau installées. <a class="footnote-backref" href="#fnref-kernel" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn-listenaddr">
<p><code>listenAddr</code> est initialement défini à <code>127.0.0.1:0</code> pour allouer
un port aléatoire. Après la première itération, il est mis à jour avec le
port alloué. <a class="footnote-backref" href="#fnref-listenaddr" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn-s1">
<p>Il s’agit de la fonction <code>setupSockets()</code> dans <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/fixtures_test.go#L16-L38"><code>fixtures_test.go</code></a>. <a class="footnote-backref" href="#fnref-s1" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn-s2">
<p>Il s’agit de la fonction <code>setupEBPF()</code> dans <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/fixtures_test.go#L40-L68"><code>fixtures_test.go</code></a>. <a class="footnote-backref" href="#fnref-s2" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn-s3">
<p>Le code complet se trouve dans <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/balancing_test.go"><code>balancing_test.go</code></a> <a class="footnote-backref" href="#fnref-s3" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn-s4">
<p>Le code complet se trouve dans <a href="https://github.com/vincentbernat/ebpf-reuseport-go/blob/f932198b577a6ae48560434ca91ec471d9ed7d75/graceful_test.go"><code>graceful_test.go</code></a> <a class="footnote-backref" href="#fnref-s4" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn-c">
<p>Avec C, nous pouvons appeler <code>poll()</code> sur la chaussette et sur un tube
pour signaler l’arrêt de la routine. Une fois la seconde condition activée,
une série d’appels non bloquants à <code>read()</code> permet de vider la chaussette
des paquets restants, jusqu’à recevoir <code>EWOULDBLOCK</code>. <a class="footnote-backref" href="#fnref-c" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
</ol>
</div>
      </div></content>
  </entry>
</feed>