普通视图
前端日志回捞系统的性能优化实践|得物技术
得物灵犀搜索推荐词分发平台演进3.0
R8疑难杂症分析实战:外联优化设计缺陷引起的崩溃|得物技术
一、背景
R8作为谷歌官方的编译优化工具,在编译阶段会对字节码进行大规模修改,以追求包体优化和性能提升。但是Android应用开发者数量太过庞大,无论测试流程多么完善,终究难以避免在一些特定场景下出现问题。
近期我们在升级项目的AGP,遇到了一个指向系统SurfaceTexture类的native崩溃问题。经反编译分析发现问题最终指向了smali字节码中多余的一行new-instance指令。
该指令创建了一个SurfaceTexture对象,但是并未调用其方法,这意味着构造方法没有执行,但是这个类重写了finalize方法,后续被gc回收时会调用其中的nativeFinalize这个JNI方法,最终在native层执行析构函数时触发了SIGNALL 11的内存访问错误.
二、复现问题
我们注意到多出来的new-instance指令下面紧接着的是对a0.e 类中的静态方法 i() 的调用,其内部实现就是SurfaceTexture的构造方法。这是典型的代码外联操作,即一段相同的代码在工程中多次出现,则会被抽出来单独作为一个静态函数,原先的调用点则替换成该函数的调用,这样可以减小代码体积,是常见的编码思路。
例如:
class Activity{
void onCreate(){
// ...
String a = xx.xxx();
String b = xx.xxx();
Log.e("log",a+b);
//...
}
void onReusme(){
// ...
String a = xx.xxx();
String b = xx.xxx();
Log.e("log",a+b);
//...
}
}
class Activity{
void onCreate(){
// ...
Activity$Outline.log();
//...
}
void onReusme(){
// ...
Activity$Outline.log();
//...
}
}
//外联生成的类
class Activity$Outline{
public static void log(){
String a = xx.xxx();
String b = xx.xxx();
Log.e("log",a+b);
}
}
我们根据这个生成类的类名可以知道是R8中ApiModelOutline功能生成了这个类。
我们进到R8工程中检索下相关的关键字,再加上demo多次尝试,可以确认满足以下条件能够必现该问题:
- 使用了高于当前minSdkVersion的系统函数/变量(仅限系统类,自己写的无效)
- 用synchronized或者try语句块包裹了该调用,或者给该函数传参时有任何计算行为(除了传局部变量)。例如:
-
- new SurfaceTexture( getParmas() )
- new SurfaceTexture( if(enable) 1 : 2)
- new SurfaceTexture ( (boolean) enable )
三、问题分析
在确认复现条件之后,我们带着几个问题来逐个分析。
ApiModel外联是什么?
R8中的优化大多数跟包体优化有关,代码外联也是其中一种,但是外联的前提是代码重复的次数满足一定阈值,但是ApiModel会对所有调用了高版本系统API的代码做外联,包括只调用一次的场景。
ApiModel并非为了包体优化,我们通过R8工程的issueTracker (issuetracker.google.com/issues/3334… 检索到了相关的信息:
译:AGP新增的ApiModel功能是为了防止在低版本设备上不可能执行的代码引起类验证错误,从而降低App启动耗时。
从这篇介绍ART虚拟机类验证的文档 (chromium.googlesource.com/chromium/sr… 就能够理解上面这句话的含义:
ART虚拟机会在APK安装之后立刻执行 AOT class verification,即对dex文件中所有的类进行验证,如果验证成功则后续运行时将不需要再进行验证,反之若失败,则该class会被ART打上RetryVerificationAtRuntime的标记,后续运行时还得重新执行类验证。
同时这些失败的类也将无法被dex2oat优化成oat格式的优化字节码(oat字节码的加载和执行速度更快)。
如果是在MainActivity,启动任务中使用了这些高版本API,那么在低版本设备App启动时就必须额外执行一次类验证(比较耗时,有的类能到8ms issues.chromium.org/issues/4057…*
//安装apk后验证失败,运行时验证失败,但是能正常执行
class MainActivity{
void onCreate(){
if(android.sdk > 26){
new SurfaceTexture(false);
}
}
}
ApiModel后
class MainActivity{
void onCreate(){
if(android.sdk > 26){
a0.b(); //这样类验证就能成功
}
}
}
//生成的外联类,类验证会失败,但是运行时不可能走到,不影响
class a0{
public static void b(){
new SurfaceTexture(false);
}
}
更多关于ApiModel的详细介绍,见这篇文章:medium.com/androiddeve…
为什么会多生成一个
new-instance指令?
介绍完ApiModel之后,我们已经知道了为什么方法的调用被替换成了一个生成函数的调用,接下来我们再分析下导致崩溃的罪魁祸首 new-instance 指令是如何出现的。
我们先来了解下java文件在编译过程中的格式转换过程,因为ApiModel是基于IRCode格式(R8自定义的格式)来做外联。
文件转换
javac
javac将java文件编译成class文件
值得一提的是sychronized语句块在javac编译之后会为其内部代码生成try-catch,这是为了确保在语句块抛异常时能够正常释放锁,因此和问题有关的是try-catch语句块,和synchronized无关。
D8
目前R8已经整合D8,因此输入class文件之后就会先通过D8转为dex格式,并持有在内存中。
转换之后的指令基本和class字节码基本类似。
IRcode
为了做进一步的优化,会将dex格式的代码转化成R8自定义的IRcode格式,其特点是代码分块。
案例:
问题根因
在R8工程里检索ApiModel关键字,最终定位到针对构造函数生成外联函数和指令替换的代码:
InstanceInitializerOutliner->rewriteCode
执行此方法之前的指令如下:
java:
new SurfaceTexture(false);
dex:
: -1: NewInstance v1 <- android.graphics.SurfaceTexture
: -1: ConstNumber v2(0) <- 0 (INT)
: -1: Invoke-Direct v1, v2(0); method: void android.graphics.SurfaceTexture.<init>(boolean)
- 对整个方法中所有的指令从上往下进行遍历,第一次遍历主要是:
-
- 检索 方法调用的指令
- 判断该方法的androidApiLevel是否高于minSDK
- 生成包含完整构造函数指令的外联函数,并替换函数调用为外联函数调用。
- 执行完替换逻辑,就记录信息到map中,key是对应的new-instance指令,value是前一步中替换的新指令。
经过这一步,字节码会变成这样:
具体替换逻辑如下(可以参考注释理解):
- 第二次遍历则是对new-instance指令的处理:
-
- 找到new-instance指令
- 查询map,确认方法已完成替换
- 根据canSkipClInit方法返回的结果分为两种场景:
-
-
- 无类初始化逻辑:直接移除new-instance指令,不影响原代码的语义。
-
-
-
- 有类初始化逻辑:生成外联函数,只包含该new-instance指令,和前一次遍历一样进行指令替换。
-
具体替换逻辑:
- 问题重点就在于canSkipClInit这个函数的实现。
它会检查 new-intance指令和invoke 指令之间是否存在任何局部变量声明以外的指令,如果存在,他会认为这些指令是这个类初始化的逻辑,因此为了保留源代码的执行顺序,这种情况下就是需要额外执行一次new-instance指令来触发类初始化。
但是实际上,如果在调用这个构造函数传参时执行了任何运算(和类加载无关),都会生成相关的指令插在中间,例如:
java写法 | new-intance和invoke 指令之间的指令 |
---|---|
new SurfaceTexture( getParmas() ) | invoke-virtual v2 <-; method: void xx.xx.xx |
new SurfaceTexture( if(enable) 1 : 2) | StaticGet v3 <- ; field: boolean xxx.xxx.xx |
new SurfaceTexture ( (boolean) enable ) | : -1: CheckCast v5 <- v3; java.lang.Boolean: -1: Invoke-Virtual v6 <- v5; method: boolean java.lang.Boolean.booleanValue() |
从作者留下的todo也能看出,后续准备扩展这个方法,实现对这些夹在中间的指令的判断,如果是对类初始化无影响的入参计算逻辑,则也将正常移除new-intance指令。
值得一提的是,我们最终APK里 new-intance指令并没有被外联,这是因为SurfaceTexture这个类本身在安卓21之前的版本就已经存在,只是入参为bool类型的构造方法是在安卓26新增的,所以他其实是被外联之后又被内联回到了调用处,因此看起来像是没有被外联。
小结
至此,我们就明白了多出来一个看似无用的new-intance指令,实际上是为了保全源代码的语义,触发类加载用的,但是作者没有考虑到这些被优化的类可能重写了finalize方法来释放一些本就不存在的资源。
而且不局限于调用native函数,只要是重写了finalize,并在里面访问一些在构造函数中初始化的成员变量,一样可能造成NPE等崩溃。
R8是如何计算出API的版本?
R83.3版本开始,它编译时会下载一个.ser格式的数据库文件,里面记录了所有系统API、变量与安卓版本号的映射信息,在运行时通过行号和偏移量来寻找各自的版本号。
为什么try-catch
也会导致该问题?
前面解释了在构造函数入参中添加函数调用等写法导致的字节码异常原因,但是实际上这次我们遇到的崩溃场景是在sychronized里new了一个SurfaceTexture。
前文中已经解释过,sychronized在编译成class后会生成try-catch语句块,这段代码改成用try-catch语句块包裹,一样会复现崩溃,因此我们跟踪try-catch在文件转换过程中对字节码的影响即可。
回到class文件转dex文件的阶段,我们发现try语句块中的每一行指令,都会在其后生成一条FALLTHROUGH指令。
dex格式:
FALLTHROUGH是什么指令,他是做什么的?
FALLTHROUGH指令表示指令自然流转,没有实际含义,它主要是为了帮助优化器识别哪些指令是可达的。
例如下面这种写法,case1没有写break,这样会接着执行case2的代码:
switch (value) {
case 1:
System.out.println("One");
// 故意不写break
case 2:
System.out.println("Two");
break;
case 3:
System.out.println("Three");
break;
}
其字节码如下:
正常有break的话,会对应一条GOTO 指令跳转到switch语句块最后一行,但是没写break的话,就会出现:
在12行执行 goto 13 跳转到13行的指令,这种指令毫无意义,且运行时会消耗性能,因此可以替换成FALLTHROUGH指令,这样最终在生成dex文件时会被移除掉,从而避免浪费性能。
public static void switchWithFallthrough(int);
Code:
stack=2, locals=1, args_size=1
// 加载参数
0: iload_0
// 检查case 1
1: iconst_1
2: if_icmpne 13 // 如果不等于1,跳转到case 2
5: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
8: ldc #3 // String One
10: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: goto 13
// case 2 (fallthrough目标)
13: iconst_2
14: if_icmpne 28 // 如果不等于2,跳转到case 3
17: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
20: ldc #5 // String Two
22: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
25: goto 40 // 跳转到switch结束
// case 3
28: iconst_3
29: if_icmpne 40 // 如果不等于3,跳转到结束
32: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
35: ldc #6 // String Three
37: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
// switch结束
40: return
既然没用为什么还要加这个指令?
class文件是通过Exception table来指定异常处理的指令范围,而dex文件则是通过为每一行可能产生throwable的指令后面添加FALLTHROUGH指令来实现try-catch。
这里会把每一行可能崩溃的指令都链接到catch指令所在的block中,确保任意位置的崩溃都能正常走到catch中。
问题根因
在R8 4.0.26版本,IRCode翻译器新增了对FALLTHROUGH指令的处理,即新建一个block并生成一条GOTO指令指向新的block。
根据前文的结论,GOTO指令一样会被认为是类初始化相关的逻辑,因此try-catch语句块一样会导致最终多出来一个new-instance字节码。
为什么只升级AGP会导致
R8功能出问题?
我们在数个版本之前就已经单独升级了R8,正好涵盖了ApiModel这个变更,但是直到近期才升级了AGP。
可以看到从AGP7.3-beta版本开始,才默认打开ApiModel功能,这就解释了为什么升级AGP之后才出现此崩溃。
四、解决方案
禁用ApiModel
ApiModel通过牺牲些微包体,换来启动阶段类验证耗时,但是从他覆盖的类范围来看,对启动速度的收益微乎其微,因此可以直接通过配置开关关闭整个功能。
System.setProperty("com.android.tools.r8.disableApiModeling", "1")
虽说这是个实验中的功能,且逻辑相对独立,但是考虑到后续还有内联优化等操作,贸然关闭整个功能无法评估影响面,潜在的稳定性风险较高。
官方修复
该问题反馈给R8团队后,官方提供了临时规避的方案,即确保高版本API在单独的函数中调用。
issuetracker.google.com/issues/4411…
随后不久就提了MR针对SurfaceTexture这个类禁用了ApiModel,并未彻底解决此问题。r8-review.googlesource.com/c/r8/+/1090…
官方的修复方案比较权威,且影响面较小,但是并未彻底解决问题。
自行修复
如果要修复此问题,关键是要将多余的new-instance指令替换成一个合适的触发类加载的指令,根据java官方文档里的介绍,只有new对象,访问静态的成员变量或者函数的指令才能安全的触发类加载,比较理想的方案是改成访问静态变量,但是很多类并没有静态变量,比如SurfaceTexture就没有。
因此我们可以考虑结合getStatic指令和扫描finalize的方式来解决该问题:
虽说可以通过打印日志来约束此改动的影响面,但毕竟要自行修改并编译R8的jar包,且需要自行长期维护,整体影响面还是偏大,对稳定性要求高的App不建议采用该方案。
业务改造(推荐)
在前文中提到的外联函数生成处打印日志,即可感知到工程中有哪些类受ApiModel影响,如果数量不多,分别让业务改造其相关的写法,确保传参时是局部变量且无try-catch/synchronized语句块即可。
考虑到App整体的稳定性,最终我们采用了业务改造的方式绕过了此问题,并在R8异常代码处添加了日志告警来预防后续增量问题,并仿照官方MR中的写法补充了类的黑名单,用于应对无法编辑的三方库引入此问题的场景。
五、总结
在Android开发中,即使是AGP、R8这样的官方工具链升级,也要保持足够的警惕。毕竟Android生态太过复杂,再加上开发者们千奇百怪的代码写法,不论多么完善的测试流程都无法规避这类特定场景的bug。
这次的ApiModel外联优化问题就是一个很好的例子——它只在特定条件下才会暴露,但一旦出现就是必现的native崩溃。所以对于这种影响面无法评估的重大升级,还是需要经过足够长时间的独立灰度验证,才能合入主干分支。
往期回顾
1. 可扩展系统设计的黄金法则与Go语言实践|得物技术
2. 得物新商品审核链路建设分享
3. 营销会场预览直通车实践|得物技术
4. 基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术
5. AI质量专项报告自动分析生成|得物技术
文 / 永乐
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。
可扩展系统设计的黄金法则与Go语言实践|得物技术
得物新商品审核链路建设分享
一、 前言
得物近年来发展迅猛,平台商品类目覆盖越来越广,商品量级越来越大。而以往得物的上新动作更多依赖于传统方式,效率较低,无法满足现有的上新诉求。那么如何能实现更加快速的上新、更加高效的上新,就成为了一个至关重要的命题。
近两年AI大模型技术的发展,使得发布和审核逐渐向AI驱动的方式转变成为可能。因此,我们可以探索利用算法能力和大模型能力,结合业务自身规则,构建更加全面和精准的规则审核点,以实现更高效的工作流程,最终达到我们的目标。
本文围绕AI审核,介绍机审链路建设思想、规则审核点实现快速接入等核心逻辑。
二、如何实现高效审核
对于高效审核的理解,主要可以拆解成“高质量”、“高效率”。目前对于“高质量”的动作包括,基于不同的类目建设对应的机审规则、机审能力,再通过人工抽查、问题Case分析的方式,优化算法能力,逐步推进“高质量”的效果。
而“高效率”,核心又可以分成业务高效与技术高效。
业务高效
- 逐步通过机器审核能力优化审核流程,以解决资源不足导致上新审核时出现进展阻碍的问题。
- 通过建设机审配置业务,产品、业务可以直观的维护类目-机审规则-白名单配置,从而高效的调整机审策略。
技术高效
- 通过建设动态配置能力,实现快速接入新的机审规则、调整机审规则等,无需代码发布,即配即生效。
Q2在搭建了动态配置能力之后,算法相关的机审规则接入效率提升了70%左右。
三、动态配置实现思路
建设新版机审链路前的调研中,我们对于老机审链路的规则以及新机审规则进行了分析,发现算法类机审规则占比超过70%以上,而算法类的机审规则接入的流程比较固化,核心分成三步:
- 与算法同学沟通定义好接口协议
- 基于商品信息构建请求参数,通过HTTP请求算法提供的URL,从而获取到算法结果。
- 解析算法返回的结果,与自身商品信息结合,输出最终的机审结果。
而算法协议所需要的信息通常都可以从商品中获取到,因此通过引入“反射机制”、“HTTP泛化调用”、“规则引擎”等能力,实现算法规则通过JSON配置即可实现算法接入。
四、商品审核方式演进介绍
商品审核方式的演进
人审
依赖商管、运营,对商品上架各字段是否符合得物上新标准进行人工核查。
机审
对于部分明确的业务规则,比如白底图、图片清晰度、是否重复品、是否同质品等,机审做前置校验并输出机审结果,辅助人工审核,降低审核成本,提升审核效率。
AI审核
通过丰富算法能力、强化AI大模型能力、雷达技术等,建设越来越多的商品审核点,并推动召回率、准确率的提升,达标的审核点可通过自动驳回、自动修改等action接管商品审核,降低人工审核的占比,降低人工成本。
五、现状问题分析
产品层面
- 机审能力不足,部分字段没覆盖,部分规则不合理:
-
- 机审字段覆盖度待提升
- 机审规则采纳率不足
- 部分机审规则不合理
- 缺少产品配置化能力,配置黑盒化,需求迭代费力度较高:
-
- 规则配置黑盒
- 规则执行结果缺乏trace和透传
- 调整规则依赖开发和发布
- 缺少规则执行数据埋点
技术层面
- 系统可扩展性不足,研发效率低:
-
- 业务链路(AI发品、审核、预检等)不支持配置化和复用
- 规则节点不支持配置化和复用
六、流程介绍
搭建机审配置后台,可以通过配置应用场景+业务身份+商品维度配置来确定所需执行的全量规则,规则可复用。
其中应用场景代表业务场景,如商品上新审核、商家发品预检、AI发品预检等;业务身份则表示不同业务场景下不同方式,如常规渠道商品上新的业务场景下,AI发布、常规商品上新(商家后台、交易后台等)、FSPU同款发布品等。
当商品变更,通过Binlog日志触发机审,根据当前的应用场景+业务身份+商品信息,构建对应的机审执行链(ProcessChain)完成机审执行,不同的机审规则不通过支持不同的action,如自动修正、自动驳回、自动通过等。
链路执行流程图如下:
七、详细设计
整体架构图
业务实体
ER图
含义解释
※ 业务场景
触发机审的应用场景,如新品发布、商家新品预检等。
※ 业务身份
对于某个应用场景,进一步区分业务场景,如新品发布的场景下,又有AI发品、常规发品、FSPU同款发品等。
※ 业务规则
各行业线对于商品的审核规则,如校验图片是否是白底图、结构化标题中的类目需与商品类目一致、发售日期不能超过60天等。同一个业务规则可以因为业务线不同,配置不同的机审规则。
※ 规则组
对规则的分类,通常是商品字段模块的名称,一个规则组下可以有多个业务规则,如商品轮播图作为规则组,可以有校验图片是否白底图、校验图片是否清晰、校验模特姿势是否合规等。
※ 机审规则
对商品某个商品字段模块的识别并给出审核结果,数据依赖机审能力以及spu本身。
※ 机审能力
商品信息(一个或多个商品字段模块)的审核数据获取,通常需要调用外部接口,用于机审规则审核识别。
※ 业务&机审规则关联关系
描述业务规则和机审规则的关联关系,同一个业务规则可以根据不同业务线,给予不同的机审规则,如轮播图校验正背面,部分业务线要求校验全量轮播图,部分业务线只需要校验轮播图首图/规格首图。
机审执行流程框架
流程框架
通过责任链、策略模式等设计模式实现流程框架。
触发机审后会根据当前的业务场景、业务身份、商品信息等,获取到对应的业务身份执行链(不同业务身份绑定不同的执行节点,最终构建出来一个执行链)并启动机审流程执行。
由于机审规则中存在数据获取rt较长的情况,如部分依赖大模型的算法能力、雷达获取三方数据等,我们通过异步回调的方式解决这种场景,也因此衍生出了“异步结果更新机审触发”。
※ 完整机审触发
完整机审触发是指商品变更后,通过Binlog日志校验当前商品是否满足触发机审,命中的机审规则中如果依赖异步回调的能力,则会生成pendingId,并记录对应的机审结果为“pending”(其他规则不受该pending结果的影响),并监听对应的topic。
※ 异步结果更新机审触发
部分pending规则产出结果后发送消息到机审场景,通过pendingId以及对应的商品信息确认业务身份,获取异步结果更新责任链(与完整机审的责任链不同)再次执行机审执行责任链。
动态配置能力建设
调研
新机审链路建设不仅要支持机审规则复用,支持不同业务身份配置接入,还要支持新机审规则快速接入,降低开发投入的同时,还能快速响应业务的诉求。
经过分析,机审规则绝大部分下游为算法链路,并且算法的接入方式较为固化,即“构建请求参数” -> “发起请求” -> “结果解析”,并且数据模型通常较为简单。因此技术调研之后,通过HTTP泛化调用实现构建请求参数、发起请求,利用规则引擎(规则表达式) 实现结果解析。
规则引擎技术选型
调研市面上的几种常用规则引擎,基于历史使用经验、上手难度、文档阅读难度、性能等方面综合考虑,最终决定选用QLExpress。
HTTP泛化调用能力建设
※ 实现逻辑
- 定义MachineAuditAbilityEnum统一的动态配置枚举,并基于MachineAuditAbilityProcess实现其实现类。
- 统一入参为Map结构,通过反射机制、动态Function等方式,实现商品信息映射成算法请求参数;另外为了提升反射的效率,利用预编译缓存的方式,将字段转成MethodHandle,后续对同一个字段做反射时,可直接获取对应的MethodHandle,提升效率。
/**
* 缓存类字段的MethodHandle(Key: Class+FieldName, Value: MethodHandle)
*/
private static final Map<String, MethodHandle> FIELD_HANDLE_CACHE = new ConcurrentHashMap<>();
/**
* 根据配置从对象中提取字段值到Map
* @return 提取后的Map
*/
public Map<String, Object> fieldValueMapping(AutoMachineAlgoRequestConfig requestConfig, Object spuResDTO) {
AutoMachineAlgoRequestConfig.RequestMappingConfig requestMappingConfig = requestConfig.getRequestMappingConfig();
Map<String, Object> targetMap = Maps.newHashMap();
//1.简单映射关系,直接将obj里的信息映射到resultMap当中
//2.遍历复杂映射关系,value是基础类型
//3.遍历复杂映射关系,value是对象
return targetMap;
}
/**
* 预编译FieldMapping
*/
private List<AutoMachineAlgoRequestConfig.FieldMapping> compileConfig(List<AutoMachineAlgoRequestConfig.FieldMapping> fieldMappingList, Object obj) {
List<AutoMachineAlgoRequestConfig.FieldMapping> mappings = new ArrayList<>(fieldMappingList.size());
//缓存反射mapping
return mappings;
}
private Object getFieldValue(Object request, String fieldName) throws Throwable {
String cacheKey = request.getClass().getName() + "#" + fieldName;
MethodHandle handle = FIELD_HANDLE_CACHE.get(cacheKey);
return handle != null ? handle.invoke(request) : null;
}
- 基于实现@FeignClient注解,实现HTTP调用的执行器,其中@FeignClient中的URL表示域名,autoMachineAuditAlgo方法中的path表示具体的URL,requestBody是请求体,另外还包含headers,不同算法需要不同headers也可动态配置。
- 返回结果均为String,而后解析成Map<String,Object>用于规则解析。
@FeignClient(
name = "xxx",
url = "${}"
)
public interface GenericAlgoFeignClient {
@PostMapping(value = "/{path}")
String autoMachineAuditAlgo(
@PathVariable("path") String path,
@RequestBody Object requestBody,
@RequestHeader Map<String, String> headers
);
@GetMapping("/{path}")
String autoMachineAuditAlgoGet(
@PathVariable("path") String path,
@RequestParam Map<String, Object> queryParams,
@RequestHeader Map<String, String> headers
);
}
- 动态配置JSON。
{
"url": "/ai-check/demo1",
"requestMappingConfig": {
"fieldMappingList": [
{
"sourceFieldName": "categoryId",
"targetKey": "categoryId"
},
{
"sourceFieldName": "brandId",
"targetKey": "brandId"
}
],
"perItemMapping": {
"mappingFunctionCode": "firstAndFirstGroundPic",
"fieldMappingList": [
{
"sourceFieldName": "imgId",
"targetKey": "imgId"
},
{
"sourceFieldName": "imgUrl",
"targetKey": "imgUrl"
}
]
}
}
}
机审规则动态解析建设
※ 实现逻辑
- 定义MachineAuditRuleEnum统一的动态配置枚举,并基于MachineAuditRuleProcess实现其统一实现类。
- 搭建QLExpress规则引擎,为了提升QLExpress规则引擎的效率,同样引入了缓存机制,在机审规则配置表达式时,则触发loadRuleFromJson,将表达式转换成规则引擎并注入到缓存当中,真正机审流程执行时会直接从缓存里获取规则引擎并执行,效率上有很大提升。
// 规则引擎实例缓存
private static final Map<String, ExpressRunner> runnerCache = new ConcurrentHashMap<>();
// 规则配置缓存
private static final Map<String, GenericEngineRule> ruleConfigCache = new ConcurrentHashMap<>();
// 规则版本信息
private static final Map<String, Integer> ruleVersionCache = new ConcurrentHashMap<>();
/**
* 加载JSON规则配置
* @param jsonConfig 规则JSON配置
*/
public GenericEngineRule loadRuleFromJson(String ruleCode, String jsonConfig) {
//如果缓存里已经有并且是最新版本,则直接返回
if(machineAuditCache.isSameRuleConfigVersion(ruleCode) && machineAuditCache.getRuleConfigCache(ruleCode) != null) {
return machineAuditCache.getRuleConfigCache(ruleCode);
}
// 如果是可缓存的规则,预加载
return rule;
}
- 机审规则执行时,通过配置中的规则名称,获取对应的规则引擎进行执行。
/**
* 根据规则名称执行规则
* @param ruleCode 规则名称
* @param context 上下文数据
* @return 规则执行结果
*/
public MachineAuditRuleResult executeRuleByCode(String ruleCode, Map<String, Object> context, MachineAuditRuleProcessData ruleProcessData) {
if (StringUtils.isBlank(ruleCode)) {
throw new IllegalArgumentException("机审-通用协议-规则-规则名称不能为空");
}
//从缓存中获取规则引擎
//基于规则引擎执行condition
//统一日志
}
※ 配置demo
- 动态配置JSON。
{
"ruleCode": "demo1",
"name": "规则demo1",
"ruleType": 1,
"priority": 100,
"functions": [
],
"conditions": [
{
"expression": "result.code == null || result.code != 0",
"action": {
"type": "NO_RESULT",
"messageExpression": "'无结果'"
}
},
{
"expression": "result.data == 0",
"action": {
"type": "PASS",
"messageExpression": "'机审通过"
}
},
{
"expression": "result.data == 1",
"action": {
"type": "REJECT",
"messageExpression": "'异常结果1'",
"suggestType": 2,
"suggestKey": "imgId",
"preAuditSuggestKey": "imgUrl"
}
},
{
"expression": "result.data == 2",
"action": {
"type": "REJECT",
"messageExpression": "'异常结果2'",
"suggestType": 2,
"suggestKey": "imgId",
"preAuditSuggestKey": "imgUrl"
}
}
],
"defaultAction": {
"type": "PASS"
}
}
八、关于数据分析&指标提升
在经历了2-3个版本搭建完新机审链路 + 数据埋点之后,指标一直没有得到很好的提升,曾经一度只是维持在20%以内,甚至有部分时间降低到了10%以下;经过大量的数据分析之后,识别出了部分规则产品逻辑存在漏洞、算法存在误识别等情况,并较为有效的通过数据推动了产品优化逻辑、部分类目规则调整、算法迭代优化等,在一系列的动作做完之后,指标提升了50%+。
在持续了比较长的一段时间的50%+覆盖率之后,对数据进行了进一步的剖析,发现这50%+在那个时间点应该是到了瓶颈,原因是像“标题描述包含颜色相关字样”、“标题存在重复文案”以及部分轮播图规则,实际就是会存在不符合预期的情况,因此紧急与产品沟通,后续的非紧急需求停止,先考虑将这部分天然不符合预期的情况进行处理。
之后指标提升的动作主要围绕:
- 算法侧产出各算法能力的召回率、准确率,达标的算法由产品与业务拉齐,是否配置自动驳回的能力。
- 部分缺乏自动修改能力的机审规则,补充临时需求建设对应的能力。
经过产研业务各方的配合,以最快速度将这些动作进行落地,指标也得到了较大的提升。
往期回顾
1.营销会场预览直通车实践|得物技术
2.基于TinyMce富文本编辑器的客服自研知识库的技术探索和实践|得物技术
3.AI质量专项报告自动分析生成|得物技术
4.社区搜索离线回溯系统设计:架构、挑战与性能优化|得物技术
5.eBPF 助力 NAS 分钟级别 Pod 实例溯源|得物技术
文 / 沃克
关注得物技术,每周更新技术干货
要是觉得文章对你有帮助的话,欢迎评论转发点赞~
未经得物技术许可严禁转载,否则依法追究法律责任。