一、背景
在移动互联网时代,用户体验是应用成功的关键因素之一。然而,即使是最精心设计的应用也难免会遇到各种问题和bug。这些问题不仅会影响用户的使用体验,还可能导致用户流失。因此,及时、高效地处理线上用户反馈显得尤为重要。
货拉拉移动端目前拥有一个完善的用户反馈处理系统,能够及时收到用户反馈,并分发给相应的值班产品经理、测试人员和开发人员进行处理。然而,该系统每天单端app收到约800条反馈,除了软件体验问题,还有大量各个模块的业务问题。目前,这些反馈依赖人工值班分发处理,反馈的处理时效大约在T+1到T+3天之间,这其中存在很大的提效空间。
研发测试同学每天焦头烂额忙着交付需求的同时,还要抽出大量时间排查用户反馈(大部分反馈并不是问题,但是必须排查确认,避免造成线上事故)。因此,如何快速地识别出其中的疑似bug,并且定位解决,就是我们希望通过打造智能排障体系来完成的课题。
二、产品方案
1. 提效思路
在设计这个系统之前,我们先回溯了所有的用户反馈,发现用户每天反馈的问题都高度集中在部分模块:例如下不了单、对支付有疑问、订单状态不对等,而研发在排查这些问题的时候,每一类问题的排查流程和思路基本一致,以下不了单为例,研发基本都是按以下步骤来分析日志:
当我们收到一条用户反馈,说“我这个号码怎么下不了单?怎么下不了单?”,研发一般会按以下步骤去分析排查:
- 分析app的离线日志,找到用户反馈时间前的下单请求,查看下单请求是否有报错,有报错就转后端跟进
- 下单接口没有报错,再去找计价接口的日志(下单前必须计价成功),查看计价请求是否有报错,有报错就转后端跟进
- 如果两个接口都没有报错记录,或者压根没有请求记录,就看用户是不是遇到闪退卡死问题了
- 如果也没有闪退卡死,那么就逐条分析用户行为日志、页面日志、网络日志,来模拟用户行为,尝试复现
所有下不了单的用户反馈,基本都是这样的排查步骤。而这些相似的排查步骤,我们是可以抽象、提取成为一个个的排障脚本,让脚本来自动分析日志,提取关键信息,从而释放研发人力、提高排障效率。
因此,我们对于「智能排障体系」的整体产品思路是:系统识别疑似bug ,自动进行排障流程,并给出排障结论。 这个思路提取出来两个关键词就是:智能化、自动化。
2. 产品设计
整个排障体系将会基于我们现有的 反馈系统、日志系统进行新功能开发。
先给大家看下最终我们的交付成果👇🏻
3. 排障示例
- 反馈系统:查看反馈详情时,页面上已经有了排障结论,无需研发测试再去排查
- 日志系统:管理排障脚本,支持手动使用排障脚本解析日志,一键排障
产品方案其实比较清晰,但如何实现智能化和自动化,让产品真正有用好用,技术实现上我们还是遇到了比较多决策点。
三、技术方案
我们想做的不仅仅是对bug的分析,我们还有更高的追求。具体我们的技术方案分为两步走。
1. AI模型识别疑似bug
1. 识别疑似bug
这部分主要有两个动作:
-
由业务方添加关键词词库,通常是业务核心服务,且用户反馈经常提到的一些词汇,比如冷藏车、拼车等等
-
训练疑似bug识别AI模型:
- 历史QAMP(内部bug管理系统)打标的疑似bug反馈数据作为模型的训练集
- 对识别数据进行正向反馈校准(人工对AI识别的疑似bug二次确认)自动加入训练集,模型在线自动训练迭代更新
2. 通过疑似bug发现新版本引入的新问题
能从用户反馈中识别出疑似bug还只是第一步,移动端的新版本覆盖有个缓慢的过程(用户很少主动更新app版本),假如新版本有线上问题,一般不会像服务端一样大面积爆发。
我们想要在版本覆盖量扩大之前,提前通过用户反馈识别到新版本新问题,因此迭代了AI模型2.0,在识别疑似bug的基础上,进行问题分类,高频新问题会及时告警。
在实现这个需求的过程中,我们尝试过使用GPT通用大模型进行语义提取,得到的结果是不稳定,有的太啰嗦并且不能准确按照要求给出语义,如下图示例:
其中的定位问题,不同的反馈描述,通用大模型会提取成定位问题、位置问题或者导航问题、甚至司机收藏问题,不好进行趋势聚合。
后来,我们又训练了微调私有垂直领域小参数大模型(0.4b),并且按以下流程进行大模型的自动迭代:
-
对发现的疑似bug数据进行语义提取分类
-
从语义提取分类中识别新问题和历史问题,分别预警
-
迭代大模型:
- 对识别不准确的数据进行在线标注校准。
- 迭代微调更新模型。
最后的聚合效果如下,比通用大模型的分类效果有很大提升。
(如果大家对这部分的技术细节有兴趣,可以在评论区留言,后面让相关同学再做个专题分享。)
2. 选择脚本语言
在做技术方案评审的时候,大家有一个争议点,就是脚本语言选哪个,用Python还是JavaScript。我们先来看下二者各方面的对比
其实从上手门槛和开发成本来看,Python和JavaScript之间差别不大,但考虑到我们项目的使用场景,有几个重要的诉求和特点:
-
脚本需要支持服务器端执行,同时也需要支持在浏览器环境中执行
-
多个脚本需要能够自动、并行执行
-
解析的数据主要为文本和JSON,提取关键信息并输出,不涉及复杂计算
-
解析的日志量用户端一般在一千条左右,司机端在一万条左右
对于一万条数据的文本和JSON,如果脚本逻辑相对简单,并且没有大量计算密集型操作,那么脚本的执行时间通常在几秒钟到几分钟之间,用Python和JavaScript差别不大。
综上对比和业务场景需求,虽然Python在复杂数据处理方面可能更为适合,但考虑到浏览器环境执行的要求,我们最终选择了使用JavaScript的方案
3. 选择脚本执行环境
这里其实选择很少了,我们的产品方案要求既要支持服务端解析,也要支持浏览器解析,并且我们的脚本语言是 JavaScript,那么脚本执行执行环境最佳选择就是 Node.js。
关键的技术选型问题解决后,接下来就给大家介绍下我们整体的技术方案。
4. 系统交互流程
整个排障体系涉及到两个系统和四个服务的交互:用户反馈系统、APP日志系统;以及feedback-server、octopus- server、node-server、log-server。
feedback 服务负责校验、存储反馈数据,关联离线日志任务;octopus服务消费反馈数据,触发node服务解析脚本;log服务组装日志包和脚本数据,从Kafka消费解析结果,更新任务状态。
node服务主要负责使用排障脚本解析离线日志,基于轻量级koa2框架实现,是整个体系中完全从0到1搭建的,也是我们开发工作量最重,调试过程中遇到问题最多的一部分,接下来会花比较多篇幅来介绍下。
Node服务的技术方案主要有以下几个部分:
-
使用Kafka来消费用户反馈的日志队列;
-
使用Node的进程池管理,来实现高并发处理大数据量的日志解析任务;
-
使用Axios网络插件,来实现数据请求/解析,支持解析脚本接收业务订单参数;
-
封装独立的日志解析模块负责脚本解析;
5. 技术挑战
在落地浏览器解析和Node服务端智能分析这两个能力的过程中,我们也遇到了许多技术挑战,但最终都找到了较为合适的解决方案。接下来,我们将分别从Web浏览器端和Node服务器端两个方面来说明我们所面临的技术难点。
1. Web浏览器日志分析
我们知道,浏览器Javascript是单线程执行,当JS执行高负载运算时,UI渲染就会阻塞,页面就会出现卡顿,用户体验较差(即使如setTimeout和setInterval也只是异步终究还是单线程,不能从根本解决问题),这里我们采用HTML5标准的Web Worker,它能支持一段JS程序运行在主线程之外的线程,与主线程互不干扰,当子线程代码执行完成后,将结果回调给主线程处理和渲染展示,就不会造成主线程阻塞以及页面卡顿。
在采用Web Worker后,我们可以观察到即使有大量的JavaScript解析任务在运行,浏览器页面仍然保持流畅,不再出现卡顿的情况。下面是我们的解析流程图:
2. Node服务端智能解析
公司有不同的业务线应用程序,比如用户端和司机端等。每个业务线每天都会收到来自用户的不同反馈。当我们接收到用户提交的问题反馈后,这条反馈会被发送到我们的Feedback系统中。
通过AI智能打标,一旦被标记为疑似bug,该反馈信息将被提交到Feedback系统中的Kafka消息队列中,等待Node服务器端的Kafka进行消费。当Node端完成分析后,会将分析结果提交到Feedback系统中。当相关人员打开该反馈时,问题的分析结果会自动显示在反馈问题页面中,从而大大提高了bug的分类和排查效率。
我们在Node端采用node-rdkafka消费Feedback系统的Kafka消息队列,但在消费消息队列的过程中,会遇到以下问题:
-
多条消息被同时消费
消息的处理顺序无法按照指定顺序进行,例如在分析某个反馈过程中(根据日志量的大小,分析所需的时间长短不一),可能会同时收到多条待处理的分析任务,导致混乱。
为解决这个问题,我们引入了心跳机制。在开始监听Kafka消息队列时,我们启动一个定时器,并增加一个是否正在解析的标记。如果正在处理任务,则停止Kafka消费。一旦当前分析任务完成,我们将该任务标记设置为false,就会继续消费新的消息任务。这样,通过心跳机制,我们能够更好地控制分析任务的顺序和并发处理。
接收Kafka新消息,将isWorking标记为true:
// 开始监听 consumer.on("data", (message) => { isWorking = true const msg = message.value.toString(); try { console.info("[kafka] consumer receive msg. ", msg); const {event} = JSON.parse(msg) if (event !== 'wait_analysis') { consumer.commit(message); isWorking = false return } processKafkaMessage(message) } catch (err) { consumer.commit(message); isWorking = false } });
增加心跳机制,当isWorking为true时,停止消费:
consumer.on("ready", () => { consumer.subscribe([KafkaConfig.OFFLINE_LOG_ANALYSIS.topic]); setInterval(() => { if (!isWorking) { if (AppConfig.app.isDEV()) { console.log('持续监听中...') } consumer.consume(1); } }, 1000); console.info("Consumer ready, start receive message."); });
解析完毕后,将isWorking标记为false,继续开始监听新的解析任务:
const processKafkaMessage = (message) => { try { handleKafkaMessage(message, async () => { console.info("######" + '该消息处理完成') consumer.commit(message); setTimeout(() => { isWorking = false }, 1000); }); }catch(err) { consumer.commit(message); } };
-
Node进行处理大量日志的解析任务时,卡死无法继续监听Kafka
我们了解到Node是单线程的,在处理异步IO方面非常高效。然而,如果我们将密集型分析任务直接在Node的主线程中处理,可能会导致主线程阻塞。
为了解决这个问题,我们采用了子进程来创建独立的子进程,并将接收到的解析任务分派给子进程来处理。这样,主线程可以专注于监听Kafka消息,类似于前端浏览器的方式。通过这种方式,我们成功解决了主线程阻塞的问题。
6. 排障脚本编写
基础能力都打通后,最后一步就是让各个app的研发根据各自业务的高频反馈编写排障脚本。为了尽可能降低大家写脚本的门槛,我们提供了脚手架、常用代码片段、预置全局参数代码,大家只需把排障逻辑写成js代码。
1. 本地编写调试脚本
- 配置本地环境
前端项目依赖的命令工具yarn,可先熟悉和安装yarn的安装和使用
- 安装项目
先将前端项目拉到本地,然后切换到脚本开发分支
// 拉取项目到本地
$ git clone [git url]
// 切换到脚本开发分支
$ git checkout -b feature/write_local_script origin/feature/write_local_script
- 启动项目
先安装项目的所有依赖
$ yarn install
然后启动项目即可
$ yarn start
-
编写脚本
-
-
/** * 本地编写脚本的方法 * @param singleLogData 当前日志对象 * @param callback 打印在页面的回调函数 * @param isLastLog 是否为最后一条消息 * @param isFirstLog 是否为第一条消息 * @param extraParams 用户自定义参数 **/ function devExcuteCustomJSScript(singleLogData = {}, callback, isLastLog = false, isFirstLog = false, extraParams = '') { // 单条log日志信息 var logInfo = singleLogData // 脚本需要输出的日志信息,此回调数据格式为JSON对象 var logCallBack = callback // 是否为第一条消息 var isFirst = isFirstLog // 是否为最后一条消息 var isLast = isLastLog // 用户自定义参数(字符串,根据输入方式进行解析) var userParams = extraParams // 请在此处编写脚本逻辑 // ............................................................................................................ // // 此处编写脚本代码 // // ............................................................................................................ }
-
2. 常用代码片段
我们提供了一些常用的脚本代码片段,研发同学可以直接复制使用。
3. 线上验证脚本
我们在日志系统的配置区提供了编写脚本的前端编辑器,大家本地调试好的脚本也可以用线上真实的日志数据进行再次验证,是否符合预期。
四、收益
- 处理效率
产品上线后,各业务线第一时间根据各自用户反馈的高频问题,编写了大概将近百个排障脚本,22w条日志解析时长在2分钟多,万条以内的日志基本10s内可以解析完成。
其中一个最优秀的排障脚本,帮Android司机端分析了 46条语音播报相关的反馈,并定位到8种原因,如果是研发一条条排查分析的话,至少需要1天时间,是智能排障体系非常优秀的提效案例。
- 处理数量
随着各端开发的排障脚本越来越多,脚本覆盖的疑似bug数也同步提升,目前排障脚本能解决的疑似bug覆盖率已达95%以上
这里的产品提单数,代表的就是人工判断是疑似bug,并提bug单给研发跟进的反馈。这个数据代表的是人工处理的反馈数,可以看到:
-
人工处理的反馈数占比极小,大部分用户反馈可能因为时间和人力原因,没有被关注到;
-
随着脚本覆盖的疑似bug数越来越多,人工处理的反馈数有在下降,因为值班产品在打开反馈详情的时候,页面上大部分已经有了排障结论,不需要再提单处理了
五、后续规划
产品上线后,在推广过程中,我们也收到了很多来自使用方的建议,包括AI模型对疑似bug识别的准确率、长尾问题的排障脚本覆盖、业务快速迭代脚本逻辑也要同步迭代带来的维护成本等等,这些也都是我们后续规划要持续打磨优化的方向。
六、总结
本篇文章重点介绍了打造智能排障体系的产品思路和技术选型,详细阐述了Node服务的设计和落地过程中遇到的问题。但其他部分没有过多涉及技术细节。如果大家对其他技术细节有兴趣或有疑问,可以在评论区留言,我们非常乐意与大家探讨和交流。