公号:码农充电站pro

主页:https://codeshellme.github.io

这 3 篇文章是我在学习 Redis 的过程中,总结的笔记:

6,Redis 高性能的影响因素

6.1,Redis 内部的阻塞式操作

与 Redis 实例交互的对象,以及交互时会发生的操作:

  • 客户端:网络 IO,键值对增删改查操作,数据库操作;
  • 磁盘:生成 RDB 快照,记录 AOF 日志,AOF 日志重写;
  • 主从节点:主库生成、传输 RDB 文件,从库接收 RDB 文件、清空数据库、加载 RDB 文件;
  • 切片集群实例:向其他实例传输哈希槽信息,数据迁移。

和客户端交互时的阻塞点

  • 网络 IO:不会引起阻塞
    • 因为 Redis 使用了 IO 多路复用机制
  • 键值对增删改查操作:复杂度高(是否为 O(N))的操作会引起阻塞
    • Redis 中涉及集合的操作复杂度通常为 O(N)
    • 例如集合元素全量查询操作 HGETALLSMEMBERS,以及集合的聚合统计操作(求交、并和差集)
    • 这些操作可以作为 Redis 的第一个阻塞点:集合全量查询和聚合操作
    • bigkey(包含大量元素的集合) 删除操作就是 Redis 的第二个阻塞点
  • 数据库操作:Redis 的第三个阻塞点:清空数据库
    • 清空数据库(例如 FLUSHDBFLUSHALL 操作)也是一个潜在的阻塞风险

和磁盘交互时的阻塞点

  • 生成 RDB 快照:由子进程完成,不阻塞
  • 记录 AOF 日志第四个阻塞点:AOF 日志同步写
    • 一个同步写磁盘的操作的耗时大约是 1~2ms
  • AOF 日志重写:由子进程完成,不阻塞

主从节点交互时的阻塞点

  • 主库生成、传输 RDB 文件:由子进程完成,不会阻塞主线程
  • 从库接收 RDB 文件、清空数据库、加载 RDB 文件
    • 需要使用 FLUSHDB 命令清空当前数据库(第三个阻塞点)
    • 加载 RDB 文件是 Redis 的第五个阻塞点

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

  • 向其他实例传输哈希槽信息、数据迁移
    • 哈希槽的信息量不大,而数据迁移是渐进式执行的,所以,这两类操作对 Redis 主线程的阻塞风险不大

为了避免阻塞式操作,Redis 提供了异步线程机制,就是,Redis 会启动一些子线程,把一些任务交给这些子线程,让它们在后台完成,而不再由主线程来执行这些任务。使用异步线程机制执行操作,可以避免阻塞主线程

如果一个操作能被异步执行,就意味着,它并不是 Redis 主线程的关键路径上的操作。关键路径上的操作就是说,客户端把请求发送给 Redis 后,等着 Redis 返回数据结果的操作。

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

对于 Redis 的五大阻塞点来说,除了“集合全量查询和聚合操作”和“从库加载 RDB 文件”,其他三个阻塞点涉及的操作都不在关键路径上,所以,我们可以使用 Redis 的异步子线程机制来实现 bigkey 删除,清空数据库,以及 AOF 日志同步写。

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

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

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

Redis 中的异步子线程执行机制:

在这里插入图片描述

异步的键值对删除和数据库清空操作是 Redis 4.0 后提供的功能,由两个命令完成:

  • 键值对删除:当集合类型中有大量元素需要删除时,建议使用 UNLINK 命令。
  • 清空数据库:可以在 FLUSHDBFLUSHALL 命令后加上 ASYNC 选项,这样就可以让后台子线程异步地清空数据库:
    • FLUSHDB ASYNC
    • FLUSHALL AYSNC

6.2,CPU 核和 NUMA 架构的影响

1,主流 CPU 架构

一个 CPU 处理器中有多个物理核,每个物理核都可以运行应用程序。每个物理核都拥有私有的一级缓存(L1 cache,包括一级指令缓存和一级数据缓存),以及私有的二级缓存(L2 cache)。

物理核的私有缓存(大小只有 KB 级别)只能被当前的物理核使用,其他的物理核无法对这个核的缓存空间进行数据存取。

物理核访问私有缓存的延迟不超过 10 纳秒,速度非常快。如果 L1、L2 缓存中没有所需的数据,应用程序就需要访问内存来获取数据,需要百纳秒的时间。

