Análisis del código fuente de Redis (24) Exploración del mecanismo BIO

Inserte la descripción de la imagen aquíEste trabajo está licenciado bajo el Acuerdo de Licencia Internacional de Atribución-Uso No Comercial-Compartir 4.0 de Creative Commons de la misma manera .
Inserte la descripción de la imagen aquíEste trabajo ( Lizhao Long Bowen por la creación de Li Zhaolong ) por la confirmación de Li Zhaolong , por favor indique los derechos de autor.

Introducción

Después de un lapso de diez meses, una vez más tomé un bolígrafo para explorar cosas relacionadas con Redis y todavía estaba un poco emocionado. Recientemente, planeo usar dos o tres artículos para revisar cosas relacionadas con Redis. No es solo un complemento de los puntos de conocimiento que faltan, sino también un punto final a la revisión de Redis en este período de tiempo.

Este artículo principalmente quiere hablar sobre una pregunta: ¿ Redis es de un solo subproceso o de varios subprocesos ? Simplemente busqué este problema en las principales plataformas y descubrí que al menos el 70% de los artículos no tienen ningún valor, pero también hay muchos artículos buenos. Este artículo se basa en la discusión de los predecesores, junto con mi propio entendimiento, para discutir este tema. Sin embargo, debido a que la versión del código fuente es demasiado baja, algunas partes de la discusión no se pueden adjuntar al código.

Hilo único o hilo múltiple

¿Cuándo deberíamos utilizar subprocesos múltiples? Como se describe en [1], está claro que hay dos razones para usar subprocesos múltiples , a saber, el uso de la eficiencia de múltiples núcleos y la separación de preocupaciones . Por el contrario, la razón por la que no se utilizan subprocesos múltiples es que los beneficios no son tan buenos como las recompensas .

Para descartar la respuesta primero, Redis usa subprocesos múltiples en lugar de subprocesos únicos . Por supuesto, el significado aquí es un poco diferente de la idea predeterminada en nuestro chat habitual. Cuando normalmente hablamos de WebServer de un solo subproceso o de varios subprocesos, en realidad discutimos si hay más o más subprocesos de trabajo, que es el general diseño de hilo del trabajador ¿Es uno o más? Para dar el ejemplo más simple, el one loop per threadmodelo utilizado por muduo es un modelo semisincrónico y semisincrónico muy clásico, que es un modelo multiproceso, en el que un subproceso maneja la conexión y el subproceso restante maneja la solicitud. La razón de esto es que WebServer (desde la perspectiva del análisis de desempeño de RabbitServer ) es un programa computacionalmente intensivo, y necesitamos aplicar mejor el aumento en la potencia de computación que trae el multi-core.

Este no es el caso de Redis. Es fácil ver que las tareas de computación realizadas por el programa del servidor de Redis son realmente muy simples. Los datos se reciben en el ciclo epoll, luego se analiza el comando y finalmente se ejecuta. Debido a que el diseño de la estructura de datos en Redis es muy inteligente, básicamente operar estos datos no lleva mucho tiempo. Puede ver el siguiente texto en [3]:

  • No es muy frecuente que la CPU se convierta en su cuello de botella con Redis, ya que normalmente Redis está vinculado a la memoria o la red . Por ejemplo, usar la canalización de Redis que se ejecuta en un sistema Linux promedio puede entregar incluso 1 millón de solicitudes por segundo, por lo que si su aplicación usa principalmente comandos O (N) u O (log (N)), difícilmente usará demasiada CPU. .
  • Sin embargo, para maximizar el uso de la CPU, puede iniciar varias instancias de Redis en la misma caja y tratarlas como servidores diferentes. En algún momento, una sola caja puede no ser suficiente de todos modos, por lo que si desea usar varias CPU, puede comenzar a pensar en alguna forma de fragmentar antes.
  • Puede encontrar más información sobre el uso de varias instancias de Redis en la página Particionamiento.
  • Sin embargo, con Redis 4.0 comenzamos a hacer que Redis tuviera más subprocesos . Por ahora, esto se limita a eliminar objetos en segundo plano y a bloquear los comandos implementados a través de los módulos de Redis. Para futuras versiones, el plan es hacer que Redis tenga cada vez más subprocesos.

