性能优化:线程资源回收

本文来自: PerfMa技术社区

PerfMa(笨马网络)官网java

1、问题

模型服务平台的排序请求出现较多超时状况,且不定时伴随空指针异常。web

2、问题发生先后的改动

召回引擎扩大了召回量,致使排序请求的item数量增长了。服务器

3、出问题的模型

基于XGBoost预测的全排序模型。网络

4、项目介绍

web-rec-model:模型服务平台。用于管理排序模型:XGBoost、TensorFlow、pmml....召回模型:item2item,key2item,vec2item....等模型的上下线、测试模型一致性、模型服务等。多线程

5、一次排序请求流程

一、以下图所示,一次排序请求流程包含:特征获取、向量获取、数据处理及预测。以上提到的三个步骤均采用多线程并行处理,均以子任务形式执行。每一个阶段中间夹杂这数据处理的流程,由主线程进行处理,且每一个阶段的执行任务均为超时返回,主线程等待子线程任务时,也采用超时等待的策略。(同事实现的一个树形任务执行,超时等待的线程框架)并发

二、特征数据闭环:该步骤为异步执行,将排序计算使用到的特征及分数,模型版本等信息记录。后续做为模型的训练样本,达到特征闭环。
模型请求流程.png框架

三、一次排序请求中,特征获取及向量获取为网络IO(IO密集型任务),超时可直接响应中断,线程可快速返回。数据处理及模型为计算步骤(CPU密集型任务)。异步

四、当前请求耗时状况:特征与向量的获取阶段耗时均为5-8ms,数据处理及模型预测阶段耗时平均在10ms左右。测试

6、问题发生现象

一、首先是调用方:推荐策略平台,监控报警排序请求的超时数量变多(调用方超时时间为300ms),且从监控上看发现排序服务的耗时明显变长:50ms+。正常高峰期的指望值为50ms如下。spa

二、其次排序服务告警出现大量超时错误。
超时错误.png

三、第三根据错误信息定位到该错误信息来自于数据处理及模型预测阶段。

四、除了超时变多之外,服务中会出现偶发性的空指针异常。

7、问题排查

一、首先解决空指针这类低级错误。
空指针错误.png

二、根据错误提示找到对应的代码,此处就不粘贴代码了,作一个简单的代码解释。代码逻辑为:从Map<String,Object>中根据特征key获取特征值进行计算。

三、疑惑点出现,首先该Map<String,Object>用于存放特征及向量键值对,且key均作了空值计算兼容。特征或者向量在查询到空值时,会在Map<String,Object>中放入一个对应的默认值。通过反复的代码确认,报错信息对应的代码不可能出现漏放默认值的状况。

四、借助Arthas的watch命令,监控空指针异常的入参。方便后面作模拟请求还原现场。

五、根据报错时的信息进行模拟请求。尝试N次,且使用不一样的报错数据进行尝试,均未重现事故。

六、此时怀疑是多线程并发进行数据处理及预测时,发生对Map<String,Object>进行修改的动做,致使部分键值对丢失。

七、反复检查代码,肯定数据处理及预测均为只读动做,不会对Map<String,Object>进行任何键值对的删改。

八、线索中断,排查一度搁置。

8、豁然开朗

一、借用Arthas进行报错观察:使用watch命令,依靠-e参数(指定报错触发打印)以及-x n 参数(打印方法入参及返回值数据层数)

二、根据观察,发现Map<String,Object>中丢失的均为向量键值对。

三、找到问题:在排序请求流程图中,在主线程进行分数归一化时,会fork子线程异步作特征数据进行压缩写入kafka。因为Map<String,Object>中存在大量的向量数据,致使保存数据过冗余的状况。此处的作法是先去除全部的向量数据,再进行保存。

四、可是该动做是发生在数据处理及模型预测后的,为什么还会由于Map<String,Object>中删除键值对致使空指针异常呢。

五、此时怀疑是数据处理及模型预测阶段,多线程任务还没完成时,主线程已经等候超时返回了。

9、验证想法

一、仍是观察超时日志。
超时请求.png

空指针数据.png

二、发现请求已经返回后,才出现空指针异常。那基本就能够验证以上的想法了。

10、问题解决

一、翻看使用的多线程框架(同事实现),主线程超时等待子线程任务。主线程超时返回后,没有通知子线程任务取消。因此才发生请求已返回,特征数据异步落地后,偶发性出现晚到的空指针异常的状况。以下图,主线程超时返回后,只取消主线程任务。
主线程超时等待.png

二、解决思路:主线程超时返回后,中断子任务(取消子任务)。因为java的中断机制为软中断,通常是经过中断标志位进行线程中断协做的。固然IO或者sleep的中断由系统帮咱们作了中断能够快速返回。对于CPU密集型的任务,是须要使用者在合适的计算点上作标志位判断,肯定是否已中断结束任务。以这种协做的方式达到中断。(此处可能有部分理解不当)

三、修改多线程框架,在主线程超时返回后,修改子线程中断标志位。
取消子线程.png

四、在计算流程中加入线程中断检查,若是被中断则提早结束计算。
响应中断.png

11、效果检查

一、修改发版后,空指针没再出现。(其实该空指针是不影响排序结果,由于结果已是错的,该异常只是附带的虫子而已)
二、超时请求减小,高峰期的超时数据减小三分之一,50ms+的排序请求有明显减小。

12、复盘

一、主线程等待子任务的场景下,若是主线程超时返回了。需通知子线程结束执行的任务。首先,主线程返回了,表示子任务已被丢弃。继续执行都是在作无用的计算,占用计算机资源。也不是说占着茅坑不拉屎,而是拉了没人要。应该尽可能减小服务器资源用在不必的消耗上。

二、该服务在数据处理及预测阶段使用的线程池队列为SynchronousQueue,若是不了解SynchronousQueue的话能够简单理解为一个0长度的队列。任务进池子时必需要有线程进行对接。与常规的BlockingQueue不一样的是,任务在池子中不会堆积,对于任务的快速响应比较友好。可是也由于若是没有空闲的线程,则会不停建立线程直到最高线程数限制而触发丢弃策略。在该项目问题中,因为部分子任务在主线程返回后仍然在执行。新的请求进来后,会出现没有空闲线程的状况,致使池子建立新线程接任务。对于CPU密集型任务来讲,过多的线程数对服务来讲是另外一种负担,毕竟线程切换的代价仍是比较大的。这就套入死循环了。(我的理解,如表述有误,还望指正)

欢迎关注 PerfMa 社区,推荐阅读

内存问题探微

JAVA线上故障排查套路

相关文章
相关标签/搜索