redis核心技术与实战(三) 性能篇

redis核心技术与实战(三) 性能篇

文章目录


影响redis性能主要有以下部分:

  1. Redis 内部的阻塞式操作;
  2. CPU核和NUMA架构
  3. Redis关键系统配置
  4. Redis内存碎片
  5. Redis缓冲区

img

下面一个个来介绍这些地方

1.《redis 有哪些阻塞点?》

redis实例主要交互的对象有以下几点,我们依据下面这些点看看redis有哪些阻塞操作:

  1. 客户端交互:网络IO,增删改查,数据库操作
  2. 磁盘交互: AOF 同步磁盘,AOF重写,RDB模式持久化
  3. 从库交互: 数据同步,RDB文件生成,RDB文件传输,清空数据库, 从库加载RDB文件
  4. 切片集群交互:向其他实例传输哈希槽信息,数据迁移。

1. reids阻塞点分析

1.客户端交互

a. redis实例与客户端网络IO交互会阻塞redis吗?

多个客户端 可以同时与 redis 交互,竞争redis 资源, 由于redis 使用了IO多路复用的线程模式,使redis 不会等待阻塞在某一个客户端,不响应与其他客户端 的交互,所以 redis与客户端的 IO 操作 不是 阻塞点

b. 读写操作

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

这些操作往往时间复杂度是O(n),所以这是

另外,大量数据删除操作,也是redis 的另一个阻塞点;

因为 redis删除操作涉及 内存空间的释放和管理,释放内存只是redis的第一步,为了更好的管理规划内存空间,在释放内存时,操作系统会维护一个内存块链表,把释放的内存空间插入空闲的内存块链表,以便后续管理使用;

当有大量内存被释放时,空闲的内存块链表操作时间就会增加,可能会造成redis主线程的阻塞;

所以,

2.磁盘交互

RDB快照和AOF重写都 是 redis fork出来的子线程执行的,只会在fork子线程的时候阻塞主线程, RDB和AOF重写都不会造成阻塞;

但是,redis AOF模式下 有三种同步落盘操作:NO,every seconds , 同步写日志;

NO,every seconds 都是异步执行的 ,同步写日志比较特殊,不靠异步子线程完成

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

所以,

3.主从交互

生成RDB文件,RDB文件传输都是 redis子线程来完成的,不阻塞;

对于从库来说, 需要主线程 加载RDB文件后,才能执行以后的 数据同步操作,

所以,

4.切片集群

**数据迁移:**当我们部署 Redis 切片集群时,每个 Redis 实例上分配的哈希槽信息需要在不同实例间进行传递,同时,当需要进行负载均衡或者有实例增删时,数据会在不同的实例间进行迁移。不过,哈希槽的信息量不大,而数据迁移是渐进式执行的,所以,一般来说,这两类操作对 Redis 主线程的阻塞风险不大。

但是,如果你使用了 Redis Cluster 方案,而且同时正好迁移的是 bigkey 的话,就会造成主线程的阻塞,因为 Redis Cluster 使用了同步迁移,所以, Redis Cluster 中 大量bigkey数据迁移,可能导致 主线程;

2.关键路径与非关键路径

总结一下,有哪些阻塞点:

  1. 集合全量查询和聚合操作;

  2. bigkey 删除和清空数据库;

  3. AOF 日志同步写;

  4. 从库加载 RDB 文件。

redis解决方案:

redis提供异步线程机制,那么以上所有阻塞点都可以用异步执行吗?我们来分析下;

在这之前我们先来看看什么是 关键路径 ,什么是非关键路径;

关键路径: 客户端需要等待redis返回具体结果,并根据结果做出操作 的是关键路径,如:读操作;

非关键路径:反之,客户端不关心 返回结果,不用给客户端返回具体数据的 操作就是非关键路径,如:删除操作;

img

集合全量查询和聚合操作: 需要给客户端返回具体结果, 关键路径;

bigkey 删除和清空数据库: 非关键路径

AOF 日志同步写:为了保证数据可靠性,Redis 实例需要保证 AOF 日志中的操作记录已经落盘,这个操作虽然需要实例等待,但它并不会返回具体的数据结果给实例。 所以,它是 非关键路径。

从库加载 RDB 文件: 库要想对客户端提供数据存取服务,就必须把 RDB 文件加载完成。 所以,它是关键路径;

3.redis异步子线程机制

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

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

但实际上,这个时候删除还没有执行,等到后台子线程从任务队列中读取任务后,才开始实际删除键值对,并释放相应的内存空间。因此,我们把这种异步删除也称为惰性删除(lazy free)。此时,删除或清空操作不会阻塞主线程,这就避免了对主线程的性能影响。

异步子线程机制:

img

