MongoDB ReplicaSet集成测试连接异常求助
MongoDB ReplicaSet集成测试连接异常问题排查与解决
问题现象
使用Testcontainers进行3节点MongoDB ReplicaSet集成测试时,出现两类连接异常:
MongoSocketOpenException: Exception opening socket:客户端尝试连接mongod1:27017、mongod2:27018、mongod3:27019,但测试环境下application.yml和application-test.yml的连接配置未生效,无法连接到Testcontainers启动的容器。MongoSocketException: Not known yet (f914961e6dbc):手动创建3个MongoDBContainer并初始化副本集后,节点被移出副本集,连接失败。
相关代码与配置
业务代码片段
…. @Transactional(readOnly = true) public Flux<AccountDto> findAll(int page, int size) { Pageable pageable = PageRequest.of(page, size); return accountRepository.findAllByIdNotNullOrderByIdAsc(pageable) .map(this::mapToDto); } ….
单节点容器测试类
@ActiveProfiles("test") @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = DemoApplication.class) @Testcontainers @AutoConfigureWebTestClient @TestInstance(TestInstance.Lifecycle.PER_CLASS) class AccountControllerIntegrationTest { @Autowired private WebTestClient webTestClient; @Autowired private AccountRepository accountRepository; @Container static MongoDBContainer mongoDBContainer = new MongoDBContainer("mongo:4.4.2") .withCommand("--replSet", "rs0") .withExposedPorts(27017); static{ mongoDBContainer.start(); } @DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { String replicaSetUrl = mongoDBContainer.getReplicaSetUrl(); System.out.println("MongoDB URL: " + replicaSetUrl); String modifiedUrl = replicaSetUrl + "&readPreference=primary"; registry.add("spring.data.mongodb.uri", () -> modifiedUrl); } @BeforeEach void setUp() { accountRepository.deleteAll().block(); } …..
自定义Mongo配置类
@Primary @Validated @Data @ConfigurationProperties(prefix = "mongodb.uri", ignoreUnknownFields = false) public class MongoCustomProperties { private SetMongoProperties set = new SetMongoProperties(); @Data public static class SetMongoProperties { String prefix = "mongodb://"; private String hostFirst = "localhost"; private String hostSecond = "localhost"; private String hostThird = "localhost"; private int portFirstInstance = 27017; private int portSecondInstance = 27018; private int portThirdInstance = 27019; private String database = "accounts"; private String username = "root"; private String password = "root"; private String authenticationDatabase = "admin"; private String replicaSetName = "replicaSet"; private int maxPoolSize = 100; private int minPoolSize = 5; private boolean retryWrites = true; private String writeAcknowledgement = "majority"; private String readPreference = "primaryPreferred"; } }
@Configuration @RequiredArgsConstructor @EnableConfigurationProperties(MongoCustomProperties.class) @EnableReactiveMongoRepositories(basePackages = "com.example.demo.repository") public class MongoConfig extends AbstractReactiveMongoConfiguration { private final MongoCustomProperties mongoProperties; @Override protected String getDatabaseName() { return mongoProperties.getSet().getDatabase(); } @Override public com.mongodb.reactivestreams.client.MongoClient reactiveMongoClient() { String mongoUri = mongoProperties.getSet().buildUri(); return com.mongodb.reactivestreams.client.MongoClients.create(mongoUri); } @Bean public ReactiveMongoTemplate reactiveMongoTemplate() { return new ReactiveMongoTemplate(reactiveMongoClient(), getDatabaseName()); } @Bean public ReactiveMongoTransactionManager transactionManager(ReactiveMongoDatabaseFactory factory) { return new ReactiveMongoTransactionManager(factory); } }
application.yml
spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.mongo.embedded.EmbeddedMongoAutoConfiguration server: port: 9080 mongodb: uri: set: prefix: 'mongodb://' host-first: mongod1 host-second: mongod2 host-third: mongod3 port-first-instance: 27017 port-second-instance: 27018 port-third-instance: 27019 database: accounts username: root password: root authentication-database: admin replicaSet-name: rs0 maxPoolSize: 100 minPoolSize: 5 retryWrites: true writeAcknowledgement: majority readPreference: primaryPreferred
application-test.yml
spring: autoconfigure: exclude: - org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration - org.springframework.boot.autoconfigure.data.mongo.MongoDataAutoConfiguration - org.springframework.boot.autoconfigure.data.mongo.MongoReactiveDataAutoConfiguration - org.springframework.boot.autoconfigure.data.mongo.MongoReactiveRepositoriesAutoConfiguration - org.springframework.boot.autoconfigure.data.mongo.MongoRepositoriesAutoConfiguration
多节点容器尝试代码
@Container static MongoDBContainer mongoDBContainer1 = new MongoDBContainer(DockerImageName.parse("mongo:latest")) .withCommand("--replSet", "rs0") .withExposedPorts(27017); @Container static MongoDBContainer mongoDBContainer2 = new MongoDBContainer(DockerImageName.parse("mongo:latest")) .withCommand("--replSet", "rs0") .withExposedPorts(27018); @Container static MongoDBContainer mongoDBContainer3 = new MongoDBContainer(DockerImageName.parse("mongo:latest")) .withCommand("--replSet", "rs0") .withExposedPorts(27019); static { mongoDBContainer1.start(); mongoDBContainer2.start(); mongoDBContainer3.start(); try { Thread.sleep(10000); String initCommand = String.format("mongo --host %s --port %d --eval \"rs.initiate({_id: 'rs0', members: [{_id: 0, host: '%s:%d'}, {_id: 1, host: '%s:%d'}, {_id: 2, host: '%s:%d'}]})\"", mongoDBContainer1.getHost(), mongoDBContainer1.getMappedPort(27017), mongoDBContainer2.getHost(), mongoDBContainer2.getMappedPort(27018), mongoDBContainer3.getHost(), mongoDBContainer3.getMappedPort(27019)); Runtime.getRuntime().exec(initCommand); } catch (IOException | InterruptedException e) { e.printStackTrace(); } }
问题原因分析
第一类异常(配置未生效)
- 自定义Mongo配置依赖
mongodb.uri.set前缀的属性,但测试类的@DynamicPropertySource中设置的是spring.data.mongodb.uri,两者不匹配,导致自定义配置依然读取application.yml中的mongod1等主机名,而这些主机在测试环境中不存在。 application-test.yml排除了Mongo自动配置,但自定义配置未被动态属性覆盖,依然使用生产环境的连接参数。
第二类异常(手动多节点副本集失败)
- 命令执行位置错误:使用
Runtime.getRuntime().exec(initCommand)是在宿主机执行命令,若宿主机未安装Mongo客户端,命令直接失败;即使安装了,也无法保证容器间的网络连通性。 - 节点地址配置错误:初始化命令中使用了宿主机的
getHost()和映射端口getMappedPort(),但Mongo副本集内部节点需要使用容器间可访问的地址(容器名+内部端口),否则节点无法互相通信,会被自动移出副本集。 - 容器端口配置错误:每个MongoDBContainer的内部端口默认是27017,无需手动指定
withExposedPorts(27018/27019),手动指定会导致容器启动失败或端口冲突。 - 等待方式不可靠:
Thread.sleep(10000)无法保证副本集初始化完成,可能在节点未就绪时就发起连接。
解决方案
针对第一类异常:修复配置动态覆盖问题
修改测试类的@DynamicPropertySource,直接覆盖自定义配置的mongodb.uri.set属性,以单节点为例:
@DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { String host = mongoDBContainer.getHost(); int port = mongoDBContainer.getMappedPort(27017); // 覆盖自定义配置的节点属性 registry.add("mongodb.uri.set.host-first", () -> host); registry.add("mongodb.uri.set.port-first-instance", () -> port); registry.add("mongodb.uri.set.host-second", () -> host); registry.add("mongodb.uri.set.port-second-instance", () -> port); registry.add("mongodb.uri.set.host-third", () -> host); registry.add("mongodb.uri.set.port-third-instance", () -> port); // 覆盖副本集名称 registry.add("mongodb.uri.set.replicaSet-name", () -> "rs0"); }
也可以修改自定义配置,支持直接读取spring.data.mongodb.uri,避免维护多套配置逻辑。
针对第二类异常:正确初始化多节点副本集
步骤1:创建共享Docker网络
让三个容器在同一网络中,使用容器名互相访问:
static Network mongoNetwork = Network.newNetwork(); @Container static MongoDBContainer mongoDBContainer1 = new MongoDBContainer("mongo:4.4.2") .withCommand("--replSet", "rs0", "--bind_ip_all") .withNetwork(mongoNetwork) .withNetworkAliases("mongod1"); @Container static MongoDBContainer mongoDBContainer2 = new MongoDBContainer("mongo:4.4.2") .withCommand("--replSet", "rs0", "--bind_ip_all") .withNetwork(mongoNetwork) .withNetworkAliases("mongod2"); @Container static MongoDBContainer mongoDBContainer3 = new MongoDBContainer("mongo:4.4.2") .withCommand("--replSet", "rs0", "--bind_ip_all") .withNetwork(mongoNetwork) .withNetworkAliases("mongod3");
步骤2:在容器内部执行初始化命令
使用execInContainer方法在主节点内执行命令,避免宿主机依赖:
static { mongoDBContainer1.start(); mongoDBContainer2.start(); mongoDBContainer3.start(); // 构建副本集初始化命令,使用容器名作为节点地址 String initReplicaSetCmd = String.format( "mongosh --eval \"rs.initiate({_id: 'rs0', members: [{_id: 0, host: 'mongod1:27017'}, {_id: 1, host: 'mongod2:27017'}, {_id: 2, host: 'mongod3:27017'}]})\"" ); try { // 在第一个容器内执行初始化命令 ExecResult result = mongoDBContainer1.execInContainer("/bin/sh", "-c", initReplicaSetCmd); if (result.getExitCode() != 0) { throw new RuntimeException("副本集初始化失败: " + result.getStderr()); } // 轮询等待副本集就绪 boolean isReady = false; int retryCount = 0; while (!isReady && retryCount < 10) { ExecResult statusResult = mongoDBContainer1.execInContainer("/bin/sh", "-c", "mongosh --eval \"rs.status().ok\""); if (statusResult.getStdout().trim().equals("1")) { isReady = true; } else { Thread.sleep(2000); retryCount++; } } if (!isReady) { throw new RuntimeException("副本集20秒内未就绪"); } } catch (IOException | InterruptedException e) { throw new RuntimeException("副本集初始化异常", e); } }
步骤3:动态覆盖多节点配置属性
@DynamicPropertySource static void setProperties(DynamicPropertyRegistry registry) { // 覆盖三个节点的主机和映射端口 registry.add("mongodb.uri.set.host-first", () -> mongoDBContainer1.getHost()); registry.add("mongodb.uri.set.port-first-instance", () -> mongoDBContainer1.getMappedPort(27017)); registry.add("mongodb.uri.set.host-second", () -> mongoDBContainer2.getHost()); registry.add("mongodb.uri.set.port-second-instance", () -> mongoDBContainer2.getMappedPort(27017)); registry.add("mongodb.uri.set.host-third", () -> mongoDBContainer3.getHost()); registry.add("mongodb.uri.set.port-third-instance", () -> mongoDBContainer3.getMappedPort(27017)); registry.add("mongodb.uri.set.replicaSet-name", () -> "rs0"); }
额外注意事项
- 使用
mongosh替代旧的mongo命令(MongoDB 4.4+官方推荐)。 - 添加
--bind_ip_all确保Mongo容器接受容器间的网络连接。 - 不要硬编码端口,使用
getMappedPort(27017)获取宿主机的随机映射端口。
内容的提问来源于stack exchange,提问作者skyho
相关产品推荐
相关产品推荐