Sin embargo, si la cantidad de datos es relativamente grande, la E / S de disco y la E / S de red pueden convertirse en el cuello de botella del rendimiento general. Porque ya sea para transferir datos al cliente, la transmisión de paquetes RDB durante la sincronización, el paquete INFO en el clúster, el paquete PING / PONG, la sincronización de comandos en el modelo maestro-esclavo, el paquete de latidos, etc. todos los costes de E / S de red grandes, mientras que el gran volumen de datos El vaciado regular de la caché AOF también es una gran presión para E / S de disco.

El método de optimización de la red IO Huan Shen también nos ha mencionado. Actualmente, hay dos formas de abrir código. Una 协议栈优化es el fastsocket de Sina ; la otra es by pass kernelel método, desde el controlador de la tarjeta de red hasta la pila de protocolos de modo de usuario. representa el DPDK de Intel . Obviamente, estos no tienen nada que ver con la forma en que se reproduce la base de datos.

Pero conocemos un problema, es decir, el IO de una tarjeta de red Gigabit general tiene un límite superior. La carga útil de un paquete en una Ethernet es generalmente [84, 1538] bytes. Suponemos que cada paquete está lleno por completo, es decir, donde los datos en 1538 bytes son 1460 bytes y el caudal máximo de una tarjeta de red gigabit por segundo es de aproximadamente 125 MB, por lo que el volumen de datos efectivo máximo que una tarjeta de red gigabit puede soportar por segundo es de aproximadamente 118 MB. ¿Qué problemas ocurrirán cuando un subproceso se ejecute en la red IO? Es posible que este hilo solitario se haya encontrado con las siguientes situaciones:

  • Operación bigkey : escribir en un bigkey lleva más tiempo al asignar memoria. Del mismo modo, eliminar bigkey y liberar memoria también producirá
  • Utilice comandos que sean demasiado complejos : como SORT / SUNION / ZUNIONSTORE / KEYS, o comandos O (N), y N es muy grande. Al igual que las listas comprimidas, también pueden producirse operaciones en cascada.
  • Una gran cantidad de claves expiraron colectivamente : el mecanismo de expiración de Redis también se ejecuta en el hilo principal. Cuando una gran cantidad de claves expira de manera concentrada, tomará tiempo eliminar las claves expiradas al procesar una solicitud, y el tiempo será hacerse más largo.
  • Estrategia de eliminación : La estrategia de caminar también se ejecuta en el hilo principal.Cuando la memoria excede el límite de memoria de Redis, algunas claves deben eliminarse para cada escritura, lo que también hará que se requiera mucho tiempo y más tiempo.
  • La sincronización completa maestro-esclavo genera RDB : aunque el proceso hijo de la bifurcación se utiliza para generar instantáneas de datos, la bifurcación bloqueará todo el hilo en este momento (copia al escribir, se copiarán todos los datos del proceso), cuanto mayor sea la instancia, el mayor el tiempo de bloqueo.

Entonces, la E / S de la red no es administrada por subprocesos durante este período, y solo se puede leer en epoll la próxima vez después de procesar todas las tareas. Si la presión de E / S de la red es realmente grande, el tiempo de bloqueo puede hacer que el búfer de recepción sea estrecho. De ese modo afecta el rendimiento de toda la aplicación y el tiempo de respuesta de las operaciones posteriores. El subproceso múltiple optimiza la E / S de la red desde esta perspectiva.

Aunque hemos discutido este resultado, un software maduro no se puede construir en un solo paso, y el desarrollo de un proyecto no se puede considerar de manera integral al principio, es siempre un proceso iterativo [5]. La lógica del procesamiento de un solo subproceso de toda la base de datos ofrece las ventajas de la facilidad de desarrollo y la facilidad de implementación. Creo que este es un punto en el que Redis es un procesamiento de un solo subproceso que no se puede ignorar.

Operación asincrónica

La razón para intervenir aquí es que muchas personas tienden a asociar la asincronía con el multiproceso. ¿La asincronía tiene que ser multiproceso? Por supuesto que no necesariamente.

Y dicho procesamiento de un solo subproceso (el único subproceso del trabajador también se cuenta) debe usar operaciones asincrónicas, de lo contrario no es un problema de baja eficiencia, sino un problema de disponibilidad. Imagina una send/recvoperación que bloquea tu hilo solitario durante 0,5 segundos, y eso es un pedo. Este tipo de operación puede extenderse a muchos lugares, como la transmisión de comandos en redis, la conexión con el servidor esclavo recién descubierto o centinela o el servidor maestro (el no bloqueo puede tardar varios segundos), o la desconexión de un enchufe.