注意点:异步的键值对删除和数据库清空操作是 Redis 4.0 后提供的功能,Redis 也提供了新的命令来执行这两个操作:

  1. 键值对删除:当你的集合类型中有大量元素(例如有百万级别或千万级别元素)需要删除时,我建议你使用 UNLINK 命令。

  2. 清空数据库:可以在 FLUSHDB 和 FLUSHALL 命令后加上 ASYNC 选项,这样就可以让后台子线程异步地清空数据库,如下所示:

    FLUSHDB ASYNC
    FLUSHALL AYSNC
    

4.建议与扩展

1.建议
  1. 集合全量查询和聚合操作:可以使用 SCAN 命令,分批读取数据,再在客户端进行聚合计算;
  2. 从库加载 RDB 文件:把主库的数据量大小控制在 2~4GB 左右,以保证 RDB 文件能以较快的速度加载。
2.扩展

我们今天学习了关键路径上的操作,你觉得,Redis 的写操作(例如 SET、HSET、SADD 等)是在关键路径上吗?

需要客户端根据业务需要来区分:

1、如果客户端依赖操作返回值的不同,进而需要处理不同的业务逻辑,那么HSET和SADD操作算关键路径,而SET操作不算关键路径。因为HSET和SADD操作,如果field或member不存在时,Redis结果会返回1,否则返回0。而SET操作返回的结果都是OK,客户端不需要关心结果有什么不同。

2、如果客户端不关心返回值,只关心数据是否写入成功,那么SET/HSET/SADD不算关键路径,多次执行这些命令都是幂等的,这种情况下可以放到异步线程中执行。

3、但是有种例外情况,如果Redis设置了maxmemory,但是却没有设置淘汰策略,这三个操作也都算关键路径。因为如果Redis内存超过了maxmemory,再写入数据时,Redis返回的结果是OOM error,这种情况下,客户端需要感知有错误发生才行。

注意:客户端经常会阻塞等待发送的命令返回结果,在上一个命令还没有返回结果前,客户端会一直等待,直到返回结果后,才会发送下一个命令。此时,即使我们不关心返回结果,客户端也要等到写操作执行完成才行。所以,在不关心写操作返回结果的场景下,可以对 Redis 客户端做异步改造。具体点说,就是使用异步线程发送这些不关心返回结果的命令,而不是在 Redis 客户端中等待这些命令的结果。

2.《为什么CPU结构也会影响Redis的性能?》

1.CPU多核架构

L1包括一级指令缓存和一级数据缓存;

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

一个CPU有多个物理核(运行核心),每个物理核有自己私有的的L1,12缓存 ,这些缓存一般只有几kb,但是访问效率确是 纳秒级别,一般不超过 10纳秒;

如果 L1、L2 缓存中没有所需的数据,应用程序就需要访问内存来获取数据。

而应用程序的访存延迟一般在 百纳秒级别,是访问 L1、L2 缓存的延迟的近 10 倍,不可避免地会对性能造成影响。

所以,不同的物理核还会共享一个共同的三级缓存(Level 3 cache,简称为 L3 cache)

为了平衡cpu和内存之间的差异,引入了 L3缓存(也就是CPU告诉缓存区),三级缓存不但平衡cpu L1,L2缓存与内内存访问速度的差异,而且提升了 访问容量,三级缓存容量可达到 几MB,甚至 几十MB ;

而且,CPU内 多个物理核 之间共享 L3三级缓存;

另外:现在主流的 CPU 处理器中, 每个物理核 内部有两个逻辑核(超级线程),他们共享 物理核私有的L1,L2缓存;

下面看下 主流CPU架构图:

img

2.CPU多核架构对Redis 性能的影响

在一个 CPU 核上运行时,应用程序需要记录自身使用的软硬件资源信息(例如栈指针、CPU 核的寄存器值等),我们把这些信息称为运行时信息

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

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

当 context switch 发生后,Redis 主线程的运行时信息需要被重新加载到另一个 CPU 核上,而且,此时,另一个 CPU 核上的 L1、L2 缓存中,并没有 Redis 实例之前运行时频繁访问的指令和数据,所以,这些指令和数据都需要重新从 L3 缓存,甚至是内存中加载。这个重新加载的过程是需要花费一定时间的。而且,Redis 实例需要等待这个重新加载的过程完成后,才能开始处理请求,所以,这也会导致一些请求的处理时间增加。

在 CPU 多核场景下,Redis 实例被频繁调度到不同 CPU 核上运行的话,那么,对 Redis 实例的请求处理时间影响就更大了。每调度一次,一些请求就会受到运行时信息、指令和数据重新加载过程的影响,这就会导致某些请求的延迟明显高于其他请求。

