实时业务审计系统在得到App的实践应用

实时业务审计系统在得到App的实践应用--八里庄技术沙龙第3期实录 https://mp.weixin.qq.com/s/quw8c8ucC-IYWKtvWeLjEQ

 

实时业务审计系统在得到App的实践应用--八里庄技术沙龙第3期实录

图片

在4月12日,八里庄技术沙龙进行了第三期的分享活动。得到BCP业务审计平台负责人徐洪鑫跟大家分享了在做业务审计系统时的一些经验,希望能对大家有参考意义。


在业务审计系统的开发前后,非常感谢阿里技术团队的无私分享和帮助。

 

实录大纲:

 

1、需求缘起

2、非实时业务审计系统

3、实时业务审计系统

4、具体应用

5、总结

 

一、需求缘起

 

大概在2018年年中,公司需要进行大规模的财务对账,过程中,我们发现有很多数据在各个子系统中不一致的情况,比如订单状态和最终的交付不一致。为了了解原因,我们考古了大量的日志,发现子一些问题:系统间各种调用超时,没有触发报警;新代码上线触发了隐藏的bug;并发导致的bug等。后来我们调研了阿里的业务审计平台(BCPBusiness Check Platform),简单来说,业务审计是每当业务走完应有的流程之后,我们再次进行结果的校检。比如:订单状态是否正确的变更、虚拟物品是否写入已购、优惠券是否正确的消耗等。BCP关注结果是否正确,就像考试完,我们再检查一遍试题是否做对了一样。

 

我们想尽快的解决问题,但是由于实时审计开发流程较长,前期,我们决定先做一个实时性不高的审计平台,能帮助我们尽快的发现每天的异常。

 

二、非实时业务审计系统

 

图片

非实时业务审计我们决定以天为维度,定时统一拉取各个子系统的数据,进行校检。架构设计也很简单:

这其实更像是一个逻辑很重的计划任务,每天凌晨在业务低峰的时候运行。

 

非实时业务审计主要的数据源如上图所示,如果业务方没有合适的数据导出接口,我们一般会协商读取该业务方的只读从库实例。一般这个从库也不会在生产环境使用。

 

审计过程主要是两个过程:序列化和数据校检逻辑。虽然是凌晨运行,我们还是希望序列化和校检的过程不是太慢,所以我们在程序内部嵌入了LevelDB:良好的写入性能以及够用的读取性能。当然,LevelDB也更加适合于我们非实时的审计业务:

 

  1. K-V结构,离线审计主要以订单维度,够用

  2. 一个文件夹内存储以天为维度的DB文件,过期便于删除

 

我们还有一个后台用来处理异常数据,每天会产生一个简单的审计报表发送给相关的开发人员,后台也会展示审计的结果:

(非真实数据)

图片

 

非实时审计上线6个月以来校测了千万级别的数据,异常数据大概在0.0103%。粗略算一下,异常数据还是很多的。

 

问题主要集中在:

  1. 服务调用失败

  2. 业务方bug

 

具体表现为:

  1. 用户重复购买

  2. 权益未交付

  3. 入库数据异常

 

你可能会很奇怪:为什么会有重复购买这种情况,其实,产生这种原因是由于用户在第一次购买之后发现没有交付,然后又付钱购买了一次,实际上付了两次钱。造成了很不好的用户体验。

 

非实时业务审计帮助我们发现解决了很多问题,但是,他自身也有缺陷:

 

  1. 第二天才能知道当天的异常

  2. 审计逻辑与系统本身耦合,如果业务方变更,审计系统本身很可能也需要重新变更发版

 

在非实时可以帮助我们发现问题的同时,我们更希望能有更好的实时性以及审计逻辑与审计系统解耦。

 

三、实时业务审计系统

 

实时审计系统设计的目的是为了解决非实时的缺陷,也是我们数据审计这件事情最终形态,他将更加通用,更完备。足以成为真正的BCP。实时业务审计我们也将从新开发设计,旧的非实时系统会继续他的使命。

  1. 审计数据实时收集

首先数据源要从之前的定时统一收集改成实时收集:

图片

 

我们希望对业务方更少的入侵,甚至是零入侵,减轻业务方接入压力。如果业务方有统一的数据源消息队列,那么我们就可以直接订阅。但是,并不是所有的业务方都有。所以binlog是一个很不错的方式,但是,需要注意的是,如果使用的是云数据库,那么可能你拿不到binlog

 

除此之外我们还提供侵入性比较强的RPC接口,这需要业务方在数据生产的地方进行埋点。

 

      2.  数据延迟能力

