之前写过一篇Redis 6.0多线程的入门文章,今天来聊聊进阶的内容。这些技巧是我在实际项目中踩坑总结出来的,包括多线程调优、IO线程配置、客户端优化、持久化优化等,可能有一些你不知道的细节。

先简单回顾一下。Redis 6.0在2020年5月正式发布(写这篇文章的时候是2020年2月,Redis 6.0还在RC阶段,但我们已经在测试环境试用了),其中最大的变化就是引入了多线程IO。在Redis 6.0之前,Redis是单线程的,所有的命令执行、IO操作都在一个线程里完成。虽然单线程的Redis性能已经很强了,但在高并发场景下,IO读写成为了瓶颈,CPU的单核利用率很高,但多核没有利用起来。

Redis 6.0的多线程,不是说命令执行变成多线程了,命令执行依然是单线程的,保证了原子性和线程安全。多线程主要是用在网络IO上,也就是读取客户端的请求和写回响应,这些操作可以用多个线程并行处理,从而提升整体的吞吐量。

之前的文章里,我介绍了Redis 6.0多线程的基本原理和开启方法。今天这篇文章,主要聊聊进阶的内容,包括多线程的调优技巧、一些容易踩的坑、以及实际项目中的最佳实践。

一、多线程IO的深入理解

在说调优之前,先深入理解一下Redis 6.0多线程IO的工作原理,只有理解了原理,才能更好地调优。

Redis 6.0的多线程IO,工作流程大概是这样的:

  1. 主线程负责接收客户端的连接,把连接分配给IO线程
  2. IO线程负责读取客户端的请求数据,解析成命令
  3. 所有的命令执行,还是在主线程中串行执行(保证原子性)
  4. 命令执行完成后,把响应结果交给IO线程,由IO线程写回给客户端

所以,多线程主要加速的是网络IO的部分,也就是读取请求和写回响应。命令执行本身还是单线程的,这一点很重要,很多人误以为Redis 6.0变成了多线程执行命令,其实不是的。

为什么不把命令执行也变成多线程呢?因为Redis的很多数据结构(比如哈希表、跳表)都不是线程安全的,如果命令执行变成多线程,就需要加锁,加锁会带来性能开销,而且可能导致死锁、竞态条件等问题。Redis的设计哲学是简单、高效,所以选择了命令执行单线程、IO多线程的方案,既利用了多核,又保证了简单性和线程安全。

理解了这个原理之后,我们就知道,多线程IO主要在以下场景下有明显的性能提升:

  • 高并发、大量客户端连接的场景
  • 读写比较频繁、网络IO占比较高的场景
  • value比较大、网络传输耗时较长的场景

而在以下场景下,多线程IO的提升可能不明显,甚至可能下降:

  • 并发不高、客户端连接少的场景
  • 命令执行本身很耗时(比如大key的操作、复杂的计算),IO占比不高
  • value很小、网络传输很快,IO不是瓶颈

所以,不是所有场景都适合开启多线程IO,要根据实际情况来判断。

二、多线程IO的调优技巧

理解了原理,再来说说调优技巧。

技巧1:合理设置IO线程数

开启多线程IO,最重要的参数是io-threads,用来设置IO线程的数量。这个参数不是越大越好,要根据实际情况来设置。

Redis官方的建议是:

  • 4核CPU,设置2到3个IO线程
  • 8核CPU,设置4到6个IO线程
  • 16核CPU,设置8到12个IO线程

为什么不建议把IO线程数设置成和CPU核数一样,甚至更多呢?因为IO线程太多的话,线程之间的上下文切换会增加,反而会降低性能。而且,主线程也需要占用一个CPU核心,如果IO线程占满了所有核心,主线程就没有足够的CPU资源了。

我们在实际测试中发现,对于8核CPU的机器,设置4个IO线程的时候,性能最好,比设置6个或8个都要好。所以,建议从较小的值开始测试,逐步增加,找到最适合自己场景的配置。

另外,还有一个参数io-threads-do-reads,用来设置IO线程是否负责读取请求。默认情况下,IO线程只负责写回响应,不负责读取请求,读取请求还是由主线程来做。如果你的场景是读多写少,而且请求比较大,可以开启这个参数,让IO线程也负责读取,进一步提升性能。

io-threads 4
io-threads-do-reads yes

技巧2:绑定CPU亲和性

