记一次优化广告服务 Redis 30G+ 内存

问题

最近在看项目遗留代码的时候，有一些操作Redis代码，都是没有设置过期时间，猜测可能因为Hash结构在使用时不能同时设置过期时间，导致后面忘记设置过期时间了.

代码里常用的结构是: key是根据广告标识相关，field是用户的标识，value是自定义的数据，有两种使用方式导致内存占用过大

第一种是拼上了date作为key的Hash结构

HSET xxx:ad_id:20220315 userid value

第二种是广告下线了，但是广告对应的key还遗留在库中

HSET xxx:ad_id userid value

操作之前，数据库的信息如下：配置是8*8G, 但是内存已经占用51G+

理论上这个服务不应该使用这么多内存的，由于遗留代码没有设置过期时间，导致运维只能通过不断增加内存来让服务正常运行

优化

查询key的分布

通过阿里云的离线分析平台可以查到大key https://help.aliyun.com/document_detail/102093.htm?spm=a2c4g.11186623.0.0.58e12542DRK90l#concept-ufz-byl-jgb

如果没用阿里云服务，查询大key的方案 (如果是访问低峰期，可以不传递-i参数)

redis-cli --bigkeys -i 0.1

重构代码

通过分析得到key的分布图，然后通过代码中模糊搜索，定位到操作Redis的相关代码

相关代码设置过期时间

TTL xxx:ad_id:20220315 86400

广告下线事件删除过期key

UNLINK xxx:ad_id

删除遗留数据

以前删除大key, 直接del会阻塞主线程，都是写一个脚本Scan然后再去删除，这样子不会阻塞

如果版本大于4.0可以直接使用unlink xxx:ad_id:20220315

如果要批量删除执行:

redis-cli --scan --pattern xxx:ad_id:* | xargs redis-cli unlink

备注: 4.0之后的Redis自动删除过期key也是会进行这个异步删除操作

删除数据之后，used_memory_rss不释放

used_memory_rss是程序申请的空间，used_memory是程序实际使用的内存

删除大量数据之后，大概率会看到used_memory_rss这个值远大于used_memory, 这时候就代表有很多内存碎片

mem_fragmentation_ratio = used_memory_rss / used_memory

mem_fragmentation_ratio大于1.5就需要处理这个内存碎片

同步处理方式memory purge(不推荐)

异步处理方式

开启自动内存碎片整理

config set activedefrag yes

开启自动内存碎片整理

config get activedefrag

1) "activedefrag"

2) "no"

设置某项阈值 (一定要设置某个选项Redis才会开始处理)

config set active-defrag-threshold-lower 10

Copy
# 开启自动内存碎片整理(总开关)
activedefrag yes
# 当碎片达到 100mb 时，开启内存碎片整理
active-defrag-ignore-bytes 100mb
# 当碎片超过 10% 时，开启内存碎片整理
active-defrag-threshold-lower 10
# 内存碎片超过 100%，则尽最大努力整理
active-defrag-threshold-upper 100
# 内存自动整理占用资源最小百分比
active-defrag-cycle-min 25
# 内存自动整理占用资源最大百分比
active-defrag-cycle-max 75

经过一个早上的处理，可以看到used_memory_rss已经开始下降了，不过看着这个进度应该还需要很长的时间来释放内存

Notes

删除数据之前可以手动备份一次数据

查看上一次备份的时间戳

LASTSAVE

开始备份

BGSAVE

之后继续运行LASTSAVE看时间戳，如果大于上一次备份的时间戳，代表此次备份已经成功，可以进行删除

过期处理

Redis 密钥以两种方式过期：被动方式和主动方式。

仅当某些客户端尝试访问密钥时，密钥就会被动过期，并且发现密钥已超时。

当然这还不够，因为有过期的密钥将永远不会被再次访问。这些密钥无论如何都应该过期，因此 Redis 会定期在设置过期的密钥中随机测试几个密钥。所有已经过期的键都会从键空间中删除。

具体来说，这是 Redis 每秒执行 10 次的操作：

从具有关联过期时间的密钥集中测试 20 个随机密钥。

删除所有发现过期的密钥。

如果超过 25% 的密钥过期，请从步骤 1 重新开始。

这是一个简单的概率算法，基本上假设我们的样本代表了整个密钥空间，并且我们继续过期直到可能过期的密钥百分比低于 25%

这意味着在任何给定时刻，使用内存的已过期密钥的最大数量等于每秒最大写入操作量除以 4。

像Laravel默认使用的缓存驱动是File, 用的就是被动方式删除，并且默认开启了ThrottleRequests/throttle中间件，稍不注意，这就是一个大坑，有大量的文件文件写入但是不会删除.