欢迎来到爱学习爱分享,在这里,你会找到许多有趣的技术 : )

Redis 6.0 解密(一):Thread/IO 多线程

开发者头条 366℃

一、背景

目前快手有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 *)

void readQueryFromClient(connection *conn) {
    client *c = connGetPrivateData(conn);
    int nread, readlen;
    size_t qblen;

    /* Check if we want to read from the client later when exiting from
     * the event loop. This is the case if threaded I/O is enabled. */
    if (postponeClientRead(c)) return;

  /

   /* 单线程解析部分代码 */
   ...
}
int postponeClientRead(client *c) {
    if (io_threads_active &&
        server.io_threads_do_reads &&
        !ProcessingEventsWhileBlocked &&
        !(c->flags & (CLIENT_MASTER|CLIENT_SLAVE|CLIENT_PENDING_READ)))
    {
        c->flags |= CLIENT_PENDING_READ;
        listAddNodeHead(server.clients_pending_read,c);
        return 1;
    } else {
        return 0;
    }
}

由上面部分的代码,可以得知客户端要使用io多线程必须满足的条件有:

1.io多线程已经激活2.io多线程允许read3.没有阻塞的处理事件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)

四、配置文件对应配置项说明

配置项 说明 默认值 备注
io-threads 配置redis的io线程个数 4 最大不超过128,且默认不开启
io-threads-do-reads io线程负责read no 默认io线程只负责发送数据,既wirte数据返回给客户端

五、Thread/IO性能测试

1.测试方法与测试指标

本次测试,采用vire-benchmark工具进行压测。redis-server与vire-benchmark对应压测参数如下:

(1)redis-server

配置 说明
io-threads io线程的数量 [1,2,3,4,5,6,7,8]
io-threads-do-reads io线程是否负责接收数据 yes

(2) vire-benchmark

压测项
客户端数量 [1,10,50]
payload大小 [20,64,256,1024,2048,8192]
pipeline个数 [10,50,100,500]
压测命令 get,set,mget_10,mget_100,mset,lpush,rpush,lpop,rpop,lrange_10,lrange_100,sadd,spop,hset,
hincrby,hget,hmset,hmget,hgetall,zadd,zrem,pfadd,pfcount

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)

描述 1 2 3 4 5 6 7 8
payload(20) 1234567 1282051 1315789 1562499 1204819 1408450 1265822 1282051
payload(64) 1250000 1234567 1315789 1333333 1176470 1388889 1333333 1428571
payload(256) 1123595 970873 1190476 1351351 1369863 1298701 1369863 1333333
payload(1024) 694444 806451 757575 793650 847457 833333 900900 862069
payload(2048) 431034 357142 381679 400000 446428 442477 485436 467289
payload(4096) 218818 232558 264550 247524 255754 237529 239808 265252

由上图可知,IO多线程对payload较大请求,处理效果提升明显,但是当payload提高到2048左右时,提升效果不明显。

Get,pipeline(50),bytes(20)

描述 1 2 3 4 5 6 7 8
client(1) 1408450 1388889 1449275 1470588 1515151 1515151 1538461 1492537
client(20) 1408450 1408450 1315789 1351351 1515151 1449275 1538461 1449275
client(50) 1234567 1282051 1315789 1562499 1204819 1408450 1265822 1282051

对于客户端数量而言,IO多线程针对多个客户端有一定提升,但是效果不是很明显。上图也可观察到,当客户端数量提高到50左右时,IO线程越多,QPS反而成下降趋势。

Get,client_count(50),payload(20)

描述 1 2 3 4 5 6 7 8
pipeline(10) 757575 869565 1010101 934579 917431 1123595 1010101 1052631
pipeline(50) 1234567 1282051 1315789 1562499 1204819 1408450 1265822 1282051
pipeline(100) 1470588 1298701 1369863 1408450 1250000 1369863 1428571 1298701

由上图可知,在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的情况

注:KCC是快手内部Redis管控平台,目前管理70万Redis、Memcache实例,3000+ElasticSearch节点。由于实例数量庞大,热点问题不可避免,为此上Redis 6也是刚需。

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开发运维工程师、KV存储开发工程师、ElasticSearch工程师

转载请注明:爱学习爱分享 » Redis 6.0 解密(一):Thread/IO 多线程

喜欢 (0)or分享 (0)