De hecho, la esencia es que la ejecución real no es cuando se emite la orden, sino en el momento adecuado.

Las operaciones asincrónicas se usan en muchos lugares en Redis, pero solo unas pocas usan subprocesos múltiples. Personalmente, creo que la razón es que estas operaciones no pueden ser asincrónicas, no importa cuánto tiempo de CPU se gaste, el bloqueo es inevitable.

¿Dónde se utilizan subprocesos múltiples?

Después de vender tantos puntos, ¿dónde usa Redis multi-threading (proceso)? La siguiente es toda la información que puedo encontrar basada en el análisis del código fuente de la versión 3.2 y los motores de búsqueda.

  • Resistencia RDB
  • AOF reescribir
  • Operación de cierre AOF
  • Caché de actualización AOF
  • Eliminar objetos de forma asincrónica lazyfree(4.0)
  • Análisis de comandos y E / S de red (6.0)

De los cuales es innecesario decir los dos primeros, por BGSAVEy BGREWRITEAOFse puede realizar en el niño la persistencia de RDB y la reescritura de AOF, por supuesto, serverCronen el tiempo para cumplir ciertas condiciones se desencadenará, no lo explique en detalle aquí.

Y el tercer y cuarto artículo es el objeto de discusión clave del artículo de hoy, es decir, el mecanismo BIO . Ponemos la descripción de este problema en el siguiente apartado.

En cuanto al artículo 56, es una característica introducida en la nueva versión. Eliminar objetos de forma asíncrona es fácil de entender [9] [10] [11]. Ya hemos hablado antes de la E / S de red.

Mecanismo BIO

La primera vez que noté este problema fue cuando estaba mirando la implementación de la parte AOF del código fuente, encontré backgroundRewriteDoneHandlerque hay bioCreateBackgroundJobuna función tan extraña en él, su función es cerrar asincrónicamente el antiguo archivo AOF que acaba de ser ejecutado, y en realidad se está ejecutando en otro subproceso. En ese momento surgieron dos preguntas en mi cabeza:

  1. ¿Por qué el cierre necesita un funcionamiento asíncrono?
  2. ¿Qué más puede hacer este hilo?

La primera pregunta se puede encontrar en los comentarios del código fuente:

  • Actualmente solo hay una operación, que es una llamada al sistema de cierre en segundo plano (2). Esto es necesario, ya que cuando el proceso es el último propietario de una referencia al cierre de un archivo, significa desvincularlo y la eliminación del archivo es lenta, lo que bloquea el servidor.
  • Actualmente solo se ejecuta en segundo plano la operación close (2): porque cuando el servidor es el último propietario de un archivo, cerrar un archivo significa desvincularlo, y eliminar el archivo es muy lento y bloqueará el sistema, así que lo haremos cerrar (2) Ponlo en segundo plano.

En cuanto a la respuesta a la segunda pregunta, también se puede considerar como otra pregunta: ¿Qué hizo exactamente BIO ? De hecho, el nombre completo de BIO Background I/O, no la bio [13] que representa la solicitud de E / S de disco, es el servicio de E / S en segundo plano de Redis, que implementa la función de realizar trabajo en segundo plano. BIO se ejecuta en varios subprocesos.

De hecho, la implementación de esto es muy simple, es un modelo productor-consumidor que usa bloqueos y variables de condición.

Llamada en la función redis.c / main initServer, que se llama bioInit, esta es la función de inicialización de BIO:

void bioInit(void) {
    
    
    pthread_attr_t attr;
    pthread_t thread;
    size_t stacksize;
    int j;
    
    // 初始化 job 队列,以及线程状态;其实也就是调用标准C库,初始化条件变量和锁,以及初始化队列头
    for (j = 0; j < REDIS_BIO_NUM_OPS; j++) {
    
    
        pthread_mutex_init(&bio_mutex[j],NULL);
        pthread_cond_init(&bio_condvar[j],NULL);
        bio_jobs[j] = listCreate();
        bio_pending[j] = 0;
    }

    // 设置栈大小;
    pthread_attr_init(&attr);
    pthread_attr_getstacksize(&attr,&stacksize);	// 默认大小为4294967298
    if (!stacksize) stacksize = 1; /* The world is full of Solaris Fixes */
    while (stacksize < REDIS_THREAD_STACK_SIZE) stacksize *= 2;
    pthread_attr_setstacksize(&attr, stacksize);

    // 创建线程
    for (j = 0; j < REDIS_BIO_NUM_OPS; j++) {
    
    
        void *arg = (void*)(unsigned long) j;
        if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
    
    
            redisLog(REDIS_WARNING,"Fatal: Can't initialize Background Jobs.");
            exit(1);
        }
        bio_threads[j] = thread;
    }
}

