Analyse du code source Redis (24) Exploration des mécanismes BIO

Insérez la description de l'image iciCe travail est concédé sous le contrat de licence international Creative Commons Attribution - Utilisation non commerciale - Partage 4.0 de la même manière .
Insérez la description de l'image iciCette oeuvre ( Lizhao Long Bowen par Li Zhaolong création) par Li Zhaolong confirmation, veuillez indiquer le copyright.

introduction

Au bout de dix mois, j'ai à nouveau pris un stylo pour explorer des choses liées à Redis, et j'étais encore un peu excité. Récemment, j'ai l'intention d'utiliser deux à trois articles pour passer en revue les choses liées à Redis. Ce n'est pas seulement un complément aux points de connaissance manquants, mais aussi un arrêt complet de la révision de Redis pendant cette période.

Cet article veut principalement parler d'une question, Redis est -il mono-thread ou multi-thread ? J'ai simplement cherché ce numéro sur les principales plates-formes et j'ai constaté qu'au moins 70% des articles sont fondamentalement sans valeur, mais il y a aussi de nombreux bons articles. Cet article est basé sur la discussion des prédécesseurs, associée à ma propre compréhension, pour discuter de cette question. Cependant, étant donné que la version du code source est trop faible, certaines parties de la discussion ne peuvent pas être jointes au code.

Fil unique ou multi fil

Quand devrions-nous utiliser le multithreading? Comme décrit dans [1], il est clair qu'il y a deux raisons d'utiliser le multithreading, à savoir l'utilisation de l' efficacité multicœur et la séparation des préoccupations . Au contraire, la raison de ne pas utiliser le multithreading est que les avantages ne sont pas aussi bons que les récompenses .

