You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Testcontainers性能慢于原生Docker的优化方案咨询

Testcontainers + Liquibase 性能优化问题

我编写了一套可用的Docker脚本,启动MySQL容器后初始化数据库,执行6次Liquibase数据库迁移以模拟6种不同环境,整体运行耗时约6分钟。但改用Java结合Testcontainers与Liquibase Java API实现相同逻辑后,运行耗时超过10分钟。我认为并非Liquibase导致该问题,因为底层调用的是相同的Liquibase代码,现寻求Testcontainers实现的性能优化方案。

Docker脚本

function die { mvn docker:stop ; exit 1; }

function initializeDatabase {
   docker exec -it mysql-1 mysql -uroot -proot \
   -e "DROP USER IF EXISTS 'myuser'@'%';" \
   -e "DROP DATABASE IF EXISTS mydb;" \
   -e "CREATE DATABASE mydb;" \
   -e "CREATE USER 'myuser'@'%' IDENTIFIED WITH mysql_native_password BY 'mypassword';" \
   -e "GRANT ALL PRIVILEGES ON mydb.* TO 'myuser'@'%';"
}

mvn docker:start && sleep 10

initializeDatabase || die

mvn liquibase:update@execution1 || die

initializeDatabase || die

mvn liquibase:update@execution2 || die

initializeDatabase || die

mvn liquibase:update@execution3 || die

initializeDatabase || die

mvn liquibase:update@execution4 || die

initializeDatabase || die

mvn liquibase:update@execution5 || die

initializeDatabase || die

mvn liquibase:update@execution6 || die

mvn docker:stop

Java代码

import liquibase.Liquibase;
import liquibase.database.jvm.JdbcConnection;
import liquibase.exception.LiquibaseException;
import liquibase.resource.FileSystemResourceAccessor;
import liquibase.resource.SearchPathResourceAccessor;
import org.junit.*;
import org.testcontainers.containers.MySQLContainer;
import org.testcontainers.utility.DockerImageName;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.sql.Statement;

public class SimpleMySQLTest {
    @ClassRule
    public static MySQLContainer<?> mysql = new MySQLContainer<>(DockerImageName.parse("mysql:latest"))
            .withEnv("TESTCONTAINERS_REUSE_ENABLED", "true")
            .withEnv("TESTCONTAINERS_CHECKS_DISABLE", "true")
            .withEnv("MYSQL_ROOT_PASSWORD", "root")
            .withEnv("MYSQL_ROOT_HOST", "%")
            .withDatabaseName("root")
            .withUsername("root")
            .withPassword("root");

    @BeforeClass
    public static void beforeClass() {
        mysql.start();
    }

    @AfterClass
    public static void afterClass() {
        mysql.stop();
    }

    @Test
    public void NAMSDev() {
        try (Connection connection = DriverManager.getConnection(mysql.getJdbcUrl().replace("/root", "/rms"), "rms", "rms")) {
            Liquibase liquibase = new Liquibase("src/main/sql/baseline/mysql/NAMSDev/databaseChangeLog.master.xml", new SearchPathResourceAccessor(".", new FileSystemResourceAccessor()), new JdbcConnection(connection));
            liquibase.update("NAMS");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } catch (LiquibaseException e) {
            throw new RuntimeException(e);
        }
    }
    
