首页 > 人工智能 > 人工智能 > OceanBase 向量索引优化指南
物格而后知至。
——《礼记》
OceanBase 最近发布了 seekdb 数据库,主打 “轻量 + 向量 + AI”。
在 seekdb 发布之后,陆续收到了许多用户关于 seekdb 中向量索引在使用上的一些问题,比如:索引创建耗时慢优化问题,创建时对内存的要求,增量达到什么规模需要重建,重建性能影响怎么消除等等等等。

因此,向量索引的研发同学夏进大佬,今天就专门在这篇文章中,从 OceanBase / seekdb 向量索引的构建过程开始讲起,为大家深入且详细地分析上述问题。有任何疑问欢迎大家留言提问~🙋
很多同学会发现,只创建了一个向量索引,却发现多出来一堆辅助表。
CREATE TABLE t1( c1 INT, c2 VECTOR(10), PRIMARY KEY(c1), VECTOR INDEX idx1(c2) WITH (distance=l2, type=hnsw, lib=vsag)); select table_id, table_name, table_type from oceanbase.__all_table where database_id = 500001; +----------+---------------------------------------------+------------+ | table_id | table_name | table_type | +----------+---------------------------------------------+------------+ | 500055 | t1 | 3 | | 500061 | __AUX_LOB_META_500061_ | 13 | | 500062 | __AUX_LOB_PIECE_500062_ | 12 | | 500056 | __idx_500055_idx1 | 5 | | 500059 | __idx_500055_idx1_index_id_table | 5 | | 500060 | __idx_500055_idx1_index_snapshot_data_table | 5 | | 500057 | __idx_500055_rowkey_vid_table | 5 | | 500058 | __idx_500055_vid_rowkey_table | 5 | +----------+---------------------------------------------+------------+ 8 rows in set (0.01 sec)
table_type 的含义详见: seekdb 开源项目代码 [2],这里只截一张图,不再细说。

在了解向量索引构建过程前,需要先了解整个向量索引的组成部分,以 HNSW(Hierarchical Navigable Small World) 索引为例,包含内存索引和磁盘索引两部分。

上图的上半部分蓝色的是内存索引的结构,由三部分组成,分别是 snapshot 快照索引(0-1)、increment增量内存索引(0-3)、valid_bitmap 内存结构(0-3),共同构成了向量索引的内存组成部分。
下半部分黑色的是磁盘索引,包含五个辅助表:
小编理解,意思是 1 号表用于记录主表主键和 vid 的对应关系,vid 的含义是 vector id,其实解释成 vector index value id 会更清楚一些。
小编理解,意思是 2 号表的作用是向量索引查询完成后,通过 vector_id 找到 rowkey 进行回表。
1 号表和 2 号表都有两个相同的列(rowkey + vid),区别是 1 号表的主键是主表主键 rowkey,2 号表的主键是 vid。
小编理解,3 号表主要是用于记录发生更改的 VectorID 和 Type,Type 只有两种:'I' 表示新增, 'D' 表示删除,每个 ID 至多被写入一次和删除一次。
小编理解:
1 ~ 2 号表,因为和主表主键都有关系,所以是共享辅助表,由一张表上的所有向量索引共用。
3,4,5 号表,是每个向量索引独占的索引辅助表,也都有 vid 列。
后面这三张表,感觉大家不需要细究其作用,可以简单理解成:向量数据维数限制很宽,所以需要用 LOB 这种大对象进行存储。大对象不能反复存储,所以只在 5 号表里存储了一份,其他表都是用来保证向量索引中大对象的更新和查询效率的。
在了解完索引辅助表在内存和磁盘上的整体结构后,我们来了解下索引表的构建流程。
首先需要创建上文中提到的 5 个辅助表以及对应内容,当前在创建辅助表的过程中,使用的是 OceanBase DDL 框架,主要以 DDL task 的形式实现。对于一个 DDL task 来说,主要是以状态机的形式进行实现,推进每一个状态的执行以及切换,处理不同辅助表的创建过程。