在这里插入图片描述

因为 L1、L2 缓存的空间很小,所以又有了 L3 缓存(能达到几 MB 到几十 MB),它并不是私有的,而是被不同的物理核共享的。当 L1、L2 缓存中没有数据缓存时,可以访问 L3,尽可能避免访问内存

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

在这里插入图片描述

一般的机器上会有多个 CPU 处理器(多 CPU Socket),不同处理器间通过总线连接。

下图显示的就是多 CPU Socket 的架构:

在这里插入图片描述

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

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

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

2,CPU 多核对 Redis 的影响

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

同时,应用程序访问最频繁的指令和数据会被缓存到 L1、L2 缓存上,以便提升执行速度。但是,在多核 CPU 的场景下,一旦应用程序需要在一个新的 CPU 核上运行,那么,运行时信息就需要重新加载到新的 CPU 核上(可认为是上下文切换)。而且,新的 CPU 核的 L1、L2 缓存也需要重新加载数据和指令,这会导致程序的运行时间增加

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

要避免 Redis 总是在不同 CPU 核上来回调度执行,可以使用 taskset 命令把 Redis 实例和 CPU 核绑定,让一个 Redis 实例固定运行在一个 CPU 核上。绑核能够有效提升 Redis 性能。

如下命令,把 Redis 实例绑在了 0 号核上,“-c”选项用于设置要绑定的核编号:

taskset -c 0 ./redis-server

3,NUMA 架构对 Redis 的影响

Linux 中的 lscpu 命令可以查看 CPU 的情况;假设有 2 个 CPU Socket,每个 Socket 上有 6 个物理核,每个物理核又有 2 个逻辑核,总共 24 个逻辑核:

> 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 是相应物理核中的第二个逻辑核编号。

所以,在绑核时,我们一定要注意,不能想当然地认为第一个 Socket 上的 12 个逻辑核的编号就是 0 到 11。否则,网络中断程序和 Redis 实例就可能绑在了不同的 CPU Socket 上。

在 CPU 多核的场景下,用 taskset 命令把 Redis 实例和一个核绑定,可以减少 Redis 实例在不同核上被来回调度执行的开销,避免较高的尾延迟;在多 CPU 的 NUMA 架构下,如果你对网络中断程序做了绑核操作,建议你同时把 Redis 实例和网络中断程序绑在同一个 CPU Socket 的不同核上,这样可以避免 Redis 跨 Socket 访问内存中的网络数据的时间开销。

6.3,Redis 关键系统配置

如何判断 Redis 是不是真的变慢了?

基于当前环境下的 Redis 基线性能做判断,基线性能就是一个系统在低压力、无干扰下的基本性能,这个性能只由当前的软硬件配置决定

从 2.8.7 版本开始,redis-cli 命令提供了 –intrinsic-latency (表示测试时长,单位是秒)参数,可以用来监测和统计测试期间内的最大延迟,这个延迟可以作为 Redis 的基线性能。为了避免网络对基线性能的影响,这个命令需要在服务器端直接运行。

比如下面的命令,会打印 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.

这里的最大延迟是 119 微秒,也就是基线性能为 119 微秒。一般情况下,运行 120 秒就足够监测到最大延迟了。

一般来说,把运行时延迟(Redis 的响应时长)和基线性能进行对比,如果观察到的 Redis 运行时延迟是其基线性能的 2 倍及以上,就可以认定 Redis 变慢了

如何处理 Redis 变慢了?

Redis 中的 key 可以使用 EXPIRE 设置过期时间,Redis 对过期 key 的删除策略是这样的:

  • 默认情况下,Redis 每 100 毫秒会删除一些过期 key
  • 采样 ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 个数的 key,并将其中过期的 key 全部删除
  • 如果超过 25% 的 key 过期了,则重复删除的过程,直到过期 key 的比例降至 25% 以下(这个过程会阻塞 Redis)
  • ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP 是 Redis 的一个参数,默认是 20

6.4,Redis 内存碎片

1,内存 swap 对 Redis 的影响

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

Redis 是内存数据库,内存使用量大,如果 Redis 的内存不够用了,操作系统会启动 swap 机制,这就会直接拖慢 Redis。swap 触发后影响的是 Redis 主 IO 线程,这会极大地增加 Redis 的响应时间。

