前言
算法脚本动态部署方案
后记
在推荐搜索体系中,一次推荐/搜索通常分为:召回、粗排、精排几个环节。
召回:一般根据用户部分特征、搜索词,从海量的物品里,快速找出一部分用户潜在感兴趣的物品,然后交给排序环节,排序环节可以融入较多特征,使用复杂模型,来精准地做个性化推荐。
粗排:由于每次召回环节召回的物品数量太多,排序环节性能跟不上,所以在召回和精排之间加入一个粗排环节,通过少量用户和物品特征,简单算法模型,来对召回的结果进行粗略排序,在保证一定精准的前提下,进一步减少往后传送的物品数量。
精排:使用更丰富全面的用户特征,更加复杂的算法模型,进一步精准地对物品进行个性化排序。
在玩物得志的推荐搜索体系中,精排引擎作为单独的应用存在,向推荐搜索服务提供提供多种排序策略,包括实时性、多样性、新颖性、惊喜度等,以及优化用户体验,提升推荐/搜索带来的整体收益。
精排引擎(排序算法策略引擎):
算法同学在迭代算法策略时,需要进行策略编码,功能性测试,以及算法效果评测,为了保证脚本性能和稳定性往往还会进行一次压测。在所有步骤完成之后,才可以上线部署。随着业务的快速发展,算法策略开始应用到各自场景中,对于算法迭代频率也随之提高,整个上线周期就成了整个算法迭代的效能瓶颈,针对一些实际效果不好的策略往往还需要回滚。对于工程来讲,在频繁的算法迭代就意味着频繁的发布,重启应用,这对于一个核心的线上应用是非常不友好的。
并提供一套完整的上线平台工具:
代码质量检测,每次算法变更,都可以在平台上进行脚本基础代码检测,能检测出一部分基础性问题,实现算法和工程同学的解耦,以减少工程同学的review时间成本。
动态上传脚本,实现脚本快速加载部署,快速进行功能性测试和算法评测。
自动化性能压测,回归采集线上真实流量请求参数进行模拟压测,保证算法脚本高效稳定。
动态部署,动态加载策略脚本到线上容器,提供脚本快速新老版本替换和历史版本回滚等功能,避免应用的频繁发布抖动。
下文将讲述精排引擎中作为核心功能 ”脚本动态部署“的具体实现方案。
主要环节:
1)代脚本编写
2)脚本发布
3)容器加载
例如在算法排序策略中
public interface Ranker {
List<Long> rank(List<Long> itemIds);
}
工程同学提供算法排序中使用的工具包和sdk,以便不精通Java语言的算法同学进行高效安全的算法代码编写。
对应算法同学和工程同学会再次review代码,检查算法策略正确性和代码安全性(是否存在不安全操作,死循环等...)
让其他人编写脚本,并且在你的应用上运行是一件非常危险的事情,因此保证脚本的安全性,是重中之重。
代码质控检测
精排引擎提供脚本部署平台,算法同学可在平台上脚本上传进行代码质量检测,并在平台显示对应错误信息。代码质量检测包含
基本编译测试:检测语法和依赖是否存在,以保证代码编译通过
代码规范检测:根据正则进行源代码检测,查看是否实现ranker接口,是否存在static变量等(为保证线程安全性,每一个算法ranker都是一个对象,不允许使用静态变量)。
类依赖检测:通过javassist 库检测算法脚本是否使用规定允许使用范围的类,例如:禁止使用Runtime 调用服务器系统命令,禁止使用File 来操作服务器上的文件等。
上传脚本
算法脚本首先会存放在算法的脚本代码库中,为了方便算法容器进行加载,我们会将脚本上传到中间媒介中(精排引擎中使用的是阿里云的oss)
在使用脚本动态部署技术时,就意味着跳过了公司内部发布平台,做不到监控、灰度、回滚三板斧,容易引发线上故障,存在一定的安全隐患,为此我们使用oss这种能存储文件历史版本的存储介质进行脚本存储,实现脚本快速回滚的功能。
发起加载
在脚本上传完毕后,会发起一个脚本加载通知,广播通知应用集群进行脚本的拉取和加载,这里我们采用的是redis的pub/sub模式( pub/sub-发布订阅,是一种消息通信模式:发送者 (pub) 发送消息,订阅者 (sub) 接收消息。当有新消息通过 PUBLISH 命令发送给频道 channel1 时, 消息就会被发送给订阅它的三个客户端)
容器加载
接收通知&脚本拉取
容器集群在接收到订阅的消息后,会到oss中拉取对应名称和版本的脚本源文件到应用本地,进行重新编译校验,避免文件可能遭到中间修改,不规范上传或跳过检测环节导致的代码不规范等安全问题。
加载脚本
在脚本加载到容器中,通过基于系统编译器 JavaCompiler自定义实现的ScriptCompiler编译出对应的二进制数据,通过自定义的ScriptClassLoader从二进制数据中加载出对应的class,再通过反射等相关技术生成具体的对象供线上调用。
对于如何保证所有容器都顺利加载了算法脚本的问题,我们在容器加载脚本后,依然会发送一个redis的订阅消息,脚本发布平台会收到来自算法集群中的加载状态,对于一些加载异常或没有回复的机器会发起尝试再次加载消息。
算法脚本容器重启后,会去主动读取配置中历史正常加载并且还在线上标记使用的算法脚本。
脚本动态部署化必须考虑垃圾回收的问题,否则随着 Class 被加载的越来越多,系统的内存很快就不够用了。在 JVM 中,对象实例在没有被引用后会被 GC,Class 作为 JVM 中一个特殊的对象,也会被 GC,需要清空方法区中 Class 的信息和堆区中的 java.lang.Class 对象。这时 Class 的生命周期就结束了。
Class 要被回收,需要满足以下三个条件:
NoInstance:该类所有的实例都已经被 GC。 NoClassLoader:加载该类的 ClassLoader 实例已经被 GC。 NoReference:该类的 java.lang.Class 没有被引用 (XXX.class,使用了静态变量/方法)。
从上面推出,JVM 自带的类加载器(Bootstrap 类加载器、Extension 类加载器)所加载的类,在JVM 的生命周期中始终不会被 GC。自定义的类加载器所加载的 Class 是可以被 GC 的,因此自定义的Classloader 一定做成局部变量,让其自然被回收。
在一定程度下,仍然可能在代码中包含死循环,或者执行时间特别长的问题,对于这种有问题的逻辑在检测中可能检测不到,针对这种情况在调用算法脚本时,会使用单独的线程来执行,当出现超时或者内存占用过大的情况会直接 kill。
牛年邀牛人
一起战斗、一起成长
技术、产品、UED、运营、职能等海量岗位
玩物得志期待你的加入