Yac conserve ses données dans deux réservoirs de mémoire partagée indépendants, configurés par yac.keys_memory_size et yac.values_memory_size. Ils se remplissent et se libèrent différemment, il est donc utile de savoir à quel réservoir se rattache un symptôme avant de toucher à l'un ou l'autre réglage.
Les deux réservoirs stockent chacun une moitié d'une entrée. Le réservoir de clés contient les clés : un slot par clé mise en cache, portant la clé elle-même (jusqu'à 48 octets) accompagnée de son hachage, de son TTL, de son nombre de succès et de sa date de dernier accès ; la valeur n'est référencée que par un pointeur vers le réservoir de valeurs. Le réservoir de valeurs contient les valeurs : chaque valeur stockée occupe un bloc d'octets sérialisés — sa forme compressée lorsque l'entrée est passée par yac.compress_threshold.
La seule exception est celle des valeurs intégrées : les petits scalaires
— null, true, false, la plupart des entiers (ceux qui tiennent sur
60 bits signés dans une compilation 64 bits), les chaînes jusqu'à 7 octets
et les tableaux vides — sont stockés directement dans le slot, dans le
pointeur qui référencerait autrement un bloc. De telles valeurs
n'occupent aucune place dans le réservoir de valeurs ; seule leur clé en
occupe.
Pour un dimensionnement approximatif :
le réservoir de clés contient environ 8 000 clés par Mo ; dimensionner
yac.keys_memory_size
comme le nombre de clés distinctes divisé par 8 000 par Mo, arrondi au
supérieur — la valeur par défaut de 8M contient
environ 64 000 clés ;
le réservoir de valeurs doit contenir toute valeur susceptible d'être encore lue ; dimensionner yac.values_memory_size comme le nombre de valeurs vivantes multiplié par leur taille sérialisée moyenne (après compression), et prévoir environ le double : le réservoir est un anneau, et une valeur ne meurt qu'une fois que le curseur d'allocation revient l'écraser.
yac.keys_memory_size
contient une table de slots de taille fixe — la valeur par défaut de
8M donne environ 65 536 slots. Chaque clé stockée
occupe exactement un slot ; ce réservoir plafonne donc le nombre
d'entrées pouvant exister simultanément. Contrairement au réservoir de
valeurs, les slots ne sont jamais libérés individuellement. Un slot
expiré — au delà de son TTL, ou la trace laissée par
Yac::delete() — est recyclé gratuitement lorsqu'une
nouvelle clé en a besoin. Ce n'est que lorsque les quatre slots candidats
d'un chemin de sondage contiennent des entrées vivantes que l'un d'eux est
expulsé pour faire de la place : un kick (le compteur
kicks de Yac::info()).
L'expulsion choisit uniquement parmi les quatre candidats vivants du chemin de sondage en collision :
le moins récemment utilisé (l'atime le plus ancien)
est expulsé ;
en cas d'égalité, l'entrée la moins souvent lue, puis la première position de sondage.
Une confusion fréquente : slots_used atteignant
slots_size n'est pas une
condition d'erreur. Un cache dont l'ensemble de clés actif est plus grand
que la table de slots tourne simplement à 100 % d'occupation à
partir de là, expulsant et réinsérant selon les besoins. La seule chose
qui dise si le réservoir de clés est correctement dimensionné est le taux
de succès (hits / (hits + miss), calculé sur les
écarts entre deux instantanés de Yac::info()
plutôt que sur la moyenne depuis le démarrage). Un compteur
kicks élevé ne signifie en lui-même pas qu'il y a un
problème : la distribution des clés n'est simplement pas uniforme et
certains chemins de sondage entrent plus souvent en collision. Ce n'est
que lorsque le taux de succès et
kicks sont tous deux mauvais que la table est trop
petite pour l'ensemble de clés, et le remède est alors un
yac.keys_memory_size
plus grand.
Seconde conséquence du fait que les slots ne sont jamais libérés : les
entrées sans TTL (ttl = 0) qui ne sont plus jamais
lues continuent d'occuper un slot jusqu'à ce qu'une expulsion les
désigne. Si une application stocke de grandes quantités de telles données
à usage unique, leur donner un TTL pour qu'elles expirent et puissent
être recyclées sans déloger des entrées vivantes, ou dimensionner le
réservoir de clés pour l'ensemble de clés complet.
yac.values_memory_size
est découpé en segments de 4M chacun, gérés comme des anneaux : les
écritures avancent un curseur par segment et la place n'est jamais
libérée entrée par entrée. Lorsqu'une allocation ne tient plus, le
curseur repart au début d'un segment : un recycle (le
compteur recycles de
Yac::info()). Un recyclage n'invalide pas le
segment d'un coup : les valeurs écrasées restent lisibles jusqu'à ce que
le curseur revenu au début les écrase effectivement, moment à partir
duquel leurs lectures échouent au contrôle d'intégrité et se
transforment en défauts de cache.
Deux tailles comptent pour ce réservoir : le total de
yac.values_memory_size
doit contenir l'ensemble actif des valeurs vivantes, et une entrée seule
ne peut pas dépasser 1 Mo une fois stockée
(YAC_MAX_RAW_COMPRESSED_LEN). Les valeurs plus
grandes sont donc toujours compressées avant d'être stockées ; une valeur
qui ne peut pas descendre en dessous de 1 Mo — le plus souvent parce
qu'il s'agit de données aléatoires — est rejetée et incrémente le
compteur fails. La limite absolue sur la valeur
elle-même est bien plus haute : les valeurs sérialisées au delà de 64 Mo
(YAC_MAX_VALUE_RAW_LEN, soit
(1 << 26) - 1 octets) sont rejetées d'emblée.
Commencer avec les valeurs par défaut et surveiller les compteurs de
Yac::info() : ils s'accumulent depuis
start_time, il faut donc comparer deux instantanés
pris à quelque temps d'intervalle :
taux de succès sain (disons >= 90 %) : le cache va bien, il n'y a rien à faire, quels que soient les autres compteurs ;
taux de succès faible et kicks en hausse : le
réservoir de clés est trop petit pour l'ensemble de clés — des entrées
vivantes sont expulsées avant d'avoir été relues. Augmenter
yac.keys_memory_size ;
recycles fréquents : c'est un vrai problème, pas un
compteur anodin. Un recyclage signifie que l'allocateur de valeurs est
revenu au début et s'apprête à écraser des entrées — tout ce qui est
écrasé meurt avant d'avoir pu être relu, les octets dépensés à le
stocker sont donc perdus et le taux de succès en souffre. Le réservoir
de valeurs est trop petit pour le volume de données vivantes. Par ordre
d'impact :
donner un TTL aux entrées. Les valeurs écrites avec
ttl = 0 restent vivantes indéfiniment, elles
continuent donc d'occuper le réservoir et forcent le curseur à
reboucler plus tôt. Un TTL borne la durée de vie de chaque entrée et
réduit l'ensemble actif que le réservoir doit contenir ;
augmenter yac.values_memory_size pour que le réservoir contienne tout l'ensemble des valeurs vivantes (penser à prévoir environ le double de l'empreinte vivante : une valeur ne meurt qu'une fois que le curseur revient l'écraser) ;
stocker moins par entrée : baisser
yac.compress_threshold
s'il est réglé au dessus du minimum de 1024, afin
que les grandes charges utiles soient compressées, et élaguer les
valeurs qui n'ont pas besoin d'être mises en cache en entier ;
fails en hausse : des valeurs qui n'ont pas pu être
stockées, le plus souvent une valeur seule dépassant la limite de 1 Mo
par entrée stockée même après compression — découper la valeur.