专业的编程技术博客社区

网站首页 > 博客文章 正文

Archer:奇麟数仓倒排索引设计实现

baijin 2024-10-27 08:06:30 博客文章 4 ℃ 0 评论

1. 背景

在大数据分析领域中,ElasticSearch常常被作为日志存储分析引擎,快速构建日志数据分析服务。但随着日志量的不断增加,ES在大规模日志存储上面的一些问题逐渐暴露出来:

1. ES 对服务器的内存磁盘性能要求较高,随着数据量增加,投入的服务器成本越来越高,不支持直接查询存储于S3的历史快照数据,无法做到存算分离;
2. ES 运维较为复杂,成本高;
3. SQL支持不完善,在聚合查询方面与传统OLAP引擎性能差距较大;
4. 与数仓数据脱节,不方便与Hive/Mysql中的数据关联分析;

针对以上几个问题,在智汇云的数仓产品中,在Iceberg基础上,开出了一个新的引擎 — Archer,将倒排索引机制引入,从而使奇麟数仓分析工具具备了全文检索的能力。由新版Archer引擎加持的数仓具备了以下几个特点:

1. 正排数据和倒排索引文件直接存储于HDFS/S3中,存储横向扩展;
2. 通过LocalCache本地缓存数据,加速索引的访问和检索速度;
3. 计算引擎构建于容器之上,支持算力的灵活弹性扩缩容,运维简单;
4. SQL支持完善,列式存储数据,OLAP查询性能强;
5. 灵活的倒排索引构建方式,满足全文检索和json数据检索场景;
6. 支持联邦查询,方便与Hive/Mysql中的数据进行关联查询;

2. 设计

Archer设计中采用了Parquet格式存储数据文件,采用Tantivy技术构建倒排索引。Tantivy启发于Lucene,用于构建搜索引擎,Tantivy对减少IO请求次数做了优化,因此更适合存储于HDFS/S3。

2.1. 索引类型

支持text/json/json_text/ip/datetime/raw六种类型索引
? text,对varchar字段进行分词并索引
? json,对varchar字段按Json object进行解析并索引,类似ES的嵌套文档
? json_text,对varchar字段按Json object进行解析,并对json内的string value进行分词,然后构建索引
? ip,对varchar字段按照ip格式解析成字节数组进行索引,支持范围查询
? datetime,对timestamp字段进行索引,支持范围查询
? raw,索引原值,支持boolean/int/long/float/double/varchar/varbinary字段类型

2.2. 架构

存算分离
与Lucene的LuceneDirectory类似,Tantivy也支持通过实现TantivyDirectory,来支持对索引文件的自定义读写模式。通过实现TantivyDirectory,可以获得直接在HDFS/S3之上读写索引文件的能力,再通过LocalCache提高了IO性能,减少对HDFS/S3的请求。

通过查询倒排索引,可以获得满足条件的doc id列表(与Parquet文件内行号一致),再通过Parquet文件内的PageOffset对Page进行裁剪,以减少IO请求,同时也避免了大量的数据解析和计算工作。默认情况下,每个Page保存1MB数据。

奇麟数仓倒排索引功能的框架如下图所示:


Parquet Page裁剪图示:

2.3. 查询

在奇麟数仓架构中,我们使用Trino作为查询引擎,需要在建表/查询上做一些兼容性工作。

? 支持查询倒排索:为了避免对Trino核心代码进行修改,我们对包含倒排索引的表,添加了一个varchar类型的隐藏列"$inverted_index_query",当where条件包含该列时,就自动获得了Trino的谓词下推能力,从而把Query提交到倒排索引。因为是enforced predicate,此时Trino不需要connector返回该隐藏列。

? 支持OR逻辑:此时Trino需要connector返回该隐藏列,这里我们在查询完倒排索引后,并不会过滤数据,而是把符合条件的行填充成query字符串本身,不符合条件的行填充成NULL

建表语句例子如下:


查询语句例子如下:

2.4. 优化

2.4.1. 文件裁剪

Tantivy会为每个segment创建最少8个文件,其中有4个是我们的场景下完全用不上的,比如fieldnorm/store/fast等文件,这类文件用于支持文档评分和列式存储。在实际的检索过程中,这些文件并不会被读取具体内容,但会增加HDFS/S3上的小文件数量,且会在加载所有segment的时候自动加载元数据部分,进一步提高了检索延迟。我们在实现TantivyDirectory的时候,会忽略这些文件的写入请求,并在读取的时候,返回符合文件规范的静态数据。

2.4.2. 优化内存

我们需要为每个parquet构建一个倒排索引,为了避免小文件过多的问题,一般单个parquet文件的大小在一定范围内是越大越好的,在写入parquet文件的过程中,内存占用的大小就是单个RowGroup的大小。但构建倒排索引文件需要对数据进行排序,在不进行外排序的情况下,需要把全量数据都缓存在内存,因此可能导致进程内存占用过多而崩溃。因此我们对单个索引segment的内存占用进行了限制,当内存超过限制时,会在后台提前写到外部存储,然后再开一个新的segment写新数据。最后关闭parquet的时候,会根据segment数量来决定是否进一步执行segment合并,来优化查询性能。

2.4.3. 预读取

文件内的term/doc id/pos id等数据都是经过排序的,因此在进行范围检索和索引文件合并的时候,有顺序读取文件的特征,但Tantivy目前的实现没有考虑这个问题,在这些场景下,会有非常多小而连续的IO请求。我们在实现TantivyDirectory的时候,对这类操作添加了preload+cache的操作,读取数据时候,会优先查看cache是否已经满足要求,如果不满足,则会在实际请求之上额外请求一部分数据并缓存,这样可以明显减少对远程存储的请求次数,性能会有 100x ~ 1000x 的提升。

2.4.4. 合并IO

对Parquet Page进行裁剪之后,可能会有不连续,但间隔比较小的Page请求。我们对间隔较小的请求进行合并,在读放大和IO次数间做了一些平衡,以减少读延迟。

2.4.5. 动态Split分配

Trino默认按128MB切分文件分发Split,如果以这种方式分发Split,同一个倒排索引会被加载和查询多次。同时,倒排索引一般可以过滤掉大部分RowGroup和Page,也就不需要通过切分文件来增加并行处理能力。因此我们添加了一种动态Split分配模式,在分配Split的时候,如果检查到倒排索引查询,将不再切分文件,而是把整个文件作为一个Split来分发。该行为可以在session级别随时调整。

2.4.6. 序列化

Tantivy采用Rust开发,而Trino采用Java开发,因此需要通过JNI交换数据。为了提高序列化和反序列性能,我们采用Arrow作为内存数据格式,避免了反复的序列化和反序列化操作。

3. 测试结果

我们使用amazon_reviews数据对全文检索进行了测试,使用内部数据对json检索进行了测试,并记录了查询时间和扫描数据量。对比iceberg connector,在使用倒排索引时,可以显著减少查询时间和扫描数据量。

3.1. 全文检索

SQL查询代码如下:

archer,1.52秒,扫描122KB数据


iceberg,15.64秒,扫描7.02GB数据

3.2. Json检索

SQL如下:


archer,1.36秒,扫描14.6MB数据

iceberg,14.37秒,扫描454MB数据



了解更多奇麟数仓大数据产品,请关注“360智汇云”

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表