所以,我们要避免 Redis 总是在不同 CPU 核上来回调度执行。于是,我们尝试着**把 Redis 实例和 CPU 核绑定了,让一个 Redis 实例固定运行在一个 CPU 核上。**我们可以使用 taskset 命令把一个程序绑定在一个核上运行。比如说,我们执行下面的命令,就把 Redis 实例绑在了 0 号核上,其中,“-c”选项用于设置要绑定的核编号。

taskset -c 0 ./redis-server

3.多CPU架构:NUMA

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

img

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

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

4.NUMA架构对redis性能影响

在实际应用 Redis 时,我经常看到一种做法,为了提升 Redis 的网络性能,把操作系统的网络中断处理程序和 CPU 核绑定。

这个做法可以避免网络中断处理程序在不同核上来回调度执行,的确能有效提升 Redis 的网络处理性能。

但是,网络中断程序是要和 Redis 实例进行网络数据交互的,一旦把网络中断程序绑核后,我们就需要注意 Redis 实例是绑在哪个核上了,这会关系到 Redis 访问网络数据的效率高低。

:网络中断处理程序从网卡硬件中读取数据,并把数据写入到操作系统内核维护的一块内存缓冲区。内核会通过 epoll 机制触发事件,通知 Redis 实例,Redis 实例再把数据从内核的内存缓冲区拷贝到自己的内存空间,如下图所示:

img

**潜在的风险:**如果网络中断处理程序和 Redis 实例各自所绑的 CPU 核不在同一个 CPU Socket 上,那么,Redis 实例读取网络数据时,就需要跨 CPU Socket 访问内存,这个过程会花费较多时间。

所以,为了避免 Redis 跨 CPU Socket 访问网络数据,我们最好把网络中断程序和 Redis 实例绑在同一个 CPU Socket 上

img

并不是先把一个 CPU Socket 中的所有逻辑核编完,再对下一个 CPU Socket 中的逻辑核编码,而是先给每个 CPU Socket 中每个物理核的第一个逻辑核依次编号,再给每个 CPU Socket 中的物理核的第二个逻辑核依次编号。

假设有 2 个 CPU Socket,每个 Socket 上有 6 个物理核,每个物理核又有 2 个逻辑核,总共 24 个逻辑核。我们可以执行 *lscpu 命令*查看到这些核的编号:

lscpu

Architecture: x86_64
...
NUMA node0 CPU(s): 0-5,12-17
NUMA node1 CPU(s): 6-11,18-23
...

可以看到,NUMA node0 的 CPU 核编号是 0 到 5、12 到 17。其中,0 到 5 是 node0 上的 6 个物理核中的第一个逻辑核的编号,12 到 17 是相应物理核中的第二个逻辑核编号。NUMA node1 的 CPU 核编号规则和 node0 一样。

5.绑核的好处和坏处,以及解决方案

1.好处和坏处

好处:

  1. 在 CPU 多核的场景下,用 taskset 命令把 Redis 实例和一个核绑定,可以减少 Redis 实例在不同核上被来回调度执行的开销,避免较高的尾延迟;

  2. 在多 CPU 的 NUMA 架构下,如果你对网络中断程序做了绑核操作,建议你同时把 Redis 实例和网络中断程序绑在同一个 CPU Socket 的不同核上,这样可以避免 Redis 跨 Socket 访问内存中的网络数据的时间开销。

坏处:

把 Redis 实例绑到一个 CPU 逻辑核上时,就会导致子进程、后台线程和 Redis 主线程竞争 CPU 资源,一旦子进程或后台线程占用 CPU 时,主线程就会被阻塞,导致 Redis 请求延迟增加。

2.解决方案

a.一个 Redis 实例对应绑一个物理核

按照上面的逻辑核编号,0,12 应该在同一个物理核内,我们可以把一个redis实例绑定一个物理核:

taskset   -c 0,12 ./redis-server

把 Redis 实例和物理核绑定,可以让主线程、子进程、后台线程共享使用 2 个逻辑核,可以在一定程度上缓解 CPU 资源竞争。但是,因为只用了 2 个逻辑核,它们相互之间的 CPU 竞争仍然还会存在。如果你还想进一步减少 CPU 竞争,我再给你介绍一种方案。

b.修改redis源码

通过编程实现绑核时,要用到操作系统提供的 1 个数据结构 cpu_set_t 和 3 个函数 CPU_ZERO、CPU_SET 和 sched_setaffinity,我先来解释下它们。

  1. cpu_set_t 数据结构:是一个位图,每一位用来表示服务器上的一个 CPU 逻辑核。
  2. CPU_ZERO 函数:以 cpu_set_t 结构的位图为输入参数,把位图中所有的位设置为 0。
  3. CPU_SET 函数:以 CPU 逻辑核编号和 cpu_set_t 位图为参数,把位图中和输入的逻辑核编号对应的位设置为 1。
  4. sched_setaffinity 函数:以进程 / 线程 ID 号和 cpu_set_t 为参数,检查 cpu_set_t 中哪一位为 1,就把输入的 ID 号所代表的进程 / 线程绑在对应的逻辑核上。

