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

MongoDB ReplicaSet集成测试连接异常求助

MongoDB ReplicaSet集成测试连接异常问题排查与解决

问题现象

使用Testcontainers进行3节点MongoDB ReplicaSet集成测试时,出现两类连接异常:

  1. MongoSocketOpenException: Exception opening socket:客户端尝试连接mongod1:27017、mongod2:27018、mongod3:27019,但测试环境下application.yml和application-test.yml的连接配置未生效,无法连接到Testcontainers启动的容器。
  2. 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();
        }
    }

问题原因分析

第一类异常(配置未生效)

  1. 自定义Mongo配置依赖mongodb.uri.set前缀的属性,但测试类的@DynamicPropertySource中设置的是spring.data.mongodb.uri,两者不匹配,导致自定义配置依然读取application.yml中的mongod1等主机名,而这些主机在测试环境中不存在。
  2. application-test.yml排除了Mongo自动配置,但自定义配置未被动态属性覆盖,依然使用生产环境的连接参数。

第二类异常(手动多节点副本集失败)

  1. 命令执行位置错误:使用Runtime.getRuntime().exec(initCommand)是在宿主机执行命令,若宿主机未安装Mongo客户端,命令直接失败;即使安装了,也无法保证容器间的网络连通性。
  2. 节点地址配置错误:初始化命令中使用了宿主机的getHost()和映射端口getMappedPort(),但Mongo副本集内部节点需要使用容器间可访问的地址(容器名+内部端口),否则节点无法互相通信,会被自动移出副本集。
  3. 容器端口配置错误:每个MongoDBContainer的内部端口默认是27017,无需手动指定withExposedPorts(27018/27019),手动指定会导致容器启动失败或端口冲突。
  4. 等待方式不可靠: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.16 22:15:56