PHP Conference Ehime 2026

Gestion de la mémoire

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.

Ce que contient chaque réservoir

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.

Les valeurs intégrées occupent un slot comme toute autre entrée, mais ne consomment aucune place dans le réservoir de valeurs : les exclure du second calcul.

Le réservoir de clés (slots)

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.

Le réservoir de valeurs (segments)

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.

Dimensionner et surveiller

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.

add a note

User Contributed Notes

There are no user contributed notes for this page.
To Top