Echemos un vistazo a cómo crear una tarea:

void bioCreateBackgroundJob(int type, void *arg1, void *arg2, void *arg3) {
    
    
    struct bio_job *job = zmalloc(sizeof(*job));

    job->time = time(NULL);
    job->arg1 = arg1;
    job->arg2 = arg2;
    job->arg3 = arg3;

    pthread_mutex_lock(&bio_mutex[type]);

    // 将新工作推入队列
    listAddNodeTail(bio_jobs[type],job);
    bio_pending[type]++;

    pthread_cond_signal(&bio_condvar[type]);

    pthread_mutex_unlock(&bio_mutex[type]);
}

Se inserta una tarea estándar de productor-consumidor, bloquear primero y luego signalhacer clic.

El código de consumidor específico está en

#define REDIS_BIO_CLOSE_FILE    0 /* Deferred close(2) syscall. */
#define REDIS_BIO_AOF_FSYNC     1 /* Deferred AOF fsync. */
#define REDIS_BIO_NUM_OPS       2

void aof_background_fsync(int fd) {
    
    
    bioCreateBackgroundJob(REDIS_BIO_AOF_FSYNC,(void*)(long)fd,NULL,NULL); 
}

if (oldfd != -1) bioCreateBackgroundJob(REDIS_BIO_CLOSE_FILE,(void*)(long)oldfd,NULL,NULL);

El código anterior es para crear dos tipos de tareas;

El código relacionado con el consumidor es el siguiente:

void *bioProcessBackgroundJobs(void *arg) {
    
    
    struct bio_job *job;
    unsigned long type = (unsigned long) arg;
    sigset_t sigset;

    /* Make the thread killable at any time, so that bioKillThreads()
     * can work reliably. */	// 设置线程取消相关的条件[14]
    pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL);
    pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL);

    pthread_mutex_lock(&bio_mutex[type]);
    /* Block SIGALRM so we are sure that only the main thread will
     * receive the watchdog signal. */
    sigemptyset(&sigset);
    sigaddset(&sigset, SIGALRM);
    if (pthread_sigmask(SIG_BLOCK, &sigset, NULL))	// 设置线程掩码
        redisLog(REDIS_WARNING,
            "Warning: can't mask SIGALRM in bio.c thread: %s", strerror(errno));

    while(1) {
    
    
        listNode *ln;

        /* The loop always starts with the lock hold. */
        if (listLength(bio_jobs[type]) == 0) {
    
    	// 没考虑虚假唤醒啊
            pthread_cond_wait(&bio_condvar[type],&bio_mutex[type]);
            continue;
        }

        /* Pop the job from the queue. 
         *
         * 取出(但不删除)队列中的首个任务
         */
        ln = listFirst(bio_jobs[type]);
        job = ln->value;

        /* It is now possible to unlock the background system as we know have
         * a stand alone job structure to process.*/
        pthread_mutex_unlock(&bio_mutex[type]);

        /* Process the job accordingly to its type. */
        // 执行任务
        if (type == REDIS_BIO_CLOSE_FILE) {
    
    	// 子线程中实际执行任务的代码
            close((long)job->arg1);

        } else if (type == REDIS_BIO_AOF_FSYNC) {
    
    
            aof_fsync((long)job->arg1);

        } else {
    
    
            redisPanic("Wrong job type in bioProcessBackgroundJobs().");
        }

        zfree(job);

        /* Lock again before reiterating the loop, if there are no longer
         * jobs to process we'll block again in pthread_cond_wait(). */
        pthread_mutex_lock(&bio_mutex[type]);
        // 将执行完成的任务从队列中删除,并减少任务计数器;需要加锁
        listDelNode(bio_jobs[type],ln);
        bio_pending[type]--;
    }
}