    @Before
    public void initializeDatabase() {
        try (Connection connection = DriverManager.getConnection(mysql.getJdbcUrl(), "root", "root")) {
            Statement statement = connection.createStatement();
            statement.execute("DROP USER IF EXISTS 'rms'@'%';");
            statement.execute("DROP DATABASE IF EXISTS rms;");
            statement.execute("CREATE DATABASE rms;");
            statement.execute("CREATE USER 'rms'@'%' IDENTIFIED WITH mysql_native_password BY 'rms';");
            statement.execute("GRANT ALL PRIVILEGES ON rms.* TO 'rms'@'%';");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void NAMSQc() {
        try (Connection connection = DriverManager.getConnection(mysql.getJdbcUrl().replace("/root", "/rms"), "rms", "rms")) {
            Liquibase liquibase = new Liquibase("src/main/sql/baseline/mysql/NAMSQc/databaseChangeLog.master.xml", new SearchPathResourceAccessor(".", new FileSystemResourceAccessor()), new JdbcConnection(connection));
            liquibase.update("NAMS");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } catch (LiquibaseException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void NAMSProd() {
        try (Connection connection = DriverManager.getConnection(mysql.getJdbcUrl().replace("/root", "/rms"), "rms", "rms")) {
            Liquibase liquibase = new Liquibase("src/main/sql/baseline/mysql/NAMSProd/databaseChangeLog.master.xml", new SearchPathResourceAccessor(".", new FileSystemResourceAccessor()), new JdbcConnection(connection));
            liquibase.update("NAMS");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } catch (LiquibaseException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void NORDICSDev() {
        try (Connection connection = DriverManager.getConnection(mysql.getJdbcUrl().replace("/root", "/rms"), "rms", "rms")) {
            Liquibase liquibase = new Liquibase("src/main/sql/baseline/mysql/NORDICSDev/databaseChangeLog.master.xml", new SearchPathResourceAccessor(".", new FileSystemResourceAccessor()), new JdbcConnection(connection));
            liquibase.update("NORDICS");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } catch (LiquibaseException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void NORDICSQc() {
        try (Connection connection = DriverManager.getConnection(mysql.getJdbcUrl().replace("/root", "/rms"), "rms", "rms")) {
            Liquibase liquibase = new Liquibase("src/main/sql/baseline/mysql/NORDICSQc/databaseChangeLog.master.xml", new SearchPathResourceAccessor(".", new FileSystemResourceAccessor()), new JdbcConnection(connection));
            liquibase.update("NORDICS");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } catch (LiquibaseException e) {
            throw new RuntimeException(e);
        }
    }

    @Test
    public void NORDICSProd() {
        try (Connection connection = DriverManager.getConnection(mysql.getJdbcUrl().replace("/root", "/rms"), "rms", "rms")) {
            Liquibase liquibase = new Liquibase("src/main/sql/baseline/mysql/NORDICSProd/databaseChangeLog.master.xml", new SearchPathResourceAccessor(".", new FileSystemResourceAccessor()), new JdbcConnection(connection));
            liquibase.update("NORDICS");
        } catch (SQLException e) {
            throw new RuntimeException(e);
        } catch (LiquibaseException e) {
            throw new RuntimeException(e);
        }
    }
}

性能优化方案

  • 最大化容器复用:
    已经开启TESTCONTAINERS_REUSE_ENABLED=true,需确保该配置生效(可通过环境变量或系统属性设置)。同时删除@AfterClass中的mysql.stop()调用,让容器在测试结束后保留,下次测试直接复用,避免重复启动容器的耗时。
  • 简化数据库初始化步骤:
    • 无需每次删除重建用户,用户权限可一次性配置完成,每次测试仅删除重建目标数据库,减少SQL执行次数。
    • 将初始化SQL合并为批量执行,或使用addBatch()+executeBatch()方法批量运行SQL,减少JDBC交互次数。
  • 复用Liquibase资源与实例:
    • 将SearchPathResourceAccessor和FileSystemResourceAccessor设置为静态变量复用,避免每次创建Liquibase实例时重复加载文件系统资源。
    • 提前解析ChangeLog文件,或使用Liquibase的缓存机制,减少XML解析的重复开销。
  • 启用测试并行执行:
    若使用JUnit 5,可通过配置文件开启并行测试,让多个Liquibase迁移任务并行执行。注意为每个测试分配独立的数据库(比如使用不同的库名),避免数据冲突。
  • 关闭Testcontainers额外组件:
    添加TESTCONTAINERS_RYUK_DISABLED=true环境变量,关闭负责清理容器的Ryuk进程,减少容器启动和管理的开销。
  • 指定具体MySQL镜像版本:
    将mysql:latest替换为具体稳定版本(如mysql:8.0.36),避免镜像标签解析和自动拉取的耗时。
  • 使用JDBC连接池:
    引入HikariCP等连接池,代替每次用DriverManager创建新连接,减少连接建立和销毁的开销。可初始化静态连接池供所有测试共用。

内容的提问来源于stack exchange,提问作者Victor Rodriguez

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 08:56:05