自我系统学习Redis小记-07

16 | 异步机制:如何避免单线程模型的阻塞?

1、前言

影响 Redis 性能的 5 大方面的潜在因素

Redis 内部的阻塞式操作;

CPU 核和 NUMA 架构的影响; 

Redis 关键系统配置; 

Redis 内存碎片;

Redis 缓冲区。

2、Redis 实例有哪些阻塞点?

Redis 实例在运行时,要和许多对象进行交互,包括:

客户端:网络 IO,键值对增删改查操作,数据库操作;

磁盘:生成 RDB 快照,记录 AOF 日志,AOF 日志重写;

主从节点:主库生成、传输 RDB 文件,从库接收 RDB 文件、清空数据库、加载 RDB文件;

切片集群实例:向其他实例传输哈希槽信息,数据迁移。

交互对象和各类操作的关系

3、和客户端交互时的阻塞点

网络 IO 有时候会比较慢,但是 Redis 使用了 IO 多路复用机制,避免了主线程一直处在等待网络连接或请求到来的状态,所以,网络 IO 不是导致 Redis 阻塞的因素

键值对的增删改查操作是 Redis 和客户端交互的主要部分,也是 Redis 主线程执行的主要任务,复杂度高的增删改查操作肯定会阻塞 Redis

如何定义复杂度高?看操作复杂度O(n)

第一个阻塞点:集合全量查询和聚合操作。

比如:如集合元素全量查询操作 HGETALL、SMEMBERS,以及集合的聚合统计操作,例如求交、并和差集。

第二个阻塞点:bigkey 删除操作就是 Redis 的第二个阻塞点

删除操作本质是释放键值对占用的内存,释放只是第一步,为了更加高效地管理内存空间,在应用程序释放内存时,操作系统需要把释放掉的内存块插入一个空闲内存块的链表,以便后续进行管理和再分配。

这个过程本身需要一定时间,而且会阻塞当前释放内存的应用程序,所以,如果一下子释放了大量内存,空闲内存块链表操作时间就会增加,相应地就会造成 Redis 主线程的阻塞

不同元素数量的集合在进行删除操作时所消耗时间

从这张表里,可以得出三个结论:

1)、当元素数量从 10 万增加到 100 万时,4 大集合类型的删除时间的增长幅度从 5 倍上升到了近 20 倍;

2.)、集合元素越大,删除所花费的时间就越长;

3)、当删除有 100 万个元素的集合时,最大的删除时间绝对值已经达到了 1.98s(Hash 类型)。Redis 的响应时间一般在微秒级别,所以,一个操作达到了近 2s,不可避免地会阻塞主线程。

删除操作对Redis 实例性能的负面影响很大,而且在实际业务开发时容易被忽略,要重视。

第三个阻塞点:清空数据库

清空数据库(例如 FLUSHDB 和 FLUSHALL 操作)必然也是一个潜在的阻塞风险,因为它涉及到删除和释放所有的键值对

4、和磁盘交互时的阻塞点

磁盘IO 一般是费时费力的,所以在生成rdb、aof时都是生成一个字线程执行,慢速的磁盘IO不会阻塞主线程。

但是,Redis 直接记录 AOF 日志时,会根据不同的写回策略(三种)对数据做落盘保存。

一个同步写磁盘的操作的耗时大约是 1~2ms,如果有大量的写操作需要记录在 AOF 日志中,并同步写回的话,就会阻塞主线程了。

第四个阻塞点:AOF 日志同步写回

5、主从节点交互时的阻塞点

在主从集群中,主库需要生成 RDB 文件,并传输给从库。主库在复制的过程中,创建和传输 RDB 文件都是由子进程来完成的,不会阻塞主线程。但是,对于从库来说,它在接收了RDB 文件后,需要使用 FLUSHDB 命令清空当前数据库,这就正好撞上了刚才我们分析的第三个阻塞点

此外,从库在清空当前数据库后,还需要把 RDB 文件加载到内存这个过程的快慢和RDB 文件的大小密切相关,RDB 文件越大,加载过程越慢,所以,加载 RDB 文件就成为了 Redis 的第五个阻塞点。

第四个阻塞点:加载 RDB 文件

6、切片集群实例交互时的阻塞点

部署 Redis 切片集群时,每个 Redis 实例上分配的哈希槽信息需要在不同实例间进行传递。

当需要进行负载均衡或者有实例增删时,数据会在不同的实例间进行迁移。不过,哈希槽的信息量不大,而数据迁移是渐进式执行的,所以,一般来说,这两类操作对 Redis 主线程的阻塞风险不大。

不过,如果使用了 Redis Cluster 方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞,因为 Redis Cluster 使用了同步迁移(这里回顾两个命令MOVED、ASKING)。这里暂时了解,当没有 bigkey 时,切片集群的各实例在进行交互时不会阻塞主线程。

