Análise do código-fonte do Redis (24) Exploração do mecanismo BIO

Insira a descrição da imagem aquiEste trabalho é licenciado sob o Acordo de Licença Internacional Creative Commons Atribuição-Uso Não-Comercial 4.0 da mesma forma .
Insira a descrição da imagem aquiEste trabalho ( Lizhao Long Bowen por Li Zhaolong criação) por Li Zhaolong confirmação, indique os direitos autorais.

introdução

Após um lapso de dez meses, mais uma vez peguei uma caneta para explorar coisas relacionadas ao Redis e ainda estava um pouco animado. Recentemente, pretendo usar de dois a três artigos para revisar coisas relacionadas ao Redis. Não é apenas um complemento aos pontos de conhecimento que faltam, mas também um ponto final para a revisão do Redis neste período de tempo.

Este artigo tem como objetivo principal falar sobre uma questão: o Redis é single-threaded ou multi-threaded ? Eu simplesmente pesquisei esse problema nas principais plataformas e descobri que pelo menos 70% dos artigos são basicamente sem valor, mas também há muitos artigos bons. Este artigo é baseado na discussão dos antecessores, juntamente com meu próprio entendimento, para discutir esse assunto. No entanto, como a versão do código-fonte é muito baixa, algumas partes da discussão não podem ser anexadas ao código.

Thread único ou multi thread

Quando devemos usar multithreading? Conforme descrito em [1], é claro que existem duas razões para usar multithreading, a saber, o uso de eficiência multi-core e separação de interesses . Pelo contrário, a razão para não usar multithreading é que os benefícios não são tão bons quanto as recompensas .

Para descartar a resposta primeiro, o Redis usa multithreading em vez de single threading . Claro, o significado aqui é um pouco diferente da ideia padrão em nosso bate-papo usual. Quando normalmente falamos sobre WebServer single-threaded ou multi-threaded, na verdade discutimos se há mais ou mais threads de trabalho, que é o geral projeto de rosca de Worker É um ou mais? Para dar o exemplo mais simples, o one loop per threadmodelo usado por muduo é um modelo semi-síncrono e semi-assíncrono muito clássico, que é um modelo multi-threaded, em que um thread trata a conexão e o thread restante trata da solicitação. A razão para isso é porque o WebServer (da perspectiva da análise de desempenho do RabbitServer ) é um programa intensivo em computação, e precisamos aplicar melhor o aumento no poder de computação trazido pelo multi-core.

Este não é o caso do Redis. É fácil ver que as tarefas de computação executadas pelo programa servidor Redis são na verdade muito simples. Os dados são recebidos no loop epoll, então o comando é analisado e finalmente executado. Como o design da estrutura de dados no Redis é muito inteligente, basicamente operar esses dados não leva muito tempo. Você pode ver o seguinte texto em [3]:

  • Não é muito frequente que a CPU se torne o seu gargalo com o Redis, já que normalmente o Redis é limitado pela memória ou pela rede . Por exemplo, usar o Redis de pipeline em execução em um sistema Linux médio pode entregar até 1 milhão de solicitações por segundo, portanto, se seu aplicativo usa principalmente comandos O (N) ou O (log (N)), dificilmente usará muita CPU .
  • No entanto, para maximizar o uso da CPU, você pode iniciar várias instâncias do Redis na mesma caixa e tratá-las como servidores diferentes. Em algum ponto, uma única caixa pode não ser suficiente de qualquer maneira, então se você quiser usar várias CPUs, pode começar a pensar em alguma forma de fragmentar mais cedo.
  • Você pode encontrar mais informações sobre como usar várias instâncias do Redis na página Particionamento.
  • No entanto, com o Redis 4.0, começamos a torná-lo mais encadeado . Por enquanto, isso se limita a excluir objetos em segundo plano e a bloquear comandos implementados por meio de módulos do Redis. Para lançamentos futuros, o plano é tornar o Redis cada vez mais encadeado.

