Redis: encogimiento, expansión y refrito progresivo

Tabla de contenido

1. Reducir y expandir

2. Refrito progresivo


1. Reducir y expandir

Con la ejecución continua de operaciones de redis, los valores clave almacenados en la tabla hash aumentarán o disminuirán gradualmente. Para mantener el factor de carga (relación) de la tabla hash dentro de un rango razonable, cuando la clave almacenada en el hash table Cuando el número de pares de valores es demasiado o muy pocos, el programa necesita expandir o reducir el tamaño de la tabla hash en consecuencia.

ratio = ht [0] .used / ht [0] .tamaño

Por ejemplo, el tamaño de la tabla hash es 4, si se han insertado 4 kvs, la relación es 1.

El factor de carga predeterminado de redis es 1, y el factor de carga máximo puede llegar a 5 (cuando persiste, se requiere la operación de la bifurcación y no se asignará memoria en este momento, por lo que hay un juicio en el código fuente de redis, si es mayor que 5 veces la longitud de los datos (5 * usados), luego expanda inmediatamente).

El trabajo de expandir y reducir la tabla hash se puede realizar realizando una operación de repetición (re-hash). La estrategia de Redis para repetir la tabla hash del diccionario es la siguiente:

1. Si la proporción es inferior a 0,1, la tabla hash se reducirá

/* Expand or create the hash table,
 * when malloc_failed is non-NULL, it'll avoid panic if malloc fails (in which case it'll be set to 1).
 * Returns DICT_OK if expand was performed, and DICT_ERR if skipped. */
int _dictExpand(dict *d, unsigned long size, int* malloc_failed)
{
    if (malloc_failed) *malloc_failed = 0;

    /* the size is invalid if it is smaller than the number of
     * elements already inside the hash table */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    dictht n; /* the new hash table */
    unsigned long realsize = _dictNextPower(size);

    /* Rehashing to the same table size is not useful. */
    if (realsize == d->ht[0].size) return DICT_ERR;

    /* Allocate the new hash table and initialize all pointers to NULL */
    n.size = realsize;
    n.sizemask = realsize-1;
    if (malloc_failed) {
        n.table = ztrycalloc(realsize*sizeof(dictEntry*));
        *malloc_failed = n.table == NULL;
        if (*malloc_failed)
            return DICT_ERR;
    } else
        n.table = zcalloc(realsize*sizeof(dictEntry*));

    n.used = 0;

    /* Is this the first initialization? If so it's not really a rehashing
     * we just set the first hash table so that it can accept keys. */
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }

    /* Prepare a second hash table for incremental rehashing */
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

/* return DICT_ERR if expand was not performed */
int dictExpand(dict *d, unsigned long size) {
    return _dictExpand(d, size, NULL);
}

/* Resize the table to the minimal size that contains all the elements,
 * but with the invariant of a USED/BUCKETS ratio near to <= 1 */
int dictResize(dict *d)
{
    unsigned long minimal;

    if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
    minimal = d->ht[0].used;
    if (minimal < DICT_HT_INITIAL_SIZE)
        minimal = DICT_HT_INITIAL_SIZE;
    return dictExpand(d, minimal);
}

int htNeedsResize(dict *dict) {
    long long size, used;

    size = dictSlots(dict);
    used = dictSize(dict);
    return (size > DICT_HT_INITIAL_SIZE &&
            (used*100/size < HASHTABLE_MIN_FILL));
}

/*
当HashTable的使用率为10%的时候,开始缩容,进行rehash操作。
*/
/* If the percentage of used slots in the HT reaches HASHTABLE_MIN_FILL
 * we resize the hash table to save memory */
void tryResizeHashTables(int dbid) {
    if (htNeedsResize(server.db[dbid].dict))
        dictResize(server.db[dbid].dict);
    if (htNeedsResize(server.db[dbid].expires))
        dictResize(server.db[dbid].expires);
}

2. El servidor no está ejecutando actualmente el comando BGSAVE o el comando BGREWRITEAOF, y el factor de carga de la tabla hash es mayor o igual a 1, entonces la tabla hash se expande y el tamaño de expansión es el ht actual [0] .usado * 2

3. El servidor está ejecutando actualmente el comando BGSAVE o el comando BGREWRITEAOF, y el factor de carga de la tabla hash es mayor o igual a 5, entonces la tabla hash se expande y la expansión es el ht [0] actual utilizado. * 2

La declaración anterior está un poco sesgada. De hecho, aunque los parámetros pasados ​​son así, por ejemplo, su ht [0] .used es 5, y el pasado es 10, pero la expansión será 2 ^ 4 = 16 , es decir, la capacidad de expansión real es 2. ^ n.

