乐趣区

关于后端:Elasticsearch-整合机器学习强化排序

本文介绍如何将机器学习预测能力迁徙至 es 外部,加强排序能力, 构建一个高性能、分布式搜排一体零碎,并通过落地更多简单模型特色和更深的计算,为业务带来新的增长点,咱们将 LR-> 树模型实现全量排序,给外围业务带来 1.2% 的 ab 增长。

背景介绍

咱们团队次要负责哈啰四轮司乘匹配的召回排序,在逆风车司乘匹配场景中,司机发单后零碎会从订单池中筛选展现适合的乘客订单,促成司机发单到完单,带来营收。整个过程排序是一个十分要害的环节,目前咱们底层的排序架构是用了经典粗排 - 精排重排级联排序。

在粗排阶段,咱们应用 LR(Logistic Regression)算法对数千个订单进行排序。而在精排阶段,咱们筛选出前 300 名订单,并应用 rankservice 实现深度排序。在重排阶段,咱们再选取 10 个订单进入业务相干重排。

咱们团队在精排阶段将模型由树模型升级成深度模型,并获得了不错的业务成果,同时也积淀了肯定的技术。因而咱们开始思考将粗排也进行精排化,即采取更加简单的模型和更多的特色。参考行业教训以及业务场景的特点,如路线匹配路线和算法离线评估,咱们决定将粗排从 LR 降级为成果更好的树模型。

在达成这个指标的同时,也会解决历史中存在的技术问题。之前的树模型受限于技术,单机只能反对 300 排序 -> 降级到 es 外部实现全排序。LR 的迭代全副手写代码 -> 编程配置化,减速迭代,减少稳定性。

整体计划

机器学习在线预测流程

须要在程序中获取一批特色 inputs 传入模型,返回模型预测分数 output。在这个过程中,依据起源分成以下几种类型:

  • 实时特色
  • 上下文特色  
  • 离线特色    
  • 组合特色

每种类型特色都须要对应一套技术解决方案。

具体计划

咱们将整个机器学习以插件的模式嵌入 es 外部,其中蕴含多个重要的组件。

1. 特色获取计划

  • 实时特色
    司机的实时特色由 flink 计算后上游查问从接口传入,订单的实时特色由 flink 写入索引。
  • 上下文特色
    司机的实时特色由调用传参带入。
  • 离线特色
    由 kkv 零碎实现离线特色的加载、查问、更新,能够反对到分钟级别。
  • 组合特色
    咱们外部设计了一套 DSL,通过配置即可实现特色生成,蕴含了组合特色。

2. 重要组件简介

  • kkv:
    在线获取离线特色。
  • 热加载:
    es 脚本插件须要重启整个集群能力实现更新,咱们在它的底层接口进行了一层形象,借助热加载的能力实现对业务插件的更新。
  • 执行引擎:
    执行引擎次要用来对模型的加载、预测,底层反对多种算法模型预测。
  • 文件散发零碎:
    次要用于将文件更新到整个集群,触发业务回调,算法同学在训练平台玩实现 kv 训练,模型训练以及配置文件设置,会通过文件散发零碎实时同步到整个 es 集群,实现更新,触发业务回调。
  • 特色生成:
    特色配置生成零碎,通过自定义 DSL 获取全副的模型入参特色。
  • DEBUG:
    用于疾速验证在 es 外部机器学习模型预测的准确性。

要害组件

执行引擎

执行引擎次要用于对模型的加载预测。

算法训练常见的模型有树模型、深度模型,还有局部自定义。不同的模型框架,算法可能须要工程做独自的适配,咱们冀望有一种通用的执行引擎能够解决掉简单的适配问题,既能够反对在 sparkml 的模型,也能够反对深度学习。咱们抉择了 mleap,它是一种通用的执行引擎,算法同学应用 sparkML 实现模型的训练,应用 mleap 进行序列化,生成对立的模型,在线上应用 mleap 的 api 进行加载预测。

kkv 零碎

次要用于离线特色的加载和查问,咱们会面对一些海量离线特色存储查问的挑战。

挑战:

  • 检索 rt 要求高
    举个例子,假如本次检索命中了 1000 个订单,模型有 100 个离线特色,单位工夫 kv 检索达到 10w 次。近程 io 无奈反对,所以做成特色本地化。
  • 特色量大
    咱们的特色曾经达到百亿级别,传统的加载无奈反对,这块咱们的解决方案是应用 mmap 内存映射技术,读取二进制实时反序列化,这个解决方案 es 的 docvalue 底层是一样的。

咱们调研了一些常见 mmap 解决实现, ohc mapdb rocksdb paldb , 发现 paldb 在性能、存储和索引速度都是最快的。

线上数据:

(数据均来自精排的深度学习)

hive 表 50G -> 索引文件 20G -> 映射内存 10G

查问速率:获取 390 订单,100 纬度离线 100 组合 50 上下文的特色数据,耗时 5.6ms。

(总 rt 18.5  tf 预测: 12.5  特色: 6)

文件散发零碎

dragonfly 次要用于更新文件触发业务回调。

