MongoDB 클러스터 연결 장애와 설정 개선

MongoDB 클러스터 연결 장애와 설정 개선

이번 사례는 AWS EKS 배포와 MongoDB Atlas 버전 업그레이드가 겹치면서 발생한 장애를 바탕으로, 클러스터 환경의 MongoDB 접속 설정을 점검한 내용입니다.

1. 예상하지 못한 장애 발생

MongoDB Atlas를 사용하는 시스템을 AWS EKS에 배포하던 중 일부 MongoDB 클러스터의 연결이 완료되지 않아 신규 WAS가 정상 기동하지 못했습니다. 즉시 오류가 발생해 프로세스가 종료된 것도 아니었습니다. 애플리케이션은 초기화 과정에서 대기했고 실제 요청을 처리할 수 없는 상태였습니다.

이 상태에서 Kubernetes Health Check와 배포 절차가 진행되며 기존 서비스 인스턴스가 종료되어 장애가 확대되었습니다. 이번 대응에서는 Health Check 자체보다 MongoDB 연결 과정을 중심으로 원인을 분석했습니다. 장애 시점의 이벤트를 확인한 결과, 배포와 동시에 MongoDB Atlas 버전 업그레이드가 진행되고 있었습니다.

2. Atlas 버전 업그레이드와 배포 시점이 겹침

MongoDB 클러스터는 여러 멤버로 구성되며, 유지보수나 버전 업그레이드 중 일부 멤버의 가용성이 일시적으로 변할 수 있습니다. Primary가 대상이면 새로운 Primary를 선출하는 과정도 발생합니다. 이는 클러스터 운영에서 고려해야 할 정상적인 상태 변화이며, 드라이버는 토폴로지를 파악해 사용 가능한 서버를 선택할 수 있어야 합니다.

이번에는 신규 WAS의 최초 접속 시점과 Atlas의 노드 전환 시점이 겹쳤습니다. 기존 WAS는 연결 풀과 토폴로지 정보를 이미 가지고 있었고, Atlas가 정상 상태였다면 신규 WAS도 평소처럼 연결됐을 가능성이 높습니다. 따라서 원인을 단순히 Atlas 유지보수로 보지 않았습니다. 유지보수, 장애조치, Primary 변경은 클러스터 환경에서 발생할 수 있으므로 신규 애플리케이션도 일부 멤버를 사용할 수 없는 상태를 견딜 수 있어야 합니다.

평상시에도 일부 클러스터 멤버의 접속 문제가 서비스 장애 없이 가려져 있었을 가능성도 검토했습니다. 다른 사용 가능한 멤버로 연결할 수 있었다면 문제가 드러나지 않다가, 업데이트에 따른 노드 전환과 신규 WAS의 최초 접속이 겹치면서 기동 문제로 나타날 수 있기 때문입니다.

3. 연결 실패 판단 시간의 문제 확인

MongoDB Java Driver에는 연결 관련 기본 Timeout이 있지만, 기본값이 서비스의 기동 및 장애 허용 시간에 적합하다는 의미는 아닙니다. 초기 접속에는 DNS 조회, TCP 연결, TLS 처리, 토폴로지 확인과 서버 선택 등이 연속해서 발생합니다. 각 단계의 대기가 겹치면 WAS가 예상보다 오래 기동 중 상태에 머물 수 있습니다.

분석의 초점은 "왜 특정 노드가 잠시 빠졌는가"보다 "그 상황에서 신규 WAS가 왜 적절한 시간 안에 연결 성공 또는 실패를 결정하지 못했는가"에 두었습니다. 이에 연결 Timeout과 서버 선택 정책을 명시했습니다.

4. 실제 운영 환경에 적용한 설정

4.1 JVM DNS 캐시 시간 조정

spec:
  template:
    spec:
      containers:
        - name: serviceName
          env:
            - name: JAVA_TOOL_OPTIONS
              value: "-Dsun.net.inetaddr.ttl=5" 

deployment.yml의 JAVA_TOOL_OPTIONS에 `-Dsun.net.inetaddr.ttl=5`를 적용했습니다. JVM이 DNS 조회 결과를 장시간 유지하지 않도록 해 클러스터 주소나 DNS 상태 변화를 비교적 빠르게 반영하기 위한 설정입니다. `sun.net.inetaddr.ttl`은 JDK 구현에 의존하는 System Property이므로 사용하는 JDK의 지원 여부를 확인해야 합니다. 이 사례에서는 별도의 Security Property 없이 해당 속성만 JVM System Property로 전달했습니다.

4.2 MongoDB Client Timeout 및 읽기·쓰기 정책 설정

public class MongoCustomConfig
        extends AbstractMongoClientConfiguration {

    @Override
    public MongoClient mongoClient() {
        //
        return getUriMongoClient();
    }

    @Override
    public MongoTemplate mongoTemplate(
            MongoDatabaseFactory databaseFactory,
            MappingMongoConverter converter) {
        //
        MongoTemplate mongoTemplate =
                new MongoTemplate(databaseFactory, converter);

        // [데이터 안정성] 과반수 노드 복제 확인으로 Primary 이탈 시 데이터 롤백 가능성 감소
        mongoTemplate.setWriteConcern(WriteConcern.MAJORITY);

        // [읽기 가용성] Primary 우선, 전환 중에는 Secondary 조회 허용
        mongoTemplate.setReadPreference(
                ReadPreference.primaryPreferred());

        return mongoTemplate;
    }

    private MongoClient getUriMongoClient() {
        //
        ConnectionString connectionString =
                new ConnectionString(dbUri);

        MongoClientSettings settings =
            MongoClientSettings.builder()
                .applyConnectionString(connectionString)
                .applyToSocketSettings(socketBuilder ->
                    socketBuilder
                        // TCP 초기 연결 대기 시간 제한
                        .connectTimeout(3, TimeUnit.SECONDS)

                        // 연결 후 응답 대기 시간 제한
                        .readTimeout(5, TimeUnit.SECONDS)
                )
                .applyToClusterSettings(clusterBuilder ->
                    clusterBuilder
                        // 사용 가능한 Replica Set 노드 탐색 대기 시간 제한
                        .serverSelectionTimeout(
                            5, TimeUnit.SECONDS)
                )
                // MongoDB 버전 변경 시 API 호환성 확인
                .serverApi(ServerApi.builder()
                    .version(ServerApiVersion.V1)
                    .strict(true)
                    .deprecationErrors(true)
                    .build())
                .build();

        return MongoClients.create(settings);
    }
}