那么,怎么在编程时把这三个函数结合起来实现绑核呢?

很简单,我们分四步走就行。

第一步:创建一个 cpu_set_t 结构的位图变量;

第二步:使用 CPU_ZERO 函数,把 cpu_set_t 结构的位图所有的位都设置为 0;

第三步:根据要绑定的逻辑核编号,使用 CPU_SET 函数,把 cpu_set_t 结构的位图相应位设置为 1;

第四步:使用 sched_setaffinity 函数,把程序绑定在 cpu_set_t 结构位图中为 1 的逻辑核上。

对于 Redis 来说,它是在 bio.c 文件中的 bioProcessBackgroundJobs 函数中创建了后台线程。bioProcessBackgroundJobs 函数类似于刚刚的例子中的 worker 函数,在这个函数中实现绑核四步操作,就可以把后台线程绑到和主线程不同的核上了。和给线程绑核类似,当我们使用 fork 创建子进程时,也可以把刚刚说的四步操作实现在 fork 后的子进程代码中,示例代码如下:

int main(){
   //用fork创建一个子进程
   pid_t p = fork();
   if(p < 0){
      printf(" fork error\n");
   }
   //子进程代码部分
   else if(!p){
      cpu_set_t cpuset;  //创建位图变量
      CPU_ZERO(&cpu_set); //位图变量所有位设置0
      CPU_SET(3, &cpuset); //把位图的第3位设置为1
      sched_setaffinity(0, sizeof(cpuset), &cpuset);  //把程序绑定在3号逻辑核
      //实际子进程工作
      exit(0);
   }
   ...
}

对于 Redis 来说,生成 RDB 和 AOF 日志重写的子进程分别是下面两个文件的函数中实现的。

  1. rdb.c 文件:rdbSaveBackground 函数;
  2. aof.c 文件:rewriteAppendOnlyFileBackground 函数。

这两个函数中都调用了 fork 创建子进程,所以,我们可以在子进程代码部分加上绑核的四步操作。

3.《响应波动延时:如何应对redis变慢?》

1.redis真的变慢了吗?

redis是否变慢需要根据 Redis 的响应延迟 与 redis实例的 基线性能比较 来判断;

大部分时候,Redis 延迟很低,但是在某些时刻,有些 Redis 实例会出现很高的响应延迟,甚至能达到几秒到十几秒,不过持续时间不长,这也叫延迟“毛刺”。当你发现 Redis 命令的执行时间突然就增长到了几秒,基本就可以认定 Redis 变慢了。

1.怎么测试redis的基线性能呢?

所谓的基线性能呢,也就是一个系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定。

从 2.8.7 版本开始,<!–redis-cli 命令提供了–intrinsic-latency 选项,–>**可以用来监测和统计测试期间内的最大延迟,这个延迟可以作为 Redis 的基线性能。**一般情况下,运行 120 秒就足够监测到最大延迟了,所以,我们可以把参数设置为 120。

./redis-cli --intrinsic-latency 120
Max latency so far: 17 microseconds.
Max latency so far: 44 microseconds.
Max latency so far: 94 microseconds.
Max latency so far: 110 microseconds.
Max latency so far: 119 microseconds.

36481658 total runs (avg latency: 3.2893 microseconds / 3289.32 nanoseconds per run).
Worst run took 36x longer than the average latency.

可以看出当前redis实例的最大延时是119 毫秒。

2. redis网络延时 测试

如果你想了解网络对 Redis 性能的影响,一个简单的方法是用 iPerf 这样的工具,测量从 Redis 客户端到服务器端的网络延迟。如果这个延迟有几十毫秒甚至是几百毫秒,就说明,Redis 运行的网络环境中很可能有大流量的其他应用程序在运行,导致网络拥塞了。这个时候,你就需要协调网络运维,调整网络的流量分配了。

2.如何应对redis变慢?

redis变慢排查我们可以从三个方面入手:

  1. redis 自身特性
  2. redis 文件系统
  3. 操作系统

img

1.redis 自身特性导致 变慢
1.慢查询

Value 类型为 String 时,GET/SET 操作主要就是操作 Redis 的哈希表索引。这个操作复杂度基本是固定的,即 O(1)。但是,当 Value 类型为 Set 时,SORT、SUNION/SMEMBERS 操作复杂度分别为 O(N+M*log(M)) 和 O(N)。其中,N 为 Set 中的元素个数,M 为 SORT 操作返回的元素个数。这个复杂度就增加了很多。

