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

  <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/><code><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>
</code></pre></div>

<p>Nous créons une partition sur chacun d’eux :</p>
<div class="language-bash-session codehilite"><pre><span/><code><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>
</code></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/><code><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>
</code></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/><code><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>
</code></pre></div>

<p>Nous obtenons la configuration suivante :</p>
<figure class="lf-fullbleed"><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://media-bernat-ch.b-cdn.net/images/raid-md0.9a97f806e8b366.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/><code><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>
</code></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/><code><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>
</code></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/><code><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>
</code></pre></div>

<p>Nous ajoutons <code>/dev/md1</code> au groupe de volumes :</p>
<div class="language-bash-session codehilite"><pre><span/><code><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>
</code></pre></div>

<p>Nous obtenons la configuration suivante<sup id="fnref:lsblk"><a class="footnote-ref" href="#fn:lsblk">3</a></sup> :</p>
<figure class="lf-fullbleed"><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://media-bernat-ch.b-cdn.net/images/raid-md0-4-md1-2.346353151ddf57.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/><code><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>
</code></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/><code><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>
</code></pre></div>

<p>Nous obtenons la disposition suivante :</p>
<figure class="lf-fullbleed"><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://media-bernat-ch.b-cdn.net/images/raid-md0-4-md1-3.d53108978a7f1f.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/><code><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>
</code></pre></div>

<p>Agrandissons le second volume RAID 5 :</p>
<div class="language-bash-session codehilite"><pre><span/><code><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>
</code></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/><code><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>
</code></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/netdev/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/netdev/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/netdev/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="Page de manuel pour 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/netdev/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/><code><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>
</code></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/><code><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
</code></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/><code><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>
</code></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/><code><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>
</code></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/><code><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>
</code></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/><code><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="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="kt">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="o">=</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="o">=</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="o">=</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>
</code></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/><code><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="kt">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="kt">uint32</span><span class="p">(</span><span class="nx">worker</span><span class="p">),</span><span class="w"> </span><span class="kt">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="kt">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>
</code></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/><code><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>
</code></pre></div>

<p>Envoyons 1000 paquets :</p>
<div class="language-go codehilite"><pre><span/><code><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="kt">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>
</code></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/><code>=== 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
</code></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/><code><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="kt">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="kt">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>
</code></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/><code>=== 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
</code></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/><code><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>
</code></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/><code><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>
</code></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>
  <entry>
    <title type="html">Répartiteur de charge à multiples niveaux avec Linux</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2018-repartiteur-charge-multi-niveaux" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2018-repartiteur-charge-multi-niveaux#isso-thread" rel="replies" type="text/html"/>
    <updated>2018-05-23T06:59:53Z</updated>
    <id>http://www.luffy.cx/fr/blog/2018-repartiteur-charge-multi-niveaux.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Une solution courante pour fournir un service hautement disponible et évolutif
consiste à insérer une couche d’équilibrage de charge pour répartir les requêtes
des utilisateurs vers les serveurs frontaux<sup id="fnref:backend"><a class="footnote-ref" href="#fn:backend">1</a></sup>. Nous avons habituellement
plusieurs attentes à l’égard d’une telle couche :</p>
<dl>
<dt>Évolutivité</dt>
<dd>Elle permet à un service de monter en charge en poussant le trafic vers des
  serveurs nouvellement provisionnés. Elle est également capable de s’étendre si
  elle devient le goulot d’étranglement.</dd>
<dt>Disponibilité</dt>
<dd>Elle fournit la haute disponibilité au service. Si un serveur devient
  indisponible, le trafic est rapidement redirigé vers un autre serveur. Elle
  doit également être elle-même hautement disponible.</dd>
<dt>Flexibilité</dt>
<dd>Elle gère aussi bien les connexions de courtes et de longues durées. Elle est
  suffisamment flexible pour offrir toutes les fonctionnalités généralement
  attendues d’un répartiteur de charge comme TLS ou le routage HTTP.</dd>
<dt>Opérabilité</dt>
<dd>Avec un peu de coopération, tout changement prévu est transparent : mise à
  niveau des frontaux, ajout ou suppression de frontaux ou changement de
  topologie de la couche de répartition elle-même.</dd>
</dl>
<p>Le problème et ses solutions sont bien connus. Parmi les articles récemment
parus sur le sujet, « <a href="https://web.archive.org/web/20181123100637/https://blog.envoyproxy.io/introduction-to-modern-network-load-balancing-and-proxying-a57f6ff80236?gi=485ded021f22" title="Introduction to modern network load balancing and proxying">Introduction to modern network load-balancing and
proxying</a> » donne un aperçu de l’état de l’art. Google a publié « <a href="https://research.google/pubs/maglev-a-fast-and-reliable-software-network-load-balancer/" title="Maglev: A Fast and Reliable Software Network Load Balancer">Maglev: A
Fast and Reliable Software Network Load Balancer</a> » décrivant en détail
leur solution interne<sup id="fnref:morningpaper"><a class="footnote-ref" href="#fn:morningpaper">2</a></sup>. Cependant, le logiciel associé n’est pas
disponible. Fondamentalement, la construction d’une solution d’équilibrage de
charge consiste à assembler trois composants :</p>
<ul>
<li>routage <abbr title="Equal-Cost Multipath">ECMP</abbr> ;</li>
<li>répartition L4 (sans état) ;</li>
<li>répartition L7 (avec état).</li>
</ul>
<p>Dans cet article, je décris une solution à multiples niveaux utilisant Linux et
des composants open-source. Cela offre une base pour construire une couche
d’équilibrage de charge prête à la production.</p>
<div class="toc">
<ul>
<li><a href="#dernier-niveau-repartition-l7">Dernier niveau : répartition L7</a></li>
<li><a href="#premier-niveau-routage-ecmp">Premier niveau : routage ECMP</a></li>
<li><a href="#second-niveau-repartition-l4">Second niveau : répartition L4</a><ul>
<li><a href="#configuration">Configuration</a></li>
<li><a href="#evaluation-de-la-robustesse">Évaluation de la robustesse</a></li>
</ul>
</li>
<li><a href="#niveau-0-repartition-de-charge-via-le-dns">Niveau 0 : répartition de charge via le DNS</a></li>
</ul>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (05.2018)</p>
<p>Facebook vient de publier <a href="https://engineering.fb.com/2018/05/22/open-source/open-sourcing-katran-a-scalable-network-load-balancer/" title="Open-sourcing Katran, a scalable network load balancer">Katran</a>, un
répartiteur de charge L4 utilisant <abbr title="eXpress Data Path">XDP</abbr> et <abbr title="Enhanced Berkeley Packet Filter">eBPF</abbr> ainsi que du hachage cohérent. Il
pourrait s’insérer dans la configuration décrite ci-dessous.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (08.2018)</p>
<p>GitHub vient de publier <a href="https://github.com/github/glb-director" title="GitHub Load Balancer Director">GLB Director</a>, un répartiteur
de charge L4 avec un <a href="https://en.wikipedia.org/wiki/Rendezvous_hashing" title="Rendezvous hashing sur Wikipedia">algorithme de hachage</a> sélectionnant
un couple de répartiteurs L7. À l’aide d’un module <em>Netfilter</em>, le premier
membre redirige le flux vers le second lorsqu’il ne trouve pas d’entrée
correspondante dans sa table de connexions.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (12.2021)</p>
<p>Cilium 1.10+ utilise une approche similaire
à celle décrite dans cet article pour gérer les services dans une
grappe Kubernetes. Jetez un œil sur la documentation sur comment
utiliser <a href="https://docs.cilium.io/en/v1.12/gettingstarted/kubeproxy-free/">Kubernetes sans kube-proxy</a>
ainsi que celle sur <a href="https://docs.cilium.io/en/v1.12/gettingstarted/bgp/">comment configurer <abbr title="Border Gateway Protocol">BGP</abbr></a>.</p>
</div>
<h1 id="dernier-niveau-repartition-l7">Dernier niveau : répartition L7</h1>
<p>Commençons par le dernier niveau. Son rôle est de fournir la haute
disponibilité, en transférant les requêtes vers les frontaux sains, ainsi que
l’évolutivité, en répartissant équitablement les requêtes. Travaillant dans les
couches supérieures du <a href="https://fr.wikipedia.org/wiki/Mod%C3%A8le_OSI" title="Modèle OSI sur Wikipedia">modèle OSI</a>, il peut également offrir des
services supplémentaires, comme la terminaison TLS, le routage HTTP, la
réécriture des entêtes, la limitation du débit des utilisateurs non
authentifiés, etc. Il peut tirer parti d’algorithmes complexes d’équilibrage de
charge. En tant que premier point de contact avec les serveurs frontaux, il doit
faciliter les maintenances et minimiser l’impact lors des changements
quotidiens.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 270px"><span class="lf-media-inner" style="padding-bottom: 185.556%"><img alt="Répartiteurs de charge L7" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/l7.6971369bdf1acd.svg" width="270" height="501" class="lf-media"/></span></div><figcaption>Le dernier niveau de la solution de répartition de charge est un ensemble d'équilibreurs de charge L7 recevant les connexions des utilisateurs et les transférant vers les frontaux.</figcaption></figure>
<p>Il termine également les connexions TCP des clients. Cela découple l’étage de
répartition des serveurs frontaux avec les avantages suivants :</p>
<ul>
<li>les connexions vers les frontaux peuvent être maintenues ouvertes pour
   réduire l’utilisation des ressources et la latence ;</li>
<li>les requêtes peuvent être réessayées de manière transparente en cas de
   défaillance ;</li>
<li>les clients peuvent utiliser un protocole IP différent des serveurs ;</li>
<li>les frontaux n’ont pas à se soucier de la <a href="https://fr.wikipedia.org/wiki/Path_MTU_discovery" title="Path MTU discovery sur Wikipedia">découverte de la <abbr title="Maximum Transmission Unit">MTU</abbr> du
   chemin</a>, des algorithmes de congestion TCP, de la gestion
   de l’<a href="/fr/blog/2014-tcp-time-wait-state-linux" title="TCP TIME-WAIT &amp; les serveurs Linux à fort trafic">état TIME-WAIT</a> ou d’autres détails
   de bas niveau.</li>
</ul>
<p>De nombreux logiciels conviennent pour cette couche et il existe une littérature
abondante sur la façon de les configurer. Vous pouvez regarder <a href="https://www.haproxy.org/" title="Reliable, High Performance TCP/HTTP Load Balancer">HAProxy</a>,
<a href="https://www.envoyproxy.io/" title="Envoy is an open source edge and service proxy">Envoy</a> ou <a href="https://traefik.io/traefik" title="Traefik: modern HTTP reverse proxy and load-balancer">Traefik</a>. Voici un exemple de configuration pour <em>HAProxy</em> :</p>
<div class="language-haproxy codehilite"><pre><span/><code><span class="c c-Singleline"># Point d'entrée de la répartition de charge L7</span>
<span class="kn">frontend</span> <span class="n">l7lb</span>
  <span class="c c-Singleline"># Écoute à la fois en IPv4 et IPv6</span>
  <span class="nf">bind</span> :80 v4v6
  <span class="c c-Singleline"># Redirige tout sur un ensemble de serveurs frontaux</span>
  <span class="nf">default_backend</span> servers
  <span class="c c-Singleline"># Vérification de la bonne santé</span>
  <span class="nf">acl</span> dead nbsrv(servers) lt <span class="m">1</span>
  <span class="nf">acl</span> disabled nbsrv(enabler) lt <span class="m">1</span>
  <span class="nf">monitor-uri</span> /healthcheck
  <span class="nf">monitor</span> fail if dead || disabled

<span class="c c-Singleline"># Serveurs frontaux en IPv6 avec tests HTTP et via un agent</span>
<span class="kn">backend</span> <span class="n">servers</span>
  <span class="nf">balance</span> roundrobin
  <span class="nf">option</span> httpchk
  <span class="nf">server</span> web1 <span class="p">[</span><span class="m">2001:db8:1:0:2::1</span><span class="p">]:</span><span class="m">80</span> send-proxy check agent-check agent-port <span class="m">5555</span>
  <span class="nf">server</span> web2 <span class="p">[</span><span class="m">2001:db8:1:0:2::2</span><span class="p">]:</span><span class="m">80</span> send-proxy check agent-check agent-port <span class="m">5555</span>
  <span class="nf">server</span> web3 <span class="p">[</span><span class="m">2001:db8:1:0:2::3</span><span class="p">]:</span><span class="m">80</span> send-proxy check agent-check agent-port <span class="m">5555</span>
  <span class="nf">server</span> web4 <span class="p">[</span><span class="m">2001:db8:1:0:2::4</span><span class="p">]:</span><span class="m">80</span> send-proxy check agent-check agent-port <span class="m">5555</span>

<span class="c c-Singleline"># Faux serveur gérant la disponibilitant du répartiteur de charge lui-même</span>
<span class="kn">backend</span> <span class="n">enabler</span>
  <span class="nf">server</span> enabler <span class="p">[</span><span class="m">::1</span><span class="p">]:</span><span class="m">0</span> agent-check agent-port <span class="m">5555</span>
</code></pre></div>

<p>Cette configuration est très sommaire mais permet d’illustrer deux notions clés
pour l’opérabilité :</p>
<ol>
<li>
<p>Les frontaux sont testés à la fois au niveau HTTP (avec <code>check</code> et <code>option
    httpchk</code>) et via un <a href="https://cbonte.github.io/haproxy-dconv/1.8/configuration.html#5.2-agent-check" title="agent-check directive in HAProxy documentation">agent auxiliaire</a> (avec
    <code>agent-check</code>). Ce dernier permet de placer un serveur en maintenance pour
    effectuer une mise en production progressive. Sur chaque frontal, un
    processus écoute sur le port 5555 et répond avec le statut du service (<code>UP</code>,
    <code>DOWN</code>, <code>MAINT</code>). Un simple <code>socat</code> fait l’affaire<sup id="fnref:agentcheck"><a class="footnote-ref" href="#fn:agentcheck">3</a></sup> :</p>
<div class="language-bash codehilite"><pre><span/><code>socat<span class="w"> </span>-ly<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>TCP6-LISTEN:5555,ipv6only<span class="o">=</span><span class="m">0</span>,reuseaddr,fork<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>OPEN:/etc/lb/agent-check,rdonly
</code></pre></div>

<p>Dans <code>/etc/lb/agent-check</code>, <code>UP</code> indique que le service est en mode nominal.
Si le test HTTP est aussi positif, <em>HAProxy</em> enverra des requêtes vers ce
nœud. Si vous devez le mettre en maintenance, écrivez <code>MAINT</code> et attendez
que les connexions en cours se terminent. Utilisez <code>READY</code> pour annuler ce
mode.</p>
</li>
<li>
<p>Le répartiteur de charge lui-même fournit un point de diagnostic
    (<code>/healthcheck</code>) pour les niveaux supérieurs. Il retourne une erreur 503 si
    aucun frontal n’est disponible ou si le serveur <code>enabler</code> est indiqué comme
    indisponible via l’agent. Le même mécanisme que pour les serveurs frontaux
    classiques peut alors être utilisé pour signaler l’indisponibilité de cet
    équilibreur de charge.</p>