实际上大部分情况,我们接收的数据都不能进行立即校检,例如支付回调数据源:你收到数据的同时,业务方也会同时收到,我们需要等在业务方处理完之后,我们再进行校检。数据延迟能力我们依赖于我们内部的一个服务叫做Taskcenter,简单讲,Taskcenter就是你告诉他一条数据和一个延迟时间,那么到期后Taskcenter会把数据回调你。当然Taskcenter不是我们今天的主题,不过还是要说一下,Taskcenter的核心是时间轮算法。

 

      3. 审计逻辑规则化

 

正如前面所说,审计逻辑和审计系统耦合,会造成很多麻烦。这部分的逻辑实际上就是一条审计规则,我们希望他可以动态拆卸,变更。

 

       4.   报警机制

 

出现了异常,我们要将异常推送给开发者,当然,我们的后台也要规范的异常的处理流程。

 

至此,整个BCP系统的架构已经出来了:

图片

 

其中调度器负责数据源的收集,以及给需要延迟的数据赋予能力。在数据初步处理完成之后,将会推送给BCP的工作集群,实际上,工作集群就是规则的运行时环境,数据在经过规则之后,将校检结果推送给BCP后台,报警这些杂活都是由后台来处理。

 

单个工作节点示意图:

图片

 

接下来我们要重点讲讲BCP系统的一个难点,就是规则的实现。

 

首先我们明确规则需要哪些能力:

  1. 可动态的加载、卸载、更新

  2. 规则有一个统一的runtime,可以为规则的开发减少工作

  3. 较好的性能

  4. 规则的编写语言功能完备,库齐全,便于我们在runtime能力不足的时候也能完整规则的开发

 

我们先后调研了很多技术:

 

  1. Lua

Lua应用比较广泛,NginxRedis中都有使用。但是它有一个缺点就是Lua更适合短小的逻辑,对于规则来说,逻辑还是比较重的。这并不是Lua擅长的领域,而且,我们需要为Luaruntime实现很多库,这才能让Lua足以完成规则的逻辑。

 

2.   PHP

你可能会很疑惑,PHP怎么实现规则?实际上我们可以部署PHP CGI服务器,通过fastcgi协议和它通讯就好了,当然,规则使用PHP编写,我们需要做一套自动化规则部署方案,就是把代码最终放在PHP CGI服务器上就可以了,这实际上是一个不错的想法,作为备选方案。

 

3.   JavaGroovy

Java自身就可以动态记载外部的java代码,且能更好的和脚本语言结合,比如Groovy。当然Java也有统一的调用其他脚本语言的功能。总归来说,Java为我们提供了很强大的功能足以我们实现规则这个功能。

 

4.  goloader

一个很有意思的项目,使得Go语言也能动态加载外部代码的功能,且加载的外部代码也是Go语言二进制文件,执行效率极好。 

 

5.   goby

goby是一个使用Go编写的VM的脚本语言,可以和Go良好的结合。gobygo的关系与JavaGroovy关系有些类似,不过也不太适合做逻辑复杂的规则,更加适合做轻量级的热插拔代码块。

 

6. Go plugin

Goplugin机制是Go官方提供的方案,基于动态链接库,只支持LinuxmacOS系统。执行性能极好,但是不支持卸载。

 

综合考虑,我们还是决定使用我们最熟悉的Go plugin来实现。公司内部有很多Go的服务,属于公司主流的技术栈,且有良好的性能。当然Java也是一个不错的的实现工具。

 

实现思路:

  1. 由于Go plugin机制不支持卸载,在BCP这种规则可能频繁变更的场景中,会造成内存泄漏。所以我们使用了多进程的方案,在子进程中加载动态链接库,这样卸载规则直接kill子进程即可。

  2. 既然使用了多进程方案,我们不希望进程间通讯(IPC)有过大的开销,所以需要实现一个高效的IPC方案。

  3. 多进程还有一个好处,子进程可以更好的隔离,假如外部逻辑逻辑有严重的bug导致挂掉,也只会对子进程造成影响,这时主进程的还是安全的,不会造成这个工作节点出现宕机的情况。

 

多进程方案简单示意图:

图片

接下来我们看一下规则的具体编码形式(Go语言缩减部分语法):

type ruleConf struct {}var Conf ruleConfvar ErrHandle func(errInfo)func init() {}func Init() {}func RuleHandle(data) {}func Shutdown() {}

 

规则的生命周期主要分为初始化,主审计函数被BCP系统调用,以及最终的关闭函数,如下图所示:

图片

 

