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. Apache代理服务器与后台服务器首次握手慢,后续请求复用长链接无卡顿现象。我们自测环境前端服务器用的是apache,配置了反向代理到后端小服务。如果客户端请求超过了代理服务器设置的最大链接数,当前请求时不会被转发到后台服务器的。
  2. 一个服务第一次被调用时会触发很多类的加载和初始化,但这个往往是第一次访问卡,后续访问就不卡了。因为类的卸载是比较苛刻的所以大多数类加载到jvm后等进程结束时才会被卸载。所以这种情况不会出现间隔性卡顿(即:第一次请求卡紧接着请求不卡,等半小时或一小时再请求又卡),仅仅第一次访问卡顿。
  3. 由于后台服务用到了数据库连接池、线程池、redis连接池可能由于参数配置不合理导致,链接长时间没被使用导致过期销毁。

首先尝试跳过代理服务器用postman直接访问后台服务器接口看是否可以复现,结果是复现了,那就排除了情况1。虽然看现象更贴近3,直接去排查连接池、线程池、redis连接池参数可能可以直接定位问题。但万一是这个后台服务器网络IO层面的问题呢?是为了避免忽略细节走弯路,首先要判断下这个请求是否能及时被服务端接收到。我用Arthas对这个后台服务进行一个监控,监控一下web层Handler方法执行时间。
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

如上图可以发现postman方法执行耗时6s,而在labelStatics中调用了staticsService.staticsLabel()方法耗时5.7s,所以可以彻底排查服务器网络IO问题了,完完全全是服务本身问题(我干~~)。使用Arthas继续对调用链路进行追踪最后发现卡顿之处在与数据库交互的方法上。
查询数据库的业务代码块

然后我理所当然去监控了一下数据池获取数据库链接的DruidDataSource.getConnection()方法,果然罪魁祸首就是它。
Druid数据池获取数据库链接方法

至此我们已经定位到了卡顿原因,由于线程池参数配置有误导致线程失效。这个小服务还在开发阶段线程池的配置就是随便网上copy的如下。

spring.datasource.primary.url=****
spring.datasource.primary.username=***
spring.datasource.primary.password=***
spring.datasource.primary.driver-class-name=org.postgresql.Driver
spring.datasource.primary.max-active=50
spring.datasource.primary.initial-size=15
spring.datasource.primary.min-idle=0 

对于Druid数据池我也没有深入了解过仅仅知道简单参数配置,以及它是一个生产消费者模式的连接池,里面包含了创建连接线程、销毁连接线程、消费连接线程(请求对getConnection方法的调用)。也趁此机会翻了一下Druid数据池的源码,Druid数据池的源码帖子网上很多,这里只探讨下导致卡顿问题出现的源码部分。

先说结论:因为当前服务在自测环境调用方不多,且没有操作数据库的定时任务,在min-idle=0的情况下初始化的15个连接在半小时内如果没有被使用到将会被销毁连接线程销毁掉。

二、Druid销毁连接线程源码分析

Druid销毁连接线程为DestroyConnectionThread

 public class DestroyConnectionThread extends Thread {

        public DestroyConnectionThread(String name){
            super(name);
            this.setDaemon(true);
        }

        public void run() {
            initedLatch.countDown();

            while (true) {
                try {
                    if (closed) {
                        break;
                    }
                    // timeBetweenEvictionRunsMillis默认值为60s。即60s调用一次destroyTask.run();方法
                    // timeBetweenEvictionRunsMillis可以通过配置修改
                    if (timeBetweenEvictionRunsMillis > 0) {
                        Thread.sleep(timeBetweenEvictionRunsMillis);
                    } else {
                        Thread.sleep(1000); //
                    }

                    if (Thread.interrupted()) {
                        break;
                    }

                    destroyTask.run();
                } catch (InterruptedException e) {
                    break;
                }
            }
        }
    }

看下核心逻辑destroyTask.run();

 public class DestroyTask implements Runnable {

        @Override
        public void run() {
        // 回收未被使用的连接逻辑
            shrink(true, keepAlive);
		// 如果remove-abandoned=true则会调用回收正在被使用的连接逻辑,防止出现代码逻辑出现死锁占用连接不释放的情况
            if (isRemoveAbandoned()) {
                removeAbandoned();
            }
        }

    }