El hilo principal bioCreateBackgroundJobinserta diferentes tipos de tareas en la lista de tareas vinculadas, por lo que esta es en realidad una cola de bloqueo, en la que solo hay dos tipos de tareas, a saber, cerrar archivos antiguos y actualizar la caché de la página AOF durante la reescritura de AOF.

#define aof_fsync fdatasync

Vale la pena mencionar que en la versión 3.2, se usa la actualización de caché fdatasync. Este método fsyncno es lo suficientemente seguro en comparación y sync_file_rangeno es eficiente (pero un poco más seguro). No sé por qué se usa. Se usa en versiones superiores. fsync, No sé si se actualizará la versión posterior.

Lo anterior es el principio y la implementación de BIO en la versión 3.2 Aparte de otras cosas, este ejemplo de la vida real del uso de productores y consumidores en la vida real es un buen material de aprendizaje.

mecanismo de lazyfree

La descripción en [11] es lo suficientemente clara, no necesito escribir otro artículo para describir este problema.

Podemos ver en [11] que lazyfreeen realidad es un subproceso BIO recién creado, que admite la eliminación de claves, diccionarios y key-slotestructuras en el clúster (tabla de salto).

La operación también es muy sencilla, es decir, verificar los parámetros entrantes antes de borrar la clave. Si es una opción asíncrona, llamar a la versión de borrado asíncrono. Lo que hace es sellar un objeto y lanzarlo a la cola de solicitudes BIO.

Por supuesto, hay otras situaciones en las que se puede eliminar la clave, por lo que Redis 4.0 agrega varias opciones de configuración nuevas, de la siguiente manera:

  • slave-lazy-flush: La opción de borrar los datos después de que el esclavo recibe el archivo RDB
  • lazyfree-lazy-eviction: Opción de desalojo total de memoria
  • lazyfree-lazy-expire: Opción de eliminación de clave caducada
  • lazyfree-lazy-server-del: Las opciones de eliminación interna, como el cambio de nombre, pueden ir acompañadas de una tecla de eliminación implícita [15].

Representar respectivamente si se debe activar en las cuatro situaciones de eliminación lazy free. Para contenido específico, consulte [15].

E / S de red

La siguiente figura proviene de [8], que básicamente describe la aplicación de multiproceso en la versión 6.0.
Inserte la descripción de la imagen aquí
El análisis de código aquí se puede ver en la descripción en [2]. Se utiliza un método de sondeo para hacer que todo el proceso esté libre de bloqueos. De hecho, es muy inteligente, pero la clave del problema es que tanto el subproceso IO como El hilo principal son las rondas ininterrumpidas. Consulta sin insomnio, ¿estará lleno de CPU durante el tiempo de inactividad? La práctica actual de Redis es cerrar estos subprocesos de E / S cuando se espera procesar menos conexiones, pero siente que todavía está tratando los síntomas en lugar de la causa raíz.

para resumir

De hecho, es una pregunta muy interesante, que involucra muchos puntos de conocimiento. Más tarde, tengo la oportunidad de profundizar en los detalles de implementación de la versión 6.0 de multiproceso. Debe ser una experiencia muy interesante.

referencia:

  1. 《Concurrencia de C ++ en acción》
  2. Compatible oficialmente con subprocesos múltiples! Comparación de rendimiento y evaluación de Redis 6.0 y la versión anterior "
  3. Preguntas frecuentes de Redis
  4. " Modelo de E / S de alto rendimiento de Redis Basics (2) "
  5. " Notas de estudio de ingeniería de software (completas) "
  6. Por qué Redis es de un solo subproceso
  7. " Un poco de comprensión de la programación de servidores Linux "
  8. " Explicación detallada del principio de subprocesos múltiples de Redis "
  9. " [Notas del estudio de Redis] nuevas funciones de la eliminación sin bloqueo de redis4.0 "
  10. " Caminando sobre hielo fino: el gran sacrificio de Redis Lazy Delete "
  11. " Redis · lazyfree · El evangelio de la eliminación de grandes claves "
  12. " Sistema BIO de Redis "
  13. " Habla de Linux IO de nuevo "
  14. Pthread_setcancelstate
  15. " Nuevas funciones de Redis4.0 (3) -Lazy Free "
  16. " Problema de eficiencia de cambio de nombre de ensayo de Redis "

Supongo que te gusta

Origin blog.csdn.net/weixin_43705457/article/details/113477954
Recomendado
Clasificación