一、问题现象与初步排查

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调度卡死的问题,别先急着重启服务,先查连接池的状态——大部分调度卡住的根源都是连接泄漏,源码层面能定位到具体的释放漏洞,优化后就能解决。核心就是记住"资源要归位",用完的连接必须放回池里,这样调度系统才能稳定跑起来。