引发我们卡顿的逻辑在shrink(true, keepAlive)中,连接尚未被使用就过期了。在分析这块代码之前先说一下Druid中有三个存放连接的关键数组:

  • connections[] 存放可以使用的连接,当有请求调用getConnection获取连接时就是从这里面取的。
  • keepAliveConnections[] 在DestroyConnectionThread在扫描connections[]中的连接进行回收时,满足某些条件的连接会被放入keepAliveConnections[]中后续会对keepAliveConnections[]中的连接进行有效性验证如果有效还可以复活更新其最后活跃时间并重新放入connections[]中,如果无效则关闭销毁连接
  • evictConnections[] 在DestroyConnectionThread在扫描connections[]中的连接进行回收时,满足某些条件的连接会被放入evictConnections[]中,后续全部关闭销毁。
 public void shrink(boolean checkTime, boolean keepAlive) {
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            return;
        }

        int evictCount = 0;
        int keepAliveCount = 0;
        try {
            if (!inited) {
                return;
            }
			// poolingCount为connections[]数组的有效长度,minIdle对应配置项中的min-idle,我们当前项目配置的0.所以checkCount=poolingCount。
            final int checkCount = poolingCount - minIdle;
            final long currentTimeMillis = System.currentTimeMillis();
            // 遍历connections[]中的连接
            for (int i = 0; i < poolingCount; ++i) {
                DruidConnectionHolder connection = connections[i];
				// 是否开始时间筛选,如果不开启则直接将连接回收到数量=min-idle
                if (checkTime) {
                // phyTimeoutMillis为连接的允许存活时间,默认为-1,如果配置了,只要 '当前时间-连接创建时间>phyTimeoutMillis' 则直接将连接添加到evictConnections[]数组中后续关闭
                    if (phyTimeoutMillis > 0) {
                        long phyConnectTimeMillis = currentTimeMillis - connection.connectTimeMillis;
                        if (phyConnectTimeMillis > phyTimeoutMillis) {
                            evictConnections[evictCount++] = connection;
                            continue;
                        }
                    }
					// idleMillis为连接的空闲时间
                    long idleMillis = currentTimeMillis - connection.lastActiveTimeMillis;
					// 如果连接空闲时间小于minEvictableIdleTimeMillis最小空闲时间则不做处理,minEvictableIdleTimeMillis默认为半小时
                    if (idleMillis < minEvictableIdleTimeMillis) {
                        break;
                    }
					// 到这里说明当前连接 '空闲时间>minEvictableIdleTimeMillis',在这个项目中checkCount==poolingCount,
					// 'checkTime && i < checkCount'恒成立,所以只要空闲时间超过minEvictableIdleTimeMillis的连接就会被回收
					// 我吐了。我理解这个配置存在的意义是将空闲线程回收到数量=minIdle
                    if (checkTime && i < checkCount) {
                        evictConnections[evictCount++] = connection;
                    } else if (idleMillis > maxEvictableIdleTimeMillis) {
                    // 当 '空闲时间>maxEvictableIdleTimeMillis最大空闲时间' 时添加到evictConnections数组直接关闭,maxEvictableIdleTimeMillis的默认值是7小时
                        evictConnections[evictCount++] = connection;
                    } else if (keepAlive) {
                    // 当 'minEvictableIdleTimeMillis<空闲时间<maxEvictableIdleTimeMillis'且keep-alive=true
					// 时将连接添加到keepAliveConnections数组中进行后续有效性验证
                        keepAliveConnections[keepAliveCount++] = connection;
                    }
                } else {
                    if (i < checkCount) {
                        evictConnections[evictCount++] = connection;
                    } else {
                        break;
                    }
                }
            }
			// 要移除的连接总数
            int removeCount = evictCount + keepAliveCount;
            if (removeCount > 0) {
                System.arraycopy(connections, removeCount, connections, 0, poolingCount - removeCount);
                Arrays.fill(connections, poolingCount - removeCount, poolingCount, null);
                poolingCount -= removeCount;
            }
            keepAliveCheckCount += keepAliveCount;
        } finally {
            lock.unlock();
        }
		// 关闭evictConnections[]数组中的连接
        if (evictCount > 0) {
            for (int i = 0; i < evictCount; ++i) {
                DruidConnectionHolder item = evictConnections[i];
                Connection connection = item.getConnection();
                JdbcUtils.close(connection);
                destroyCount.incrementAndGet();
            }
            Arrays.fill(evictConnections, null);
        }
		// 判断keepAliveConnections[]数组中的连接是否有效,如果有效更新其最后活跃时间lastActiveTime并放入connections[]数组中,无效的关闭连接
        if (keepAliveCount > 0) {
            this.getDataSourceStat().addKeepAliveCheckCount(keepAliveCount);
            // keep order
            for (int i = keepAliveCount - 1; i >= 0; --i) {
                DruidConnectionHolder holer = keepAliveConnections[i];
                Connection connection = holer.getConnection();
                holer.incrementKeepAliveCheckCount();

                boolean validate = false;
                try {
                    this.validateConnection(connection);
                    validate = true;
                } catch (Throwable error) {
                    if (LOG.isDebugEnabled()) {
                        LOG.debug("keepAliveErr", error);
                    }
                    // skip
                }

                if (validate) {
                    holer.setLastActiveTimeMillis(System.currentTimeMillis());
                    put(holer);
                } else {
                    JdbcUtils.close(connection);
                }
            }
            Arrays.fill(keepAliveConnections, null);
        }
    }