Pour jeter la réponse en premier, Redis utilise le multithreading au lieu d'un thread unique . Bien sûr, la signification ici est un peu différente de l'idée par défaut dans notre chat habituel. Lorsque nous parlons généralement de WebServer monothread ou multithread, nous discutons en fait s'il y a plus ou plus de threads de travail, ce qui est globalement conception de fil de travailleur Est-ce un ou plusieurs? Pour donner l'exemple le plus simple, le one loop per threadmodèle utilisé par muduo est un modèle semi-synchrone et semi-asynchrone très classique, qui est un modèle multi-thread, dans lequel un thread gère la connexion et le thread restant gère la demande. La raison en est que WebServer (du point de vue de l'analyse des performances de RabbitServer ) est un programme intensif en calcul, et nous devons mieux appliquer l'augmentation de la puissance de calcul apportée par le multicœur.

Ce n'est pas le cas avec Redis. Il est facile de voir que les tâches de calcul effectuées par le programme serveur Redis sont en fait très simples. Les données sont reçues dans la boucle epoll, puis la commande est analysée et enfin exécutée. Parce que la conception de la structure de données dans Redis est très intelligente, l'exploitation de ces données ne prend pas trop de temps. Vous pouvez voir le texte suivant dans [3]:

  • Il n'est pas très fréquent que le processeur devienne votre goulot d'étranglement avec Redis, car Redis est généralement lié à la mémoire ou au réseau . Par exemple, l'utilisation de Redis pipelining s'exécutant sur un système Linux moyen peut fournir même 1 million de requêtes par seconde, donc si votre application utilise principalement des commandes O (N) ou O (log (N)), elle n'utilisera guère trop de CPU. .
  • Cependant, pour maximiser l'utilisation du processeur, vous pouvez démarrer plusieurs instances de Redis dans la même boîte et les traiter comme des serveurs différents. À un moment donné, une seule boîte peut ne pas suffire de toute façon, donc si vous souhaitez utiliser plusieurs processeurs, vous pouvez commencer à penser à un moyen de partitionner plus tôt.
  • Vous pouvez trouver plus d'informations sur l'utilisation de plusieurs instances Redis dans la page Partitionnement.
  • Cependant, avec Redis 4.0, nous avons commencé à rendre Redis plus threadé . Pour l'instant, cela se limite à la suppression d'objets en arrière-plan et au blocage des commandes implémentées via les modules Redis. Pour les versions futures, le plan est de rendre Redis de plus en plus threadé.

Toutefois, si la quantité de données est relativement importante, les E / S disque et les E / S réseau peuvent devenir le goulot d'étranglement des performances globales. Car que ce soit pour transférer des données vers le client, la transmission des paquets RDB lors de la synchronisation, le paquet INFO dans le cluster, le paquet PING / PONG, la synchronisation des commandes dans le modèle maître-esclave, le paquet Heartbeat, etc. sont tous les gros coûts d'E / S réseau, tandis que les données volumineuses Le vidage régulier du cache AOF est également une pression énorme pour les E / S disque.

La méthode d'optimisation du réseau IO que Huan Shen nous a également mentionnée. Actuellement, il existe deux façons d'ouvrir la source. L'une 协议栈优化est la fastsocket de Sina ; l'autre est by pass kernella méthode, du pilote de la carte réseau à la pile de protocoles en mode utilisateur. Utilisé, il représente le DPDK d'Intel . Évidemment, cela n'a rien à voir avec la façon dont la base de données est lue.

Mais nous connaissons un problème, c'est-à-dire que les E / S d'une carte réseau Gigabit générale ont une limite supérieure. La charge utile d'un paquet sur un Ethernet est généralement de [84, 1538] octets. Nous supposons que chaque paquet est rempli au maximum, c'est-à-dire où les données en 1538 octets sont de 1460 octets et le débit maximal d'une carte réseau gigabit par seconde est d'environ 125 Mo, de sorte que le volume de données effectif maximal qu'une carte réseau gigabit peut supporter par seconde est d'environ 118 Mo. Quels problèmes se produiront lorsqu'un thread exécute des E / S réseau? Il est possible que ce thread solitaire ait rencontré les situations suivantes:

  • Opération bigkey : l'écriture sur un bigkey prend plus de temps lors de l'allocation de mémoire. De même, la suppression de bigkey et la libération de mémoire seront également chronophages
  • Utilisez des commandes trop complexes : telles que les commandes SORT / SUNION / ZUNIONSTORE / KEYS ou O (N), et N est très grand. À l'instar des listes compressées, des opérations en cascade peuvent également se produire.
  • Un grand nombre de clés ont expiré collectivement : le mécanisme d'expiration de Redis est également exécuté dans le thread principal. Lorsqu'un grand nombre de clés expire de manière concentrée, il faudra du temps pour supprimer les clés expirées lors du traitement d'une demande, et le temps sera devenir plus long.
  • Stratégie d'élimination : La stratégie de marche est également exécutée dans le thread principal.Lorsque la mémoire dépasse la limite de mémoire Redis, certaines clés doivent être éliminées à chaque écriture, ce qui prendra également du temps et plus longtemps.
  • La synchronisation complète maître-esclave génère RDB : bien que le processus fils de fork soit utilisé pour générer des instantanés de données, le fork bloquera le thread entier à ce moment (copie lors de l'écriture, toutes les données de processus seront copiées), plus l'instance est grande, le plus le temps de blocage.

Ensuite, les E / S réseau ne sont pas gérées par les threads pendant cette période et ne peuvent être lues que dans epoll la prochaine fois après le traitement de toutes les tâches. Si la pression d'E / S réseau est vraiment élevée, le temps de blocage peut entraîner une réduction du tampon de réception., Cela affecte le débit de l'ensemble de l'application et le temps de réponse des opérations ultérieures. Le multithreading optimise les E / S réseau de ce point de vue.

Bien que nous ayons discuté de ce résultat, un logiciel mature ne peut pas être construit en une seule étape, et le développement d'un projet ne peut pas être envisagé globalement au départ, il s'agit toujours d'un processus itératif [5]. La logique du traitement mono-thread de l'ensemble de la base de données apporte les avantages de la facilité de développement et de la facilité de mise en œuvre.Je pense que c'est un point que Redis est un traitement mono-thread qui ne peut être ignoré.

Fonctionnement asynchrone

La raison de l'interjection ici est que de nombreuses personnes ont tendance à associer l'asynchronie au multithreading. L'asynchronie doit-elle être multithread? Bien sûr pas nécessairement.

Et un tel traitement mono-thread (le thread unique du travailleur est également compté) doit utiliser des opérations asynchrones, sinon ce n'est pas un problème de faible efficacité, mais un problème de disponibilité. Imaginez une send/recvopération qui bloque votre thread solitaire pendant 0,5 seconde, et c'est un pet. Ce type d'opération peut être étendu à de nombreux endroits, comme la transmission de commandes dans redis, la connexion avec le serveur esclave ou sentinelle nouvellement découvert ou le serveur maître (le non-blocage peut prendre jusqu'à plusieurs secondes), ou la déconnexion de une prise.

En fait, l'essentiel est que l'exécution réelle ne se fait pas au moment de l'émission de l'ordre, mais à un moment approprié.

Les opérations asynchrones sont utilisées dans de nombreux endroits dans Redis, mais seuls quelques-uns utilisent le multithreading. Je pense personnellement que la raison en est que ces opérations ne peuvent pas être asynchrones, quel que soit le temps processeur passé, le blocage est inévitable.

Où le multithreading est-il utilisé

Après avoir vendu autant de points, où Redis utilise-t-il le multi-threading (processus)? Voici toutes les informations que je peux trouver sur la base de l'analyse du code source de la version 3.2 et des moteurs de recherche.

  • Endurance RDB
  • Réécriture AOF
  • Opération de fermeture AOF
  • Cache de rafraîchissement AOF
  • Supprimer des objets de manière asynchrone lazyfree(4.0)
  • Analyse des E / S réseau et des commandes (6.0)

Dont les deux premiers inutiles à dire, par BGSAVEet BGREWRITEAOFpeuvent être effectués dans la persistance RDB enfant et la réécriture AOF, bien sûr, serverCronà temps pour répondre à certaines conditions se déclenchera, ne pas expliquer en détail ici.

Et le troisième et quatrième article est le principal objet de discussion de l'article d'aujourd'hui, c'est-à-dire le mécanisme BIO . Nous mettons la description de ce problème dans la section suivante.

Quant à l'article 56, il s'agit d'une fonctionnalité introduite dans la nouvelle version. La suppression d'objets de manière asynchrone est facile à comprendre [9] [10] [11]. Nous avons déjà évoqué les E / S réseau auparavant.

Mécanisme BIO

La première fois que j'ai remarqué ce problème, c'est lorsque je regardais l'implémentation de la partie AOF du code source, j'ai trouvé backgroundRewriteDoneHandlerqu'il y avait bioCreateBackgroundJobune fonction tellement étrange dedans, sa fonction est de fermer de manière asynchrone l'ancien fichier AOF qui vient d'être exécuté, et il est en fait exécuté dans un autre Thread. Deux questions ont surgi dans ma tête à l'époque:

  1. Pourquoi la fermeture nécessite-t-elle une opération asynchrone?
  2. Que peut faire d'autre ce fil?

La première question se trouve dans les commentaires du code source:

  • Actuellement, il n'y a qu'une seule opération, c'est-à-dire un appel système de fermeture en arrière-plan (2). Cela est nécessaire car lorsque le processus est le dernier propriétaire d'une référence à un fichier, la fermeture signifie le dissocier et la suppression du fichier est lente, bloquant le serveur.
  • Actuellement, seule l'opération close (2) est exécutée en arrière-plan: car lorsque le serveur est le dernier propriétaire d'un fichier, fermer un fichier signifie le dissocier, et supprimer le fichier est très lent et bloquera le système, nous allons donc close (2) Mettez-le en arrière-plan.

Quant à la réponse à la deuxième question, elle peut également être considérée comme une autre question: qu'a fait exactement BIO ? En fait, le nom complet de BIO Background I/O, et non la bio [13] qui représente la demande d'E / S disque, est le service d'E / S d'arrière-plan de Redis, qui implémente la fonction d'effectuer un travail en arrière-plan. BIO est exécuté dans plusieurs threads.

En fait, la mise en œuvre de cette chose est très simple, c'est un modèle producteur-consommateur utilisant des verrous et des variables de condition.

Appelée dans la fonction redis.c / main initServer, qui est appelée bioInit, c'est la fonction d'initialisation 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;
    }
}

