Redis 6.0引入了多线程IO模型,性能有了大幅提升。但在实际使用中也踩了不少坑,从配置调优到客户端兼容性,从持久化到集群部署,每个环节都有需要注意的地方。今天把这些经验整理出来,分享给大家。

Redis一直以高性能著称,但它的单线程模型也一直是大家讨论的焦点。虽然单线程避免了锁竞争和上下文切换,但在高并发场景下,单个核心的处理能力终究是有上限的。Redis 6.0终于引入了多线程IO模型,在网络IO层面使用多线程来处理,而命令执行仍然是单线程的,这样既利用了多核的优势,又保持了Redis简单的编程模型。

我们团队在Redis 6.0发布之后不久,就把生产环境的Redis集群从5.0升级到了6.0,并且开启了多线程IO。升级之后性能确实有提升,但过程中也踩了不少坑。今天把这些经验分享出来。

一、多线程IO的原理

在说踩坑之前,先简单说说Redis 6.0多线程IO的原理。

Redis 6.0之前,Redis是完全单线程的:一个线程负责网络IO、命令解析、命令执行、数据持久化等所有工作。在高并发场景下,网络IO(读取客户端请求、写入响应数据)会占用大量的CPU时间,导致命令执行的时间被压缩,整体性能上不去。

Redis 6.0的多线程IO,就是把网络IO的工作交给多个线程来处理。主线程负责监听客户端连接,当有新的连接建立或者有数据可读时,主线程把这些连接分配给多个IO线程,由IO线程负责读取请求数据、解析命令、写入响应数据。而命令的执行仍然由主线程来做,这样就保证了命令执行的原子性,不需要加锁。

简单来说,多线程IO就是:多线程负责网络读写,单线程负责命令执行。这样既利用了多核CPU来处理网络IO,又保持了Redis单线程命令执行的简单性和原子性。

需要注意的是,Redis的多线程IO默认是关闭的,需要手动配置开启。配置项是io-threads,设置IO线程的数量。一般来说,IO线程数设置为CPU核心数的一半比较合适,不要超过CPU核心数,因为主线程也需要一个核心。

二、踩坑记录

了解了原理之后,说说我们在实际使用中踩的坑。

第一个坑是多线程IO的配置不当。我们一开始开启多线程的时候,把io-threads设成了8(服务器是8核),结果发现性能不但没有提升,反而下降了。排查了半天才发现,IO线程数设得太多了,导致线程之间的竞争和上下文切换开销增加,而且主线程和IO线程争抢CPU资源,反而影响了命令执行的效率。

后来我们把io-threads调整成了4(8核的一半),性能就有了明显提升。这里的经验是:IO线程数不是越多越好,要根据实际的CPU核心数和业务场景来调整。一般来说,4核CPU设2个IO线程,8核设4个,16核设6到8个比较合适。而且,如果你的QPS不是特别高(比如低于1万),其实不需要开启多线程IO,单线程就足够了,开启多线程反而可能因为线程开销导致性能下降。

第二个坑是客户端的兼容性问题。我们升级到Redis 6.0之后,有一些老的客户端出现了连接异常或者数据读取错误的问题。排查之后发现,是因为多线程IO模式下,响应数据的写入方式和单线程模式有所不同,一些老版本的客户端(尤其是自己实现的Redis客户端)没有正确处理这种情况,导致解析响应出错。

解决方案是升级客户端到支持Redis 6.0的版本,或者对于无法升级的老客户端,在服务端关闭多线程IO(可以针对特定的客户端连接关闭)。我们的做法是把所有官方客户端都升级到了最新版本,对于一些自研的老客户端,花了一些时间做了兼容性修复。这里提醒大家,升级Redis版本之前,一定要先测试所有客户端的兼容性,不要直接在生产环境升级。

第三个坑是持久化的性能问题。开启多线程IO之后,我们发现RDB快照和AOF重写的耗时增加了,而且在持久化期间,Redis的响应延迟会明显升高。排查之后发现,是因为多线程IO模式下,fork子进程进行持久化的时候,子进程和父进程的内存拷贝开销更大了,而且IO线程和持久化的IO操作争抢磁盘IO资源,导致整体性能下降。

解决方案是调整持久化的策略:第一,把RDB快照的频率降低,从原来的每5分钟一次改成每15分钟一次;第二,AOF重写的触发阈值调大,避免频繁重写;第三,把持久化操作安排在业务低峰期执行(可以用cron手动触发);第四,如果对数据丢失不敏感,可以考虑关闭RDB,只用AOF的everysec模式。调整之后,持久化对性能的影响就小了很多。

