作者:极光高级工程师—包利
摘要
极光推送后台标签/别名系统存储超过百亿条数据, 高峰期 QPS 超过 50 万, 且随着业务的发展,存储容量和访问量还在不断增加。之前系统存在的一些瓶颈也逐渐显现,所以近一两年持续做了很多的优化工作,最终达到非常不错的效果。近期,经过积累和沉淀后,将这一部分的工作进行总结。
背景
当前的旧系统中主要存储标签/别名与注册 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 的读性能
旧系统缺点分析
考虑到旧系统存在以上的缺点, 主要从以下几个方向解决:
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 的读性能
分析: 这部分手动操作, 应该优化为自动触发, 自动完成迁移, 减少人工干预, 节省人力成本。
Redis 哨兵模式
Redis 哨兵为 Redis/Pika 提供了高可用性, 可以在无需人工干预的情况下抵抗某些类型的故障, 还支持监视, 通知, 自动故障转移, 配置管理等功能:
监视: 哨兵会不断检查主实例和从实例是否按预期工作
通知: 哨兵可以将出现问题的实例以 Redis 的 Pub/Sub 方式通知到应用程序
自动故障转移: 如果主实例出现问题, 可以启动故障转移, 将其中一个从实例升级为主, 并将其他从实例重新配置为新主实例的从实例, 并通知应用程序要使用新的主实例
配置管理: 创建新的从实例或者主实例不可用时等都会通知给应用程序
同时, 哨兵还具有分布式性质, 哨兵本身被设计为可以多个哨兵进程协同工作, 当多个哨兵就给定的主机不再可用这一事实达成共识时, 将执行故障检测, 这降低了误报的可能性。 即使不是所有的哨兵进程都在工作, 哨兵仍能正常工作, 从而使系统能够应对故障。
Redis 哨兵+主从模式能够在 Redis/Pika 发生故障时及时反馈实例的健康状态, 并在必要时进行自动主从切换, 同时会通过 Redis 的 sub/pub 机制通知到订阅此消息的应用程序。从而应用程序感知这个主从切换, 能够短时间将链接切换到健康的实例, 保障服务的正常运行。
没有采用 Redis 的集群模式, 主要有以下几点原因:
当前的存储规模较大, 集群模式在故障时, 恢复时间可能很长
集群模式的主从复制通过异步方式, 故障恢复期间不保证数据的一致性
集群模式中从实例不能对外提供查询, 只是主实例的备份
无法全局模糊匹配 Key, 即无法遍历所有实例来查询一个模糊匹配的 Key
最终解决方案
综上, 为了保证整个存储集群的高可用, 减少故障恢复的时间, 甚至做到故障时对部分业务零影响, 我们采用了 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 实例主从切换时通知业务模块处理容易出错, 这部分功能已经过严格的测试以及线上长时间的功能检验
其他优化
除了通过以上架构优化, 本次优化还包括以下方面:
通过 IO 复用, 由原来的每个线程一个实例链接, 改为在同一个线程同时管理多个链接,提高了服务的 QPS, 降低高峰期的资源使用率(如负载等)
之前的旧系统存在多个模块互相调用情况, 减少模块间耦合, 减少部署及运维成本
之前的旧系统模块间使用 MQ 交互, 效率较低, 改为 RPC 调用, 提高了调用效率, 保证调用的成功率, 减少数据不一致, 方便定位问题
不再使用 Redis/Pika 定制化版本, 可根据需要, 升级到 Redis/Pika 官方的最新稳定版本
查询模块在查询大 Key 时增加缓存, 缓存查询结果, 此缓存过期前不再查询 Redis/Pika, 提高了查询效率
展望
未来此系统还可以从以下几个方面继续改进和优化:
大 Key 存储状态更加智能化管理, 增加设置时的大 Key 自动化迁移, 使存储更加均衡
制定合理的 Redis 数据过期机制, 降低 Redis 的存储量, 减少服务存储成本
增加集合操作服务, 实现跨 Redis/Pika 实例的交集/并集等操作, 并添加缓存机制, 提高上游服务访问效率, 提高推送消息下发效率
总结
本次系统优化在原有存储组件的基础上, 根据服务和数据的特点, 合理优化服务间调用方式, 优化数据存储的空间, 将 Redis 当作缓存, 只存储访问量较大的数据, 降低了资源使用率。Pika 作为数据落地并承载访问量较小的请求, 根据不同存储组件的优缺点, 合理分配请求方式。同时将所有服务设计为高可用, 提高了系统可用性, 稳定性。最后通过增加缓存等设计, 提高了高峰期的查询 QPS, 在不扩容的前提下, 保证系统高峰期的响应速度。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。