Redis6.0解密-1.Thread/IO多线程
一、背景
目前快手有70w+的Redis实例,在线上的Redis集群,我们经常会碰到以下的一些情况:
(1) 由于键值设计不合理或者业务特性导致的热点问题(集群整体QPS不高,但是集群内某个实例的请求特别高),严重影响业务侧请求的返回时间
(2) 集群内某个实例直连集群连接数过多,单线程模型处理缓慢,影响其他的请求
(3) 集群内某个实例网络不稳定后者pipeline个数较多,导致协议解析频繁调用,导致cpu时间占用过长,影响其他的客户端请求
以上这些问题,相信大家也都碰到过,那么这些问题与Redis的单线程模型又有什么关系?
1. 为什么Redis6之前是单线程设计?
首先,我们明确一点,Redis6之前的Redis4,Redis5并不是单线程程序。通常我们说的Redis的单线程,是指Redis接受链接,接收数据并解析协议,发送结果等命令的执行,都是在主线程中执行的。
Redis之前之所以将这些都放在主线程中执行,主要有以下几方面的原因:
° Redis的主要瓶颈不在cpu,而在内存和网络IO
° 使用单线程设计,可以简化数据库结构的设计
° 可以减少多线程锁带来的性能损耗
2. 什么是IO多线程?
既然Redis的主要瓶颈不在CPU,为什么又要引入IO多线程?Redis的整体处理流程如下图:
结合上图可知,当 socket 中有数据时,Redis 会通过系统调用将数据从内核态拷贝到用户态,供 Redis 解析用。这个拷贝过程是阻塞的,术语称作 “同步阻塞IO”,数据量越大拷贝的延迟越高,解析协议时间消耗也越大,糟糕的是这些操作都是在主线程中处理的,特别是链接数特别多的情况下,这种情况更加明显。基于以上原因,Redis作者提出了Thread/IO线程,既将接收与发送数据来使用多线程并行处理,从而降低主线程的等待时间。
二、Thread/IO整体流程及程序实现设计
1.Thread/IO整体实现思路
(1).创建一组大小为io线程个数的等待队列,用来存储客户端的网络套接字。(2).分均分配客户端网络套接字到等待队列中(3).等待线程组接收解协议完毕或者发送数据完毕(4).执行后续操作,然后跳转到第2步继续执行
2.Thread/IO涉及到的代码文件
关于IO多线程部分的代码,在src/network.c中。
3.Thread/IO整体流程图
三、关键代码流程详解
1.io线程的初始化
•Redis多线程相关线程的初始化顺序
初始化相关流程图及代码创建线程流程图
创建线程代码具体位置
•src/network.c: initThreadIO(void)
•src/network.c: IOThreadMain(void *)
2.readQueryFromClient部分
代码具体位置:src/network.c: readQueryFromClient(connection *)
由上面部分的代码,可以得知客户端要使用io多线程必须满足的条件有:
1.io多线程已经激活
2.io多线程允许read
3.没有阻塞的处理事件
4.客户端不是主节点、从节点、已分配的延迟接收客户端
3.处理读取待分配任务
•处理读取待分配任务流程图
处理读取待分配任务代码
•src/networking.c: handleClientsWithPendingReadsUsingThreads(void)
4.处理写待分配任务
写分配任务具体流程同读分配流程大同小异,看参照上图阅读具体的代码实现,代码位置:
•src/networking.c: handleClientsWithPendingWritesUsingThreads(void)
5.IO线程的动态开与关
•主线程中整体读与写的逻辑流程图
•具体代码位置:src/server.c: beforSleep(struct aeEventLoop *)
•IO线程的开关循序流程图
由上图可知,IO线程的开关,实际取决于延迟发送客户端的数量是否小于IO线程数*2,否则IO线程一般都处于阻塞状态(即使设置了IO多线程)。
•具体代码位置
•src/networking.c: handleClientsWithPendingWritesUsingThreads(void)•src/networking.c: int stopThreadedIOIfNeeded(void)•src/networking.c: startThreadedIO(void)
四、配置文件对应配置项说明
五、Thread/IO性能测试
1.测试方法与测试指标
本次测试,采用vire-benchmark工具进行压测。redis-server与vire-benchmark对应压测参数如下:
(1)redis-server
(2) vire-benchmark
2. 测试结果分析
(1) 各个命令的极限QPS
注:压测结果不考虑客户端数量,payload大小,pipeline个数,只是直观展现Redis6开启io多线程后,针对各个命令所能达到的极限QPS。
(2) IO线程数量与极限QPS的关系
在极限QPS的测试结果中,client个数基本上都为1,不具备实际意义,再结合线上实际业务情况,选取GET,client_count(50),payload(20),pipeline(50)作为结果分析的参考基准。
•
Get,client_count(50),payload(20),pipeline(50)
由上图可知,针对Get等简单命令,IO线程也并不是越多越好,这也符合代码中的自旋锁的实现逻辑。但是IO多线程,确实能够再一定程度上提升QPS,但是效果并不是很明显。
Get,client_count(50),pipeline(50)
由上图可知,IO多线程对payload较大请求,处理效果提升明显,但是当payload提高到2048左右时,提升效果不明显。
•Get,pipeline(50),bytes(20)
对于客户端数量而言,IO多线程针对多个客户端有一定提升,但是效果不是很明显。上图也可观察到,当客户端数量提高到50左右时,IO线程越多,QPS反而成下降趋势。
•Get,client_count(50),payload(20)
由上图可知,在pipeline个数较少时,qps随着IO线程数量增多缓慢增加,当pipeline个数大于50个以上,随着线程数的增加,QPS成缓慢下降趋势。
3. 测试结果总结
•IO线程总体提升效果并不是非常明显,以基准结果为例,当IO线程为4时,较单线程在同条件下,QPS提升25%左右
•IO线程,对val与pipeline较大的操作,有明显的提升,当IO线程为4时,较单线程在同条件下,QPS最大提升30%左右,但随着val的增大,提升并不明显
•对于多个客户端,IO多线程的提升并不明显
•通过测试数据,发现Redis的IO多线程,在设置为4个时可达到最佳效率,到达最高QPS
六、使用Thread/IO注意事项
•io-threads个数设置为1时,实际上还是只使用主线程
•io-threads默认只负责write,既发送数据给客户端
•io-threads的个数一旦设置,不能通过config动态设置
•当设置ssl后,io-threads将不工作
•通常io线程并不能有太大提升,除非cpu占用特别明显或者客户端链接特别多的情况下,否则不建议使用
•io线程只能是读,或者写,不存在读写并存的情况
•Io线程的开关,实际取决于延迟发送客户端的数量是否小于IO线程数*2
七、线上当前Redis6的情况
1.KCC线上目前已上线30+,集群运行稳定,如需申请,在KCC平台请注明使用Redis6版本即可
2.改造代理内核源码,覆盖Redis5/6新增的全部命令,如:bitfield_ro、stralgo、zpopmin、zpopmax等
3.改造Redis 6升级工具,支持持Redis3、4、5、6任一版本升降。
4.由于键值设计不合理以及业务特性带来的热点问题,Redis单线程版本单个实例达到请求瓶颈,切换到Redis6版本后,单实例QPS可到20~30W+,效果符合预期,降低了业务高负载时的请求延时问题
文章转自公众号:Redis开发运维实战