咱们有配置文件 jar 模型 kv 文件,须要被散发到 es 外部,触发回调。咱们针对共性的需要开发了一个散发零碎。

逻辑很简略,文件通过 dragonlfy 上传到存储系统中(OSS,HDFS 等),批改 meta,consumer 监听近程文件,发现 meta 变更主动下载文件 -> 校验 md5-> 触发回调用。

性能:

  • 文件变更主动下载最新文件,触发业务回调;
  • 极速 MD5 校验本地记录 MD5;
  • 易用性,反对注解驱动;
  • 反对灰度加载,可与任意配置核心整合 apollo,自定义配置;
  • 更新回调状态,用于监控;
  • 反对多环境,采纳虚拟环境;
  • 反对多回调等等。

以下为 rankservice 与 spring 整合的截图。

 // 文件监听
  @Dragonfly(storagePath = "model/lo_cc_deep_model_v1.tar.gz")
    public void getDeepFMModel(File file, String path) throws IOException {super.getDeepFMModel(file,path);
    }
  // 多回调
  @Dragonfly(storagePath = "model/lo_cc_deep_model_v1.tar.gz")
    public void test(File file, String path) throws IOException {System.out.println("单体测试 3");
    }
  // 目录监听
  @Dragonfly(dirPathMonitor = "model/deep",filterBean = ApolloFilter.class)
    public void getDeepFMModel(File file, String path) throws IOException {super.getDeepFMModel(file, path);
    }

热加载

不须要重启整个集群即可实现插件更新的性能。

es 启动的时候就会进行热加载插件的加载,通过 dragonfly 监听 / 回调业务.jar,装载实现进入插件库,es query 中指定相干的实现即可实现对业务执行。

jar 外面蕴含了多种类型插件实现:

  • filter 实现:eta 过滤,夹角过滤,沿途间隔过滤等
  • sort 实现:排序有顺路度,mleap 排序(树模型排序,tf 排序)
  • script_field 实现:字段插件有顺路度

插件开发 tips:

  • 是否存在内部资源
    须要手动敞开
  • 是否存在第三方 jar,存在内存透露
    热加载常见问题内存透露,能够通过压测来发现
  • 提前加载预热,避免突刺
    对 class 提前初始化,存在资源加载的状况
    模型提前预热
  • 分层热加载
    轻资源 class 的加载和卸载
    重资源独立,不参加热加载,比方 kv 热加载会导致之前的 kv pagecache 淘汰,从新 reload,会耗费系统资源
  • 谬误日志限度输入
    es 文档计算是 row by row,有多少文档输入多少次日志,重大耗费零碎 cpu,导致服务不稳固
    限度一次申请只能输入一个谬误日志

配置化的迭代

咱们冀望特色迭代配置化,算法工程同学不写一行代码。

难点:特色组合(特色穿插),特色解决的逻辑变幻无穷,须要设计一套计划来解决特色灵便变换的问题。

介绍:如何获取一个组合特色

如图所示, 原始 user 特色 + 原始 item 特色,通过特色组合或者穿插,生成模型特色。

咱们看下之前的解决方案:每个特色都须要手写代码,一个模型有几百个特色,十分麻烦,并且容易出错。咱们的解决方案:应用自定义算子 + 原始特色,利用反射来实现特色生成。

咱们外部自研了一套 dsl,通过配置就实现模型任意类型特色的生成。(实时 kv 上下文 组合)

咱们会将验证精确的特色配置录入到特色表中,算法同学在录入模型特色的时候主动出提醒,保障录入的准确性与效率,如果是模型微调 v2 版本,间接复制 v1 批改几个特色即可。整个算法特色被治理起来,保障同组应用的唯一性。

DEBUG

在 ES 外部疾速实现机器学习 debug。

解决方案:
ES 外部有一个 API
org.elasticsearch.script.ScoreScript#execute(ExplanationHolderexplanation explanation)
咱们将计算每个 item 预测过程进行对外输入。

比方以下例子:蕴含了策略名称,申请入参数,模型入参数

explanation.set(String.format("mleap(policyName=%s,params=%s,detail=%s)", this.sortPolicy, params,tempMap));

如图所示:能够对匹配到的订单进行 debug

能够取得:原始 user 特色,原始 item 特色,模型入参特色详细信息,这样就能够对模型的精确进行校验。

稳定性

欠缺压测计划

每次新上线模型前除了惯例的 fat、uat 环境测试,上了 pre 环境会进行压测回归 & 新性能验证。

针对存在的危险点进行极限压测

波及变更点:业务插件,模型,特色;

要求:新老插件交替,模型特色重置,保障不呈现抖动,在任意变更点保障服务都是稳固的;

计划:天级别变更为分钟级别变更;

后果:通过一周的压测,服务整体稳固,没有呈现内存泄露,根本判断计划成了。

灰度变更

稳定性三板斧:可灰度,可监控,可回滚。因为新上模型,须要做变更灰度。咱们借助 dragonfly 程序加载 & 灰度来实现。

dragonfly 优先监听加载灰度文件,针对灰度文件配置加载其余文件。

如上图所示:

  • 模型 1:容许 2 台机器加载
  • 模型 2:只容许节点为 ES1 进行加载
  • jar:容许两台机器加载