可以通过 Redis 日志,或者是 latency monitor 工具,查询变慢的请求;

解决慢查询:

  1. 用其他高效命令代替。比如说,如果你需要返回一个 SET 中的所有成员时,不要使用 SMEMBERS 命令,而是要使用 SSCAN 多次迭代返回,避免一次返回大量数据,造成线程阻塞
  2. 当你需要执行排序、交集、并集操作时,可以在客户端完成,而不要用 SORT、SUNION、SINTER 这些命令,以免拖慢 Redis 实例。
  3. KEYS 命令需要遍历存储的键值对,所以操作延时高,KEYS 命令一般不被建议用于生产环境中
2. 大量key 同时过期

过期 key 的自动删除机制,默认情况下,Redis 每 100 毫秒会删除一些过期 key:

  1. 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的 key 全部删除;
  2. 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下。

ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 是 Redis 的一个参数,默认是 20;

删除操作是阻塞的(Redis 4.0 后可以用异步线程机制来减少阻塞影响),频繁使用带有相同时间参数的 EXPIREAT 命令设置过期 key,这就会导致,在同一秒内有大量的 key 同时过期。那么就会导致一直执行 第二步,阻塞redis

尽量设置不同的过期时间,可以在设置时添加一个随机数。

2.redis文件系统

Redis 会持久化保存数据到磁盘,这个过程要依赖文件系统来完成,所以,文件系统将数据写回磁盘的机制,会直接影响到 Redis 持久化的效率。而且,在持久化的过程中,Redis 也还在接收其他请求,持久化的效率高低又会影响到 Redis 处理请求的性能。

1.AOF日志模式

AOF 日志提供了三种日志写回策略:no、everysec、always。这三种写回策略依赖文件系统的两个系统调用完成,也就是 write 和 fsync。

write 只要把日志记录写到内核缓冲区,就可以返回了,并不需要等待日志实际写回到磁盘;

而 fsync 需要把日志记录写回到磁盘后才能返回,时间较长。

img

当写回策略配置为 everysec 时,Redis 会使用后台的子线程异步完成 fsync 的操作。

而对于 always 策略来说,Redis 需要确保每个操作记录日志都写回磁盘,如果用后台子线程异步完成,主线程就无法及时地知道每个操作是否已经完成了,这就不符合 always 策略的要求了。所以,always 策略并不使用后台子线程来执行。

2.AOF重写压力过大导致fsync 阻塞

AOF重写由子进程完成, 会有大量的IO操作;而fsync 虽然 是由后台子线程完成写入磁盘操作,但是,主线程在进行 写操作时会监视 fsync的执行情况,如果子线程还未完成写盘操作,主进程就会阻塞,不会返回给客户端结果;

正是由于 主线程监视 上一次fsync操作执行情况,在写磁盘压力大时,可能导致 主线程阻塞;

例子:当主线程使用后台子线程执行了一次 fsync,需要再次把新接收的操作记录写回磁盘时,如果主线程发现上一次的 fsync 还没有执行完,那么它就会阻塞。所以,如果后台子线程执行的 fsync 频繁阻塞的话(比如 AOF 重写占用了大量的磁盘 IO 带宽),主线程也会阻塞,导致 Redis 性能变慢。

img

由于 fsync 后台子线程和 AOF 重写子进程的存在,主 IO 线程一般不会被阻塞。但是,如果在重写日志时,AOF 重写子进程的写入量比较大,fsync 线程也会被阻塞,进而阻塞主线程,导致延迟增加。

如果业务应用对延迟非常敏感,但同时允许一定量的数据丢失,那么,可以把配置项 no-appendfsync-on-rewrite 设置为 yes,如下所示:

no-appendfsync-on-rewrite yes

no-appendfsync-on-rewrite 表示 AOF重写时选择不 进行 fsync刷盘操作,yes表示可以不执行fsync,no表示执行fsync

针对延迟非常敏感,但同时允许一定量的数据丢失的应用我们可以 设置no-appendfsync-on-rewrite yes

另外,我们可以采用高速的固态硬盘作为 AOF 日志的写入设备。

3.操作系统:swap和内存大页机制THP
1.Swap

内存 swap 是操作系统里将内存数据在内存和磁盘间来回换入和换出的机制,涉及到磁盘的读写,所以,一旦触发 swap,无论是被换入数据的进程,还是被换出数据的进程,其性能都会受到慢速磁盘读写的影响。

