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
相关产品推荐
相关产品推荐