总结下:

1)、集合全量查询和聚合操作;(不能异步)

2)、bigkey 删除;

3)、清空数据库;

4)、AOF 日志同步写;

5)、从库加载 RDB 文件。(不能异步)

7、哪些阻塞点可以异步执行?

同步和异步

客户端需要实时等待redis返回结果的,不能用异步

在这个例子中,操作 1 就不算关键路径上的操作,因为它不用给客户端返回具体数据,所以可以由后台子线程异步执行。而操作 2 需要把结果返回给客户端,它就是关键路径上的操作,所以主线程必须立即把这个操作执行完。

读操作是典型的关键路径操作,所以阻塞一“集合全量查询和聚合操作”不能异步操作。

删除操作并不需要给客户端返回具体的数据结果,所以不算是关键路径操作,所以阻塞二,阻塞三可以异步执行。

阻塞点四,AOF同步写回,为了保证可靠性,是写后日志,需要等返回,但不是返回具体结果给客户端,所以可以异步执行。

阻塞点五,从库想对客户端服务,就需要加载完RDB文件,所以是关键路径操作,不能异步。

8、异步的子线程机制

Redis 主线程启动后,会使用操作系统提供的 pthread_create 函数创建 3 个子线程,分别由它们负责 AOF 日志写操作、键值对删除以及文件关闭的异步执行

主线程通过一个链表形式的任务队列和子线程进行交互。

当收到键值对删除和清空数据库的操作时,主线程会把这个操作封装成一个任务,放入到任务队列中,然后给客户端返回一个完成信息,表明删除已经完成。

实际上是没有及时的执行删除,这里是惰性删除。等到后台子线程从任务队列中读取任务后,才开始实际删除键值对,并释放相应的内存空间。

惰性删除不会影响redis主线程。

和惰性删除类似,当 AOF 日志配置成 everysec 选项后,主线程会把 AOF 写日志操作封装成一个任务,也放到任务队列中。后台子线程读取任务后,开始自行写入 AOF 日志,这样主线程就不用一直等待 AOF 日志写完了。

异步子线程执行机制

9、小结

1)、Redis 实例运行时的 4 大类交互对象:客户端、磁盘、主从库实例、切片集群实例

2)、五大阻塞点

1)、集合全量查询和聚合操作;(不能异步)

2)、bigkey 删除;

3)、清空数据库;

4)、AOF 日志同步写;

5)、从库加载 RDB 文件。(不能异步)

3)、非关键路径操作,创建三个子线程异步操作

4)、小建议1------reids4.0之前不能异步删除,bigkey 删除时

使用SCAN操作,每次读取一小部分进行删除,如果是HASH则命令改为HSCAN

5)、小建议2------集合全量查询和聚合操作、从库加载 RDB 文件是在关键路径上,无法异步

a、集合全量查询和聚合操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算;

b、从库加载 RDB 文件:把主库的数据量大小控制在 2~4GB 左右,以保证 RDB 文件能以较快的速度加载。


17 | 为什么CPU结构也会影响Redis的性能?

1、前言

学习主流服务器的 CPU 架构,以及基于 CPU 多核架构多 CPU架构优化 Redis 性能的方法。

2、主流 CPU 架构

一个 CPU 处理器中一般有多个运行核心,我们把一个运行核心称为一个物理核,每个物理核都可以运行应用程序。每个物理核都拥有私有的一级缓存(Level 1 cache,简称 L1cache),包括一级指令缓存和一级数据缓存,以及私有的二级缓存(Level 2 cache,简称 L2 cache)。

物理核的私有缓存。它其实是指缓存空间只能被当前的这个物理核使用,其他的物理核无法对这个核的缓存空间进行数据存取。

CPU 物理核的架构

L1 和 L2 缓存是每个物理核私有的,所以,当数据或指令保存在 L1、L2 缓存时,物理核访问它们的延迟不超过 10 纳秒,速度非常快。Redis 把要运行的指令或存取的数据保存在 L1 和 L2 缓存的话,就能高速地访问这些指令和数据。

L1 和 L2 缓存的大小受限于处理器的制造技术,一般只有 KB 级别,存不下太多的数据。

如果访问到L1和L2中没有的数据,就需要访问应用程序来获取数据,应用程序访存延迟在百纳秒级别,是访问L1和L2缓存延迟的10倍,会对性能造成影响。

所以,不同的物理核还会共享一个共同的三级缓存(Level 3 cache,简称为 L3 cache)。L3 缓存能够使用的存储资源比较多,所以一般比较大,能达到几 MB 到几十 MB,这就能让应用程序缓存更多的数据。当 L1、L2 缓存中没有数据缓存时,可以访问 L3,尽可能避免访问内存。