一旦 swap 被触发了,Redis 的请求操作需要等到磁盘数据读写完成才行,swap 触发后影响的是 Redis 主 IO 线程,这会极大地增加 Redis 的响应时间。

  1. Redis 实例自身使用了大量的内存,导致物理机器的可用内存不足;
  2. 和 Redis 实例在同一台机器上运行的其他进程,在进行大量的文件读写操作。文件读写本身会占用系统内存,这会导致分配给 Redis 实例的内存量变少,进而触发 Redis 发生 swap。

解决思路:

  1. 切片集群
  2. 加大内存

1.首先查看redis进程号,这里是5332

$ redis-cli info | grep process_id
process_id: 5332

2.Redis 所在机器的 /proc 目录下的该进程目录中:

Redis 所在机器的 /proc 目录下的该进程目录中:

3.查看该 Redis 进程的使用情况,这里截取部分结果

$cat smaps | egrep '^(Swap|Size)'
Size: 584 kB
Swap: 0 kB
Size: 4 kB
Swap: 4 kB
Size: 4 kB
Swap: 0 kB
Size: 462044 kB
Swap: 462008 kB
Size: 21392 kB
Swap: 0 kB

Size 代表当前redis所占内存,Swap表示这块 Size 大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等,就表示这块内存区域已经完全被换出到磁盘了。

2. 内存大页THP

该机制支持 2MB 大小的内存页分配,而常规的内存页分配是按 4KB 的粒度来执行的。

Redis 为了提供数据可靠性保证,需要将数据做持久化保存。这个写入过程由额外的线程执行,所以,此时,Redis 主线程仍然可以接收客户端写请求。客户端的写请求可能会修改正在进行持久化的数据。在这一过程中,Redis 就会采用写时复制机制,也就是说,一旦有数据要被修改,Redis 并不会直接修改内存中的数据,而是将这些数据拷贝一份,然后再进行修改。

如果采用了内存大页,那么,即使客户端请求只修改 100B 的数据,Redis 也需要拷贝 2MB 的大页。相反,如果是常规内存页机制,只用拷贝 4KB。两者相比,你可以看到,当客户端请求修改或新写入数据较多时,内存大页机制将导致大量的拷贝,这就会影响 Redis 正常的访存操作,最终导致性能变慢。

查看内存大页:

cat /sys/kernel/mm/transparent_hugepage/enabled

always代表使用了THP,never代表未使用

禁止使用THP:

echo never /sys/kernel/mm/transparent_hugepage/enabled
4.总结

梳理了一个包含 9 个检查点的 Checklist,希望你在遇到 Redis 性能变慢时,按照这些步骤逐一检查,高效地解决问题:

  1. 获取 Redis 实例在当前环境下的基线性能。
  2. 是否用了慢查询命令?如果是的话,就使用其他命令替代慢查询命令,或者把聚合计算命令放在客户端做。
  3. 是否对过期 key 设置了相同的过期时间?对于批量删除的 key,可以在每个 key 的过期时间上加一个随机数,避免同时删除。
  4. 是否存在 bigkey? 对于 bigkey 的删除操作,如果你的 Redis 是 4.0 及以上的版本,可以直接利用异步线程机制减少主线程阻塞;如果是 Redis 4.0 以前的版本,可以使用 SCAN 命令迭代删除;对于 bigkey 的集合查询和聚合操作,可以使用 SCAN 命令在客户端完成。
  5. Redis AOF 配置级别是什么?业务层面是否的确需要这一可靠性级别?如果我们需要高性能,同时也允许数据丢失,可以将配置项 no-appendfsync-on-rewrite 设置为 yes,避免 AOF 重写和 fsync 竞争磁盘 IO 资源,导致 Redis 延迟增加。当然, 如果既需要高性能又需要高可靠性,最好使用高速固态盘作为 AOF 日志的写入盘。
  6. Redis 实例的内存使用是否过大?发生 swap 了吗?如果是的话,就增加机器内存,或者是使用 Redis 集群,分摊单机 Redis 的键值对数量和内存压力。同时,要避免出现 Redis 和其他内存需求大的应用共享机器的情况。
  7. 在 Redis 实例的运行环境中,是否启用了透明大页机制?如果是的话,直接关闭内存大页机制就行了。
  8. 是否运行了 Redis 主从集群?如果是的话,把主库实例的数据量大小控制在 2~4GB,以免主从复制时,从库因加载大的 RDB 文件而阻塞。
  9. 是否使用了多核 CPU 或 NUMA 架构的机器运行 Redis 实例?使用多核 CPU 时,可以给 Redis 实例绑定物理核;使用 NUMA 架构时,注意把 Redis 实例和网络中断处理程序运行在同一个 CPU Socket 上。

4.redis 中 的内存碎片

1. 内存碎片带来的影响?

question 1: 为什么redis已经删除了数据,使用top命令还会显示redis 内存占用较大呢?