通常,触发 swap 的原因主要是物理机器内存不足,对于 Redis 而言,有两种常见的情况:

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

如何查看进程的 swap 情况?

操作系统本身会在后台记录每个进程的 swap 使用情况,即有多少数据量发生了 swap。

可先通过下面的命令查看 Redis 的进程号:

$ redis-cli info | grep process_id
process_id: 5332

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

$ cd /proc/5332

查看该 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 实例所用的一块内存大小,而 Size 下方的 Swap 和它相对应,表示这块 Size 大小的内存区域有多少已经被换出到磁盘上了。如果这两个值相等,就表示这块内存区域已经完全被换出到磁盘了。

作为内存数据库,Redis 本身会使用很多大小不一的内存块,所以,可以看到有很多 Size 行,有的很小,就是 4KB,而有的很大,例如 462044KB。

当出现百 MB,甚至 GB 级别的 swap 大小时,就表明,此时 Redis 实例的内存压力很大,很有可能会变慢。所以,swap 的大小是排查 Redis 性能变慢是否由 swap 引起的重要指标。

2,内存大页对 Redis 的影响

内存大页机制也会影响 Redis 性能;常规的内存页分配是按 4KB 的粒度来执行的,Linux 内核从 2.6.38 开始支持内存大页机制,该机制支持 2MB 大小的内存页分配。

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

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

查看内存大页机制是否开启:

# 如果执行结果是 always,就表明内存大页机制被启动了
# 如果是 never,就表示,内存大页机制被禁止
cat /sys/kernel/mm/transparent_hugepage/enabled

关闭内存大页:

echo never > /sys/kernel/mm/transparent_hugepage/enabled

在实际生产环境中部署时,建议不使用内存大页机制

3,检查 Redis 变慢的原因,9 个检查点

这里梳理了一个包含 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 上。
关于如何分析、排查、解决Redis变慢问题,我总结的checklist如下:

1、使用复杂度过高的命令(例如SORT/SUION/ZUNIONSTORE/KEYS),或一次查询全量数据(例如LRANGE key 0 N,但N很大)

分析:a) 查看slowlog是否存在这些命令 b) Redis进程CPU使用率是否飙升(聚合运算命令导致)

解决:a) 不使用复杂度过高的命令,或用其他方式代替实现(放在客户端做) b) 数据尽量分批查询(LRANGE key 0 N,建议N<=100,查询全量数据建议使用HSCAN/SSCAN/ZSCAN)

2、操作bigkey

分析:a) slowlog出现很多SET/DELETE变慢命令(bigkey分配内存和释放内存变慢) b) 使用redis-cli -h $host -p $port --bigkeys扫描出很多bigkey

解决:a) 优化业务,避免存储bigkey b) Redis 4.0+可开启lazy-free机制

3、大量key集中过期

分析:a) 业务使用EXPIREAT/PEXPIREAT命令 b) Redis info中的expired_keys指标短期突增

解决:a) 优化业务,过期增加随机时间,把时间打散,减轻删除过期key的压力 b) 运维层面,监控expired_keys指标,有短期突增及时报警排查

4、Redis内存达到maxmemory

分析:a) 实例内存达到maxmemory,且写入量大,淘汰key压力变大 b) Redis info中的evicted_keys指标短期突增

解决:a) 业务层面,根据情况调整淘汰策略(随机比LRU快) b) 运维层面,监控evicted_keys指标,有短期突增及时报警 c) 集群扩容,多个实例减轻淘汰key的压力

5、大量短连接请求

分析:Redis处理大量短连接请求,TCP三次握手和四次挥手也会增加耗时

解决:使用长连接操作Redis

6、生成RDB和AOF重写fork耗时严重

分析:a) Redis变慢只发生在生成RDB和AOF重写期间 b) 实例占用内存越大,fork拷贝内存页表越久 c) Redis info中latest_fork_usec耗时变长

解决:a) 实例尽量小 b) Redis尽量部署在物理机上 c) 优化备份策略(例如低峰期备份) d) 合理配置repl-backlog和slave client-output-buffer-limit,避免主从全量同步 e) 视情况考虑关闭AOF f) 监控latest_fork_usec耗时是否变长

7、AOF使用awalys机制

分析:磁盘IO负载变高

