好,下面我们就开始讲讲 Redis 内存溢出(OOM)这个让人头疼,但又不得不面对的问题。
大家好,今天咱们来聊聊 Redis 内存溢出,也就是传说中的 OOM(Out Of Memory)。这玩意儿就像你家的冰箱,东西塞太多了,不光门关不上,还会把好好的食材都给挤坏了。Redis 也是一样,内存满了,性能下降是小事,数据丢失就是大事了。
啥是 Redis OOM?
简单来说,就是 Redis 用光了分配给它的所有内存。这就像你去银行取钱,结果银行告诉你:“不好意思,没钱了。”Redis 如果没内存了,就没法存新的数据,也没法好好处理请求,轻则响应变慢,重则直接崩溃。
为啥会 OOM?
原因有很多,咱们一点点来扒:
-
数据量太大: 这就像你家冰箱塞满了东西,最直接的原因就是东西太多了。Redis 存储的数据量超过了
maxmemory
配置的值,就会触发 OOM。 -
内存碎片: Redis 在频繁进行数据的增删改查操作时,会产生内存碎片。这些碎片就像冰箱里乱七八糟的包装盒,占据了空间,但又没法用来存放有效数据。
-
大 Key: 有些 Key 特别大,比如一个 List 里面存了几百万个元素,或者一个 Hash 里面有几百万个 field。这种大 Key 会一下子占用大量的内存,更容易导致 OOM。
-
使用不当: 比如一次性写入大量数据,或者使用复杂度很高的命令(比如
SORT
),这些都会消耗大量的内存。 -
过期键清理不及时: 如果过期键没有及时清理,也会占用大量的内存。
-
Redis 自身 Bug: 虽然这种情况比较少见,但也不能排除 Redis 自身存在 Bug 导致内存泄漏的可能性。
OOM 了会咋样?
OOM 发生时,Redis 会根据 maxmemory-policy
配置的策略来处理:
策略 | 描述 |
---|---|
noeviction |
默认策略。当内存满了,不删除任何数据,直接返回错误。就像你往冰箱里硬塞东西,冰箱告诉你:“塞不下了,别塞了!” |
allkeys-lru |
当内存满了,移除最近最少使用的 Key。就像你清理冰箱,把很久没用的东西扔掉。 |
volatile-lru |
当内存满了,移除设置了过期时间的 Key 中,最近最少使用的 Key。就像你清理冰箱,只扔掉快过期的东西。 |
allkeys-random |
当内存满了,随机移除 Key。就像你清理冰箱,随便扔掉一些东西。 |
volatile-random |
当内存满了,随机移除设置了过期时间的 Key。就像你清理冰箱,随便扔掉一些快过期的东西。 |
volatile-ttl |
当内存满了,移除设置了过期时间的 Key 中,剩余 TTL 时间最短的 Key。就像你清理冰箱,优先扔掉马上就要过期的东西。 |
allkeys-lfu |
当内存满了,移除最近最不常用的 Key (Least Frequently Used)。 |
volatile-lfu |
当内存满了,移除设置了过期时间的 Key 中,最近最不常用的 Key (Least Frequently Used)。 |
如果配置的是 noeviction
,那么客户端在尝试写入新数据时会收到 (error) OOM command not allowed when used memory > 'maxmemory'.
错误。其他的策略会尝试删除一些数据,但如果删除速度跟不上写入速度,最终还是会 OOM。
怎么排查 OOM?
排查 OOM 就像侦探破案,需要收集各种线索:
-
查看 Redis 日志: Redis 日志会记录 OOM 相关的信息,比如触发 OOM 的命令、删除了哪些 Key 等。仔细分析日志可以找到一些线索。
-
使用
INFO
命令:INFO
命令可以查看 Redis 的各种状态信息,包括内存使用情况、Key 的数量、连接数等。redis-cli info memory redis-cli info stats
关注以下几个指标:
used_memory
:Redis 实际使用的内存大小。used_memory_rss
:Redis 占用的物理内存大小(包括 Redis 进程自身占用的内存和 Redis 使用的共享库占用的内存)。used_memory_peak
:Redis 曾经使用过的最大内存大小。mem_fragmentation_ratio
:used_memory_rss
/used_memory
的比值。如果这个值大于 1.5,说明内存碎片比较严重。total_connections_received
:连接总数,如果连接数暴增,需要排查是否存在连接泄漏。
-
使用
redis-cli --bigkeys
命令: 这个命令可以找出 Redis 中最大的 Key。redis-cli --bigkeys
输出结果会显示最大 Key 的类型、大小等信息。
-
使用
MEMORY USAGE
命令: 这个命令可以查看指定 Key 占用的内存大小。redis-cli memory usage mykey
-
使用
SLOWLOG
命令:SLOWLOG
可以记录执行时间超过指定阈值的命令。分析SLOWLOG
可以找出执行时间长的命令,这些命令可能消耗大量的内存。redis-cli slowlog get 10 # 获取最近 10 条慢查询日志
-
使用
redis-rdb-tools
工具: 这是一个开源工具,可以分析 Redis 的 RDB 文件,找出占用内存最多的 Key。# 安装 redis-rdb-tools pip install rdbtools # 分析 RDB 文件 rdb -c memory /path/to/dump.rdb
-
监控 Redis 性能: 使用监控工具(比如 Prometheus + Grafana)监控 Redis 的内存使用情况、CPU 使用率、QPS 等指标。通过监控可以及时发现潜在的 OOM 风险。
怎么预防 OOM?
预防胜于治疗,咱们得提前做好准备:
-
合理设置
maxmemory
:maxmemory
是 Redis 可以使用的最大内存。设置maxmemory
时要考虑以下因素:- 服务器的物理内存大小。
- 操作系统和其他应用程序需要的内存。
- Redis 存储的数据量。
- Redis 的性能要求。
一般来说,建议将
maxmemory
设置为物理内存的 50% ~ 75%。 -
选择合适的
maxmemory-policy
: 根据业务场景选择合适的maxmemory-policy
。如果对数据丢失比较敏感,可以选择noeviction
。如果允许删除一些数据,可以选择allkeys-lru
或volatile-lru
。 -
避免大 Key: 尽量避免存储大 Key。如果必须存储大 Key,可以考虑以下方案:
- 将大 Key 分解成多个小 Key。
- 使用压缩算法对大 Key 进行压缩。
- 使用 Redis 的数据结构(比如 List、Hash)存储复杂的数据。
-
设置 Key 的过期时间: 给 Key 设置合理的过期时间,让 Redis 可以自动清理过期数据。
-
优化 Redis 配置: 调整 Redis 的配置参数,比如
hash-max-ziplist-entries
、list-max-ziplist-entries
等,可以减少内存的使用。 -
使用 Redis 集群: 将数据分散存储到多个 Redis 节点上,可以降低单个节点的内存压力。
-
监控 Redis 性能: 实时监控 Redis 的内存使用情况,及时发现潜在的 OOM 风险。
-
定期进行内存碎片整理: Redis 提供了
MEMORY PURGE
命令,可以尝试进行内存碎片整理。但是,这个命令会阻塞 Redis 的正常运行,所以要谨慎使用。 -
代码优化: 优化代码,避免一次性写入大量数据,或者使用复杂度很高的命令。
代码示例:
-
避免一次性写入大量数据:
# 不推荐 import redis r = redis.Redis(host='localhost', port=6379) data = {} for i in range(100000): data[f'key:{i}'] = f'value:{i}' r.mset(data) # 一次性写入 10 万条数据,容易导致 OOM # 推荐 import redis r = redis.Redis(host='localhost', port=6379) pipe = r.pipeline() for i in range(100000): pipe.set(f'key:{i}', f'value:{i}') if i % 1000 == 0: pipe.execute() # 每 1000 条数据批量写入 pipe.execute() # 写入剩余数据
-
使用压缩算法对大 Key 进行压缩:
import redis import zlib r = redis.Redis(host='localhost', port=6379) # 压缩数据 data = 'This is a very long string that needs to be compressed.' * 1000 compressed_data = zlib.compress(data.encode('utf-8')) # 存储压缩后的数据 r.set('compressed_key', compressed_data) # 获取压缩后的数据并解压缩 compressed_data_from_redis = r.get('compressed_key') if compressed_data_from_redis: decompressed_data = zlib.decompress(compressed_data_from_redis).decode('utf-8') print(decompressed_data[:100]) # 打印部分数据
-
使用 Lua 脚本原子性地更新数据:
-- Lua 脚本 local key = KEYS[1] local increment = tonumber(ARGV[1]) local current_value = redis.call("GET", key) if current_value then current_value = tonumber(current_value) local new_value = current_value + increment redis.call("SET", key, new_value) return new_value else redis.call("SET", key, increment) return increment end
import redis r = redis.Redis(host='localhost', port=6379) # 加载 Lua 脚本 script = """ local key = KEYS[1] local increment = tonumber(ARGV[1]) local current_value = redis.call("GET", key) if current_value then current_value = tonumber(current_value) local new_value = current_value + increment redis.call("SET", key, new_value) return new_value else redis.call("SET", key, increment) return increment end """ increment_script = r.register_script(script) # 执行 Lua 脚本 result = increment_script(keys=['mykey'], args=[10]) print(result)
总结:
Redis OOM 是一个复杂的问题,需要综合考虑各种因素。排查 OOM 需要耐心和细致,预防 OOM 需要提前做好准备。希望今天的分享能帮助大家更好地理解和解决 Redis OOM 问题。记住,就像对待你家的冰箱一样,定期清理,合理存放,才能让 Redis 更好地为我们服务。
好了,今天的分享就到这里。大家有什么问题,可以随时提问。感谢大家的聆听!