</li>
</ol>
<p>De plus, la directive <code>send-proxy</code> permet d’utiliser le <a href="https://www.haproxy.com/blog/use-the-proxy-protocol-to-preserve-a-clients-ip-address" title="Proxy Protocol">protocole proxy</a> afin de transmettre les adresses IP réelles des clients. Ce protocole
fonctionne également pour les connexions non-HTTP et est supporté par de
nombreux serveurs, y compris <em>nginx</em> :</p>
<div class="language-nginx-configuration-file codehilite"><pre><span/><code><span class="k">http</span><span class="w"> </span><span class="p">{</span>
<span class="w">  </span><span class="kn">server</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="kn">listen</span><span class="w"> </span><span class="s">[::]:80</span><span class="w"> </span><span class="s">default</span><span class="w"> </span><span class="s">ipv6only=off</span><span class="w"> </span><span class="s">proxy_protocol</span><span class="p">;</span>
<span class="w">    </span><span class="kn">root</span><span class="w"> </span><span class="s">/var/www</span><span class="p">;</span>
<span class="w">    </span><span class="kn">set_real_ip_from</span><span class="w"> </span><span class="p">::</span><span class="s">/0</span><span class="p">;</span>
<span class="w">    </span><span class="kn">real_ip_header</span><span class="w"> </span><span class="s">proxy_protocol</span><span class="p">;</span>
<span class="w">  </span><span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p>En l’état, cette solution n’est pas complète. Nous avons déplacé le problème de
disponibilité et d’évolutivité ailleurs. Comment répartir les demandes entre les
équilibreurs de charge ?</p>
<h1 id="premier-niveau-routage-ecmp">Premier niveau : routage <abbr title="Equal-Cost Multipath">ECMP</abbr></h1>
<p>Sur la plupart des réseaux IP modernes, il existe des chemins redondants entre
les clients et les serveurs. Pour chaque paquet, les routeurs doivent choisir
une branche. Lorsque le coût associé à chaque trajet est égal, les flux
entrants<sup id="fnref:flow"><a class="footnote-ref" href="#fn:flow">4</a></sup> sont répartis entre les destinations disponibles. Cette
caractéristique peut être utilisée pour répartir les connexions entre les
équilibreurs de charge disponibles :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 251px"><span class="lf-media-inner" style="padding-bottom: 141.036%"><img alt="Routage ECMP" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/ecmp.ce13edbd2c96c5.svg" width="251" height="354" class="lf-media"/></span></div><figcaption>Le routage ECMP est utilisé comme premier étage. Les flux sont répartis entre les équilibreurs de charge L7 disponibles. Le routage est sans état et asymétrique. Les serveurs frontaux ne sont pas représentés.</figcaption></figure>
<p>Il y a peu de contrôle sur la répartition des flux, mais le routage <abbr title="Equal-Cost Multipath">ECMP</abbr> apporte
la possibilité de faire évoluer horizontalement les deux niveaux. Une mise en
œuvre courante d’une telle solution est d’utiliser <abbr title="Border Gateway Protocol">BGP</abbr>, un protocole de routage
pour échanger des routes entre les équipements du réseau. Chaque répartiteur de
charge annonce aux routeurs auxquels il est connecté les adresses IP qu’il
dessert.</p>
<p>En supposant que vous avez déjà des routeurs avec <abbr title="Border Gateway Protocol">BGP</abbr>, <a href="https://github.com/Exa-Networks/exabgp" title="The BGP swiss army knife of networking">ExaBGP</a> est une
solution flexible pour permettre aux répartiteurs de charge d’annoncer leur
disponibilité. Voici un exemple de configuration :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="c c-Singleline"># Test de disponibilité en IPv6</span>
<span class="k">process</span> service-v6 <span class="p">{</span>
  run python -m exabgp healthcheck -s --interval <span class="m">10</span> --increase <span class="m">0</span> --cmd <span class="s2">"test -f /etc/lb/v6-ready -a ! -f /etc/lb/disable"</span><span class="p">;</span>
  encoder text;
<span class="p">}</span>

<span class="k">template</span> <span class="p">{</span>
  <span class="c c-Singleline"># Patron pour un routeur IPv6</span>
  neighbor v6 <span class="p">{</span>
    router-id <span class="m">192.0.2.132</span><span class="p">;</span>
    local-address <span class="m">2001:db8::192</span>.0.2.132;
    local-as <span class="m">65000</span><span class="p">;</span>
    peer-as <span class="m">65000</span><span class="p">;</span>
    hold-time <span class="m">6</span><span class="p">;</span>
    family <span class="p">{</span>
      ipv6 unicast;
    <span class="p">}</span>
    api services-v6 <span class="p">{</span>
      processes [ service-v6 ];
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">}</span>

<span class="c c-Singleline"># Premier routeur</span>
<span class="k">neighbor</span> <span class="m">2001:db8::192</span>.0.2.254 <span class="p">{</span>
  inherit v6;
<span class="p">}</span>

<span class="c c-Singleline"># Second routeur</span>
<span class="k">neighbor</span> <span class="m">2001:db8::192</span>.0.2.253 <span class="p">{</span>
  inherit v6;
<span class="p">}</span>
</code></pre></div>

<p>Si <code>/etc/lb/v6-ready</code> est présent mais que <code>/etc/lb/disable</code> est absent, toutes
les IP configurées sur l’interface <code>lo</code> sont annoncées aux deux routeurs. Si les
autres répartiteurs de charge ont une configuration similaire, les routeurs leur
distribuent équitablement les flux reçus. Un processus externe doit gérer
l’existence du fichier <code>/etc/lb/v6-ready</code> en vérifiant la bonne santé du
répartiteur de charge (à l’aide du point <code>/healthcheck</code> par exemple). Un
opérateur peut retirer un répartiteur de charge de la rotation en créant le
fichier <code>/etc/lb/disable</code>.</p>
<p>Pour plus de détails concernant cette partie, jetez un œil sur « <a href="/fr/blog/2013-exabgp-haute-dispo" title="Redondance avec ExaBGP">Redondance
avec ExaBGP</a> ». Si vous êtes hébergés dans les
nuages, ce tiers est généralement mis en place par votre fournisseur sous forme
d’une IP « élastique » ou d’un service de répartition L4.</p>
<p>Malheureusement, cette solution n’est pas robuste lorsqu’un changement, prévu ou
non, se produit. Notamment, lors de l’ajout ou de la suppression d’un
équilibreur de charge, le nombre de routes disponibles pour une destination
change. L’algorithme de hachage utilisé par les routeurs n’est pas cohérent et
les flux sont redistribués entre les répartiteurs de charge disponibles, rompant
les connexions existantes :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 611px"><span class="lf-media-inner" style="padding-bottom: 52.209%"><img alt="Stabilité du routage ECMP 1/2" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/ecmp-failure1.1e4c8015bd6071.svg" width="611" height="319" class="lf-media"/></span></div><figcaption>Le routage ECMP est instable lorsqu'un changement se produit. Un équilibreur de charge supplémentaire est ajouté et chaque flux est acheminé vers un répartiteur différent qui n'a pas les entrées appropriées dans sa table de connexions.</figcaption></figure>
<p>De plus, chaque routeur peut choisir ses propres chemins. Quand un routeur
devient indisponible, le second peut router les mêmes flux différemment :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 531px"><span class="lf-media-inner" style="padding-bottom: 60.075%"><img alt="Stabilité du routage ECMP 2/2" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/ecmp-failure2.93310967861cf4.svg" width="531" height="319" class="lf-media"/></span></div><figcaption>Un routeur devient indisponible et le routeur restant route différemment ses flux. L'un d'entre eux est acheminé vers un répartiteur de charge différent qui n'a pas l'entrée appropriée dans sa table des connexions.</figcaption></figure>
<p>Si vous pensez que ce n’est pas un résultat acceptable, notamment si vous devez
gérer de longues connexions comme le téléchargement de fichiers, le streaming
vidéo ou les connexions <em>websocket</em>, vous avez besoin d’un niveau
supplémentaire. Continuez la lecture !</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (03.2021)</p>
<p>Certains constructeurs proposent un
algorithme de hachage résilient pour contourner cette limitation.
Jetez par exemple un œil sur la documentation chez
<a href="https://www.juniper.net/documentation/us/en/software/junos/interfaces-ethernet-switches/topics/topic-map/hashing-algoriths-lag-bundle-ecmp.html#id_ng1_p23_rfc" title="Understanding the Use of Resilient Hashing to Minimize Flow Remapping in Trunk/ECMP Groups">Juniper</a> et
<a href="https://docs.nvidia.com/networking-ethernet-software/cumulus-linux-41/Layer-3/Equal-Cost-Multipath-Load-Sharing-Hardware-ECMP/" title="Equal Cost Multipath Load Sharing - Hardware ECMP">Cumulus</a>. Merci à Jessy Vetter pour cette
information ! Cette fonctionnalité a également été <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=2a0186a37700b0d5b8cc40be202a62af44f02fa2" title="nexthop: Resilient next-hop groups">introduite dans
Linux 5.12</a>.</p>
</div>
<h1 id="second-niveau-repartition-l4">Second niveau : répartition L4</h1>
<p>Le deuxième niveau est la glue entre le monde <em>sans état</em> des routeurs IP et le
pays <em>avec état</em> de l’équilibrage de charge L7. Il est mis en œuvre grâce à
l’équilibrage de charge L4. La terminologie peut être un peu confuse ici : ce
niveau route les datagrammes IP (pas de terminaison TCP) mais l’algorithme de
répartition utilise à la fois l’IP de destination et le port pour choisir un
serveur disponible dans le niveau suivant. Le but de cet étage est de s’assurer
que tous les membres prennent la même décision d’ordonnancement pour un paquet
entrant.</p>
<p>Il y a deux possibilités :</p>
<ul>
<li>répartition de charge L4 avec synchronisation des états ;</li>
<li>répartition de charge L4 avec hachage cohérent.</li>
</ul>
<p>La première option augmente la complexité et limite l’évolutivité. Nous ne
l’explorons pas<sup id="fnref:conntrackd"><a class="footnote-ref" href="#fn:conntrackd">5</a></sup>. La seconde option est moins robuste aux
changements mais cela peut être amélioré via une approche hybride avec un état
local.</p>
<p>Nous utilisons <a href="https://www.linuxvirtualserver.org/software/ipvs.html" title="Advanced Layer-4 Switching">IPVS</a>, un répartiteur de charge L4 performant fonctionnant
dans le noyau Linux. Il est piloté par <a href="https://www.keepalived.org/" title="Keepalived for Linux">Keepalived</a> qui dispose d’un ensemble
de tests de disponibilité pour détecter les problèmes. <em>IPVS</em> est configuré pour
utiliser <em>Maglev</em>, un <a href="https://research.google/pubs/maglev-a-fast-and-reliable-software-network-load-balancer/" title="Maglev: A Fast and Reliable Software Network Load Balancer">algorithme de hachage cohérent créé par Google</a>.
Dans sa famille, c’est un bon algorithme car il répartit les connexions de
manière équitable, minimise les impacts consécutifs à un changement et est
particulièrement rapide pour construire sa table de correspondance. Enfin, pour
améliorer les performances, le dernier niveau (les répartiteurs de charge L7)
répond aux clients directement sans impliquer le second niveau (les répartiteurs
de charge L4). Ce mécanisme est connu sous le nom de <em>direct server return</em>
(<abbr title="Direct Server Return">DSR</abbr>) ou <em>direct routing</em> (<abbr title="Direct Routing">DR</abbr>).</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 314px"><span class="lf-media-inner" style="padding-bottom: 181.847%"><img alt="Second niveau : répartition L4" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/l4.7a1fa53f25316a.svg" width="314" height="571" class="lf-media"/></span></div><figcaption>Équilibrage de charge L4 avec IPVS et hachage cohérent liant le premier et le troisième niveau. Les serveurs frontaux ont été omis. Les lignes en pointillés représentent le chemin pris par les paquets retour.</figcaption></figure>
<p>Avec une telle configuration, on s’attend à ce que les paquets appartenant à un
flux puissent se déplacer librement entre les composants des deux premiers
niveaux tout en finissant sur le même équilibreur de charge L7.</p>
<h2 id="configuration">Configuration</h2>
<p>Une fois <em>ExaBGP</em> configuré comme décrit dans la section précédente, nous
pouvons passer à la configuration de <em>Keepalived</em> :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">virtual_server_group</span> VS_GROUP_MH_IPv6 <span class="p">{</span>
  <span class="m">2001:db8::198</span>.51.100.1 <span class="m">80</span>
<span class="p">}</span>
<span class="k">virtual_server</span> group VS_GROUP_MH_IPv6 <span class="p">{</span>
  lvs_method TUN  <span class="c c-Singleline"># Mode tunnel pour DSR</span>
  lvs_sched mh    <span class="c c-Singleline"># Algorithme : Maglev</span>
  mh-port         <span class="c c-Singleline"># Prend en compte les ports TCP</span>
  protocol TCP
  delay_loop <span class="m">5</span>
  alpha           <span class="c c-Singleline"># Les serveurs sont considérés inaccessibles au démarrage</span>
  omega           <span class="c c-Singleline"># Exécute quorum_down à l'arrêt</span>
  quorum_up   <span class="s2">"/bin/touch /etc/lb/v6-ready"</span>
  quorum_down <span class="s2">"/bin/rm -f /etc/lb/v6-ready"</span>

  <span class="c c-Singleline"># Premier répartiteur de charge L7</span>
  real_server <span class="m">2001:db8::192</span>.0.2.132 <span class="m">80</span> <span class="p">{</span>
    weight <span class="m">1</span>
    HTTP_GET <span class="p">{</span>
      url <span class="p">{</span>
        path /healthcheck
        status_code <span class="m">200</span>
      <span class="p">}</span>
      connect_timeout <span class="m">2</span>
    <span class="p">}</span>
  <span class="p">}</span>

  <span class="c c-Singleline"># Tous les autres...</span>
<span class="p">}</span>
</code></pre></div>

<p>Les directives <code>quorum_up</code> et <code>quorum_down</code> définissent les commandes à exécuter
quand le service devient respectivement accessible et inaccessible. Le fichier
<code>/etc/lb/v6-ready</code> est utilisé pour signaler à <em>ExaBGP</em> s’il doit ou non publier
l’adresse IP du service aux routeurs.</p>
<p>De plus, <em>IPVS</em> doit être configuré pour router les paquets appartenant à un
flux traité initialement par un autre nœud. Il doit également continuer de
router les paquets quand une destination devient indisponible afin de s’assurer
qu’on puisse mettre hors service proprement un répartiteur de charge L7.</p>
<div class="language-bash codehilite"><pre><span/><code><span class="c1"># Prend aussi en charge les paquets non SYN</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.vs.sloppy_tcp<span class="o">=</span><span class="m">1</span>
<span class="c1"># Ne PAS rerouter une connexion quand une destination</span>
<span class="c1"># devient invalide.</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.vs.expire_nodest_conn<span class="o">=</span><span class="m">0</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.vs.expire_quiescent_template<span class="o">=</span><span class="m">0</span>
</code></pre></div>

<p>L’algorithme <em>Maglev</em> sera disponible dans Linux 4.18, grâce au travail de <a href="https://git.kernel.org/pub/scm/linux/kernel/git/netdev/net-next.git/commit/?id=039f32e8cdea29b4d0680df7a83817b5ec4166e1" title="netfilter: ipvs: Add Maglev hashing scheduler">Inju
Song</a>. Pour les noyaux plus anciens, j’ai préparé un
<a href="https://github.com/vincentbernat/ip_vs_mh" title="Backport of consistent source hashing scheduler for Linux IPVS">rétroportage</a><sup id="fnref:backport"><a class="footnote-ref" href="#fn:backport">6</a></sup>. Le substituer par un autre algorithme, tel
que <code>sh</code>, rend l’ensemble moins robuste.</p>
<p>Le <abbr title="Direct Server Return">DSR</abbr> est mis en place avec le mode tunnel. Cette méthode est compatible avec
les réseaux routés. Les requêtes sont encapsulées vers le nœud choisi à l’aide du
<a href="https://web.archive.org/web/2018/http://www.networksorcery.com/enp/protocol/ip-ip.htm" title="IP in IP Encapsulation">protocole IPIP</a>. Cela ajoute un léger surcoût et entraîne
des problèmes de <abbr title="Maximum Transmission Unit">MTU</abbr>. Si possible, utilisez une <abbr title="Maximum Transmission Unit">MTU</abbr> plus grande entre le second
et troisième niveau<sup id="fnref:mtu"><a class="footnote-ref" href="#fn:mtu">7</a></sup>. Dans le cas contraire, autorisez explicitement la
fragmentation des paquets IP :</p>
<div class="language-bash codehilite"><pre><span/><code>sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.vs.pmtu_disc<span class="o">=</span><span class="m">0</span>
</code></pre></div>

<p>Il faut aussi configurer les répartiteurs de charge L7 pour accepter ce trafic
encapsulé<sup id="fnref:firewall"><a class="footnote-ref" href="#fn:firewall">8</a></sup> :</p>
<div class="language-bash codehilite"><pre><span/><code><span class="c1"># Configure le tunnel IPIP pour accepter des paquets de n'importe quelle source</span>
ip<span class="w"> </span>tunnel<span class="w"> </span>add<span class="w"> </span>tunlv6<span class="w"> </span>mode<span class="w"> </span>ip6ip6<span class="w"> </span><span class="nb">local</span><span class="w"> </span><span class="m">2001</span>:db8::192.0.2.132
ip<span class="w"> </span>link<span class="w"> </span><span class="nb">set</span><span class="w"> </span>up<span class="w"> </span>dev<span class="w"> </span>tunlv6
ip<span class="w"> </span>addr<span class="w"> </span>add<span class="w"> </span><span class="m">2001</span>:db8::198.51.100.1/128<span class="w"> </span>dev<span class="w"> </span>tunlv6
</code></pre></div>

<h2 id="evaluation-de-la-robustesse">Évaluation de la robustesse</h2>
<p>Ainsi configuré, le second niveau améliore la robustesse de l’ensemble pour deux
raisons :</p>
<ol>
<li>
<p>L’utilisation d’un algorithme de hachage cohérent pour choisir la
    destination réduit l’impact négatif d’un changement, prévu ou non, en
    minimisant le nombre de flux déplacés vers une nouvelle destination.
    « <a href="https://dgryski.medium.com/consistent-hashing-algorithmic-tradeoffs-ef6b8e2fcae8" title="Consistent Hashing: Algorithmic Tradeoffs">Consistent Hashing: Algorithmic Tradeoffs</a> » offre plus de détails sur
    ce sujet.</p>
</li>
<li>
<p><em>IPVS</em> garde localement une table des connexions pour les flux connus. Quand
    un changement n’impacte que le dernier niveau, les flux existants continuent
    d’être routés correctement en utilisant cette table.</p>
</li>
</ol>
<p>Si nous ajoutons ou retirons un répartiteur L4, les flux existants ne sont pas
impactés car chaque répartiteur prend la même décision grâce au hachage cohérent :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 685px"><span class="lf-media-inner" style="padding-bottom: 77.080%"><img alt="Instabilité de la répartition L4 1/3" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/l4-failure1.6f22cd88a8416b.svg" width="685" height="528" class="lf-media"/></span></div><figcaption>La perte d'un équilibreur L4 n'a pas d'impact sur les flux existants. Chaque flèche est un exemple de flux. Les points sont des connexions liées à l'équilibreur de charge associé. S'ils s'étaient déplacés vers un autre équilibreur, la connexion aurait été perdue.</figcaption></figure>
<p>Lors de l’ajout d’un répartiteur L7, les flux existants ne sont pas impactés non
plus car seules les nouvelles connexions sont routées vers le nouveau
répartiteur. Pour les connexions existantes, <em>IPVS</em> utilise sa table de
connexion locale et continue de router les paquets vers la destination
originale. De manière similaire, le retrait d’un répartiteur L7 n’impacte que
les connexions liées à celui-ci. Les autres connexions sont routées correctement :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 685px"><span class="lf-media-inner" style="padding-bottom: 77.080%"><img alt="Instabilité de la répartition L4 2/3" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/l4-failure2.b944a25431a499.svg" width="685" height="528" class="lf-media"/></span></div><figcaption>La perte d'un équilibreur L7 n'impacte que les flux qu'il hébergeait.</figcaption></figure>
<p>Seuls des changements simultanés sur les deux niveaux mènent à un impact
notable. Par exemple, lors de l’ajout d’un équilibreur de charge L4 et d’un
équilibreur de charge L7, seules les connexions déplacées vers un répartiteur L4
sans état et programmées vers le nouveau répartiteur seront rompues. Grâce à
l’algorithme de hachage cohérent, les autres connexions resteront liées au
répartiteur L7 adéquat. Lors d’un changement planifié, cette perturbation peut
être minimisée en ajoutant d’abord les nouveaux équilibreurs L4, en attendant
quelques minutes puis en ajoutant les nouveaux équilibreurs L7.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 685px"><span class="lf-media-inner" style="padding-bottom: 77.080%"><img alt="Instabilité de la répartition L4 3/3" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/l4-failure3.2608491d5365cb.svg" width="685" height="528" class="lf-media"/></span></div><figcaption>Un équilibreur de charge L4 et un équilibreur de charge L7 reviennent à la vie. L'algorithme de hachage cohérent garantit que seul un cinquième des connexions existantes serait déplacé vers le nouvel équilibreur L7. Certains d'entre eux continuent d'être acheminés par le répartiteur L4 d'origine qui connait la destination correcte, ce qui atténue l'impact.</figcaption></figure>
<p>De plus, <em>IPVS</em> route correctement les messages ICMP vers les mêmes répartiteurs
L7 que les flux associés<sup id="fnref:icmp"><a class="footnote-ref" href="#fn:icmp">9</a></sup>. Cela permet à la découverte de la <abbr title="Maximum Transmission Unit">MTU</abbr> du chemin
de fonctionner correctement sans utiliser de <a href="https://blog.cloudflare.com/path-mtu-discovery-in-practice/" title="Path MTU discovery in practice">techniques palliatives</a>.</p>
<h1 id="niveau-0-repartition-de-charge-via-le-dns">Niveau 0 : répartition de charge via le DNS</h1>
<p>Il est également possible d’ajouter un équilibrage de charge DNS à l’ensemble.
Ceci est utile si votre installation est répartie sur plusieurs centres de
données, plusieurs régions ou si vous voulez diviser une large ferme de
répartition de charge en morceaux plus petits. Il n’est pas destiné à remplacer
le premier niveau car il ne partage pas les mêmes caractéristiques : la
répartition de charge est déséquilibrée (elle n’est pas basée sur les flux) et
la guérison après une panne est lente.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 571px"><span class="lf-media-inner" style="padding-bottom: 130.998%"><img alt="Répartition de charge globale" src="https://media-bernat-ch.b-cdn.net/images/multitier-lb/dns.9617b861d4ac1c.svg" width="571" height="748" class="lf-media"/></span></div><figcaption>Une solution complète de répartition de charge sur deux centres de données.</figcaption></figure>
<p><a href="https://gdnsd.org/" title="Authoritative-only DNS server which does geographic balancing, redirection, weighting, and service-state-conscious failover at the DNS layer">gdnsd</a> est un serveur DNS faisant autorité avec des tests de disponibilité
intégrés. Il peut servir des zones au format <a href="https://www.rfc-editor.org/rfc/rfc1035" title="RFC 1035: Domain names - implementation and specification">RFC 1035</a> :</p>
<div class="language-zone codehilite"><pre><span/><code><span class="o">@</span><span class="w"> </span><span class="kt">SOA</span><span class="w"> </span><span class="l">ns1</span><span class="w"> </span><span class="l">ns1.example.org.</span><span class="w"> </span><span class="mi">1</span><span class="w"> </span><span class="mi">7200</span><span class="w"> </span><span class="mi">1800</span><span class="w"> </span><span class="mi">259200</span><span class="w"> </span><span class="mi">900</span>
<span class="o">@</span><span class="w"> </span><span class="kt">NS</span><span class="w"> </span><span class="l">ns1.example.com.</span>
<span class="o">@</span><span class="w"> </span><span class="kt">NS</span><span class="w"> </span><span class="l">ns1.example.net.</span>
<span class="o">@</span><span class="w"> </span><span class="kt">MX</span><span class="w"> </span><span class="mi">10</span><span class="w"> </span><span class="l">smtp</span>

<span class="o">@</span><span class="w">     </span><span class="mi">60</span><span class="w"> </span><span class="kt">DYNA</span><span class="w"> </span><span class="l">multifo!web</span>
<span class="n">www</span><span class="w">   </span><span class="mi">60</span><span class="w"> </span><span class="kt">DYNA</span><span class="w"> </span><span class="l">multifo!web</span>
<span class="n">smtp</span><span class="w">     </span><span class="kt">A</span><span class="w">    </span><span class="l">198.51.100.99</span>
</code></pre></div>

<p>L’enregistrement spécial <code>DYNA</code> retourne des entrées <code>A</code> et <code>AAAA</code> après avoir
consulté le greffon spécifié. Ici, le greffon <code>multfifo</code> implémente une
surveillance en mode actif/actif des adresses IP de la ferme :</p>
<div class="language-apacheconf codehilite"><pre><span/><code><span class="nb">service_types</span><span class="w"> </span>=&gt;<span class="w"> </span>{
<span class="w">  </span><span class="nb">web</span><span class="w"> </span>=&gt;<span class="w"> </span>{
<span class="w">    </span><span class="nb">plugin</span><span class="w"> </span>=&gt;<span class="w"> </span>http_status
<span class="w">    </span><span class="nb">url_path</span><span class="w"> </span>=&gt;<span class="w"> </span><span class="sx">/healthcheck</span>
<span class="w">    </span><span class="nb">down_thresh</span><span class="w"> </span>=&gt;<span class="w"> </span><span class="m">5</span>
<span class="w">    </span><span class="nb">interval</span><span class="w"> </span>=&gt;<span class="w"> </span><span class="m">5</span>
<span class="w">  </span><span class="err">}</span>
<span class="w">  </span><span class="nb">ext</span><span class="w"> </span>=&gt;<span class="w"> </span>{
<span class="w">    </span><span class="nb">plugin</span><span class="w"> </span>=&gt;<span class="w"> </span>extfile
<span class="w">    </span><span class="nb">file</span><span class="w"> </span>=&gt;<span class="w"> </span><span class="sx">/etc/lb/ext</span>
<span class="w">    </span><span class="nb">def_down</span><span class="w"> </span>=&gt;<span class="w"> </span>false
<span class="w">  </span><span class="err">}</span>
<span class="err">}</span>

<span class="nb">plugins</span><span class="w"> </span>=&gt;<span class="w"> </span>{
<span class="w">  </span><span class="nb">multifo</span><span class="w"> </span>=&gt;<span class="w"> </span>{
<span class="w">    </span><span class="nb">web</span><span class="w"> </span>=&gt;<span class="w"> </span>{
<span class="w">      </span><span class="nb">service_types</span><span class="w"> </span>=&gt;<span class="w"> </span>[<span class="w"> </span>ext,<span class="w"> </span>web<span class="w"> </span>]
<span class="w">      </span><span class="nb">addrs_v4</span><span class="w"> </span>=&gt;<span class="w"> </span>[<span class="w"> </span><span class="m">198.51.100.1</span>,<span class="w"> </span><span class="m">198.51.100.2</span><span class="w"> </span>]
<span class="w">      </span><span class="nb">addrs_v6</span><span class="w"> </span>=&gt;<span class="w"> </span>[<span class="w"> </span><span class="m">2001</span>:db8::198.51.100.1,<span class="w"> </span><span class="m">2001</span>:db8::198.51.100.2<span class="w"> </span>]
<span class="w">    </span><span class="err">}</span>
<span class="w">  </span><span class="err">}</span>
<span class="err">}</span>
</code></pre></div>

<p>En mode nominal, une requête <code>A</code> recevra en réponse à la fois <code>198.51.100.1</code> et
<code>198.51.100.2</code>. Si un test de disponibilité échoue, l’ensemble retourné est mis
à jour. Il est également possible de retirer une IP volontairement en modifiant
le fichier <code>/etc/lb/ext</code>. Par exemple, en mettant le contenu suivant,
<code>198.51.100.2</code> ne fera plus partie des réponses :</p>
<div class="language-text-only codehilite"><pre><span/><code>198.51.100.1 =&gt; UP
198.51.100.2 =&gt; DOWN
2001:db8::c633:6401 =&gt; UP
2001:db8::c633:6402 =&gt; UP
</code></pre></div>

<hr/>
<p>Tous les fichiers de configuration pour la mise en place de chaque niveau sont
disponibles dans un <a href="https://github.com/vincentbernat/network-lab/tree/master/lab-3tier-lb" title="Fichiers de configuration pour une répartition de charge à 4 niveaux">dépôt Git</a>. Si vous voulez reproduire
cette configuration à une échelle plus petite, il est possible de fusionner le
second et le troisième niveau, soit avec des espaces de nom, soit avec une
configuration spécifique de type <a href="https://web.archive.org/web/2018/http://www.austintek.com/LVS/LVS-HOWTO/HOWTO/LVS-HOWTO.localnode.html" title="LVS: LocalNode"><em>localnode</em></a>. Même si vous n’avez
pas besoin de ces services directs, vous devriez garder le dernier niveau :
alors que les serveurs frontaux vont et viennent, les équilibreurs de charge L7
apportent de la stabilité, rendant l’ensemble plus robuste.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:backend">
<p>Dans cet article, les « serveurs frontaux » sont les serveurs
derrière la couche de répartition de charge. Dans la version anglaise,
j’utilise le terme « <em>backend</em> » mais l’équivalent français n’est pas très
agréable. <a class="footnote-backref" href="#fnref:backend" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:morningpaper">
<p>Un bon résumé de ce papier est fait par <a href="https://web.archive.org/web/2018/https://blog.acolyer.org/2016/03/21/maglev-a-fast-and-reliable-software-network-load-balancer/" title="Maglev: A Fast and Reliable Software Network Load Balancer">Adrian Colyer</a>. Du
même auteur, jetez aussi un œil sur le résumé de « <a href="https://web.archive.org/web/2018/https://blog.acolyer.org/2018/05/03/stateless-datacenter-load-balancing-with-beamer/" title="Stateless datacenter load-balancing with Beamer">Stateless datacenter
load-balancing with Beamer</a> ». <a class="footnote-backref" href="#fnref:morningpaper" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:agentcheck">
<p>Si vous pensez que cette solution est fragile, n’hésitez pas à
développer votre propre agent. Il pourrait se coordonner avec un registre
clés/valeurs pour déterminer l’état souhaité du serveur. Il est possible de
centraliser l’agent à un seul endroit, mais vous risquez d’avoir un problème
de poule et d’œuf pour assurer sa disponibilité. <a class="footnote-backref" href="#fnref:agentcheck" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:flow">
<p>Un flux est généralement déterminé par l’IP source et destination et le
protocole L4. Alternativement, le port source et le port de destination
peuvent également être utilisés. Le routeur hache ces informations pour
déterminer la destination. Concernant Linux, vous trouverez plus
d’informations à ce sujet dans « <a href="https://web.archive.org/web/2018/https://cumulusnetworks.com/blog/celebrating-ecmp-part-one/" title="Celebrating ECMP in Linux">Celebrating <abbr title="Equal-Cost Multipath">ECMP</abbr> in Linux</a> ». <a class="footnote-backref" href="#fnref:flow" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:conntrackd">
<p>Avec Linux, cela peut être mis en place en utilisant
<a href="https://www.netfilter.org/" title="Packet filtering framework for Linux">Netfilter</a> pour la répartition de charge et <a href="https://conntrack-tools.netfilter.org/about.html" title="Connection tracking userspace tools for Linux">conntrackd</a> pour
synchroniser les états. <em>IPVS</em> ne permet qu’une synchronisation
actif/passif, limitant l’évolutivité. <a class="footnote-backref" href="#fnref:conntrackd" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:backport">
<p>Le rétroportage n’est pas fonctionnellement équivalent à la version
originale. Consultez le <a href="https://github.com/vincentbernat/ip_vs_mh/blob/master/README.md#differences-with-the-upstream-module">fichier <code>README</code></a> pour comprendre les
différences. <a class="footnote-backref" href="#fnref:backport" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:mtu">
<p>Au moins 1520 pour IPv4 et 1540 pour IPv6. <a class="footnote-backref" href="#fnref:mtu" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:firewall">
<p>En l’état, cette configuration n’est pas sûre. Vous devez vous
assurer que seuls les répartiteurs de charge L4 seront en mesure d’envoyer
du trafic IPIP. <a class="footnote-backref" href="#fnref:firewall" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:icmp">
<p>Cette fonctionnalité a été ajoutée dans Linux 4.4. Voir par exemple le
<a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=1471f35efa86407fc180ca6d55363c684d166ef" title="ipvs: sh: support scheduling icmp/inverse packets consistently">commit 1471f35efa86</a>. <a class="footnote-backref" href="#fnref:icmp" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Empaquetage d'un module tiers pour Debian avec DKMS</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2018-empaquetage-pilote-debian-dkms" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2018-empaquetage-pilote-debian-dkms#isso-thread" rel="replies" type="text/html"/>
    <updated>2018-03-07T16:38:52Z</updated>
    <id>http://www.luffy.cx/fr/blog/2018-empaquetage-pilote-debian-dkms.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<blockquote>
<p><a href="https://github.com/dkms-project/dkms" title="Dynamic Kernel Module Support sur GitHub"><abbr title="Dynamic Kernel Module Support">DKMS</abbr></a> est un système conçu pour permettre la mise à jour
individuelle de modules noyau sans changer le noyau en entier. Il
est également très facile de reconstruire des modules lorsque vous
mettez à jour le noyau.</p>
</blockquote>
<p>Sur les systèmes de type Debian<sup id="fnref:rpm"><a class="footnote-ref" href="#fn:rpm">1</a></sup>, <em><abbr title="Dynamic Kernel Module Support">DKMS</abbr></em> permet l’installation de
différents pilotes, de <a href="https://packages.debian.org/sid/zfs-dkms" title="Paquet pour le module ZFS on Linux pour Debian">ZFS</a> aux modules
<a href="https://packages.debian.org/sid/virtualbox-dkms" title="Module noyau VirtualBox kernel pour Debian">VirtualBox</a> ou <a href="https://packages.debian.org/sid/nvidia-kernel-dkms" title="Pilote NVIDIA pour Debian">NVIDIA</a>. Ces modules externes ne sont pas distribués sous forme
binaire : une fois installés, ils doivent être compilés pour votre
noyau actuel. Tout se fait automatiquement :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp"># </span>apt<span class="w"> </span>install<span class="w"> </span>zfs-dkms
<span class="go">Reading package lists... Done</span>
<span class="go">Building dependency tree</span>
<span class="go">Reading state information... Done</span>
<span class="go">The following additional packages will be installed:</span>
<span class="go">  binutils cpp cpp-6 dkms fakeroot gcc gcc-6 gcc-6-base libasan3 libatomic1 libc-dev-bin libc6-dev</span>
<span class="go">  libcc1-0 libcilkrts5 libfakeroot libgcc-6-dev libgcc1 libgomp1 libisl15 libitm1 liblsan0 libmpc3</span>
<span class="go">  libmpfr4 libmpx2 libnvpair1linux libquadmath0 libstdc++6 libtsan0 libubsan0 libuutil1linux libzfs2linux</span>
<span class="go">  libzpool2linux linux-compiler-gcc-6-x86 linux-headers-4.9.0-6-amd64 linux-headers-4.9.0-6-common</span>
<span class="go">  linux-headers-amd64 linux-kbuild-4.9 linux-libc-dev make manpages manpages-dev patch spl spl-dkms</span>
<span class="go">  zfs-zed zfsutils-linux</span>
<span class="go">[…]</span>
<span class="go">3 upgraded, 44 newly installed, 0 to remove and 3 not upgraded.</span>
<span class="go">Need to get 42.1 MB of archives.</span>
<span class="go">After this operation, 187 MB of additional disk space will be used.</span>
<span class="go">Do you want to continue? [Y/n]</span>
<span class="go">[…]</span>
<span class="gp"># </span>dkms<span class="w"> </span>status
<span class="go">spl, 0.6.5.9, 4.9.0-6-amd64, x86_64: installed</span>
<span class="go">zfs, 0.6.5.9, 4.9.0-6-amd64, x86_64: installed</span>
<span class="gp"># </span>modinfo<span class="w"> </span>zfs<span class="w"> </span><span class="p">|</span><span class="w"> </span>head
<span class="go">filename:       /lib/modules/4.9.0-6-amd64/updates/dkms/zfs.ko</span>
<span class="go">version:        0.6.5.9-5</span>
<span class="go">license:        CDDL</span>
<span class="go">author:         OpenZFS on Linux</span>
<span class="go">description:    ZFS</span>
<span class="go">srcversion:     42C4AB70887EA26A9970936</span>
<span class="go">depends:        spl,znvpair,zcommon,zunicode,zavl</span>
<span class="go">retpoline:      Y</span>
<span class="go">vermagic:       4.9.0-6-amd64 SMP mod_unload modversions</span>
<span class="go">parm:           zvol_inhibit_dev:Do not create zvol device nodes (uint)</span>
</code></pre></div>

<p>Lors de l’installation d’un nouveau noyau, la compilation du module
est automatiquement déclenchée.</p>
<h1 id="construction-dun-paquet-compatible-dkms">Construction d’un paquet compatible <abbr title="Dynamic Kernel Module Support">DKMS</abbr></h1>
<p>Supposons que vous avez mis la main sur un serveur avec une carte
réseau <a href="https://www.intel.com/content/www/us/en/products/sku/95260/intel-ethernet-network-adapter-xxv710da2/specifications.html" title="Intel Ethernet Network Adapter XXV710-DA2">Intel XXV710-DA2</a>. Cette carte est gérée par le pilote
<code>i40e</code>. Malheureusement, elle n’est prise en charge que depuis Linux 4.10 et vous utilisez un noyau 4.9 issu de Debian Stretch. <em><abbr title="Dynamic Kernel Module Support">DKMS</abbr></em>
fournit ici une solution simple !</p>
<p>Téléchargez les <a href="https://www.intel.com/content/www/us/en/download/18026/intel-network-adapter-driver-for-pcie-40-gigabit-ethernet-network-connections-under-linux.html" title="Intel Network Adapter Driver for PCIe 40 Gigabit Ethernet Network Connections Under Linux">sources du pilote chez Intel</a>,
décompressez-les dans un répertoire et ajoutez un sous-répertoire
<code>debian/</code> avec les fichiers suivants :</p>
<ul>
<li>
<p><code>debian/changelog</code>:</p>
<div class="language-text-only codehilite"><pre><span/><code>i40e-dkms (2.4.6-0) stretch; urgency=medium

  * Initial package.

 -- Vincent Bernat &lt;bernat@debian.org&gt;  Tue, 27 Feb 2018 17:20:58 +0100
</code></pre></div>

</li>
<li>
<p><code>debian/control</code>:</p>
<div class="language-debian-control-file codehilite"><pre><span/><code><span class="k">Source</span><span class="p">:</span><span class="w"> </span><span class="s">i40e-dkms</span>
<span class="k">Maintainer</span><span class="p">:</span><span class="w"> </span>Vincent Bernat <span class="gs">&lt;bernat@debian.org&gt;</span>
<span class="k">Build-Depends</span><span class="p">:</span><span class="w"> </span><span class="nf">debhelper</span> <span class="p">(</span><span class="o">&gt;=</span><span class="w"> </span><span class="m">9</span><span class="p">)</span>, <span class="nf">dkms</span>

<span class="nf">Package</span><span class="err">:</span> <span class="nf">i40e-dkms</span>
<span class="k">Architecture</span><span class="p">:</span><span class="w"> </span><span class="s">all</span>
<span class="k">Depends</span><span class="p">:</span><span class="w"> </span><span class="o">$</span><span class="p">{</span><span class="ni">misc</span><span class="p">:</span>Depends<span class="p">}</span>
<span class="k">Description</span><span class="gs">: DKMS source for the Intel i40e network driver</span>
</code></pre></div>

</li>
<li>
<p><code>debian/rules</code>:</p>
<div class="language-makefile codehilite"><pre><span/><code><span class="c">#!/usr/bin/make -f</span>

<span class="cp">include /usr/share/dpkg/pkg-info.mk</span>

<span class="nf">%</span><span class="o">:</span>
<span class="w">        </span>dh<span class="w"> </span><span class="nv">$@</span><span class="w"> </span>--with<span class="w"> </span>dkms

<span class="nf">override_dh_install</span><span class="o">:</span>
<span class="hll"><span class="w">        </span>dh_install<span class="w"> </span>src/*<span class="w"> </span>usr/src/i40e-<span class="k">$(</span>DEB_VERSION_UPSTREAM<span class="k">)</span>/
</span>
<span class="nf">override_dh_dkms</span><span class="o">:</span>
<span class="w">        </span>dh_dkms<span class="w"> </span>-V<span class="w"> </span><span class="k">$(</span>DEB_VERSION_UPSTREAM<span class="k">)</span>

<span class="nf">override_dh_auto_configure</span><span class="o">:</span>
<span class="nf">override_dh_auto_build</span><span class="o">:</span>
<span class="nf">override_dh_auto_test</span><span class="o">:</span>
<span class="nf">override_dh_auto_install</span><span class="o">:</span>
<span class="nf">override_dh_auto_clean</span><span class="o">:</span>
</code></pre></div>

</li>
<li>
<p><code>debian/i40e-dkms.dkms</code>:</p>
<div class="language-ini codehilite"><pre><span/><code><span class="na">PACKAGE_NAME</span><span class="o">=</span><span class="s">"i40e"</span>
<span class="na">PACKAGE_VERSION</span><span class="o">=</span><span class="s">"#MODULE_VERSION#"</span>
<span class="na">BUILT_MODULE_NAME[0]</span><span class="o">=</span><span class="s">"$PACKAGE_NAME"</span>
<span class="na">DEST_MODULE_LOCATION[0]</span><span class="o">=</span><span class="s">"/updates/dkms"</span>
<span class="na">AUTOINSTALL</span><span class="o">=</span><span class="s">"YES"</span>
<span class="na">REMAKE_INITRD</span><span class="o">=</span><span class="s">"YES"</span>
</code></pre></div>

</li>
<li>
<p><code>debian/compat</code>:</p>
<div class="language-text-only codehilite"><pre><span/><code>9
</code></pre></div>

</li>
</ul>
<p>Le fichier <code>debian/changelog</code> permet essentiellement de spécifier la
version du paquet. Celle du pilote est 2.4.6. Par conséquent, nous
utilisons <code>2.4.6-0</code> comme version de paquet. Dans <code>debian/rules</code>, nous
copions les sources du pilote dans <code>/usr/src/i40e-2.4.6</code> (la version
est extraite de <code>debian/changelog</code>).</p>
<p>Le contenu de <code>debian/i40e-dkms.dkms</code> est décrit en détail dans la
<a href="https://manpages.debian.org/dkms.8.html#DKMS.CONF" title="Page de manuel pour dkms(8)">page de manuel <code>dkms(8)</code></a>. Le pilote <code>i40e</code> est assez
standard et <code>dkms</code> sait comment le compiler. Cependant, si un module
noyau ne suit pas les conventions habituelles, c’est le bon endroit
pour remplacer la commande de construction.</p>
<p>Une fois tous les fichiers en place, vous pouvez transformer le
répertoire en un paquet Debian à l’aide, par exemple, de la commande
<code>dpkg-buildpackage</code> command<sup id="fnref:apt"><a class="footnote-ref" href="#fn:apt">2</a></sup>. Vous obtenez un paquet compatible
<em><abbr title="Dynamic Kernel Module Support">DKMS</abbr></em> : <code>i40e-dkms_2.4.6-0_all.deb</code>. Placez le dans votre <a href="/fr/blog/2014-depots-apt-locaux" title="Dépôts APT locaux en entreprise">dépôt
interne</a> et installez-le sur le serveur cible.</p>
<h1 id="eviter-la-phase-de-compilation">Éviter la phase de compilation</h1>
<div class="admonition">
<p class="admonition-title">Mise à jour (03.2024)</p>
<p>Les mainteneurs de <em><abbr title="Dynamic Kernel Module Support">DKMS</abbr></em> ont supprimé la commande
<code>dkms mkbmdeb</code>. Cela concerne les versions à partir de la 2.8.8, y compris celles
fournies dans Debian Bookworm et Ubuntu Noble. Voir [le bug Debian</p>
<h1 id="1009179debian-bug1009179-pour-plus-dinformations">1009179][Debian bug#1009179] pour plus d’informations.</h1>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (09.2026)</p>
<p>Pour la remplacer, vous pouvez utiliser
<a href="https://github.com/uecasm/dkms-mkbmdeb" title="Script de remplacement pour dkms mkbmdeb"><code>dkms-mkbmdeb</code></a>, un script avec les mêmes fonctionnalités.
Échangez <code>dkms mkbmdeb</code> avec <code>dkms-mkbmdeb</code> dans l’exemple ci-dessous. Merci à
Gavin !</p>
</div>
<p>Si la présence d’outils de développement sur un serveur en production
vous rend nerveux, il y a une solution ! Depuis la version
2.2.0.3-5<sup id="fnref:version"><a class="footnote-ref" href="#fn:version">3</a></sup>, grâce à <a href="https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=554843#92" title="Debian bug #554843">Thijs Kinkhorst</a>, <code>dkms</code> peut
construire des paquets contenant uniquement les modules
précompilés. Pour chaque version du noyau, un tel paquet peut être
construit via votre système d’intégration :</p>
<div class="language-bash codehilite"><pre><span/><code><span class="nv">KERNEL_VERSION</span><span class="o">=</span><span class="m">4</span>.9.0-6-amd64<span class="w"> </span><span class="c1"># could be a Jenkins parameter</span>
apt<span class="w"> </span>-qyy<span class="w"> </span>install<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>i40e-dkms<span class="w"> </span><span class="se">\</span>
<span class="w">      </span>linux-image-<span class="si">${</span><span class="nv">KERNEL_VERSION</span><span class="si">}</span><span class="w"> </span><span class="se">\</span>
<span class="w">      </span>linux-headers-<span class="si">${</span><span class="nv">KERNEL_VERSION</span><span class="si">}</span>

<span class="nv">DRIVER_VERSION</span><span class="o">=</span><span class="k">$(</span>dkms<span class="w"> </span>status<span class="w"> </span>i40e<span class="w"> </span><span class="p">|</span><span class="w"> </span>awk<span class="w"> </span>-F<span class="s1">', '</span><span class="w"> </span><span class="s1">'{print $2}'</span><span class="k">)</span>
<span class="hll">dkms<span class="w"> </span>mkbmdeb<span class="w"> </span>i40e/<span class="si">${</span><span class="nv">DRIVER_VERSION</span><span class="si">}</span><span class="w"> </span>-k<span class="w"> </span><span class="si">${</span><span class="nv">KERNEL_VERSION</span><span class="si">}</span>
</span>
<span class="nb">cd</span><span class="w"> </span>/var/lib/dkms/i40e/<span class="si">${</span><span class="nv">DRIVER_VERSION</span><span class="si">}</span>/bmdeb/
dpkg<span class="w"> </span>-c<span class="w"> </span>i40e-modules-<span class="si">${</span><span class="nv">KERNEL_VERSION</span><span class="si">}</span>_*
dpkg<span class="w"> </span>-I<span class="w"> </span>i40e-modules-<span class="si">${</span><span class="nv">KERNEL_VERSION</span><span class="si">}</span>_*
</code></pre></div>

<p>Voici la sortie abrégée des deux dernières commandes :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp"># </span>dpkg<span class="w"> </span>-c<span class="w"> </span>i40e-modules-<span class="si">${</span><span class="nv">KERNEL_VERSION</span><span class="si">}</span>_*
<span class="go">[…]</span>
<span class="hll"><span class="go">-rw-r--r-- root/root    551664 2018-03-01 19:16 ./lib/modules/4.9.0-6-amd64/updates/dkms/i40e.ko</span>
</span><span class="go">[…]</span>
<span class="gp"># </span>dpkg<span class="w"> </span>-I<span class="w"> </span>i40e-modules-<span class="si">${</span><span class="nv">KERNEL_VERSION</span><span class="si">}</span>_*
<span class="go"> new debian package, version 2.0.</span>
<span class="go">[…]</span>
<span class="go"> Package: i40e-modules-4.9.0-6-amd64</span>
<span class="go"> Source: i40e-dkms-bin</span>
<span class="go"> Version: 2.4.6</span>
<span class="go"> Architecture: amd64</span>
<span class="go"> Installed-Size: 555</span>
<span class="go"> Depends: linux-image-4.9.0-6-amd64</span>
<span class="hll"><span class="go"> Provides: i40e-modules</span>
</span><span class="go"> Section: misc</span>
<span class="go"> Priority: optional</span>
<span class="go"> Description: i40e binary drivers for linux-image-4.9.0-6-amd64</span>
<span class="go">  This package contains i40e drivers for the 4.9.0-6-amd64 Linux kernel,</span>
<span class="go">  built from i40e-dkms for the amd64 architecture.</span>
</code></pre></div>

<p>Le paquet Debian ainsi créé contient le pilote précompilé et ne dépend
que du noyau associé. Vous pouvez l’installer sans tirer des dizaines
de dépendances supplémentaires.</p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:rpm">
<p><em><abbr title="Dynamic Kernel Module Support">DKMS</abbr></em> est également compatible avec les distributions à base
de RPM mais le contenu de cet article ne s’applique pas à
celles-ci. <a class="footnote-backref" href="#fnref:rpm" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:apt">
<p>Quelques paquets additionels sont nécessaires :
<code>build-essential</code>, <code>fakeroot</code> et <code>debhelper</code>. <a class="footnote-backref" href="#fnref:apt" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:version">
<p>Disponible pour Debian Stretch ainsi que pour Debian
Jessie, via un rétroportage. Toutefois, la version dans Ubuntu
Xenial est trop ancienne. <a class="footnote-backref" href="#fnref:version" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Routage L3 jusqu'à l'hyperviseur avec BGP</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2018-routage-l3-hyperviseur" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2018-routage-l3-hyperviseur#isso-thread" rel="replies" type="text/html"/>
    <updated>2018-01-29T19:25:44Z</updated>
    <id>http://www.luffy.cx/fr/blog/2018-routage-l3-hyperviseur.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>La mise en haute disponibilité des réseaux L2 peut se faire de
plusieurs façons :</p>
<ul>
<li>d’antiques protocoles tels que le <a href="https://fr.wikipedia.org/wiki/Spanning_Tree_Protocol" title="Spanning Tree Protocol sur Wikipedia">Spanning Tree Protocol</a> ;</li>
<li>des protocoles propriétaires tels que le <a href="https://www.juniper.net/documentation/us/en/software/junos/virtual-chassis/topics/concept/virtual-chassis-switches-overview.html" title="Juniper: EX Series Virtual Chassis Overview">chassis virtuel chez
   Juniper</a>, le
   <a href="https://web.archive.org/web/2018/https://www.cisco.com/c/en/us/products/collateral/switches/nexus-5000-series-switches/configuration_guide_c07-543563.html" title="Cisco: Virtual PortChannel Quick Configuration Guide">vPC chez Cisco</a> ainsi que de <a href="https://www.juniper.net/documentation/us/en/software/junos/mc-lag/topics/concept/mc-lag-feature-summary-best-practices.html" title="Juniper: MC-LAG Technical Overview">nombreuses</a>
   <a href="https://docs.nvidia.com/networking-ethernet-software/cumulus-linux-43/Layer-2/Multi-Chassis-Link-Aggregation-MLAG/" title="Cumulus: Multi-Chassis Link Aggregation - MLAG">implémentations</a> de <a href="https://arista.my.site.com/AristaCommunity/s/article/mlag-basic-configuration" title="Arista: MLAG"><abbr title="Multi-Chassis Link Aggregation">MC-LAG</abbr></a><sup id="fnref:mclag"><a class="footnote-ref" href="#fn:mclag">1</a></sup> ;</li>
<li>des protocoles standards tels que <a href="https://sci-hub.fr/10.1109/IEEESTD.2014.6991462" title="IEEE 802.1Q-2014 on Sci-Hub">Shortest Path Bridging Protocol</a> ou <a href="https://www.rfc-editor.org/rfc/rfc6325" title="RFC 6325: Routing Bridges (RBridges): Base Protocol Specification">TRILL</a> ;</li>
<li>la délégation de cette problématique au réseau sous-jacent (dans le
   cas des protocoles reposant sur d’autres réseaux, tels que
   <a href="/fr/blog/2017-vxlan-linux" title="VXLAN &amp; Linux">VXLAN</a>).</li>
</ul>
<p>Les réseaux L2 nécessitent très peu de configuration mais sont
difficiles à opérer de manière fiable dans des configurations hautement
disponibles : un incident risque souvent d’impacter l’ensemble du
réseau<sup id="fnref:incident"><a class="footnote-ref" href="#fn:incident">2</a></sup>. Il est donc préférable de <strong>limiter la portée de
chaque réseau L2</strong>. Par exemple, il est courant d’avoir un réseau L2
dans chaque baie et de les connecter entre eux via du routage L3. Un
incident impacte rarement l’ensemble d’un réseau IP.</p>
<p>Dans le schéma ci-dessous, les commutateurs de baie fournissent une
passerelle par défaut pour les clients. Afin d’assurer une certaine
redondance, ils utilisent une implémentation de <abbr title="Multi-Chassis Link Aggregation">MC-LAG</abbr>. La portée de
chaque réseau L2 est alors limitée à une seule baie. Chaque
sous-réseau IP est lié à une baie et les informations de routage sont
partagées entre les commutateurs de baie et les routeurs en utilisant
un protocole tel qu’<abbr title="Open Shortest Path First">OSPF</abbr>.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 726px"><span class="lf-media-inner" style="padding-bottom: 71.350%"><img alt="Conception classique d'un réseau L2" src="https://media-bernat-ch.b-cdn.net/images/l3-routing/legacy.bdd34c1fce0fd7.svg" width="726" height="518" class="lf-media"/></span></div><figcaption>Conception classique avec routage au niveau des commutateurs d'accès : chaque paire de commutateurs agit en tant que passerelle. La portée d'un réseau L2 est limitée à une baie. Les hyperviseurs étendent le réseau L2 aux machines virtuelles. Chaque baie gère ses propres sous-réseaux IP.</figcaption></figure>
<p>Il y a deux défauts dans cette conception :</p>
<ol>
<li>
<p>La <strong>portée de chaque réseau L2 reste très importante</strong>. Une baie peut
    contenir plusieurs dizaines d’hyperviseurs et plusieurs milliers
    de machines virtuelles. Un incident réseau a donc un impact
    majeur.</p>
</li>
<li>
<p>Les <strong>sous-réseaux IP sont liés à une baie</strong>. Une machine
    virtuelle ne peut pas migrer dans une autre baie et les IP non
    utilisées dans une baie ne peuvent pas être utilisées dans une
    autre.</p>
</li>
</ol>
<p>Pour résoudre ces deux problèmes, il est possible de pousser le réseau
L3 encore plus au sud, transformant chaque hyperviseur en routeur. Il
convient toutefois de cacher ce changement aux machines virtuelles qui
continuent d’obtenir leur configuration via <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> (IP, sous-réseau et
passerelle).</p>
<div class="toc">
<ul>
<li><a href="#hyperviseur-comme-routeur">Hyperviseur comme routeur</a><ul>
<li><a href="#configuration-du-routage">Configuration du routage</a></li>
<li><a href="#routes-pour-les-machines-virtuelles">Routes pour les machines virtuelles</a></li>
<li><a href="#configuration-bgp">Configuration BGP</a><ul>
<li><a href="#fabrique">Fabrique</a></li>
<li><a href="#reflecteurs-de-routes">Réflecteurs de routes</a></li>
<li><a href="#configuration-de-lhyperviseur">Configuration de l’hyperviseur</a></li>
</ul>
</li>
<li><a href="#performance">Performance</a></li>
<li><a href="#filtrage-par-la-source">Filtrage par la source</a></li>
</ul>
</li>
<li><a href="#leurrer-les-machines-virtuelles">Leurrer les machines virtuelles</a></li>
<li><a href="#conclusion-et-avenir">Conclusion et avenir</a></li>
</ul>
</div>
<h1 id="hyperviseur-comme-routeur">Hyperviseur comme routeur</h1>
<p>En bref, pour une machine virtuelle disposant d’une adresse IPv4 :</p>
<ul>
<li>l’hyperviseur hôte configure une route <code>/32</code> sur l’interface virtuelle ;</li>
<li>cette route est distribuée aux autres hyperviseurs et routeurs via
   <abbr title="Border Gateway Protocol">BGP</abbr>.</li>
</ul>
<p>Nous voulons aussi gérer deux domaines de routage : un domaine public
pour les machines virtuelles de clients connectées à Internet et un
domaine privé pour notre propre usage, notamment la gestion des
hyperviseurs. À cet effet, chaque hyperviseur utilise deux tables de
routage.</p>
<p>L’illustration suivante montre la configuration d’un hyperviseur avec
cinq machines virtuelles. Notez l’absence de tout pont réseau.</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 930px"><span class="lf-media-inner" style="padding-bottom: 56.989%"><img alt="Routage L3 à l'intérieur d'un hyperviseur" src="https://media-bernat-ch.b-cdn.net/images/l3-routing/hypervisor.650f5a106382f5.svg" width="930" height="530" class="lf-media"/></span></div><figcaption>Routage L3 pour un hyperviseur. Chaque hyperviseur est connecté à un réseau « public » et un réseau « privé ». Les machines virtuelles peuvent être rattachées à n'importe lequel de ces réseaux. Chaque table de routage contient des routes directement connectées pour les machines virtuelles locales, des routes pour les machines virtuelles distantes et une route par défaut pour les autres destinations.</figcaption></figure>
<p>La configuration complète décrite dans cet article est également
disponible sur <a href="https://github.com/vincentbernat/network-lab/tree/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv" title="Fichiers de configuration du labo">GitHub</a>. Un agent reste nécessaire pour mettre en
place la configuration lors d’un changement. Pour ce faire, ce dernier
recevrait des notifications de la part du système de gestion des
machines virtuelles.</p>
<p><a href="https://www.tigera.io/project-calico/" title="Project Calico: Secure networking for the cloud native era">Calico</a> est un projet à l’objectif similaire (routage L3 jusqu’à
l’hyperviseur) et des idées très proches (à l’exception de
l’utilisation de <em>Netfilter</em> pour assurer l’isolation des domaines de
routage). Il fournit un agent, <em>Felix</em>, qui s’interface avec des
orchestrateurs (tels qu’<em>OpenStack</em> ou <em>Kubernetes</em>). C’est une
alternative possible si on désire une solution clé en main.</p>
<h2 id="configuration-du-routage">Configuration du routage</h2>
<p>À l’aide de règles de routage, chaque interface est « attachée » à une
table de routage :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>ip<span class="w"> </span>rule<span class="w"> </span>show
<span class="go">0:  from all lookup local</span>
<span class="go">20: from all iif lo lookup main</span>
<span class="go">21: from all iif lo lookup local-out</span>
<span class="hll"><span class="go">30: from all iif eth0.private lookup private</span>
</span><span class="hll"><span class="go">30: from all iif eth1.private lookup private</span>
</span><span class="hll"><span class="go">30: from all iif vnet8 lookup private</span>
</span><span class="hll"><span class="go">30: from all iif vnet9 lookup private</span>
</span><span class="hll"><span class="go">40: from all lookup public</span>
</span></code></pre></div>

<p>Les règles les plus importantes sont surlignées (priorités 30 et 40) :
tout trafic provenant d’une interface privée utilise la table
<code>private</code>. Tout le trafic restant utilise la table <code>public</code>.</p>
<p>Les deux règles en <code>iif lo</code> gèrent le routage des paquets émis par
l’hyperviseur lui-même. La table <code>local-out</code> est une combinaison des
tables <code>private</code> et <code>public</code>. À première vue, l’hyperviseur n’a besoin
que de la table <code>private</code> mais il doit être capable de contacter les
machines virtuelles locales (par exemple, pour répondre à un ping) via
la table <code>public</code>. Ces tables contiennent en temps normal une route
par défaut toutes les deux (pas de chaînage possible). La table
<code>local-out</code> est construite en <a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/bird-common/hypervisor.conf#L11-L40" title="Configuration de la table local-out">copiant toutes les routes</a>
de la table <code>private</code> et les routes directement connectées de la table
<code>public</code>.</p>
<p>Pour éviter toute fuite de trafic accidentelle, les tables <code>public</code>,
<code>private</code> et <code>local-out</code> contiennent une route par défaut avec une
métrique élevée<sup id="fnref:nodistribute"><a class="footnote-ref" href="#fn:nodistribute">3</a></sup>. En temps normal, ces routes sont
éclipsées par une véritable route par défaut :</p>
<div class="language-bash codehilite"><pre><span/><code>ip<span class="w"> </span>route<span class="w"> </span>add<span class="w"> </span>blackhole<span class="w"> </span>default<span class="w"> </span>metric<span class="w"> </span><span class="m">4294967294</span><span class="w"> </span>table<span class="w"> </span>public
ip<span class="w"> </span>route<span class="w"> </span>add<span class="w"> </span>blackhole<span class="w"> </span>default<span class="w"> </span>metric<span class="w"> </span><span class="m">4294967294</span><span class="w"> </span>table<span class="w"> </span>private
ip<span class="w"> </span>route<span class="w"> </span>add<span class="w"> </span>blackhole<span class="w"> </span>default<span class="w"> </span>metric<span class="w"> </span><span class="m">4294967294</span><span class="w"> </span>table<span class="w"> </span>local-out
</code></pre></div>

<p>Les choses sont plus simples avec IPv6 car il n’y a qu’un seul domaine
de routage. Nous gardons cependant une table <code>public</code> mais il n’y a
nul besoin de la table <code>local-out</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>ip<span class="w"> </span>-6<span class="w"> </span>rule<span class="w"> </span>show
<span class="go">0:  from all lookup local</span>
<span class="go">20: from all lookup main</span>
<span class="go">40: from all lookup public</span>
</code></pre></div>

<p>Une fois cette configuration en place, le routage est activé et la
taille maximale du cache des routes IPv6 est augmentée (la valeur par
défaut est seulement 4096)<sup id="fnref:ddos"><a class="footnote-ref" href="#fn:ddos">4</a></sup> :</p>
<div class="language-bash codehilite"><pre><span/><code>sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.conf.all.forwarding<span class="o">=</span><span class="m">1</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv6.conf.all.forwarding<span class="o">=</span><span class="m">1</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv6.route.max_size<span class="o">=</span><span class="m">524288</span>
</code></pre></div>

<div class="admonition">
<p class="admonition-title">Mise à jour (12.2019)</p>
<p>À partir de Linux 4.2, les entrées pour le
cache des routes ne sont créées que s’il est nécessaire de noter une
valeur spéciale pour le PMTU. Ainsi, il est préférable de garder la
valeur par défaut et de surveiller le fichier <code>/proc/net/rt6_stats</code> :
l’avant-dernière valeur est la taille actuelle du cache.</p>
</div>
<h2 id="routes-pour-les-machines-virtuelles">Routes pour les machines virtuelles</h2>
<p>La seconde étape est de configurer les routes pour atteindre chaque
machine virtuelle. Pour IPv6, l’adresse du lien local (dérivée de
l’adresse MAC) est utilisée comme prochain saut<sup id="fnref:privacy"><a class="footnote-ref" href="#fn:privacy">5</a></sup> :</p>
<div class="language-bash codehilite"><pre><span/><code>ip<span class="w"> </span>-6<span class="w"> </span>route<span class="w"> </span>add<span class="w"> </span><span class="m">2001</span>:db8:cb00:7100:5254:33ff:fe00:f/128<span class="w"> </span><span class="se">\</span>
<span class="w">    </span>via<span class="w"> </span>fe80::5254:33ff:fe00:f<span class="w"> </span>dev<span class="w"> </span>vnet6<span class="w"> </span><span class="se">\</span>
<span class="w">    </span>table<span class="w"> </span>public
</code></pre></div>

<p>Ajouter d’autres adresses IP ou sous-réseaux peut se faire en
spécifiant d’autres routes sur le même modèle :</p>
<div class="language-bash codehilite"><pre><span/><code>ip<span class="w"> </span>-6<span class="w"> </span>route<span class="w"> </span>add<span class="w"> </span><span class="m">2001</span>:db8:cb00:7107::/64<span class="w"> </span><span class="se">\</span>
<span class="w">    </span>via<span class="w"> </span>fe80::5254:33ff:fe00:f<span class="w"> </span>dev<span class="w"> </span>vnet6<span class="w"> </span><span class="se">\</span>
<span class="w">    </span>table<span class="w"> </span>public
</code></pre></div>

<p>En IPv4, la route utilise comme prochain saut l’interface à laquelle
la machine virtuelle est connectée. Linux émettra une requête <abbr title="Address Resolution Protocol">ARP</abbr>
avant de pouvoir router les paquets<sup id="fnref:arp"><a class="footnote-ref" href="#fn:arp">6</a></sup> :</p>
<div class="language-bash codehilite"><pre><span/><code>ip<span class="w"> </span>route<span class="w"> </span>add<span class="w"> </span><span class="m">192</span>.0.2.15/32<span class="w"> </span>dev<span class="w"> </span>vnet6<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>table<span class="w"> </span>public
</code></pre></div>

<p>Les IP et sous-réseaux supplémentaires peuvent être ajoutées de la
même façon. Cela impose toutefois que chaque adresse IP réponde aux
requêtes <abbr title="Address Resolution Protocol">ARP</abbr>. Pour éviter cela, le routage peut se faire via la
première IP configurée<sup id="fnref:onlink"><a class="footnote-ref" href="#fn:onlink">7</a></sup> :</p>
<div class="language-bash codehilite"><pre><span/><code>ip<span class="w"> </span>route<span class="w"> </span>add<span class="w"> </span><span class="m">203</span>.0.113.128/28<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>via<span class="w"> </span><span class="m">192</span>.0.2.15<span class="w"> </span>dev<span class="w"> </span>vnet6<span class="w"> </span>onlink<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>table<span class="w"> </span>public
</code></pre></div>

<h2 id="configuration-bgp">Configuration <abbr title="Border Gateway Protocol">BGP</abbr></h2>
<p>La troisième étape consiste à partager les routes entre les
hyperviseurs à l’aide de <abbr title="Border Gateway Protocol">BGP</abbr>. Cette partie de la configuration dépend
du type de réseau connectant les hyperviseurs.</p>
<h3 id="fabrique">Fabrique</h3>
<p>Les hyperviseurs peuvent être connectés de plusieurs façons. Une
première possibilité naturelle est d’utiliser un réseau L3 de type
<em>leaf-spine</em> :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 745px"><span class="lf-media-inner" style="padding-bottom: 38.658%"><img alt="Fabrique routée" src="https://media-bernat-ch.b-cdn.net/images/l3-routing/full-v2.88ba996f18d400.svg" width="745" height="288" class="lf-media"/></span></div><figcaption>Fabrique L3 de type leaf-spine. Une session BGP est établie pour chacun des liens. Les routeurs spine doivent apprendre toutes les routes.</figcaption></figure>
<p>Chaque hyperviseur établit une session <abbr title="External Border Gateway Protocol">eBGP</abbr> vers chaque routeur de
type <em>leaf</em>. Ceux-ci établissent une session <abbr title="External Border Gateway Protocol">eBGP</abbr> avec chaque routeur
de type <em>spine</em>. Cette solution peut s’avérer coûteuse car les
routeurs de type <em>spine</em> doivent être capables de gérer l’intégralité
des routes. Avec la génération actuelle de routeurs, cela implique une
limite sur le nombre total de routes en fonction de la densité
voulue<sup id="fnref:current"><a class="footnote-ref" href="#fn:current">8</a></sup>. Elle nécessite de plus beaucoup de configuration
pour définir les sessions <abbr title="Border Gateway Protocol">BGP</abbr>, à moins d’utiliser certaines
fonctionnalités d’autoconfiguration actuellement encore peu répandues.
D’un autre côté, les routeurs de type <em>leaf</em> (et les hyperviseurs)
peuvent apprendre moins de routes et pousser le reste du trafic vers
le nord.</p>
<p>Une autre solution est d’utiliser une fabrique de type L2. Cela peut sembler surprenant après avoir critiqué les réseaux L2 pour leur manque de fiabilité mais nous n’avons pas besoin de haute disponibilité. Ils permettent alors d’obtenir une solution peu coûteuse et facile à mettre en place<sup id="fnref:scale"><a class="footnote-ref" href="#fn:scale">9</a></sup> :</p>
<figure class="lf-fullbleed"><div class="lf-media-outer" style="width: 804px"><span class="lf-media-inner" style="padding-bottom: 48.010%"><img alt="Fabrique de type L2" src="https://media-bernat-ch.b-cdn.net/images/l3-routing/l2.a56c6e139047af.svg" width="804" height="386" class="lf-media"/></span></div><figcaption>Fabrique L2 de type leaf-spine utilisant 4 réseaux L2 distincts. Chaque hyperviseur établit une session BGP vers au moins un réflecteur de route sur chaque réseau L2.</figcaption></figure>
<p>Chaque hyperviseur est connecté à <strong>4 réseaux L2 distincts</strong>. Si un
incident survient sur l’un d’eux, nous ne perdons qu’un quart de la
bande passante disponible. Cette solution repose entièrement sur
<abbr title="Internal Border Gateway Protocol">iBGP</abbr>. Pour éviter de multiplier les connexions <abbr title="Border Gateway Protocol">BGP</abbr> entre les
hyperviseurs, des <strong>réflecteurs de routes</strong> sont utilisés. Chaque
hyperviseur établit une session <abbr title="Internal Border Gateway Protocol">iBGP</abbr> avec un ou plusieurs réflecteurs
sur chaque réseau L2. Les réflecteurs d’un même réseau L2 se partagent
également leurs routes via <abbr title="Internal Border Gateway Protocol">iBGP</abbr>. <em>Calico</em> <a href="https://docs.tigera.io/calico/latest/reference/architecture/design/l2-interconnect-fabric" title="Calico over an Ethernet interconnect fabric">documente ce
concept</a> de manière plus détaillée.</p>
<p>C’est la solution utilisée par la suite. La partie publique et la
partie privée partagent la même infrastructure sur des VLAN
différents.</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (07.2022)</p>
<p>Bien que cette solution avait du sens il y
a quelques années, le matériel récent est désormais capable de gérer
bien plus de routes et je vous conseille donc d’opter pour une
fabrique L3. Dans ce cas, nul besoin de réflecteurs de routes. En
utilisant <em>FRR</em> au lieu de <em>BIRD</em>, il est même possible de n’utiliser
qu’une unique session <abbr title="External Border Gateway Protocol">eBGP</abbr> IPv6 avec VPNv4 (public et privé), VPNv6
(public) et EVPN (pour <a href="/fr/blog/2017-vxlan-linux" title="VXLAN &amp; Linux">VXLAN</a>, non présenté ici). Selon les
équipements impliqués dans le réseau, vous pouvez configurer des
sessions <abbr title="Border Gateway Protocol">BGP</abbr> sans IP. On peut alors se passer de définir un plan de
numérotation et la configuration est alors encore réduite.</p>
</div>
<h3 id="reflecteurs-de-routes">Réflecteurs de routes</h3>
<p>Les réflecteurs de routes centralisent et redistribuent les routes via
<abbr title="Border Gateway Protocol">BGP</abbr> mais ne routent aucun trafic. Il en faut au moins un dans chaque
réseau L2. Pour une meilleure disponibilité, on peut en utiliser
plusieurs.</p>
<p>Voici un exemple de configuration avec <em>Junos</em><sup id="fnref:instances"><a class="footnote-ref" href="#fn:instances">10</a></sup> :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">protocols</span> <span class="p">{</span>
    bgp <span class="p">{</span>
        group public-v4 <span class="p">{</span>
            family inet <span class="p">{</span>
                unicast <span class="p">{</span>
                    no-install; <span class="c c-Singleline"># ❶</span>
                <span class="p">}</span>
            <span class="p">}</span>
            type internal;
            cluster <span class="m">198.51.100.126</span><span class="p">;</span> <span class="c c-Singleline"># ❷</span>
            allow <span class="m">198.51.100.0/25</span><span class="p">;</span> <span class="c c-Singleline"># ❸</span>
            neighbor <span class="m">198.51.100.127</span><span class="p">;</span>
        <span class="p">}</span>
        group public-v6 <span class="p">{</span>
            family inet6 <span class="p">{</span>
                unicast <span class="p">{</span>
                    no-install;
                <span class="p">}</span>
            <span class="p">}</span>
            type internal;
            cluster <span class="m">198.51.100.126</span><span class="p">;</span>
            allow <span class="m">2001:db8:c633:6401::/64</span><span class="p">;</span>
            neighbor <span class="m">2001:db8:c633:6401::198</span>.51.100.127;
        <span class="p">}</span>
        ttl <span class="m">255</span><span class="p">;</span>
        bfd-liveness-detection <span class="p">{</span> <span class="c c-Singleline"># ❹</span>
            minimum-interval <span class="m">100</span><span class="p">;</span>
            multiplier <span class="m">5</span><span class="p">;</span>
        <span class="p">}</span>
    <span class="p">}</span>
<span class="p">}</span>
<span class="k">routing-options</span> <span class="p">{</span>
    router-id <span class="m">198.51.100.126</span><span class="p">;</span>
    autonomous-system <span class="m">65000</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Ce réflecteur accepte et redistribue toutes les routes IPv4 et
IPv6. En ❶, on s’assure que les routes ne sont pas installées dans la
FIB car un réflecteur n’est pas un routeur.</p>
<p>Chaque réflecteur doit disposer d’un <em>cluster identifier</em> qui est
utilisé pour détecter les boucles. Dans notre cas, nous utilisons
l’adresse IPv4 à cet effet (en ❷). En utilisant un identifiant
différent pour chaque réflecteur attaché à un même réseau L2, on
s’assure qu’ils s’échangeront les routes reçues, apportant ainsi une
meilleure résilience.</p>
<p>Au lieu de déclarer explicitement chacun des hyperviseurs devant se
connecter au réflecteur, un sous-réseau entier est autorisé en
❸<sup id="fnref:security"><a class="footnote-ref" href="#fn:security">11</a></sup>. Nous déclarons aussi le second réflecteur présent sur le
même réseau L2 afin qu’ils s’échangent leurs routes.</p>
<p>Un autre point important est de réagir rapidement en cas
d’<strong>indisponibilité d’un chemin</strong>. Avec des sessions <abbr title="Border Gateway Protocol">BGP</abbr> directement
connectées, un lien défaillant peut être détecté immédiatement et la
session <abbr title="Border Gateway Protocol">BGP</abbr> est aussitôt invalidée. Cela n’est pas toujours fiable et
dans notre cas, cela ne fonctionne pas en raison de la présence de
commutateurs sur les chemins. En ❹, nous activons <abbr title="Bidirectional Forwarding Detection">BFD</abbr>, un protocole
qui permet de détecter en moins d’une seconde<sup id="fnref:minimum"><a class="footnote-ref" href="#fn:minimum">12</a></sup> un problème
entre deux pairs <abbr title="Border Gateway Protocol">BGP</abbr> (<a href="https://www.rfc-editor.org/rfc/rfc5880" title="RFC 5880: Bidirectional Forwarding Detection (BFD)">RFC 5880</a>).</p>
<p>Un dernier point à prendre en compte est la possibilité de faire du
<strong>routage anycast</strong> : si une IP est publiée sur plusieurs
hyperviseurs, deux solutions sont acceptables :</p>
<ul>
<li>envoyer tous les flux vers un seul hyperviseur ;</li>
<li>répartir les flux entre les hyperviseurs.</li>
</ul>
<p>Le second choix permet d’obtenir un répartiteur de charge L3. Avec la
configuration ci-dessus, pour chaque préfixe, le réflecteur va choisir
un seul chemin et redistribuer celui-ci. Ainsi, un seul hyperviseur
recevra les paquets. Pour obtenir une répartition de charge, il faut
redistribuer l’ensemble des chemins possibles (<a href="https://www.rfc-editor.org/rfc/rfc7911" title="RFC 7911: Advertisement of Multiple Paths in BGP">RFC 7911</a>)<sup id="fnref:addpath"><a class="footnote-ref" href="#fn:addpath">13</a></sup> :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">set</span> protocols bgp group public-v4 family inet  unicast add-path send path-count <span class="m">4</span>
<span class="k">set</span> protocols bgp group public-v6 family inet6 unicast add-path send path-count <span class="m">4</span>
</code></pre></div>

<p>Voici un extrait de <code>show route</code> montrant quelques routes « simples »
ainsi qu’une route <em>anycast</em> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">&gt; </span>show<span class="w"> </span>route<span class="w"> </span>protocol<span class="w"> </span>bgp
<span class="go">inet.0: 6 destinations, 7 routes (7 active, 1 holddown, 0 hidden)</span>
<span class="go">+ = Active Route, - = Last Active, * = Both</span>

<span class="go">0.0.0.0/0        *[BGP/170] 00:09:01, localpref 100</span>
<span class="go">                    AS path: I, validation-state: unverified</span>
<span class="go">                  &gt; to 198.51.100.1 via em1.90</span>
<span class="go">192.0.2.15/32    *[BGP/170] 00:09:00, localpref 100</span>
<span class="go">                    AS path: I, validation-state: unverified</span>
<span class="go">                  &gt; to 198.51.100.101 via em1.90</span>
<span class="go">203.0.113.1/32   *[BGP/170] 00:09:00, localpref 100</span>
<span class="go">                    AS path: I, validation-state: unverified</span>
<span class="go">                  &gt; to 198.51.100.101 via em1.90</span>
<span class="go">203.0.113.6/32   *[BGP/170] 00:09:00, localpref 100</span>
<span class="go">                    AS path: I, validation-state: unverified</span>
<span class="go">                  &gt; to 198.51.100.102 via em1.90</span>
<span class="go">203.0.113.18/32  *[BGP/170] 00:09:00, localpref 100</span>
<span class="go">                    AS path: I, validation-state: unverified</span>
<span class="go">                  &gt; to 198.51.100.103 via em1.90</span>
<span class="go">203.0.113.10/32  *[BGP/170] 00:09:00, localpref 100</span>
<span class="go">                    AS path: I, validation-state: unverified</span>
<span class="hll"><span class="go">                  &gt; to 198.51.100.101 via em1.90</span>
</span><span class="go">                  [BGP/170] 00:09:00, localpref 100</span>
<span class="go">                    AS path: I, validation-state: unverified</span>
<span class="hll"><span class="go">                  &gt; to 198.51.100.102 via em1.90</span>
</span></code></pre></div>

<p>La configuration complète est disponible sur <a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/junos-RR3.conf" title="Configuration de Junos en réflecteur">GitHub</a>. Des
configurations similaires pour <a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/gobgpd.public-ipv4.yaml" title="Configuration de GoBGP en réflecteur"><em>GoBGP</em></a>, <a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/bird.RR1.conf" title="Configuration de BIRD en réflecteur"><em>BIRD</em></a>
ou <a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/RR7/etc/quagga/Quagga.conf" title="Configuration de FRR en réflecteur"><em>FRR</em></a> (sur Cumulus Linux) sont également
disponibles<sup id="fnref:bird"><a class="footnote-ref" href="#fn:bird">14</a></sup>. La configuration pour le domaine de routage privé
est identique. Pour éviter d’investir dans du matériel dédié pour les
réflecteurs, il est possible de convertir certains commutateurs de
baie à cet usage.</p>
<h3 id="configuration-de-lhyperviseur">Configuration de l’hyperviseur</h3>
<p>Passons maintenant à la dernière étape : la configuration de
l’hyperviseur. <a href="https://bird.network.cz/" title="The BIRD Internet Routing Daemon">BIRD</a> (1.6.x) est utilisé en tant que démon <abbr title="Border Gateway Protocol">BGP</abbr>. Il
maintient trois tables de routage internes (<code>public</code>, <code>private</code> and
<code>local-out</code>). Nous définissons un patron avec les propriétés communes
pour se connecter à un réflecteur :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">template</span> bgp rr_client <span class="p">{</span>
  local as <span class="m">65000</span><span class="p">;</span>   <span class="c c-Singleline"># ASN local correspond à l'ASN des réflecteurs</span>
  import all;       <span class="c c-Singleline"># Accepte toutes les routes reçues</span>
  export all;       <span class="c c-Singleline"># Envoie toutes les routes de la table</span>
  next hop self;    <span class="c c-Singleline"># Modifie le saut suivant avec l'IP de la session BGP</span>
  bfd yes;          <span class="c c-Singleline"># Active BFD</span>
  direct;           <span class="c c-Singleline"># Pair directement connecté</span>
  ttl security yes; <span class="c c-Singleline"># GTSM activé</span>
  add paths rx;     <span class="c c-Singleline"># Accepte ADD-PATH en réception</span>

  <span class="c c-Singleline"># Réétablissement rapide des sessions BGP</span>
  connect delay time <span class="m">1</span><span class="p">;</span>
  connect retry time <span class="m">5</span><span class="p">;</span>
  error wait time <span class="m">1</span>,5;
  error forget time <span class="m">10</span><span class="p">;</span>
<span class="p">}</span>

<span class="k">table</span> public;
<span class="k">protocol</span> bgp RR1_public from rr_client <span class="p">{</span>
  neighbor <span class="m">198.51.100.126</span> as <span class="m">65000</span><span class="p">;</span>
  table public;
<span class="p">}</span>
<span class="c c-Singleline"># […]</span>
</code></pre></div>

<p>Avec la configuration ci-dessus, toutes les routes de la table
<code>public</code> de <em>BIRD</em> sont envoyées au réflecteur
<code>198.51.100.126</code>. Toutes les routes reçues sont acceptées. Il nous
reste à connecter la table <code>public</code> de <em>BIRD</em> à celle du
noyau<sup id="fnref:rt_tables"><a class="footnote-ref" href="#fn:rt_tables">15</a></sup> :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">protocol</span> kernel kernel_public <span class="p">{</span>
  persist;
  scan time <span class="m">10</span><span class="p">;</span>
  import filter <span class="p">{</span>
    <span class="c c-Singleline"># Accepte n'importe quelle route du noyau</span>
    <span class="c c-Singleline"># sauf celle de dernier secours</span>
    if krt_metric &lt; <span class="m">4294967294</span> then accept;
    reject;
  <span class="p">};</span>
  export all;      <span class="c c-Singleline"># Envoie toutes les routes au noyau</span>
  learn;           <span class="c c-Singleline"># Apprend les routes autres que celles de BIRD</span>
  merge paths yes; <span class="c c-Singleline"># Utilise des routes ECMP si besoin</span>
  table public;    <span class="c c-Singleline"># Nom de la table de routage dans BIRD</span>
  kernel table <span class="m">90</span><span class="p">;</span> <span class="c c-Singleline"># Numéro de la table de routage dans le noyau</span>
<span class="p">}</span>
</code></pre></div>

<p>Il faut également activer <abbr title="Bidirectional Forwarding Detection">BFD</abbr> sur toutes les interfaces :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">protocol</span> bfd <span class="p">{</span>
  interface <span class="s2">"*"</span> <span class="p">{</span>
    interval <span class="m">100</span>ms;
    multiplier <span class="m">5</span><span class="p">;</span>
  <span class="p">};</span>
<span class="p">}</span>
</code></pre></div>

<p>Afin d’éviter qu’un encombrement temporaire de la table de suivi de
<em>Netfilter</em> n’impacte <abbr title="Bidirectional Forwarding Detection">BFD</abbr>, il est prudent de désactiver le suivi de
connexions pour ces paquets :</p>
<div class="language-bash codehilite"><pre><span/><code>ip46tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-A<span class="w"> </span>PREROUTING<span class="w"> </span>-p<span class="w"> </span>udp<span class="w"> </span>--dport<span class="w"> </span><span class="m">3784</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-m<span class="w"> </span>addrtype<span class="w"> </span>--dst-type<span class="w"> </span>LOCAL<span class="w"> </span>-j<span class="w"> </span>CT<span class="w"> </span>--notrack
ip46tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-A<span class="w"> </span>OUTPUT<span class="w"> </span>-p<span class="w"> </span>udp<span class="w"> </span>--dport<span class="w"> </span><span class="m">3784</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-m<span class="w"> </span>addrtype<span class="w"> </span>--src-type<span class="w"> </span>LOCAL<span class="w"> </span>-j<span class="w"> </span>CT<span class="w"> </span>--notrack
</code></pre></div>

<p>Il faut également ajouter :</p>
<ul>
<li><a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/bird-common/hypervisor.conf#L11-L40" title="Configuration de la table local-out">la configuration de la table <code>local-out</code></a> ;</li>
<li><a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/bird-common/rr-client-private.conf" title="Configuration pour la table de routage privée">la configuration de la table <code>private</code></a> ;</li>
<li><a href="https://github.com/vincentbernat/network-lab/blob/88b9b875c3c48c2f9e3ad4fb4e2719d6c4b3cc22/lab-l3-hyperv/bird-common/rr-client6.conf" title="Configuration pour IPv6">la configuration pour IPv6</a><sup id="fnref:ecmpv6"><a class="footnote-ref" href="#fn:ecmpv6">16</a></sup>.</li>
</ul>
<p>Une fois les sessions <abbr title="Border Gateway Protocol">BGP</abbr> établies, on peut vérifier les routes
apprises :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>ip<span class="w"> </span>route<span class="w"> </span>show<span class="w"> </span>table<span class="w"> </span>public<span class="w"> </span>proto<span class="w"> </span>bird
<span class="go">default</span>
<span class="go">        nexthop via 198.51.100.1 dev eth0.public weight 1</span>
<span class="go">        nexthop via 198.51.100.254 dev eth1.public weight 1</span>
<span class="go">203.0.113.6</span>
<span class="go">        nexthop via 198.51.100.102 dev eth0.public weight 1</span>
<span class="go">        nexthop via 198.51.100.202 dev eth1.public weight 1</span>
<span class="go">203.0.113.18</span>
<span class="go">        nexthop via 198.51.100.103 dev eth0.public weight 1</span>
<span class="go">        nexthop via 198.51.100.203 dev eth1.public weight 1</span>
</code></pre></div>

<h2 id="performance">Performance</h2>
<p>Avec de nombreuses routes, on peut se soucier de la <strong>quantité de
mémoire utilisée par Linux</strong>. Elle est très raisonnable :</p>
<ul>
<li>128 Mio permettent de gérer 1 million de routes IPv4 ;</li>
<li>512 Mio permettent de gérer 1 million de routes IPv6.</li>
</ul>
<p>Les chiffres doivent être doublés pour prendre en compte la mémoire
utilisée par <em>BIRD</em>. En ce qui concerne les <strong>temps de recherche d’une
route</strong>, les performances sont aussi excellentes avec IPv4 et très
bonnes avec IPv6 :</p>
<ul>
<li>30 ns par recherche avec 1 million de routes IPv4 ;</li>
<li>1.25 µs par recherche avec 1 million de routes IPv6.</li>
</ul>
<p>Ainsi, l’impact de laisser la gestion d’un très grand nombre de routes
à Linux est très faible. Pour plus de détails sur le sujet, voyez
« <a href="/fr/blog/2017-ipv4-table-routage-linux" title="Fonctionnement de la table de routage IPv4 sous Linux">Fonctionnement de la table de routage IPv4 sous Linux</a> » et « <a href="/fr/blog/2017-ipv6-table-routage-linux" title="Fonctionnement de la table de routage IPv6 sous Linux">Fonctionnement de la table de routage IPv6
sous Linux</a> ».</p>
<h2 id="filtrage-par-la-source">Filtrage par la source</h2>
<p>Pour éviter l’usurpation d’adresse IP, le filtrage par la source
(<em>reverse-path filtering</em>) est activé sur chaque interface virtuelle :
Linux va vérifier la légitimité de chaque IP source en vérifiant
qu’une réponse serait renvoyée sur la même interface. Cela interdit
toute tentative d’usurpation de la part d’une machine virtuelle.</p>
<p>Pour IPv4, il est possible d’activer cette fonctionnalité à travers un
<em>sysctl</em><sup id="fnref:sysctl"><a class="footnote-ref" href="#fn:sysctl">17</a></sup> ou à travers <em>Netfilter</em>. Pour IPv6, seule la
dernière option est disponible.</p>
<div class="language-bash codehilite"><pre><span/><code><span class="c1"># Pour IPv6, utilise Netfilter</span>
ip6tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-N<span class="w"> </span>RPFILTER
ip6tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-A<span class="w"> </span>RPFILTER<span class="w"> </span>-m<span class="w"> </span>rpfilter<span class="w"> </span>-j<span class="w"> </span>RETURN
ip6tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-A<span class="w"> </span>RPFILTER<span class="w"> </span>-m<span class="w"> </span>rpfilter<span class="w"> </span>--accept-local<span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-m<span class="w"> </span>addrtype<span class="w"> </span>--dst-type<span class="w"> </span>MULTICAST<span class="w"> </span>-j<span class="w"> </span>DROP
ip6tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-A<span class="w"> </span>RPFILTER<span class="w"> </span>-m<span class="w"> </span>limit<span class="w"> </span>--limit<span class="w"> </span><span class="m">5</span>/s<span class="w"> </span>--limit-burst<span class="w"> </span><span class="m">5</span><span class="w"> </span><span class="se">\</span>
<span class="w">  </span>-j<span class="w"> </span>LOG<span class="w"> </span>--log-prefix<span class="w"> </span><span class="s2">"NF: rpfilter: "</span><span class="w"> </span>--log-level<span class="w"> </span>warning
ip6tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-A<span class="w"> </span>RPFILTER<span class="w"> </span>-j<span class="w"> </span>DROP
ip6tables<span class="w"> </span>-t<span class="w"> </span>raw<span class="w"> </span>-A<span class="w"> </span>PREROUTING<span class="w"> </span>-i<span class="w"> </span>vnet+<span class="w"> </span>-j<span class="w"> </span>RPFILTER

<span class="c1"># Pour IPv4, utilise les sysctls</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.conf.all.rp_filter<span class="o">=</span><span class="m">0</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.conf.all.rp_filter<span class="o">=</span><span class="m">0</span>
<span class="k">for</span><span class="w"> </span>iface<span class="w"> </span><span class="k">in</span><span class="w"> </span>/sys/class/net/vnet*<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">    </span>sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.conf.<span class="si">${</span><span class="nv">iface</span><span class="p">##*/</span><span class="si">}</span>.rp_filter<span class="o">=</span><span class="m">1</span>
<span class="k">done</span>
</code></pre></div>

<p>Il n’est pas utile de s’inquiéter d’une usurpation sur le L2,
l’attaquant n’y gagnerait aucun avantage.</p>
<h1 id="leurrer-les-machines-virtuelles">Leurrer les machines virtuelles</h1>
<p>Un point important est de s’assurer que les machines virtuelles
continuent de penser qu’elles sont rattachées à un réseau L2 classique
(une IP, un sous-réseau, une passerelle).</p>
<p>La première étape consiste à leur fournir la passerelle par
défaut. Sur l’hyperviseur, il suffit d’assigner l’IP associée à
l’interface virtuelle correspondante :</p>
<div class="language-bash codehilite"><pre><span/><code>ip<span class="w"> </span>addr<span class="w"> </span>add<span class="w"> </span><span class="m">203</span>.0.113.254/32<span class="w"> </span>dev<span class="w"> </span>vnet5<span class="w"> </span>scope<span class="w"> </span>link
</code></pre></div>

<p>Le but de cette manœuvre est de s’assurer que Linux répondra aux
requêtes <abbr title="Address Resolution Protocol">ARP</abbr> concernant cette IP. La configuration d’un <code>/32</code> est
suffisante et il ne faut pas configurer un sous-réseau plus grand :
Linux installerait la route correspondante sur cette interface, ce qui
serait incorrect<sup id="fnref:noprefixroute"><a class="footnote-ref" href="#fn:noprefixroute">18</a></sup>.</p>
<p>Pour IPv6, ce n’est pas utile car les adresses de lien local sont
utilisées comme passerelle.</p>
<p>Une machine virtuelle peut également vouloir échanger des paquets avec
des machines partageant le même sous-réseau IP. L’hyperviseur va
répondre aux requêtes <abbr title="Address Resolution Protocol">ARP</abbr> à leur place. Une fois que le trafic IP est
reçu, il lui suffira alors de le router normalement. Cela peut se
faire en activant le proxy <abbr title="Address Resolution Protocol">ARP</abbr> sur l’interface virtuelle :</p>
<div class="language-bash codehilite"><pre><span/><code>sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.conf.vnet5.proxy_arp<span class="o">=</span><span class="m">1</span>
sysctl<span class="w"> </span>-qw<span class="w"> </span>net.ipv4.neigh.vnet5.proxy_delay<span class="o">=</span><span class="m">0</span>
</code></pre></div>

<p>Pour IPv6, le proxy <abbr title="Neighbor Discovery Protocol">NDP</abbr> de Linux est beaucoup moins pratique. À la
place, le démon <a href="https://github.com/DanielAdolfsson/ndppd/" title="NDP Proxy Daemon">ndppd</a> va gérer efficacement cette tâche. Pour
chaque interface, nous utilisons la configuration suivante :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">proxy</span> vnet5 <span class="p">{</span>
  rule <span class="m">2001:db8:cb00:7100::/64</span> <span class="p">{</span>
    static
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div>

<p><em>ndppd</em> <a href="https://github.com/DanielAdolfsson/ndppd/issues/41" title="Source address used by ndppd may not be a neighbor address #41">n’usurpe pas l’adresse source</a> et certains systèmes sont <a href="https://github.com/openbsd/src/blob/442f4bb483c9350a9d4c74fd7a2ae45f06c454dd/sys/netinet6/nd6_nbr.c#L155-L159" title="nd6_ns_input() dans OpenBSD">plus pointilleux à ce sujet</a>. Par conséquent, une adresse adéquate doit être
configurée sur chaque interface :</p>
<div class="language-bash codehilite"><pre><span/><code>ip<span class="w"> </span>addr<span class="w"> </span>add<span class="w"> </span><span class="m">2001</span>:db8:cb00:7100::1/64<span class="w"> </span>dev<span class="w"> </span>vnet5<span class="w"> </span>noprefixroute<span class="w"> </span>nodad
</code></pre></div>

<p>En ce qui concerne le <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr>, certains démons peuvent être contrariés
par l’adresse en <code>/32</code> associée à l’interface. Toutefois, <a href="https://thekelleys.org.uk/dnsmasq/doc.html">dnsmasq</a>
accepte de s’y faire. Si besoin, l’écriture d’un démon <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> est
relativement triviale. Pour IPv6, si l’adresse assignée est de type
<a href="https://fr.wikipedia.org/wiki/Adresse_IPv6#Assignation_des_adresses_IPv6_dans_un_r%C3%A9seau_local" title="Assgnation des adresses IPv6 dans un réseau local sur Wikipedia">EUI-64</a>, <a href="https://radvd.litech.org/" title="Linux IPv6 Router Advertisement Daemon (radvd)">radvd</a> fonctionne parfaitement avec cette configuration :</p>
<div class="language-junos codehilite"><pre><span/><code><span class="k">interface</span> vnet5 <span class="p">{</span>
  AdvSendAdvert on;
  prefix <span class="m">2001:db8:cb00:7100::/64</span> <span class="p">{</span>
    AdvOnLink on;
    AdvAutonomous on;
    AdvRouterAddr on;
  <span class="p">};</span>
<span class="p">};</span>
</code></pre></div>

<h1 id="conclusion-et-avenir">Conclusion et avenir</h1>
<p>La configuration présentée ici fonctionne avec <em>BIRD</em> 1.6.3 et Linux 3.15 ou plus récent. Elle permet de s’affranchir des limitations
inhérentes des réseaux L2 en termes de flexibilité ou de disponibilité
tout en restant transparent du point de vue des machines virtuelles
hébergées. En faisant reposer l’effort de routage sur Linux, la
solution est également économique car le matériel existant peut être
réutilisé. Enfin, l’exploitation d’un tel réseau reste simple après
avoir compris les bases (règles de routage, tables de routage et
sessions <abbr title="Border Gateway Protocol">BGP</abbr>).</p>
<p>Il existe plusieurs améliorations potentielles :</p>
<dl>
<dt>Utilisation des <abbr title="Virtual Routing and Forwarding">VRF</abbr></dt>
<dd>À partir de Linux 4.3, les domaines virtuels de routage L3 (<abbr title="Virtual Routing and Forwarding">VRF</abbr>)
  permettent d’associer des interfaces à des tables de routage. Nous
  pourrions donc définir trois <abbr title="Virtual Routing and Forwarding">VRF</abbr> : <code>public</code>, <code>private</code> et
  <code>local-out</code>. Cela permettrait d’améliorer les performances en
  retirant la nécessité de parcourir plusieurs règles de routage
  (cependant, avant Linux 4.8, les performances sont dégradées en
  raison de la non-utilisation des fonctionnalités d’accélération,
  voir le <a href="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=7889681f4a6c2148e1245604bac751a1cae8f882" title="net: vrf: Update flags and features settings">commit 7889681f4a6c</a>). Pour plus d’informations, reportez
  vous à la <a href="https://www.kernel.org/doc/Documentation/networking/vrf.txt" title="Virtual Routing and Forwarding (VRF)">documentation du noyau</a>.</dd>
<dt>Routage L3 de bout en bout</dt>
<dd>Plutôt que de reposer sur une fabrique L2, la configuration <abbr title="Border Gateway Protocol">BGP</abbr> peut
  être améliorée et simplifiée en utilisant un certain nombre de
  mécanismes d’autoconfiguration. Cumulus a publié un livret sur le
  sujet : « <a href="https://www.nvidia.com/en-us/networking/border-gateway-protocol/" title="BGP in the data center: a complete guide to BGP for the modern data center"><abbr title="Border Gateway Protocol">BGP</abbr> in the datacenter</a> ». Toutefois, cela nécessite que
  toutes les implémentations <abbr title="Border Gateway Protocol">BGP</abbr> utilisées supportent ces
  fonctionnalités. Sur les hyperviseurs, cela impose l’utilisation de
  <em>FRR</em> et sur les équipements réseau, seul <em>Cumulus Linux</em> remplit
  les critères adéquats.</dd>
<dt>Utilisation de <abbr title="Border Gateway Protocol">BGP</abbr> <abbr title="Long-Lived Graceful Restart">LLGR</abbr></dt>
<dd>Utiliser <abbr title="Bidirectional Forwarding Detection">BFD</abbr> avec des temps de réaction courts permet d’invalider
  très rapidement un chemin non fonctionnel. En contre-partie, en cas
  de congestion ou de charge élevée, des paquets <abbr title="Bidirectional Forwarding Detection">BFD</abbr> peuvent être
  perdus rendant l’hyperviseur et les machines qu’il héberge
  indisponibles jusqu’au rétablissement des sessions <abbr title="Border Gateway Protocol">BGP</abbr>. Certaines
  implémentations de <abbr title="Border Gateway Protocol">BGP</abbr> supportent le <em>Long-Lived <abbr title="Border Gateway Protocol">BGP</abbr> Graceful
  Restart</em>, une extension conservant les routes perdues mais avec une
  priorité plus faible (voir
  <a href="https://datatracker.ietf.org/doc/html/draft-uttaro-idr-bgp-persistence-03" title="Support for Long-lived BGP Graceful Restart">draft-uttaro-idr-bgp-persistence-03</a>). C’est une solution idéale
  pour notre problème : les routes perdues ne sont utilisées qu’en
  dernier secours quand tous les liens sont devenus
  indisponibles. <del>Actuellement, aucune implémentation libre n’existe.</del>
  Jetez un œil sur « <a href="/fr/blog/2018-bgp-llgr" title="BGP LLGR : sessions BGP fiables et réactives"><abbr title="Border Gateway Protocol">BGP</abbr> <abbr title="Long-Lived Graceful Restart">LLGR</abbr> : sessions <abbr title="Border Gateway Protocol">BGP</abbr> fiables et réactives</a> »
  pour plus de détails.</dd>
</dl>
<div class="admonition">
<p class="admonition-title">Mise à jour (08.2019)</p>
<p>Pour une meilleure sécurité, il est
possible d’utiliser les fonctionnalités autour de la RPKI pour valider
les annonces de chaque hyperviseur. Voir « <a href="/fr/blog/2019-bgp-serveur-rpki" title="Sécuriser BGP jusqu'à l'hôte avec la validation de l'origine">Sécuriser <abbr title="Border Gateway Protocol">BGP</abbr> sur le
serveur avec la validation de l’origine</a> ».</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (05.2021)</p>
<p>Facebook a publié « <a href="https://www.usenix.org/system/files/nsdi21-abhashkumar.pdf">Running <abbr title="Border Gateway Protocol">BGP</abbr> in Data
Centers at Scale</a> ». Ce papier explique comment Facebook utilise <abbr title="Border Gateway Protocol">BGP</abbr>
dans ses datacentres. Il s’agit d’une problématique orthogonale au
fait d’étendre <abbr title="Border Gateway Protocol">BGP</abbr> jusqu’au serveur mais c’est une lecture
intéressante avec, notamment, l’utilisation des politiques de routage
pour faciliter l’opérabilité.</p>
</div>
<div class="admonition">
<p class="admonition-title">Mise à jour (10.2021)</p>
<p>Nous avons publié la configuration
utilisée chez Blade dans nos datacentres de San Francisco et Séoul qui
sont en <abbr title="Border Gateway Protocol">BGP</abbr> jusqu’aux serveurs avec une fabrique L3. Cela comprend la
configuration pour des switchs <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/to1-p2.ussfo03.blade-group.net/config.txt">top-of-the-rack Juniper</a> et des
switchs <a href="https://github.com/jerikan-network/cmdb/tree/generated-public/output/to1-p5.ussfo03.blade-group.net">top-of-the-rack Cumulus</a>. Ceux-ci disposent d’un serveur
<abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> pour le provisioning. La configuration du <abbr title="Border Gateway Protocol">BGP</abbr> sur les serveurs se
fait via une <a href="https://github.com/jerikan-network/cmdb/blob/generated-public/output/gateway1.ussfo03.blade-group.net/nginx.conf">API servie par nginx</a>.</p>
</div>
<div class="footnote">
<hr/>
<ol>
<li id="fn:mclag">
<p>Le <abbr title="Multi-Chassis Link Aggregation">MC-LAG</abbr> a été standardisé dans <a href="https://sci-hub.fr/10.1109/IEEESTD.2014.7055197" title="IEEE 802.1AX-2014 on Sci-Hub">IEEE 802.1AX-2014</a>. Toutefois, il est probable que la plupart des
vendeurs conservent leurs propres implémentations. La
caractéristique essentielle d’un <abbr title="Multi-Chassis Link Aggregation">MC-LAG</abbr> est de garder des plans de
contrôle indépendants. <a class="footnote-backref" href="#fnref:mclag" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:incident">
<p>Un incident peut se déclencher à la suite d’une erreur
humaine mais également à cause de bugs logiciels. Ceux-ci
surviennent notamment lors des opérations peu fréquentes, comme
une mise à jour. <a class="footnote-backref" href="#fnref:incident" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:nodistribute">
<p>Ces routes ne doivent pas être distribuées via
<abbr title="Border Gateway Protocol">BGP</abbr>. Une route par défaut avec une métrique plus faible doit être
apprise depuis les routeurs de bordure. <a class="footnote-backref" href="#fnref:nodistribute" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:ddos">
<p>Il ne faut pas augmenter cette valeur de manière trop brutale.
Il est possible pour un attaquant de remplir le cache assez
facilement. La valeur proposée ici correspond à 256 Mio de
mémoire. Voir « <a href="/fr/blog/2017-ipv6-table-routage-linux#mise-en-cache" title="Fonctionnement de la table de routage IPv6 sous Linux">Fonctionnement de la table de routage IPv6 sous
Linux</a> » pour plus de détails. <a class="footnote-backref" href="#fnref:ddos" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:privacy">
<p>Cet exemple ne fonctionne que si la machine virtuelle
n’utilise pas les extensions pour la vie privée (<a href="https://www.rfc-editor.org/rfc/rfc3041" title="RFC 3041: Privacy Extensions for Stateless Address Autoconfiguration in IPv6">RFC 3041</a>).
Une solution alternative est d’assigner un <code>/64</code> à chaque machine
virtuelle. Cela présente aussi l’avantage de ne plus nécessiter de
traiter les paquets <abbr title="Neighbor Discovery Protocol">NDP</abbr>. <a class="footnote-backref" href="#fnref:privacy" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
<li id="fn:arp">
<p>Une entrée <abbr title="Address Resolution Protocol">ARP</abbr> statique peut également être ajoutée. <a class="footnote-backref" href="#fnref:arp" title="Jump back to footnote 6 in the text">↩</a></p>
</li>
<li id="fn:onlink">
<p>Le mot-clé <code>onlink</code> est nécessaire pour que le démon <abbr title="Border Gateway Protocol">BGP</abbr>
<em>BIRD</em> accepte la route. Dans le cas contraire, il considère qu’il
n’y a pas de route directement connectée valide pour résoudre le
prochain saut. <a class="footnote-backref" href="#fnref:onlink" title="Jump back to footnote 7 in the text">↩</a></p>
</li>
<li id="fn:current">
<p>Par exemple, un <a href="https://www.juniper.net/us/en/products/switches/qfx-series/qfx5100-ethernet-switch-datasheet.html" title="QFX5100 10/40GbE data center switches">Juniper QFX5100</a> accepte environ
200 000 routes IPv4 (pour environ 10 000 $, avec des puces
Broadcom Trident II). Un <a href="https://www.arista.com/en/products/7280r-series" title="Arista 7280R Series">Arista 7208SR</a> accepte quant à lui
plus d’un million de routes IPv4 (pour environ 20 000 $, avec des
puces Broadcom Jericho), à travers l’utilisation d’une <abbr title="Ternary content-addressable memory">TCAM</abbr>
externe. Un <a href="https://web.archive.org/web/2017/https://www.juniper.net/us/en/products/routers/mx-series/mx240-universal-routing-platform.html" title="MX240 edge router">Juniper MX240</a> peut gérer quant à lui plus de 2
millions de routes IPv4 (pour environ 30 000 $ pour un châssis
vide et deux cartes de routage, avec des puces Juniper Trio) avec
une densité moindre. <a class="footnote-backref" href="#fnref:current" title="Jump back to footnote 8 in the text">↩</a></p>
</li>
<li id="fn:scale">
<p>En ce qui concerne la taille maximale d’une telle fabrique,
avec des commutateurs capables de gérer 32 000 adresses MAC, la
fabrique peut héberger 8000 hyperviseurs (plus de 5 millions de
machines virtuelles). Ainsi, que ce soit pour la partie <em>leaf</em> ou
pour la partie <em>spine</em>, il est possible d’utiliser des
commutateurs classiques. Chaque hyperviseur devra savoir gérer
toutes les routes, ce qui n’est pas un problème avec Linux. <a class="footnote-backref" href="#fnref:scale" title="Jump back to footnote 9 in the text">↩</a></p>
</li>
<li id="fn:instances">
<p>L’utilisation d’instances de routage permettrait de
configurer plusieurs réflecteurs sur le même équipement. L’exemple
ne montre pas cette possibilité mais elle réduirait les coûts. <a class="footnote-backref" href="#fnref:instances" title="Jump back to footnote 10 in the text">↩</a></p>
</li>
<li id="fn:security">
<p>Cela interdit généralement d’utiliser un mécanisme
d’authentification. <abbr title="Border Gateway Protocol">BGP</abbr> utilise habituellement les signatures TCP
MD5 (<a href="https://www.rfc-editor.org/rfc/rfc2385" title="RFC 2385: Protection of BGP Sessions via the TCP MD5 Signature Option">RFC 2385</a>). Sur la plupart des systèmes, cela implique de
connaître par avance les pairs. Pour améliorer la sécurité, il est
possible d’utiliser le <em>Generalized TTL Security Mechanism</em> (<a href="https://www.rfc-editor.org/rfc/rfc5082" title="RFC 5082: The Generalized TTL Security Mechanism (GTSM)">RFC 5082</a>). Pour <em>Junos</em>, la configuration présentée ici (avec <code>ttl
255</code>) est incomplète. Un <a href="https://www.juniper.net/documentation/us/en/software/junos/cli-reference/topics/ref/statement/multihop-edit-protocols-bgp.html" title="Juniper documentation: ttl (Protocols BGP)">filtre de firewall est aussi
nécessaire</a>. <a class="footnote-backref" href="#fnref:security" title="Jump back to footnote 11 in the text">↩</a></p>
</li>
<li id="fn:minimum">
<p>Sur la série de commutateurs QFX5000, l’intervalle minimal
est <a href="https://www.juniper.net/documentation/us/en/software/junos/high-availability/topics/topic-map/bfd.html#id-understanding-bfd-for-static-routes-for-faster-network-failure-detection" title="Understanding Bidirectional Forwarding Detection (BFD)">officiellement</a> d’une seconde. C’est regrettable
car cela rend <abbr title="Bidirectional Forwarding Detection">BFD</abbr> quasiment inutile. <a class="footnote-backref" href="#fnref:minimum" title="Jump back to footnote 12 in the text">↩</a></p>
</li>
<li id="fn:addpath">
<p>Malheureusement, pour une raison particulièrement
mystérieuse, <em>Junos</em> ne supporte pas l’extension <abbr title="Border Gateway Protocol">BGP</abbr> <em>add-path</em>
dans une instance de routage. Une telle configuration est
toutefois possible avec Cumulus Linux. <a class="footnote-backref" href="#fnref:addpath" title="Jump back to footnote 13 in the text">↩</a></p>
</li>
<li id="fn:bird">
<p>Seul <em>BIRD</em> implémente <abbr title="Bidirectional Forwarding Detection">BFD</abbr> mais il ne supporte pas les pairs
implicites. <em>FRR</em> nécessite d’être couplé à <a href="https://github.com/CumulusNetworks/ptm" title="Prescriptive Topology Manager Daemon">PTMD</a> de
Cumulus. Si <abbr title="Bidirectional Forwarding Detection">BFD</abbr> ne vous intéresse pas, <em>GoBGP</em> fait un excellent
réflecteur de routes. <a class="footnote-backref" href="#fnref:bird" title="Jump back to footnote 14 in the text">↩</a></p>
</li>
<li id="fn:rt_tables">
<p>Les tables de routage du noyau sont numérotées. La
commande <code>ip</code> peut utiliser les noms déclarés dans le fichier
<code>/etc/iproute2/rt_tables</code>. <a class="footnote-backref" href="#fnref:rt_tables" title="Jump back to footnote 15 in the text">↩</a></p>
</li>
<li id="fn:ecmpv6">
<p>À noter que <em>BIRD</em> 1.6.1 est nécessaire pour faire
fonctionner l’ECMP avec IPv6. De plus, avec un Linux 4.11 ou plus
récent, le <a href="https://gitlab.nic.cz/labs/bird/-/commit/98bb80a243b58c43453e9be69d19d0350286549c" title="KRT: Fix IPv6 ECMP handling with Linux 4.11+">commit 98bb80a243b5</a> est également requis. <a class="footnote-backref" href="#fnref:ecmpv6" title="Jump back to footnote 16 in the text">↩</a></p>
</li>
<li id="fn:sysctl">
<p>Pour une interface donnée, Linux utilise la valeur maximale
entre le <em>sysctl</em> pour <code>all</code> et celui de l’interface. <a class="footnote-backref" href="#fnref:sysctl" title="Jump back to footnote 17 in the text">↩</a></p>
</li>
<li id="fn:noprefixroute">
<p>Il est possible d’empêcher Linux d’installer la
route connectée en utilisant le drapeau <code>noprefixroute</code>. Celui-ci
n’est disponible pour IPv4 que depuis Linux 4.4. Il n’est utile
que si le serveur <abbr title="Dynamic Host Configuration Protocol">DHCP</abbr> donne du fil à retordre car cela peut
entraîner d’autres problèmes liés à la promotion des IP
secondaires. <a class="footnote-backref" href="#fnref:noprefixroute" title="Jump back to footnote 18 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
  <entry>
    <title type="html">Mesure des performances d'une fonction du noyau Linux</title>
    <author><name>Vincent Bernat</name></author>
    <link href="https://vincent.bernat.ch/fr/blog/2017-microbenchmark-noyau-linux" rel="alternate"/>
    <link href="https://vincent.bernat.ch/fr/blog/2017-microbenchmark-noyau-linux#isso-thread" rel="replies" type="text/html"/>
    <updated>2017-12-28T08:27:30Z</updated>
    <id>http://www.luffy.cx/fr/blog/2017-microbenchmark-noyau-linux.html</id>

    <content type="xhtml">
      <div xmlns="http://www.w3.org/1999/xhtml">
<p>Habituellement, les performances d’un sous-système du noyau Linux sont
mesurées à travers un processus externe (local ou distant). Selon le
point d’entrée utilisé, de nombreuses fonctions peuvent être
impliquées. Pour tester les performances d’une seule fonction, une
solution est d’écrire un <strong>module noyau</strong>.</p>
<div class="toc">
<ul>
<li><a href="#module-noyau-minimal">Module noyau minimal</a></li>
<li><a href="#parametrisation">Paramétrisation</a></li>
<li><a href="#meilleures-statistiques">Meilleures statistiques</a></li>
<li><a href="#validite">Validité</a></li>
<li><a href="#alternative">Alternative</a></li>
</ul>
</div>
<h1 id="module-noyau-minimal">Module noyau minimal</h1>
<p>Supposons que l’on désire mesurer les performances de la fonction
<code>fib_lookup()</code> qui permet de rechercher une route IPv4. La fonction
noyau suivante exécute 1000 recherches pour 8.8.8.8 et retourne une
moyenne<sup id="fnref:loop"><a class="footnote-ref" href="#fn:loop">1</a></sup>. Elle utilise la fonction <code>get_cycles()</code> pour calculer
le temps d’exécution.</p>
<div class="language-c codehilite"><pre><span/><code><span class="cm">/* Exécute le test pour fib_lookup() and place</span>
<span class="cm">   le résultat dans `buf`. */</span>
<span class="k">static</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="nf">do_bench</span><span class="p">(</span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">t1</span><span class="p">,</span><span class="w"> </span><span class="n">t2</span><span class="p">;</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">i</span><span class="p">;</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="n">count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1000</span><span class="p">;</span>
<span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">err</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">    </span><span class="k">struct</span><span class="w"> </span><span class="nc">fib_result</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="w">    </span><span class="k">struct</span><span class="w"> </span><span class="nc">flowi4</span><span class="w"> </span><span class="n">fl4</span><span class="p">;</span>

<span class="w">    </span><span class="n">memset</span><span class="p">(</span><span class="o">&amp;</span><span class="n">fl4</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">sizeof</span><span class="p">(</span><span class="n">fl4</span><span class="p">));</span>
<span class="w">    </span><span class="n">fl4</span><span class="p">.</span><span class="n">daddr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">in_aton</span><span class="p">(</span><span class="s">"8.8.8.8"</span><span class="p">);</span>

<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">t1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_cycles</span><span class="p">();</span>
<span class="hll"><span class="w">        </span><span class="n">err</span><span class="w"> </span><span class="o">|=</span><span class="w"> </span><span class="n">fib_lookup</span><span class="p">(</span><span class="o">&amp;</span><span class="n">init_net</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">fl4</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">res</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">);</span>
</span><span class="w">        </span><span class="n">t2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_cycles</span><span class="p">();</span>
<span class="w">        </span><span class="n">total</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span><span class="n">t2</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">t1</span><span class="p">;</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span><span class="w"> </span><span class="s">"err=%d msg=</span><span class="se">\"</span><span class="s">lookup error</span><span class="se">\"\n</span><span class="s">"</span><span class="p">,</span><span class="w"> </span><span class="n">err</span><span class="p">);</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span><span class="w"> </span><span class="s">"avg=%llu</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="w"> </span><span class="n">total</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="n">count</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>

<p>Nous devons placer cette fonction dans un module noyau. Le code
suivant déclare un dossier <em>sysfs</em> contenant un pseudo-fichier
<code>run</code>. Quand l’utilisateur consulte ce fichier, le module exécute le
test et retourne le résultat en tant que contenu.</p>
<div class="language-c codehilite"><pre><span/><code><span class="cp">#define pr_fmt(fmt) "kbench: " fmt</span>

<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;linux/kernel.h&gt;</span>
<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;linux/version.h&gt;</span>
<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;linux/module.h&gt;</span>
<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;linux/inet.h&gt;</span>
<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;linux/timex.h&gt;</span>
<span class="cp">#include</span><span class="w"> </span><span class="cpf">&lt;net/ip_fib.h&gt;</span>

<span class="cm">/* Quand un utilisateur consulte le fichier "run", exécute</span>
<span class="cm">   la fonction de test. */</span>
<span class="k">static</span><span class="w"> </span><span class="kt">ssize_t</span><span class="w"> </span><span class="nf">run_show</span><span class="p">(</span><span class="k">struct</span><span class="w"> </span><span class="nc">kobject</span><span class="w"> </span><span class="o">*</span><span class="n">kobj</span><span class="p">,</span>
<span class="w">                        </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="o">*</span><span class="n">attr</span><span class="p">,</span>
<span class="w">                        </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">do_bench</span><span class="p">(</span><span class="n">buf</span><span class="p">);</span>
<span class="p">}</span>

<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="n">run_attr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">__ATTR_RO</span><span class="p">(</span><span class="n">run</span><span class="p">);</span>
<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">attribute</span><span class="w"> </span><span class="o">*</span><span class="n">bench_attributes</span><span class="p">[]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="o">&amp;</span><span class="n">run_attr</span><span class="p">.</span><span class="n">attr</span><span class="p">,</span>
<span class="w">    </span><span class="nb">NULL</span>
<span class="p">};</span>
<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">attribute_group</span><span class="w"> </span><span class="n">bench_attr_group</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="p">.</span><span class="n">attrs</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">bench_attributes</span><span class="p">,</span>
<span class="p">};</span>
<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobject</span><span class="w"> </span><span class="o">*</span><span class="n">bench_kobj</span><span class="p">;</span>

<span class="kt">int</span><span class="w"> </span><span class="nf">init_module</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">rc</span><span class="p">;</span>
<span class="w">    </span><span class="cm">/* ❶ Création d'un kobject "kbench" dans /sys/kernel. */</span>
<span class="hll"><span class="w">    </span><span class="n">bench_kobj</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">kobject_create_and_add</span><span class="p">(</span><span class="s">"kbench"</span><span class="p">,</span><span class="w"> </span><span class="n">kernel_kobj</span><span class="p">);</span>
</span><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">bench_kobj</span><span class="p">)</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="o">-</span><span class="n">ENOMEM</span><span class="p">;</span>

<span class="w">    </span><span class="cm">/* ❷ Création des fichiers associés au kobject. */</span>
<span class="hll"><span class="w">    </span><span class="n">rc</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sysfs_create_group</span><span class="p">(</span><span class="n">bench_kobj</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">bench_attr_group</span><span class="p">);</span>
</span><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">rc</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">kobject_put</span><span class="p">(</span><span class="n">bench_kobj</span><span class="p">);</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">rc</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="mi">0</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">void</span><span class="w"> </span><span class="nf">cleanup_module</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="n">kobject_put</span><span class="p">(</span><span class="n">bench_kobj</span><span class="p">);</span>
<span class="p">}</span>

<span class="n">MODULE_LICENSE</span><span class="p">(</span><span class="s">"GPL"</span><span class="p">);</span>
<span class="n">MODULE_DESCRIPTION</span><span class="p">(</span><span class="s">"Microbenchmark for fib_lookup()"</span><span class="p">);</span>
</code></pre></div>

<p>En ❶, <code>kobject_create_and_add()</code> crée un nouveau <code>kobject</code> appelé
<code>kbench</code>. Un <code>kobject</code> est l’abstraction derrière le système de
fichiers <em>sysfs</em>. Ce <code>kobject</code> sera visible en tant que
<code>/sys/kernel/kbench/</code>.</p>
<p>En ❷, <code>sysfs_create_group()</code> attache un ensemble d’attributs au
<code>kobject</code>. Ceux-ci se matérialisent comme des fichiers dans
<code>/sys/kernel/kbench/</code>. Nous n’en déclarons qu’un, <code>run</code>, avec la macro
<code>__ATTR_RO</code>. Cet attribut est en lecture seule (<code>0444</code>) et quand un
utilisateur tente d’en récupérer le contenu, la fonction <code>run_show()</code>
est invoquée avec un tampon de <code>PAGE_SIZE</code> octets comme dernier
argument. Elle doit retourner le nombre d’octets effectivement
écrits.</p>
<p>Pour plus de détails, consultez la <a href="https://elixir.bootlin.com/linux/v4.14.5/source/Documentation/kobject.txt" title="Everything you never wanted to know about kobjects, ksets, and ktypes">documentation du noyau</a>
et l’<a href="https://elixir.bootlin.com/linux/v4.14.5/source/samples/kobject/kobject-example.c" title="Sample kobject implementation">exemple associé</a>. De nombreux exemples sur le web
(incluant celui-ci) peuvent être partiellement obsolètes<sup id="fnref:backward"><a class="footnote-ref" href="#fn:backward">2</a></sup>.</p>
<p>Le <code>Makefile</code> suivant permet de compiler l’exemple :</p>
<div class="language-makefile codehilite"><pre><span/><code><span class="c"># Kernel module compilation</span>
<span class="nv">KDIR</span><span class="w"> </span><span class="o">=</span><span class="w"> </span>/lib/modules/<span class="k">$(</span>shell<span class="w"> </span>uname<span class="w"> </span>-r<span class="k">)</span>/build
<span class="nv">obj-m</span><span class="w"> </span><span class="o">+=</span><span class="w"> </span>kbench_mod.o
<span class="nf">kbench_mod.ko</span><span class="o">:</span><span class="w"> </span><span class="n">kbench_mod</span>.<span class="n">c</span>
<span class="w">    </span>make<span class="w"> </span>-C<span class="w"> </span><span class="k">$(</span>KDIR<span class="k">)</span><span class="w"> </span><span class="nv">M</span><span class="o">=</span><span class="k">$(</span>PWD<span class="k">)</span><span class="w"> </span>modules
</code></pre></div>

<p>Après exécution de <code>make</code>, vous devez obtenir un fichier
<code>kbench_mod.ko</code> :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>modinfo<span class="w"> </span>kbench_mod.ko
<span class="go">filename:       /home/bernat/code/…/kbench_mod.ko</span>
<span class="go">description:    Microbenchmark for fib_lookup()</span>
<span class="go">license:        GPL</span>
<span class="go">depends:</span>
<span class="go">name:           kbench_mod</span>
<span class="go">vermagic:       4.14.0-1-amd64 SMP mod_unload modversions</span>
</code></pre></div>

<p>Il convient de le charger et d’exécuter le test :</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>insmod<span class="w"> </span>./kbench_mod.ko
<span class="gp">$ </span>ls<span class="w"> </span>-l<span class="w"> </span>/sys/kernel/kbench/run
<span class="go">-r--r--r-- 1 root root 4096 déc.  10 16:05 /sys/kernel/kbench/run</span>
<span class="gp">$ </span>cat<span class="w"> </span>/sys/kernel/kbench/run
<span class="go">avg=75</span>
</code></pre></div>

<p>Le résultat est un nombre de cycles. Pour obtenir un temps
approximatif, on le divise par la fréquence du processeur en
gigahertz (25 ns pour un processeur cadencé à 3 GHz)<sup id="fnref:frequency"><a class="footnote-ref" href="#fn:frequency">3</a></sup>.</p>
<h1 id="parametrisation">Paramétrisation</h1>
<p>Le module utilise deux constantes : le nombre de boucles et la
destination à tester. En les exposant comme des attributs
supplémentaires de notre <code>kobject</code> et en définissant une paire de
fonctions pour les consulter et les modifier, on permet à
l’utilisateur de les manipuler :</p>
<div class="language-c codehilite"><pre><span/><code><span class="k">static</span><span class="w"> </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">loop_count</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="mi">5000</span><span class="p">;</span>
<span class="k">static</span><span class="w"> </span><span class="n">u32</span><span class="w">           </span><span class="n">flow_dst_ipaddr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mh">0x08080808</span><span class="p">;</span>

<span class="k">static</span><span class="w"> </span><span class="n">DEFINE_MUTEX</span><span class="p">(</span><span class="n">kb_lock</span><span class="p">);</span>

<span class="cm">/* Retourne le nombre de boucles. */</span>
<span class="k">static</span><span class="w"> </span><span class="kt">ssize_t</span><span class="w"> </span><span class="nf">loop_count_show</span><span class="p">(</span><span class="k">struct</span><span class="w"> </span><span class="nc">kobject</span><span class="w"> </span><span class="o">*</span><span class="n">kobj</span><span class="p">,</span>
<span class="w">                               </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="o">*</span><span class="n">attr</span><span class="p">,</span>
<span class="w">                               </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">ssize_t</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="w">    </span><span class="n">mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="n">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span><span class="w"> </span><span class="s">"%lu</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="w"> </span><span class="n">loop_count</span><span class="p">);</span>
<span class="w">    </span><span class="n">mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/* Modifie le nombre de boucles. */</span>
<span class="k">static</span><span class="w"> </span><span class="kt">ssize_t</span><span class="w"> </span><span class="nf">loop_count_store</span><span class="p">(</span><span class="k">struct</span><span class="w"> </span><span class="nc">kobject</span><span class="w"> </span><span class="o">*</span><span class="n">kobj</span><span class="p">,</span>
<span class="w">                                </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="o">*</span><span class="n">attr</span><span class="p">,</span>
<span class="w">                                </span><span class="k">const</span><span class="w"> </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">,</span>
<span class="w">                                </span><span class="kt">size_t</span><span class="w"> </span><span class="n">count</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">val</span><span class="p">;</span>
<span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">err</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">kstrtoul</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">val</span><span class="p">);</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">err</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">err</span><span class="p">;</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">val</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">1</span><span class="p">)</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="o">-</span><span class="n">EINVAL</span><span class="p">;</span>
<span class="w">    </span><span class="n">mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="n">loop_count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">val</span><span class="p">;</span>
<span class="w">    </span><span class="n">mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">count</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/* Retourne l'adresse de destination. */</span>
<span class="k">static</span><span class="w"> </span><span class="kt">ssize_t</span><span class="w"> </span><span class="nf">flow_dst_ipaddr_show</span><span class="p">(</span><span class="k">struct</span><span class="w"> </span><span class="nc">kobject</span><span class="w"> </span><span class="o">*</span><span class="n">kobj</span><span class="p">,</span>
<span class="w">                                    </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="o">*</span><span class="n">attr</span><span class="p">,</span>
<span class="w">                                    </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">ssize_t</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="w">    </span><span class="n">mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="n">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span><span class="w"> </span><span class="s">"%pI4</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">flow_dst_ipaddr</span><span class="p">);</span>
<span class="w">    </span><span class="n">mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/* Modifie l'adresse de destination. */</span>
<span class="k">static</span><span class="w"> </span><span class="kt">ssize_t</span><span class="w"> </span><span class="nf">flow_dst_ipaddr_store</span><span class="p">(</span><span class="k">struct</span><span class="w"> </span><span class="nc">kobject</span><span class="w"> </span><span class="o">*</span><span class="n">kobj</span><span class="p">,</span>
<span class="w">                                     </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="o">*</span><span class="n">attr</span><span class="p">,</span>
<span class="w">                                     </span><span class="k">const</span><span class="w"> </span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">,</span>
<span class="w">                                     </span><span class="kt">size_t</span><span class="w"> </span><span class="n">count</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="n">mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="n">flow_dst_ipaddr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">in_aton</span><span class="p">(</span><span class="n">buf</span><span class="p">);</span>
<span class="w">    </span><span class="n">mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">count</span><span class="p">;</span>
<span class="p">}</span>

<span class="cm">/* Définition des deux nouveaux attributs. */</span>
<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="n">loop_count_attr</span><span class="w">      </span><span class="o">=</span><span class="w"> </span><span class="n">__ATTR_RW</span><span class="p">(</span><span class="n">loop_count</span><span class="p">);</span>
<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="n">flow_dst_ipaddr_attr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">__ATTR_RW</span><span class="p">(</span><span class="n">flow_dst_ipaddr</span><span class="p">);</span>
<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">kobj_attribute</span><span class="w"> </span><span class="n">run_attr</span><span class="w">             </span><span class="o">=</span><span class="w"> </span><span class="n">__ATTR_RO</span><span class="p">(</span><span class="n">run</span><span class="p">);</span>
<span class="k">static</span><span class="w"> </span><span class="k">struct</span><span class="w"> </span><span class="nc">attribute</span><span class="w"> </span><span class="o">*</span><span class="n">bench_attributes</span><span class="p">[]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="o">&amp;</span><span class="n">loop_count_attr</span><span class="p">.</span><span class="n">attr</span><span class="p">,</span>
<span class="w">    </span><span class="o">&amp;</span><span class="n">flow_dst_ipaddr_attr</span><span class="p">.</span><span class="n">attr</span><span class="p">,</span>
<span class="w">    </span><span class="o">&amp;</span><span class="n">run_attr</span><span class="p">.</span><span class="n">attr</span><span class="p">,</span>
<span class="w">    </span><span class="nb">NULL</span>
<span class="p">};</span>
</code></pre></div>

<p>L’adresse IPv4 est stockée dans un entier 32 bits mais affichée et
modifiée en utilisant la notation en quadruplet. Le noyau fournit de
sympathiques fonctions à cet usage.</p>
<p>Après ce changement, deux nouveaux fichiers apparaissent dans le
dossier <code>/sys/kernel/kbench</code>. Nous pouvons lire et modifier leurs
contenus.</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp"># </span><span class="nb">cd</span><span class="w"> </span>/sys/kernel/kbench
<span class="gp"># </span>ls<span class="w"> </span>-l
<span class="go">-rw-r--r-- 1 root root 4096 déc.  10 19:10 flow_dst_ipaddr</span>
<span class="go">-rw-r--r-- 1 root root 4096 déc.  10 19:10 loop_count</span>
<span class="go">-r--r--r-- 1 root root 4096 déc.  10 19:10 run</span>
<span class="gp"># </span>cat<span class="w"> </span>loop_count
<span class="go">5000</span>
<span class="gp"># </span>cat<span class="w"> </span>flow_dst_ipaddr
<span class="go">8.8.8.8</span>
<span class="gp"># </span><span class="nb">echo</span><span class="w"> </span><span class="m">9</span>.9.9.9<span class="w"> </span>&gt;<span class="w"> </span>flow_dst_ipaddr
<span class="gp"># </span>cat<span class="w"> </span>flow_dst_ipaddr
<span class="go">9.9.9.9</span>
</code></pre></div>

<p>Il ne nous reste qu’à modifier la fonction <code>do_bench()</code> pour utiliser
ces variables :</p>
<div class="language-c codehilite"><pre><span/><code><span class="k">static</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="nf">do_bench</span><span class="p">(</span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="cm">/* … */</span>
<span class="w">    </span><span class="n">mutex_lock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>
<span class="w">    </span><span class="n">count</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">loop_count</span><span class="p">;</span>
<span class="w">    </span><span class="n">fl4</span><span class="p">.</span><span class="n">daddr</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">flow_dst_ipaddr</span><span class="p">;</span>
<span class="w">    </span><span class="n">mutex_unlock</span><span class="p">(</span><span class="o">&amp;</span><span class="n">kb_lock</span><span class="p">);</span>

<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="cm">/* … */</span>
</code></pre></div>

<h1 id="meilleures-statistiques">Meilleures statistiques</h1>
<p>Actuellement, seule une moyenne est calculée. Cette valeur est
habituellement peu significative :</p>
<ul>
<li>
<p>Un petit nombre de <strong>données aberrantes</strong> peut augmenter sensiblement
   la moyenne. Une aberration peut apparaître lorsque la fonction
   testée est préemptée du CPU. Cela ne se produit pas souvent si
   celle-ci est de courte durée (moins d’une milliseconde) mais
   lorsque cela a lieu, la différence est de l’ordre de plusieurs
   millisecondes, ce qui peut représenter plusieurs ordres de grandeur
   et fausser totalement la moyenne. Il est donc préférable d’utiliser
   la médiane<sup id="fnref:preempt"><a class="footnote-ref" href="#fn:preempt">4</a></sup>.</p>
</li>
<li>
<p>La <strong>distribution</strong> des valeurs peut être asymétrique et avoir
   plusieurs maxima locaux. Calculer plusieurs percentiles ou afficher
   un graphe de distribution est alors utile.</p>
</li>
</ul>
<p>Pour calculer des statistiques supplémentaires, les résultats sont
placés dans un tableau.</p>
<div class="language-c codehilite"><pre><span/><code><span class="k">static</span><span class="w"> </span><span class="kt">int</span><span class="w"> </span><span class="nf">do_bench</span><span class="p">(</span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="o">*</span><span class="n">results</span><span class="p">;</span>
<span class="w">    </span><span class="cm">/* … */</span>

<span class="w">    </span><span class="n">results</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">kmalloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">results</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">count</span><span class="p">,</span><span class="w"> </span><span class="n">GFP_KERNEL</span><span class="p">);</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">results</span><span class="p">)</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span><span class="w"> </span><span class="s">"msg=</span><span class="se">\"</span><span class="s">no memory</span><span class="se">\"\n</span><span class="s">"</span><span class="p">);</span>

<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">t1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_cycles</span><span class="p">();</span>
<span class="w">        </span><span class="n">err</span><span class="w"> </span><span class="o">|=</span><span class="w"> </span><span class="n">fib_lookup</span><span class="p">(</span><span class="o">&amp;</span><span class="n">init_net</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">fl4</span><span class="p">,</span><span class="w"> </span><span class="o">&amp;</span><span class="n">res</span><span class="p">,</span><span class="w"> </span><span class="mi">0</span><span class="p">);</span>
<span class="w">        </span><span class="n">t2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_cycles</span><span class="p">();</span>
<span class="hll"><span class="w">        </span><span class="n">results</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">t2</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">t1</span><span class="p">;</span>
</span><span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">err</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">kfree</span><span class="p">(</span><span class="n">results</span><span class="p">);</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span><span class="w"> </span><span class="s">"err=%d msg=</span><span class="se">\"</span><span class="s">lookup error</span><span class="se">\"\n</span><span class="s">"</span><span class="p">,</span><span class="w"> </span><span class="n">err</span><span class="p">);</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="cm">/* Calcule et affiche les statistiques */</span>
<span class="w">    </span><span class="n">display_statistics</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">results</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">);</span>

<span class="w">    </span><span class="n">kfree</span><span class="p">(</span><span class="n">results</span><span class="p">);</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">strnlen</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div>

<p>Il nous faut ensuite une fonction pour calculer des percentiles :</p>
<div class="language-c codehilite"><pre><span/><code><span class="k">static</span><span class="w"> </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="nf">percentile</span><span class="p">(</span><span class="kt">int</span><span class="w"> </span><span class="n">p</span><span class="p">,</span>
<span class="w">                                     </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="o">*</span><span class="n">sorted</span><span class="p">,</span>
<span class="w">                                     </span><span class="kt">unsigned</span><span class="w"> </span><span class="n">count</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">int</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">p</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">count</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">100</span><span class="p">;</span>
<span class="w">    </span><span class="kt">int</span><span class="w"> </span><span class="n">index2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">index</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">p</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">count</span><span class="w"> </span><span class="o">%</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span>
<span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">sorted</span><span class="p">[</span><span class="n">index</span><span class="p">];</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">index2</span><span class="w"> </span><span class="o">&gt;=</span><span class="w"> </span><span class="n">count</span><span class="p">)</span>
<span class="w">        </span><span class="n">index2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">index</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">index2</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span>
<span class="w">        </span><span class="n">index2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">index</span><span class="p">;</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="p">(</span><span class="n">sorted</span><span class="p">[</span><span class="n">index</span><span class="p">]</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">sorted</span><span class="p">[</span><span class="n">index</span><span class="o">+</span><span class="mi">1</span><span class="p">])</span><span class="w"> </span><span class="o">/</span><span class="w"> </span><span class="mi">2</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Cette fonction nécessite un tableau trié en entrée. Le noyau fournit à
cet effet une fonction de <a href="https://elixir.bootlin.com/linux/v4.14.5/source/lib/sort.c#L62" title="Kernel sort() function">tri par tas</a>,
<code>sort()</code>. Une autre valeur utile est la déviation par rapport à la
médiane. La fonction ci-dessous calcule la <a href="https://en.wikipedia.org/wiki/Median_absolute_deviation" title="Déviation absolue médiane sur Wikipedia">déviation absolue
médiane</a><sup id="fnref:mad"><a class="footnote-ref" href="#fn:mad">5</a></sup> :</p>
<div class="language-c codehilite"><pre><span/><code><span class="k">static</span><span class="w"> </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="nf">mad</span><span class="p">(</span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="o">*</span><span class="n">sorted</span><span class="p">,</span>
<span class="w">                              </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">median</span><span class="p">,</span>
<span class="w">                              </span><span class="kt">unsigned</span><span class="w"> </span><span class="n">count</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="o">*</span><span class="n">dmedian</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">kmalloc</span><span class="p">(</span><span class="k">sizeof</span><span class="p">(</span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="p">)</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="n">count</span><span class="p">,</span>
<span class="w">                                          </span><span class="n">GFP_KERNEL</span><span class="p">);</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="n">i</span><span class="p">;</span>

<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="o">!</span><span class="n">dmedian</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span>
<span class="w">    </span><span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">sorted</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">&gt;</span><span class="w"> </span><span class="n">median</span><span class="p">)</span>
<span class="w">            </span><span class="n">dmedian</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">sorted</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">median</span><span class="p">;</span>
<span class="w">        </span><span class="k">else</span>
<span class="w">            </span><span class="n">dmedian</span><span class="p">[</span><span class="n">i</span><span class="p">]</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">median</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">sorted</span><span class="p">[</span><span class="n">i</span><span class="p">];</span>
<span class="w">    </span><span class="p">}</span>
<span class="w">    </span><span class="n">sort</span><span class="p">(</span><span class="n">dmedian</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">,</span><span class="w"> </span><span class="k">sizeof</span><span class="p">(</span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="p">),</span><span class="w"> </span><span class="n">compare_ull</span><span class="p">,</span><span class="w"> </span><span class="nb">NULL</span><span class="p">);</span>
<span class="w">    </span><span class="n">res</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">percentile</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span><span class="w"> </span><span class="n">dmedian</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">);</span>
<span class="w">    </span><span class="n">kfree</span><span class="p">(</span><span class="n">dmedian</span><span class="p">);</span>
<span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">res</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div>

