一文详解粗排服务 向量计算引擎单机高可用模式设计

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。

  1. 程序在启动的时候扫描这个表,并行的去加载数据向量。向量加载完成后放到内存中的map,然后对外提供服务。
  2. 程序在运行中,可以监控这个表,不断的看看是否有新的向量想要加入,或者是否有更新任务,然后加载向量
    1. 这里的向量加载完毕后理论上不能立刻投入使用,还是要看业务方的信号,给业务方一个接口,让业务方选择合适的时间切换向量
  3. 因为需要操作数据库,这样看来程序需要一个操作数据库的manager端,同时为了简化向量计算服务的逻辑,server短所有的指令都通过检查数据库获取,而不是从外部调用获取,简化了server端的处理逻辑。
  4. 同样的,因为是存储服务,所以对稳定性要求比较重要,服务中的报警通知,异常管理要比较清晰,严重的异常能够通知到位,同时要能够查看各个服务节点的业务向量信息,像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. 注册接口
参数类型含义样例
srcstring业务标识,需要保证唯一性xxxx
descriptionstringsrc简介推荐实验
personstring负责人chenc@aaa.com
dimensionsint向量维度64
2.上传向量接口
参数类型含义样例备注
srcstring注册接口使用的srcxxxx
hdfsstring对应的hdfs 路径/user/u_vector/show/20211007每次更新想地址不能重复,否则可能不加载
wait_for_loadboolean是否等待向量加载完毕true|false
work_after_loadboolean是否在向量加载完毕后立刻投入使用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.向量生效接口
参数类型含义样例
srcstring注册接口使用的srcprofile_feed

该接口修改status=4,等待server端切换向量

备注

  向量生效接口并不一定必须要调用,取决于向量上传接口中的 wait_for_loadwork_after_load的设置

wait_for_loadwork_after_load是否需要再调用向量生效接口备注
truetrueNO
truefalseYES
falsetrue|falseNO因为调用方此时无法获取何时向量加载完毕的信息,所以work_after_load强制设置为true

使用建议

  1. 在上传新的向量的时候,如果感觉影响比较小或者不会产生向量的一致性问题(模型未更新),可以直接设置wait_for_load=false
  2. 如果使用方模型更新比较快的话,可以设置wait_for_load=true&work_after_load=true 这样的话,不一致的时间窗口只是使用方的模型更新时间
  3. 如果使用方更新模型影响比较大,耗时比较长,可以设置wait_for_load=true&work_after_load=false,
    1. 使用方在调用上传接口返回后再更新自己的模型
    2. 模型更新完成后再调用粗排服务的向量生效接口
    3. 这样不一致的时间控制的会相对比较小,一般应该在分钟级别
4.server通知开始加载向量接口

这个接口是预留给server端开始加载向量的时候调用的,驱动manager通过数据库发出新的一轮指令,在接口收到请求的时候对应的记录的status=2, over_host 中添加server信息

参数类型含义样例备注
srcstring注册接口使用的srcxxxx
hdfsstring对应的hdfs 路径/user/u_vector/show/20211007每次更新想地址不能重复,否则可能不加载
hoststring节点的hostname
5.server通知完成加载接口

这个接口是预留给server端加载向量完成的时候调用的,驱动manager通过数据库发出新的一轮指令,manager需要知道server的数量(可以做到配置文件中server_num),

  1. 收到请求后load_success_num++,
    1. 如果load_success_num==server_num, 那么说明加载都完成了,
      1. 数据库中的work_after_load=true的话则修改status=4, 驱动server端切换向量来使用
      2. 如果work_after_load=false的话,则修改status=3
  2. 同时更新over_host中记录的信息
参数类型含义样例备注
srcstring注册接口使用的srcxxxx
hdfsstring对应的hdfs 路径/user/u_vector/show/20211007
hoststring节点的hostname
doc_numint向量的数量
6.server通知完成切换接口

这个接口是预留给server端向量生效使用的时候调用的,驱动manager通过数据库发出新的一轮指令

  1. 收到请求后switch_success_num++
    1. 如果switch_success_num==server_num则说明都切换完成了,status=5
参数类型含义样例备注
srcstring注册接口使用的srcxxxx
hdfsstring对应的hdfs 路径/user/u_vector/show/20211007每次更新想地址不能重复,否则可能不加载
hoststring节点的hostname
7.向量详情接口
参数类型含义样例
srcstring注册接口使用的srcxxx

3. server端设计

1. 项目启动的加载

slelect所有status>0的记录,直接并行加载即可,中间不需要调用manager系统。

2. 定时任务
  1. 定时任务,每分钟执行一次,slelect所有status>0的记录
  2. 判断状态,执行行为
    1. 如果status=1||status=2,说明该向量文件需要被加载,
      1. 先判断内存中是否有正在加载该向量的任务,有的话忽略
        1. 需要增加一个互斥锁判断
        2. 互斥锁条件满足后(不存在互斥锁)再判断一下数据库中的数据over_host.status_on_load_hosts是否有自己,有的话也忽略
      2. 没有话执行加载(加载中,或者加载已经完成了)
        1. 加载之前通知manager
        2. 加载完成后调用manager告知加载完毕
    2. 如果status=3|5,不做任何处理
    3. 如果status=4,则说明需要切换向量投入使用
      1. 判断是否正在切换该向量,正在切换,则忽略(切换中或者切换已经完成了)
        1. 需要增加一个互斥锁判断
        2. 互斥锁条件满足后再判断一下数据库中的数据over_host.status_over_switch_hosts是否有自己,有的话也忽略
      2. 没有切换的话
        1. 切换向量
        2. 通知manager
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值