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.
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.
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.
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.
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.