No entanto, se a quantidade de dados for relativamente grande, IO de disco e IO de rede podem se tornar o gargalo de desempenho geral. Porque seja para transferir dados para o cliente, a transmissão de pacotes RDB durante a sincronização, o pacote INFO no cluster, o pacote PING / PONG, a sincronização de comandos no modelo mestre-escravo, o pacote de pulsação, etc. todos os grandes custos de E / S de rede, enquanto big data A liberação regular do cache AOF também é uma grande pressão para E / S de disco.

O método de otimização da rede IO Huan Shen também nos mencionou. Atualmente, existem duas maneiras de abrir o código-fonte. Uma 协议栈优化é o fastsocket do Sina ; a outra é by pass kernelo método, do driver da placa de rede à pilha de protocolo do modo de usuário. ele representa o DPDK da Intel . Obviamente, isso não tem nada a ver com a forma como o banco de dados é reproduzido.

Mas sabemos de um problema, isto é, o IO de uma placa de rede Gigabit geral tem um limite superior. A carga útil de um pacote em uma Ethernet é geralmente [84, 1538] bytes. Assumimos que cada pacote está totalmente preenchido, ou seja, onde os dados em 1538 bytes são 1460 bytes e a taxa de fluxo máxima de uma placa de rede gigabit por segundo é de cerca de 125 MB, portanto, o volume de dados efetivo máximo que uma placa de rede gigabit pode suportar por segundo é de cerca de 118 MB. Que problemas ocorrerão quando um thread executar o IO da rede? É possível que este tópico solitário tenha encontrado as seguintes situações:

  • Operação bigkey : escrever em um bigkey leva mais tempo ao alocar memória. Da mesma forma, excluir o bigkey e liberar memória também irá consumir muito tempo
  • Use comandos muito complexos : como SORT / SUNION / ZUNIONSTORE / KEYS ou comandos O (N) e N é muito grande. Como as listas compactadas, também podem ocorrer operações em cascata.
  • Um grande número de chaves expirou coletivamente : o mecanismo de expiração do Redis também é executado no thread principal. Quando um grande número de chaves expiram de maneira concentrada, levará algum tempo para excluir as chaves expiradas ao processar uma solicitação, e o tempo será tornar-se mais longo.
  • Estratégia de eliminação : A estratégia de caminhada também é executada no thread principal.Quando a memória ultrapassa o limite de memória do Redis, algumas chaves precisam ser eliminadas a cada gravação, o que também demorará e será mais demorado.
  • A sincronização completa mestre-escravo gera RDB : Embora o processo filho fork seja usado para gerar instantâneos de dados, o fork irá bloquear todo o thread neste momento (cópia na gravação, todos os dados do processo serão copiados), quanto maior a instância, o mais tempo de bloqueio.

Então, o IO da rede não é gerenciado por threads durante esse período e só pode ser lido no epoll na próxima vez após o processamento de todas as tarefas. Se a pressão do IO da rede for realmente grande, o tempo de bloqueio pode fazer com que o buffer de recebimento fique apertado., Isso afeta a taxa de transferência de todo o aplicativo e o tempo de resposta das operações subsequentes. O multithreading otimiza o IO da rede a partir dessa perspectiva.

Embora tenhamos discutido esse resultado, um software maduro não pode ser construído em uma única etapa, e o desenvolvimento de um projeto não pode ser considerado de forma abrangente no início, sendo sempre um processo iterativo [5]. A lógica do processamento single-threaded de todo o banco de dados traz as vantagens de facilidade de desenvolvimento e facilidade de implementação.Eu acho que esse é um ponto que o Redis é um processamento single-threaded que não pode ser ignorado.

Operação assíncrona

O motivo da interjeição aqui é que muitas pessoas tendem a associar assincronia com multithreading. A assincronia precisa ser multithreading? Claro que não necessariamente.

E esse processamento de thread único (thread único do trabalhador também é contado) deve usar operações assíncronas, caso contrário, não é um problema de baixa eficiência, mas um problema de disponibilidade. Imagine uma send/recvoperação que bloqueia seu fio solitário por 0,5 segundo, e isso é um peido. Este tipo de operação pode ser estendido a muitos lugares, como a transmissão de comandos no redis, a conexão com o servidor escravo ou sentinela recém-descoberto ou o servidor mestre (o não-bloqueio pode levar vários segundos), ou a desconexão de um soquete.

