背景

  • 网上很多对于redis的话题都谈到了要防止造成大key,因为删除会造成主线程阻塞。看到过一个评论说测试删除2G的一个大key,零碎阻塞了大略80秒的工夫。
  • 已经面试时被问到如何删除一个大key。
  • 最近在浏览redis5.0.8源码,看到其中对于大key的删除,实际上只是在主线程删除了key相干的数据,而理论的value及其内存开释是放在异步删除的线程进行的。这种操作应该不会像网上所说,造成80秒主线程阻塞那么恐怖。

测试筹备

  • redis5.0.x服务端程序一份(不便起见,此处应用docker构建)。
  • php7.3(无他,零碎自带7.3版本),也可应用其它脚本语言。
  • c编译器,增加数据的程序应用的c开发(因为自带的php没装置pcntl扩大=,=,所以应用c的多线程来并发的结构和增加数据),这里次要用于结构数据,能够应用其它语言。

结构数据

  • 编写数据结构代码,此处基于macos平台编写,可依据理论操作系统稍作调整。
  • 写的比拟毛糙,增加不同数量的数据以及批改服务端信息都须要批改宏定义,而后从新编译。
  • #include <stdio.h>#include <pthread.h>#include <stdio.h>#include <unistd.h>#include <netinet/in.h>#include <sys/socket.h>#include <sys/wait.h>#include <sys/types.h>#include <string.h>#include <errno.h>#include <arpa/inet.h>#include <time.h>#include <sys/time.h>#include <signal.h>#define RESV_BUFF_SIZE 512#define SEND_BUFF_SIZE 1024/** * redis服务端的IP地址 */#define DEST_ADDR "172.20.23.83"/** * redis服务端的端口号 */#define DEST_PORT 6379/** * 每个线程总共执行插入动作的次数 */#define TRANS_PER_THREAD 10000/** * 每隔多少次插入进行一次日志打印 */#define LOG_STEP 255/** * 总共开启线程数量 */#define THREADS_NUM 100int numLen(int num) {  int i = 0;  do {      num /= 10;      i++;  } while (num != 0);  return i;}/** * 线程执行代码 * @param p 从0-99的线程编号,用于确定以后线程执行插入的数据段 * 比方编号为1,则插入TRANS_PER_THREAD*1 至 TRANS_PER_THREAD*1 + TRANS_PER_THREAD 区间的数据 */void threadMain(void *p){  int sockfd,*index=p,rlen;  *index *= TRANS_PER_THREAD;  int end = *index + TRANS_PER_THREAD;  struct sockaddr_in dest_addr;  bzero(&(dest_addr), sizeof(dest_addr));  char resvbuf[RESV_BUFF_SIZE];  char sendbuf[SEND_BUFF_SIZE];  sockfd = socket(AF_INET, SOCK_STREAM, 0);  if (sockfd == -1) {      printf("socket create failed:%d!\n",sockfd);  }  dest_addr.sin_family = AF_INET;  dest_addr.sin_port = htons(DEST_PORT);  inet_pton(AF_INET,DEST_ADDR,&dest_addr.sin_addr);  if (connect(sockfd,(struct sockaddr*)&dest_addr, sizeof(struct sockaddr)) == -1) {      printf("connect failed:%d!\n",errno);      perror("error: ");  } else {      printf("connect success!\n");      struct timeval tv,ltv;      for (int i = *index; i < end; ++i) {          rlen = numLen(i);          memset(sendbuf,0,SEND_BUFF_SIZE);          /**           * 结构当次申请发送的数据           * 需满足RESP协定标准           */          sprintf(sendbuf,"*4\r\n$4\r\nHSET\r\n$6\r\nmigkey\r\n$%d\r\nmest%d\r\n$%d\r\nmest67890%d\r\n",rlen+4,i,rlen+9,i);          write(sockfd,sendbuf,strlen(sendbuf));          /**           * 尽管咱们并不关怀服务端的返回           * 此处仍然进行了读取操作,防止缓冲堆满           */          read(sockfd,resvbuf,RESV_BUFF_SIZE);          /**           * 每执行LOG_STEP+1次就打印一条日志信息           * LOG_STEP需满足(2^n - 1)           */          if ((i & LOG_STEP) == 0) {              gettimeofday(&tv, NULL);              if (i > *index) {                  printf("used %ld.%d seconds.\n",tv.tv_sec - ltv.tv_sec,tv.tv_usec - ltv.tv_usec);                  fflush(stdout);              }              ltv = tv;          }      }  }  printf("finished index:%d.\n",*index);  close(sockfd);}void handle_pipe(int sig) {//    printf("sig %d ignore.\n",sig);}int main() {  /**   * 因为tcp的client敞开后,服务端依然有可能向咱们发送数据   * 这样会造成咱们的过程收到SIGPIPE信号   * 所以此处注册信号处理函数   */  struct sigaction sa;  sa.sa_handler = handle_pipe;  sigemptyset(&sa.sa_mask);  sa.sa_flags = 0;  sigaction(SIGPIPE,&sa,NULL);  pthread_t pts[THREADS_NUM];  int indexs[THREADS_NUM];  struct timeval tv,ltv;  gettimeofday(&ltv, NULL);  /**   * 创立线程   */  for (int i = 0; i < THREADS_NUM; ++i) {      indexs[i] = i;      if (pthread_create(&pts[i], NULL, threadMain, &indexs[i]) != 0) {          printf("create thread error!\n");      } else {          printf("thread %d created!\n",i);      }  }  /**   * 期待线程   */  for (int i = 0; i < THREADS_NUM; ++i) {      pthread_join(pts[i], NULL);  }  gettimeofday(&tv, NULL);  printf("\n------All finished!------\n""used %ld.%d seconds.\n",tv.tv_sec - ltv.tv_sec,tv.tv_usec - ltv.tv_usec);  return 0;}
  • 编译并执行,此处通过批改代码以及屡次执行,总共向服务端增加了3个大key。别离为:
  • bigkey,数据长度100万,占用内存约58M。
  • migkey,数据长度100万,占用内存约58M。
  • sigkey,数据长度10万,占用内存5.5M。

