PHP Conference Ehime 2026

Gestión de la memoria

Yac guarda sus datos en dos reservas de memoria compartida independientes, configuradas con yac.keys_memory_size y yac.values_memory_size. Se llenan y se liberan de maneras distintas, por lo que conviene saber a qué reserva pertenece un síntoma antes de tocar cualquiera de los dos ajustes.

Qué contiene cada reserva

Las dos reservas almacenan mitades distintas de una entrada. La reserva de claves contiene las claves: una ranura por clave en caché, que lleva la propia clave (hasta 48 bytes) junto con su hash, su TTL, su número de aciertos y su fecha de último acceso; el valor solo se referencia mediante un puntero a la reserva de valores. La reserva de valores contiene los valores: cada valor almacenado ocupa un bloque de bytes serializados, en su forma comprimida cuando la entrada ha pasado por yac.compress_threshold.

La única excepción son los valores incrustados: los escalares pequeños (null, true, false, la mayoría de los enteros —los que caben en 60 bits con signo en compilaciones de 64 bits—, las cadenas de hasta 7 bytes y los arrays vacíos) se almacenan directamente dentro de la ranura, en el puntero que de otro modo referenciaría un bloque. Esos valores no ocupan espacio alguno en la reserva de valores; solo lo hace su clave.

Como guía aproximada de dimensionamiento:

  • la reserva de claves contiene unas 8 000 claves por MB, así que hay que dimensionar yac.keys_memory_size como el número de claves distintas dividido entre 8 000 por MB, redondeando al entero superior: el valor predeterminado de 8M contiene unas 64 000 claves;

  • la reserva de valores debe contener todos los valores que todavía puedan leerse, así que hay que dimensionar yac.values_memory_size como el número de valores vivos multiplicado por su tamaño serializado medio (después de la compresión), y prever aproximadamente el doble: la reserva es un anillo, y un valor no muere hasta que el cursor del asignador vuelve a pasar por encima de él.

Los valores incrustados ocupan una ranura como cualquier otra entrada, pero no consumen espacio en la reserva de valores, así que hay que dejarlos fuera del segundo cálculo.

La reserva de claves (ranuras)

yac.keys_memory_size contiene una tabla de ranuras de tamaño fijo: el valor predeterminado de 8M proporciona unas 65 536 ranuras. Cada clave almacenada ocupa exactamente una ranura, de modo que esta reserva limita el número de entradas que pueden existir a la vez; a diferencia de la reserva de valores, las ranuras nunca se liberan individualmente. Una ranura caducada —más allá de su TTL, o el marcador de borrado que deja Yac::delete()— se recicla sin coste cuando una clave nueva la necesita. Solo cuando las cuatro ranuras candidatas de un camino de sondeo contienen entradas vivas se expulsa una de ellas para hacer sitio: un kick (el contador kicks de Yac::info()).

La expulsión elige únicamente entre las cuatro candidatas vivas del camino de sondeo en colisión:

  • se expulsa la usada menos recientemente (el atime más antiguo);

  • en caso de empate, la entrada con menos aciertos y, después, la primera posición de sondeo.

Un punto de confusión frecuente: que slots_used alcance slots_size no es una condición de error. Una caché cuyo conjunto de trabajo de claves sea mayor que la tabla de ranuras simplemente funciona al 100 % de ocupación a partir de ese momento, expulsando y reinsertando según haga falta. Lo único que indica si la reserva de claves está bien dimensionada es la tasa de aciertos (hits / (hits + miss), calculada sobre las diferencias entre dos instantáneas de Yac::info() y no sobre la media desde el arranque). Un contador kicks alto no significa por sí mismo que haya un problema: sencillamente la distribución de las claves no es uniforme y algunos caminos de sondeo colisionan más que otros. Solo cuando la tasa de aciertos y kicks son ambos malos la tabla es demasiado pequeña para el conjunto de claves, y el remedio es un yac.keys_memory_size mayor.

Una segunda consecuencia de que las ranuras nunca se liberen: las entradas sin TTL (ttl = 0) que ya no vuelven a leerse siguen ocupando una ranura hasta que una expulsión da la casualidad de elegirlas. Si una aplicación almacena grandes cantidades de datos de un solo uso, conviene darles un TTL para que caduquen y puedan reciclarse sin desplazar entradas vivas, o dimensionar la reserva de claves para el conjunto completo de claves.

La reserva de valores (segmentos)

yac.values_memory_size se divide en segmentos de 4M cada uno, gestionados como anillos: las escrituras hacen avanzar un cursor por segmento y el espacio nunca se libera por entrada. Cuando una asignación ya no cabe, el cursor vuelve al principio de un segmento: un recycle (el contador recycles de Yac::info()). Un reciclado no invalida el segmento de inmediato: los valores sobrescritos siguen siendo legibles hasta que el cursor que ha dado la vuelta los sobrescribe realmente, momento en el que sus lecturas no superan el control de integridad y se convierten en fallos de caché.

Para esta reserva importan dos tamaños: el total de yac.values_memory_size debe contener el conjunto de trabajo de los valores vivos, y una única entrada puede ocupar como mucho 1 MB una vez almacenada (YAC_MAX_RAW_COMPRESSED_LEN). Por eso los valores mayores se comprimen siempre antes de almacenarse; un valor que no puede reducirse por debajo de 1 MB —lo más habitual, porque son datos aleatorios— es rechazado e incrementa el contador fails. El límite absoluto sobre el valor en sí es mucho más alto: los valores serializados por encima de 64 MB (YAC_MAX_VALUE_RAW_LEN, es decir (1 << 26) - 1 bytes) se rechazan de plano.

Dimensionamiento y qué vigilar

Conviene empezar con los valores predeterminados y vigilar los contadores de Yac::info(): se acumulan desde start_time, así que hay que comparar dos instantáneas tomadas con cierta separación en el tiempo:

  • tasa de aciertos saludable (por ejemplo >= 90 %): la caché está bien; no hay nada que hacer, sea lo que sea que muestren los demás contadores;

  • tasa de aciertos baja y kicks en aumento: la reserva de claves es demasiado pequeña para el conjunto de claves, las entradas vivas se expulsan antes de volver a leerse. Hay que aumentar yac.keys_memory_size;

  • recycles frecuentes: esto sí es un problema real, no un contador inofensivo. Un reciclado significa que el asignador de valores ha dado la vuelta y está a punto de sobrescribir entradas: todo lo sobrescrito muere antes de poder volver a leerse, de modo que los bytes gastados en almacenarlo se han desperdiciado y la tasa de aciertos se resiente. La reserva de valores es demasiado pequeña para el volumen de datos vivos. Por orden de impacto:

    • hay que dar un TTL a las entradas. Los valores escritos con ttl = 0 siguen vivos para siempre, de modo que ocupan la reserva y obligan al cursor a dar la vuelta antes. Un TTL limita cuánto puede vivir cada entrada y reduce el conjunto de trabajo vivo que la reserva debe contener;

    • hay que aumentar yac.values_memory_size para que la reserva contenga todo el conjunto de valores vivos (hay que recordar prever aproximadamente el doble de la huella viva: un valor no muere hasta que el cursor vuelve a pasar por encima de él);

    • hay que almacenar menos por entrada y bajar yac.compress_threshold si está por encima del mínimo de 1024, para que las cargas útiles grandes se compriman, y recortar los valores que no necesitan almacenarse por completo;

  • fails en aumento: valores que no han podido almacenarse, casi siempre un único valor mayor que el límite de 1 MB de tamaño almacenado incluso después de comprimirlo; hay que dividir el valor.

add a note

User Contributed Notes

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