另外,现在主流的 CPU 处理器中,每个物理核通常都会运行两个超线程,也叫作逻辑核同一个物理核的逻辑核会共享使用 L1、L2 缓存

物理核和逻辑核,以及一级、二级缓存的关系

在主流服务器上,一个 CPU 处理器会有 10 到 20 多个物理核。同时,为了提升服务器的处理能力,服务器上通常还会有多个 CPU 处理器(也称为多 CPU Socket),每个处理器有自己的物理核(包括 L1、L2 缓存),L3 缓存,以及连接的内存,同时,不同处理器间通过总线连接。

多 CPU Socket 的架构

在多 CPU 架构上,应用程序可以在不同的处理器上运行。在刚才的图中,Redis 可以先在Socket 1 上运行一段时间,然后再被调度到 Socket 2 上运行。

如果应用程序先在一个 Socket 上运行,并且把数据保存到了内存,然后被调度到另一个 Socket 上运行,此时,应用程序再进行内存访问时,就需要访问之前 Socket 上连接的内存,这种访问属于远端内存访问。和访问 Socket 直接连接的内存相比,远端内存访问会增加应用程序的延迟。

在多 CPU 架构下,一个应用程序访问所在 Socket 的本地内存和访问远端内存的延迟并不一致,所以,把这个架构称为非统一内存访问架构(Non-Uniform Memory Access,NUMA 架构)

知道了主流的 CPU 多核架构和多 CPU 架构,简单总结下 CPU 架构对应用程序运行的影响:

1)、L1、L2 缓存中的指令和数据的访问速度很快,所以,充分利用 L1、L2 缓存,可以有效缩短应用程序的执行时间;

2)、在 NUMA 架构下,如果应用程序从一个 Socket 上调度到另一个 Socket 上,就可能会出现远端内存访问的情况,这会直接增加应用程序的执行时间。

3、CPU 多核对 Redis 性能的影响

CPU核记录自身的运行时信息,同时应用程序访问最频繁的指令和数据还会被缓存到 L1、L2 缓存上,以便提升执行速度。

在多核 CPU 的场景下,一旦应用程序需要在一个新的 CPU 核上运行,那么,运行时信息就需要重新加载到新的 CPU 核上。而且,新的 CPU 核的 L1、L2 缓存也需要重新加载数据和指令,这会导致程序的运行时间增加。

4、略过一些...

5、小结

CPU 架构对 Redis 性能的影响。首先,了解了目前主流的多核CPU 架构,以及 NUMA 架构

在多核 CPU 架构下,Redis 如果在不同的核上运行,就需要频繁地进行上下文切换,这个过程会增加 Redis 的执行时间,客户端也会观察到较高的尾延迟了。所以,建议你在Redis 运行时,把实例和某个核绑定,这样,就能重复利用核上的 L1、L2 缓存,可以降低响应延迟。

为了提升 Redis 的网络性能,我们有时还会把网络中断处理程序和 CPU 核绑定。在这种情况下,如果服务器使用的是 NUMA 架构,Redis 实例一旦被调度到和中断处理程序不在同一个 CPU Socket,就要跨 CPU Socket 访问网络数据,这就会降低 Redis 的性能。所以,建议你把 Redis 实例和网络中断处理程序绑在同一个 CPU Socket 下的不同核上,这样可以提升 Redis 的运行性能

虽然绑核可以帮助 Redis 降低请求执行时间,但是,除了主线程,Redis 还有用于 RDB 和AOF 重写的子进程,以及 4.0 版本之后提供的用于惰性删除的后台线程。当 Redis 实例和一个逻辑核绑定后,这些子进程和后台线程会和主线程竞争 CPU 资源,也会对 Redis 性能造成影响。所以,我给了你两个建议:

1)、如果你不想修改 Redis 代码,可以把按一个 Redis 实例一个物理核方式进行绑定,这样,Redis 的主线程、子进程和后台线程可以共享使用一个物理核上的两个逻辑核

2)、如果你很熟悉 Redis 的源码,就可以在源码中增加绑核操作,把子进程和后台线程绑到不同的核上,这样可以避免对主线程的 CPU 资源竞争。

Redis 的低延迟是我们永恒的追求目标,而多核 CPU 和 NUMA 架构已经成为了目前服务器的主流配置。

自我总结:

1、频繁切换:redis运行实例和某个核绑定

2、redis实例和网络中断处理程序绑定在同一个cpu的不同核

3、子线程竞争cpu,按一个redis实例一个物理核绑定,主、子共享一个物理核上的两个逻辑核

你可能感兴趣的:(自我系统学习Redis小记-07)