解决:a) 使用everysec机制 b) 丢失数据不敏感的业务不开启AOF

8、使用Swap

分析:a) 所有请求全部开始变慢 b) slowlog大量慢日志 c) 查看Redis进程是否使用到了Swap

解决:a) 增加机器内存 b) 集群扩容 c) Swap使用时监控报警

9、进程绑定CPU不合理

分析:a) Redis进程只绑定一个CPU逻辑核 b) NUMA架构下,网络中断处理程序和Redis进程没有绑定在同一个Socket下

解决:a) Redis进程绑定多个CPU逻辑核 b) 网络中断处理程序和Redis进程绑定在同一个Socket下

10、开启透明大页机制

分析:生成RDB和AOF重写期间,主线程处理写请求耗时变长(拷贝内存副本耗时变长)

解决:关闭透明大页机制

11、网卡负载过高

分析:a) TCP/IP层延迟变大,丢包重传变多 b) 是否存在流量过大的实例占满带宽

解决:a) 机器网络资源监控,负载过高及时报警 b) 提前规划部署策略,访问量大的实例隔离部署

总之,Redis的性能与CPU、内存、网络、磁盘都息息相关,任何一处发生问题,都会影响到Redis的性能。

主要涉及到的包括业务使用层面和运维层面:业务人员需要了解Redis基本的运行原理,使用合理的命令、规避bigke问题和集中过期问题。运维层面需要DBA提前规划好部署策略,预留足够的资源,同时做好监控,这样当发生问题时,能够及时发现并尽快处理。

4,内存碎片

当数据删除后,Redis 释放的内存空间会由内存分配器管理,并不会立即返回给操作系统。

内存碎片的形成有内因和外因两个层面的原因:

  • 内因是操作系统的内存分配机制
    • 内存分配器的分配策略决定了操作系统无法做到按需分配
    • 内存分配器一般是按固定大小来分配内存,而不是完全按照应用程序申请的内存空间大小给程序分配
    • Redis 可以使用 libcjemalloctcmalloc 多种内存分配器来分配内存,默认使用 jemalloc
    • jemalloc 是按照一系列固定的大小划分内存空间,例如 8 字节、16 字节、32 字节、48 字节,…, 2KB、4KB、8KB 等。当程序申请的内存最接近某个固定值时,jemalloc 会给它分配相应大小的空间。这样的分配方式本身是为了减少分配次数,但同时也很容易造成内存碎片
  • 外因是 Redis 的数据特征
    • 不同业务应用的数据都可能保存在 Redis 中,这就会带来不同大小的键值对。这样一来,Redis 申请内存空间分配时,本身就会有大小不一的空间需求
    • 持续不断地增删改数据,也会造成内存碎片

大量内存碎片的存在,会造成 Redis 的内存实际利用率变低。

5,如何判断是否有内存碎片

Redis 提供了 INFO 命令,可以用来查询内存使用的详细信息:

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 # 当前的内存碎片率

其中:

  • used_memory_rss 是操作系统实际分配给 Redis 的物理内存空间(里面包含了碎片)
  • used_memory 是 Redis 为了保存数据实际申请使用的空间
  • mem_fragmentation_ratio 是 Redis 当前的内存碎片率
    • mem_fragmentation_ratio = used_memory_rss / used_memory
    • 1 < mem_fragmentation_ratio < 1.5,这种情况是合理的
    • mem_fragmentation_ratio > 1.5,这表明内存碎片率已经超过了 50%。这时,就需要采取一些措施来降低内存碎片率了

6,如何清理内存碎片

当 Redis 发生内存碎片后,一个简单粗暴的方法就是重启 Redis 实例,但这并不是一个“优雅”的方法。

4.0-RC3 版本以后,Redis 提供了一种内存碎片自动清理的方法,其基本原理就是搬家让位,合并空间

在这里插入图片描述

碎片清理是有代价的,操作系统需要把多份数据拷贝到新位置,把原有空间释放出来,这会带来时间开销,导致 Redis 无法及时处理请求,性能就会降低。

我们可以通过设置参数,来控制碎片清理的开始和结束时机,以及占用的 CPU 比例,从而减少碎片清理对 Redis 本身请求处理的性能影响。

要让 Redis 启用自动内存碎片清理,可以把 activedefrag 配置项设置为 yes,命令如下:

config set activedefrag yes