筹备测试程序

  • 测试代码,一个简略的php脚本,每100ms向服务端发送一个get命令。察看在执行了删除大key的命令后,这个get命令执行的延时距离判断对主线程的阻塞水平。
  • 脚本代码如下(此处是查问一个key为m的string类型数据。):
  • <?php  $socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);  socket_connect($socket, '172.20.23.83',6379);  $st = explode(' ',microtime());  $st = $st[0] + $st[1];  while (true) {      $msg = "*2\r\n$3\r\nGET\r\n$1\r\nm\r\n";      socket_write($socket,$msg,strlen($msg));      $s = socket_read($socket,100);      $et = explode(' ',microtime());      $et = $et[0] + $et[1];      if (($et - $st) > 0.13) {          echo "-------------------xxx---------------------\n";      }      echo microtime().'--'.$s;      usleep(100000);      $st = $et;  }  socket_close($socket);

执行测试

  • 应用redis-cli连贯redis服务端。
  • 设置一个key为m的string类型数据。
  • 执行用于观测的php脚本。
  • redis-cli执行删除命令。
  • 敞开php脚本。
  • 查找-------------------xxx---------------------标记。
  • 观测标记前后两次输入的工夫距离长度。

测试后果

  • migkey:
  • 0.27018300 1643271634--$4mack-------------------xxx---------------------0.66599900 1643271634--$4mack
  • bigkey(前面的两个标记工夫距离尽管超过了0.13秒,然而相差不远,能够确定为网络稳定造成。这里应该察看第一个标记的前后时间差。):
  • 0.23842800 1643271538--$4mack-------------------xxx---------------------0.76545200 1643271538--$4mack-------------------xxx---------------------0.90037200 1643271538--$4mack0.00909800 1643271539--$4mack-------------------xxx---------------------0.48198300 1643271539--$4mack
  • sigkey:未观测到稳定。

初步后果总结

  • 5.5M的key未造成显著阻塞。
  • 58M左右的key造成约50ms左右的阻塞。

持续结构更大的数据

  • 与网上所说的2G数据差距太大,须要构建更大的数据。
  • 因为通过网络客户端构建数据,即便是多线程,构建效率仍旧不高,所以此次大数据采纳特地的形式(利用redis-cli带pipe参数)进行构建。
  • 应用php脚本构建一个蕴含3000万个redis命令文件0.txt,脚本如下:
  • <?phpecho "start...\n";$st = explode(' ', microtime());$st = $st[0] + $st[1];for ($i=0; $i < 30000000; $i++) {  $rstr = "{$i}";  $slen = strlen($rstr);  $klen = $slen + 4;  $vlen = $slen + 9;  $msg = "*4\r\n$4\r\nHSET\r\n$6\r\nligkey\r\n\${$klen}\r\ntest{$rstr}\r\n\${$vlen}\r\ntest67890{$rstr}\r\n";   file_put_contents('0.txt',$msg,FILE_APPEND);}$et = explode(' ', microtime());$et = $et[0] + $et[1];$used = $et - $st;echo "finished...\n","used $used seconds.\n";
  • 执行脚本生成0.txt文件。
  • 应用命令 cat 0.txt | redis-cli --pipe 导入数据。

再次察看

  • 通过redis命令查看到此次数据约为1.8G。
  • 执行观测脚本。
  • 删除数据ligkey。

大数据观测后果

  • 0.31861400 1643279405--$4mack-------------------xxx---------------------0.39795600 1643279424--$4mack
  • 删除1.8G数据耗时约19.08s。

论断

  • 删除大key的确会造成主线程长时间阻塞,且随着数据的增大阻塞工夫也变长。
  • 下次笔记中须要从源码的角度了解为何会阻塞主线程以及是否有解决方案。