站内链接:


CPU 占比高

问题

  1. 报表系统

报表系统涉及实时数据和离线数据,离线读 pg,实时读 redis。

现象: 实时报表主要访问 redis 数据,监控发现 Redis CPU 占用过高,高峰期 2 个从库实例的 CPU 达到 100%,由于 redis 是单进程单线程结构,所以单核 CPU 达到 100%导致查询阻塞。

该系统的框架结构:1 主 1 从 ,应用手动读写分离,持久化主从默认都开启开启 rdb 持久化,没有做 aof,参数基本走默认

基本

当 Redis 服务的 CPU 占比过高时,可以考虑以下解决思路:

  1. 检查业务端优化 redis 的使用是否合法:评估 Redis 的使用模式,考虑是否存在频繁的读写操作、大量的 Key 操作、复杂的数据结构等因素导致 CPU 负载过高。可以通过优化数据结构、减少不必要的操作、合并批量操作等方式来降低 CPU 负载。
  2. 检查持久化策略是否影响 CPU:如果 Redis 启用了持久化机制(如 RDB 快照或 AOF 日志),检查持久化频率和触发机制是否合理。频繁的持久化操作可能会增加 CPU 负载。可以考虑适当调整持久化参数或采用异步持久化方式来降低对 CPU 的影响。
  3. 内存优化:Redis 是基于内存的数据库,内存的使用情况直接影响性能。检查 Redis 实例的内存使用情况,确保没有内存溢出或频繁的内存交换操作。可以适当调整 Redis 的内存限制或使用内存优化技术,如压缩存储、使用 Redis Cluster 等。
  4. 检查并发连接数:如果 Redis 面临大量的并发连接,可能导致 CPU 负载过高。检查并发连接数是否超出系统处理能力,如果是,可以考虑增加 Redis 实例或使用连接池等方式来优化连接管理,命令:info clients
  5. 检查是否存在慢连接,例如:slowlog get 50
  6. 检查主从是否存在频繁的全量同步,例如 redis.log 中的Full resync from master...
  7. 检查当前架构是否有问题,比如一主一从架构中所有业务读取都从一个从库读取
  8. 网络是否存在异常
  9. 检查 redis.log 日志中是否存在异常的重复日志信息

综合考虑以上因素,根据具体情况进行适当的调整和优化,可以有效降低 Redis 服务的 CPU 占比,提升性能和稳定性。

线上办法

  1. 查找并禁用高消耗命令:高消耗命令一般是时间度达到O(n)或者更高的命令,例如:
  • KEYS:用于匹配指定模式的键,但在大规模数据集中使用时,会导致遍历整个键空间,消耗大量 CPU 和内存资源。
  • SCAN:用于迭代遍历键空间,同样在大规模数据集中使用时,会对 CPU 和内存产生较大压力。
  • SORT:用于对列表、集合或有序集合进行排序操作,当数据集较大时,排序操作可能会消耗大量的 CPU 和内存资源。
  • EVAL/EVALSHA:用于执行 Lua 脚本,复杂的 Lua 脚本可能会引起较高的 CPU 消耗。
  • FLUSHALL/FLUSHDB:用于清空整个 Redis 数据库或指定数据库,执行这些命令会导致删除大量键,消耗较高的 CPU 和 IO 资源。
  • HGETALL:用于获取哈希表中的所有字段和值,如果哈希表较大,会产生较高的网络传输和 CPU 消耗。
  • MSET/MGET:用于批量设置或获取多个键值对,当操作的键值对较多时,会产生较高的网络传输和 CPU 消耗。

由于 redis 单线程的特性,Redis 在执行高消耗命令时会引发排队导致应用响应变慢,一些极端情况下可能导致实例整体阻塞,引发应用超时中断或者流量直接穿过缓存到达数据库测,引发雪崩效应。那么如何禁用命令?

  • a. 在 redis 配置中通过rename-command来禁用命令:rename-command COMMAND NEW_NAME,例如:rename-command FLUSHALL DISABLED_FLUSHALL
  • b. 通过 ACL 访问控制机制,创建一个没有执行某些命令权限的用户,并在客户端连接时使用该用户进行访问,例如:ACL SETUSER limited_user on >mypass ~* +@all -@disable_command1 -@disable_command2,其中disable_command1disable_command2是要禁用的命令

对于一些 SORT 等业务命令,在业务端尽可能降低这些高消耗的命令使用频率或者禁止使用

  1. 慢查询日志查看,具体见 redis 博客中的笔记内容,即使 redis 6.0 之后开启了多线程,但是 redis 读仍然是单线程,慢查询会阻塞后续命令的执行,这个是比较常见的。另外,根据慢日志还可以定位不合理的业务,在业务层进行改进
  2. 关闭 redis 持久化中的 AOF 方式,减少 fsync 导致的 QPS 和 CPU 损耗,同时将 RDB 的持久化时间窗口降低,当然,这个还得看实际的业务场景。
  3. 降级:在系统资源紧张或负载过高的情况下,为了保证核心功能的正常运行,有意地减少或关闭一些非核心功能。例如缓存降级是指缓存失效或缓存服务器挂掉的情况下,不去访问数据库,直接返回默认数据或访问服务的内存数据。
  4. 临时关闭某些连接并设置最大连接数:
1
2
3
4
5
6
7
8
9
10
11
# 查看连接数
info clients

# 查看最大连接
config get maxclients

# 删除某个连接
client kill IP:PORT

# 获取空间连接多长时间关闭(在高CPU场景下非常重要)
config get timeout