Voyons comment créer une tâche:

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

Une tâche producteur-consommateur standard est insérée, verrouillez d'abord, puis signalcliquez sur.

Le code de la consommation spécifique 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);

Le code ci-dessus consiste à créer deux types de tâches;

Le code relatif au consommateur est le suivant:

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

Le thread principal bioCreateBackgroundJobinsère différents types de tâches dans la liste liée de tâches, il s'agit donc en fait d'une file d'attente de blocage, dans laquelle il n'y a que deux types de tâches, à savoir la fermeture d'anciens fichiers et l'actualisation du cache de page AOF lors de la réécriture AOF.

#define aof_fsync fdatasync

Il est à noter que dans la version 3.2, l’actualisation du cache est utilisée fdatasync. Cette méthode n’est fsyncpas assez sûre par rapport à et sync_file_rangepas efficace (mais légèrement plus sûre). Je ne sais pas pourquoi elle est utilisée. Elle est utilisée dans les versions supérieures. fsync, Je ne sais pas si la dernière version sera mise à jour.

Ce qui précède est le principe et la mise en œuvre de BIO dans la version 3.2 Outre d'autres choses, cet exemple réel d'utilisation des producteurs et des consommateurs dans la vie réelle est un bon matériel d'apprentissage.

mécanisme paresseux

