IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 大数据 -> 记一次线上接口慢查询问题排查 -> 正文阅读

[大数据]记一次线上接口慢查询问题排查

目录

问题描述

解决方案

1 消息中间件

2 代码及数据库优化

3?ConcurrentLinkedQueue方案

定时任务

异步处理

业务流程

其他

参考文献


问题描述

? ? ? ?有一个分类预测的接口,主要业务逻辑是输入一段文本,接口内部调用模型对文本进行分类预测。 模型数据是直接在内存中,所以预测的过程本身很快。预测完成之后,往预测记录表插入一条数据。后续有其他应用会对该记录进行矫正,判断是否预测成功,以便后续进行自学习。

? ? ? ?接口上线初期好评如潮,不管是做智能派遣还是经办单位流转预测使用效果都很好,随着自学习功能加入,预期应用应该会越跑越好。但是前两天客户现场突然反馈系统很慢,做智能派遣和流转的时候要三四秒甚至五六秒才能出结果。

? ? ? ?最开始想的是现场的分类模型太多,模型树也比较深,所以一次多级预测不可避免会比较耗时,但是核查后发现最多的模型树只有四级,单个模型直接验证预测结果都是毫秒出结果,但是直接调用接口就很慢。 如果不是算法模型的问题,也不是机器资源的问题,那一定是代码逻辑中有没有考虑到的点,而且随着数据量激增,问题越来越明显。

? ? ? ?经过简单排查,发现问题非常低级,造成慢的主要原因是因为预测记录表的数据已经将近100w了,而且这个表没有任何索引,只有一个自增主键,程序中,因为业务逻辑需要,每次调用预测接口都会有一个查询再更新或者插入的动作,并且是同步的。那么随着数据量的激增,接口的RT注定会越来越慢。

解决方案

? ? ? ?问题是个小问题,主要原因还在于接口设计的初期没有做深入的考虑,代码写的很漂亮,只可惜不经考验,绣花枕头一个。对于这种问题,最直观的方案就是解耦,预测和预测记录入库做成异步的。 当然如果有业务场景硬要求不得不做成同步,也可以从代码层面和数据库层面做优化增加处理速度。

1 消息中间件

(1)可以直接引入kafka或者RocketMQ进行解耦,这种比较保险,数据不会丢,而且可以做到核心功能和非核心功能解耦。是主流解决方案,并且支持横向扩展和分布式。

(2)如果不想使用MQ,也可以直接使用Redis,Redis也可以实现消息队列的功能,参考这里

2 代码及数据库优化

? ? ? ?如果不得不同步操作,那就要从代码层面进行优化和调整,主要以下几个思路:

(1)给查询的字段添加索引,通过数据库的能力提升IO

(2)设置分区表或者天表(月表)的概念,降低单表的数据量

(3)代码优化,将查询后的更新或者插入合并为一个sql操作,参考这里

insertOrUpdate的实现是基于mysql的on duplicate key update 来实现的。

使用ON DUPLICATE KEY UPDATE,如果行作为新行插入,则每行受影响的行值为1。如果更新现有行,则每行受影响的行值为2;如果将现有行设置为其当前值,则每行受影响的行值为0(可以通过配置,使其受影响的行值为1)。

官方地址:

13.2.6.2 INSERT … ON DUPLICATE KEY UPDATE Statement

? ? ? ?对于异步解耦这种方案,如果之前应用本身没有MQ、Redis,为了解决该问题安装这些中间件,从某种程度上来说这无疑增加了系统复杂度好运维集成的工作量。基于此,我们也可以使用java自身的多线程来实现异步,基本思路就是在需要进行IO操作的地方,直接开启一个新线程去处理。

? ? ? ?但是如果请求量很大,这无疑会造成频繁的线程创建、释放资源问题,如果引入线程池,又可能会出现因为资源用完阻塞的情况,这不能根本上解决同步的问题。?

? ? ? ?可以通过java多线程模拟消息队列,在需要进行IO操作的业务代码处,将业务数据封装为Bo放到一个队列中。有一个独立的线程对队列进行消费处理即可。可以参考这里

? ? ? ?我主要是使用了ConcurrentLinkedQueue来解决了此问题,基本思路是,创建了一个定时任务,每30秒执行一次,每次都去处理ConcurrentLinkedQueue队列中的数据,将数据入库。 而业务代码中是将业务数据封装成Bo放到队列中去了。

3?ConcurrentLinkedQueue方案

定时任务

@Component
public class TaskJob
{
    private static final Logger logger = LoggerFactory.getLogger(TaskJob.class);

    @Scheduled(cron = "*/10 * * * * ?") //每10秒执行一次,异步处理预测结果信息,入库
    public void execuPredictResult() throws Exception
    {
        SyncSavePredictResultService syncSavePredictResultService = new SyncSavePredictResultService();
        syncSavePredictResultService.consumeData();
    }
}

异步处理

public class SyncSavePredictResultService
{
    private static final Logger logger = LoggerFactory.getLogger(SyncSavePredictResultService.class);

    //预测记录的消息队列,异步处理入库操作
    public static final ConcurrentLinkedQueue<PredictResultBo> RESULT_BO = new ConcurrentLinkedQueue<>();