第四个坑是集群模式下的不均衡。我们的Redis集群是用Redis Cluster部署的,开启多线程IO之后,发现各个节点的CPU使用率不均衡,有些节点的CPU很高,有些很低。排查之后发现,是因为多线程IO的性能提升和节点的连接数、请求量有很大关系,而我们的集群中,有些分片的热点key比较多,请求量很大,这些节点开启多线程之后性能提升明显;而有些分片的请求量很小,多线程的开销反而导致性能略有下降。

解决方案是:第一,做好key的分布,尽量避免热点key,让请求均匀分布到各个分片;第二,对于请求量特别大的热点分片,可以单独调整该节点的IO线程数;第三,如果集群整体的QPS不是特别高,可以考虑只在主节点开启多线程IO,从节点保持单线程(因为从节点的请求量通常比较小)。调整之后,集群的整体性能和均衡性都有了改善。

第五个坑是监控指标的变化。开启多线程IO之后,我们发现原来的一些监控指标不准确了。比如,原来的instantaneousopsper_sec指标在多线程模式下统计有偏差,导致监控面板上的QPS数据不准。还有一些延迟指标,因为IO线程的引入,统计方式也有所变化。

解决方案是升级监控工具到支持Redis 6.0的版本,并且重新校准监控指标。我们用的是Prometheus + redis_exporter,升级到最新版本之后,大部分指标都正常了。还有一些自定义的监控脚本,我们根据Redis 6.0的变化做了相应的调整。这里提醒大家,升级Redis版本之后,一定要检查所有的监控和告警是否正常,不要因为监控不准而忽略了真正的问题。

三、性能调优建议

除了上面说的踩坑记录,这里再分享一些Redis 6.0多线程模式下的性能调优建议。

第一,合理设置IO线程数。如前所述,IO线程数不是越多越好,一般设为CPU核心数的一半。如果你的CPU是超线程的,按物理核心数来算。设置之后,可以用redis-benchmark做一下性能测试,找到最优的配置。

第二,确保CPU亲和性。在多核服务器上,可以把Redis的主线程和IO线程绑定到特定的CPU核心上,避免线程在不同核心之间迁移,减少上下文切换的开销。可以用taskset命令来设置CPU亲和性。比如,把Redis进程绑定到0-3号核心:taskset -c 0-3 redis-server。

第三,优化网络配置。多线程IO模式下,网络性能更加重要。可以做以下优化:开启TCPNODELAY(禁用Nagle算法,减少延迟);调整TCP缓冲区大小(net.core.rmemmax和net.core.wmemmax);开启SOREUSEPORT(允许多个线程绑定同一个端口,提高连接处理效率);如果是内网环境,可以考虑使用Unix Domain Socket来连接Redis,比TCP更快。

第四,合理使用pipeline。多线程IO提升的是网络IO的处理能力,但如果客户端的请求是串行的(发一个请求等一个响应),那么多线程的优势就发挥不出来。建议客户端使用pipeline批量发送请求,这样可以充分利用多线程IO的并发处理能力,大幅提升吞吐量。

第五,注意内存管理。多线程IO模式下,内存的使用和单线程模式基本一致,但因为IO线程的引入,会有一些额外的内存开销(每个IO线程都有自己的缓冲区)。所以在设置maxmemory的时候,要预留一些额外的内存空间,不要把内存设得太满。另外,内存淘汰策略(maxmemory-policy)要根据业务场景合理选择,常用的有allkeys-lru(所有key中LRU淘汰)和volatile-lru(设置了过期时间的key中LRU淘汰)。

四、写在最后

Redis 6.0的多线程IO是一个很重要的特性,它在保持Redis简单性的同时,充分利用了多核CPU的优势,大幅提升了高并发场景下的性能。但任何新特性都有一个磨合的过程,在实际使用中难免会遇到各种问题。

我们的经验是:升级之前做好充分的测试,包括性能测试、兼容性测试、故障恢复测试;升级之后密切关注监控指标,及时发现和解决问题;根据自己的业务场景和硬件配置,合理调整多线程相关的参数,不要盲目照搬别人的配置。

Redis是一个非常优秀的开源项目,它的每一个版本更新都带来了实实在在的改进。作为使用者,我们要做的就是了解它的原理,用好它的特性,避开它的坑,让它更好地为我们的业务服务。

希望我的这些踩坑经验和调优建议能对大家有帮助。如果有什么问题或者不同的看法,欢迎在评论区交流。