在高并发场景下,CPU的上下文切换会影响性能。可以通过设置CPU亲和性,把Redis的主线程和IO线程绑定到固定的CPU核心上,减少上下文切换。

在Linux上,可以用taskset命令来设置CPU亲和性:

taskset -c 0,1,2,3 redis-server /etc/redis/redis.conf

这样,Redis的所有线程就只会在0、1、2、3这四个核心上运行,不会被调度到其他核心上,减少了缓存失效和上下文切换。

不过,这个技巧只有在高并发、CPU利用率很高的场景下才有明显效果。如果CPU利用率不高,设置CPU亲和性的意义不大。

技巧3:优化客户端连接

多线程IO的性能,和客户端的连接方式也有很大关系。以下是一些客户端优化的建议:

  • 使用连接池:不要每次请求都新建连接,连接的建立和销毁是很耗时的。使用连接池,复用连接,可以大幅提升性能。
  • 控制连接数:不是连接数越多越好,连接数太多会占用大量内存,而且会增加主线程的负担。要根据实际的并发量来设置合理的连接数,一般来说,每个客户端实例保持10到20个连接就够了。
  • 使用pipeline:如果有多个连续的命令,可以用pipeline一次性发送,减少网络往返次数,提升吞吐量。
  • 避免大key:大key的读写会占用大量的网络带宽和IO时间,影响多线程IO的效果。尽量把大key拆分成小key,或者用合适的数据结构。

技巧4:合理设置TCP参数

网络IO的性能,和TCP参数也有关系。可以通过调整以下TCP参数来提升性能:

  • tcp-backlog:设置TCP的backlog大小,也就是已完成三次握手但还没被accept的连接队列大小。默认值是511,如果并发连接很多,可以调大这个值,比如1024或2048,避免连接被拒绝。
  • tcp-keepalive:设置TCP的keepalive时间,检测死连接。默认是300秒,可以根据实际情况调整。
  • 关闭TCPNODELAY:也就是开启Nagle算法,合并小的数据包,减少网络包的数量。不过,对于延迟敏感的场景,建议开启TCPNODELAY(关闭Nagle),减少延迟。

三、容易踩的坑

说了调优技巧,再说说实际使用中容易踩的坑。

坑1:以为开启多线程就一定更快

很多人开启多线程IO之后,发现性能没有提升,甚至下降了,就觉得Redis 6.0的多线程是骗人的。其实不是的,多线程IO不是万能的,它只在特定的场景下有效果。

如果你的场景是低并发、小value、命令执行很快,那IO本来就不是瓶颈,开启多线程之后,增加了线程调度的开销,性能反而可能下降。这时候,单线程的Redis反而更快。

所以,开启多线程之前,先分析一下你的场景,看看IO是不是瓶颈。如果CPU的单核利用率很高(超过80%),而且大部分时间花在网络IO上,那开启多线程会有明显提升。否则,可能不需要开启。

坑2:IO线程数设置得太多

有些人觉得,IO线程数越多越好,直接设置成和CPU核数一样,甚至更多。结果发现性能反而下降了。

原因是,IO线程太多的话,线程之间的竞争和上下文切换会增加,而且主线程的CPU资源会被挤压。Redis的主线程负责命令执行,这是最核心的部分,如果主线程没有足够的CPU资源,整体性能就会下降。

所以,IO线程数要合理设置,一般不要超过CPU核数减1(留一个核心给主线程)。而且,要通过实际测试来找到最优值,不要想当然。

坑3:大key导致多线程失效

如果你的Redis里有很多大key(比如value是几MB甚至几十MB的字符串,或者元素很多的哈希/列表),那多线程IO的效果会大打折扣。

因为大key的读写,会占用一个IO线程很长时间,导致这个线程无法处理其他请求。而且,大key的命令执行(比如删除、修改)也会占用主线程很长时间,阻塞其他命令。这时候,不管你开多少个IO线程,性能都上不去。

所以,一定要避免大key。如果有大key,想办法拆分成小key,或者用合适的数据结构。比如,一个很大的哈希,可以按一定的规则拆分成多个小哈希;一个很大的列表,可以用分页的方式存储。

坑4:持久化和多线程的冲突

Redis的持久化(RDB和AOF)会fork子进程来做,fork子进程的时候,会复制主线程的内存页表,这个过程是很耗时的,尤其是内存很大的时候。在fork的过程中,主线程是阻塞的,无法处理请求,这时候多线程IO也帮不上忙。