<p>Ces deux fonctions nous permettent de fournir des statistiques
supplémentaires :</p>
<div class="language-c codehilite"><pre><span/><code><span class="k">static</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">display_statistics</span><span class="p">(</span><span class="kt">char</span><span class="w"> </span><span class="o">*</span><span class="n">buf</span><span class="p">,</span>
<span class="w">                               </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="o">*</span><span class="n">results</span><span class="p">,</span>
<span class="w">                               </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">count</span><span class="p">)</span>
<span class="p">{</span>
<span class="w">    </span><span class="kt">unsigned</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="kt">long</span><span class="w"> </span><span class="n">p95</span><span class="p">,</span><span class="w"> </span><span class="n">p90</span><span class="p">,</span><span class="w"> </span><span class="n">p50</span><span class="p">;</span>

<span class="w">    </span><span class="n">sort</span><span class="p">(</span><span class="n">results</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">,</span><span class="w"> </span><span class="k">sizeof</span><span class="p">(</span><span class="o">*</span><span class="n">results</span><span class="p">),</span><span class="w"> </span><span class="n">compare_ull</span><span class="p">,</span><span class="w"> </span><span class="nb">NULL</span><span class="p">);</span>
<span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">count</span><span class="w"> </span><span class="o">==</span><span class="w"> </span><span class="mi">0</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">        </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span><span class="w"> </span><span class="s">"msg=</span><span class="se">\"</span><span class="s">no match</span><span class="se">\"\n</span><span class="s">"</span><span class="p">);</span>
<span class="w">        </span><span class="k">return</span><span class="p">;</span>
<span class="w">    </span><span class="p">}</span>