至此我们彻底警情定位了问题所在就是我们Druid参数配置不合理导致的。

三、Druid 参数修改

然后我重新修改了Druid 参数,问题也是顺利解决

  • spring.datasource.druid.initial-size=15
    初始化连接数
  • spring.datasource.druid.max-active=100
    最大活跃连接数,包括池内和池外 max-active>=activeCount(池外) + poolingCount(connections数组的有效长度)
  • spring.datasource.druid.min-idle=15
    连接池内最小可用连接数,回收空闲连接时,将保证至少有minIdle个连接.
  • spring.datasource.druid.max-wait=60000
    获取连接超时的时间,即getConnection方法的超时时间
  • spring.datasource.druid.time-between-eviction-runs-millis=60000
    Druid销毁连接线程为DestroyConnectionThread的调用间隔
  • spring.datasource.druid.min-evictable-idle-time-millis=300000
    连接最小空闲时间
  • spring.datasource.druid.test-on-borrow=false
    如果为true则获取连接时会验证一下连接的有效性,生产环境开启影响性能
  • spring.datasource.druid.test-on-return=false
    如果为true则返回链接时会验证一下连接的有效性,生产环境开启影响性能
  • spring.datasource.druid.test-while-idle=true
    如果为true则获取连接时且当前连接的空闲时间>time-between-eviction-runs-millis时会验证连接的有效性,生产环境开启防止DestroyConnectionThread由于系统资源紧张得不到cpu执行时连接未被回收失效
  • spring.datasource.druid.validation-query=SELECT 1
    验证连接有效性时执行的语句
  • spring.datasource.druid.validation-query-timeout=1000
    验证有效性语句SELECT 1的超时时间,超过这个时间则认为无效
  • spring.datasource.druid.keep-alive=true
    对于空闲时间超过min-evictable-idle-time-millis的连接如果keep-alive为true时,则会调用validation-query对其进行验证并更新最后活跃时间保证连接池内最小可用连接数不小于 min-idle
  • spring.datasource.druid.remove-abandoned=true
    如果remove-abandoned=true则会调用回收正在被使用的连接逻辑,防止出现代码逻辑出现死锁占用连接不释放的情况
  • spring.datasource.druid.remove-abandoned-timeout=180
    设置druid 强制回收连接的时限,当程序从池中get到连接开始算起,超过此 值后,druid将强制回收该连接,单位秒。应大于业务运行最长时间
  • spring.datasource.druid.log-abandoned=true
    当druid强制回收连接后,是否将stack trace 记录到日志中
  开发工具 最新文章
Postman接口测试之Mock快速入门
ASCII码空格替换查表_最全ASCII码对照表0-2
如何使用 ssh 建立 socks 代理
Typora配合PicGo阿里云图床配置
SoapUI、Jmeter、Postman三种接口测试工具的
github用相对路径显示图片_GitHub 中 readm
Windows编译g2o及其g2o viewer
解决jupyter notebook无法连接/ jupyter连接
Git恢复到之前版本
VScode常用快捷键
上一篇文章      下一篇文章      查看所有文章
加:2022-04-15 00:19:38  更:2022-04-15 00:20:59 
 
开发: 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年11日历 -2024/11/14 15:09:40-

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