/* Our hash table capability is a power of two */
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;

    if (size >= LONG_MAX) return LONG_MAX + 1LU;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;   //体现了扩容的大小不是传入的size(也就是ht[0].used*2),而是距离这个size最近的2^n。
    }
}
#define dictIsRehashing(d) ((d)->rehashidx != -1)

/* Because we may need to allocate huge memory chunk at once when dict
 * expands, we will check this allocation is allowed or not if the dict
 * type has expandAllowed member function. */
static int dictTypeExpandAllowed(dict *d) {
    if (d->type->expandAllowed == NULL) return 1;
    return d->type->expandAllowed(
                    _dictNextPower(d->ht[0].used + 1) * sizeof(dictEntry*),
                    (double)d->ht[0].used / d->ht[0].size);
}

/* Expand the hash table if needed */
static int _dictExpandIfNeeded(dict *d)
{
    /* Incremental rehashing already in progress. Return. */
    if (dictIsRehashing(d)) return DICT_OK;

    /* If the hash table is empty expand it to the initial size. */
    if (d->ht[0].size == 0) return dictExpand(d, DICT_HT_INITIAL_SIZE);

    /* If we reached the 1:1 ratio, and we are allowed to resize the hash
     * table (global setting) or we should avoid it but the ratio between
     * elements/buckets is over the "safe" threshold, we resize doubling
     * the number of buckets. */
    if (d->ht[0].used >= d->ht[0].size &&
        (dict_can_resize ||
         d->ht[0].used/d->ht[0].size > dict_force_resize_ratio) &&
        dictTypeExpandAllowed(d))
    {
        return dictExpand(d, d->ht[0].used + 1);
    }
    return DICT_OK;
}

Los pasos de expansión son los siguientes:

1. Asigne un espacio adecuado para la tabla hash del diccionario ht [1];

2. Refrescar todos los pares clave-valor en ht [0] a ht [1]: refrito se refiere a recalcular el valor hash y el valor de índice de la clave, y luego colocar el par clave-valor en la tabla hash de ht [1 ] Ubicación designada

3. Cuando todos los pares clave-valor contenidos en ht [0] se hayan migrado a ht [1] (ht [0] se convierte en una tabla vacía), suelte ht [0] y establezca ht [1] en ht [0] , Y cree una nueva tabla hash vacía en ht [1] para prepararse para el próximo refrito.

 

2. Refrito progresivo

Para expandir o reducir la tabla hash, todos los pares clave-valor en ht [0] deben volver a pasar a ht [1]. Sin embargo, esta acción de refrito no se realiza de manera centralizada y única, sino de forma múltiple y progresiva. formas. Completado.

La razón de esto es que si hay cuatro pares clave-valor almacenados en ht [0], el servidor puede repetir todos estos pares clave-valor a ht [1] en un instante; sin embargo, si los pares clave-valor almacenados en la tabla hash No cuatro, sino cuatro millones, cuarenta millones o incluso 400 millones de pares clave-valor. Si desea repetir todos estos pares clave-valor a ht [1] a la vez, la enorme cantidad de cálculos puede hacer que el servidor pasar un período de tiempo Detener el servicio dentro de.

Por lo tanto, para evitar el impacto del refrito en el rendimiento del servidor, el servidor no repite todos los pares clave-valor en ht [0] a ht [1] a la vez, sino que divide todos los pares clave-valor en ht [ 0] en ht [0] gradualmente. Los pares clave-valor se repiten lentamente a ht [1].

Los siguientes son los pasos detallados del refrito progresivo de hash:

1. Asigne espacio para ht [1] de modo que el diccionario contenga dos tablas hash, ht [0] y ht [1].

2. Mantenga una variable de contador de índice rehashidx en el diccionario y establezca su dedo en 0, lo que indica que el trabajo de repetición ha comenzado oficialmente.

3. Durante el proceso de refrito, cada vez que se agrega, borra, busca o actualiza el diccionario, además de realizar la operación especificada, el programa también agregará todos los pares clave-valor de la tabla hash ht [0] en el rehashidx Rehash to ht [1], cuando se complete el trabajo de refrito, el programa rehashidx valor del atributo 1.

4. Con la ejecución continua de las operaciones del diccionario, finalmente en un momento determinado, todos los pares clave-valor de ht [0] se volverán a convertir en ht [1], en este momento el programa establece el atributo rehashidx en -1, indicando que se ha operado el refrito realizar

La ventaja del refrito progresivo es que adopta un enfoque de divide y vencerás. El trabajo de cálculo requerido para el refrito de pares clave-valor se distribuye por igual a cada operación cruda en el diccionario, e incluso se inicia un temporizador en segundo plano, que solo funciona durante un milisegundo cada ciclo de tiempo, evitando así la enorme cantidad de cálculo causado por el refrito centralizado.

Supongo que te gusta

Origin blog.csdn.net/weixin_40179091/article/details/115273824
Recomendado
Clasificación