<span class="w">    </span><span class="n">p95</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">percentile</span><span class="p">(</span><span class="mi">95</span><span class="p">,</span><span class="w"> </span><span class="n">results</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">);</span>
<span class="w">    </span><span class="n">p90</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">percentile</span><span class="p">(</span><span class="mi">90</span><span class="p">,</span><span class="w"> </span><span class="n">results</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">);</span>
<span class="w">    </span><span class="n">p50</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">percentile</span><span class="p">(</span><span class="mi">50</span><span class="p">,</span><span class="w"> </span><span class="n">results</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">);</span>
<span class="w">    </span><span class="n">scnprintf</span><span class="p">(</span><span class="n">buf</span><span class="p">,</span><span class="w"> </span><span class="n">PAGE_SIZE</span><span class="p">,</span>
<span class="w">          </span><span class="s">"min=%llu max=%llu count=%lu 95th=%llu 90th=%llu 50th=%llu mad=%llu</span><span class="se">\n</span><span class="s">"</span><span class="p">,</span>
<span class="w">          </span><span class="n">results</span><span class="p">[</span><span class="mi">0</span><span class="p">],</span>
<span class="w">          </span><span class="n">results</span><span class="p">[</span><span class="n">count</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">1</span><span class="p">],</span>
<span class="w">          </span><span class="n">count</span><span class="p">,</span>
<span class="w">          </span><span class="n">p95</span><span class="p">,</span>
<span class="w">          </span><span class="n">p90</span><span class="p">,</span>
<span class="w">          </span><span class="n">p50</span><span class="p">,</span>
<span class="w">          </span><span class="n">mad</span><span class="p">(</span><span class="n">results</span><span class="p">,</span><span class="w"> </span><span class="n">p50</span><span class="p">,</span><span class="w"> </span><span class="n">count</span><span class="p">));</span>
<span class="p">}</span>
</code></pre></div>

