本文主要是介绍一文详解粗排服务 向量计算引擎单机高可用模式设计,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
文章目录
- 1. 前言
- 2.初步设计
- 1.需求
- 3.单机的实现
- 4. 高可用的实现
- 3.详细设计
- 1. 数据库设计
- 2.manager设计
- 1.manager接口
- 1. 注册接口
- 2.上传向量接口
- 3.向量生效接口
- 4.server通知开始加载向量接口
- 5.server通知完成加载接口
- 6.server通知完成切换接口
- 7.向量详情接口
- 3. server端设计
- 1. 项目启动的加载
- 2. 定时任务
1. 前言
这篇向量计算引擎就是粗排服务,选用了双塔的粗排服务之后,粗排的实现就是快速的向量计算了。
这次项目的选型过程历经时间比较长,前期调研了ES的实现方案,vearch的实现方案,以及milvus的实现方案。vearch的bug比较多,可能有些还是比较常见的问题,milvus目前尚且不支持标量的过滤,而且使用的k8s来管理的,可能还要有一些成本。ES的实现方案是计算的时间比较长,500个数据,可能都要消耗50ms,在性能上无法满足使用的要求。之所以花了这么多时间调研解决方案,主要还是想要更好的解决高可用的方案,因为这个涉及到了数据的存储,所以对服务的稳定性要求也比较高,本来想着是把这一块儿托管到第三方来做的,毕竟自己想要实现难度还是比较大的,因为开始是想做成ES那种数据可以分片存储,又可以通过副本来备份的方式来支持分布式存储+分布式计算。
坑爹的是,我最开始的技术选型定的是ES,使用ES效果不佳的情况下强行花了很多时间优化,最开始实际上我做过一些测试,500个基本上是在30ms左右,所以才决定选用ES,但是我忽略了es计算的稳定性,于是在开发很多代码后只能硬上了(换别的方案已经来不及了)。最开始使用2000个物品id去访问ES,发现响应不行,于是改成了500,这样的话自己的任务中就需要做并行处理,代码的复杂度也会上升,同时,我还破解了ES的默认的routing规则,每次请求中的500个id都在同一个shard上面,但是即使这样,还是不行,上线即使只开了10%的流量,服务的稳定性波动都比较大,根本就是无法支持线上服务的状态。就在调用方同学即将放弃的时候,我又想到了一个优化点,就是把es的索引设置为内存模式,这个设置最终算是救了我一命吧,至少支持住了20%的实验流量。让我阶段性的完成了任务。
"store" : {"type" : "mmapfs"}
本次使用es集群做粗排方案时使用的是一个线上的集群,部署的有其他业务,尚且不确定假如使用的是是单独的ES集群效果会有多大的提升。我总体的感觉是ES是一个通用解决方案,可能他的向量计算解决方案尚且不够优秀,没有做太多加速,我计算的时候,有些只需要20ms,有些需要100ms,感觉表现很不稳定。cpu也很容易就打上来了。
最终迫不得已自己来做一个向量计算引擎,简单的使用map放到内存中进行计算的话,5000个向量的点积计算也就是毫秒级别,所以非常的快,相对ES提升了太多。因为性能吊打ES,所以就决定自己来开发向量的计算引擎了。因为前面使用ES做解决方案花费了很多时间(两周),所以后面留给我的时间已经非常少了,所以勉强在一周多一点的时间开发了一个高可用的向量计算引擎,真是自己坑自己啊。
2.初步设计
1.需求
要从600w向量中,找出5000个,计算和目标向量的点积,然后按照点积大小排序,选择前topN个进行排序。向量的维度32-128,均为float32。前期的工作可以暂时不考虑增量的数据情况,只需要做好全量的向量更新情况即可。向量文件存储在hdfs上。
在粗排系统更新完向量之后,对应的后续的预测系统也需要更新预测是模型才行,因为他们是放在一起进行计算的。所以业务方的模型和粗排系统的向量需要协同更新才行。
3.单机的实现
分为server端和manager端,server端负责向量的计算服务,manager端负责管理向量的业务的元数据。
考虑加入只有一个单机如何实现,可以用一个数据表来保存各个业务的向量,每个业务要有一个业务编号src。
- 程序在启动的时候扫描这个表,并行的去加载数据向量。向量加载完成后放到内存中的map,然后对外提供服务。
- 程序在运行中,可以监控这个表,不断的看看是否有新的向量想要加入,或者是否有更新任务,然后加载向量
- 这里的向量加载完毕后理论上不能立刻投入使用,还是要看业务方的信号,给业务方一个接口,让业务方选择合适的时间切换向量
- 因为需要操作数据库,这样看来程序需要一个操作数据库的manager端,同时为了简化向量计算服务的逻辑,server短所有的指令都通过检查数据库获取,而不是从外部调用获取,简化了server端的处理逻辑。
- 同样的,因为是存储服务,所以对稳定性要求比较重要,服务中的报警通知,异常管理要比较清晰,严重的异常能够通知到位,同时要能够查看各个服务节点的业务向量信息,像ES的cluster state一样。
4. 高可用的实现
同样的,因为是线上服务,肯定不能只有一个节点,因为单个服务器出现一些故障还是有可能的,如果节点提升到两个,服务的稳定性就大大提升了。如果是两个服务的话,就会引入更多的问题,比如向量更新的时候需要保证两个节点都更新成功了才算成功,可以对外提供服务了,所以需要一些同步机制。
manager如何感知server端对向量更新的状态呢,我这里使用的是manager端提供http api,server端在完成对应的进程后调用manager端,由manager端来更新数据库的状态。同时为了避免数据库竞争,manager端使用了单点模式,同时内部使用互斥锁来同步对数据库的访问。实际上这里的方案不是最好的,比如server端和manager端是可以通过kafka来做到真正的解耦的,同时,manager端的互斥也可以使用分布式的锁,这样的话manager也可以是高可用的了,我这里为了赶进度,就用了最简单的方式来处理了。
3.详细设计
1. 数据库设计
数据库是manager和server端的交互协议层,比较重要的业务逻辑都可以通过数据库来表达。
CREATE TABLE `quick_rank_meta` (`id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT '主键',`src` varchar(64) NOT NULL DEFAULT '""' COMMENT '业务名称,唯一',`description` varchar(255) NOT NULL DEFAULT '""' COMMENT '描述',`person` varchar(100) NOT NULL DEFAULT '-' COMMENT '该向量负责人',`dimensions` int(11) NOT NULL DEFAULT '32' COMMENT 'vector 维度',`doc_count` int(255) NOT NULL DEFAULT '100000' COMMENT 'vector doc 数量',`local_path` varchar(255) NOT NULL DEFAULT '""' COMMENT 'local path',`hdfs_path` varchar(255) NOT NULL DEFAULT '""' COMMENT 'hdfs path',`build_start` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'index build time',`build_end` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'index recv time',`wait_for_load` int(11) NOT NULL DEFAULT '0' COMMENT '是否等待vector加载完毕,当前http接口是否会立即返回 0,不等待 1,等待',`load_success_num` int(11) NOT NULL DEFAULT '0' COMMENT '已经完成加载的服务器节点数量',`switch_success_num` int(11) NOT NULL DEFAULT '0' COMMENT '已经完成向量切换的服务器节点数量',`status` int(11) DEFAULT NULL COMMENT '-1 下线状态,0 初始化,1 待更新,2 更新中,3 更新完成,4切换向量中, 5正在提供服务',`crtime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 'create time',`uptime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'uptime',`work_after_load` int(11) NOT NULL DEFAULT '0' COMMENT '是否在向量加载完毕之后立刻生效,不再等待调用生效接口',`over_host` varchar(255) NOT NULL DEFAULT '-' COMMENT '处在当前status状态下的节点的hostname',PRIMARY KEY (`id`),UNIQUE KEY `uniq_src` (`src`)
) ENGINE=InnoDB AUTO_INCREMENT=12 DEFAULT CHARSET=utf8mb4 COMMENT='store quick rank meta info';
over_host 字段的内容详情
{"status_on_load_hosts":["quick-rank-online1","quick-rank-online2"],"status_over_load_hosts":["quick-rank-online2","quick-rank-online1"],"status_over_switch_hosts":["quick-rank-online1","quick-rank-online2"]
2.manager设计
1.manager接口
1. 注册接口
参数 | 类型 | 含义 | 样例 |
---|---|---|---|
src | string | 业务标识,需要保证唯一性 | xxxx |
description | string | src简介 | 推荐实验 |
person | string | 负责人 | chenc@aaa.com |
dimensions | int | 向量维度 | 64 |
2.上传向量接口
参数 | 类型 | 含义 | 样例 | 备注 |
---|---|---|---|---|
src | string | 注册接口使用的src | xxxx | |
hdfs | string | 对应的hdfs 路径 | /user/u_vector/show/20211007 | 每次更新想地址不能重复,否则可能不加载 |
wait_for_load | boolean | 是否等待向量加载完毕 | true|false | |
work_after_load | boolean | 是否在向量加载完毕后立刻投入使用 | true|false |
wait_for_load
- true: 为true的时候,该接口会持续阻塞,直到粗排系统将向量全部加载(但是不会立刻投入使用),这个过程耗时相对较长,一般500w左右的向量耗时大概在20分钟左右
- false:为false的时候,该接口会立刻返回,这个时候 work_after_load 自动设置为true,也就是在向量加载完成后会立刻投入使用
work_after_load
- true:为true的时候,向量在加载完成后会自动切换,使用新的向量
- false: 为false的时候,向量在加载完成后不会自动切换,需要使用方再调用一次下面的向量生效接口
这两个参数主要是在更新向量的时候使用,如果是第一次上线,则直接设置wait_for_load=false即可,向量更新完毕后在相关群里会有通知,业务方再择机上线对应的粗排接口调用即可
这个接口需要生成local_path, server端会对应的下载到这个地址,防止重复下载
3.向量生效接口
参数 | 类型 | 含义 | 样例 |
---|---|---|---|
src | string | 注册接口使用的src | profile_feed |
该接口修改status=4,等待server端切换向量
备注
向量生效接口并不一定必须要调用,取决于向量上传接口中的 wait_for_load
和work_after_load
的设置
wait_for_load | work_after_load | 是否需要再调用向量生效接口 | 备注 |
---|---|---|---|
true | true | NO | |
true | false | YES | |
false | true|false | NO | 因为调用方此时无法获取何时向量加载完毕的信息,所以work_after_load强制设置为true |
使用建议
- 在上传新的向量的时候,如果感觉影响比较小或者不会产生向量的一致性问题(模型未更新),可以直接设置wait_for_load=false
- 如果使用方模型更新比较快的话,可以设置wait_for_load=true&work_after_load=true 这样的话,不一致的时间窗口只是使用方的模型更新时间
- 如果使用方更新模型影响比较大,耗时比较长,可以设置wait_for_load=true&work_after_load=false,
- 使用方在调用上传接口返回后再更新自己的模型
- 模型更新完成后再调用粗排服务的向量生效接口
- 这样不一致的时间控制的会相对比较小,一般应该在分钟级别
4.server通知开始加载向量接口
这个接口是预留给server端开始加载向量的时候调用的,驱动manager通过数据库发出新的一轮指令,在接口收到请求的时候对应的记录的status=2, over_host 中添加server信息
参数 | 类型 | 含义 | 样例 | 备注 |
---|---|---|---|---|
src | string | 注册接口使用的src | xxxx | |
hdfs | string | 对应的hdfs 路径 | /user/u_vector/show/20211007 | 每次更新想地址不能重复,否则可能不加载 |
host | string | 节点的hostname |
5.server通知完成加载接口
这个接口是预留给server端加载向量完成的时候调用的,驱动manager通过数据库发出新的一轮指令,manager需要知道server的数量(可以做到配置文件中server_num),
- 收到请求后load_success_num++,
- 如果load_success_num==server_num, 那么说明加载都完成了,
- 数据库中的work_after_load=true的话则修改status=4, 驱动server端切换向量来使用
- 如果work_after_load=false的话,则修改status=3
- 如果load_success_num==server_num, 那么说明加载都完成了,
- 同时更新over_host中记录的信息
参数 | 类型 | 含义 | 样例 | 备注 |
---|---|---|---|---|
src | string | 注册接口使用的src | xxxx | |
hdfs | string | 对应的hdfs 路径 | /user/u_vector/show/20211007 | |
host | string | 节点的hostname | ||
doc_num | int | 向量的数量 |
6.server通知完成切换接口
这个接口是预留给server端向量生效使用的时候调用的,驱动manager通过数据库发出新的一轮指令
- 收到请求后switch_success_num++
- 如果switch_success_num==server_num则说明都切换完成了,status=5
参数 | 类型 | 含义 | 样例 | 备注 |
---|---|---|---|---|
src | string | 注册接口使用的src | xxxx | |
hdfs | string | 对应的hdfs 路径 | /user/u_vector/show/20211007 | 每次更新想地址不能重复,否则可能不加载 |
host | string | 节点的hostname |
7.向量详情接口
参数 | 类型 | 含义 | 样例 |
---|---|---|---|
src | string | 注册接口使用的src | xxx |
3. server端设计
1. 项目启动的加载
slelect所有status>0的记录,直接并行加载即可,中间不需要调用manager系统。
2. 定时任务
- 定时任务,每分钟执行一次,slelect所有status>0的记录
- 判断状态,执行行为
- 如果status=1||status=2,说明该向量文件需要被加载,
- 先判断内存中是否有正在加载该向量的任务,有的话忽略
- 需要增加一个互斥锁判断
- 互斥锁条件满足后(不存在互斥锁)再判断一下数据库中的数据over_host.status_on_load_hosts是否有自己,有的话也忽略
- 没有话执行加载(加载中,或者加载已经完成了)
- 加载之前通知manager
- 加载完成后调用manager告知加载完毕
- 先判断内存中是否有正在加载该向量的任务,有的话忽略
- 如果status=3|5,不做任何处理
- 如果status=4,则说明需要切换向量投入使用
- 判断是否正在切换该向量,正在切换,则忽略(切换中或者切换已经完成了)
- 需要增加一个互斥锁判断
- 互斥锁条件满足后再判断一下数据库中的数据over_host.status_over_switch_hosts是否有自己,有的话也忽略
- 没有切换的话
- 切换向量
- 通知manager
- 判断是否正在切换该向量,正在切换,则忽略(切换中或者切换已经完成了)
- 如果status=1||status=2,说明该向量文件需要被加载,
这篇关于一文详解粗排服务 向量计算引擎单机高可用模式设计的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!