Na verdade, a essência é que a execução real não ocorre quando a ordem é emitida, mas em um momento apropriado.

As operações assíncronas são usadas em muitos lugares no Redis, mas apenas alguns usam multithreading. Eu pessoalmente acho que o motivo é que essas operações não podem ser assíncronas, não importa quanto tempo de CPU seja gasto, o bloqueio é inevitável.

Onde o multithreading é usado

Depois de vender tantos pontos, onde o Redis usa multi-threading (processo)? A seguir estão todas as informações que posso encontrar com base na análise do código-fonte da versão 3.2 e motores de busca.

  • Resistência RDB
  • AOF reescrever
  • Operação de fechamento AOF
  • Cache de atualização AOF
  • Excluindo objetos de forma assíncrona lazyfree(4.0)
  • IO de rede e análise de comando (6.0)

Dos quais os dois primeiros desnecessários dizer, por BGSAVEe BGREWRITEAOFpodem ser executados na persistência RDB filho e reescrever AOF, é claro, serverCronna hora de atender a certas condições irá disparar, não explique em detalhes aqui.

E o terceiro e o quarto artigo são o principal objeto de discussão do artigo de hoje, ou seja, o mecanismo de BIO . Colocamos a descrição desse problema na próxima seção.

Quanto ao Artigo 56, é uma característica introduzida na nova versão.A exclusão de objetos de forma assíncrona é fácil de entender [9] [10] [11]. Já discutimos IO de rede antes.

Mecanismo BIO

A primeira vez que notei esse problema foi quando estava olhando para a implementação da parte AOF do código-fonte, descobri backgroundRewriteDoneHandlerque há bioCreateBackgroundJobuma função tão estranha nela, sua função é fechar de forma assíncrona o antigo arquivo AOF que acabou de ser executado e, na verdade, está sendo executado em outro Thread. Duas perguntas surgiram na minha cabeça na época:

  1. Por que o fechamento precisa de operação assíncrona?
  2. O que mais esse tópico pode fazer?

A primeira pergunta pode ser encontrada nos comentários do código-fonte:

  • Atualmente, há apenas uma única operação, que é uma chamada de sistema de fechamento em segundo plano (2). Isso é necessário, pois quando o processo é o último dono de uma referência a um arquivo, o fechamento significa desvinculá-lo, e a exclusão do arquivo é lenta, bloqueando o servidor.
  • Atualmente, apenas a operação close (2) é executada em segundo plano: porque quando o servidor é o último dono de um arquivo, fechar um arquivo significa desvinculá-lo, e deletar o arquivo é muito lento e irá bloquear o sistema, então iremos fechar (2) Colocar em segundo plano.

Quanto à resposta à segunda pergunta, ela também pode ser considerada como outra pergunta: o que exatamente a BIO fez ? Na verdade, o nome completo do BIO Background I/O, não o bio [13] que representa a solicitação de IO do disco, é o serviço IO de fundo do Redis, que implementa a função de realizar trabalho em segundo plano. BIO é executado em vários threads.

Na verdade, a implementação dessa coisa é muito simples, é um modelo produtor-consumidor usando bloqueios e variáveis ​​de condição.

Chamado na função redis.c / main initServer, que é chamada bioInit, esta é a função de inicialização do 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;
    }
}

Vamos dar uma olhada em como criar uma tarefa:

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]);
}

Uma tarefa produtor-consumidor padrão é inserida, bloqueie primeiro e, em seguida, signalclique em.

O código do consumidor específico está em

#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);

O código acima é para criar dois tipos de tarefas;

O código relacionado ao consumidor é o seguinte:

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]--;
    }
}