<div class="admonition">
<p class="admonition-title">Mise à jour (01.2018)</p>
<p><a href="https://web.archive.org/web/2017/https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf" title="How to Benchmark Code Execution Times on Intel ® IA-32 and IA-64 Instruction Set Architectures">Intel</a> recommande d’utiliser le minimum
lorsque sa variance est faible.</p>
</div>
<p>Enfin, ajoutons un <a href="https://github.com/vincentbernat/network-lab/blob/7cd27509497f19250c34f38af90178bf781fc163/lab-routes-ipv4/kbench_mod.c#L165-L207">graphique de la distribution</a> (incluant la fonction de répartition) :</p>
<div class="language-text-only codehilite"><pre><span/><code>min=72 max=33364 count=100000 95th=154 90th=142 50th=112 mad=6
    value │                      ┊                         count
       72 │                                                   51
       77 │▒                                                3548
       82 │▒▒░░                                             4773
       87 │▒▒░░░░░                                          5918
       92 │░░░░░░░                                          1207
       97 │░░░░░░░                                           437
      102 │▒▒▒▒▒▒░░░░░░░░                                  12164
      107 │▒▒▒▒▒▒▒░░░░░░░░░░░░░░                           15508
      112 │▒▒▒▒▒▒▒▒▒▒▒░░░░░░░░░░░░░░░░░░░░░░               23014
      117 │▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░             6297
      122 │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░              905
      127 │▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░           3845
      132 │▒▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░       6687
      137 │▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░     4884
      142 │▒▒░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░   4133
      147 │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  1015
      152 │░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░░  1123
