1. 클라이언트-복제 셋 연결 동작
- 몽고DB 드라이버는 서버가 독립 실행형 몽고DB 인스턴스든 복제 셋이든 관계없이 몽고DB 서버와의 통신을 관리하도록 설계됨
- 복제 셋이면 기본적으로 드라이버는 Primary에 연결되고 모든 트래픽을 Primary에 라우팅함
- 애플리케이션은 복제 셋이 조용히 백그라운드에서 대기 상태를 유지하는 동안 마치 독립 실행형 서버와 통신하듯이 읽기와 쓰기를 수행할 수 있음
- 복제 셋에 대한 연결은 단일 서버에 대한 연결과 비슷함
- 드라이버에 MongoClient를 사용하고, 연결할 드라이버를 위한 시드 목록 (서버 목록)을 제공하면 됨
- 드라이버는 시드에 연결되면 다른 멤버들을 발견하므로 시드 목록에 모든 멤버를 나열할 필요는 없음
"mongodb://server-1:27017, server-2:27017, server-3:27017"
- 추가적인 복원력을 제공하려면 DNS Seedlist 연결 형식을 사용해 애플리케이션 복제 셋에 연결하는 방법을 지정해야 함
- DNS 사용의 장점은 클라이언트를 재구성할 필요 없이 몽고DB 복제 셋 멤버를 호스팅 하는 서버를 순환적으로 변경할 수 있다는 점
- 모든 몽고DB 드라이버는 서버 검색 및 모니터링 (SDAM) 사양을 준수함
- 복제 셋의 토폴로지를 지속적으로 모니터링해 애플리케이션이 셋의 모든 멤버에 도달하는 기능에서 변화를 감지하고 드라이버는 어떤 멤버가 Primary인지 알기 위해 셋을 모니터링
- 복제 셋의 목적은 네트워크 파티션이나 서버가 다운될 때도 데이터의 가용성을 높이는 것
- 일반적으로 복제 셋은 애플리케이션이 데이터를 계속 읽고 쓰도록 새로운 Primary를 선출해 문제에 적절히 대응함
- Primary가 다운되면 드라이버는 자동으로 새로운 Primary를 찾고, 가능한 한 빨리 그 Primary로 요청을 라우팅함
- 그러나 도달할 수 있는 Primary가 없는 동안에는 애플리케이션이 쓰기를 수행할 수 없음
- Primary를 선출하는 동안 혹은 도달할 수 있는 멤버 모두 Primary가 될 수 없으면 동안에 이용 가능한 Primary가 없을 수 있음
- 기본적으로 드라이버는 해당 기간 동안 읽기 및 쓰기 요청을 처리하지 않음
- 애플리케이션에서 필요하다면 읽기 요청에 Secondary를 사용하도록 드라이버를 구성할 수 있음
- 몽고DB 드라이버는 복제셋(replica set) 환경에서 장애 조치(failover) 상황이 발생할 때, 즉 기존 Primary 서버가 내려가고 새로운 Primary가 선출되는 과정을 최대한 사용자로부터 투명하게 감추려고 설계되어 있지만 현실적으로는 드라이버가 모든 장애 조치 과정을 완벽하게 숨기지는 못함
- 첫째로 드라이버는 오랫동안 Primary의 부재만을 숨길 수 없으며
- 둘째로 드라이버는 Primary가 다운됨을 연산 실패로 알게 될 때가 많고, 이는 Primary가 다운되기 전에 해당 연산을 수행했는지 여부를 드라이버가 알지 못한다는 의미
- 이에 따라 문제가 발생했을 때 처리할 전략이 필요하며 올바른 전략은 최대 한 번만 재시도하는 방법
- 몽고DB 3.6부터 서버와 모든 몽고DB 드라이버는 재시도 가능한 쓰기 옵션을 지원함
- 재시도 가능한 쓰기라면 드라이버는 자동으로 최대 한 번 재시도하는 전략을 따름
- 명령 오류는 클라이언트 측 처리를 위해 애플리케이션에 반환됨
- 네트워크 오류는 일반적인 상황에서 Primary 선출을 수용할 수 있도록 적절한 지연 후 한 번 재시도됨
- 재시도 가능한 쓰기를 설정하면 서버는 각 쓰기 연산에 대해 고유한 식별자를 유지하고, 따라서 이미 성공한 명령을 드라이버가 재시도하는 시기를 확인할 수 있음
- 쓰기를 다시 적용하는 대신 쓰기가 성공함을 나타내는 메시지를 반환함으로써 일시적인 네트워크 문제로 인한 문제를 극복함
2. 쓰기 시 복제 대기하기
- 애플리케이션의 요구 사항에 따라, 모든 쓰기가 서버에서 확인되기 전에 대부분의 복제 셋에 복제되도록 요구할 수 있음
- 드물게 복제 셋의 Primary가 중단되고 새로 선출된 Primary가 마지막 쓰기를 이전 Primary에 복제하지 않았을 때는, 이전 Primary가 다시 Primary가 될 때 해당 쓰기가 롤백됨
- 복구할 수 있지만 수동 개입이 필요함
- 많은 애플리케이션에서 롤백되는 쓰기의 수가 적으면 문제가 되지 않지만 다른 애플리케이션에서는 쓰기 롤백을 피해야 함
- 애플리케이션이 Primary에 쓰기를 보낸다고 가정했을 때 쓰기가 작성됐다는 확인을 받지만 Secondary가 해당 쓰기를 복제하기 전에 Primary가 손상됨
- 애플리케이션은 해당 쓰기에 접근할 수 있다고 생각하지만 복제 셋의 현재 멤버는 쓰기의 사본이 없음
- 어느 시점에서 Secondary가 Primary로 선출되고 새 쓰기를 수행할 수 있음
- 이전 Primary가 다시 선출되면 현재 Primary에 없는 쓰기를 발견하며 이를 바로잡기 위해 현재 Primary의 연산 순서와 일치하지 않는 쓰기를 실행 취소함
- 이러한 연산은 없어지지 않고 현재 Primary에 수동으로 작용돼야 하는 특수한 롤백 파일에 기록됨
- 몽고DB는 이러한 쓰기를 자동으로 적용할 수 없는데 이는 충돌 이후 발생한 다른 쓰기와 충돌할 수 있기 때문
- 따라서 쓰기는 관리자가 현재 Primary에 롤백 파일을 적용할 때까지 본질적으로 사라짐
- 과반수에 쓰기를 수행하면 앞서 설명한 상황을 방지할 수 있음
- 애플리케이션이 쓰기가 성공했다는 확인을 받으면, 새 Primary는 선출되려면 쓰기 사본이 있어야 함
- Primary가 손상되기 전에 쓰기가 복제 셋의 과반수로 전파되지 않았기 때문에 애플리케이션이 서버로부터 승인을 받지 못하거나 오류를 받으면 다시 시도해야 한다는 것을 알게 됨
- 따라서 복제 셋에 어떤 일이 발생하든 쓰기가 지속되려면 각 쓰기가 복제 셋 멤버 과반수에 전파돼야 하며 이때 writeConcern을 사용
- 쓰기 결과 확인 과반수와 복제 셋 선출 프로토콜을 승인된 쓰기가 있는 최신 Secondary만 Primary로 선출되게 하며 이러한 방식은 롤백이 발생하지 않도록 보장함
- 시간 제한 옵션과 더불어 조정 가능한 설정이 있어 애플리케이션 계층에서 장기 실행되는 쓰기를 감지하고 플래그를 지정할 수 있음
2.1 "w"에 대한 다른 옵션
- 앞선 예제에서 사용했던 "majority"가 유일한 writeConcern 옵션이 아님
- 몽고DB는 다음처럼 "w"에 숫자를 전달함으로써 몇 개의 서버에 복제할지 임의로 명시할 수 있음
- 아래 예제는 두 멤버에 쓰기가 있을 때까지 대기함 (Primary 하나, Secondary 하나)
- "w" 값은 Primary를 포함하므로 n 개의 Secondary에 쓰기를 전달하기 위해서는 "w"를 n + 1로 설정해야 함
3. 사용자 정의 복제 보증
- 복제 셋의 과반수에 쓰기를 하면 `안전`하다고 여겨지지만 어떤 복제 셋은 요구 사항이 더 복잡할 수 있음
- 데이터 센터마다 최소 한 개의 서버 혹은 숨겨지지 않은 노드의 과반수에 쓰기를 수행하도록 보장할 수 있음
- 복제 셋은 어떤 서버의 조합이 필요하든 관계없이 복제를 보장하기 위해 사용자 규칙을 만들어 "getLastError"에 넘겨주게 해 줌
3.1 데이터 센터당 하나의 서버 보장하기
- 데이터 센터 간의 네트워크 문제는 데이터 센터 내의 문제보다 훨씬 더 일반적이며 여러 데이터 센터에 걸쳐 서버들이 동등하게 영향을 받기보다는 한 개의 데이터 센터 전체가 오프라인이 될 가능성이 더 높음
- 따라서 데이터 센터에 특화된 쓰기 로직이 필요할 수 있음
- 성공 통보를 받기 전에 모든 데이터 센터에 쓰기를 보장하는 것은, 오프라인이 돼가는 데이터 센터에 의해 수행된 쓰기의 경우, 다른 데이터 센터 모두 로컬 복제본을 적어도 한 개 가짐을 의미
- 이를 설정하려면 먼저 멤버를 데이터 센터별로 분류해야 하며 이를 위해 복제 셋 구성에 "tags" 필드를 추가로 수행해야 함
- "tags" 필드는 객체이며, 각 멤버는 여러 태그를 가질 수 있음 i.g. {"dc": "uss-east", "quality": "high"}와 같은 태그 필드는 "us-east" 데이터 센터의 `고성능` 서버를 의미함
- 두 번째 단계로, 복제 셋 구성에 "getLastErrorMode" 필드를 생성해 추가하면 됨
- 복제본 구성에서 "getLastErrorModes"의 경우 각 규칙의 형식은 "name": {"key": number}}
- "name"은 규칙명이며, 클라이언트가 getLastError를 호출할 때 이름을 사용하므로 클라이언트가 이해할 수 있는 방식으로 해당 규칙이 무엇을 수행하는지 적어둬야 함
- "key" 필드는 태그에서의 키 필드이며 예제에서 키는 "dc"가 됨
- number는 규칙을 충족하는 데 필요한 그룹의 개수이며 number는 항상 `number 개의 그룹 각각에서 적어도 하나의 서버`를 의미
- "getLastErrorModes"는 복제 셋 구성의 하위 객체인 "settings"에 있으며, 복제 셋의 선택적인 설정을 몇 가지 포함시키며 이제 쓰기에 해당 규칙을 사용할 수 있음
3.2 숨겨지지 않은 멤버의 과반수 보장하기
- 숨겨진 멤버는 종종 이류 멤버로 여겨짐
- 사용자는 이러한 멤버를 위해 장애를 복구하지 않으며 거기서 어떤 읽기를 수행하지도 않음
- 사용자는 숨겨지지 않은 멤버가 쓰기를 받았다는 데만 신경 쓰고, 숨겨진 멤버는 스스로 해결하도록 둬야 함
- 이해를 돕기 위해, 다섯 개의 멤버(host0부터 host4까지)로 구성된 복제셋을 예로 들고 이 중 host4는 숨겨진(hidden) 멤버로 설정되어 있다고 가정
- 이를 위한 규칙을 만들기 위해서는 먼저 숨겨진 멤버 각각을 태깅해야 하는데 숨겨진 멤버인 host4는 태깅되지 않았다고 가정
- 이후 서버의 과반수에 대한 규칙을 추가하고 애플리케이션에 해당 규칙을 사용할 수 있음
- 규칙 적용 시 숨겨지지 않은 멤버 중 적어도 세 개가 쓰기를 가질 때까지 대기함
3.3 기타 보장 생성하기
- 사용자가 생성할 수 있는 규칙에는 제한이 없으며 사용자 정의 복제 규칙을 만들려면 다음 두 단계를 거쳐야 함
- key-value 쌍을 할당해서 멤버들을 태깅하
- 생성한 분류 체계에 기반해 규칙을 생성하는데 규칙의 형태는 항상 {"name": {"key": number}}와 같고, 쓰기가 성공하기 전에 number 개의 그룹에서 적어도 하나의 서버는 쓰기를 가져야 함
- 이제 위 규칙을 getLastErrorModes에서 사용할 수 있음
- 복제 규칙을 이해하고 세부적으로 구성하는 것은 복제를 유연하고 강력하게 제어할 수 있는 방법이지만, 특별한 복제 요구사항이 없다면 단순히 "w": "majority" 옵션만 사용해도 안전하게 복제를 운용할 수 있음
4. 세컨더리로 읽기 전송
- 기본적으로 드라이버는 모든 요청을 Primary로 라우팅함
- 일반적으로 사용자가 원하는 바지만, 드라이버에서 읽기 선호도를 설정해 다른 옵션을 구성할 수 있음
- 읽기 선호도를 통해 쿼리가 보내져야 하는 서버의 타입을 명시할 수 있음
- 읽기 요청을 Secondary에 보내면 일반적으로 좋지 않음
- 몇몇 특정 상황에서는 의미가 있지만, 일반적으로 모든 트래픽은 Priamry로 전송해야 함
- Secondary로 읽기를 전송하려고 고려한다면 그전에 장단점을 신중히 생각해봐야 함
4.1 일관성 고려 사항
- 매우 일관된 읽기가 필요한 애플리케이션은 Secondary로부터 읽기를 수행하면 안 됨
- Secondary는 보통 Primary의 몇 ms 이내에 있어야 하지만 이는 보장되지 않음
- 때때로 Secondary는 부하, 잘못된 구성, 네트워크 오류 등의 문제로 인해 분, 시간, 심지어 일 단위로 뒤처질 수 있음
- 클라이언트 라이브러리는 Secondary가 얼마나 최신인지 모르기 때문에 클라이언트는 가까이 훨씬 뒤처진 Secondary에 쿼리를 전송함
- 클라이언트 읽기로부터 Secondary를 숨길 수 있지만 이는 수동 프로세스이므로 애플리케이션에서 최신 데이터가 필요하다면 Secondary에서 읽으면 안 됨
- 애플리케이션이 자기 자신의 쓰기를 읽어야 한다면 쓰기가 이전처럼 "w"를 이용해 모든 Secondary에 대해 복제를 대기하지 않는 한, 읽기 요청을 Secondary에 보내면 안 됨
- 그렇지 않으면 애플리케이션은 성공적으로 쓰기를 수행하고 값을 읽으려 하지만 값이 복제되기 전에 읽기를 Secondary로 보내기 때문에 해당 값을 찾을 수 없음
- 클라이언트는 복제가 연산을 복사하는 속도보다 빠르게 요청을 발행할 수 있음
- 읽기 요청을 항상 Primary로 보내려면 읽기 선호도를 primary로 설정하는 것을 권장
- 만약 Primary가 없거나 Primary가 다운되면 애플리케이션이 쿼리 할 수 없지만
- 애플리케이션이 장애 조치나 네트워크 분할 동안 다운타임에 대처할 수 있을 때 혹은 실효 데이터를 받아오는 것이 용납되지 않을 때는 받아들일만한 옵션
4.2 부하 고려 사항
- 많은 사용자가 부하를 분산하기 위해 읽기를 Secondary로 전송하지만 이러한 확장법은 위험함
- 뜻하지 않게 시스템에 과부하를 유발할 수 있고, 과부하가 발생하면 회복하기 어렵기 때문에 위험
- 과부하는 복제가 느려지도록 하며 남은 Secondary 역시 뒤처질 수 있음
- 갑작스럽게 멤버가 다운되거나 지연되고, 모두 과부하에 걸릴 가능성이 있음
4.3 세컨더리에서 읽기를 하는 이유
- 몇몇 경우에는 애플리케이션 읽기를 Secondary로 전송하는 것이 합리적
- ex) Primary가 다운되더라도 애플리케이션이 지속적으로 읽기 작업을 수행하기를 원할 때 읽기를 Secondary에 분산하는 가장 일반적인 경우
- 복제 셋이 Primary를 잃으면 사용자는 임시로 읽기 전용 모드를 원하며 읽기 선호도를 primaryPreferred라고 함
- Secondary로부터의 읽기와 관련된 일반적인 문제로, 지연율이 낮은 읽기가 중요함
- nearest를 읽기 선호도로 지정함으로써, 드라이버에서 복제 셋 멤버까지의 평균 핑 시간을 기반으로 지연율이 가장 낮은 멤버에 요청을 라우팅 할 수 있음
- 애플리케이션이 여러 데이터 센터의 같은 도큐먼트 중 지연율이 낮은 멤버에 접근해야 한다면 nearest가 유일한 방법이지만 도큐먼트가 더 위치 기반이라면 샤딩으로 처리할 수 있음
- 애플리케이션에 지연율이 낮은 읽기와 쓰기가 필요하다면 반드시 샤딩을 사용해야 함
- 아직 모든 쓰기를 복제하지 못한 멤버로부터 읽기를 수행하려면 일관성은 가까이 희생해야 하지만 대신 쓰기가 모든 멤버에 복제될 때까지 기다린다면 쓰기 속도를 희생할 수도 있음
- 애플리케이션이 임의의 실효 데이터와 그런대로 동작한다면 secondary 또는 secondaryPreferred 읽기 선호도를 사용할 수 있음
- secondary는 항상 세컨더리에 읽기 요청을 전송하며 이용 가능한 세컨더리가 없으면 읽기 요청을 Primary에 보내지 않고 오류를 발생시킴
- 실효 데이터를 별로 신경 쓰지 않고 Primary를 쓰기에만 사용하는 애플리케이션에서 사용할 수 있지만 데이터 실효에 관한 우려가 있다면 해당 방식을 권장하지 않음
- secondaryPreferred는 Secondary에 읽기 요청을 보내며 이용 가능한 Secondary가 없으면 Primary에 요청을 보냄
- 읽기 부하와 쓰기 부하는 서로 다를 수 있으며, 읽기 작업이 쓰기 작업과는 전혀 다른 데이터를 대상으로 할 때도 있음
- 오프라인 처리를 위해 꽤 많은 인덱스가 필요할 수 있는데 이때 Secondary를 Primary와 다른 인덱스로 설정할 수 있음
- Secondary를 이러한 용도로 사용하려면, 복제 셋 연결 대신 드라이버에서 Secondary로 직접적인 연결을 만듦
- 옵션을 적절히 조합할 수도 있음
- 어느 정도의 읽기 요청이 Primary로부터 발생한다면 primary를 사용
- 다른 읽기는 데이터가 최신이 아니어도 괜찮다면 primaryPreferred를 사용
- 요청에 일관성보다는 낮은 지연율이 필요하다면 nearest를 사용
참고
몽고DB 완벽 가이드 3판 - 한빛미디어
반응형
'DB > 몽고DB 완벽 가이드 3판' 카테고리의 다른 글
[13장] 복제 셋 관리 (0) | 2025.04.26 |
---|---|
[11장] 복제 셋 구성 요소 (0) | 2025.04.24 |
[10장] 복제 셋 설정 (0) | 2025.04.22 |
[9장] 애플리케이션 설계 (0) | 2025.04.21 |
[8장] 트랜잭션 (0) | 2025.04.16 |