共计 5258 个字符,预计需要花费 14 分钟才能阅读完成。
背景
- 网上很多对于 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 100 int 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(<v, 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--$4 mack -------------------xxx--------------------- 0.66599900 1643271634--$4 mack
- bigkey(前面的两个标记工夫距离尽管超过了 0.13 秒,然而相差不远,能够确定为网络稳定造成。这里应该察看第一个标记的前后时间差。):
-
0.23842800 1643271538--$4 mack -------------------xxx--------------------- 0.76545200 1643271538--$4 mack -------------------xxx--------------------- 0.90037200 1643271538--$4 mack 0.00909800 1643271539--$4 mack -------------------xxx--------------------- 0.48198300 1643271539--$4 mack
- sigkey: 未观测到稳定。
初步后果总结
- 5.5M 的 key 未造成显著阻塞。
- 58M 左右的 key 造成约 50ms 左右的阻塞。
持续结构更大的数据
- 与网上所说的 2G 数据差距太大,须要构建更大的数据。
- 因为通过网络客户端构建数据,即便是多线程,构建效率仍旧不高,所以此次大数据采纳特地的形式(利用 redis-cli 带 pipe 参数)进行构建。
- 应用 php 脚本构建一个蕴含 3000 万个 redis 命令文件 0.txt,脚本如下:
-
<?php echo "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--$4 mack -------------------xxx--------------------- 0.39795600 1643279424--$4 mack
- 删除 1.8G 数据耗时约 19.08s。
论断
- 删除大 key 的确会造成主线程长时间阻塞,且随着数据的增大阻塞工夫也变长。
- 下次笔记中须要从源码的角度了解为何会阻塞主线程以及是否有解决方案。
正文完