</code></pre></div>

<h1 id="validite">Validité</h1>
<p>Bien que les tests fournissent quelques chiffres attrayants, leur
validité peut laisser dubitatif. Il y a plusieurs écueils à
considérer :</p>
<dl>
<dt>Code mort</dt>
<dd>Un compilateur peut retirer le code non utilisé. Dans notre exemple,
  nous combinons le résultat de la fonction testée avec une variable
  pour éviter ceci.</dd>
<dt>Échauffement</dt>
<dd>La mise en place des données initiales peut affecter négativement le
  test. En C, c’est un phénomène peu probable. Toutefois, l’ajout
  d’une <a href="https://github.com/vincentbernat/network-lab/blob/7cd27509497f19250c34f38af90178bf781fc163/lab-routes-ipv4/kbench_mod.c#L258-L265">phase d’échauffement</a> reste possible.</dd>
<dt>Jeu de données trop petit</dt>
<dd>Si le test utilise toujours les mêmes arguments en entrée, le
  résultat peut être calculé entièrement à partir de données en cache. Cela
  avantage le test indûment. Il convient donc
  d’<a href="https://github.com/vincentbernat/network-lab/blob/7cd27509497f19250c34f38af90178bf781fc163/lab-routes-ipv6/kbench_mod.c#L278-L309">itérer sur un grand ensemble de données</a>.</dd>