La description dans [11] est assez claire, je n'ai pas besoin d'écrire un autre article pour décrire ce problème.

Nous pouvons voir dans [11] lazyfreequ'il s'agit en fait d'un thread BIO nouvellement créé, qui prend en charge la suppression de clés, de dictionnaires et de key-slotstructures dans le cluster (table de saut).

L'opération est également très simple, c'est-à-dire vérifier les paramètres entrants avant de supprimer la clé. S'il s'agit d'une option asynchrone, appelez la version de suppression asynchrone. Elle scelle un objet et le jette dans la file d'attente des demandes BIO.

Bien sûr, il existe d'autres situations où la clé peut être supprimée, donc Redis 4.0 ajoute plusieurs nouvelles options de configuration, comme suit:

  • slave-lazy-flush: L'option d'effacer les données après que l'esclave reçoive le fichier RDB
  • lazyfree-lazy-eviction: Option d'éviction complète de la mémoire
  • lazyfree-lazy-expire: Option de suppression de clé expirée
  • lazyfree-lazy-server-del: Les options de suppression internes, telles que le changement de nom, peuvent être accompagnées d'une touche de suppression implicite [15].

Représenter respectivement s'il faut activer dans les quatre situations de suppression lazy free. Pour un contenu spécifique, veuillez vous référer à [15].

Réseau IO

La figure suivante provient de [8], qui décrit essentiellement l'application du multithreading dans la version 6.0.
Insérez la description de l'image ici
L'analyse de code ici peut être vue à partir de la description dans [2]. Une méthode d'interrogation est utilisée pour rendre l'ensemble du processus sans verrouillage. C'est en effet très intelligent, mais la clé du problème est que le sous-thread IO et le fil principal sont des rondes non-stop Enquête sans insomnie, alors sera-t-il plein de CPU pendant le temps d'inactivité? La pratique actuelle de Redis consiste à fermer ces threads d'E / S en attendant de traiter moins de connexions, mais il estime qu'il traite toujours les symptômes plutôt que la cause première.

Pour résumer

C'est en effet une question très intéressante, impliquant beaucoup de points de connaissance.Plus tard, j'ai l'occasion d'approfondir les détails d'implémentation de la version 6.0 du multithreading.Ce doit être une expérience très intéressante.

référence:

  1. 《Concurrence C ++ en action》
  2. " Supporte officiellement le multi-threading! Comparaison des performances et évaluation de Redis 6.0 et de l’ancienne version "
  3. Redis FAQ
  4. " Redis Basics (2) Modèle d'E / S hautes performances "
  5. " Notes d'étude en génie logiciel (complètes) "
  6. Pourquoi Redis est monofiltre
  7. " Un peu de compréhension de la programmation des serveurs Linux "
  8. " Explication détaillée du principe de multithreading Redis "
  9. " [Notes d'étude Redis] nouvelles fonctionnalités de la suppression non bloquante redis4.0 "
  10. " Marcher sur de la glace mince - Le grand sacrifice de Redis Lazy Delete "
  11. " Redis · paresseux · L'évangile de la suppression des grandes clés "
  12. « Système Redis BIO »
  13. " Parlez à nouveau de Linux IO "
  14. Pthread_setcancelstate
  15. " Nouvelles fonctionnalités de Redis4.0 (3) -Lazy Free "
  16. " Problème d'efficacité du changement de nom de l'essai Redis "

Je suppose que tu aimes

Origine blog.csdn.net/weixin_43705457/article/details/113477954
conseillé
Classement