单线程的Redis一向以简洁高效著称,但也有其阿喀琉斯之踵:阻塞!单个线程在最容易产生瓶颈的网络读写(Redis大key)请求完成之前,其他所有请求都将会被阻塞,严重影响其效率,因此Redis的多线程呼声就越来越高。由于是基于内存的操作延迟非常低,所以即便是单线程模式下CPU资源也不会是的瓶颈。最容易出现瓶颈的还是网络IO操作。在Redis 6.0开始支持多线程之后,所谓的多线程也只是socket层面的多线程,核心的内存读写还是单线程模式。

弄清楚了多线程的本质之后,就会有一系列的问题,多线程会比单线程有多大的提升?设置多少个线程合适?见一些大神测试过,其结果也非常理想,但只是看看也不太过瘾,决定一试为快,本文将对Redis的多线程进行一个粗浅的测试验证。同时需要思考另外一个问题:面对多线程版本的Redis,和Redis cluster,该如何选择?

多线程Redis

redis 6.0 的“多线程”特性让很多标题党高潮连连,参考图片源自于:美图技术团队侵删,核心的线程(Execute Command)还是单线程,多线程是指网络IO(socket)读写的多线程化。
如下图所示,读写网络socket中的数据是可以用多个线程,所以Redis的多线程也叫做io thread,相关参数:“io-threads”。另一个参数是io-threads-do-reads,这里涉及另外一个细节:多线程IO主要用在请求完成后返回结果集的过程,也就是socket写操作,至于读socket,单线程本身采用的多路IO复用技术,也不会产生瓶颈,因此Redis给出了一个io-threads-do-reads 参数,决定读socket的时候是否启用多线程。其实io-threads-do-reads是否启用,对性能的影响并不大,最后会做一个验证。
Redis 6.0 多线程性能测试结果及分析_多线程性能测试

 

测试策略

本机配置:centos 7,16C+32GB内存

Redis 6.0 多线程性能测试结果及分析_Redis 6.0_02
Redis版本
Redis 6.0 多线程性能测试结果及分析_多线程性能测试_03

下面分别以1线程,2线程,4线程,6线程,8线程,10线程的配置下,同时为避免网络延迟带来的影响,redis-benchmark在Redis实例本地,测试Redis的get和set性能。

 

翻车

整个测试开始之前,经历了两次翻车才得以继续

翻车现场1
centos 7上默认的gcc是4.*版本,无法编译Redis 6.0,所以需要升级gcc,因为本机不支持yum安装,参考这里使用源码包安装,gcc编译的时候那个酸爽,本机16C+32GB内存的环境下,因为缺少某些依赖包,导致失败了几次,最终编译成功的一次,花了大概1个小时10分钟
Redis 6.0 多线程性能测试结果及分析_多线程性能测试_04

翻车现场2
没有认真读配置文件中的说明,设置io-threads后,重启Redis服务后,上来就用redis-benchmark直接怼,其结果跟单线程差不多,令人大跌眼镜。
最后还是在原始配置文件发现了这段话:
If you want to test the Redis speedup using redis-benchmark, make sure you also run the benchmark itself in threaded mode, using the --threads option to match the number of Redis threads, otherwise you'll not be able to notice the improvements.

意思是必须在redis-benchmark设置--threads参数,并且要match Redis中的线程设置,--threads参数是redis 6.0后新增的一个参数。只有加上--threads这个参数才能体现出来多线程Redis的效率。

Redis 6.0 多线程性能测试结果及分析_多线程性能测试_05

关于Thread IO的说明

经历了第二次翻车之后决定好好看一看redis.conf中关于thread io的注释信息

################################ THREADED I/O #################################