具体什么时候清理,会受到下面这两个参数的控制。这两个参数分别设置了触发内存清理的一个条件,如果同时满足这两个条件,就开始清理。在清理的过程中,只要有一个条件不满足了,就停止自动清理。

  • active-defrag-ignore-bytes 100mb:表示内存碎片的字节数达到 100MB 时,开始清理
  • active-defrag-threshold-lower 10:表示内存碎片空间占操作系统分配给 Redis 的总空间比例达到 10% 时,开始清理

还有两个参数,分别用于控制清理操作占用的 CPU 时间比例的上、下限,既保证清理工作能正常进行,又避免了降低 Redis 性能。

  • active-defrag-cycle-min 25: 表示自动清理过程所用 CPU 时间的比例不低于 25%,保证清理能正常开展;
  • active-defrag-cycle-max 75:表示自动清理过程所用 CPU 时间的比例不高于 75%,一旦超过,就停止清理,从而避免在清理时,大量的内存拷贝阻塞 Redis,导致响应延迟升高。

6.5,Redis 缓冲区

缓冲区在 Redis 中的主要应用场景:

  • 在客户端和服务器端之间进行通信时,用来暂存客户端发送的命令数据,或者是服务器端返回给客户端的数据结果
  • 在主从节点间进行数据同步时,用来暂存主节点接收的写命令和数据

在这里插入图片描述

当数据量太大时,会造成缓冲区溢出的问题:

  • 输入缓冲区溢出
    • 当客户端写入大量命令的话,就会引起客户端输入缓冲区溢出
  • 输出缓冲区溢出

对于输入输出缓冲区溢出,Redis 的处理办法就是把客户端连接关闭。

1,输入缓冲区

要查看和服务器端相连的每个客户端对输入缓冲区的使用情况,可以使用 CLIENT LIST 命令:


> 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 命令返回的信息虽然很多,但我们只需要重点关注两类信息就可以了:

  • 一类是与服务器端连接的客户端的信息。
    • 这个案例展示的是一个客户端的输入缓冲区情况,如果有多个客户端,输出结果中的 addr 会显示不同客户端的 IP 和端口号。
  • 另一类是与输入缓冲区相关的三个参数:
    • cmd,表示客户端最新执行的命令。这个例子中执行的是 CLIENT 命令。
    • qbuf,表示输入缓冲区已经使用的大小。这个例子中的 CLIENT 命令已使用了 26 字节大小的缓冲区。
    • qbuf-free,表示输入缓冲区尚未使用的大小。
      • 这个例子中的 CLIENT 命令还可以使用 32742 字节的缓冲区。
      • qbuf 和 qbuf-free 的总和就是,Redis 服务器端当前为已连接的这个客户端分配的缓冲区总大小。
      • 这个例子中总共分配了 26 + 32742 = 32768 字节,也就是 32KB 的缓冲区。

Redis 服务器端不止服务一个客户端,当多个客户端连接占用的内存总量,超过了 Redis 的 maxmemory 配置项时(例如 4GB),就会触发 Redis 进行数据淘汰。一旦数据被淘汰出 Redis,再要访问这部分数据,就需要去后端数据库读取,这就降低了业务应用的访问性能。

避免输入缓冲区溢出,可以从两个角度去考虑:

  • 一是把缓冲区调大:Redis 并没有提供参数让我们调节客户端输入缓冲区的大小
    • Redis 的客户端输入缓冲区大小的上限阈值,在代码中就设定为了 1GB。即,Redis 服务器端允许为每个客户端最多暂存 1GB 的命令和数据。
  • 二是从数据命令的发送和处理速度入手:避免客户端写入 bigkey,以及避免 Redis 主线程阻塞

2,输出缓冲区

Redis 为每个客户端设置的输出缓冲区包括两部分:

  • 一部分,是一个大小为 16KB 的固定缓冲空间,用来暂存 OK 响应和出错信息;
  • 另一部分,是一个可以动态增加的缓冲空间,用来暂存大小可变的响应结果。

发生输出缓冲区溢出的三种情况:

  • 服务器端返回 bigkey 的大量结果
  • 执行了 MONITOR 命令
  • 缓冲区大小设置得不合理

MONITOR 命令是用来监测 Redis 执行的。执行这个命令之后,就会持续输出监测到的各个命令操作,如下所示:

