星环科技分布式搜索引擎 Transwarp Scope 查询优化技术解读
概述
分布式数据库系统是物理上分布而逻辑上集中的数据库系统,为了提高性能并最大限度地减少资源争用,其被广泛用于海量数据处理的场景中。在这种情况下,数据库查询速度是系统性能表现的决定性指标。而由于数据分布在不同节点上并通过网络通信在不同节点间传输,分布式查询的处理流程比单机集中式查询更加复杂。与传统的集中式数据库系统相比,对分布式查询的构建和优化需要同时考虑 CPU、I/O 成本以及网络通信成本。
本文旨在从分布式集群视角,对 Transwarp Scope 查询相关原理和优化技术进行较为全面的解读。
整体流程
对于分布式搜索引擎来说,一般情况下, 一次查询涉及到多台机器的多个分片,正确的结果需要汇总多个分片的各自结果之后才能获得。因此,无论是 Transwarp Scope 还是 es,其查询过程都包括一个 Merger 的角色存在,这个 Merger 在 es 中是 Coordinating node, 而在 NS 中是 Client。而整个流程以 Phase 划分,可以分为 DFS, QUERY, FETCH 三类 Phase。
专用词与明确
分片一般也被成为 shard/tablet
Phase 简介
DFS Phase:统计数据收集阶段,对于文本信息来说,其在单个 text 中的 freq 等信息是准确的。但是类似与 idf 这样的全局统计信息而言,每个分片只能明确该文本在分片内部的 idf,也就是一个局部的 idf。如果不进行全局 idf 综合统计,仅以 local idf 计算 score,得出来的分数是不准确的。所以,在很多对打分结果准确性要求较高的场景下, 都会有 dfs 这个阶段进行全局统计信息汇总。当然,也因为多了这个阶段,相应地响应速度也会受到影响。
Query Phase:查询阶段, 根据 client 输入的信息在各个分片上找到匹配的文档集合。这一阶段基本上会做 3 件事情:match(匹配),score(打分),local_sort(本地排序)。各个分片会将匹配的 doc_id 集合,返回给 Merger 节点。Merger 节点会对各个分片汇报上来的 doc_set 进行 merge + global_sort。然后根据 client 设定的 from,size, 从 global_result_set 中 cut 出[from, from + size],再进行下一阶段。
Fetch 阶段:获取 doc 原始内容的 phase。该 Phase 会根据 Query Phase 结束后的 global_result_set 向各个分片索要目标的 doc_set, 包括文档的原始内容以及可能的某些再加工内容,比如 Highlight。由于要真正地加载文档内容[source],所以 Fetch 阶段会产生比较大的 io 负载(page cache 缺失的情况下)。因此,如果是一些大宽表(500 列+)的场景,其行数据 size 比较大的情况下,更可行的方式其实是把 ES/NS 作为一张纯粹的 Index Table,即只对目标列设置索引 + 对外表主键列存储 source。如此,当 query 阶段阶段执行完之后,进行 fetch phase 的时候只需要加载 rowkey 这一列的值,再 global_result_set 中的外表 rowkey 值去外部行数据库中拿到原始内容,这样做能明显减轻 es/ns 集群的存储和读写压力。
从整体上来看,查询部分基本的架构原则就是用各种不同的 Phase 拼接执行不同的查询动作,即 Compose Phases into Action.如上图示意。
查询操作类型简介
查询操作本身可以按照如上图这样进行细分, 各自含义如下表:
点查询图解
点查,或者说排序查询是核心功能,举例如下。
对于一张成绩表 schema=(姓名、数学成绩、语文成绩、 英语成绩),整张表格有 3 个 tablet, 现在要获取全部成绩的前 3 名,则整体流程如下图所示。
如上图所示,即为单次点查询的原理示意图。在 Query 阶段,所有 Tablet 都将自己的数学成绩的前 3 名汇总给 Merger, Merger 进行全局排序之后,发现真正的前三名是 tablet1 的 11,4 号, tablet3 的 4 号。然后在 Fetch 阶段,将这些对应 doc 标识发送给 tablet1, tablet3, 再拿到对应的文档原始内容,这里有 2 处细节值得提及。
二维全局 rowKey。在上图所示数据分布体系中, 用以表示全局唯一 row 或者 doc 的标识是一个(tablet, docId)的二元组,及 tablet1 和 tablet3 都有 doc4, 但 2 者没有关系。
上图所示是在全局数据本身无序分布的情况下进行排序查询的流程,如果对数据本身就是有序分布的, 那么流程会大大简化,这一点会在后续内容中讨论。
分页查询
所谓分页查询,或者扫描,就是当结果集比较大的时候,分成多次 rpc 返回结果。
1.并发分页查询
所谓并发分页,如下图所示,就是 client 同时向所有的 tablet 发送 request。这种情况下,每一页的具体流程以排序/不排序分可以对应上文点查/轻量点查。
2.顺序分页查
所谓顺序分页查,如上右图所示,指的是每一页并不是将 rpc 同时发送给所有 tablet, 而是对所有 tablet 进行逐个扫描,tablet1,tablet2,tablet3。这种扫描方式的明显好处就是大幅度减少了 rpc 的数量,降低了集群整体负载。又因为每个 rpc 只有 1 个 tablet 的结果,所以也不需要进行多个 tablet 结果的合并,降低了 client 的处理负载。
3.动态超分页查询
对于查询操作来说,缓存是很有效果的优化措施。尤其是对一些单线程扫描全表的应用,其客户端内存可能大量闲置。这种场景下,合理地使用客户端内存作为缓存来优化查询速度,就是动态超分页查询的思想,其基本原理仍以是否排序分 2 种情况讨论。
对于不排序场景,缓存的策略很简单,如上图所示,就是一次 rpc 取 n 个整页,放在客户端内存中备用,从第二次之后,直接从本地内存中取用。而为了在保证稳定性的基础上尽可能地加快 scan,对于 N 这个值采用二进制试探+回退的方式进行控制。即最开始只取一页,然后是 2,4,8,16。在这个过程中,保存 Page 的平均大小和已经使用的内存量,综合 jvm 内存大小,从而计算出下一次 scan 最大能拿多少页。从而让 N 回退,降低 client 内存压力,保证客户端程序的稳定。在实际使用中,一般会限定客户端 jvm_heap 的 8%作为 scan_cache 的上限。
此外,为了避免 N 过大导致延迟过长问题,当单次时间超过一定阈值的时候,N 也会相应回退,避免让客户端感觉到太明显的卡顿。
对于排序场景,缓存不能像 no sort 场景下这么鲁莽。因为排序本身存在一个回收率(1/s)的问题,即前文所提及的,3 个 shard, 取前 3 名,则实际上需要拿到 3×3=9 行数据,最终有效返回却是只有 3 行,所以回收率=1/3。在超大集群场景下,一张大表可能有 500+个 shard,此时如果贸然地扩大 N 倍,一次性从 server 端取回 4000-5000 个 page,很有可能造成 client 剧烈的 gc, 影响程序稳定。因此,排序场景下客户端缓存,Transwarp Scope 采用了客户端复用的方式来进行。
如上图所示,续前文所述排序场景下 QueryThenFetch 的流程,当第一轮 Fetch 结束之后,真正的全局前三被 fetch 之后,剩余的(图中标红的)T1-doc5, T2-doc11-doc22-doc15 和 T3-doc32-doc5,一定是下一轮全局排序的备选项,所以下一轮 query 阶段并不需要再从每个 tablet 拿 3 个了,对于 tablet1,只需要再拿 2 个,tablet3 再拿 3 个, 而 tablet2 则不需要在 round2 进行 query 阶段。在超大表的场景下,以 500shard, page_size=1000 为例, 那么 98%的 row 都可以在客户端进行复用,从而大大减少了 rpc 次数和 server 端查询排序的开销。当然,实际生产环境中也要考虑到 rpc_size 的问题,配合整页缓存一起使用。
查询优化的基础:分区
分区是最直接有效的查询加速手段,尤其是对于超大规模的集群的大表(1000+ shard, 单表 50T)这样的场景,如果能在查询真正开始之前将搜索范围缩小到全量数据集合的 1-2%,即 10-20shard,500G-1000G 这个规模。那么实际表现出来的性能就是百毫秒到秒级级别。
最常见的两种分区机制,是 Range 分区和 Hash 分区。
1.Range 分区
如上图所示,即为 Range 分区的基本原理示意图,所有的 row, 按照 age 这一列进行划分 partition。当 select (14,19)之间的 row 时,就可以通过 partition prune 将查询限制在一个 tablet1 上,从而避免了全表搜索,大幅度减少了集群负载。
另外,在排序场景下,如果要获取全局 age 最大的 5 个 row, 那么在已有范围分区的情况下,只需要对 tablet1 和 tablet2 的数据进行排序, 填满结果集即可,避免了对 Tablet1 的无效查询和排序。
又如上图所示,在 Range 分区的基础上,配合分片内部的预排序,就可以保证整张表格数据的全局有序。此时的升序扫表动作,就转换成了顺序依次扫描每个 shard,从而完全避免了分片级别/表级别的排序动作,极大提升速度。
2.Hash 分区
hash 分区的即是根据指定列的 hash 值进行分区,如上图所示,当搜索 age=13 的所有 row 时, 由于 13 的 hash 值是 1,所以搜索可以被剪枝到 tablet1 上,从而避免了 tablet0, tablet2 的无效搜索。
3.混合分区
分区的实际意义在于,通过对数据进行物理分布上的隔离,从而查询时进行大片的剪枝。在实际使用中,真实数据可能有很多的细化查询需求,需要对数据进行不止一层或一种分区,这就对应了混合分区的概念。
如上图所示,数据全集采用 2 层分区进行物理隔离,在 shard 级别,按照 age 进行范围分区。在每个 shard 内部,再按照 rid 进行 hash 分区。那么对于如上图 sql, 查询操作能立刻通过 partiton prune 将范围缩小到 shard1 的 P0 Parition 上,查询范围大大缩小。
注意,在同一个物理隔离级别上,只能有一个 Range 分区标准,否则会有歧义导致无法排序。而 Hash 分区可以组合多个。
总结
本文分别从客户端和集群的视角,介绍了 Transwarp Scope 的查询的基本流程、基本原理、实现方式以及不同类型分区对查询速度带来的优化。
评论