在状态机中流程共有三步。
待上述流程全部完成后,即进入索引生效状态,然后将索引创建流程结束,即可开始使用。
对于 DDL task 的执行流程,主要以状态机的形式来进行流程处理。主要逻辑是根据当前的状态,进行对应状态的处理,以及对下一个状态的转移。可能会有用户感到疑惑,为什么要引入索引状态机?

利用状态机的好处主要有两点:一是流程可视化,二是状态持久化。
考虑到可能存在一些异常场景,例如 LS(LogStream) 切主,或者重启、宕机等。在这些异常场景下,如果当前在进行 5 号表的补数据流程,在过程中发生了切主或重启,在场景异常恢复正常后,只需要从五号表到进行中状态继续往下进行,而不需要从头再进行一遍,以提高异常场景下的容错能力。
通过两张图,来分析一下索引构建过程中的耗时点。

图中集群的本地数据量为 2000 万,在该集群创建构建索引,通过查询内部表 _allrootservice eventhistory 得出构建索引每个状态的对应耗时,可以看出:
因此,整个构建索引过程大部分耗时都集中在 WAIT VIDRPWKEY TABLECOMPLEWEMT 状态中。

通过查询内部表 _allrootservice eventhistory 的 WAIT VIDRPWKEY TABLECOMPLEWEMT 状态下对应表的构建子任务的状态和耗时可以看出:耗时比较久的是 REDEFINITION 状态,耗时为 3.5 小时,基本接近上文 WAIT VIDRPWKEY TABLECOMPLEWEMT 状态的耗时,也就是说WAIT VIDRPWKEY TABLECOMPLEWEMT 状态的耗时点在 REDEFINITION 状态。
小编划重点:
从这里开始的内容,一定要看下!
推荐收藏,以备不时之需~
GV$SESSION_LONGOPS 视图用于展示集群 DDL 操作的执行状态和进度,从该视图中可以得出构建过程的各个状态。

首先需要关注并行度。图中的并行度 PARALLELISM 为 1,也就是说同时进行的构建过程只有一个,构建过程中的补数据操作的并行度也是 1,因此该场景下的后键过程是比较慢的,也就是说造成构建慢的很重要因素是没有开并行。
第二个因素是补数据过程中的采样点。

上图是红框为每个采样点的数值,第一个采样点是 122000,第二个采样点是 178000,第三个采样点是 297000,第四个采样点是 406000,第五个采样点是 642000,第六个采样点是 648000。
第二个采样点和第一个采样点的值相差 5 万左右,因此第一个分片有 5 万左右的数据。
以此类推:第二个分片有 12 万左右的数据,第三个分片有 11 万左右的数据,第四个分片有 20 万左右的数据,第五个分片只有 0.6 万左右的数据,从第三个分片开始,采样的数据量差距越来越大,到第五个分片却只有 0.6 万数据。
可以得出一个结论:采样可能不均衡。
那么这些分片是什么意思,然后他在构建过程中是有什么用呢?
OceanBase 在补数据的过程中,利用了 PX 并行框架,可在创建索引时指定使用的线程数。上文补数据过程中并行度为 1,可能是在创建索引时未加 Hint,未指定并行度,导致只使用了一个线程进行补数据操作。
假设开 10 个线程用于补数据,PX 框架中会先把一些数据采样出来,由于每个分片大小不一样,同时补数据是按照线程处理分配的,假设有十个数据,对应十个分片,每个线程处理一个分片的数据。如果采样不均衡,可能会存在某个分片的数量特别大,某个分片的数量特别小。
例如现在有 100 万的数据,指定 10 个线程数,分成 10 个分片。可能第一个分片处理了 99 万数据,第二个分片或剩下的分片只处理了几千的数据。最终导致大部分时间都消耗在了第一个线程中,造成整个构建效率索引不高,因此第二个创建索引耗时的影响因素是采样不均。
第三个造成后键索引速度慢的原因是:单条写入慢。如果表是非分区表,在对非分区表补数据时,相当于只有一个内存索引。假设内存索引存储了 100 万数据,在做补数据时,会将全部 100 万的数据写入一个分区内。HNSW 索引是 HNSW 的图结构,插入索引时,如果图的数据量越大,在搜图的耗时就越长。可能在插入到 90 万条数据后,插入速度已经变得非常缓慢。如果将表改为分区表,例如将 100 万的数据平摊到 10 个分区中,相当于在使用并行,此时插入效率会比一个分区快很多。
因此,构建索引慢的优化方法有:加并行、提高采样率、改分区表三种。
通过查询 _allvirtual vectorindex_info 类目表,可以得出几个关键信息。