MONITOR
OK
1600617456.437129 [0 127.0.0.1:50487] "COMMAND"
1600617477.289667 [0 127.0.0.1:50487] "info" "memory"

MONITOR 的输出结果会持续占用输出缓冲区,并越占越多,最后的结果就是发生溢出。

我们可以通过 client-output-buffer-limit 配置项,来设置缓冲区的大小。具体设置的内容包括两方面:

  • 设置缓冲区大小的上限阈值
  • 设置输出缓冲区持续写入数据的数量上限阈值,和持续写入数据的时间的上限阈值

设置普通客户端,命令如下:

# 对于普通客户端来说,它每发送完一个请求,会等到请求结果返回后,再发送下一个请求,这种发送方式称为阻塞式发送。在这种情况下,如果不是读取体量特别大的 bigkey,服务器端的输出缓冲区一般不会被阻塞的。
# 所以,我们通常把普通客户端的缓冲区大小限制,以及持续写入量限制、持续写入时间限制都设置为 0,也就是不做限制。
client-output-buffer-limit normal 0 0 0

其中:

  • normal 表示当前设置的是普通客户端
  • 第 1 个 0 设置的是缓冲区大小限制
  • 第 2 个 0 表示缓冲区持续写入量限制
  • 第 3 个 0 表示持续写入时间限制

对于和 Redis 实例进行交互的应用程序来说,主要使用两类客户端和 Redis 服务器端交互,分别是常规和 Redis 服务器端进行读写命令交互的普通客户端,以及订阅了 Redis 频道的订阅客户端

设置订阅客户端,命令如下:

# 对于订阅客户端来说,一旦订阅的 Redis 频道有消息了,服务器端都会通过输出缓冲区把消息发给客户端。
# 所以,订阅客户端和服务器间的消息发送方式,不属于阻塞式发送。不过,如果频道消息较多的话,也会占用较多的输出缓冲区空间。
client-output-buffer-limit pubsub 8mb 2mb 60

其中:

  • pubsub 表示订阅客户端
  • 8mb 表示输出缓冲区的大小上限为 8MB,一旦实际占用的缓冲区大小要超过 8MB,服务器端就会直接关闭客户端的连接
  • 2mb 和 60 表示,如果连续 60 秒内对输出缓冲区的写入量超过 2MB 的话,服务器端也会关闭客户端连接

3,主从集群中的缓冲区

主从集群间的数据复制包括全量复制增量复制两种

  • 全量复制是同步所有数据,会用到复制缓冲区
  • 增量复制只会把主从库网络断连期间主库收到的命令,同步给从库,会用到复制积压缓冲区

复制缓冲区

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

在这里插入图片描述

如果在全量复制时,从节点接收和加载 RDB 较慢,同时主节点接收到了大量的写命令,写命令在复制缓冲区中就会越积越多,最终导致溢出。复制缓冲区一旦发生溢出,主节点会直接关闭和从节点进行复制操作的连接,导致全量复制失败

可以使用 client-output-buffer-limit 配置项,来设置合理的复制缓冲区大小。

# slave 表明是复制缓冲区
# 512mb 代表将缓冲区大小的上限设置为 512MB
# 128mb 和 60 代表的设置是,如果连续 60 秒内的写入量超过 128MB 的话,也会触发缓冲区溢出
config set client-output-buffer-limit slave 512mb 128mb 60

主节点上复制缓冲区的内存开销,会是每个从节点客户端输出缓冲区占用内存的总和。如果集群中的从节点数非常多的话,主节点的内存开销就会非常大。所以,我们还必须得控制和主节点连接的从节点个数,不要使用大规模的主从集群。

复制积压缓冲区

增量复制时使用的缓冲区称为复制积压缓冲区repl_backlog_buffer)。

主节点在把接收到的写命令同步给从节点时,同时会把这些写命令写入复制积压缓冲区。一旦从节点发生网络闪断,再次和主节点恢复连接后,从节点就会从复制积压缓冲区中,读取断连期间主节点接收到的写命令,进而进行增量同步。

在这里插入图片描述

复制积压缓冲区是一个大小有限的环形缓冲区。当主节点把复制积压缓冲区写满后,会覆盖缓冲区中的旧命令数据。如果从节点还没有同步这些旧命令数据,就会造成主从节点间重新开始执行全量复制

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