简单说一下规则如何与runtime提供的能力交互,正如伪代码所示,在代码中,你只需要定义一个变量即可,BCP在拉起规则的时候,会把相应的资源注册给你(初始化过程):例如ErrHandle这个报警函数,我们只需要按照函数原型以及命名定义到这里就可以了,BCP会自动把实现注册给你,规则中我们不需要关注函数的具体实现。

 

规则的编译:

实际上规则编译非常简单只需要使用Go提供的命令即可:

 

go build -buildmode=plugin

 

但是本地编译能在服务端跑的plugin需要满足两个条件:

  1. 本地需要进行交叉编译,编译服务器目标平台的plugin

  2. Go只能加载相同版本编译出来的plugin

 

解决这个问题,我们可以做一个统一的编译工具,编译工具可以帮助开发者完成这些兼容,工具的核心原理也很简单,拉取一个和服务端相同go版本的go docker镜像即可。

 

IPC方案:

 

我们希望高效的IPC方案,所以我们前期否定掉了常用的socket,毕竟socket需要内核态到用户态,用户态到内核态的数据拷贝,如果使用了网络协议比如TCP,还需要经过内核的网络协议处理,这些都是们不希望的性能消耗。

 

所以我们选用了共享内存(mmap)作为数据载体,POSIX信号量作为同步工具的通讯方案:

图片



IPC过程如下:

 

  1. A向共享区域写入数据,写入信号量+1

  2. A等待读取信号量,B被唤醒

  3. B读取数据,读取信号量+1

  4. A被唤醒,继续以上流程

 

这里要提一下POSIX信号量:

1. POSIX信号量总的来讲是用户态的CAS操作加上系统调用FUTEX实现,不是频繁争抢的过程中可以节约系统调用的开销。

2. macOSPOSIX信号量由内核实现,在Go中我们可以这样调用:syscall.Syscall(273),其中273sem_post的系统调用号。而Linux中,则由glibc实现,所以只能通过cgo调用:C.sem_post

3. 如果想要节约cgo的开销,则可能需要使用Go语言基于CAS+FUTEX实现POSIX信号量的功能,这个如果没有研究过内核同步机制的,则会一头雾水。这一部分技术难度较大,我们没有做优化,当然我们也不想过于技术思维,现在也还是足够好用的,未来再考虑优化这部分。

 

下面我们来看一下一个工作节点的细节:

图片

 

规则实现的最后,分享几点设计经验:

  1. 贴近公司技术栈,我们希望谁都能快速的开开发规则。

  2. 评估一下性能开销影响,很多时候,前期不需要过强的性能,够用就好,我们这里也犯了这样一个错误,当然带来的性能提升也很难有用武之地。

  3. 评估规则runtime交互的实现开发量,我们方案使用Go官方的plugin方式,runtime交互实现异常简单。

  4. 所以尽量使用官方提供的方案吧。

  5. 编码之前还是思考一下需要的能力,提前做好编码规划。例如我们思考了规则逻辑繁重,我们更希望编码规则自身的代码就是一个功能完备的语言,在前期runtime能力不足的时候,规则也能自给自足。

 

四、应用

 

  1. 购买业务线审计

 

拿一个我们现在在跑的规则,说明一个应用场景:

图片

 

该场景是我们Apple内购场景的审计,流程大致是:

  1. Apple内购凭证数据实时推送到BCPkafka

  2. 数据经过调度器被延迟6

  3. 数据被推送到工作集群,相应的规则完成以上图所示的几点校检

 

实现以上这个流程很简单,作为开发者,只需要编写一个规则,然后在后台填写上数据源消息队列的相关信息。规则的配置信息(如果需要),。

 

2.   广义的BCP系统

 

实际上BCP系统可以抽象为数据源加上一个可替换的数据处理器,我们可以在数据处理器中做很多事情:异常数据重试,数据预警,比如某商品库存不足这些非数据异常类的问题。

 

五、总结

 

最初我们想尽快解决问题,开发了非实时业务审计系统到后来我们做通用的实时审计平台。可以说是从一开始的业务主导技术实现到最后的技术引导业务问题的解决。实时性和通用性的特性可以帮助我们更加高效的做好业务审计,及时发现业务问题,完善用户体验。不过目前实时审计系统还处于很初级的阶段,我们还要为这个生态做很多事情,比如更规范的消息源,相关工具链的开发。

 

最后如果你有更好的想法或者对分享有疑问都可以联系我:xuhongxin@luojilab.com

 

posted @ 2022-11-21 15:30  papering  阅读(106)  评论(0编辑  收藏  举报