<dt>Jeu de données trop régulier</dt>
<dd>Quand les arguments donnés en entrée sont trop réguliers, le
  résultat peut également être impacté positivement : chaque test peut
  être dérivé en utilisant les données du test précédent (qui se
  trouvent en cache). Dans notre exemple, les routes sont organisées
  dans un arbre et il convient donc de ne pas parcourir linéairement
  l’espace d’adresses. Ce dernier peut être exploré aléatoirement en utilisant
  un <a href="https://github.com/vincentbernat/network-lab/blob/7cd27509497f19250c34f38af90178bf781fc163/lab-routes-ipv6/kbench_mod.c#L113-L116">simple générateur congruentiel linéaire</a>.</dd>
<dt>Coûts additionnels</dt>
<dd>Lorsque la fonction testée s’exécute en quelques nanosecondes, le
  coût induit par l’infrastructure de test peut s’avérer trop
  élevé. La méthode présentée ici implique un coût d’environ 5
  nanosecondes. La fonction <code>get_cycles()</code> est une mince couche autour
  de l’<a href="https://fr.wikipedia.org/wiki/RDTSC" title="RDTSC sur Wikipedia">instruction RDTSC</a> qui permet d’obtenir le
  nombre de cycles depuis la dernière mise à zéro du processeur. Elle
  est aussi disponible en version virtualisée pour un coût similaire
  lorsque le test est effectué sur une machine virtuelle. Pour mesurer
  une fonction avec une précision accrue, une solution est d’évaluer
  le temps d’exécution d’une boucle. Toutefois, les boucles apportent
  leur propre coût, notamment quand il faut calculer des arguments
  différents pour chaque itération. Elles cachent de plus la
  distribution des résultats et les compilateurs aiment les optimiser.</dd>