架构和业务

对于这种高 CPU 的 redis 处理问题,除了线上临时解决办法之外,还需要在系统上线之前尽可能避免

  1. 分析和排查存在异常的业务访问,例如异常的命令、来自某台应用主机的异常访问等,此时就应该从业务上进行优化
  2. 若不存在异常的业务,在正常使用命令的情况下就达到了高负载,那么此时就需要考虑增加实例的硬件配置,并使用 redis 读写分离架构或者 redis cluster 来实现高可用和自动故障切换的读写分离配置
  3. 热 key 的二级缓存结构,将频繁的热 key 保存在本地缓存中,避免频繁的与 redis 进行交互,减少 redis 的并发压力(优点对 redis 职能分担的意思)
  4. 对于一主多从架构,可以将持久化放到从库上进行备份处理,例如:主/从业务库关闭 rdb 和 aof 持久化,新增一台从库(不参与业务)单独做 rdb 持久化

内存占比高

内存组成

  1. 数据内存:这是 Redis 存储键值对数据的主要部分,包括键和值的实际数据。Redis 将所有的数据存储在内存中,以提供快速的读写访问。
  2. 管理内存:一般比较小,这部分内存用于管理 Redis 的内部数据结构和元数据。它包括用于维护键空间和索引结构的内存,例如哈希表、有序集合、列表等。管理内存也用于存储一些 Redis 的内部状态信息和配置参数。
  3. 动态内存:Redis 使用动态内存分配来管理内存。它会根据需要动态分配和释放内存,以适应数据的大小变化。动态内存包括用于存储新数据的分配内存和存储已删除数据的释放内存。Redis 通过自身的内存管理机制来优化内存使用效率,减少内存碎片和浪费

排查方式

  1. 使用MEMORY STATS查看内存的具体使用,其各行输出如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
 1) "peak.allocated" //Redis进程自启动以来消耗内存的峰值。
2) (integer) 79492312
3) "total.allocated" //Redis使用其分配器分配的总字节数,即当前的总内存使用量。
4) (integer) 79307776
5) "startup.allocated" //Redis启动时消耗的初始内存量。
6) (integer) 45582592
7) "replication.backlog" //复制积压缓冲区的大小。
8) (integer) 33554432
9) "clients.slaves" //主从复制中所有从节点的读写缓冲区大小。
10) (integer) 17266
11) "clients.normal" //除从节点外,所有其他客户端的读写缓冲区大小。
12) (integer) 119102
13) "aof.buffer" //AOF持久化使用的缓存和AOF重写时产生的缓存。
14) (integer) 0
15) "db.0" //业务数据库的数量。
16) 1) "overhead.hashtable.main" //当前数据库的hash链表开销内存总和,即元数据内存。
2) (integer) 144
3) "overhead.hashtable.expires" //用于存储key的过期时间所消耗的内存。
4) (integer) 0
17) "overhead.total" //数值=startup.allocated+replication.backlog+clients.slaves+clients.normal+aof.buffer+db.X。
18) (integer) 79273616
19) "keys.count" //当前Redis实例的key总数
20) (integer) 2
21) "keys.bytes-per-key" //当前Redis实例每个key的平均大小,计算公式:(total.allocated-startup.allocated)/keys.count。
22) (integer) 16862592
23) "dataset.bytes" //纯业务数据占用的内存大小。
24) (integer) 34160
25) "dataset.percentage" //纯业务数据占用的内存比例,计算公式:dataset.bytes*100/(total.allocated-startup.allocated)。
26) "0.1012892946600914"
27) "peak.percentage" //当前总内存与历史峰值的比例,计算公式:total.allocated*100/peak.allocated。
28) "99.767860412597656"
29) "fragmentation" //内存的碎片率。
30) "0.45836541056632996"
  1. 分析业务,优化大 key

答疑

持续写入快照

问题: 在做 RDB 的时候 key 有了改动会出现什么情况,即一个持续写入的数据库如何生成快照呢?

首先,RDB 的快照或者备份话流程如下:

1
2
3
a. 在执行RDB持久化开始时会创建一个快照,它会对整个数据库进行遍历和保存,将数据库中的数据状态在某一时刻的快照保存到磁盘上。
b. 在 RDB 持久化完成后,Redis 主进程会使用新的快照文件替换原来的快照文件,以更新数据库的持久化状态。
c. 在加载快照文件时,Redis 会将整个快照文件读入内存并恢复数据库的状态,此时数据库将反映出持久化完成时的数据状态。

所以,如果在 RDB 持久化期间数据库发生了新的写入操作,这些写入操作不会被包含在当前的快照中,只有在下次执行 RDB 持久化并生成新的快照时才会包含这些新的改动。

当然,如果对数据的实时性要求较高,RDB 持久化可能无法满足你的需求。在这种情况下,可以考虑使用 AOF 持久化方式,它以追加日志的方式记录每个写入操作,可以更及时地反映数据库的最新状态。

写时复制

问题:服务器内存 4 G ,Redis 占了 3 G ,在 RDB 模式下有没有问题?

在 Linux 中引入了”写时复制”技术,也就是只有进程空间的各段的内容要发生变化时,才会将父进程的内容复制一份给子进程。

内核只为新生成的子进程创建虚拟空间结构,它们来复制于父进程的虚拟究竟结构,但是不为这些段分配物理内存,它们共享父进程的物理空间,当父进程中有更改相应段的行为发生时,再为子进程相应的段分配物理空间。

参考: