Redis 性能优化:多维度技术解析与实战策略
文章目录
- 1 基准性能
- 2 使用 slowlog 优化耗时命令
- 3 big key 优化
- 4 使用 lazy free 特性
- 5 缩短键值对的存储长度
- 6 设置键值的过期时间
- 7 禁用耗时长的查询命令
- 8 使用 Pipeline 批量操作数据
- 9 避免大量数据同时失效
- 10 客户端使用优化
- 11 限制 Redis 内存大小
- 12 使用物理机而非虚拟机
- 13 检查数据持久化策略
- 14 使用分布式架构来增加读写速度
- 15 持久化
- 16 绑定 CPU
- 17 内存 swap
1 基准性能
什么是基准性能?
简单来讲,基准性能就是指 Redis 在一台负载正常的机器上,其最大的响应延迟和平均响应延迟分别是怎样的?
为什么要测试基准性能?我参考别人提供的响应延迟,判断自己的 Redis 是否变慢不行吗?
答案是否定的。
因为 Redis 在不同的软硬件环境下,它的性能是各不相同的。
例如,我的机器配置比较低,当延迟为 2ms 时,我就认为 Redis 变慢了,但是如果你的硬件配置比较高,那么在你的运行环境下,可能延迟是 0.5ms 时就可以认为 Redis 变慢了。
所以,你只有了解了你的 Redis 在生产环境服务器上的基准性能,才能进一步评估,当其延迟达到什么程度时,才认为 Redis 确实变慢了。
具体如何做?
为了避免业务服务器到 Redis 服务器之间的网络延迟,你需要直接在 Redis 服务器上测试实例的响应延迟情况。执行以下命令,就可以测试出这个实例 60 秒内的最大响应延迟:
$ redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 60
Max latency so far: 1 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 17 microseconds.
Max latency so far: 18 microseconds.
Max latency so far: 31 microseconds.
Max latency so far: 32 microseconds.
Max latency so far: 59 microseconds.
Max latency so far: 72 microseconds.
1428669267 total runs (avg latency: 0.0420 microseconds / 42.00 nanoseconds per run).
Worst run took 1429x longer than the average latency.
从输出结果可以看到,这 60 秒内的最大响应延迟为 72 微秒(0.072毫秒)。
你还可以使用以下命令,查看一段时间内 Redis 的最小、最大、平均访问延迟:
$ redis-cli -h 127.0.0.1 -p 6379 --latency-history -i 1
min: 0, max: 1, avg: 0.13 (100 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.12 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.10 (99 samples) -- 1.01 seconds range
min: 0, max: 1, avg: 0.13 (98 samples) -- 1.00 seconds range
min: 0, max: 1, avg: 0.08 (99 samples) -- 1.01 seconds range
...
以上输出结果是,每间隔 1 秒,采样 Redis 的平均操作耗时,其结果分布在 0.08 ~ 0.13 毫秒之间。
了解了基准性能测试方法,那么你就可以按照以下几步,来判断你的 Redis 是否真的变慢了:
- 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
- 找到你认为可能变慢的 Redis 实例,测试这个实例的基准性能
- 如果你观察到,这个实例的运行延迟是正常 Redis 基准性能的 2 倍以上,即可认为这个 Redis 实例确实变慢了
2 使用 slowlog 优化耗时命令
我们可以使用 slowlog 功能找出最耗时的 Redis 命令进行相关的优化,以提升 Redis 的运行速度,慢查询有两个重要的配置项:
slowlog-log-slower-than:用于设置慢查询的评定时间,也就是说超过此配置项的命令,将会被当成慢操作记录在慢查询日志中,它执行单位是微秒(1 秒等于 1000000 微秒);
slowlog-max-len:用来配置慢查询日志的最大记录数。
我们可以根据实际的业务情况进行相应的配置,其中慢日志是按照插入的顺序倒序存入慢查询日志中,我们可以使用
来获取相关的慢查询日志,再找到这些慢查询对应的业务进行相关的优化。
Redis 提供了慢日志命令的统计功能,它记录了有哪些命令在执行时耗时比较久。
查看 Redis 慢日志之前,你需要设置慢日志的阈值。例如,设置慢日志的阈值为 5 毫秒,并且保留最近 500 条慢日志记录:
# 命令执行耗时超过 5 毫秒,记录慢日志
CONFIG SET slowlog-log-slower-than 5000
# 只保留最近 500 条慢日志
CONFIG SET slowlog-max-len 500
设置完成之后,所有执行的命令如果操作耗时超过了 5 毫秒,都会被 Redis 记录下来。
此时,你可以执行以下命令,就可以查询到最近记录的慢日志:
127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 32693 # 慢日志ID
2) (integer) 1593763337 # 执行时间戳
3) (integer) 5299 # 执行耗时(微秒)
4) 1) "LRANGE" # 具体执行的命令和参数
2) "user_list:2000"
3) "0"
4) "-1"
2) 1) (integer) 32692
2) (integer) 1593763337
3) (integer) 5044
4) 1) "GET"
2) "user_info:1000"
...
通过查看慢日志,我们就可以知道在什么时间点,执行了哪些命令比较耗时。
如果你的应用程序执行的 Redis 命令有以下特点,那么有可能会导致操作延迟变大:
经常使用 O(N) 以上复杂度的命令,例如 SORT、SUNION、ZUNIONSTORE 聚合类命令
使用 O(N) 复杂度的命令,但 N 的值非常大
第一种情况导致变慢的原因在于,Redis 在操作内存数据时,时间复杂度过高,要花费更多的 CPU 资源。
第二种情况导致变慢的原因在于,Redis 一次需要返回给客户端的数据过多,更多时间花费在数据协议的组装和网络传输过程中。
另外,我们还可以从资源使用率层面来分析,如果你的应用程序操作 Redis 的 OPS 不是很大,但 Redis 实例的 CPU 使用率却很高,那么很有可能是使用了复杂度过高的命令导致的。
除此之外,我们都知道,Redis 是单线程处理客户端请求的,如果你经常使用以上命令,那么当 Redis 处理客户端请求时,一旦前面某个命令发生耗时,就会导致后面的请求发生排队,对于客户端来说,响应延迟也会变长。
针对这种情况,你可以使用以下方法优化你的业务:
尽量不使用 O(N) 以上复杂度过高的命令,对于数据的聚合操作,放在客户端做
执行 O(N) 命令,保证 N 尽量的小(推荐 N <= 300),每次获取尽量少的数据,让 Redis 可以及时处理返回
3 big key 优化
如果你查询慢日志发现,并不是复杂度过高的命令导致的,而都是 SET / DEL 这种简单命令出现在慢日志中,那么你就要怀疑你的实例否写入了 bigkey。Redis 在写入数据时,需要为新的数据分配内存,相对应的,当从 Redis 中删除数据时,它会释放对应的内存空间。
如果一个 key 写入的 value 非常大,那么 Redis 在分配内存时就会比较耗时。同样的,当删除这个 key 时,释放内存也会比较耗时,这种类型的 key 我们一般称之为 bigkey。
此时,你需要检查你的业务代码,是否存在写入 bigkey 的情况。你需要评估写入一个 key 的数据大小,尽量避免一个 key 存入过大的数据。
bigkey 带来问题如下:
Redis使用过程中经常会有各种大key的情况, 比如:
- 单个简单的key存储的value很大
- hash, set,zset,list 中存储过多的元素(以万为单位)
由于redis是单线程运行的,如果一次操作的value很大会对整个redis的响应时间造成负面影响,所以,业务上能拆则拆,阿里云 redis 的最佳实践推荐小于10 KB,建议每个key不要超过M级别
,过大的Value会引发数据倾斜、热点Key、实例流量或CPU性能被占满等问题,应从设计源头上避免此类问题带来的影响。
大key的风险:
- 读写大key会导致超时严重,甚至阻塞服务。
- 删除一个 bigkey 造成主库较长时间的阻塞,使得其他请求无法执行,并引发同步中断或主从切换;
- Redis 内存不断变大引发 OOM,或者达到 maxmemory 设置值引发写阻塞或重要 Key 被逐出;
- Redis Cluster 中的某个 node 内存远超其余 node,但因 Redis Cluster 的数据迁移最小粒度为 Key 而无法将 node 上的内存均衡化;
- bigkey 的读请求占用过大带宽,自身变慢的同时影响到该服务器上的其它服务;
单个简单的key存储的value很大
-
改对象需要每次都整存整取
可以尝试将对象分拆成几个key-value, 使用multiGet获取值,这样分拆的意义在于分拆单次操作的压力,将操作压力平摊到多个redis实例中,降低对单个redis的IO影响; -
该对象每次只需要存取部分数据
可以像第一种做法一样,分拆成几个key-value, 也可以将这个存储在一个hash中,每个field代表一个具体的属性,使用hget,hmget来获取部分的value,使用hset,hmset来更新部分属性。
hash, set,zset,list 中存储过多的元素:
将大key进行分割,为了均匀分割,可以对field进行hash并通过质数N取余,将余数加到key上面,我们取质数N为997。
那么新的key则可以设置为:
newKey = order_20181010_String.valueOf( Math.abs(order_id.hashcode() % 997) )
field = order_id
value = 10
hset (newKey, field, value) ;
hget(newKey, field)
如果已经写入了 bigkey,那有没有什么办法可以扫描出实例中 bigkey 的分布情况呢?
答案是可以的。
Redis 提供了扫描 bigkey 的命令,执行以下命令就可以扫描出,一个实例中 bigkey 的分布情况,输出结果是以类型维度展示的:
$ redis-cli -h 127.0.0.1 -p 6379 --bigkeys -i 0.01
...
-------- summary -------
Sampled 829675 keys in the keyspace!
Total key length in bytes is 10059825 (avg len 12.13)
Biggest string found 'key:291880' has 10 bytes
Biggest list found 'mylist:004' has 40 items
Biggest set found 'myset:2386' has 38 members
Biggest hash found 'myhash:3574' has 37 fields
Biggest zset found 'myzset:2704' has 42 members
36313 strings with 363130 bytes (04.38% of keys, avg size 10.00)
787393 lists with 896540 items (94.90% of keys, avg size 1.14)
从输出结果我们可以很清晰地看到,每种数据类型所占用的最大内存 / 拥有最多元素的 key 是哪一个,以及每种数据类型在整个实例中的占比和平均大小 / 元素数量。
其实,使用这个命令的原理,就是 Redis 在内部执行了 SCAN 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLEN、LLEN、HLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、容器类型(List、Hash、Set、ZSet)的元素个数。
这里我需要提醒你的是,当执行这个命令时,要注意 2 个问题:
- 对线上实例进行 bigkey 扫描时,Redis 的 OPS 会突增,为了降低扫描过程中对 Redis 的影响,最好控制一下扫描的频率,指定 -i 参数即可,它表示扫描过程中每次扫描后休息的时间间隔,单位是秒
- 扫描结果中,对于容器类型(List、Hash、Set、ZSet)的 key,只能扫描出元素最多的 key。但一个 key 的元素多,不一定表示占用内存也多,你还需要根据业务情况,进一步评估内存占用情况
那针对 bigkey 导致延迟的问题,有什么好的解决方案呢?
这里有两点可以优化:
- 业务应用尽量避免写入 bigkey
- 如果你使用的 Redis 是 4.0 以上版本,用 UNLINK 命令替代 DEL,此命令可以把释放 key 内存的操作,放到后台线程中去执行,从而降低对 Redis 的影响
- 如果你使用的 Redis 是 6.0 以上版本,可以开启 lazy-free 机制(lazyfree-lazy-user-del = yes),在执行 DEL 命令时,释放内存也会放到后台线程中执行
但即便有解决方案 2,我也不建议你在实例中存入 bigkey。
这是因为 bigkey 在很多场景下,依旧会产生很多前面提到性能问题。
4 使用 lazy free 特性
lazy free 特性是 Redis 4.0 新增的一个非常实用的功能,它可以理解为惰性删除或延迟删除。意思是在删除的时候提供异步延时释放键值的功能,把键值释放操作放在 BIO(Background I/O)单独的子线程处理中,以减少删除对 Redis 主线程的阻塞,可以有效地避免删除 big key 时带来的性能和可用性问题。
lazy free 对应了 4 种场景,默认都是关闭的:
lazyfree-lazy-eviction no
lazyfree-lazy-expire no
lazyfree-lazy-server-del no
slave-lazy-flush no
它们代表的含义如下:
- lazyfree-lazy-eviction:表示当 Redis 运行内存超过 maxmeory 时,是否开启 lazy free 机制删除;
- lazyfree-lazy-expire:表示设置了过期时间的键值,当过期之后是否开启 lazy free 机制删除;
- lazyfree-lazy-server-del:有些指令在处理已存在的键时,会带有一个隐式的 del 键的操作,比如 rename 命令,当目标键已存在,Redis 会先删除目标键,如果这些目标键是一个 big key,就会造成阻塞删除的问题,此配置表示在这种场景中是否开启 lazy free 机制删除;
- slave-lazy-flush:针对 slave(从节点)进行全量数据同步,slave 在加载 master 的 RDB 文件前,会运行 flushall 来清理自己的数据,它表示此时是否开启 lazy free 机制删除。
建议开启其中的 lazyfree-lazy-eviction、lazyfree-lazy-expire、lazyfree-lazy-server-del 等配置,这样就可以有效的提高主线程的执行效率。
5 缩短键值对的存储长度
键值对的长度是和性能成反比的,比如我们来做一组写入数据的性能测试,执行结果如下:
从以上数据可以看出,在 key 不变的情况下,value 值越大操作效率越慢,因为 Redis 对于同一种数据类型会使用不同的内部编码进行存储,比如字符串的内部编码就有三种:int(整数编码)、raw(优化内存分配的字符串编码)、embstr(动态字符串编码),这是因为 Redis 的作者是想通过不同编码实现效率和空间的平衡,然而数据量越大使用的内部编码就越复杂,而越是复杂的内部编码存储的性能就越低。
这还只是写入时的速度,当键值对内容较大时,还会带来另外几个问题:
- 内容越大需要的持久化时间就越长,需要挂起的时间越长,Redis 的性能就会越低;
- 内容越大在网络上传输的内容就越多,需要的时间就越长,整体的运行速度就越低;
- 内容越大占用的内存就越多,就会更频繁地触发内存淘汰机制,从而给 Redis 带来了更多的运行负担。
因此在保证完整语义的同时,我们要尽量地缩短键值对的存储长度,必要时要对数据进行序列化和压缩再存储,以 Java 为例,序列化我们可以使用 protostuff 或 kryo,压缩我们可以使用 snappy。
6 设置键值的过期时间
我们应该根据实际的业务情况,对键值设置合理的过期时间,这样 Redis 会帮你自动清除过期的键值对,以节约对内存的占用,以避免键值过多的堆积,频繁的触发内存淘汰策略。
7 禁用耗时长的查询命令
Redis 绝大多数读写命令的时间复杂度都在 O(1) 到 O(N) 之间,在官方文档对每命令都有时间复杂度说明:https://redis.io/commands
如下图所示:
其中 O(1) 表示可以安全使用的,而 O(N) 就应该当心了,N 表示不确定,数据越大查询的速度可能会越慢。因为 Redis 只用一个线程来做数据查询,如果这些指令耗时很长,就会阻塞 Redis,造成大量延时。
要避免 O(N) 命令对 Redis 造成的影响,可以从以下几个方面入手改造:
- 决定禁止使用 keys 命令;
- 避免一次查询所有的成员,要使用 scan 命令进行分批的,游标式的遍历;
- 通过机制严格控制 Hash、Set、Sorted Set 等结构的数据大小;
- 将排序、并集、交集等操作放在客户端执行,以减少 Redis 服务器运行压力;
- 删除(del)一个大数据的时候,可能会需要很长时间,所以建议用异步删除的方式 unlink,它会启动一个新的线程来删除目标数据,而不阻塞 Redis 的主线程。
8 使用 Pipeline 批量操作数据
Pipeline(管道技术)是客户端提供的一种批处理技术,用于一次处理多个 Redis 命令,从而提高整个交互的性能。
性能可以提升数十倍。
9 避免大量数据同时失效
Redis 过期键值删除使用的是贪心策略,它每秒会进行 10 次过期扫描,此配置可在 redis.conf 进行配置,默认值是 hz 10
Redis 会随机抽取 20 个值,删除这 20 个键中过期的键,如果过期 key 的比例超过 25%,重复执行此流程。
如果在大型系统中有大量缓存在同一时间同时过期,那么会导致 Redis 循环多次持续扫描删除过期字典,直到过期字典中过期键值被删除的比较稀疏为止,而在整个执行过程会导致 Redis 的读写出现明显的卡顿,卡顿的另一种原因是内存管理器需要频繁回收内存页,因此也会消耗一定的 CPU。
为了避免这种卡顿现象的产生,我们需要预防大量的缓存在同一时刻一起过期,最简单的解决方案就是在过期时间的基础上添加一个指定范围的随机数。
10 客户端使用优化
在客户端的使用上我们除了要尽量使用 Pipeline 的技术外,还需要注意要尽量使用 Redis 连接池,而不是频繁创建销毁 Redis 连接,这样就可以减少网络传输次数和减少了非必要调用指令。
11 限制 Redis 内存大小
在 64 位操作系统中 Redis 的内存大小是没有限制的,也就是配置项
maxmemory <bytes>
是被注释掉的,这样就会导致在物理内存不足时,使用 swap 空间既交换空间,而当操心系统将 Redis 所用的内存分页移至 swap 空间时,将会阻塞 Redis 进程,导致 Redis 出现延迟,从而影响 Redis 的整体性能。因此我们需要限制 Redis 的内存大小为一个固定的值,当 Redis 的运行到达此值时会触发内存淘汰策略,内存淘汰策略在
Redis 4.0 之后有 8 种:
noeviction:不淘汰任何数据,当内存不足时,新增操作会报错,Redis 默认内存淘汰策略;
allkeys-lru:淘汰整个键值中最久未使用的键值;
allkeys-random:随机淘汰任意键值;
volatile-lru:淘汰所有设置了过期时间的键值中最久未使用的键值;
volatile-random:随机淘汰设置了过期时间的任意键值;
volatile-ttl:优先淘汰更早过期的键值。
在 Redis 4.0 版本中又新增了 2 种淘汰策略:
volatile-lfu:淘汰所有设置了过期时间的键值中,最少使用的键值;
allkeys-lfu:淘汰整个键值中最少使用的键值。
其中 allkeys-xxx 表示从所有的键值中淘汰数据,而 volatile-xxx 表示从设置了过期键的键值中淘汰数据。
我们可以根据实际的业务情况进行设置,默认的淘汰策略不淘汰任何数据,在新增时会报错。
12 使用物理机而非虚拟机
在虚拟机中运行 Redis 服务器,因为和物理机共享一个物理网口,并且一台物理机可能有多个虚拟机在运行,因此在内存占用上和网络延迟方面都会有很糟糕的表现,我们可以通过 ./redis-cli --intrinsic-latency 100
命令查看延迟时间,如果对 Redis 的性能有较高要求的话,应尽可能在物理机上直接部署 Redis 服务器。
13 检查数据持久化策略
Redis 的持久化策略是将内存数据复制到硬盘上,这样才可以进行容灾恢复或者数据迁移,但维护此持久化的功能,需要很大的性能开销。
在 Redis 4.0 之后,Redis 有 3 种持久化的方式:
- RDB(Redis DataBase,快照方式)将某一个时刻的内存数据,以二进制的方式写入磁盘;
- AOF(Append Only File,文件追加方式),记录所有的操作命令,并以文本的形式追加到文件中;
- 混合持久化方式,Redis 4.0 之后新增的方式,混合持久化是结合了 RDB 和 AOF 的优点,在写入的时候,先把当前的数据以 RDB 的形式写入文件的开头,再将后续的操作命令以 AOF 的格式存入文件,这样既能保证 Redis 重启时的速度,又能减低数据丢失的风险。
RDB 和 AOF 持久化各有利弊,RDB 可能会导致一定时间内的数据丢失,而 AOF 由于文件较大则会影响 Redis 的启动速度,为了能同时拥有 RDB 和 AOF 的优点,Redis 4.0 之后新增了混合持久化的方式,因此我们在必须要进行持久化操作时,应该选择混合持久化的方式。
查询是否开启混合持久化可以使用
命令,执行结果如下图所示:
其中 yes 表示已经开启混合持久化,no 表示关闭,Redis 5.0 默认值为 yes。
如果是其他版本的 Redis 首先需要检查一下,是否已经开启了混合持久化,如果关闭的情况下,可以通过以下两种方式开启:
通过命令行开启
通过修改 Redis 配置文件开启
通过命令行开启
使用命令config set aof-use-rdb-preamble yes
执行结果如下图所示:
命令行设置配置的缺点是重启 Redis 服务之后,设置的配置就会失效。
通过修改 Redis 配置文件开启
在 Redis 的根路径下找到 redis.conf 文件,把配置文件中的
aof-use-rdb-preamble no
改为
aof-use-rdb-preamble yes
如下图所示:
配置完成之后,需要重启 Redis 服务器,配置才能生效,但修改配置文件的方式,在每次重启 Redis 服务之后,配置信息不会丢失。
需要注意的是,在非必须进行持久化的业务中,可以关闭持久化,这样可以有效地提升 Redis 的运行速度,不会出现间歇性卡顿的困扰。
14 使用分布式架构来增加读写速度
Redis 分布式架构有三个重要的手段:
- 主从同步
- 哨兵模式
- Redis Cluster 集群
使用主从同步功能我们可以把写入放到主库上执行,把读功能转移到从服务上,因此就可以在单位时间内处理更多的请求,从而提升的 Redis 整体的运行速度。
而哨兵模式是对于主从功能的升级,但当主节点奔溃之后,无需人工干预就能自动恢复 Redis 的正常使用。
Redis Cluster 是 Redis 3.0 正式推出的,Redis 集群是通过将数据分散存储到多个节点上,来平衡各个节点的负载压力。
Redis Cluster 采用虚拟哈希槽分区,所有的键根据哈希函数映射到 0~16383 整数槽内,计算公式:
slot = CRC16(key) & 16383
每一个节点负责维护一部分槽以及槽所映射的键值数据。这样 Redis 就可以把读写压力从一台服务器,分散给多台服务器了,因此性能会有很大的提升。
在这三个功能中,我们只需要使用一个就行了,毫无疑问 Redis Cluster 应该是首选的实现方案,它可以把读写压力自动地分担给更多的服务器,并且拥有自动容灾的能力。
15 持久化
一个典型的配置如下:
127.0.0.1:6379> config get *append*
1) "no-appendfsync-on-rewrite"
2) "no"
3) "appendonly"
4) "yes"
5) "appendfsync"
6) "everysec"
bgrewriteaof机制,在一个子进程中进行aof的重写,从而不阻塞主进程对其余命令的处理,同时解决了aof文件过大问题。
有一种情况是同时在执行bgrewriteaof操作和主进程写aof文件的操作,两者都会操作磁盘,而bgrewriteaof往往会涉及大量磁盘操作,这样就会造成主进程在写aof文件的时候出现阻塞的情形,有两种思路解决:
- 将no-appendfsync-on-rewrite设置为yes. 这样可以避免与appendfsync竞争磁盘资源,但是在rewrite期间的AOF有丢失的风险。
- 给当前Redis实例添加slave节点,当前节点设置为master, 然后master节点关闭AOF,slave节点开启AOF。这样的方式的风险是如果master挂掉,尚没有同步到slave的数据会丢失。
no-appendfsync-on-rewrite参数设置为no,是最安全的方式,不会丢失数据,但是要忍受潜在的阻塞的问题;如果设置为yes相当于将appendfsync设置为no,这说明写aof文件的时候并没有真正刷盘,只是写入了内存缓冲区aof_buf,因此这样并不会造成阻塞(因为没有竞争磁盘),但是如果这个时候redis挂掉,就会丢失数据。丢失多少数据呢?在linux的操作系统的默认设置下,最多可能丢失30s的数据。
一种可能的方案:
- 在master节点设置将no-appendfsync-on-rewrite设置为yes,同时将auto-aof-rewrite-percentage参数设置为0关闭主动重写。
- 为了防止AOF文件越来越大,可以在服务器上设置定时任务,在低峰期执行bgrewriteaof命令完成每日一次的AOF重写。
- 在重写时为了避免硬盘空间不足或者IO使用率高影响重写功能,可以添加硬盘空间和IO使用率报警确保aof重写机制能正常运行。
16 绑定 CPU
很多时候,我们在部署服务时,为了提高服务性能,降低应用程序在多个 CPU 核心之间的上下文切换带来的性能损耗,通常采用的方案是进程绑定 CPU 的方式提高性能。
但在部署 Redis 时,如果你需要绑定 CPU 来提高其性能,我建议你仔细斟酌后再做操作。
为什么?
因为 Redis 在绑定 CPU 时,是有很多考究的,如果你不了解 Redis 的运行原理,随意绑定 CPU 不仅不会提高性能,甚至有可能会带来相反的效果。
我们都知道,一般现代的服务器会有多个 CPU,而每个 CPU 又包含多个物理核心,每个物理核心又分为多个逻辑核心,每个物理核下的逻辑核共用 L1/L2 Cache。
而 Redis Server 除了主线程服务客户端请求之外,还会创建子进程、子线程。
其中子进程用于数据持久化,而子线程用于执行一些比较耗时操作,例如异步释放 fd、异步 AOF 刷盘、异步 lazy-free 等等。
如果你把 Redis 进程只绑定了一个 CPU 逻辑核心上,那么当 Redis 在进行数据持久化时,fork 出的子进程会继承父进程的 CPU 使用偏好。
而此时的子进程会消耗大量的 CPU 资源进行数据持久化(把实例数据全部扫描出来需要耗费CPU),这就会导致子进程会与主进程发生 CPU 争抢,进而影响到主进程服务客户端请求,访问延迟变大。
这就是 Redis 绑定 CPU 带来的性能问题。
那如何解决这个问题呢?
如果你确实想要绑定 CPU,可以优化的方案是,不要让 Redis 进程只绑定在一个 CPU 逻辑核上,而是绑定在多个逻辑核心上,而且,绑定的多个逻辑核心最好是同一个物理核心,这样它们还可以共用 L1/L2 Cache。
当然,即便我们把 Redis 绑定在多个逻辑核心上,也只能在一定程度上缓解主线程、子进程、后台线程在 CPU 资源上的竞争。
因为这些子进程、子线程还是会在这多个逻辑核心上进行切换,存在性能损耗。
如何再进一步优化?
可能你已经想到了,我们是否可以让主线程、子进程、后台线程,分别绑定在固定的 CPU 核心上,不让它们来回切换,这样一来,他们各自使用的 CPU 资源互不影响。
Redis 在 6.0 版本已经推出了这个功能,我们可以通过以下配置,对主线程、后台线程、后台 RDB 进程、AOF rewrite 进程,绑定固定的 CPU 逻辑核心:
# Redis Server 和 IO 线程绑定到 CPU核心 0,2,4,6
server_cpulist 0-7:2
# 后台子线程绑定到 CPU核心 1,3
bio_cpulist 1,3
# 后台 AOF rewrite 进程绑定到 CPU 核心 8,9,10,11
aof_rewrite_cpulist 8-11
# 后台 RDB 进程绑定到 CPU 核心 1,10,11
# bgsave_cpulist 1,10-1
如果你使用的正好是 Redis 6.0 版本,就可以通过以上配置,来进一步提高 Redis 性能。
这里我需要提醒你的是,一般来说,Redis 的性能已经足够优秀,除非你对 Redis 的性能有更加严苛的要求,否则不建议你绑定 CPU。
绑定 CPU 需要对计算机体系结构有比较清晰的了解,否则要谨慎操作。
17 内存 swap
如果你发现 Redis 突然变得非常慢,每次的操作耗时都达到了几百毫秒甚至秒级,那此时你就需要检查 Redis 是否使用到了 swap,在这种情况下 Redis 基本上已经无法提供高性能的服务了。
操作系统使用虚拟内存,有时候为了缓解内存不足对应用程序的影响,允许把一部分进程内存中的数据交换到磁盘上,以达到应用程序对内存使用的缓冲,这些内存数据被换到磁盘上的区域,就是 swap。
问题就在于,当内存中的数据被换到磁盘上后,Redis 再访问这些数据时,就需要从磁盘上读取,访问磁盘的速度要比访问内存慢几百倍!
尤其是针对 Redis 这种对性能要求极高、性能极其敏感的数据库来说,这个操作延时是无法接受的。
此时,你需要检查 Redis 机器的内存使用情况,确认是否存在使用了 Swap。
你可以通过以下方式来查看 Redis 进程是否使用到了 Swap:
# 先找到 Redis 的进程 ID
$ ps -aux | grep redis-server
# 查看 Redis Swap 使用情况
$ cat /proc/$pid/smaps | egrep '^(Swap|Size)'
输出结果如下:
Size: 1256 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 63488 kB
Swap: 0 kB
Size: 132 kB
Swap: 0 kB
Size: 65404 kB
Swap: 0 kB
Size: 1921024 kB
Swap: 0 kB
...
这个结果会列出 Redis 进程的内存使用情况。(或者也可以用 vmstat 命令查看 swap 的情况)
每一行 Size 表示 Redis 所用的一块内存大小,Size 下面的 Swap 就表示这块 Size 大小的内存,有多少数据已经被换到磁盘上了,如果这两个值相等,说明这块内存的数据都已经完全被换到磁盘上了。
如果只是少量数据被换到磁盘上,例如每一块 Swap 占对应 Size 的比例很小,那影响并不是很大。如果是几百兆甚至上 GB 的内存被换到了磁盘上,那么你就需要警惕了,这种情况 Redis 的性能肯定会急剧下降。
此时的解决方案是:
- 增加机器的内存,让 Redis 有足够的内存可以使用
- 整理内存空间,释放出足够的内存供 Redis 使用,然后释放 Redis 的 Swap,让 Redis 重新使用内存
释放 Redis 的 Swap 过程通常要重启实例,为了避免重启实例对业务的影响,一般会先进行主从切换,然后释放旧主节点的 Swap,重启旧主节点实例,待从库数据同步完成后,再进行主从切换即可。
可见,当 Redis 使用到 Swap 后,此时的 Redis 性能基本已达不到高性能的要求(你可以理解为武功被废),所以你也需要提前预防这种情况。
预防的办法就是,需要对 Redis 机器的内存和 Swap 使用情况进行监控,在内存不足或使用到 Swap 时报警出来,及时处理。