Salle 8
L’ARC
Un cache en mémoire doit choisir ce qu’il oublie. Qu’il oublie le bloc utilisé le moins récemment, comme un cache LRU, et une seule grosse lecture, une sauvegarde ou le parcours d’un gros fichier, chasse tout ce qui servait : chaque bloc parcouru est plus récent que l’ensemble de travail. Qu’il oublie le moins fréquemment utilisé, comme un cache LFU, et les blocs chauds d’hier restent, si froids soient-ils aujourd’hui.
L’ARC, le cache de ZFS, range les blocs vus une fois récemment dans une liste, MRU, et les blocs vus au moins deux fois dans une autre, MFU : un parcours ne fait que traverser la première. Et il se souvient de ce qu’il vient d’évincer : une liste fantôme garde l’en-tête de chaque bloc évincé, sans ses données. Une lecture qui tombe sur un fantôme est un échec qu’une liste plus grande aurait changé en succès, et l’ARC déplace son équilibre vers cette liste.
1 Lire, parcourir, relire
Le cache ci-dessous tient 16 blocs de 128 Kio : c’est sa taille cible, arc_c. Il s’ouvre sur un
ensemble chaud, H1 à H6, lu deux fois à une seconde d’intervalle, donc tout entier dans MFU. Chaque liste va de sa
tête, le plus récent, à sa queue, évincée en premier. Lisez un nouveau bloc, parcourez-en douze nouveaux, relisez
l’ensemble chaud, ou sélectionnez n’importe quel bloc pour le lire, en cache, fantôme ou oublié. Une horloge avance
d’une seconde avant chaque action, ou de 10 ms si vous le choisissez.
Essayez d’abord un parcours : il traverse MRU, et l’ensemble chaud reste. Lisez ensuite un bloc de la liste MRU
fantôme et regardez arc_pd et la cible de MRU monter. Relisez quelques blocs du parcours pour les
promouvoir, lisez de nouveaux blocs jusqu’à ce que MFU cède un bloc chaud, et relisez celui-ci depuis la liste MFU
fantôme : l’équilibre repart dans l’autre sens. Enfin, à 10 ms, lisez deux fois le même nouveau bloc.
MRU 0 bloc et MFU 6, sur 16 ; MRU fantôme 0 en-tête et MFU fantôme 0. arc_pd 50 % : la cible de MRU est de 8 blocs sur 16.
arc_pd 50 % : la cible de MRU est de 8 blocs sur 16.
MRU fantôme
vide
MRU
vide
MFU
- H6
- H5
- H4
- H3
- H2
- H1
MFU fantôme
vide
Oubliés
vide
Ce que la dernière action a fait
- Rien encore. L’état de départ : l’ensemble chaud H1 à H6 a été lu deux fois, à une seconde d’intervalle.
Version texte de cette figure
arc_c vaut 16 blocs de 128 Kio. Horloge : 2,01 s. Chaque liste de la tête (le plus récent) à la queue (évincé en premier) :
- MRU fantôme, 0 : vide.
- MRU, 0 : vide.
- MFU, 6 : H6, H5, H4, H3, H2, H1.
- MFU fantôme, 0 : vide.
Oubliés (en-têtes détruits), du plus récent au plus ancien : vide.
arc_pd 50 % : la cible de MRU est de 8 blocs sur 16. Succès fantômes jusqu’ici : MRU fantôme 0 Kio, MFU fantôme 0 Kio.
L’idée, à trois profondeurs
En une phrase
L’ARC sépare les blocs vus une fois des blocs vus deux fois, garde les en-têtes de ce qu’il a évincé, et quand une lecture tombe sur l’un de ces fantômes, il donne plus de place à la liste qui aurait gardé le bloc.
En un paragraphe
Un nouveau bloc entre dans MRU. Une deuxième lecture plus d’ARC_MINTIME, environ 62 ms, après la
première le promeut dans MFU ; des relectures dans une même rafale comptent pour un seul usage. Quand le cache
dépasse arc_c, une passe d’éviction prend des blocs à la queue de MRU si MRU dépasse sa part, et à la
queue de MFU sinon. Un bloc évincé perd ses données, mais son en-tête passe dans la liste fantôme correspondante,
qui peut tenir la moitié de ce que tiennent les autres listes. Lire un fantôme est un échec : le bloc est relu sur
le disque, passe dans MFU, et sa taille compte comme un succès fantôme. Il n’y a plus d’arc_p dans
OpenZFS 2.4.4. Les parts sont des fractions en virgule fixe, arc_pd pour la part de MRU dans les
données, arc_pm pour les métadonnées et arc_meta entre les deux, et la passe d’éviction
suivante les déplace selon les succès fantômes qu’elle trouve : vers le haut pour des succès dans la liste MRU
fantôme, vers le bas pour des succès dans la liste MFU fantôme, d’environ 1/32 au plus à chaque fois.
En détail
États et en-têtes. Un bloc dans l’ARC a un en-tête, arc_buf_hdr_t, indexé par
le DVA et le txg de naissance du bloc, et l’en-tête est dans l’un de sept
états : anon, MRU, MRU fantôme, MFU, MFU fantôme, L2C seul et non caché. La figure en dessine quatre, en
listes. L’en-tête note son état, l’heure de son dernier accès, b_arc_access, et
des compteurs de succès. Un nouveau bloc passe d’anon à MRU.
Vu une fois, vu deux fois. Un succès sur un bloc de MRU :
arc_access
Extrait de module/zfs/arc.c, lignes
5398–5428, OpenZFS zfs-2.4.4.
Voir sur GitHub
} else if (hdr->b_l1hdr.b_state == arc_mru) {
/*
* This buffer has been accessed once recently and either
* its read is still in progress or it is in the cache.
*/
if (HDR_IO_IN_PROGRESS(hdr)) {
hdr->b_l1hdr.b_arc_access = now;
return;
}
hdr->b_l1hdr.b_mru_hits++;
ARCSTAT_BUMP(arcstat_mru_hits);
/*
* If the previous access was a prefetch, then it already
* handled possible promotion, so nothing more to do for now.
*/
if (was_prefetch) {
hdr->b_l1hdr.b_arc_access = now;
return;
}
/*
* If more than ARC_MINTIME have passed from the previous
* hit, promote the buffer to the MFU state.
*/
if (ddi_time_after(now, hdr->b_l1hdr.b_arc_access +
ARC_MINTIME)) {
hdr->b_l1hdr.b_arc_access = now;
DTRACE_PROBE1(new_state__mfu, arc_buf_hdr_t *, hdr);
arc_change_state(arc_mfu, hdr);
}- L5403–L5406 Un succès sur un bloc dont la lecture est encore en cours ne fait que rafraîchir son heure. Dans la figure, chaque lecture se termine aussitôt.
- L5407–L5408 Le succès est compté sur l’en-tête.
- L5414–L5417 Un bloc lu d’abord par préchargement n’est pas promu par la lecture à la demande qui suit. La figure n’a pas de préchargement.
-
L5423–L5428
Plus d’
ARC_MINTIMEaprèsb_arc_access: vers MFU, et l’heure est rafraîchie. Sinon rien ne bouge, pas mêmeb_arc_access, si bien qu’une série de relectures à moins de 62 ms d’intervalle se mesure depuis la première. Le test est strict : 62 ms tout juste ne promeut pas.
ARC_MINTIME vaut hz >> 4, « 62 ms ». Un succès sur un
bloc de MFU reste dans MFU et rafraîchit b_arc_access.
Tête et queue. arc_read appelle arc_access sur un
succès, et sur un échec avant d’allouer les données. Tant qu’un lecteur tient un bloc,
son en-tête est hors de sa liste ; la dernière libération l’y remet,
en tête, et l’éviction prend à la queue. Chaque liste est donc dans
l’ordre du dernier usage. Dans la figure, le lecteur lâche chaque bloc aussitôt.
L’éviction garde l’en-tête. Quand un bloc est évincé de MRU ou de MFU :
arc_evict_hdr
Extrait de module/zfs/arc.c, lignes
3876–3891, OpenZFS zfs-2.4.4.
Voir sur GitHub
if (hdr->b_l1hdr.b_pabd != NULL)
arc_hdr_free_abd(hdr, B_FALSE);
if (HDR_HAS_RABD(hdr))
arc_hdr_free_abd(hdr, B_TRUE);
arc_change_state(evicted_state, hdr);
DTRACE_PROBE1(arc__evict, arc_buf_hdr_t *, hdr);
if (evicted_state == arc_anon) {
arc_hdr_destroy(hdr);
*real_evicted += HDR_FULL_SIZE;
} else {
ASSERT(HDR_IN_HASH_TABLE(hdr));
}
return (bytes_evicted);- L3876–L3880 Les données sont libérées.
- L3882 L’en-tête passe dans l’état fantôme choisi plus haut : MRU vers MRU fantôme, MFU vers MFU fantôme. Dans la figure, il glisse en tête de la liste fantôme, sans données.
- L3884–L3889 Seul l’en-tête d’un tampon non caché est détruit. Un en-tête fantôme reste dans la table de hachage : une lecture ultérieure du même bloc le retrouve.
Quelle liste cède un bloc. La passe d’éviction, après ses étapes de métadonnées :
arc_evict
Extrait de module/zfs/arc.c, lignes
4527–4556, OpenZFS zfs-2.4.4.
Voir sur GitHub
/* Evict MRU metadata. */
w = wt * (int64_t)(arc_meta * arc_pm >> 48) >> 16;
e = MIN((int64_t)(asize - ac), (int64_t)(mrum - w));
bytes = arc_evict_impl(arc_mru, ARC_BUFC_METADATA, e);
total_evicted += bytes;
mrum -= bytes;
asize -= bytes;
/* Evict MFU metadata. */
w = wt * (int64_t)(arc_meta >> 16) >> 16;
e = MIN((int64_t)(asize - ac), (int64_t)(m - bytes - w));
bytes = arc_evict_impl(arc_mfu, ARC_BUFC_METADATA, e);
total_evicted += bytes;
mfum -= bytes;
asize -= bytes;
/* Evict MRU data. */
wt -= m - total_evicted;
w = wt * (int64_t)(arc_pd >> 16) >> 16;
e = MIN((int64_t)(asize - ac), (int64_t)(mrud - w));
bytes = arc_evict_impl(arc_mru, ARC_BUFC_DATA, e);
total_evicted += bytes;
mrud -= bytes;
asize -= bytes;
/* Evict MFU data. */
e = asize - ac;
bytes = arc_evict_impl(arc_mfu, ARC_BUFC_DATA, e);
mfud -= bytes;
total_evicted += bytes;- L4527–L4541 Les métadonnées d’abord : celles de MRU jusqu’à leur cible, puis celles de MFU jusqu’à la part des métadonnées. La figure ne tient que des blocs de données : ces étapes n’évincent rien.
-
L4543–L4550
Les données de MRU, jusqu’à leur cible
w = wt · arc_pd, et pas plus que le dépassement d’arc_c(asize - ac). Un MRU à sa cible ou en dessous ne cède rien. - L4552–L4556 Les données de MFU cèdent ce qui reste du dépassement.
wt est ce qui peut être évincé, arc_c lui-même quand tout bloc du
cache est évinçable, comme dans la figure. Chaque étape évince au plus ce que tient la liste,
et rien pour une quantité négative, depuis la queue, jusqu’à avoir évincé au moins ce qui est demandé. MRU
n’est donc pas ramené à sa cible d’un coup : il ne cède sa queue que tant que le cache dépasse arc_c.
Le premier parcours de la figure le montre : MRU garde 10 blocs pour une cible de 8, parce que chaque nouveau bloc
ne fait dépasser le cache que d’un bloc.
Combien de temps durent les fantômes. À la fin de la même passe :
arc_evict
Extrait de module/zfs/arc.c, lignes
4558–4587, OpenZFS zfs-2.4.4.
Voir sur GitHub
/*
* Evict ghost lists
*
* Size of each state's ghost list represents how much that state
* may grow by shrinking the other states. Would it need to shrink
* other states to zero (that is unlikely), its ghost size would be
* equal to sum of other three state sizes. But excessive ghost
* size may result in false ghost hits (too far back), that may
* never result in real cache hits if several states are competing.
* So choose some arbitraty point of 1/2 of other state sizes.
*/
gsrd = (mrum + mfud + mfum) / 2;
e = zfs_refcount_count(&arc_mru_ghost->arcs_size[ARC_BUFC_DATA]) -
gsrd;
(void) arc_evict_impl(arc_mru_ghost, ARC_BUFC_DATA, e);
gsrm = (mrud + mfud + mfum) / 2;
e = zfs_refcount_count(&arc_mru_ghost->arcs_size[ARC_BUFC_METADATA]) -
gsrm;
(void) arc_evict_impl(arc_mru_ghost, ARC_BUFC_METADATA, e);
gsfd = (mrud + mrum + mfum) / 2;
e = zfs_refcount_count(&arc_mfu_ghost->arcs_size[ARC_BUFC_DATA]) -
gsfd;
(void) arc_evict_impl(arc_mfu_ghost, ARC_BUFC_DATA, e);
gsfm = (mrud + mrum + mfud) / 2;
e = zfs_refcount_count(&arc_mfu_ghost->arcs_size[ARC_BUFC_METADATA]) -
gsfm;
(void) arc_evict_impl(arc_mfu_ghost, ARC_BUFC_METADATA, e);- L4569–L4572 La liste fantôme des données de MRU peut tenir la moitié des trois autres listes vivantes. La figure n’a pas de listes de métadonnées : c’est la moitié de MFU.
- L4579–L4582 La liste fantôme des données de MFU : la moitié de MRU dans la figure.
Une liste fantôme n’a pas la taille du cache. Dans la figure, avec un MFU vide, la liste MRU fantôme ne tient rien. Un en-tête retiré d’une liste fantôme est détruit ; la figure le range parmi les oubliés, et relire le bloc, c’est un nouveau bloc pour l’ARC.
Un succès fantôme. Lire un bloc dont l’en-tête est dans la liste MRU fantôme :
arc_access
Extrait de module/zfs/arc.c, lignes
5429–5449, OpenZFS zfs-2.4.4.
Voir sur GitHub
} else if (hdr->b_l1hdr.b_state == arc_mru_ghost) {
arc_state_t *new_state;
/*
* This buffer has been accessed once recently, but was
* evicted from the cache. Would we have bigger MRU, it
* would be an MRU hit, so handle it the same way, except
* we don't need to check the previous access time.
*/
hdr->b_l1hdr.b_mru_ghost_hits++;
ARCSTAT_BUMP(arcstat_mru_ghost_hits);
hdr->b_l1hdr.b_arc_access = now;
wmsum_add(&arc_mru_ghost->arcs_hits[arc_buf_type(hdr)],
arc_hdr_size(hdr));
if (was_prefetch) {
new_state = arc_mru;
DTRACE_PROBE1(new_state__mru, arc_buf_hdr_t *, hdr);
} else {
new_state = arc_mfu;
DTRACE_PROBE1(new_state__mfu, arc_buf_hdr_t *, hdr);
}
arc_change_state(new_state, hdr);- L5432–L5435 « Would we have bigger MRU, it would be an MRU hit » : un succès fantôme est un échec qu’une liste plus grande aurait évité. Les données sont relues sur le disque.
- L5437–L5441 Compté sur l’en-tête, et la taille du bloc (sa taille compressée s’il est compressé) ajoutée au compteur de succès de la liste MRU fantôme. La figure montre ce compteur sous chaque liste fantôme.
- L5442–L5449 Retour dans le cache, dans MFU, puisque c’est son deuxième usage ; dans MRU si le bloc venait d’un préchargement.
Un succès dans MFU fantôme fait de même avec le compteur de la liste MFU fantôme et
revient dans MFU. arc_access ne change aucune cible : il ne fait qu’additionner des octets. La passe
d’éviction les lit.
Des succès fantômes à un équilibre. Au début de chaque passe :
arc_evict
Extrait de module/zfs/arc.c, lignes
4479–4497, OpenZFS zfs-2.4.4.
Voir sur GitHub
/* Get ARC ghost hits since last eviction. */
ngrd = wmsum_value(&arc_mru_ghost->arcs_hits[ARC_BUFC_DATA]);
uint64_t grd = ngrd - ogrd;
ogrd = ngrd;
ngrm = wmsum_value(&arc_mru_ghost->arcs_hits[ARC_BUFC_METADATA]);
uint64_t grm = ngrm - ogrm;
ogrm = ngrm;
ngfd = wmsum_value(&arc_mfu_ghost->arcs_hits[ARC_BUFC_DATA]);
uint64_t gfd = ngfd - ogfd;
ogfd = ngfd;
ngfm = wmsum_value(&arc_mfu_ghost->arcs_hits[ARC_BUFC_METADATA]);
uint64_t gfm = ngfm - ogfm;
ogfm = ngfm;
/* Adjust ARC states balance based on ghost hits. */
arc_meta = arc_evict_adj(arc_meta, gsrd + gsrm + gsfd + gsfm,
grm + gfm, grd + gfd, zfs_arc_meta_balance);
arc_pd = arc_evict_adj(arc_pd, gsrd + gsfd, grd, gfd, 100);
arc_pm = arc_evict_adj(arc_pm, gsrm + gsfm, grm, gfm, 100);- L4480–L4491 Les octets de succès fantômes depuis la passe précédente, en quatre : MRU fantôme et MFU fantôme, données et métadonnées. Les valeurs précédentes sont gardées d’une passe à l’autre.
-
L4494–L4495
arc_meta, la part des métadonnées : vers le haut avec les succès fantômes de métadonnées, vers le bas avec ceux de données, le pas vers le bas divisé parzfs_arc_meta_balance / 100, 5 par défaut. -
L4496
arc_pd, la part de MRU dans les données : vers le haut avec les succès fantômes de données dans MRU, vers le bas avec ceux dans MFU. La jauge de la figure. -
L4497
arc_pm, de même pour les métadonnées.
Ces trois-là remplacent arc_p. Ce sont des fractions de 232, qui
partent de 1/4 pour les métadonnées et de 1/2 pour chaque part de MRU. Le premier argument
après la fraction est la taille cible des fantômes à la passe précédente : un succès déplace donc une fraction
d’environ sa taille rapportée à celle des listes fantômes.
arc_evict_adj
Extrait de module/zfs/arc.c, lignes
4405–4442, OpenZFS zfs-2.4.4.
Voir sur GitHub
static uint64_t
arc_evict_adj(uint64_t frac, uint64_t total, uint64_t up, uint64_t down,
uint_t balance)
{
if (total < 32 || up + down == 0)
return (frac);
/*
* We should not have more ghost hits than ghost size, but they may
* get close. To avoid overflows below up/down should not be bigger
* than 1/5 of total. But to limit maximum adjustment speed restrict
* it some more.
*/
if (up + down >= total / 16) {
uint64_t scale = (up + down) / (total / 32);
up /= scale;
down /= scale;
}
/* Get maximal dynamic range by choosing optimal shifts. */
int s = highbit64(total);
s = MIN(64 - s, 32);
ASSERT3U(frac, <=, 1ULL << 32);
uint64_t ofrac = (1ULL << 32) - frac;
if (frac >= 4 * ofrac)
up /= frac / (2 * ofrac + 1);
up = (up << s) / (total >> (32 - s));
if (ofrac >= 4 * frac)
down /= ofrac / (2 * frac + 1);
down = (down << s) / (total >> (32 - s));
down = down * 100 / balance;
ASSERT3U(up, <=, (1ULL << 32) - frac);
ASSERT3U(down, <=, frac);
return (frac + up - down);
}- L4409–L4410 Aucun changement tant qu’il n’y a pas de cible fantôme (la première passe de la figure), ni sans succès.
- L4417–L4421 Des succès d’au moins 1/16 du total fantôme sont ramenés à environ 1/32 de celui-ci : une passe déplace une fraction d’environ 1/32 de son étendue au plus. Dans la figure, un seul succès fantôme suffit à atteindre cette limite.
- L4424–L4426 Un décalage choisi pour que la division garde le plus de bits possible.
- L4428–L4435 Près de 0 ou de 1 (un côté quatre fois l’autre), le pas vers cette extrémité est encore divisé : une fraction approche ses bornes de plus en plus lentement.
-
L4436
Le pas vers le bas divisé par
balance / 100: 1 pourarc_pd.
Le premier succès fantôme de la figure, pas à pas : arc_pd = 231, la moitié ; des cibles
fantômes de 8 blocs, total = 220 octets ; un succès de 128 Kio, 217. C’est plus
de 1/16 du total, il est donc divisé par 4, à 215 ; le décalage vaut 32 ; up =
247 / 220 = 227, et arc_pd devient 231 + 227,
53,125 %. La cible de MRU passe de 8 à 8,5 blocs. Un succès dans la liste MFU fantôme retire ces mêmes
227.
Quand une passe s’exécute. Pas sur un succès : un succès n’alloue rien. Allouer les données d’un
échec peut réveiller le fil d’éviction, une fois que le cache dépasse arc_c de
plus de zfs_max_recordsize, 16 Mio sur les systèmes
64 bits. Le fil exécute arc_evict et continue tant
qu’il évince quelque chose et que le cache dépasse encore arc_c.
arc_c lui-même relève d’un autre réglage. Il grandit de la taille de
chaque allocation, au moins 128 Kio, tant que le cache en est à moins de 32 Mio (deux
fois la plus grande taille de bloc), jusqu’à arc_c_max, et rétrécit
sous la pression mémoire. Les succès fantômes déplacent les parts d’arc_c,
jamais arc_c. La figure le garde à son maximum.
Coda : le L2ARC
Un périphérique de cache peut prolonger l’ARC : selon son commentaire de conception, « a cache layer in-between main memory and disk », sur des disques SSD par exemple. Le même commentaire dit comment on l’alimente :
Level 2 ARC
Extrait de module/zfs/arc.c, lignes
8261–8274, OpenZFS zfs-2.4.4.
Voir sur GitHub
* 1. There is no eviction path from the ARC to the L2ARC. Evictions from
* the ARC behave as usual, freeing buffers and placing headers on ghost
* lists. The ARC does not send buffers to the L2ARC during eviction as
* this would add inflated write latencies for all ARC memory pressure.
*
* 2. The L2ARC attempts to cache data from the ARC before it is evicted.
* It does this by periodically scanning buffers from the eviction-end of
* the MFU and MRU ARC lists, copying them to the L2ARC devices if they are
* not already there. It scans until a headroom of buffers is satisfied,
* which itself is a buffer for ARC eviction. If a compressible buffer is
* found during scanning and selected for writing to an L2ARC device, we
* temporarily boost scanning headroom during the next scan cycle to make
* sure we adapt to compression effects (which might significantly reduce
* the data volume we write to L2ARC). The thread that does this is- L8261–L8264 L’éviction n’écrit pas sur le périphérique de cache : elle libère les données et laisse les en-têtes sur les listes fantômes, comme plus haut.
- L8266–L8270 Un fil d’alimentation copie des blocs depuis le côté éviction de MRU et de MFU avant qu’ils ne soient évincés.
Ce fil, l2arc_feed_thread, se réveille environ une fois par
seconde et lit les listes depuis leur queue. Un en-tête fantôme dont le bloc est sur le
périphérique de cache n’est pas détruit quand il est retiré : il reste, en L2C seul, et une
lecture de ce bloc le ramène dans MRU.
Ce que la figure laisse de côté
La figure exécute les fonctions ci-dessus sur des blocs de données seulement, tous de 128 Kio et non compressés : les
listes de métadonnées sont vides, les termes de métadonnées de la passe valent donc zéro, et arc_meta et
arc_pm, qui ne changent alors aucune cible de données, ne sont pas dessinés. Chaque état est une seule
liste ; le code répartit les en-têtes en sous-listes selon un hachage et les évince
à tour de rôle, si bien que son ordre est proche de celui du dernier usage, sans lui
être identique. Une passe s’exécute juste après chaque échec qui fait dépasser arc_c, plutôt que dans un
fil réveillé 16 Mio plus tard, ce qui à l’échelle de la figure ferait huit caches. arc_c est fixe,
l’horloge est logique, et chaque bloc est libéré dès qu’il est lu. Pas de préchargement (les blocs préchargés ont aussi
une durée de vie minimale), pas de tampons non cachés ni sales,
pas de copies compressées ou chiffrées, pas de L2ARC.