5. 각 설정의 적용 목적

`connectTimeout`은 개별 서버와 TCP 연결을 만드는 대기 시간을 제한합니다. 이번 환경에서는 3초로 설정해 연결 불가 서버에서 오래 대기하지 않도록 했습니다. `readTimeout`은 연결 후 서버 응답 대기 시간을 제한하며 5초로 설정했습니다. 정상 쿼리가 이를 자주 넘는다면 Timeout을 늘리기 전에 쿼리와 인덱스를 점검할 필요가 있습니다.

`serverSelectionTimeout`은 드라이버가 현재 클러스터에서 요청을 처리할 서버를 찾는 시간을 제한합니다. Primary 전환이나 일부 멤버의 일시적 이탈을 고려해 5초로 설정했습니다. 세 Timeout은 대상이 다릅니다. `connectTimeout`은 개별 서버와의 연결, `serverSelectionTimeout`은 클러스터 내 서버 선택, `readTimeout`은 연결 후 응답 대기를 제어하며, 이번의 3초·5초 값은 운영 환경에 맞춘 값이지 모든 시스템의 권장값은 아닙니다.

`WriteConcern.MAJORITY`는 쓰기가 과반수 멤버에 반영된 것을 기준으로 성공을 판단해 장애조치 시 데이터 롤백 위험을 낮추기 위한 설정입니다. `ReadPreference.primaryPreferred()`는 일반 조회에서 데이터 최신성을 우선하면서 Primary를 사용할 수 없는 짧은 구간에는 Secondary 조회를 허용합니다. 다만 대용량 벌크·배치 조회처럼 Primary 부하 분산이 중요한 작업은 `secondaryPreferred` 등 별도의 읽기 정책을 적용할 수 있습니다. Read Preference는 서비스 특성에 따라 선택해야 합니다. Server API V1의 strict/deprecationErrors는 장애조치 기능은 아니지만 서버 버전 변경 시 API 호환성 문제를 확인하는 데 도움이 됩니다.

6. 다른 유지보수 작업으로 적용 범위 확장

이번 사례를 검토하면서 버전 업그레이드 외의 유지보수 상황도 함께 고려했습니다. 대량 데이터 삭제 후 실제 디스크 공간 회수가 필요한 경우에는 환경과 버전에 따라 `compact` 같은 작업을 검토할 수 있습니다. 핵심은 특정 유지보수 기능이 아니라 작업 중 클러스터 멤버의 역할이나 가용성이 달라질 수 있다는 점입니다. 버전 업그레이드, Primary 전환, 노드 유지보수 모두 애플리케이션에는 일부 서버의 일시적 접속 불가로 나타날 수 있습니다.

따라서 작업별 예외 처리보다 클러스터의 일부 멤버가 제외되어도 계속 동작하거나, 정해진 시간 안에 명확히 실패하도록 접속 정책을 구성하는 것이 중요합니다.

7. Health Check와 배포 과정에서 장애 확대

신규 WAS가 MongoDB 연결을 완료하지 못한 상태에서 Kubernetes Health Check와 배포가 진행되며 기존 서비스가 종료되어 장애 범위가 커졌습니다. 다만 실제 대응에서 Health Check 설정은 변경하지 않았습니다. 직접적인 개선 대상은 MongoDB 연결 Timeout, 서버 선택, DNS 캐시와 읽기·쓰기 정책이었습니다.

8. 설정 적용 결과 및 사례에서 얻은 점

설정 적용 후 현재까지 동일한 문제는 재발하지 않았습니다. 다만 배포와 클러스터 상태 변화가 겹쳐야 나타나는 드문 조건이므로 미재발만으로 효과를 단정할 수는 없습니다. 개선의 핵심은 드라이버와 JVM 기본 동작에만 의존하지 않고, 서비스가 허용할 연결 대기 시간과 클러스터 상태 변화에 대한 정책을 명시한 것입니다.

이번 장애는 Atlas에서 발생했지만 교훈은 Atlas에 한정되지 않습니다. Replica Set 등 여러 멤버로 구성된 MongoDB에서는 유지보수, 장애조치, Primary 변경으로 일부 멤버를 일시적으로 사용할 수 없는 상황을 고려해야 합니다. 고가용성 DB를 사용한다고 애플리케이션의 가용성이 자동으로 보장되지는 않습니다. 클라이언트가 적절한 서버를 선택하고 제한된 시간 안에 성공 또는 실패를 판단할 수 있어야 하며, 이를 위해 연결 Timeout, server selection, DNS 캐시, Read/Write 정책을 함께 점검할 필요가 있습니다.

Daniel(K)

Site footer