    public void consumeData()
    {
        PredictResultBo resultBo = RESULT_BO.poll();
        while (resultBo != null)
        {
            //进行业务逻辑处理
            excuSubPdResult(resultBo.getContent(),
                    resultBo.getBegin(),
                    resultBo.getEnd(),
                    resultBo.getmId(),
                    resultBo.getFlagValue(),
                    resultBo.getPredictResult(),
                    resultBo.getRootPid(),
                    resultBo.getD());
            logger.info("异步写入预测记录,对象内容为:{}", resultBo.toString());
            //更新resultBo
            resultBo = RESULT_BO.poll();
        }
    }

    private void excuSubPdResult(String pstr, long begin, long end, Integer mId, String flagValue, String result, Integer rootPid, Date d)
    {
        NlpSubPredictResults nResult = NlpSubPredictResults.GetInstance().findFirst("select * from nlp_sub_predict_results where flag_value=?", flagValue);
        if (nResult != null)
        {
            nResult.set("predict_result", result)
                   .set("start_time", begin)
                   .set("end_time", end)
                   .set("content", pstr)
                   .set("updated_at", d)
                   .update();
        }
        else
        {
            nResult = NlpSubPredictResults.GetInstance();
            nResult.set("flag_value", flagValue)
                   .set("root_p_id", rootPid)
                   .set("m_id", mId)
                   .set("predict_result", result)
                   .set("start_time", begin)
                   .set("end_time", end)
                   .set("content", pstr)
                   .set("created_at", d)
                   .set("updated_at", d)
                   .save();
        }
    }
}

业务流程

private void excuSubPdResult(String pstr,long begin,long end,Integer mId,String flagValue,String typeId,String typeName,Integer rootPid){
        Map<String, String> rMap = new HashMap<>();
        rMap.put("name",typeName);
        rMap.put("id", typeId);
        String result = JSON.toJSONString(rMap);


        Date                 d       = new Date();
        PredictResultBo predictResultBo = new PredictResultBo(mId,rootPid,flagValue,result,begin,end,pstr,d);
        SyncSavePredictResultService.RESULT_BO.add(predictResultBo);

        /*
        //2022-05-30改为异步
        NlpSubPredictResults nResult = NlpSubPredictResults.GetInstance().findFirst("select * from nlp_sub_predict_results where m_id=? and flag_value=? and root_p_id=?",mId,flagValue,rootPid);
        if(nResult!=null){
            nResult.set("predict_result", result)
                   .set("start_time",begin)
                   .set("end_time",end)
                   .set("content",pstr)
                   .set("updated_at",d)
                   .update();
        }else{
            nResult = NlpSubPredictResults.GetInstance();
            nResult.set("flag_value",flagValue)
                   .set("root_p_id",rootPid)
                   .set("m_id",mId)
                   .set("predict_result", result)
                   .set("start_time",begin)
                   .set("end_time",end)
                   .set("content",pstr)
                   .set("created_at",d)
                   .set("updated_at",d)
                   .save();
        }
         */
    }

其他

? ? ? ?如上的改造方式实际上是有隐患和弊端的

(1)如果接口调用量很大,难免会有消息积压,这时候如果节点挂了,那数据就丢失了。

(2)消息如果有大量积压,有可能撑爆内存,这样是会影响应用正常使用,也会造成数据丢失。

(3)无法支持多个消费程序。

(4)消费入库的逻辑实际上和核心的预测功能没关系,但是如果因为消费数据多,势必会影响核心预测功能的使用。这从软件架构上来说是不合理的。

? ? ? ?上述这些问题通过MQ或者Redis都能很好的解决。?

? ? ? ?但是,但是任何事情没有绝对的,很多时候需要因地制宜,需要根据实际的业务要求、数据要求、项目紧急情况、成本预算等等,考虑到底使用哪种解决方案

? ? ? ?比如:通过单节点多线程ConcurrentLinkedQueue的方式就能解决99%的问题,所需要的成本预算是1,开发周期1天,而通过MQ能解决99.9%的问题,所需成本预算是10,开发周期7天。客户允许的容错是5%。这时候明显没有必要使用MQ的方案,没意义。

? ? ? ?其实我所想表达的意思是,任何事情没有绝对的,我们始终应该抱着一种开放的心态去面对问题,没必要为了0.1%的优点引入99%的额外投入

参考文献

【1】redis实现消息队列-java代码实现

【2】mybatis insert or update 用法

【3】ConcurrentLinkedQueue使用和方法介绍

  大数据 最新文章
实现Kafka至少消费一次
亚马逊云科技:还在苦于ETL?Zero ETL的时代
初探MapReduce
【SpringBoot框架篇】32.基于注解+redis实现
Elasticsearch:如何减少 Elasticsearch 集
Go redis操作
Redis面试题
专题五 Redis高并发场景
基于GBase8s和Calcite的多数据源查询
Redis——底层数据结构原理
上一篇文章      下一篇文章      查看所有文章
加:2022-06-06 17:25:11  更:2022-06-06 17:27:24 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年5日历 -2024/5/19 21:36:50-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码