Redis-Server 线程模型源码剖析

@李彪  May 31, 2019

Redis-Server 线程模型源码剖析

一. 背景描述

最近有同事咨询Redis线程模型有关的情况,对于Redis线程模型,网上的说法总体是单线程模型,但是对于内部线程结构的分布及线程的主要作用多数没有涉及。接下来,我们就来好好探索一下Redis的线程模型。

二. 环境模拟

首先,我们搭建一个Redis-Server并把它运行起来,然后,我们用拦截一下进程的线程列表,效果如下:

通过上图,可以看到Redis-Server启动了一个主线程(线程号:3836) ,但是Redis-Server还会产生三个子线程(线程号分别为3839、3840、3841)。

通过上面的图片,有人就会发问,那三个线程究竟是做什么的,下面我们从分析redis源代码的角度给大家解密一下。

三. 源码探索

3.1 main函数分析

源代码的分析入口,我们选择了server.c的main函数,这个是Redis-Server的主源文件。在此源文件中,有相关代码如下:

4858      server.supervised = redisIsSupervised(server.supervised_mode);
4859      int background = server.daemonize && !server.supervised;
4860      if (background) daemonize();
4861  
4862      initServer(); //初始化server服务
4863      if (background || server.pidfile) createPidFile();
4864      redisSetProcTitle(argv[0]);
4865      redisAsciiArt();
4866      checkTcpBacklogSettings();

代码中调用了关键的 initServer函数。

3.2 initServer函数

initServer这个函数顾名思义就是完成Redis-Server的初始化工作,我们再跟进这个函数内部,详情如下:

2858      if (server.cluster_enabled) clusterInit();
2859      replicationScriptCacheInit();
2860      scriptingInit(1);
2861      slowlogInit();
2862      latencyMonitorInit();
2863      bioInit();
2864      server.initial_memory_usage = zmalloc_used_memory();

在上面的代码中,有个关键的函数bioInit ,这个bio 是 Background IO的缩写,目前可以猜出这个函数主要用于后台IO的初始化工作。

3.3 BIO分析--关键

Redis-Server在工作流程中,还是有一些工作需要在后台处理,比如一些非常慢的IO场景:数据持久化关闭、一些内存懒释放。关于BIO的作用,redis中的bio.c源文件中有相关描述如下:

/* Background I/O service for Redis.
2   *
3   * This file implements operations that we need to perform in the background.
4   * Currently there is only a single operation, that is a background close(2)
5   * system call. This is needed as when the process is the last owner of a
6   * reference to a file closing it means unlinking it, and the deletion of the
7   * file is slow, blocking the server.
8   *
9   * In the future we'll either continue implementing new things we need or
10   * we'll switch to libeio. However there are probably long term uses for this
11   * file as we may want to put here Redis specific background tasks (for instance
12   * it is not impossible that we'll need a non blocking FLUSHDB/FLUSHALL
13   * implementation).

从上面的描述可以看出BIO目前只包括一个操作,就是后台 close内核函数操作,因为这个操作牵扯到很重的文件IO,文件IO会严重阻塞redis-server,关于BIO具体的功能函数我们在3.4节进行阐述,目前我们接着分析那三个线程的来源。

在BIO源码中,有这样一段:

96  void bioInit(void) {
97      pthread_attr_t attr;
98      pthread_t thread;
99      size_t stacksize;
100      int j;
101  
102      /* Initialization of state vars and objects */
103      for (j = 0; j < BIO_NUM_OPS; j++) {
104          pthread_mutex_init(&bio_mutex[j],NULL);
105          pthread_cond_init(&bio_newjob_cond[j],NULL);
106          pthread_cond_init(&bio_step_cond[j],NULL);
107          bio_jobs[j] = listCreate();
108          bio_pending[j] = 0;
109      }
110  
111      /* Set the stack size as by default it may be small in some system */
112      pthread_attr_init(&attr);
113      pthread_attr_getstacksize(&attr,&stacksize);
114      if (!stacksize) stacksize = 1; /* The world is full of Solaris Fixes */
115      while (stacksize < REDIS_THREAD_STACK_SIZE) stacksize *= 2;
116      pthread_attr_setstacksize(&attr, stacksize);
117  
118      /* Ready to spawn our threads. We use the single argument the thread
119       * function accepts in order to pass the job ID the thread is
120       * responsible of. */
121      for (j = 0; j < BIO_NUM_OPS; j++) {
122          void *arg = (void*)(unsigned long) j;
123          if (pthread_create(&thread,&attr,bioProcessBackgroundJobs,arg) != 0) {
124              serverLog(LL_WARNING,"Fatal: Can't initialize Background Jobs.");
125              exit(1);
126          }
127          bio_threads[j] = thread;
128      }
129  }

这个函数,从名字就可以看出它的主要功能是完成BIO的初始化操作,在源代码中我们找到了线程开辟操作,这里的思路是线程池,那么问题来了,BIO线程池中到底开辟几个线程呢?

通过观察第121行,可以看到BIO_NUM_OPS参数影响了BIO线程池的线程量,那么这个数值到底为多少呢,我们稍微跟踪一下就可以获取:

42  #define BIO_NUM_OPS       3   //bio.h文件

现在可以看出BIO_NUM_OPS默认数值为3,这个数值加上1个主线程,正好是图片中的4个线程。

3.4 BIO 后台任务源码分析

在这里,我多做一些分析,分析BIO到底线程在干啥,下面是主要功能源代码,我通过源码注释来阐述相关功能。

171      while(1) {
172          listNode *ln;
173  
174          /* The loop always starts with the lock hold. */
175          if (listLength(bio_jobs[type]) == 0) {
176              pthread_cond_wait(&bio_newjob_cond[type],&bio_mutex[type]);
177              continue;
178          }
179          /* Pop the job from the queue. */
180          ln = listFirst(bio_jobs[type]); //从任务队列中获取任务
181          job = ln->value;
182          /* It is now possible to unlock the background system as we know have
183           * a stand alone job structure to process.*/
184          pthread_mutex_unlock(&bio_mutex[type]);
185  
186          /* Process the job accordingly to its type. */
187          if (type == BIO_CLOSE_FILE) { //文件关闭操作
188              close((long)job->arg1);
189          } else if (type == BIO_AOF_FSYNC) { //异步文件同步操作
190              redis_fsync((long)job->arg1);
191          } else if (type == BIO_LAZY_FREE) { //redis内存懒释放
192              /* What we free changes depending on what arguments are set:
193               * arg1 -> free the object at pointer.
194               * arg2 & arg3 -> free two dictionaries (a Redis DB).
195               * only arg3 -> free the skiplist. */
196              if (job->arg1)
197                  lazyfreeFreeObjectFromBioThread(job->arg1); //懒释放对象
198              else if (job->arg2 && job->arg3)
199                  lazyfreeFreeDatabaseFromBioThread(job->arg2,job->arg3);//懒释数据库
200              else if (job->arg3)
201                  lazyfreeFreeSlotsMapFromBioThread(job->arg3);//懒释放槽位型Map内存
202          } else {
203              serverPanic("Wrong job type in bioProcessBackgroundJobs().");
204          }
205          zfree(job);

四. 分析总结


通过最初的现象,我们可以看出Redis-server开辟了四个线程,并通过源代码分析,我们可以看出后三个线程是BIO线程,这三个线程完成的功能是一样的,主要包括:从BIO任务队列中取出任务文件描述符关闭磁盘文件同步内存对象懒释放操作

其他的任务均由主线程完成


评论已关闭