百万并发场景中倒排索引与位图计算的实践
Tech导读
本文将深入探讨如何在百万级别的高并发场景下实现高效的数据检索和处理。重点关注倒排索引的实现机制,这是一种使搜索更加迅速的数据结构,以及位图计算,一种优化存储和提高检索效率的技术。通过实际案例分析可以了解这些技术如何帮助处理大规模数据集,保证响应速度,并在高负载环境下维持系统的稳定性。
导读
本文将深入探讨如何在百万级别的高并发场景下实现高效的数据检索和处理。重点关注倒排索引的实现机制,这是一种使搜索更加迅速的数据结构,以及位图计算,一种优化存储和提高检索效率的技术。通过实际案例分析可以了解这些技术如何帮助处理大规模数据集,保证响应速度,并在高负载环境下维持系统的稳定性。
01 背景
在今年的敏捷团队建设中,我通过Suite执行器实现了一键自动化单元测试。Juint除了Suite执行器还有哪些执行器呢?由此我的Runner探索之旅开始了!
该系统也是Promise侧并发量最大的系统,双11高峰集群流量TPS在百万级别,对系统的性能要求非常高,SLA要求在5ms以内,因此对海量请求在规则库(几十万)中如何快速正确匹配规则是该系统的技术挑战点。
02
朴素的解决方案
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
按照朴素的思想,在工程建设上,通过异步方式将规则库逐行缓存到Redis,Key为规则条件,Value为规则对应结果;当用户请求过来时,对请求Request(a,b,c,d..)中的参数做全组合,根据全组合出的Key尝试找出所有可能命中的规则,再从中筛选出最优的规则。如下图所示
该方案面临的问题是全组合的时间复杂度是2**n,n≈12;算法的时间复杂度高且算法稳定性差,最差情况一次请求需要4096次计算和读取操作。当然在工程上可以使用本地缓存做一些优化,但是无法解决最根本的性能问题。架构简图如下所示:
03 新的解决方案
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
04 算法详细设计
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
4.1 预计算生成列的倒排索引和位图
通过对每列的值进行分组合并生成Posting List,建立列值和Posting List的KV关系。以下图为例,列A可生成的倒排索引为:301={1},201={2,3,4,5}等,需要说明的一点,空值也是一种候选项,也需要生成KV关系,如nil={7}。
4.2 生成列的倒排索引对应位图
将用户请求中的入参作为Key,查找符合条件的位图,对每一列进行列内和空值做||运算,最后列间位图做&运算,得到的结果是候选规则集,如下图所示:
4.3 根据用户请求查找列位图,通过位图计算生成候选规则集
4.4 从候选规则库中,根据业务优先级排序,查找最优的规则
05 复杂度分析
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
在空间复杂度方面,相比原来的行式存储,倒排索引的存储方式,每列都需要存储行ID,相当于多了(n-1)*Posting List存储空间,当然这是粗略计算,因为实际上行ID的存储最终转换为位图存储,在空间上有非常大的压缩空间。
06 工程问题—压缩位图
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
如果倒排索引位图非常稀疏,系统会存在非常大的空间浪费。举一个极端case,若千万规则库中命中的行ID是第1000万位,按照传统方式BitSet进行存储,需要消耗1.2MB空间,在内存中占用存在严重浪费,有没有压缩优化方案,在RoaringBitMap压缩位图方案中我们找到,相同场景在压缩位图方式下仅占144bytes;即使在1000万的位图空间,随机存储1万个值,两者比也是在31K vs 2MB,近100倍的差距,总的来说RoaringBitMap压缩率非常大。
07 适用场景分析
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
回顾上面的设计方案可以看到,这种方式仅适用于PostingList简单如行ID的形式,如果是复杂对象就不适合用位图来存储。另外仅适用于等值查询,不适用于like、in的范围查询,为什么有这种局限性?因为这种方式依赖于搜索条件的空间,在方案中将值的条件作为搜索的Key,值的条件空间希望尽可能是一个有限的、方便穷举的、小的空间。而范围查询导致这个空间变成难以穷举、近乎无限扩张的、所以不适用。
08 其他优化方式
理解,首先 MCube 会依据模板缓存状态判断是否需要网络获取最新模板,当获取到模板后进行模板加载,加载阶段会将产物转换为视图树的结构,转换完成后将通过表达式引擎解析表达式并取得正确的值,通过事件解析引擎解析用户自定义事件并完成事件的绑定,完成解析赋值以及事件绑定后进行视图的渲染,最终将目标页面展示到屏幕。
除了使用位运算的方式对倒排索引加速,考虑到Posting List的有序性,还有其他的方式比如使用跳表、Hash表等方式,以ES中采用的跳表为例,进行&运算实际就是在查找两个有序Posting List公共部分,以相互二分查找的形式,将时间复杂度控制在log(n)的级别。
Taro | 高性能小程序的最佳实践
求分享
求点赞
求在看