揭秘Redis影响性能的5个方面:
- Redis内存的阻塞操作
- CPU核和NUMA架构的影响
- Redis关键系统配置
- Redis内存碎片
- Redis缓冲区
先来分析一下Redis潜在的阻塞点有哪些?
- 客户端:网络IO,键值对增删改查操作,数据库操作
- 磁盘:生成RDB快照,记录AOF日志,AOF日志重写
- 主从节点:主库生成、传输RDB快照,从库接收RDB快照、清空数据库、加载RDB快照
- 切换集群实例:向其他实例传输哈希信息,数据迁移
客户端
网络IO比较慢时在阻塞IO模型下会导致性能问题。Redis使用了多路复用机制,避免了主线程一直等待,网络造成的阻塞很不明显。
键值对操作对于复杂度比较高的O(N)的操作会比较缓慢,阻塞主线程。如集合全查HGETALL、SMEMBERS,或者集合的全查和聚合统计,交并差复杂度比较高。集合自身的删除操作本质是释放键值对占用内存空间。 操作系统释放内存是为了更高效的管理内存,释放内存块,操作系统会把它记录到空闲内存块的链表,以便后续进行管理再分配。如果一下释放了大量内存,空闲内存块链表操作时间就会增加,造成Redis主线程的阻塞。
这里释放大量内存,一个可能性比较大的场景就是大bigkey删除。下图是不同集合删除大量元素消耗时间:
得出了三个规律:
- 10w~100w 数据,删除时间增长了5倍到20倍
- 元素越大,删除耗时越大
- 删除元素达到100w时,耗时接近2s,不可避免阻塞主线程
除此之外,数据库FLUSHDB和FLUSHALL清空数据库命令也会删除释放键值对。
磁盘
磁盘IO一直都是性能瓶颈。需要重点关注。Redis设计使用子进程的方式来生成RDB快照,AOF日志重写,以防止慢速的磁盘IO就不会阻塞主线程。
AOF日志除了重写以后还有正常执行命令,记录AOF日志,会根据不同的写回策略对数据落盘保存。 每一个命令的写磁盘操作耗时大约是1~2ms。如果有大量写操作需要记录在AOF日志,并同步写回,阻塞主线程。AOF日志同步写
同步策略Redis预设了三种:
- appendfsync always 每次写操作都进行同步 性能最差
- appendfsync everysec 每秒Redis同步一次写操作 性能中
- appendfsync no Redis不进行主动刷盘,刷盘依靠操作系统 性能最好
主从交互的阻塞点,主库在复制过程中需要fork进程生成RDB快照,并传输给从库。只有fork会阻塞主线程,根据这点的优化,切记不可让主节点挂载太多从节点。从库清空数据库还需要把RDB快照加载到内存里,RDB越大加载越缓慢,故而主Redis节点不可使用太大的内存,建议2~6G。如需要扩大内存,考虑使用切片方案。
切片集群
Redis切片集群存在哈希槽信息和数据迁移的操作,由于都是渐进式执行,阻塞风险不大。但仍需要注意bigkey迁移造成的阻塞。
Redis为了性能做了哪些优化?
上述阻塞点有的可以使用异步操作来进行优化。如何判断是否可以使用异步操作优化,需要确认操作是否是Redis主线程上的关键路径。
上图里左边操作1不用给客户端返回具体数据,主线程可以委托后台子线程执行,不等子线程执行完成直接返回。右边操作2则需要等待返回,把操作1不需要返回的部分委托子线程执行。
对于Redis来说读操作是典型的关键路径操作。
因此聚合查询均无法利用异步操作,而应该减小数据集。或利用更专业的列式存储来做这种类似BI(Business Intelligence)统计的场景。
同样的bigkey删除和清空数据并不在关键路径上,可以使用后台子线程来异步执行。
AOF日志同步写,根据策略可以实现实例等待或启动子线程来执行AOF日志同步写。
从库加载RDB属于从库操作的关键路径,必须从库主线程来执行。
异步的子线程机制
Redis主线程启动,操作系统提供了pthread_create创建3个子线程来负责AOF日志写操作,键值对删除以及文件关闭的异步执行。
主线程通过一个链表的任务队列和子线程交互,这样就可以增加吞吐量。
Redis删除执行的是异步删除也就是惰性删除,客户端执行删除只是标记了数据为删除,待元素再次被访问才真正执行内存释放。除此之外,后台还有一个子线程来定时执行释放内存的操作。同样的操作还有AOF写和文件关闭。
异步执行键值对和数据库清空操作是Redis4.0提供的:
- 标记删除键值对 UNLINK
- 清空数据库 FLUSHDB FLUSHALL ASYNC
CPU结构对Redis性能的影响。很多人认为Redis和CPU关系简单,CPU快则Redis处理就快。这认知很片面。
首先CPU有多核和多CPU架构,也会对Redis性能造成影响。主流的CPU架构由一个CPU处理器中有多个运行的核心,每个核心称为一个物理核,拥有私有的一级缓存(L1)包含一级指令缓存和一级数据缓存。包含私有二级缓存(L2) L2缓存实际是L1缓存和主内存之间的缓冲器。
不同物理核还会共享一个三级缓存(L3),三级级别的内存访问速度,空间分别差一个数量级。另外主流的CPU处理器通常回运行两个超线程也叫逻辑核。
单CPU多核的架构是简单性能好的方案,但成本高.故而多数服务器CPU厂商会平衡成本和性能,采用多CPU多核的方案。但是CPU需要CPU socket利用总线进行数据交换。
如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。这种架构我们也叫Non-Uniform Memory Access NUMA架构。
context switch 是指线程的上下文切换,这里的上下文就是线程的运行时信息。在 CPU 多核的环境中,一个线程先在一个 CPU 核上运行,之后又切换到另一个 CPU 核上运行,这时就会发生 context switch。
多核CPU环境下对Redis性能调优的办法:
方案一:一个Redis实例对应绑一个物理核
taskset -c 0 ./redis-server
在 CPU 的 NUMA 架构下,当网络中断处理程序、Redis 实例分别和 CPU 核绑定后,就会有一个潜在的风险:如果网络中断处理程序和 Redis 实例各自所绑的 CPU 核不在同一个 CPU Socket 上,那么,Redis 实例读取网络数据时,就需要跨 CPU Socket 访问内存,这个过程会花费较多时间
为了避免 Redis 跨 CPU Socket 访问网络数据,我们最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket 上
需要注意的是CPU核编号并不是顺序的,应该使用lscpu命令核查看NUMA架构下的CPU物理核以及逻辑核分布,这样才不会绑错核。
绑核的风险,多线程运行在单线程上会造成争抢CPU資源,造成主线程阻塞,Redis请求延迟增加。