那是因为redis虽然删除了这些数据,回收了内存空间,但是 redis内存分配器不会立刻把 内存空间 返还给操作系统;

所以,出现redis已经删除了数据,但是 任务管理器还会显示redis占用大内存的情况;

question 2: redis 由内存分配器回收,分配内存,如果没有内存自动整理功能(整理内存碎片),会有什么风险?

即使有大量的内存 ,但是空间碎片较多,内存利用率低, 当写 bigkey 要求分配大量且连续的内存空间时,没有较大的连续内存空间导致无法处理这个操作;

虽然有空闲空间,Redis 却无法用来保存数据,不仅会减少 Redis 能够实际保存的数据量,还会降低 Redis 运行机器的成本回报率。

2.内存碎片如何形成的?

主要有两种方式:

  1. 内因:操作系统的内存分配机制
  2. 外因:Redis 的负载特征(键值大小不一致, 键值对修改,删除等)
1. 内因:内存分配器的分配策略

Redis 可以使用 libc、jemalloc、tcmalloc 多种内存分配器来分配内存,默认使用 jemalloc

jemalloc 的分配策略之一,是按照一系列固定的大小划分内存空间。例如 8 字节、16 字节、32 字节、48 字节,…, 2KB、4KB、8KB 等。当程序申请的内存最接近某个固定值时,jemalloc 会给它分配相应大小的空间。

这样的分配方式本身是为了减少分配次数。例如,Redis 申请一个 20 字节的空间保存数据,jemalloc 就会分配 32 字节,此时,如果应用还要写入 10 字节的数据,Redis 就不用再向操作系统申请空间了,因为刚才分配的 32 字节已经够用了,这就避免了一次分配操作。

2.外因:键值对大小不一样和删改操作
  1. 不同大小的键值对,Redis 申请内存空间分配时,本身就会有大小不一的空间需求。
  2. 键值对修改删除带来的内存空间变化,删除和修改都会带来空间碎片;

3.redis内存碎片处理

1.判断是否有内存碎片
INFO memory
# Memory
used_memory:1073741736
used_memory_human:1024.00M
used_memory_rss:1997159792
used_memory_rss_human:1.86G
…
mem_fragmentation_ratio:1.86

mem_fragmentation_ratio: 表示Redis 当前的内存碎片率。它就是上面的命令中的两个指标 used_memory_rss 和 used_memory 相除的结果。

used_memory_rss:操作系统实际分配给 Redis 的物理内存空间,里面就包含了碎片

used_memory :redis 申请的内存空间

问题:那么,该如何设置这个mem_fragmentation_ratio的值呢?这里有一些经验设置阈值:

  • 1 <= mem_fragmentation_ratio <= 1.5:这种情况是合理的。
  • mem_fragmentation_ratio > 1.5 :这表明内存碎片率已经超过了 50%。一般情况下,这个时候,我们就需要采取一些措施来降低内存碎片率了。
2.内存碎片的清理
a.内存清理

4.0-RC3 版本以后,Redis 自身提供了一种内存碎片自动清理的方法,我们先来看这个方法的基本机制。

内存碎片清理,简单来说,就是**“搬家让位,合并空间”**。

img

碎片清理是有代价的:

操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销。

因为 Redis 是单线程,在数据拷贝时,Redis 只能等着,这就导致 Redis 无法及时处理请求,性能就会降低。

而且,有的时候,数据拷贝还需要注意顺序,就像刚刚说的清理内存碎片的例子,操作系统需要先拷贝 D,并释放 D 的空间后,才能拷贝 B。这种对顺序性的要求,会进一步增加 Redis 的等待时间,导致性能降低

b.如何 降低内存清理时对redis性能的影响?

启动内存碎片自动清理:

config set activedefrag yes

主要从两个方面三个参数控制内存清理:

  1. 满足两个条件自动进行内存清理:
    1. active-defrag-ignore-bytes 100mb:内存碎片字节数到达100MB
    2. active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理。
    3. 注意:清理过程中,不满足以上条件时立刻停止自动清理,满足条件后会继续自动清理
  2. 控制内存碎片CPU执行时间:
    1. active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展;
    2. active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。

5.《redis 缓冲区》

1. 什么是缓冲区?

1.缓冲区解决什么问题?

redis缓冲区解决客户端请求堆积或服务器处理数据速度过慢带来的数据丢失以及性能问题

2.缓冲区在redis 中有哪些应用场景?

  1. cliet-server服务器模式高并发下暂存客户端发送的命令数据,或者是服务器端返回给客户端的数据结果
  2. 主从节点间进行数据同步时,用来暂存主节点接收的写命令和数据。

注意:redis之所以适合做缓存是因为它有高性能的内存结构 ,以及完善的淘汰机制;

2.客户端与服务器之间的缓冲区

img