内存索引主要由三个部分组成,分别是:
其中内存占用大部分在增量索引内存和快照索引内存,因此主要需要优化这两部分的内存占用。
导致内存占用高的阶段主要可能有构建过程中的内存占用和建完后的 DML 和持久化操作。
关于内存占用分析,有如下几个场景的优化建议。
除此之外,建议使用内存预估提前规划好内存。针对目前一些客户的反馈问题,例如在后期过程中,如果发现内存不足导致报错、卡住等现象,可以在建内存索引前使用工具预估内存,例如 OceanBase 官方提供的 DBMS 工具,进行预估内存、提前规划,即可避免后续出现相关报错。

内存预估能力会在 OceanBase V4.3.5_BP3 之后的版本支持。
综上所述,构建速度优化和构建内存优化的方式总结如下。
随着 DML 操作带来的更新数据变多,查询内存增量索引和 valid_bitmap 的代价变大,重建的目的是减少增量索引的内存占用和查询代价。
重建索引的原理其实很简单,即新建一个同名索引表,完成数据导入后,再删除旧索引,再交换索引名,使新索引生效。下图是重建索引的框架图,如图所示,是从 RS 中做驱动,执行 DDL 任务的流程,最终完成索引的创建。

REBUILD INDEX 过程用于全量刷新(即重建)向量索引,触发重建索引的语法为(不设置并行度):call dbmsvector.rebuild_index('idx1','t1','c2')。

更多介绍可参见文档: OceanBase 官方文档 —— REBUILD_INDEX [3]。
重建索引是表级别的 Rebuild,较为耗时,一般建议增量数据占比超过快照数据的 20%,或者查询时出现 3 号表的数据访问热点时,选择重建。建议在 CPU 和内存空闲的时间段进行。
由于重建索引过程中会同时存在新旧两个索引,因此内存占用最大可能会是原来索引的 2 倍,重建完成后内存下降新索引内存水位。该过程有一些可用的优化手段,因为在做构建索引时,补数据是按照分区进行的,即不是一下补所有分区,而是一个分区一个分区进行,可以在某个分区重建索引好,立即将原内存索引删除。例如有一个分区表,有 10 个分区,原索引占用了 10G 内存,在资源有限的情况下,可以只预留 11G 或 12G 的内存空间,每次只进行单分区的索引重建及重建后删除,整体不会占用太多的磁盘和内存空间。
重建后的内存绝大部分集中在 snap index 快照索引,但若重建过程中有 DML 操作,重建后的incrindex 增量索引也会有新的内存开销,因此建议在 Rebuild 索引时,将 DML 流量关掉。

关于 seekdb 和 OceanBase 向量索引未来的能力有如下 3 点展望:
参考资料
[1] 在线体验环境: https://www.oceanbase.com/demo/ob-hybrid-search-quick-start
[2] seekdb 开源项目代码: https://github.com/oceanbase/seekdb/blob/develop/src/share/schema/obschema struct.h
[3] OceanBase 官方文档 —— REBUILD INDEX: _https://www.oceanbase.com/docs/common-oceanbase-database-cn-1000000003980771
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70047136/viewspace-3105705/ ,如需转载,请注明出处,否则将追究法律责任。