如果你的Redis内存很大(比如超过10GB),而且持久化很频繁,那fork的开销会很大,影响整体性能。这时候,可以考虑以下优化:

  • 合理设置持久化的频率,不要太频繁
  • 使用AOF的everysec模式,而不是always,减少fsync的次数
  • 关闭RDB,只用AOF,或者在从节点上做持久化
  • 升级到Redis 6.0的新版本,新版本对fork做了一些优化

坑5:主从复制的影响

如果你的Redis是主从架构,主节点的写操作会同步到从节点。主从复制也是通过网络IO来做的,如果从节点很多,或者网络不好,主节点的IO线程会被复制占用很多,影响正常的客户端请求。

这时候,可以考虑以下优化:

  • 控制从节点的数量,不要太多
  • 优化主从之间的网络,保证带宽和延迟
  • 使用读写分离,把读请求分散到从节点,减轻主节点的压力
  • 对于写多读少的场景,可以考虑用集群模式,把数据分散到多个主节点

四、实际项目中的最佳实践

最后,分享一些我们在实际项目中总结的最佳实践。

实践1:先压测,再配置

不要上来就开启多线程,也不要想当然地设置IO线程数。先做压力测试,了解当前的性能瓶颈在哪里,然后根据瓶颈来配置。

我们的做法是:

  1. 先用单线程的Redis做压测,记录吞吐量、延迟、CPU利用率等指标
  2. 分析瓶颈,如果是IO瓶颈,再开启多线程
  3. 开启多线程后,从2个IO线程开始,逐步增加,每次都做压测,找到最优的配置
  4. 最后,用真实的业务流量做验证,确保配置在生产环境下也有效

实践2:监控和告警

开启多线程之后,要加强监控,关注以下指标:

  • 吞吐量(ops/sec):每秒处理的命令数
  • 延迟(latency):命令的平均延迟和P99延迟
  • CPU利用率:主线程和IO线程的CPU利用率
  • 内存使用:内存的使用量和碎片率
  • 连接数:当前的客户端连接数
  • 持久化:RDB和AOF的执行时间和频率

如果发现异常(比如延迟突然升高、CPU利用率异常、内存持续增长),要及时告警,排查问题。

实践3:滚动升级,灰度验证

如果是从Redis 5.0升级到6.0,或者从单线程切换到多线程,建议用滚动升级的方式,先在一个节点上测试,确认没问题之后,再逐步推广到其他节点。

我们的做法是:

  1. 先在测试环境部署Redis 6.0,开启多线程,做充分的测试
  2. 然后在生产环境选一个从节点,升级到Redis 6.0,开启多线程,观察一段时间
  3. 确认没问题之后,再升级其他从节点
  4. 最后升级主节点(升级主节点的时候,先做一次主从切换,把主节点变成从节点,再升级)

这样,即使出现问题,影响也很小,可以快速回滚。

实践4:做好回滚准备

升级之前,一定要做好回滚的准备。比如,保留旧版本的二进制文件和配置文件,确保可以快速回滚到旧版本。如果升级之后发现了严重的问题(比如性能下降、数据丢失、兼容性问题),可以快速回滚,减少影响。

另外,升级之前要做好数据备份,虽然Redis升级一般不会丢失数据,但以防万一,还是备份一下比较好。

五、写在最后

Redis 6.0的多线程IO,是一个很重要的特性,它让Redis在高并发场景下的性能有了进一步的提升。但它不是万能的,不是所有场景都适合开启,也不是开启了就一定更快。要根据实际的场景和瓶颈,合理地配置和调优,才能发挥它的最大性能。

这篇文章里分享的技巧和坑,都是我们在实际项目中踩坑总结出来的,希望能对大家有所帮助。当然,每个项目的场景都不一样,具体的配置还是要根据实际情况来调整,最好的方式就是多测试、多监控、多优化。

Redis是一个很优秀的开源项目,它的每一个版本都在不断进步。Redis 6.0除了多线程IO,还有很多其他的新特性,比如SSL支持、ACL权限控制、RESP3协议等,这些特性也值得我们去学习和使用。

最后,希望Redis越来越好,也希望大家都能用好Redis,让它为我们的业务提供更好的服务。如果有什么问题或者不同的看法,欢迎在评论区交流。