<dt>Préemption</dt>
<dd>Pendant l’exécution du test, le CPU peut être préempté pour une
  autre tâche. Lorsque la fonction testée prend moins d’une
  milliseconde, c’est un cas particulièrement rare qui est filtré par
  l’utilisation des percentiles.</dd>
<dt>Bruit</dt>
<dd>D’autres processus (ou d’autres machines virtuelles) peuvent
  accaparer le CPU de manière peu prévisible d’un test à l’autre. Il
  n’est donc pas souhaitable de faire tourner de tels tests dans un
  nuage public. D’un autre côté, un test peut être rendu plus réaliste
  par ajout d’un bruit contrôlé. Dans notre exemple, la recherche
  d’une route ne représente qu’une petite partie du travail nécessaire
  pour router un paquet. Mesurer cette fonction dans une boucle serrée
  affecte positivement le test.</dd>
<dt>Synchronisation de tests parallèles</dt>
<dd>Bien qu’il soit possible de lancer plusieurs tests en parallèle, il
  est difficile de s’assurer qu’ils s’exécutent bien tous en même
  temps. Cela fausse les résultats car certains tests tournent sans
  contention. Idéalement, chaque test devrait tourner à vide jusqu’à
  ce que tous les tests soient prêts. Cela ne semble pas une
  modification triviale.</dd>
</dl>
<p>En conclusion, le module de test présenté ici est plutôt primitif
(notamment par rapport à des outils tels que <a href="https://openjdk.org/projects/code-tools/jmh/" title="OpenJDK jmh">JMH</a> pour Java) mais
il peut produire des résultats concluants tels que ceux présentés dans
« <a href="/fr/blog/2017-ipv4-table-routage-linux" title="Fonctionnement de la table de routage IPv4 sous Linux">Fonctionnement de la table de routage IPv4 sous Linux</a> » et « <a href="/fr/blog/2017-ipv6-table-routage-linux" title="Fonctionnement de la table de routage IPv6 sous Linux">Fonctionnement de la table de routage IPv6
sous Linux</a> ».</p>
<div class="admonition">
<p class="admonition-title">Mise à jour (01.2018)</p>
<p><a href="https://web.archive.org/web/2017/https://www.intel.com/content/dam/www/public/us/en/documents/white-papers/ia-32-ia-64-benchmark-code-execution-paper.pdf" title="How to Benchmark Code Execution Times on Intel ® IA-32 and IA-64 Instruction Set Architectures">Intel</a> recommande de désactiver toutes
les optimisations liées à l’énergie, notamment le changement dynamique
de la fréquence (<code>cpupower frequency-set -g performance</code>) et le mode
<em>turbo</em> (<code>echo 1 &gt; /sys/devices/system/cpu/intel_pstate/no_turbo</code>) car
le <abbr title="Time Stamp Counter">TSC</abbr> est <a href="https://en.wikipedia.org/wiki/Time_Stamp_Counter#Implementation_in_various_processors" title="Time Stamp Counter: Implementation in various processors">indépendant de la fréquence réelle</a>.</p>
</div>
<h1 id="alternative">Alternative</h1>
<p>L’utilisation d’un outil de <em>tracing</em> constitue une approche
alternative. Pour mesurer le temps de recherche des routes IPv4, nous
pouvons faire tourner le processus suivant :</p>
<div class="language-bash codehilite"><pre><span/><code><span class="k">while</span><span class="w"> </span>true<span class="p">;</span><span class="w"> </span><span class="k">do</span>
<span class="w">  </span>ip<span class="w"> </span>route<span class="w"> </span>get<span class="w"> </span><span class="k">$((</span><span class="nv">RANDOM</span><span class="o">%</span><span class="m">100</span><span class="k">))</span>.<span class="k">$((</span><span class="nv">RANDOM</span><span class="o">%</span><span class="m">100</span><span class="k">))</span>.<span class="k">$((</span><span class="nv">RANDOM</span><span class="o">%</span><span class="m">100</span><span class="k">))</span>.5
<span class="w">  </span>sleep<span class="w"> </span><span class="m">0</span>.1
<span class="k">done</span>
</code></pre></div>

<p>Ensuite, nous instrumentons la fonction <code>__fib_lookup()</code> via <abbr title="Extended Berkeley Packet Filter">eBPF</abbr> (à
l’aide de <a href="https://github.com/iovisor/bcc" title="BCC - Tools for BPF-based Linux IO analysis, networking, monitoring, and more">BCC</a>):</p>
<div class="language-bash-session codehilite"><pre><span/><code><span class="gp">$ </span>sudo<span class="w"> </span>funclatency-bpfcc<span class="w"> </span>__fib_lookup
<span class="go">Tracing 1 functions for "__fib_lookup"... Hit Ctrl-C to end.</span>
<span class="go">^C</span>
<span class="go">     nsecs               : count     distribution</span>
<span class="go">         0 -&gt; 1          : 0        |                    |</span>
<span class="go">         2 -&gt; 3          : 0        |                    |</span>
<span class="go">         4 -&gt; 7          : 0        |                    |</span>
<span class="go">         8 -&gt; 15         : 0        |                    |</span>
<span class="go">        16 -&gt; 31         : 0        |                    |</span>
<span class="go">        32 -&gt; 63         : 0        |                    |</span>
<span class="go">        64 -&gt; 127        : 0        |                    |</span>
<span class="go">       128 -&gt; 255        : 0        |                    |</span>
<span class="go">       256 -&gt; 511        : 3        |*                   |</span>
<span class="go">       512 -&gt; 1023       : 1        |                    |</span>
<span class="go">      1024 -&gt; 2047       : 2        |*                   |</span>
<span class="go">      2048 -&gt; 4095       : 13       |******              |</span>
<span class="go">      4096 -&gt; 8191       : 42       |********************|</span>
</code></pre></div>

<p>Actuellement, le coût de l’instrumentation est très élevé et ne permet
pas d’obtenir de résultats pertinents. <del>Cela peut changer à l’avenir
lorsque Linux ajoutera la possibilité de <a href="https://lwn.net/Articles/740831/" title="tracing: Inter-event (e.g. latency) support">mesurer la latence entre
deux évènements</a>.</del></p>
<div class="footnote">
<hr/>
<ol>
<li id="fn:loop">
<p>Dans ce cas simplifié, il serait plus précis d’utiliser :</p>
<div class="language-c codehilite"><pre><span/><code><span class="n">t1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_cycles</span><span class="p">();</span>
<span class="k">for</span><span class="w"> </span><span class="p">(</span><span class="n">i</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">0</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="w"> </span><span class="o">&lt;</span><span class="w"> </span><span class="n">count</span><span class="p">;</span><span class="w"> </span><span class="n">i</span><span class="o">++</span><span class="p">)</span><span class="w"> </span><span class="p">{</span>
<span class="w">    </span><span class="n">err</span><span class="w"> </span><span class="o">|=</span><span class="w"> </span><span class="n">fib_lookup</span><span class="p">(</span><span class="err">…</span><span class="p">);</span>
<span class="p">}</span>
<span class="n">t2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">get_cycles</span><span class="p">();</span>
<span class="n">total</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">t2</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="n">t1</span><span class="p">;</span>
</code></pre></div>

<p>Cependant, cela nous interdit de calculer d’autres statistiques ainsi
que de faire évoluer cet exemple pour faire varier les arguments de la
fonction <code>fib_lookup()</code>. <a class="footnote-backref" href="#fnref:loop" title="Jump back to footnote 1 in the text">↩</a></p>
</li>
<li id="fn:backward">
<p>La compatibilité descendante de l’API du noyau <a href="https://elixir.bootlin.com/linux/v4.14.5/source/Documentation/process/stable-api-nonsense.rst" title="The Linux Kernel Driver Interface">n’est pas
un but</a> recherché par ses développeurs. <a class="footnote-backref" href="#fnref:backward" title="Jump back to footnote 2 in the text">↩</a></p>
</li>
<li id="fn:frequency">
<p>La fréquence actuelle du processeur s’obtient avec la
commande <code>cpupower frequency-info</code>. Comme cette fréquence varie dans
le temps (même en utilisant le gouverneur <code>performance</code>), ce n’est
pas forcément précis mais cela donne une représentation plus simple
à comprendre (à condition d’utiliser la même fréquence pour comparer
les résultats). <a class="footnote-backref" href="#fnref:frequency" title="Jump back to footnote 3 in the text">↩</a></p>
</li>
<li id="fn:preempt">
<p>Il est possible de désactiver la préemption dans le
noyau :</p>
<div class="language-c codehilite"><pre><span/><code><span class="n">preempt_disable</span><span class="p">();</span>
<span class="n">raw_local_irq_save</span><span class="p">(</span><span class="n">flags</span><span class="p">);</span>
<span class="cm">/* […] */</span>
<span class="n">raw_local_irq_restore</span><span class="p">(</span><span class="n">flags</span><span class="p">);</span>
<span class="n">preempt_enable</span><span class="p">();</span>
</code></pre></div>

<p>Cependant, sur les machines physiques, le <em>management</em> peut toujours
voler le CPU et sur les machines virtuelles, il n’est pas possible
d’influencer l’hôte. <a class="footnote-backref" href="#fnref:preempt" title="Jump back to footnote 4 in the text">↩</a></p>
</li>
<li id="fn:mad">
<p>Dans le noyau, seule l’arithmétique des entiers est
disponible. Bien qu’il soit possible de calculer une approximation
de la déviation standard avec des entiers, la déviation absolue
médiane réutilise la fonction <code>percentile()</code> définie précédemment. <a class="footnote-backref" href="#fnref:mad" title="Jump back to footnote 5 in the text">↩</a></p>
</li>
</ol>
</div>
      </div>
    </content>
  </entry>
</feed>