一、背景

目前快手有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的整体处理流程如下图:

redisTemplate在多线程环境下的线程安全_多线程每个线程默认初始化模型

结合上图可知,当 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整体流程图

redisTemplate在多线程环境下的线程安全_多线程_02

三、关键代码流程详解

1.io线程的初始化

•Redis多线程相关线程的初始化顺序

redisTemplate在多线程环境下的线程安全_Redis_03

•初始化相关流程图及代码创建线程流程图

redisTemplate在多线程环境下的线程安全_Redis_04

•创建线程代码具体位置

•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.处理读取待分配任务

•处理读取待分配任务流程图

redisTemplate在多线程环境下的线程安全_Redis_05

•处理读取待分配任务代码

•src/networking.c: handleClientsWithPendingReadsUsingThreads(void)

4.处理写待分配任务

写分配任务具体流程同读分配流程大同小异,看参照上图阅读具体的代码实现,代码位置:

•src/networking.c: handleClientsWithPendingWritesUsingThreads(void)

5.IO线程的动态开与关

•主线程中整体读与写的逻辑流程图

redisTemplate在多线程环境下的线程安全_多线程每个线程默认初始化模型_06

•具体代码位置:src/server.c: beforSleep(struct aeEventLoop *)

•IO线程的开关循序流程图

redisTemplate在多线程环境下的线程安全_客户端_07

由上图可知,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。

redisTemplate在多线程环境下的线程安全_Redis_08

(2) IO线程数量与极限QPS的关系

在极限QPS的测试结果中,client个数基本上都为1,不具备实际意义,再结合线上实际业务情况,选取GET,client_count(50),payload(20),pipeline(50)作为结果分析的参考基准。

Get,client_count(50),payload(20),pipeline(50)

redisTemplate在多线程环境下的线程安全_多线程每个线程默认初始化模型_09

由上图可知,针对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

redisTemplate在多线程环境下的线程安全_多线程每个线程默认初始化模型_10

由上图可知,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

redisTemplate在多线程环境下的线程安全_客户端_11

对于客户端数量而言,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

redisTemplate在多线程环境下的线程安全_多线程_12

由上图可知,在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+,效果符合预期,降低了业务高负载时的请求延时问题