Redis性能瓶颈Top 5及解决方案
通过这份必备指南,解锁Redis部署的极致性能。学习识别并解决慢O(N)命令、过多网络往返、内存压力与低效淘汰策略、持久化开销以及CPU密集型操作等常见瓶颈。本文提供从利用管道和`SCAN`到优化数据结构和持久化的可操作步骤、实践示例及最佳实践,确保您的Redis实例在缓存、消息传递和数据存储需求中保持快速可靠。
Redis性能瓶颈Top 5及解决方案
Redis性能问题通常看起来神秘莫测,直到你想起一件事:Redis很快,但它并非不劳而获。一个遍历百万个键的命令仍然会遍历百万个键。一个每次网络往返只发送一个命令的客户端仍然要为每次往返付出代价。一个内存耗尽的服务器仍然需要根据配置进行驱逐、交换、拒绝写入或崩溃。
当Redis变慢时,不要从随意更改设置开始。从证据开始:
redis-cli INFO
redis-cli SLOWLOG GET 20
redis-cli LATENCY DOCTOR
redis-cli INFO commandstats
redis-cli INFO memory
这些命令通常指向五个瓶颈之一:慢命令、网络往返、内存压力、持久化开销或CPU饱和。
1. 大数据集上的慢命令
Redis有许多微小的常量时间操作,但并非每个命令都很小。诸如KEYS、大型LRANGE、SMEMBERS、HGETALL、大范围ZRANGE、SORT以及长Lua脚本等命令会在运行时阻塞其他客户端。
经典事件始于一个清理或调试命令:
KEYS *
在小型开发实例上,它会立即返回。在拥有数百万个键的生产键空间中,它可能使服务器停滞足够长的时间,导致应用程序请求堆积。同样的模式也发生在一个哈希上,它最初是“每个用户几个字段”,后来悄然变成了一个巨大的对象。
查找证据:
redis-cli SLOWLOG GET 20
redis-cli INFO commandstats
redis-cli LATENCY LATEST
SLOWLOG记录超过配置阈值的命令。INFO commandstats显示每个命令的调用次数和累计时间。如果一个命令占用了大部分时间,就从那里开始。
修复访问模式:
redis-cli --scan --pattern 'user:*'
使用SCAN代替KEYS进行键空间迭代。使用HSCAN、SSCAN和ZSCAN处理大型哈希、集合和有序集合。获取分页或范围而不是整个结构:
LRANGE feed:user:42 0 49
ZRANGE leaderboard 0 99 WITHSCORES
如果某个对象变得过大,根据应用程序的读取方式拆分它。一个包含数千个不相关字段的单一user:42哈希可能便于写入,但对于只需要配置文件设置的读取来说却很痛苦。像user:42:profile、user:42:prefs和user:42:counters这样的独立键可以减少每次请求涉及的数据量。
对于删除,当值可能很大时,优先使用UNLINK:
UNLINK old:large:set
UNLINK从键空间中移除键并异步释放内存。对于大值来说,它比DEL更安全,尽管批量清理仍然需要节流。
2. 过多的网络往返
Redis可能在微秒内处理一个命令,而你的应用程序却在等待网络上花费毫秒。如果一个请求路径发送了50个连续的Redis命令,即使Redis本身健康,网络也可能主导总时间。
这在如下代码中很常见:
for user_id in user_ids:
profile = redis.get(f"user:{user_id}:profile")
每个GET在下一个开始之前等待自己的响应。通过网络,这代价高昂。
使用管道:
pipe = redis.pipeline(transaction=False)
for user_id in user_ids:
pipe.get(f"user:{user_id}:profile")
profiles = pipe.execute()
管道允许在不等待每个单独回复的情况下发送多个命令。Redis仍然按顺序执行命令,但客户端避免了为每个命令支付一次往返。
在合适的地方使用多键命令:
MGET user:1:profile user:2:profile user:3:profile
不要将每个请求都变成一个巨大的管道。大型管道可能增加内存使用并造成响应突发。批量处理到足以消除明显的往返浪费,然后进行测量。
对于读密集型路径,还要检查你的应用程序是否反复向Redis请求那些可以一次获取并在请求期间重复使用的值。一个小的本地请求缓存可以在不改变Redis的情况下消除意外的重复读取。
3. 内存压力和驱逐抖动
Redis以内存为中心。一旦内存紧张,性能会以多种方式恶化:驱逐消耗CPU,写入在noeviction下可能失败,持久化fork可能变得更困难,副本可能滞后,如果主机配置不当或过载,操作系统可能进行交换。
检查内存:
redis-cli INFO memory
redis-cli INFO stats | grep evicted_keys
redis-cli CONFIG GET maxmemory
redis-cli CONFIG GET maxmemory-policy
重要迹象:
used_memory接近maxmemory。evicted_keys快速增长。- 主机正在交换。
- 大键消耗的内存超出预期。
- 缓存键实际上没有TTL。
小心地查找大键。不要在高峰流量期间运行广泛的昂贵命令。使用--bigkeys进行采样可能会有所帮助:
redis-cli --bigkeys
对于缓存,设置内存限制和与数据匹配的驱逐策略:
maxmemory 4gb
maxmemory-policy allkeys-lru
当所有键都是缓存条目时,allkeys-lru或allkeys-lfu可能合理。volatile-lru仅驱逐具有TTL的键,这在持久键与缓存键共享实例时很有用。noeviction通常适用于Redis作为主数据存储的情况,因为静默驱逐看起来持久的数据比返回错误更糟糕。
在写入时设置TTL:
SET cache:product:123 "$json" EX 300
对于会话存储,要慎重。没有过期时间的会话键通常是一个错误。对于速率限制器,计数器应与窗口一起过期。对于流,修剪旧条目。Redis中的内存泄漏通常是未获得生命周期的应用程序数据。
在必要时减少键和值的开销。数千个小键可能比预期消耗更多的元数据。有时紧凑的哈希比许多单独的键更好;有时相反,因为读取只需要一个字段。根据实际访问模式进行测量,而不是假设某种形状总是最好的。
4. 持久化和磁盘I/O停顿
持久化保护数据,但它引入了你需要理解的磁盘和fork行为。RDB快照和AOF重写通常是后台操作,但它们仍然可能通过fork时间、写时复制内存压力和磁盘I/O导致延迟。
检查持久化状态:
redis-cli INFO persistence
redis-cli LATENCY LATEST
iostat -xz 1
查找失败的后台保存、长fork时间、AOF重写活动和磁盘饱和。如果延迟峰值与BGSAVE或BGREWRITEAOF对齐,持久化调整应列入候选名单。
对于AOF,主要的持久性/性能设置是:
appendfsync everysec
everysec是通常的平衡选择。always每次写入都同步,可能非常慢。no将同步留给操作系统,并在崩溃时接受更多的数据丢失风险。
对于RDB,避免在繁忙的写入工作负载上频繁触发的快照规则,除非这是有意的:
save 900 1
save 300 10
save 60 10000
这些示例默认值并不自动适用于每个工作负载。用作一次性缓存的高写入Redis可能根本不需要持久化。用作作业队列或会话存储的Redis实例可能需要,但可接受的丢失窗口必须明确。
如果持久化与应用程序流量竞争,请考虑:
- 更快的本地SSD存储。
- 将Redis持久化与其他磁盘密集型服务分离。
- 在主节点可以容忍该设计时,在副本上运行持久化。
- 保持数据集大小低于主机可以舒适fork的范围。
- 在Redis建议用于后台保存的地方设置Linux
vm.overcommit_memory=1。
不要盲目禁用持久化来“修复性能”,除非数据确实是可丢弃的。它可能使图表看起来更好,同时将重启变成数据丢失。
5. CPU饱和和单线程命令执行
Redis命令执行主要是单线程的,尽管现代Redis为一些I/O和后台工作使用额外的线程。如果一个核心被Redis占用,在同一实例上添加更多空闲核心可能无法帮助热命令路径。
检查主机和Redis命令组合:
top -H -p $(pgrep redis-server)
redis-cli INFO commandstats
redis-cli SLOWLOG GET 20
redis-cli INFO clients
常见的CPU原因:
- 大型集合、有序集合、列表或哈希操作。
- 繁重的Lua脚本。
- 应用程序中的压缩或序列化开销导致值比预期大。
- 非常高的发布/订阅扇出。
- 内存压力下的昂贵驱逐。
- 太多连接不断重新连接或发出小命令。
通过减少工作、拆分工作或分发工作来修复CPU。
通过更改命令和数据形状来减少工作。如果你只需要50个项目,就不要获取5000个。如果每个请求解析一个500 KB的JSON blob来读取一个标志,将该标志拆分为一个更小的键或字段。
通过使用增量扫描将长循环移动到客户端来拆分工作:
HSCAN big:hash 0 COUNT 100
使用副本进行读取或使用Redis Cluster进行分片来分发工作。副本有助于读密集型流量,但它们不会使主节点上的写入更便宜。Redis Cluster跨主节点分布键,这可以增加总CPU和内存容量,但也增加了操作复杂性和键槽约束。
对于发布/订阅,监视输出缓冲区和扇出:
redis-cli PUBSUB NUMSUB events:updates
redis-cli CLIENT LIST
慢速订阅者可能变成内存压力。数千个订阅者可能将一个发布变成大量的网络输出。如果发布/订阅很重,考虑将其隔离在单独的Redis实例上。
快速分类工作流程
当Redis延迟上升时,按顺序运行这些检查:
redis-cli --latency
redis-cli SLOWLOG GET 10
redis-cli LATENCY DOCTOR
redis-cli INFO memory
redis-cli INFO clients
redis-cli INFO persistence
redis-cli INFO commandstats
然后问:
- 是否出现了慢命令?
- 内存是否达到
maxmemory或开始驱逐? - 持久化是否开始了保存或重写?
- 连接的客户端或阻塞的客户端是否跳升?
- 某个命令的调用次数或时间是否激增?
- 应用程序部署是否改变了Redis访问模式?
大多数Redis瓶颈不是通过一个神奇设置解决的。它们是通过使工作负载更小、更增量、更批量或更好隔离来解决的。最好的Redis部署是无聊的:键在应该过期时过期,大操作被分页,客户端明智地使用管道,持久化设置与数据的价值匹配,监控在用户感受到之前捕捉趋势。
修复前后测量什么
性能修复只有在相关工作负载的图表发生变化时才是真实的。在更改代码或配置之前,捕获一个小基线:
redis-cli INFO stats
redis-cli INFO commandstats
redis-cli INFO memory
redis-cli INFO persistence
redis-cli SLOWLOG GET 20
在系统级别,捕获CPU、磁盘、内存、交换和网络吞吐量。如果Redis运行在容器中,检查容器限制和主机压力。一个Redis进程在其自身内存视图中可能看起来正常,而主机却因其他服务而承受磁盘或CPU压力。
更改后,比较:
- Redis支持请求的p50、p95和p99应用程序延迟。
- Redis命令延迟,而不仅仅是请求延迟。
- 按命令划分的慢日志条目。
- 驱逐率和内存余量。
- 连接的客户端和拒绝的连接。
- 持久化fork时间和AOF/RDB状态。
- 如果副本提供读取或保护持久性,则副本滞后。
对仅移动痛点的修复保持怀疑。例如,大型管道可能减少请求延迟但增加内存峰值。禁用AOF可能消除磁盘延迟但削弱恢复能力。增加maxmemory可能延迟驱逐,但如果机器已经共享,则可能使主机资源枯竭。
一个有用的做法是围绕你更改的确切Redis模式编写一个小型负载测试。如果旧代码执行了40个连续的GET,用实际负载大小测试连续GET与MGET或管道。如果旧代码使用了HGETALL,测试请求实际需要的字段的HGET。当你对实际运行的形状进行基准测试时,Redis调整要容易得多,而不是一个通用的“Redis每秒操作数”数字。
最后,保持回滚简单。一个Redis性能更改通常同时涉及应用程序代码、客户端设置和服务器配置。尽可能一次更改一件事。如果你必须更改多件事,写下每个更改应该改善哪个症状。这可以防止下一个工程师继承一堆没人想移除的神秘设置。