cliet-server模式之间的缓冲区,分为输入缓冲区的输出缓冲区,服务器端给每个连接的客户端都设置了一个输入缓冲区和输出缓冲区

client把请求命令和数据 放入 输入缓冲区,server 逐步读取命令把执行结果发送到输出缓冲区,client去输出缓冲区拿响应结果。

3.如何处理输入缓冲区溢出问题

1.查看输入缓冲区使用情况
CLIENT LIST
id=5 addr=127.0.0.1:50487 fd=9 name= age=4 idle=0 flags=N db=0 sub=0 psub=0 multi=-1 qbuf=26 qbuf-free=32742 obl=0 oll=0 omem=0 events=r cmd=client

client list 命令查看所有与server 相连的客户端输入缓冲区的信息

主要看两类信息:

  1. client 的信息,ip 地址,端口号

  2. 输入缓冲区相关信息:

    ​ cmd: 客户端最新执行的命令(当前为client)

    ​ qbuf: 当前客户端已使用 缓冲区大小

    ​ qbuf-free: 剩余 缓冲区大小

2.什么情况下出现输入缓冲区溢出问题?以及解决方案
  1. 有批量bigkey请求
  2. server性能低, 频繁阻塞或阻塞时间教久

怎么解决呢?

由于输入缓冲区 并不能设置缓冲区的大小,默认最多1G,所有只能避免bigkey ,避免server性能低

注意:当多个客户端连接占用的内存总量,超过了 Redis 的 maxmemory 配置项时(例如 4GB),就会触发 Redis 进行数据淘汰。

4.如何处理输出缓存冲溢出问题?

1.什么情况下出现输出缓冲区溢出问题?
  1. 输出bigkey结果
  2. 执行MONITOR
  3. 输出缓冲区设置过小
2.怎么设置输出缓冲区的大小?

client-output-buffer-limit 配置项,来设置缓冲区的大小

例子:client-output-buffer-limit normal 0 0 0

四个参数代表什么?

one:类型,normal代表普通客户端

two: 缓冲区最大限制

three : 持续输出最大的数据量

four:持续输出最大时间

3.不同的客户端不同分配模式
  1. 普通客户端

     client-output-buffer-limit normal 0 0 0
    

    普通客户端来说,它每发送完一个请求,会等到请求结果返回后,再发送下一个请求,这种发送方式称为阻塞式发送。在这种情况下,如果不是读取体量特别大的 bigkey,服务器端的输出缓冲区一般不会被阻塞的。

    所以,我们通常把普通客户端的缓冲区大小限制,以及持续写入量限制、持续写入时间限制都设置为 0,也就是不做限制。

  2. 订阅客户端

    一旦订阅的 Redis 频道有消息了,服务器端都会通过输出缓冲区把消息发给客户端。所以,订阅客户端和服务器间的消息发送方式,不属于阻塞式发送。

    因此,我们会给订阅客户端设置缓冲区大小限制、缓冲区持续写入量限制,以及持续写入时间限制,可以在 Redis 配置文件中这样设置:

    client-output-buffer-limit pubsub 8mb 2mb 60
    

5.主从集群中的缓冲区

1.复制缓冲区(replication_buffer)

replication_buffer的大小不算入 maxmemory

全量复制过程中,主节点在向从节点传输 RDB 文件的同时,会继续接收客户端发送的写命令请求。这些写命令就会先保存在复制缓冲区中,主节点上会为每个从节点都维护一个复制缓冲区,来保证主从节点间的数据同步。

img

按通常的使用经验,我们会**把主节点的数据量控制在 2~4GB,**这样可以让全量同步执行得更快些,避免复制缓冲区累积过多命令。

config set client-output-buffer-limit slave 512mb 128mb 60
2.复制积压缓冲区(repl_backlog_buffer)

repl_backlog_buffer算入maxmemeory

增量复制时使用的缓冲区,这个缓冲区称为复制积压缓冲区,为了应对复制积压缓冲区的溢出问题,我们可以调整复制积压缓冲区的大小,也就是设置 repl_backlog_size 这个参数的值.

6. 总结

  1. 针对命令数据发送过快过大的问题,对于普通客户端来说可以避免 bigkey,而对于复制缓冲区来说,就是避免过大的 RDB 文件。
  2. 针对命令数据处理较慢的问题,解决方案就是减少 Redis 主线程上的阻塞操作,例如使用异步的删除操作。
  3. 针对缓冲区空间过小的问题,解决方案就是使用 client-output-buffer-limit 配置项设置合理的输出缓冲区、复制缓冲区和复制积压缓冲区大小。当然,我们不要忘了,输入缓冲区的大小默认是固定的,我们无法通过配置来修改它,除非直接去修改 Redis 源码。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值