关于索引:得物推荐引擎-DGraph
1 前言随着得物业务规模的一直减少,举荐业务也越来越简单,对举荐零碎也提出了更高的要求。咱们于2022年下半年启动了DGraph的研发,DGraph是一个C++我的项目,指标是打造一个高效易用的举荐引擎。举荐场景的特点是表多、数据更新频繁、单次查问会波及多张表。理解这些特点,对于举荐引擎的设计十分重要。通过浏览本文,心愿能对大家理解举荐引擎有肯定帮忙。为什么叫DGraph?因为举荐场景次要是用x2i(KVV)表举荐为主,而x2i数据是图(Graph)的边,所以咱们给得物的举荐引擎取名DGraph。 2 注释2.1 整体架构DGraph能够划分为索引层&服务层。索引层实现了索引的增删改查。服务层则蕴含Graph算子框架、对外服务、Query解析、输入编码、排序框架等偏业务的模块。 图1 2.2 索引框架在DGraph外面参考图1,索引的治理被形象成5个模块:Reader 索引查问、Writer 索引写入、Compaction 增量全量合并、LifeCycle 索引生命周期治理、Schema 索引配置信息。 不同类型的索引只须要实现下面的5个类即可,不同类型的索引只须要关注索引自身的实现形式,而不须要关怀索引的治理问题,通过这种模式,索引治理模块实现了索引的形象治理,如果业务须要,能够疾速在DGraph面退出一种新的索引。 DGraph数据的治理都是按表(table)进行的(图2),简单的索引会应用到DGraph的内存分配器D-Allocator,比方KVV/KV的增量局部 & 倒排索引 & 向量索引等。在DGraph所有数据更新都是DUMP(耗时)->索引构建(耗时)->引擎更新(图3),索引平台会依据DGraph引擎的内存状况主动抉择在线更新还是分批重启更新。这种形式让DGraph引擎的索引更新速度&服务的稳定性失去了很大的晋升。 图2图3 2.3 索引数据一致性 相比订单、交易等对于数据一致性要求十分严格的场景。在搜推场景,数据不须要严格的一致性,只须要最终一致性。若一个集群有N个引擎,通过增量向集群写入一条数据,每个引擎是独立更新这条数据的,因为是独立的,所以有些机器会更新快一点,有些机器会更新慢一点,这个时间尺度在毫秒级左近,实践上在某一时刻,不同引擎上的数据是不统一的,但这对业务影响不大,因为最终这些数据会保持一致。 最终一致性这个个性十分重要,因为实现严格的一致性很简单,2PC&3PC等操作在分布式场景下,代价很高。所以事件就变得简略了很多,引擎的读写模型只须要满足最终一致性即可。这能够让咱们的零碎,更偏差于提供更高的读性能。这个前提也是DGraph目前很多设计的根因。 读写模型 举荐场景须要反对在线服务更新数据,因而引擎有读也有写,所以它也存在读写问题。另外引擎还须要对索引的空间进行治理,相似于JAVA零碎外面JVM的内存管理工作,不过引擎做的简略很多。读写问题常见的解决方案是数据加锁。数据库和大部分业务代码外面都能够这么做,这些场景加锁是解决读写问题最靠谱的抉择。然而在举荐引擎外面,对于读取的性能要求十分高,外围数据的拜访如果引入锁,会让引擎的查问性能受到很大的限度。 举荐引擎是一个读多写少的场景,因而咱们在技术路线上抉择的是无锁数据结构RCU。RCU在很多软件系统外面有利用,比方Linux 内核外面的kfifo。大部分RCU的实现都是基于硬件提供的CAS机制,反对无锁下的单写单读、单写多读、多写单读等。DGraph抉择的是单写多读+提早开释类型的无锁机制。效率上比基于CAS机制的RCU构造好一点,因为CAS尽管无锁,然而CAS会锁CPU缓存总线,这在肯定水平上会影响CPU的吞吐率。 如果简略形容DGraph的索引构造,能够了解为实现了RcuDoc(正排)、RcuRoaringBitMap(倒排)、RcuList、RcuArray、RcuList、RcuHashMap等。用举荐场景可推池来举一个例子,可推池表的存储构造能够形象成RcuHashMap<Key, RcuDoc> table。这里用RcuList来举例子,能够用来了解DGraph的RCU机制。其中MEMORY_BARRIER是为了禁止编译器对代码重排,避免乱序执行。 图4 图5 图5是删除的例子,简略讲一下,在RcuList外面,删除一个元素的时候,比方Node19,因为删除期间可能有其余线程在拜访数据,所以对List的操作和惯例的操作有些不同,首先将Node11的Next节点指向Node29,保障前面进来的线程不会拜访Node19,而后把Node19的Next指向Null,因为这个时候可能还有线程在拜访Node19,因而咱们不能立刻把Node19删除,而是把Node19放入删除队列,提早15秒之后再删除,另外删除的动作不是被动的,而是由下一个须要申请内存的操作触发,因而删除是延时且Lazy的。 数据长久化 在DGraph外面咱们构建了一个内存分配器D-Allocator(每个索引只能申请一个/可选),用于存储增量或者倒排索引等简单数据结构。采纳了相似TcMalloc按大小分类的管理模式。D-Allocator利用Linux零碎的mmap办法每次从固定的空间申请128M ~ 1GB大小,而后再按块划分&组织。由零碎的文件同步机制保证数据的长久化。目前64位x86 CPU理论寻址空间只有48位,而在Linux下无效的地址区间是 0x00000000 00000000 ~ 0x00007FFF FFFFFFFF 和 0xFFFF8000 00000000 ~ 0xFFFFFFFF FFFFFFFF 两个地址区间。而每个地址区间都有128TB的地址空间能够应用,所以总共是256TB的可用空间。在Linux下,堆的增长方向是从下往上,栈的增长方向是从上往下,为了尽可能保证系统运行的安全性,咱们把0x0000 1000 0000 0000 到 0x0000 6fff ffff ffff调配给索引空间,一共96TB,每个内存分配器能够最大应用100GB空间。为了方便管理,咱们引入了表keyID,用于固定地址寻址,表地址 = 0x0000 1000 0000 0000 + keyId * 100GB, 引擎治理平台会对立治理每个集群的keyId,偶数位调配给表,奇数位保留作为表切换时应用。keyId 0 - 600 调配给集群独享表,keyId 600-960调配给全局表。因而单个集群能够最多加载300个独享表+最多180共享表(备注:不是所有表都须要D-Allocator,目前没有增量的KVV/KV表不受这个规定限度)。 ...