# Redis is mostly single threaded, however there are certain threaded
# operations such as UNLINK, slow I/O accesses and other things that are
# performed on side threads.
#
# Now it is also possible to handle Redis clients socket reads and writes
# in different I/O threads. Since especially writing is so slow, normally
# Redis users use pipelining in order to speed up the Redis performances per
# core, and spawn multiple instances in order to scale more. Using I/O
# threads it is possible to easily speedup two times Redis without resorting
# to pipelining nor sharding of the instance.
#
# By default threading is disabled, we suggest enabling it only in machines
# that have at least 4 or more cores, leaving at least one spare core.
# Using more than 8 threads is unlikely to help much. We also recommend using
# threaded I/O only if you actually have performance problems, with Redis
# instances being able to use a quite big percentage of CPU time, otherwise
# there is no point in using this feature.
#
# So for instance if you have a four cores boxes, try to use 2 or 3 I/O
# threads, if you have a 8 cores, try to use 6 threads. In order to
# enable I/O threads use the following configuration directive:
#
# io-threads 4
#
# Setting io-threads to 1 will just use the main thread as usual.
# When I/O threads are enabled, we only use threads for writes, that is
# to thread the write(2) syscall and transfer the client buffers to the
# socket. However it is also possible to enable threading of reads and
# protocol parsing using the following configuration directive, by setting
# it to yes:
#
# io-threads-do-reads no
#
# Usually threading reads doesn't help much.
#
# NOTE 1: This configuration directive cannot be changed at runtime via
# CONFIG SET. Aso this feature currently does not work when SSL is
# enabled.
#
# NOTE 2: If you want to test the Redis speedup using redis-benchmark, make
# sure you also run the benchmark itself in threaded mode, using the
# --threads option to match the number of Redis threads, otherwise you'll not
# be able to notice the improvements.

大概意思如下:
大多数情况下redis是以单线程的方式运行的,然而,有一些线程操作,如断开链接,耗时的I/O操作(bgsave,expired key清理之类的操作)和其他任务是在side线程(主线程fork出来的子线程)中执行的。

现在也可以在不同的I/O线程中处理Redis客户端socket读和写。由于写入(指socket写入)速度非常慢,Redis用户通常使用pipelining来提高Redis在单核上的性能,并使用多个实例的方式来扩容。使用I/O线程可以很容易地提升Redis在socket读写上的性能,而无需求助于管道或实例的分片。

Redis 6.0中默认情况下多线程被是被禁用的,建议至少有4个或更多核的机器中启用多线程,且至少留下1备用核。使用超过8个线程不大可能有太大帮助。

由于Redis实例能够充分利用CPU资源(译者注:意思是即便是单线程下,CPU并不是瓶颈),多线程I/O只有在你确实有性能问题的情况下才能提升运行效率,否则就没有必要使用这个特性。

如果你有一个4核的服务器,尝试使用2或3个I/O线程,如果是8核,尝试使用6个线程。要启用多线程I/O,请使用以下配置参数:io-threads 4

设置io-threads为1会像传统的redis一样启用单个主线程,当I/O threads被启用之后,仅仅支持写操作(译者注:指的是socket的写操作,socket的读操作使用多路io复用技术,本身也不是瓶颈)即IO线程调用syscall并将客户端缓冲区传输到socket。但是,也可以启用读写线程,使用以下配置指令进行协议解析,方法是将其设置为“yes”:io-threads-do-reads no

通常情况下,threading reads线程对性能的提升帮助并不大

注1:此配置指令不能在运行时通过配置集进行更改,只能在修改配置文件之后重启。启用SSL时,当前此特性也无效。

注2:如果你想用Redis-benchmark测试Redis的性能,务必以threaded mode的方式运行Redis-benchmark,使用--threads选项来匹配Redis线程的数量,否则无法观察到测试结果的提升。

 

测试结果及分析

如下是不同线程requests per second测试结果的横向对比
Redis 6.0 多线程性能测试结果及分析_多线程性能测试_06

Redis 6.0 多线程性能测试结果及分析_多线程性能测试_07

从中可以看到:

1,1个线程,也就是传统的单线程模式,get 操作的QPS可以达到9W左右

