一、问题现象与初步排查
1.1 卡死表现
公司用的调度工具(DolphinScheduler)突然"卡壳",前端显示SQL任务全是"运行中",但后台日志里没有新任务提交的痕迹。之前明明跑完的SQL任务,数据库连接却没释放,就像餐厅收了客人却没擦桌子,后来的客人(新任务)拿不到座位(连接),导致整个流程停摆,连带着后续的定时报表、数据同步都没法跑。
1.2 初步排查方向
先看DolphinScheduler的Master节点日志,发现调度线程一直卡在"等待数据源连接"的步骤;再查连接池的监控,活跃连接数全满,超过了配置的最大值,初步判断是连接没还回池里(连接泄漏),导致后续任务拿不到连接,最终整个调度卡住。
二、DolphinScheduler源码层面定位根源
2.1 调度线程阻塞的关键节点
DolphinScheduler的核心调度逻辑在MasterServer里,每个SQL任务都会先调用DataSourceUtils类的方法拿连接,执行完必须把连接放回池里。如果某个任务用完连接没还,后续的调度线程再去拿连接时,就会一直等,像超市结账台都在忙,新来的顾客只能排队,导致整个调度队列被堵死。
2.2 连接池泄漏的源码验证
看DolphinScheduler 3.1.5版本的DataSourceUtils源码,原来的连接释放方法有漏洞:
// 简化版:有问题的连接释放代码
public static void releaseConnection(Connection conn) {
if (conn != null) {
try {
conn.close(); // 关闭连接
} catch (SQLException e) {
// 只打错误日志,没做补救——连接没放回池,直接泄漏了
logger.error("释放连接失败", e);
}
}
}
如果conn.close()时抛异常,连接就会停在"活跃"状态,不会被池回收,时间长了所有连接都被占满,新任务拿不到连接。
三、复现示例(单一技术栈:Java 8 + HikariCP 4.0.3 + DolphinScheduler 3.1.5)
3.1 模拟连接泄漏的完整代码
这个示例用常用的HikariCP连接池,故意制造连接泄漏,复现调度卡住的场景,注释都标清了:
import com.zaxxer.hikari.HikariConfig;
import com.zaxxer.hikari.HikariDataSource;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.SQLException;
// 技术栈说明:Java 8作为开发语言,HikariCP做数据源连接池,模拟DolphinScheduler的SQL任务场景
public class ConnectionLeakDemo {
// 静态连接池:对应DolphinScheduler的数据源配置
private static HikariDataSource dataSource;
// 初始化连接池,最大连接数设为2(方便快速复现泄漏)
static {
HikariConfig config = new HikariConfig();
config.setJdbcUrl("jdbc:mysql://localhost:3306/testdb?useSSL=false");
config.setUsername("root"); // 替换成自己的数据库账号
config.setPassword("123456"); // 替换成自己的数据库密码
config.setMaximumPoolSize(2); // 连接池最多同时用2个连接,超过就阻塞
dataSource = new HikariDataSource(config);
}
// 模拟DolphinScheduler的SQL任务:故意泄漏连接
public static void runSqlTaskLeak() throws SQLException {
Connection conn = null;
PreparedStatement stmt = null;
try {
conn = dataSource.getConnection(); // 拿连接,像任务申请资源
stmt = conn.prepareStatement("SELECT * FROM user WHERE id = 1");
stmt.executeQuery(); // 执行SQL
// 【关键错误】:这里没关闭conn,连接没还回池,变成泄漏
// 正常应该在finally里关闭所有资源
} finally {
stmt.close(); // 只关闭了SQL语句,没关连接
}
}
public static void main(String[] args) throws InterruptedException {
// 测试步骤:先启动2个任务,用完不释放
runSqlTaskLeak(); // 第一个任务,占用池的1个连接
runSqlTaskLeak(); // 第二个任务,占用池的另1个连接,池满了
Thread.sleep(1000); // 等1秒,连接还是没释放,池一直空不了
// 尝试启动第三个任务——对应DolphinScheduler里的新调度任务,这时候会卡住
try {
runSqlTaskLeak();
} catch (SQLException e) {
System.out.println("获取连接失败,线程阻塞!");
}
}
}
这个示例跑起来会直接输出"获取连接失败,线程阻塞!",完美复现DolphinScheduler调度卡住的场景。
四、技术解析与优化方案
4.1 连接池泄漏的核心原因
总结下来就是三类:① 开发者忘记关连接(比如示例里的情况);② 关闭连接时异常没处理(比如源码里的releaseConnection方法);③ 连接对象被错误覆盖,没释放旧连接。本质都是"资源没及时归位",就像借了公司的工具不还,后来的人没法用。
4.2 调度卡死的逻辑链
DolphinScheduler的Master节点是单线程或者有限线程处理调度,每个SQL任务必须拿连接才能执行。当连接池满了,调度线程就会一直等,不会去处理其他任务,整个调度队列被堵死,前端显示任务运行中,实际是线程在"干等",连日志都不会输出新内容。
4.3 修复与优化措施
① 代码层面:用Java的try-with-resource自动关闭资源,不用手动写finally,彻底避免忘记关的情况,优化后的代码:
// 优化后的安全版SQL任务,自动释放连接
public static void runSqlTaskSafe() throws SQLException {
// try-with-resource会自动关实现AutoCloseable的资源,不用手动写关闭代码
try (Connection conn = dataSource.getConnection();
PreparedStatement stmt = conn.prepareStatement("SELECT * FROM user WHERE id = 1")) {
stmt.executeQuery(); // 正常执行SQL
} // 执行到这里,conn和stmt会自动关闭,连接归还给池,不会泄漏
}
② 框架层面:优化DolphinScheduler的releaseConnection方法,异常时做补救处理,比如强制关闭连接,优化后的代码片段:
// 优化后的连接释放方法,避免异常导致的泄漏
public static void releaseConnection(Connection conn) {
if (conn != null) {
try {
conn.close();
} catch (SQLException e) {
logger.error("释放连接失败,尝试强制关闭", e);
// 即使close异常,也尝试强制终止数据库连接,不让它占着池
try {
conn.createStatement().execute("KILL CONNECTION " + getConnectionId(conn));
} catch (SQLException ex) {
logger.error("强制关闭连接失败", ex);
}
}
}
}
③ 监控层面:给连接池加监控,设置连接超时时间,比如超过10秒没释放就自动回收,避免泄漏的连接一直占着池。
五、应用场景、技术优缺点、注意事项
5.1 应用场景
这个问题主要出现在用DolphinScheduler做大数据调度的场景,比如电商的定时报表生成、用户行为统计、数据仓库的增量同步这些,这些场景会频繁跑SQL任务,连接池的使用频率极高,很容易出现泄漏问题。
5.2 技术优缺点
优点:连接池复用数据库连接,不用每次创建销毁(创建连接比执行SQL耗资源),能大幅提高SQL任务的执行效率;DolphinScheduler支持多种数据源(MySQL、Oracle、PostgreSQL),适合复杂的调度编排。缺点:连接泄漏问题的定位难度高,源码级的排查需要对框架有一定了解;如果没优化,连接池满后会导致整个调度系统不可用,影响业务的定时任务执行。
5.3 注意事项
① 开发时必须用try-with-resource,这是目前最稳妥的资源自动释放方式,避免手动关闭的漏洞;② 使用框架前,一定要搞清楚框架的资源管理逻辑,比如DolphinScheduler的数据源是全局复用的,不能自己随便改连接配置;③ 给连接池设置合理的最大连接数,不要设太大(比如超过数据库的最大连接数会压垮数据库),也不要设太小(容易被占满);④ 遇到调度卡死,先看连接池的监控,再查源码里的资源释放方法,不要盲目重启Master节点(重启会丢失正在跑的任务状态)。
六、总结
遇到DolphinScheduler调度卡死的问题,别先急着重启服务,先查连接池的状态——大部分调度卡住的根源都是连接泄漏,源码层面能定位到具体的释放漏洞,优化后就能解决。核心就是记住"资源要归位",用完的连接必须放回池里,这样调度系统才能稳定跑起来。
评论
围绕“SQL任务数据源连接池泄漏引发调度卡死,从DolphinScheduler源码级别定位线程阻塞根源”参与讨论