陈凯玲(“KL博主”)
:TapTap infra 工程师。
开源项目 kkFileView 等 kk 系列项目作者,Apollo 配置中心项目 PMC 。
优化成果
2023 年,通过切换低成本的 Redis ESSD 实例、实施流量压缩方案、清理无效数据、治理实例 TTL、下线无用实例等措施,自研了 Redis 流量复制 & 流量放大、Redis 数据迁移、Redis 数据在线压缩 & 解压缩、Redis 数据定向清理 & 定向指定 TTL、Redis 扫描分析 Key 最后访问时间等工具辅助方案落地。
实现 Redis 费用降本 46 万 / 月
。
PS:文中所述 Redis , 均为阿里云的 Redis 相关产品。
优化措施
以下优化措施没有优先顺序,在我司的大致的优化比例如下表:
优化措施
|
优化比例
|
1、清理未使用的实例
|
5%
|
2、实例降配:提高内存使用率
|
15%
|
3、使用场景打标,允许部分场景内存用满
|
1%
|
4、合理设置 TTL
|
8%
|
5、清理历史数据
|
6%
|
6、改进 kv 结构
|
1%
|
7、定期 scan, 释放已过期的内存
|
1%
|
8、降低可用性
|
3%
|
9、压缩 value
|
32%
|
10、迁移到兼容 Redis 协议的磁盘存储项目
|
28%
|
1、清理未使用的实例
有的业务下线了,实例还在,需要清理这类实例。
这个是最容易实现的,最快能优化的,通过采集 Redis Metrics 数据,筛出长期 QPS 非常低的实例(集群版本 Redis, 自身也会有 ping 等保活访问),观测连接数据等,和业务确认并释放
2、实例降配:提高内存使用率
各种因素,导致实际使用的内存,和申请的内存差别较大,导致使用率上不去,存在内存浪费。
比如:
-
-
业务进入稳定期,内存不在快速增长,内存使用率较稳定
这些场景需要通过降配,将使用率至少稳定在 70% 左右。
降配时,需要注意如下:
1. 集群降配到主从
,注意业务端的链接模式是否兼容
2. 大集群降配到小集群,
注意是否有大 key 限制
3. 大集群降配到小集群
,注意数据倾斜问题导致小集群节点塞满了
4. 大集群降配到小集群,注意节点数的变化,比如 128 节点降配到 32 节点,性能落差会非常大。
5. 除了内存使用率,降配时还需要注意带宽的使用情况,
6. 降配时,尽量在同等容量规格的情况下,选择节点数最多的规格,节点数越多性能越好。比如同样是 256GB 的,优先选择 128 节点 2GB 的集群,不要选择 32 节点 * 8GB 的集群。
7. 降配时,会存在连接闪断抖动,尽量在业务低峰期进行
3、使用场景打标,允许部分场景内存用满
使用 Redis,在我司可归类为【被动缓存】、【非被动缓存】两类场景。
【被动缓存】
典型的缓存场景,用来挡在 MySQL 前面抗流量(先请求 Redis 拿数据,没拿到则到 MySQL 里取数据,设置到 Redis),
Redis 内存用满时也不用扩容,设置好驱逐策略即可。
【非被动缓存】
搜广推场景的特征数据,Redis 里的数据是实时流、离线等业务主动写入的。如果内存到了 90% 告警线,需要及时扩容,避免重要数据被驱逐。
通过合理的区分不同 Redis 实例的使用场景,制定不一样的告警策略,允许部分场景 Redis 使用率 100%,可进一步提高整个 Redis 的内存使用率。
实施时需要注意如下:
1. 被标记为【被动缓存】的实例,需要明确所有的 key 都设置了 TTL ,明确允许所有的 key 都允许被驱逐。否则会导致内存用满,然后写入被拒的情况。
2.
被标记为【被动缓存】的实例,在主从架构中,可关闭主服务的 aof(aof 关闭涉及到 config rewrite 会阻塞主进程 100ms ~ 400ms 左右,会对线上集群 rt 产生影响,应尽量在业务低峰期操作),降低数据驱逐时的性能影响,提高性能。
4、合理设置 TTL
使用 Redis 时,99% 的场景 Key 是可过期删除的。
只不过,不同的场景对 TTL 的要求是不一样的,这个需要业务自行判断。
但是 infra 可以把大实例的 TTL 比例全部捞出来从大到小依次过一遍。
- 有的实例 512G 没有设置 TTL
,实际活跃实际只有 256G 不到,通过设置合理的 TTL ,可以优化 50% 的内存。
- 有的实例 TTL 设置了一年
,实际活跃数据只有半年,或者一个月
以上都可以通过设置合理的 TTL,来优化内存使用。实施时需要注意如下:
1. 实施前需要采集 Redis Metrics 数据,筛出内存规格大,未设置 TTL key 占比高的实例。
2. 大部分场景其实业务也不知道合理的 TTL 是多少。所以需要准备好 Key 的访问时间(最后一次访问)分布情况,这个需要 scan 分析,后文会提到这个工具。提供给业务做 TTL 决策。
3. 有了预期的 TTL 后,需要将所有的数据都刷一遍。所以需要一个刷 TTL 的运维工具脚本,同时可以指定清理最后一次访问距今时间的数据。
5、清理历史数据
部分业务把 Redis 当持久化存储,但是中间数据结构发生过变化,导致历史无用数据占用内存空间。
这个场景和
和
合理设置 TTL
有点类似,同样需要:
1. 准备好 Key 的访问时间(最后一次访问)分布情况,提供给业务做数据清理决策。
2. 有了预期的清理逻辑后,需要一个清理 Key 的运维工具脚本,功能包含【指定 key 前缀清理】、【指定清理最后一次访问距今时间的数据 Key】。
6、改进 kv 结构
1. 部分业务把整个大的 json 对象一股脑的存到 Redis ,其实部分字段用不着,可以裁剪 value 的大小,减少内存使用。
2. 部分场景,比如判断用户设备是否是新设备,采用 Bloomfilter 相比 String,相同数据规模,可以节省 90% 以上的内存。
7、定期 scan,释放已过期的内存
因为 Redis 已过期 key 的清理策略,是惰性删除的(已经过期的 key ,只有被访问过或者 hz 扫描到才会被删除),所以通过定期使用 SCAN 命令扫描键空间并手动删除已过期的键,可以更精细地管理 Redis 内存,提高系统的稳定性和资源利用效率。
实施时需要注意:
1. 选择在业务低峰期进行,控制好 SCAN 的速率,尽量减少对线上业务的影响。
8、降低可用性
在 dev、qa 环境中,可通过如下措施来进一步降低成本:
1. 合并小实例,部分实例已经是最小规格,但是内存使用率只有不到 10% ,这种情况下,可以通过小实例合并来缩减成本
2. 在 dev、qa 等对可用性要求不高的环境里,可通过对集群、主备版本,降配到单实例规格降低成本。
9、压缩 value
选择合适的压缩算法,对 value 进行压缩后存储。
在我司的场景里,压缩率高的可达 50%~80% ,意味着部分实例至少可优化 50% 左右的内存。
因为这个方案实施比较复杂,所以尽量挑选 100GB 以上的实例进行优化,性价比较高。适合压缩的特征如下:
我们准备了 3 个主流的压缩算法,gzip 、zstd、snappy 。测试下来 ,不同的压缩算法特点如下:
我本地对 852k 的一个 user_profile 数据做了一个测试。
(不同大小的数据性能差异较大,最终应该以线上数据流量的测试结果为准)
测试方法:
运行 100 次取均值
snappy
-
压缩耗时:122722us
-
压缩率:22.60%
-
解压耗时:49us
gzip
-
压缩耗时:39821us
-
压缩率:15.79%
-
解压耗时:3521us
zstd
-
压缩耗时:6953us
-
压缩率:17.08%
-
解压耗时:3153us
最后 snappy 就直接否了,因为压缩的资延时不符合预期。
然后不同的实例都验证下 gzip、zstd 哪个性能好就选择哪个。具体实施时需要注意如下:
1. 准备好 Redis 流量复制的工具,复制线上流量进行 zstd、gzip 的压缩、解压缩性能测试,明确压缩率、压缩延时和解压缩延时数据。
2. 定制 Redis 访问客户端,无缝兼容各种指令的 gzip 、zstd 压缩数据,做到应用端不用改代码,只需要升级客户端版本就可以完成压缩、解压缩兼容。
3. 研发存量数据全量压缩的运维工具,这个工具支持指定压缩算法、支持指定 Key 前缀压缩、支持解压缩等。
4. 注意开启压缩后,对应用 CPU 等资源消耗的的影响,读取延时普遍会增加 1~2ms 左右。
5. 刷线上存量的数据压缩前,确保应用都更新到兼容压缩的 Redis 访问客户端了,然后对部分 key 灰度压测,观测线上情况,没有问题后在全量刷。发现问题后及时解压缩。
10、迁移到兼容 Redis 协议的磁盘存储项目
早期,本着性能优先的原则,所有的 Redis 实例都是【内存型】。
随着数据规模增大,内存资源占用会随之递增,Redis 存储成本大幅攀升。
另一方面,随着业务的发展,当业务进入发展后期,数据量已经形成一定规模,而数据的访问频度则慢慢降下来,资源使用率普遍偏低 。
针对这类问题,可重新从性能、和成本综合评估业务更适合【内存型】、【磁盘型】的 Redis 实例。
内存型
磁盘型
基于 RocksDB 实现的磁盘存储、兼容 Redis 协议的产品,在阿里云叫 Tair ESSD 型。
社区里也有 pika 、kvrocks 这类项目。
这个优化措施涉及到数据的迁移,且需要兼顾业务稳定性。所以实施起来比【压缩 Value】还要复杂,主要注意点如下:
1. 如何筛选迁移到【磁盘型】的 Redis 实例?写请求低于 10k/s ,读请求低于 100k/s, 对 p99 时延要求不敏感,比如可以接受 avg 大于 0.5ms ,p99 5ms 的场景,且存储容量非常大 100GB 以上 ,而且预期存储容量会进一步增长
2. 采集好【磁盘型】的 Metrics ,做好监控观测。阿里云只给了基础的 Metrics,底层的数据指标,比如 RocksDB 层的、以及指令级的延时(对后面判断指令性能问题非常有帮助)、QPS 等都需要通过 info 指令拿到信息后自己解析采集。
3. 准备好 Redis 流量复制工具,复制线上流量到【磁盘型】Redis 目标实例,对流量进行等比,加倍回放,对每个迁移的实例针对性的进行性能压测验证。确保覆盖每个指令的性能问题、兼容问题。
4. 对【磁盘型】实例做好容量规划,QPS 增长评估。
5. 使用 DTS 进行数据迁移时,确保原实例内存至少有 20% 的冗余,否则需要先扩容原实例在迁移。因为 DTS 是基于【Redis 主从复制这套逻辑】来进行同步的,同步时,缓冲区需要占用内存。
6. 如果原实例的写入流量较大,DTS 迁移前,最好先调整下【client-output-buffer-limit replica】、【repl-backlog-size 】、【DTS 收到 Master 的 RDB 后,立马回复 ACK 的开关】这些参数,避免同步反复失败。
下面记录整个项目使用阿里云 Tair 期间发现的性能问题、Bug,这个项目能够顺利完成,我们几乎是从小白鼠开始,一路升级。
-
hash 结构的 hgetall 性能 Bug, 已修复
-
全量离线分析不可用 Bug , 已修复
-
Cpu 100% 的监控问题 ,已修复
-
数据每天的冷备份 backup 操作会对性能有比较大的影响,预计会将 backup 操作迁移到从库执行,已修复
-
zset 的 zrange 性能慢,用 zscan 可以避免这个问题
-
lrem 在 list 当 mq 使用的场景存在严重的性能问题
-
spop 会随着 count 数、set 总数增加线性增加
也给阿里云提了很多需求(下面所列只是冰山一角),这里非常感谢阿里云的支持,协助整个项目落地。
-
Dataworks 支持写入数据到非集群部署模式的 Redis 实例,也就是支持 Redis ESSD 实例
-
Redis ESSD 实例监控指标优化
-
ESSD 云盘 IO 瓶颈导致的性能尖刺问题,会限制批量刷盘时的写入速度(9 月中旬)已上线,需要升级小版本到 2.4.2.2
-
Redis ESSD 目前指令验证这块比较繁琐麻烦,后面能不能在 Redis ESSD 文档里加上每个指令的算法复杂度,或者本身性能很糟糕的指令单独标出来。给选型做参考
优化工具
在 Redis 成本优化项目落地过程中,我们沉淀了一套辅助项目落地的 Redis 成本优化工具,这些工具缺一不可,发挥了重要的作用。