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을 사용
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 예제에서는 "majority"라는 쓰기 결과 확인을 지정 | |
try { | |
db.products.insertOne( | |
{ "_id": 10, "item": "envelopes", "qty": 100, type: "Self-Sealing" }, | |
{ writeConcern: { "w": "majority", "wtimeout": 100 }} | |
); | |
} catch (e) { | |
print (e); | |
} | |
// 결과 성공 시 | |
{ "acknowledged": true, "insertedId": 10 } | |
// 쓰기 작업이 복제 셋 멤버 과반수에 복제될 때까지 서버가 응답하지 않을 경우 | |
// 복제되고 나서야 애플리케이션이 쓰기가 성공했다는 승인을 받음 | |
// 지정한 제한 시간 내 쓰기가 성공하지 못하면 발생하는 오류 메시지 | |
writeConcernError({ | |
"code": 64, | |
"errInfo": { | |
"wtimeout": true | |
}, | |
"errmsg": "waiting for replication timed out" | |
}) |
- 쓰기 결과 확인 과반수와 복제 셋 선출 프로토콜을 승인된 쓰기가 있는 최신 Secondary만 Primary로 선출되게 하며 이러한 방식은 롤백이 발생하지 않도록 보장함
- 시간 제한 옵션과 더불어 조정 가능한 설정이 있어 애플리케이션 계층에서 장기 실행되는 쓰기를 감지하고 플래그를 지정할 수 있음
2.1 "w"에 대한 다른 옵션
- 앞선 예제에서 사용했던 "majority"가 유일한 writeConcern 옵션이 아님
- 몽고DB는 다음처럼 "w"에 숫자를 전달함으로써 몇 개의 서버에 복제할지 임의로 명시할 수 있음
- 아래 예제는 두 멤버에 쓰기가 있을 때까지 대기함 (Primary 하나, Secondary 하나)
- "w" 값은 Primary를 포함하므로 n 개의 Secondary에 쓰기를 전달하기 위해서는 "w"를 n + 1로 설정해야 함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
db.products.insertOne( | |
{ "_id": 10, "item": "envelopes", "qty": 100, type: "Self-Sealing" }, | |
{ writeConcern: { "w": "2", "wtimeout": 100 }} | |
); |
3. 사용자 정의 복제 보증
- 복제 셋의 과반수에 쓰기를 하면 `안전`하다고 여겨지지만 어떤 복제 셋은 요구 사항이 더 복잡할 수 있음
- 데이터 센터마다 최소 한 개의 서버 혹은 숨겨지지 않은 노드의 과반수에 쓰기를 수행하도록 보장할 수 있음
- 복제 셋은 어떤 서버의 조합이 필요하든 관계없이 복제를 보장하기 위해 사용자 규칙을 만들어 "getLastError"에 넘겨주게 해 줌
3.1 데이터 센터당 하나의 서버 보장하기
- 데이터 센터 간의 네트워크 문제는 데이터 센터 내의 문제보다 훨씬 더 일반적이며 여러 데이터 센터에 걸쳐 서버들이 동등하게 영향을 받기보다는 한 개의 데이터 센터 전체가 오프라인이 될 가능성이 더 높음
- 따라서 데이터 센터에 특화된 쓰기 로직이 필요할 수 있음
- 성공 통보를 받기 전에 모든 데이터 센터에 쓰기를 보장하는 것은, 오프라인이 돼가는 데이터 센터에 의해 수행된 쓰기의 경우, 다른 데이터 센터 모두 로컬 복제본을 적어도 한 개 가짐을 의미
- 이를 설정하려면 먼저 멤버를 데이터 센터별로 분류해야 하며 이를 위해 복제 셋 구성에 "tags" 필드를 추가로 수행해야 함
- "tags" 필드는 객체이며, 각 멤버는 여러 태그를 가질 수 있음 i.g. {"dc": "uss-east", "quality": "high"}와 같은 태그 필드는 "us-east" 데이터 센터의 `고성능` 서버를 의미함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
> var config = rs.config() | |
> config.members[0].tags = {"dc": "us-east"} | |
> config.members[1].tags = {"dc": "us-east"} | |
> config.members[2].tags = {"dc": "us-east"} | |
> config.members[3].tags = {"dc": "us-east"} | |
> config.members[4].tags = {"dc": "us-west"} | |
> config.members[5].tags = {"dc": "us-west"} | |
> config.members[6].tags = {"dc": "us-west"} |
- 두 번째 단계로, 복제 셋 구성에 "getLastErrorMode" 필드를 생성해 추가하면 됨
- 복제본 구성에서 "getLastErrorModes"의 경우 각 규칙의 형식은 "name": {"key": number}}
- "name"은 규칙명이며, 클라이언트가 getLastError를 호출할 때 이름을 사용하므로 클라이언트가 이해할 수 있는 방식으로 해당 규칙이 무엇을 수행하는지 적어둬야 함
- "key" 필드는 태그에서의 키 필드이며 예제에서 키는 "dc"가 됨
- number는 규칙을 충족하는 데 필요한 그룹의 개수이며 number는 항상 `number 개의 그룹 각각에서 적어도 하나의 서버`를 의미
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
> config.settings = {} | |
> config.settings.getLastErrorModes = [{"eachDC": {"dc": 2}}] | |
> rs.reconfig(config) |
- "getLastErrorModes"는 복제 셋 구성의 하위 객체인 "settings"에 있으며, 복제 셋의 선택적인 설정을 몇 가지 포함시키며 이제 쓰기에 해당 규칙을 사용할 수 있음
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
db.products.insertOne( | |
{ "_id": 10, "item": "envelopes", "qty": 100, type: "Self-Sealing" }, | |
{ writeConcern: { "w": "eachDC", wtimeout: 1000 }} | |
); |
3.2 숨겨지지 않은 멤버의 과반수 보장하기
- 숨겨진 멤버는 종종 이류 멤버로 여겨짐
- 사용자는 이러한 멤버를 위해 장애를 복구하지 않으며 거기서 어떤 읽기를 수행하지도 않음
- 사용자는 숨겨지지 않은 멤버가 쓰기를 받았다는 데만 신경 쓰고, 숨겨진 멤버는 스스로 해결하도록 둬야 함
- 이해를 돕기 위해, 다섯 개의 멤버(host0부터 host4까지)로 구성된 복제셋을 예로 들고 이 중 host4는 숨겨진(hidden) 멤버로 설정되어 있다고 가정
- 이를 위한 규칙을 만들기 위해서는 먼저 숨겨진 멤버 각각을 태깅해야 하는데 숨겨진 멤버인 host4는 태깅되지 않았다고 가정
- 이후 서버의 과반수에 대한 규칙을 추가하고 애플리케이션에 해당 규칙을 사용할 수 있음
- 규칙 적용 시 숨겨지지 않은 멤버 중 적어도 세 개가 쓰기를 가질 때까지 대기함
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 태깅 | |
> var config = rs.config() | |
> config.members[0].tags = [{"normal": "A"}] | |
> config.members[1].tags = [{"normal": "B"}] | |
> config.members[2].tags = [{"normal": "C"}] | |
> config.members[3].tags = [{"normal": "D"}] | |
// 과반수에 대한 규칙 추가 | |
> config.settings.getLastErrorModes = [{"visibleMajority": {"normal": 3}}] | |
> rs.reconfig(config) | |
// 애플리케이션에 규칙 적용 | |
db.products.insertOne( | |
{ "_id": 10, "item": "envelopes", "qty": 100, type: "Self-Sealing" }, | |
{ writeConcern: { "w": "visibleMajority", wtimeout: 1000 }} | |
); |
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판' 카테고리의 다른 글
[14장] 샤딩 소개 (0) | 2025.05.16 |
---|---|
[13장] 복제 셋 관리 (0) | 2025.04.26 |
[11장] 복제 셋 구성 요소 (0) | 2025.04.24 |
[10장] 복제 셋 설정 (0) | 2025.04.22 |
[9장] 애플리케이션 설계 (0) | 2025.04.21 |