机器学习分组加载

各个业务的模型不一样,须要的特色也不一样,咱们冀望针对每一个模型进行分组加载,而不是让每台机器全量加载。这里咱们借助的是 es 的分组个性和 index 的模板。

咱们在创立 index 的时候会间接绑定这个 index 在哪些机器上生成,如 index1 在 group1 上,index2 在 group2 上,index3 在 group3 上,通过文件散发零碎来实现对整个机器学习的分组加载。同时,咱们也能够通过对每个业务模型特色,进行分组加载来缩小不必要的开销。例如,某些业务可能只须要局部特色进行训练,kkv 零碎反对按需进行索引局部字段提供线上查问,这样就能够缩小特色的维度,从而升高了计算成本,晋升了零碎性能。

模型预测减速

咱们从三个维度进行机器学习的性能减速。首先是申请缓存,第一个是 user 特色的缓存,计算一次后可重复使用。第二个是对象复用,因为 es 的计算是 row by row 的计算,咱们计算完一个 row 后,它的模型入参 inputs 能够持续复用,下一个计算开始的时候间接对 item 维度特色批改即可。

其次是模型缓存,即模型预测减速。咱们固定了输出 / 输入的 schema,并预调配足够的内存空间用于存储所有的后果数据,从而防止了多余的计算和内存空间动态分配的开销。同时,咱们也采纳了模型入参缩小 key 的输出优化形式,进一步缩短了计算工夫。

接下来是全局缓存,咱们通过 mmap 内存映射技术做整个 kv 的减速,还有一些做特色减速。咱们会有一些高频的特色,比方某个特色的均值、方差、最大值、最小值等,具备量小、高频拜访个性,所以咱们能够把它长驻堆内。

上线后业务上的体现

1. 反对 spark 全副的模型;
2. 模型迭代,免开发,通过特色配置化、热加载、压测、灰度能够疾速稳固上线;
3. 算法插件组件化、可插拔、灵便编排和反对多轮排序;

这里是一个常见的例子,es 召回实现之后,间接进行级联排序, 模型 B 进行 score,模型 C 进行 rescore。其次是灵便编排,咱们整个模型库可能有 ABCDEF 的模型,假如在第一阶段有 10000 个订单,咱们应用模型 ABC 同时进行排序,排序后组合取 Top1000 进行模型 D 排序,排序后取 300 个进行模型 E 排序,整个过程非常灵活。

4. 热加载,特色模型 jar 实时更新,无抖动;
5. 火焰图,单核心场景,排序只占到 7% 的 cpu 耗费;
6. 在单机单分片场景 1500 深度下,  树模型相比 LR 多了 10ms,全场景 LR  -> 树模型,逆风车外围 ab 减少 1.2%。

后续动作

短期指标:后续咱们打算会补齐 es 的排序短板,反对深度学习。

目前次要的问题在于 es 的计算是 row by row 的,没有方法应用 TF 的 batch 计算,每一次计算 TF 都要开启 session,这是十分耗资源的。这块将来会通过 es rescore 插件来解决。

同时咱们会去整合 openvino,目前在业内有很多机器学习框架,如 Tensorflow、飞桨、Pytorch、Caffe 等,算法又有这方面的诉求,须要工程同学去做每一个框架适配,咱们在思考有没有对立的解决方案,能够将支流的 DL 框架收拢起来,应用一个 API 就能实现预测。咱们发现能够应用 openvino 来解决这个问题。目前曾经在做相干的技术调研,预计下半年能够上到咱们的 rankservice 中,稳固半年以上就会开始迁徙到 ES。

咱们最终冀望实现的指标是将所有排序都在 es 外部实现,能够达到一排到底,做到极致的性能及高度的灵便。

问题:排序零碎为什不独自做?

咱们抉择将排序零碎与搜寻零碎联合在一起,次要是为了在无限的资源内兼顾性能和业务成果。

这样做的劣势包含:

  • 性能弱小:咱们将搜排零碎交融在一起,使得排序零碎具备极强的性能。
  • 开发、保护和运维成本低:与独自构建排序零碎相比,咱们不须要关注数据品质、存储、分片、查问 mapreduce 等问题,因为搜寻零碎曾经提供了这些性能,因而咱们所需关注的就是排序办法,这使得咱们的排序零碎更加灵便并易于保护,相当于了实现 Serverless 运行。
  • Lucene 的欠缺反对:咱们的排序零碎应用 Lucene 作为根底,并利用其丰盛的性能(例如 function_score、score、rescore 和 debug),这使得咱们能够进行灵便编排和反对多轮排序的操作。
  • 稳固 & 效率:咱们的排序零碎应用特色配置化的形式来进行开发,通过标准化的上线流程、压测、灰度和分组等一系列措施来保障其稳定性,这能够使得咱们的零碎更加易于治理和调试。

综上所述,将排序零碎和搜寻零碎联合在一起,能够实现性能优良、保护成本低、功能丰富、操作灵便和稳定性高的排序零碎,这正是咱们所须要的。

(本文作者:彭晟)

退出移动版