2,2个线程,get 操作的QPS可以达到18W左右,相比单线程有100%+的提升

3,4个线程,与2线程相比,会有20%左右的提高,但是已经没有从1个线程到2个线程翻一倍的提升了

4,6个线程,与4线程相比,没有明显的提升

5,8个线程,与4线程和6线程相比,8线程下大概有10%的提升

6,10个线程,相比效率最高的8线程,此时性能反倒是开始下降了,与4线程或者6线程的效率相当
因此在本机环境下,io-threads 4设置成2或者4个都ok,最多不超过8个,超出后性能反而会下降,同时也不能超出cpu的个数,正如配置文件中注释中说的,至少要留出一个CPU。

 

如下是不同线程下10测试结果中GET和SET的requests per second 平均值对比:
Redis 6.0 多线程性能测试结果及分析_多线程性能测试_08

 

关于io-threads-do-reads参数

上文提到过io-threads-do-reads这个参数,它是决定socket读操作是否开启多线程,Redis的socket读操作采用多路IO复用技术,本身不会成为瓶颈,所以这个参数对多线程下测试影响比较小。依旧参考这里的这个图 侵删,这个io-threads-do-reads在笔者理解起来,就是socket读的线程,是否开启影响不大。
Redis 6.0 多线程性能测试结果及分析_多线程性能测试_09

参考如下截图,在开启了两个线程的情况下,分别开启和禁用io-threads-do-reads,从整体上看,性能影响差别很小。当然专业的大神可以从源码的角度去分析。
io-threads为2,且启动io-threads-do-reads
Redis 6.0 多线程性能测试结果及分析_Redis 6.0_10

io-threads为2,且禁动io-threads-do-reads
Redis 6.0 多线程性能测试结果及分析_Redis 6.0_11

 

多线程版本的Redis和Redis Cluster的选择

redis集群有两种模式:sentinel和cluster,这里暂时先不提sentinel,来思考多线程版本的Redis和Redis Cluster的选择问题。
Redis的Cluster解决的就是扩展性和单节点单线程的阻塞隐患,如果Redis支持了多线程(目前多线程的Redis最对不建议超出8个线程),在不考虑单个节点网卡瓶颈的情况下,其实这两个问题都已经解决了,单节点可以支持多线程和充分利用多核CPU,单节点可以支持到25W QPS,还要啥自行车?
同时要考虑到Redis cluster的痛点:
1,不支持multiple操作(第三方插件也不一定稳定)。
2,cluster中每个主节点要挂一个从节点,不管这个节点是不是独立的物理节点还是单机多实例中的一个节点,终究是增加了维护成本。
3,只能使用一个数据库
4,集群自身扩容、缩容带来的一系列slot迁移等性能问题,以及集群的管理问题
这些所谓的痛点也不复存在了,所以这里就面临一个重新选择的问题:是使用多线程版本的Redis,还是使用Redis cluster?这是一个需要思考的问题。

 

疑问

关于redis-benchmark 测试时候 ./bin/redis-benchmark -d 128  -c 200 -n 1000000 -t set -q --threads 2,涉及的两个参数-c和--threads,个人是不太理解的
-c的解释是:指定并发连接数
--threads是redis-benchmark的线程数?
对此去跟老钱(素未谋面,感谢)求证了一下,说该参数是redis-benchmark客户端的epoll,服务器端的多路IO复用原理已经看得我七荤八素了,客户端也是带epoll的,还是不太理解这两者之间的关系。

 

redis-benchmark测试现场

如下是redis-benchmark测试过程中部分截图

Redis 6.0 多线程性能测试结果及分析_多线程性能测试_12

Redis 6.0 多线程性能测试结果及分析_多线程性能测试_13

Redis 6.0 多线程性能测试结果及分析_Redis 6.0_14

Redis 6.0 多线程性能测试结果及分析_Redis 6.0_15

Redis 6.0 多线程性能测试结果及分析_Redis 6.0_16