生产级Redis 高并发分布式锁实战1:高并发分布式锁如何实现 https://www.cnblogs.com/yizhiamumu/p/16556153.html
生产级Redis 高并发分布式锁实战2:缓存架构设计问题优化 https://www.cnblogs.com/yizhiamumu/p/16556667.html
总结篇3:redis 典型缓存架构设计问题及性能优化 https://www.cnblogs.com/yizhiamumu/p/16557996.html
总结篇4:redis 核心数据存储结构及核心业务模型实现应用场景 https://www.cnblogs.com/yizhiamumu/p/16566540.html
DB\redis\zookeeper分布式锁设计 https://www.cnblogs.com/yizhiamumu/p/16663243.html
在缓存和数据库双写场景下,一致性是如何保证的 https://www.cnblogs.com/yizhiamumu/p/16686751.html
如何保证 Redis 的高并发和高可用?讨论redis的单点,高可用,集群 https://www.cnblogs.com/yizhiamumu/p/16586968.html
分布式缓存应用场景与redis持久化机制 https://www.cnblogs.com/yizhiamumu/p/16702154.html
Redisson 源码分析及实际应用场景介绍 https://www.cnblogs.com/yizhiamumu/p/16706048.html
Redis 高可用方案原理初探 https://www.cnblogs.com/yizhiamumu/p/16709290.html
RedisCluster集群架构原理与通信原理 https://www.cnblogs.com/yizhiamumu/p/16704556.html
redis 基准性能测试与变慢优化 https://www.cnblogs.com/yizhiamumu/p/16712463.html
Redis过期策略以及Redis的内存淘汰机制 https://www.cnblogs.com/yizhiamumu/p/16725009.html
Raft协议 https://www.cnblogs.com/yizhiamumu/p/16737578.html
在日常使用Redis时,我们会遇到过以下问题:
- Redis服务过去一直很稳定,突然从某个时间点开始变慢了?
- 访问Redis相同的命令,有时响应很快,有时却非常慢?
- 访问Redis突然卡住了,过一会又自动恢复了,这也导致业务请求出现很多的毛刺?
首先,我们需要对 Redis 进行基准的性能测试,以便对目前 Redis 服务在当前环境服务器上的基准性能有一个把控。
什么是基准性能?
基准性能指在一台负载正常的服务器上,访问Redis的最大的响应延迟和平均响应延迟分别是怎样的?
因为Redis 在不同的软硬件环境下,它的性能表现差别特别大,不同主频型号的CPU、不同的SSD硬盘,都会极大影响Redis的性能表现。服务器配置比较低时延迟为 10ms 时,才认为 Redis响应变慢了,但是如果配置比较高,那么可能延迟是 1ms 时就可以认为 Redis 变慢了。
所以不能直接的参考官方提供的响应延迟测试,来判断自己的 Redis服务是否变慢。
那么有哪些方式可以完成基准测试呢?
Redis自带的工具可以帮助我们。
方式一:redis-cli --intrinsic-latency
为了避免业务测试服务器到 Redis 服务器之间的网络延迟,需要直接在 Redis 服务器上测试实例的响应延迟情况。
执行以下命令,就可以测试出这个实例 120 秒内的最大响应延迟:
shell> redis-cli -h 127.0.0.1 -p 6379 --intrinsic-latency 120
Max latency so far: 4 microseconds.
Max latency so far: 5 microseconds.
Max latency so far: 15 microseconds.
Max latency so far: 23 microseconds.
Max latency so far: 64 microseconds.
Max latency so far: 196 microseconds.
Max latency so far: 245 microseconds.
Max latency so far: 246 microseconds.
Max latency so far: 254 microseconds.
Max latency so far: 259 microseconds.
29298480 total runs (avg latency: 4.0958 microseconds / 40957.76 nanoseconds per run).
Worst run took 63x longer than the average latency.
从输出结果可以看到,这 120 秒内的最大响应延迟为 259 微秒(0.259毫秒)。
方式二:redis-benchmark
Redis-benchmark是Redis官方自带的Redis性能测试工具,可以有效的测试Redis服务的性能.
shell> redis-benchmark -h 127.0.0.1 -p 6379 -t set,get -c 500 -n 100000
====== SET ======
100000 requests completed in 1.02 seconds
500 parallel clients
3 bytes payload
keep alive: 1
0.00% <= 1 milliseconds
0.05% <= 2 milliseconds
99.09% <= 3 milliseconds
99.88% <= 4 milliseconds
100.00% <= 4 milliseconds
97847.36 requests per second
====== GET ======
100000 requests completed in 1.02 seconds
500 parallel clients
3 bytes payload
keep alive: 1
0.00% <= 1 milliseconds
0.05% <= 2 milliseconds
99.29% <= 3 milliseconds
99.92% <= 4 milliseconds
100.00% <= 4 milliseconds
97656.24 requests per second
该命令对set和get命令的操作响应时间进行测评,并发500个执行10w次操作。
结果,set的QPS达到了97847,响应时间都在4ms以内;get的QPS达到了97656,最大响应时间也在4ms以内;
清楚了基准性能测试方法,我们就可以找到具体的redis 实例进行对比测试,进而可以判断 Redis 是否真的变慢了:
- 在相同配置的服务器上,测试一个正常 Redis 实例的基准性能
- 找到可能变慢的 Redis 实例,测试这个实例的基准性能
- 对比这个实例的运行延迟与正常 Redis 基准性能,如果性能差距在两倍以上,就可以认为这个 Redis 服务确实响应变慢了
如果确认是 Redis服务变慢了,那如何排查是哪里发生了问题呢?
由于Redis命令使用不当造成的性能影响
- redis 慢日志系统
- keys 命令正则匹配
- 大量使用复杂度高的命令
- 存储使用bigkey
- 不合理使用批处理命令
- 大批量key 集中过期
- 预估内存不足,使用内存达到上限
- 集群过载,实际请求量超出预期
一、Redis慢日志功能
分析Redis访问变慢,其中有个最基础的方法就是先去看Redis是否有慢日志【就像MySQL的慢SQL一样】。
Redis提供了一个简单的慢命令统计记录功能,它会记录有哪些命令在执行时耗时较长。
Redis慢日志功能由两个核心参数控制:
slowlog-log-slower-than 1000
#慢日志命令执行阈值,这里指超过1ms就会被记录【单位为微秒】
slowlog-max-len 4096
#保留慢日志命令的个数,类似一个先进先出的队列,超过4096个最早的就会被清理
Redis的这个慢日志功能比较粗糙简单,有个严重的不足:没有持久化记录能力。
由于Redis的慢日志记录都在内存中,不像MySQL会持久化到文件里,那么如果慢日志产生较快,即使设置的slowlog-max-len比较大也会很快被填满,诊断问题时也就不能统计到那个时间段产生的所有慢命令详情。
为了避免产生的慢日志被清理,目前一个折中的解决方案是写一个收集程序周期性的将新增慢命令查出并记录到MySQL或者本地文件中,以备事后分析。
但是这个频率一般都是分钟级,Redis处理的吞吐能力又太大,在慢命令较多的情况下往往也不能全部记录下来。
配置好慢日志相关阈值后,可以执行以下命令查询最近的慢日志记录了:
127.0.0.1:6379> SLOWLOG get 5
1) 1) (integer) 42343
2) (integer) 1653659194 #慢日志产生的时间戳
3) (integer) 73536 #慢日志执行的耗时
4) 1) "KEYS" #慢日志命令详情
2) "permission::userMenuList:*"
5) "192.168.1.11:20504" #慢日志命令发起来源IP【4.0及以后版本支持】
6) ""2) 1) (integer) 42342
2) (integer) 1653659194
3) (integer) 73650
4) 1) "KEYS"
2) "userPermission:*"
5) "192.168.1.10:20362"
6) ""
3) 1) (integer) 42341
2) (integer) 1653659193
3) (integer) 81505
4) 1) "KEYS"
2) "userRole:*"
5) "192.168.1.13:19926"
6) ""
二、不合理使用
1.使用keys命令进行正则匹配
Keys的正则匹配是阻塞式的、全量扫描过滤,这对于单线程服务的Redis来说是致命的,仅仅几十万个Key的匹配查询在高并发访问下就有可能将Redis打崩溃!
redis> SLOWLOG get 5
1) 1) (integer) 42343
2) (integer) 1653659194
3) (integer) 73536
4) 1) "KEYS"
2) "Testper::userList:*"
5) "192.168.1.10:20504"
6) ""
2) 1) (integer) 42342
2) (integer) 1653659194
3) (integer) 73650
4) 1) "KEYS"
2) "TestuserPermission:*"
5) "192.168.1.11:20362"
6) ""
3) 1) (integer) 42341
2) (integer) 1653659193
3) (integer) 81505
4) 1) "KEYS"
2) "TestuserRole:*"
5) "192.168.1.12:19926"
6) ""
上述示例中使用Keys来模糊查询某些Key,每次的执行都在70ms以上,严重影响了正常的Redis响应时长和吞吐。
针对这种问题的一个解决方案是使用scan代替keys。
这是一个查询迭代命令,用于迭代当前数据库中的缓存数据。它是一个基于游标的迭代器,每次被调用之后, 都会向用户返回一个新的游标, 用户在下次迭代时需要使用这个新游标作为Scan命令的游标参数, 以此来延续之前的迭代过程。具体的命令语法这里不再详述。
2.大量使用了复杂度较高的命令
(1)应用中高频使用了 O(N) 及以上复杂度的命令,例如:SUNION、SORT、ZUNIONSTORE、ZINTERSTORE 聚合类命令。SORT命令的时间复杂度:O(N+M*log(M)), N 为要排序的列表或集合内的元素数量, M 为要返回的元素数量。
这种导致Redis请求变慢的原因是,Redis 在操作数据排序时,时间复杂度过高,要花费更多的 CPU计算资源。
(2)使用 O(N) 复杂度的命令,但 N 的值非常大,比如hgetall、smembers、lrange、zrange等命令。
这种变慢的原因在于,Redis 一次需要返回给客户端的数据过多,需要花费更多时间在数据组装和网络传输中。对于hgetall、smembers这种命令,需要警惕项目刚上线之初hash、set或者list存储的成员个数较少,但是随着业务发展成员数量极有可能会膨胀的非常大,如果仍然采用上述命令不加控制,会极大拖累整个Redis服务的响应时间。
针对这两种情况还都可以从资源使用率层面来分析,如果应用程序访问 Redis 的QPS不是很大,但 Redis 实例的 CPU 使用率却很高,那么很有可能是使用了复杂度过高的命令导致的。
因为Redis 是单线程处理请求的,如果你经常使用以上复杂度较高的命令,那么当 Redis 处理程序请求时,一旦前面某个命令发生耗时较长,就会导致后面的请求发生阻塞排队,对于应用程序来说,响应延迟也会变长。
3.存储使用了bigkey
在分析慢日志发现很多请求并不是复杂度高的命令,都是一些del、set、hset等的低复杂度命令,那么就要评估是否写入了大key。
在往Redis写入数据时,需要为新数据分配内存块,相对应的,当删除数据时,Redis也会释放对应的内存空间。如果一个 key 写入Redis的值非常大,那么在分配内存时就会相对比较耗时。同样的当删除这个 key 时,释放内存也会比较耗时,这种被称为bigKey。
当然这个描述仍然比较宽泛,因为Redis中的数据库结构类型比较多,更完善的一些说法可以这么定义:将含有较大数据或含有大量成员、列表数的Key定义为bigkey。
我们一般要求研发使用Redis时,对于String类型Value大小不要超过1KB。
大Key带来的问题比较多,主要有下面几种情况:
- 由于大Key的内存分配及释放开销变大,直接影响就是导致应用访问Redis的响应变慢;
- 删除时会造成较长时间的阻塞并有可能造成集群主备节点切换【4.0之前的版本有这个问题】;
- 内存占用过多甚至达到maxmemory配置,会造成新写入阻塞或一些不应该被提前删除的Key被逐出,甚至导致OOM发生;
- 并发读请求因为Key过大会可能打满服务器带宽,如果单机多实例部署则同时会影响到该服务器上的其它服务【假设一个bigkey为1MB,客户端每秒访问量为1000,那么每秒产生1000MB的流量】;
- 运维麻烦,比如RedisCluster的数据跨节点均衡,因为均衡迁移原理是通过migrate命令来完成的,这个命令实际是通过dump + restore + del三个命令组合成原子命令完成,如果是bigkey,可能会使迁移失败,而且较慢的migrate也会阻塞Redis正常请求;
- 分片集群RedisCluster中的出现严重的数据倾斜,导致某个节点的内存使用过大;
那么对于已经写入的数据,如何分析找出里面的bigkey进行优化呢?可以通过Redis官方客户端redis-cli的bigkeys参数来定位大Key分布。
shell> redis-cli -h 127.0.0.1 -p 18708 -a xxxx --bigkeys -i 0.01
[00.00%] Biggest string found so far 'urlcount:www.guprocessorSuccessMid' with 1 bytes
[00.01%] Biggest string found so far 'TestDomain:www:config:scheduler' with 3847 bytes
[00.03%] Biggest string found so far 'TestDomain:www:config:scheduler' with 211306 bytes
[00.88%] Biggest set found so far 'specialTestJobSet:www' with 20 members
[01.69%] Biggest list found so far 'TestDomain:www:urlList' with 9762 items
[07.13%] Biggest list found so far 'TestDomain:bx:urlList' with 457676 items
[07.39%] Biggest set found so far 'specialTestJobSet:www' with 100 members
[13.99%] Biggest string found so far 'TestDomain:wwwe:config:scheduler' with 540731 bytes
[18.74%] Biggest set found so far 'TestJobSet' with 300 members
[58.09%] Biggest string found so far 'TestDomain:wwwrt:config:scheduler' with 739024 bytes
[64.19%] Biggest string found so far 'TestDomain:bx:config:scheduler' with 1335468 bytes
-------- summary -------
Sampled 62522 keys in the keyspace!
Total key length in bytes is 2471471 (avg len 39.53)
Biggest list found 'TestDomain:bx:urlList' has 457676 items
Biggest string found 'TestDomain:bx:config:scheduler' has 1335468 bytes
Biggest set found 'TestJobSet' has 300 members
208 lists with 2408539 items (00.33% of keys, avg size 11579.51)
0 hashs with 0 fields (00.00% of keys, avg size 0.00)
62283 strings with 32642667 bytes (99.62% of keys, avg size 524.10)
0 streams with 0 entries (00.00% of keys, avg size 0.00)
31 sets with 1354 members (00.05% of keys, avg size 43.68)
0 zsets with 0 members (00.00% of keys, avg size 0.00)
从输出结果我们可以看到,每种数据类型所占用的最大长度或含有最多成员的 key 是哪一个,以及每种数据类型在整个实例中的占比和平均大小及成员数量。
其实,使用这个命令的原理就是 Redis 在内部执行了 SCAN 命令,遍历整个实例中所有的 key,然后针对 key 的类型,分别执行 STRLEN、HLEN、LLEN、SCARD、ZCARD 命令,来获取 String 类型的长度、集合类型(Hash、List、Set、ZSet)的成员个数.
注意,使用该--bigkeys进行大key的统计时要注意:
- 对于集合类型的Hash、List、Set、ZSet仅仅统计的是包含的成员个数,个数多并代表占用的内存大,仅仅是个参考;
- 对于高并发访问的集群,使用该命令会造成QPS增加,带来额外的性能开销,建议在业务低峰或者从节点进行扫描。
那针对 bigkey 导致延迟的问题,有什么好的解决方案呢?
1)对大Key进行拆分
如将一个含有数万成员的HASH Key拆分为多个HASH Key,并确保每个Key的成员数量在合理范围。特别是在RedisCluster架构下中,大Key的拆分对各节点间的内存平衡能够起到显著作用。
2)优化使用删除Key的命令。
Redis自4.0起提供了UNLINK命令,该命令可以替换DEL,能够以非阻塞的方式放到后台线程中缓慢逐步的清理大Key所占用的内存块,从而减轻了对Redis的影响;
Redis 6.0 以上版本,建议开启 lazy-free 机制(配置参数:lazyfree-lazy-user-del = yes,6.2版本之后默认开启了),这样在 DEL删除大Key时,释放内存的动作也是在后台线程中执行的;
3)尽量不写入大Key首先评估使用其他的存储形式,比如文档性数据库 MongoDB等;如果还无法避免使用BigKey,可以将大Key进行压缩后存储,并尽量根据业务精简Value的内容;建议单个Key的大小不要超过1K;
4.不合理使用批处理命令
有不少关于批量处理的一些优化,如使用mget、mset代替多次的get、set等,减少网络IO开销以此提高redis的处理效率,特别是对于一些php短连接效果尤其明显。
但是对于这些批量处理命令原生的mget、mset,非原生命令如pipeline,一定要注意控制单次批量操作的元素个数,否则会阻塞其它请求命令!建议控制在500以内。
针对该种场景的优化方案:
- 降低使用 O(N) 以上复杂度的命令,对于数据的计算聚合操作等可以适当的放在应用程序侧处理;
- 使用O(N) 复杂度的命令时,保证 N 尽量的小(推荐 N <= 500),每次处理的更小的数据量,降低阻塞的时长;
- 对于Hgetall、Smembers操作的集合对象,应从应用层面保证单个集合的成员个数不要过大,可以进行适当的拆分等。
5.大批量Key集中过期
不知道你是否会经常遇见这样的反馈,应用没有上线变更调整,但是访问的Redis经常出现超时的问题。
这种问题大部分表现为:超时问题出现的时间点有规律,比如每隔一个小时出现一次,或者每天零点过后发生。
如果出现了这种情况,那么需要从两个方面排查一下:
- 是否有定时任务的脚本程序,定时或者间隔性的操作Redis
- Redis的Key数量出现集中过期清理
第一种情况不做过多解读。
我们重点分析下Redis的Key数量为什么会出现集中过期,集中过期为什么会造成Redis的访问变慢。
这就需要我们了解 Redis 的Key过期策略是怎样的。Redis 的过期数据采用被动过期 + 主动过期两种策略:
- 被动过期:只有应用发起访问某个key 时,才判断这个key是否已过期,如果已过期,则从Redis中删除
- 主动过期:在Redis 内部维护了一个定时任务,默认每隔 100 毫秒(1秒10次)从全局的过期哈希表中随机取出 20 个 key,判断然后删除其中过期的 key,如果过期 key 的比例超过了 25%,则继续重复此过程,直到过期 key 的比例下降到 25% 以下,或者这次任务的执行耗时超过了 25 毫秒,才会退出循环
注意:Redis的key主动过期清理的定时任务,是在 Redis 主线程中执行的,也就意味着会阻塞正常的请求命令。
进一步说就是如果在执行主动过期的过程中,出现了需要大量删除过期 key 的请求,那么此时应用程序在访问 Redis 时,必须要等待这个过期任务执行结束,Redis 才可以继续处理新请求。此时现象就是上面说的应用访问 Redis 延时突然变大了。
特别是由于批量清理Key这个操作的命令是内部发起的并不会记录在慢日志中,但我们的应用程序却感知到了延迟变大,其实时间都花费在了删除过期 key 上,这种情况就经常被忽略。
如果确实是集中过期 key 导致的访问变慢,那么可以采用如下处理方案:
业务Key设置过期时间时,预计的过期时间加上一个随机过期时间段,比如5分钟,将集中过期时间打散,降低 Redis批量清理时的压力。
由于这种情况分析比较麻烦,强烈建议对过期key的数量进行监控,对于短时间过期较多key的情况进行预警,通过执行info命令获取过期Key数量【expired_keys】的统计值:
# Stats
total_connections_received:1359356
total_commands_processed:2705619999
instantaneous_ops_per_sec:157
total_net_input_bytes:232498789314
total_net_output_bytes:279219680360
instantaneous_input_kbps:11.01
instantaneous_output_kbps:17.07
rejected_connections:0
sync_full:2
sync_partial_ok:1
sync_partial_err:0
expired_keys:215099347
evicted_keys:0
keyspace_hits:984222771
keyspace_misses:610235483
pubsub_channels:1
pubsub_patterns:0
latest_fork_usec:9484
说明:expired_keys为一个累计值,可以在监控系统中配置为1分钟的增加值,当1分钟过期的key超过一定阈值时进行预警。
6.预估内存不足,使用的数据内存达到了最大值
由于服务器内存有限,一般使用Redis时都会配置当前实例可用的最大内存maxmemory,那么当使用的内存达到了 maxmemory 后,虽然配置了数据的自动淘汰策略,但是在此之后每次写入新数据,操作延迟都会变长。
核心原因在于,当 Redis 内存达到 maxmemory 后,每次写入新的数据之前,Redis 必须先从实例中剔除一部分数据,让整个实例的内存维持在 maxmemory 之下,然后才能把新数据写进来。很多小伙伴以为只要配置了maxmemory就可以了,实际上由于Redis特殊的清理策略,无法避免会对正常的使用造成影响!
为了降低内存自动清理对服务的影响,可以配置Redis的最大内存数据清理策略,主要有以下几种:
- allkeys-lru:清理最近最少使用(LRU)的Key,不管 key 是否设置了过期时间
- volatile-lru:清理最近最少使用(LRU)的Key,但是只回收有设置过期的Key
- allkeys-random:随机清理部分Key,不管 key 是否设置了过期时间
- allkeys-lfu:不管 key 是否设置了过期,清理访问频次最低的 key(4.0+版本支持)
- volatile-lfu:清理访问频次最低且设置了过期时间 key(4.0+版本支持)
- volatile-random:随机清理部分设置了过期时间的部分Key
- volatile-ttl:清理有设置过期的Key,尝试先回收离 TTL 最短时间的Key
- noeviction:不清理任何Key,当到达内存最大限制时,当客户端尝试执行命令时会导致更多内存占用时直接返回错误(大多数写命令,除了 DEL 和一些例外)。
我们需要根据实际的应用场景来选择使用哪种清理策略。比如有些业务用于存储强调准确性,即使访问有损了也不能逐出数据,那么就要配置noeviction;还有些业务是缓存,有些清理那些早期写入的Key,则可以选择volatile-lru或allkeys-lru。
常使用的是 allkeys-lru / volatile-lru 的淘汰策略,它们的处理逻辑是,每次从实例中随机取出一批 key(maxmemory-samples控制数量),然后淘汰一个最少访问的key,然后把剩余的 key 暂存到一个池子中,继续随机取一批 key,并与之前池子中的 key 比较,再淘汰一个最少访问的 key。以此循环往复,直到实例内存降到 maxmemory值以下才停止。所以这段时间是会影响新的数据写入的,应用层就会有超时或者请求响应变慢的问题发生。
针对内存达到上限的情况,可以采用如下优化方案:
- 合理预估内存占用,避免达到内存的使用上限。这里有两种方法可以参考:
(1)根据写入Key的类型、数量及平均大小计算预估,不同的数据类型有不同的数据结构及编码方式,后续开文专门介绍;
(2)写入一小部分比例的真实业务数据,然后进行预估。
- 设置合理的Key过期时间,满足业务的最小保留时间即可。
- 数据量过大建议拆分成多套Redis或者使用RedisCluster分片集群,建议单集群最大内存不超过20G。
- 数据清理策略改为随机模式,随机清理比 LRU 要快很多(不过这个要根据业务情况评定,业务优先满足原则)。
- 如果使用的是 Redis 4.0 及以上版本,开启 layz-free 机制,把淘汰 key 释放内存的操作放到后台线程中执行(配置 lazyfree-lazy-eviction = yes)
- 增加剩余可用内存的监控,提前预警并进行最大内存上限的扩容或者提前清理释放内存。
7.实际请求量超过了Redis的处理能力
一些大促活动时业务流量往往出现暴涨,很容易就会达到Redis的处理瓶颈。这种在业务上的表现除了访问Redis变慢,一些简单的命令如get、set也开始出现在慢日志中。
这时如果查看Redis的CPU使用情况,基本是100%的状态。
为了解决这种问题,就需要评估当前集群的处理吞吐力,每个Redis所在的服务器配置不一样,处理能力就不一样,参考官方的测评结果QPS 10W因此不具备实际的参考意义。
更由于每个Redis承载的服务模型不同,比如使用的命令类型、访问比例等,那么处理的吞吐也会有很大不同。
这种情况最好的方案就是业务上线前,可以模拟真实的业务进行压力测评,给出一个大概的吞吐处理能力。如果评估单节点无法承载过多请求,建议进行读写分离架构或者拆分为多套集群扩容.
最后就是不要忽略运维监控,可以对使用的CPU使用率、访问的QPS等进行有效监控,不管是性能指标、内存使用、持久化、网络连接等,提前发现是否达到集群的处理瓶颈,并决定是否进行扩容或架构调整。
Redis服务层的工作原理及使用
- redis 持久化:RDB,AOF
- 内存管理:碎片率过大,碎片整理
一.数据持久化的影响
为了保证 Redis 数据的安全性,我们可能会开启Redis的持久化将数据落盘,避免Redis服务崩溃或者服务器宕机导致的数据丢失。
Redis当前支持两种典型的持久化模式:RDB、AOF。
- RDB持久化,称为内存快照。这种模式是把当前Redis服务的内存数据在某一点dump生成快照保存到磁盘上的过程,由于是某一时刻的快照,开启快照后发起后所有操作命令都不会再被记录。
- AOF 持久化。AOF持久化以日志的形式记录Redis所执行的每个写操作,注意查询操作不会记录,可以打开磁盘文件看到每条详细的操作记录。
关于Redis持久化这里不做过多详细介绍,大家需要记住开启持久化后会对Redis的访问性能带来影响就行,后面会专文讲解两种持久化模式的细节。本文主要对持久化影响Redis访问响应进行分析说明。
1.RDB镜像落盘及AOF重写时的影响
Redis开始执行RDB或者AOF Rewrite后,主进程都会创建出一个子进程进行数据的持久化落盘操作。在这个过程中,则会调用操作系统的 fork 操作。
通过 fork 对内存数据的 copy-On-Write 机制最廉价的实现内存镜像。虽然内存是 copy on write 的,但是虚拟内存表是在 fork 的瞬间就需要分配,所以这个操作会造成主线程短时间的卡顿(停止所有读写操作),这个卡顿时间和当前 Redis 的内存使用量有关。
根据经验 GB 量级的 Redis 进行 fork 操作的时间在毫秒级。
如果这个Redis实例很大,CPU负载再高些,那么 fork 的耗时就会更长,甚至达到秒级,也就会严重影响 Redis 的访问响应时间。这时反映到业务层面表现就是仿佛Redis服务有一瞬间卡主了,所有的请求不再快速返回,大量的超时出现,然后一会突然又好了。
# latest_fork_usec 相关监控指标上一次fork操作耗时,单位微秒
redis> info stats
# Stats
total_connections_received:434441735
total_commands_processed:127367251467
instantaneous_ops_per_sec:6048
total_net_input_bytes:6089035011002
total_net_output_bytes:37379100018132
instantaneous_input_kbps:274.54
instantaneous_output_kbps:1738.93
rejected_connections:0
sync_full:43
sync_partial_ok:0
sync_partial_err:0
expired_keys:162619318
evicted_keys:0
keyspace_hits:122311115418
keyspace_misses:1269177349
pubsub_channels:1
pubsub_patterns:0
latest_fork_usec:254154
可以添加一个监控,如果发现这个耗时过长且频繁出现,就需要警惕了。
为了避免这种情况,可以采取以下优化方式:
- 关闭RDB和AOF的自动触发机器,避免业务高峰自动触发执行;
- 控制 Redis 使用内存大小,建议控制在20G 以下,因为执行 fork 的耗时与数据内存大小有关,数据越多,耗时会越久;
- 对于主从集群架构,建议关闭主库AOF,从库开启;对于有备份需求的集群,也可以在从库发起RDB备份操作;
- 合理配置 repl-backlog-size大小,降低主从全量重传【2.8版本之前的节点强烈建议升级】;
- 尽量不要使用虚拟机,fork 的耗时也与系统也有关,虚拟机比物理机耗时更长。
2.AOF持久化磁盘IO带来的影响
前文主要介绍了两种持久化过程中Fork操作对性能的影响,现在主要说下AOF持久化开启后对性能的影响。关于AOF持久化刷盘的三种策略【no/everysec/always】,这里不过多讲解,大家可以自行查阅资料。
当 Redis 开启 AOF持久化 后,两个主要动作:
- Redis 接收写命令后,把命令写入 AOF 文件缓冲区中(AOF write)
- 根据AOF 刷盘策略【everysec/always】,把 AOF 缓冲数据刷到磁盘上(AOF fsync)
AOF 持久化最耗时的刷盘操作,都是在后台线程执行的,但为什么也会影响到 Redis 主线程处理请求呢?
具体处理流程:
- 数据写入请求来后,主线程写入AOF缓冲区;
- AOF fsync后台线程每秒一次执行磁盘文件刷入操作,并记录最近一次同步时间;
- 主线程对比AOF同步时间
- 如果距离上次fsync同步时间在两秒内,主线程继续进行写入
- 如果距离上次fsync同步时间超过两秒(比如磁盘的 IO 负载很高导致同步写磁盘很慢,还在持续写入没有结束),主线程将会被阻塞, 直到同步完成。
如果fsync过慢,这时系统日志中会有如下提示信息:
Asynchronous AOF fsync is taking too long (disk is busy?). Writing the AOF buffer without waiting for fsync to complete, this may slow down Redis.
Redis自身也提供了相关的性能指标:aof_delayed_fsync
redis>info Persistence
# Persistence
loading:0
rdb_changes_since_last_save:4368195
rdb_bgsave_in_progress:0
rdb_last_save_time:1662581898
rdb_last_bgsave_status:ok
rdb_last_bgsave_time_sec:76
rdb_current_bgsave_time_sec:-1
aof_enabled:1
aof_rewrite_in_progress:0
aof_rewrite_scheduled:0
aof_last_rewrite_time_sec:56
aof_current_rewrite_time_sec:-1
aof_last_bgrewrite_status:ok
aof_last_write_status:ok
aof_current_size:64666745475
aof_base_size:7570210201
aof_pending_rewrite:0
aof_buffer_length:0
aof_rewrite_buffer_length:0
aof_pending_bio_fsync:0
aof_delayed_fsync:2
如果aof_delayed_fsync一直在增加,说明主线程频繁出现被阻塞情况,那么就需要关注是否持久化过慢造成Redis访问变慢了。
针对AOF持久化对Redis性能可能带来的影响可以参考如下几种解决方案:
- SSD 磁盘存储,确保AOF刷盘时有充足的IO能力
- 对于主从集群架构,建议关闭主库AOF,从库开启
- 将no-appendfsync-on-rewrite参数设置为yes, 确保aof文件rewrite期间不做fsync操作,减少IO争用
- 单台服务器不要部署过多持久化实例节点,避免磁盘IO争抢带来持久化压力
二、内存碎片过大及整理带来的性能损耗
Redis 一般所有数据都在内存中,当应用频繁进行修改时,就会产生内存碎片。过高的内存碎片率,不仅会浪费内存资源还会影响请求处理的效率。
那么,是什么原因导致Redis 产生碎片的呢?原理是什么,能避免吗?
当前Redis 都默认使用jemalloc内存分配器来分配内存,它一般是按固定大小来分配内存空间,而不会按照应用程序申请的内存大小给实际分配。当程序申请的内存大小最接近某个固定值时,如8 byte、16 byte,…,2KB、4KB 等,jemalloc 会给它分配相应大小的空间。这样的方式好处是为了减少分配次数。
假设Redis申请一个 10 byte的内存空间存储数据,jemalloc 会分配 16 byte,此时,如果应用还要再写入 4 byte的数据,Redis 就不用再向操作系统申请空间了,避免了一次额外分配操作开销。
所以Redis每次分配的内存空间一般都会比申请的实际需求空间大一些,这种分配方式就自然会导致形成碎片。
目前Redis内存的分配机制,碎片问题无法完全避免。
Redis 的内存利用率的高低除了成本外,也会直接影响到 Redis 运行效率的高低。
我们可以使用如下命令查看Redis内存使用、碎片率、分配器版本等详细信息:
redis> info Memory
used_memory:6617819416
used_memory_human:6.16G
used_memory_rss:9788588032
used_memory_rss_human:9.12G
...
rss_overhead_ratio:1.00
rss_overhead_bytes:-21159936
mem_fragmentation_ratio:1.48
mem_fragmentation_bytes: 3250855264
...
mem_allocator:jemalloc-5.1.0
...
mem_fragmentation_ratio 就是Redis 当前的内存碎片率大小,碎片率计算方法:
mem_fragmentation_ratio=used_memory_rss/used_memoryused_memory 表示存储的数据实际占用内存的大小,而used_memory_rss 指操作系统分配给 Redis进程服务的实际大小,也就是使用top命令查看Redis进程占用的内存。一般当 mem_fragmentation_ratio>1.5 时,就说明内存碎片率已经超过了50%,此时建议采取措施来降低内存碎片大小。
如何清理内存碎片呢?redis 根据版本的不同有不同的处理方式:
- Redis 4.0 以前的低版本,只能通过重启实例来解决,不能自动配置回收
- 从 4.0版本以后,提供了一种内存碎片自动回收的方法,可以通过配置动态开启碎片整理
但要注意:开启内存碎片整理,会导致 Redis 服务性能下降。
Redis 的碎片整理工作是在主线程中执行的,当其进行碎片整理时,操作系统会把多份数据拷贝到新位置以把原有空间释放出来,这会带来时间开销,而这个过程就会阻塞Redis处理请求。
为了降低碎片整理带来的性能影响,Redis 为自动内存碎片整理功机制提供了多个参数,具体有:
activedefrag yes #是否开启碎片整理
active-defrag-ignore-bytes 500mb #碎片大小超过 500MB 时才会触发整理
active-defrag-threshold-lower 20 #碎片大小占操作系统分配总空间比超过 20% 时触发整理
active-defrag-cycle-min 15 #碎片整理过程占用的CPU比例不低于 15%,保证整理可以正常执行
active-defrag-cycle-max 70 #碎片整理过程占用的CPU比例不高于70%,一旦超过就暂停整理,避免大量的内存拷贝等整理过程占用过多的CPU进而影响正常请求
active-defrag-max-scan-fields 500 #碎片整理过程中,对于 Hash、List、Set、ZSet 等成员集合类型一次扫描的元素数量
在开启碎片自动整理时,一定要优先评估当前 Redis 服务的负载状态,以及应用程序可接受的响应延迟。
合理设置碎片整理的参数值和回收时间段【比如放到凌晨程序定时触发】,来尽可能降低碎片整理期间对Redis服务的影响。
操作系统参数配置与优化
- swap 被使用
- 网卡被打爆
- 磁盘io 能力不足
- 系统参数配置不合理
- CPU 绑核及主频影响
- 驱动版本过低或应用配置不合理
一、服务器预留足够内存,监控SWAP使用
Swap是操作系统层面行为,指当服务器内存不足时,会将原本在内存中的一部分数据拿出放入磁盘,如果再次访问这部分数据就会响应很慢,因为磁盘的访问速度是远远不如内存的。
Redis作为内存数据库,所有的数据默认都是在内存中,不存在一部分在内存一部分在磁盘中的情况,除非被迫发生了SWAP。
说明:Redis在2.6版本之前有个VM【虚拟内存】特性,可以支持数据存放在内存和磁盘中,不过带来的性能波动影响太大,因此被废弃了。
官方VM废弃说明:https://redis.io/docs/reference/internals/internals-vm/
可以通过以下方式来查看 Redis 进程是否使用到了 Swap:
1.获取redis对应的进程id
shell> redis-cli info | grep process_id
2.查看 Swap 使用情况
shell>cat /proc/$pid/smaps | egrep '^(Swap|Size)'
#输出结果如下:
Size: 1492 kB
Swap: 0 kB
Size: 32 kB
Swap: 0 kB
Size: 2196 kB
Swap: 0 kB
Size: 2048 kB
Swap: 0 kB
Size: 4 kB
Swap: 0 kB
Size: 1576 kB
上述中size代表Redis进程占用的一块内存空间大小,并对应一个Swap。
Swap后的数字表示该内存空间有多少已经被换到磁盘上了,如果两者相等,则代表这块内存空间的数据全都被换到了上了。
针对使用swap的解决方案可以参考如下:
- 提高Redis所在服务器的内存并预留可用内存,建议剩余可用内存至少保留20%以上;
- Redis单独部署或降低单机部署实例数量,不和其他应用程序混合部署,避免多服务争用内存导致Redis数据被swap到磁盘上。
平时对Redis所在服务器的剩余可用内存及Swap 使用情况进行监控,在内存不足或使用到 Swap 时报警出来,及时干预处理。
二、使用万兆网卡,避免网络带宽打满
Redis 的高性能,一是数据都在内存,二就在于网络 IO 了。
网络带宽过载的情况下,比如带宽被打满,那么服务器在 TCP 层和网络层就会出现数据包发送延迟、丢包等情况。
如果确实出现这种情况,我们需要及时核对原因,主要有以下几个:
- 某个Redis服务访问量过大,可能QPS高再叠加操作的Key过大,导致网络满载;
- 所在服务器网卡上限过小,如千兆网卡或者虚拟机限速200MB等;
- 服务器网卡/网线/驱动等问题,导致万兆的网卡降频为千兆或者被限流。
查看网卡速度:ethtool eth0
针对网络过载可以采用以下方案来解决:
- 降低单机部署Redis实例个数,打散重度使用网络带宽的Redis服务到多台服务器;
- 对Redis服务使用的网络带宽进行监控,可以关注性能指标:instantaneous_input_kbps、instantaneous_output_kbps
- 使用万兆网卡的服务器,并添加对带宽上限【警惕网卡从万兆降为千兆】、网络带宽使用、丢包情况的监控;
- 遵守Redis使用规范,比如控制写入Redis中的VALUE大小、限制使用smembers或hgetall等操作的集合成员个数等。
三、根据场景选择是否使用SSD磁盘
要根据自己的实际场景判断,比如使用单节点且用于缓存服务的情形,就不需要使用SSD磁盘。
但是如果希望使用Redis的持久化能力来保证数据安全,那么磁盘IO能力就不得不重视了。
Redis中对IO比较敏感的操作主要有下面几类:
- a.AOF持久化,相关磁盘操作有:AOF命令落盘、AOF文件重写;
- b.RDB持久化,相关磁盘操作有:主从复制主节点RDB生成快照、从节点加载RDB文件、备份触发RDB快照、配置触发自动RDB快照
上面列出的都会严重依赖磁盘IO能力,特别是单机部署多Redis实例的情况,如果磁盘IO能力不足,将会严重影响Redis的性能。
四、系统参数配置
1.内存分配策略参数vm.overcommit_memory
Redis启动给出Warning提示:
WARNING overcommit_memory is set to 0!
Background save may fail under low memory condition.
To fix this issue add ‘vm.overcommit_memory = 1’ to /etc/sysctl.conf
and then reboot or run the command ‘sysctl vm.overcommit_memory=1’ for this to take effect.
什么是Overcommit?
Linux对大部分申请内存的请求都回复"yes",以便能跑更多更大占用内存的程序。因为申请内存后,并不会马上使用内存。这种技术叫做Overcommit。当linux发现内存不足时,会发生OOM killer(OOM=out-of-memory)。它会选择杀死一些进程(用户态进程,不是内核线程),以便释放内存。
overcommit_memory的几个主要值的说明:
0:表⽰内核将检查是否有⾜够的可⽤内存供应⽤进程使⽤;如果有⾜够的可⽤内存,内存申请允许;否则,内存申请失败,并把错误返回给应⽤进程;
1: 表⽰内核允许分配所有的物理内存,⽽不管当前的内存状态如何;
2: 表⽰内核允许分配超过所有物理内存和交换空间总和的内存。
这里建议调整为1,相关调整方式:
永久生效:编辑vim /etc/sysctl.conf ,改vm.overcommit_memory=1,然后sysctl -p 使配置文件生效
临时生效:
echo 1 > /proc/sys/vm/overcommit_memory
上述日志中的Background save代表的是bgsave和bgrewriteaof, 如果当前可用内存不足, 操作系统应该如何处理fork操作呢?
如果vm.overcommit_memory=0, 代表如果没有可用内存, 就申请内存失败, 对应到Redis就是执行fork失败, 在Redis的日志会出现:Cannot allocate memoryRedis建议把这个值设置为1, 是为了让fork操作能够在低内存下也执行成功。
2.操作系统内存大页参数配置
Redis启动给出Warning提示:
WARNING you have Transparent Huge Pages (THP) support enabled in your kernel.
This will create latency and memory usage issues with Redis.
To fix this issue run the command 'echo never > /sys/kernel/mm/transparent_hugepage/enabled' as root, and add it to your /etc/rc.
local in order to retain the setting after a reboot. Redis must be restarted after THP is disabled.
意思是:你的Redis所在服务器当前使用的是内存大页机制,可能导致Redis访问延迟和内存使用问题。
那什么是内存大页呢?
应用程序向操作系统申请内存空间时,是按内存页为单位进行申请的,默认大小是4KB。不过Linux从 2.6.38内核版本开始,支持了内存大页机制,可以允许向操作系统一次申请 2MB 大小的内存。
由于申请的内存单位变大,也意味着申请耗时相对变长。那对于 Redis服务会有什么影响呢?
当 Redis 在执行后台 RDB 和 AOF rewrite 时,采用 fork 子进程的方式来处理。但主进程 fork 子进程后,此时的主进程依旧是可以接收写请求的,而进来的写请求,会采用 Copy On Write(写时复制)的方式操作内存数据。也就是说,主进程一旦有数据需要修改,Redis 并不会直接修改现有内存中的数据,而是先将这块内存数据拷贝出来,再修改这块新内存的数据,这就是所谓的「写时复制」。
写时复制可以理解为:需要发生写操作哪个Key,就需要先拷贝这个Key,然后再修改。这里注意,主进程在修改拷贝内存数据时,这个阶段就涉及到新内存的申请。如果此时操作系统开启了内存大页,那么在此期间,应用程序即便只修改 10B 的数据,Redis 在申请内存时也会以 2MB 为单位向操作系统申请,申请内存的耗时变长,进而导致每个写请求的延迟增加,影响到 Redis 性能。
所以为了避免过多的内存申请,我们需要关闭内存大页机制:
cat /sys/kernel/mm/transparent_hugepage/enabled
[always] madvise never
如果输出选项是 always,就表示目前开启了内存大页机制,我们需要关掉它:
echo never > /sys/kernel/mm/transparent_hugepage/enabled
其实,操作系统提供的内存大页机制,其优势是可以在一定程序上降低应用程序申请内存的次数。
比如针对大数据、对象存储相关的服务来说可能会更好,但是对于 Redis 这种对性能和延迟极其敏感的数据库来说,我们希望 Redis 在每次申请内存时,耗时尽量短,建议关闭这个参数。
五、其他影响访问Redis的性能的因素
1.应用程序配置不合理
a.合理的相关参数。
比如jedis,默认MaxActive最大连接数只有8个,在高QPS时就会出现无法获取新连接的提示:redis.clients.jedis.exceptions.JedisConnectionException: Could not get a resource from the pool … Caused by: java.util.NoSuchElementException: Timeout waiting for idle object at org.apache.commons.pool2.impl.GenericObjectPool.borrowObject(GenericObjectPool.java:449)
b.驱动版本过低。
低版本的Driver连接高版本Redis,除了无法使用最新的特性外,还会经常出现连接不释放、内存泄露、访问缓慢等问题。
2.使用连接池配置
避免使用短连接模式.特别是使用PHP的应用,频繁的连接创建与销毁,在高QPS访问时网络开销巨大;
3.CPU绑核及主频影响
Redis是单线程模型处理处理用户需求,那么处理的吞吐、效率就会极度依赖CPU的处理能力,所以选型CPU时,如果部署的Redis平时QPS较高,可以采购主频高些的CPU.
另外现在的CPU都是多核处理,为了提高服务性能,降低应用程序在多个 CPU 核心之间的上下文切换带来的性能损耗,通常采用的方案是进程绑定 CPU 的方式提高性能。
(Redis的绑核操作过于复杂,对于单机多实例的管理挑战过高,不建议绑定 CPU来处理)。