O thread principal bioCreateBackgroundJobinsere diferentes tipos de tarefas na lista vinculada de tarefas, então esta é na verdade uma fila de bloqueio, na qual existem apenas dois tipos de tarefas, a saber, fechar arquivos antigos e atualizar o cache de página AOF durante a reescrita de AOF.

#define aof_fsync fdatasync

Vale ressaltar que na versão 3.2, a atualização do cache é usada fdatasync. Este método fsyncnão é seguro o suficiente em comparação e sync_file_rangenão é eficiente (mas um pouco mais seguro). Não sei por que isso é usado. É usado em versões superiores. fsync, Não sei se a versão posterior será atualizada.

A descrição acima é o princípio e a implementação da BIO na versão 3.2. Além de outras coisas, este exemplo da vida real do uso de produtores e consumidores na vida real é um bom material de aprendizagem.

mecanismo preguiçoso

A descrição em [11] é clara o suficiente, não preciso escrever outro artigo para descrever este problema.

Podemos ver em [11] que lazyfreeé na verdade um thread BIO recém-criado, que suporta a exclusão de chaves, dicionários e key-slotestruturas no cluster (tabela de salto).

A operação também é muito simples, ou seja, verifique os parâmetros de entrada antes de excluir a chave. Se for uma opção assíncrona, chame a versão de exclusão assíncrona. O que ela faz é lacrar um objeto e jogá-lo na fila de solicitações BIO.

Claro, existem outras situações em que a chave pode ser excluída, então o Redis 4.0 adiciona várias novas opções de configuração, como segue:

  • slave-lazy-flush: A opção de limpar os dados após o escravo receber o arquivo RDB
  • lazyfree-lazy-eviction: Opção de despejo total de memória
  • lazyfree-lazy-expire: Opção de exclusão de chave expirada
  • lazyfree-lazy-server-del: Opções de exclusão internas, como renomear, podem ser acompanhadas por uma tecla de exclusão implícita [15].

Respectivamente, represente se deseja ativar nas quatro situações de exclusão lazy free. Para conteúdo específico, consulte [15].

Rede IO

A figura a seguir vem de [8], que basicamente descreve a aplicação do multithreading na versão 6.0.
Insira a descrição da imagem aqui
A análise de código aqui pode ser vista a partir da descrição em [2]. Um método de votação é usado para tornar todo o processo livre de bloqueio. Na verdade, é muito inteligente, mas a chave para o problema é que tanto o sub-thread de IO quanto o tópico principal são rodadas ininterruptas Consulta sem insônia, então ela estará cheia de CPU durante o tempo ocioso? A prática atual do Redis é fechar esses threads de E / S ao aguardar para processar menos conexões, mas parece que ainda está tratando os sintomas, e não a causa raiz.

Resumindo

Na verdade, é uma questão muito interessante, envolvendo muitos pontos de conhecimento. Mais tarde, tenho a oportunidade de dar uma olhada mais aprofundada nos detalhes de implementação da versão 6.0 do multithreading. Deve ser uma experiência muito interessante.

referência:

  1. 《Simultaneidade C ++ em ação》
  2. " Suporta oficialmente multi-threading! Comparação e avaliação de desempenho do Redis 6.0 e da versão antiga "
  3. Redis FAQ
  4. " Redis Basics (2) Modelo IO de alto desempenho "
  5. " Notas do estudo de engenharia de software (completo) "
  6. Por que o Redis é de segmento único
  7. " Um pouco de compreensão da programação de servidor Linux "
  8. " Explicação detalhada do princípio de multithreading do Redis "
  9. " [Notas de estudo do Redis] novos recursos de exclusão sem bloqueio redis4.0 "
  10. " Caminhando sobre o gelo fino - O grande sacrifício do Redis Lazy Delete "
  11. " Redis · preguiçoso · O Evangelho da Exclusão de Grandes Chaves "
  12. " Sistema Redis BIO "
  13. " Fale sobre o Linux IO novamente "
  14. Pthread_setcancelstate
  15. " Novos recursos do Redis4.0 (3) -Lazy Free "
  16. " Problema de eficiência de redefinição de redação de redis "

Acho que você gosta

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