百亿级数据的实时存取优化与实践
极光笔记丨百亿级数据的实时存取优化与实践 https://mp.weixin.qq.com/s/PCcms6Rs_B7z5InzZdvxCQ
摘要
极光推送后台标签/别名系统存储超过百亿条数据, 高峰期QPS超过50万, 且随着业务的发展,存储容量和访问量还在不断增加。之前系统存在的一些瓶颈也逐渐显现,所以近一两年持续做了很多的优化工作,最终达到非常不错的效果。近期,经过积累和沉淀后,将这一部分的工作进行总结。
01
背景
当前的旧系统中主要存储标签/别名与注册ID的相互映射关系, 使用Key-Value结构存储, 考虑到一个注册ID可能有多个标签, 同时一个标签也存在多个不同的注册ID, 这部分数据使用Redis存储中的Set数据结构; 而一个注册ID只有一个别名, 同时一个别名也存在多个不同的注册ID, 这部分数据使用String/Set数据结构。由于此部分数据量过大, 考虑到存储成本, Redis使用的单Master模式, 而最终数据的落地使用Pika存储(一种类Redis的文件存储)。Pika与Redis中的数据由业务方保持一致, Redis正常可用时, 读Redis; 当Redis不可用时读Pika, Redis恢复可用后, 从Pika恢复数据到Redis, 重新读Redis. 旧系统的存储架构如下:
从上面的架构图可以看到, Redis/Pika均采用主从模式, 其中Redis只有Master, 配置管理模块用来维护Redis/Pika集群的主从关系, 配置写入ZooKeeper中, 业务模块从ZooKeeper中读取配置, 不做配置变更。所有的配置变更由配置管理模块负责。当需要迁移, 扩容, 缩容的时候, 必须通过配置管理模块操作. 这个旧系统的优缺点如下:
优点:
-
配置集中管理, 业务模块不需要分开单独配置
-
读取Redis中数据, 保证了高并发查询效率
-
Pika 主从模式, 保证了数据落地, 不丢失
-
配置管理模块维护分片slot与实例的映射关系, 根据Key的slot值路由到指定的实例
缺点:
-
Redis与Pika中存储的数据结构不一致, 增加了代码复杂度
-
Redis单Master模式, Redis某个节点不可用时, 读请求穿透到Pika, 而Pika不能保证查询效率, 会造成读请求耗时增加甚至超时
-
Redis故障恢复后, 需要从Pika重新同步数据, 增加了系统不可用持续时间, 且数据一致性需要更加复杂的计算来保证
-
当迁移/扩容/缩容时需要手动操作配置管理模块, 步骤繁琐且容易出错
-
Redis中存储了与Pika同样多的数据, 占用了大量的内存存储空间, 资源成本很高
-
整个系统的可用性还有提升空间, 故障恢复时间可以尽量缩短
-
配置管理模块为单点, 非高可用, 当此服务down掉时, 整个集群不是高可用, 无法感知Redis/Pika的心跳状态
-
超大Key打散操作需要手动触发. 系统中存在个别标签下的注册ID过多, 存储在同一个实例上, 容易超过实例的存储上限, 而且单个实例限制了该Key的读性能
02
旧系统缺点分析
考虑到旧系统存在以上的缺点, 主要从以下几个方向解决:
Redis与Pika中存储的数据结构不一致, 增加了代码复杂度
分析: 旧系统中Redis与Pika数据不一致主要是Pika早期版本Set数据结构操作效率不高, String数据结构操作的效率比较高, 但获取标签/别名下的所有注册ID时需要遍历所有Pika实例, 这个操作非常耗时, 考虑到最新版本Pika已经优化Set数据结构, 提高了Set数据结构的读写性能, 应该保持Redis与Pika数据结构的一致性。
Redis单Master模式, Redis某个节点不可用时, 读请求穿透到Pika, 而Pika不能保证查询效率, 会造成读请求耗时增加甚至超时
分析: Redis单Master模式风险极大。需要优化为主从模式, 这样能够在某个Master故障时能够进行主从切换, 不再从Pika中恢复数据, 减少故障恢复时间, 减少数据不一致的可能性。
Redis故障恢复后, 需要从Pika重新同步数据, 增加了系统不可用持续时间, 且数据一致性需要更加复杂的计算来保证
分析: 这个系统恢复时间过长是由于Redis是单Master模式, 且没有持久化, 需要把Redis优化成主从模式且必须开启持久化, 从而几乎不需要从Pika恢复数据了, 除非Redis的主从实例全部同时不可用。不需要从Pika恢复数据后, 那么Redis中的数据在Redis主从实例发生故障时, 就和Pika中的数据一致了。
当迁移/扩容/缩容时需要手动操作配置管理模块, 步骤繁琐且容易出错
分析: 配置管理模块手动干预操作过多, 非常容易出错, 这部分应尽量减少手动操作, 考虑引入Redis哨兵, 能够替换大部分的手动操作步骤。
Redis中存储了与Pika同样多的数据, 占用了大量的内存存储空间, 资源成本很高
分析: 通过对Redis中的各个不同维度数据进行数据量和访问量以及访问来源分析(如下图)。外部请求量(估算) 这栏的数据反应了各个不同Key的单位时间内访问量情况。
Redis的存储数据主要分为标签/别名到注册ID和注册ID到标签/别名两部分数据. 通过分析得知, 标签/别名到注册ID的数据约占1/3左右的存储空间, 访问量占到80%; 注册ID到标签/别名的数据约占2/3左右的存储空间, 访问量占到20%。可以看到, 红色数字部分为访问的Pika, 黑色部分访问的Redis, 3.7%这项的数据可以优化成访问Redis, 那么可以得出结论, 红色的数据在Redis中是永远访问不到的。所以可以考虑将Redis中注册ID到标签/别名这部分数据删掉, 访问此部分数据请求到Pika, 能够节省约2/3的存储空间, 同时还能保证整个系统的读性能。
整个系统的可用性还有提升空间, 故障恢复时间可以尽量缩短
分析: 这部分主要由于其中一项服务为非高可用, 而且整个系统架构的复杂性较高, 以及数据一致性相对比较难保证, 导致故障恢复时间长, 考虑应将所有服务均优化为高可用, 同时简化整个系统的架构。
配置管理模块为单点, 非高可用, 当此服务down掉时, 整个集群不是高可用, 无法感知Redis/Pika的心跳状态
分析: 配置手动管理风险也非常大, Pika主从关系通过配置文件手动指定, 配错后将导致数据错乱, 产生脏数据. 需要使用Redis哨兵模式, 用哨兵管理Redis/Pika, 配置管理模块不再直接管理所有Redis/Pika节点, 而是通过哨兵管理, 同时再发生主从切换或者节点故障时通知配置管理模块, 自动更新配置到Zookeeper中, 迁移/扩容/缩容时也基本不用手动干预。
超大Key打散操作需要手动触发。系统中存在个别标签下的注册ID过多, 存储在同一个实例上, 容易超过实例的存储上限, 而且单个实例限制了该Key的读性能
分析: 这部分手动操作, 应该优化为自动触发, 自动完成迁移, 减少人工干预, 节省人力成本。
03
Redis哨兵模式
Redis哨兵为Redis/Pika提供了高可用性, 可以在无需人工干预的情况下抵抗某些类型的故障, 还支持监视, 通知, 自动故障转移, 配置管理等功能:
-
监视: 哨兵会不断检查主实例和从实例是否按预期工作
-
通知: 哨兵可以将出现问题的实例以Redis的Pub/Sub方式通知到应用程序
-
自动故障转移: 如果主实例出现问题, 可以启动故障转移, 将其中一个从实例升级为主, 并将其他从实例重新配置为新主实例的从实例, 并通知应用程序要使用新的主实例
-
配置管理: 创建新的从实例或者主实例不可用时等都会通知给应用程序
同时, 哨兵还具有分布式性质, 哨兵本身被设计为可以多个哨兵进程协同工作, 当多个哨兵就给定的主机不再可用这一事实达成共识时, 将执行故障检测, 这降低了误报的可能性。即使不是所有的哨兵进程都在工作, 哨兵仍能正常工作, 从而使系统能够应对故障。
Redis哨兵+主从模式能够在Redis/Pika发生故障时及时反馈实例的健康状态, 并在必要时进行自动主从切换, 同时会通过Redis的sub/pub机制通知到订阅此消息的应用程序。从而应用程序感知这个主从切换, 能够短时间将链接切换到健康的实例, 保障服务的正常运行。
没有采用Redis的集群模式, 主要有以下几点原因:
-
当前的存储规模较大, 集群模式在故障时, 恢复时间可能很长
-
集群模式的主从复制通过异步方式, 故障恢复期间不保证数据的一致性
-
集群模式中从实例不能对外提供查询, 只是主实例的备份
-
无法全局模糊匹配Key, 即无法遍历所有实例来查询一个模糊匹配的Key
04
最终解决方案
综上, 为了保证整个存储集群的高可用, 减少故障恢复的时间, 甚至做到故障时对部分业务零影响, 我们采用了Redis哨兵+Redis/Pika主从的模式, Redis哨兵保证整个存储集群的高可用, Redis主从用来提供查询标签/别名到注册ID的数据, Pika主从用来存储全量数据和一些注册ID到标签/别名数据的查询。需要业务保证所有Redis与Pika数据的全量同步。新方案架构如下:
从上面架构图来看, 当前Redis/Pika都是多主从模式, 同时分别由不同的多个哨兵服务监视, 只要主从实例中任一个实例可用, 整个集群就是可用的。Redis/Pika集群内包含多个主从实例, 由业务方根据Key计算slot值, 每个slot根据配置管理模块指定的slot与实例映射关系。如果某个slot对应的Redis主从实例均不可用, 会查询对应的Pika, 这样就保证整个系统读请求的高可用。这个新方案的优缺点如下:
优点:
-
整个系统所有服务, 所有存储组件均为高可用, 整个系统可用性非常高
-
故障恢复时间快, 理论上当有Redis/Pika主实例故障, 只会影响写入请求, 故障时间是哨兵检测的间隔时间; 当Redis/Pika从实例故障, 读写请求都不受影响, 读服务可以自动切换到主实例, 故障时间为零, 当从实例恢复后自动切换回从实例
-
标签/别名存储隔离, 读写隔离, 不同业务隔离, 做到互不干扰, 根据不同场景扩缩容
-
减少Redis的内存占用2/3左右, 只使用原有存储空间的1/3, 减少资源成本
-
配置管理模块高可用, 其中一个服务down掉, 不影响整个集群的高可用, 只要其中一台服务可用, 那么整个系统就是可用
-
可以平滑迁移/扩容/缩容, 可以做到迁移时无需业务方操作, 无需手动干预, 自动完成; 扩容/缩容时运维同步好数据, 修改配置管理模块配置然后重启服务即可
-
超大Key打散操作自动触发, 整个操作对业务方无感知, 减少运维成本
缺点:
-
Redis主从实例均可不用时, 整个系统写入这个实例对应slot的数据均失败, 考虑到从Pika实时同步Redis数据的难度, 并且主从实例均不可用的概率非常低, 选择容忍这种情况
-
哨兵管理增加系统了的复杂度, 当Redis/Pika实例主从切换时通知业务模块处理容易出错, 这部分功能已经过严格的测试以及线上长时间的功能检验
05
其他优化
除了通过以上架构优化, 本次优化还包括以下方面:
-
通过IO复用, 由原来的每个线程一个实例链接, 改为在同一个线程同时管理多个链接,提高了服务的QPS, 降低高峰期的资源使用率(如负载等)
-
之前的旧系统存在多个模块互相调用情况, 减少模块间耦合, 减少部署及运维成本
-
之前的旧系统模块间使用MQ交互, 效率较低, 改为RPC调用, 提高了调用效率, 保证调用的成功率, 减少数据不一致, 方便定位问题
-
不再使用Redis/Pika定制化版本, 可根据需要, 升级到Redis/Pika官方的最新稳定版本
-
查询模块在查询大Key时增加缓存, 缓存查询结果, 此缓存过期前不再查询Redis/Pika, 提高了查询效率
06
展望
未来此系统还可以从以下几个方面继续改进和优化:
-
大Key存储状态更加智能化管理, 增加设置时的大Key自动化迁移, 使存储更加均衡
-
制定合理的Redis数据过期机制, 降低Redis的存储量, 减少服务存储成本
-
增加集合操作服务, 实现跨Redis/Pika实例的交集/并集等操作, 并添加缓存机制, 提高上游服务访问效率, 提高推送消息下发效率
07
总结
本次系统优化在原有存储组件的基础上, 根据服务和数据的特点, 合理优化服务间调用方式, 优化数据存储的空间, 将Redis当作缓存, 只存储访问量较大的数据, 降低了资源使用率。Pika作为数据落地并承载访问量较小的请求, 根据不同存储组件的优缺点, 合理分配请求方式。同时将所有服务设计为高可用, 提高了系统可用性, 稳定性。最后通过增加缓存等设计, 提高了高峰期的查询QPS, 在不扩容的前提下, 保证系统高峰期的响应速度。