데이터 중심 애플리케이션 설계 5장 정리

백종현·2023년 8월 28일
1

복제 : 네트워크로 연결된 여러 장비에 동일한 데이터의 복사본 유지

필요 이유

  • 지리적으로 사용자와 가깝게 하여 지연시간을 줄임
  • 시스템 일부에 장애 발생해도 지속적으로 동작 → 내결함성, 고가용성 달성
  • 읽기 질의를 제공하는 장비의 수 확장 → 읽기 처리량 향상

복제 알고리즘

  • 단일 리더(single-leader)
  • 다중 리더(multi-leader)
  • 리더가 없음(leaderless)

리더와 팔로워

모든 복제 서버에 모든 데이터가 있다는 사실을 어떻게 보장할까, 일반적으로 리더 기반 복제

리더(leader)

= 마스터, 프라이머리(primary)
클라이언트의 쓰기, 읽기 요청을 처리
쓰기 기록 후 팔로워에게 이를 전달

팔로워(follower)

= 읽기 복제 서버(read replica), 슬레이브, 2차(secondary), 핫 대기(hot standby)
리더가 보낸 데이터 변경 로그, 변경 스트림을 전달 받아 데이터 복제본을 갱신
클라이언트의 읽기 요청 만을 처리

관계형 데이터베이스, NoSQL 및 더 나아가 메시지 브로커에서도 사용된다.

동기식 대 비동기식 복제

동기식 복제

  • 리더가 팔로워의 쓰기 수신에 대한 응답을 확인하기 위해 대기한다.
  • 확인이 끝나면 사용자에게 성공을 보고 후 해당 쓰기를 보여준다.

장단점

  • 리더와 팔로워의 일관성을 보장한다.
  • 팔로워가 (죽거나 네트워크 등의 문제로) 응답하지 않을 시 쓰기가 처리될 수 없다.
  • 리더는 모든 쓰기를 차단(block)하고 팔로워가 사용가능할 때까지 기다려야 한다.

비동기식 복제

  • 리더가 팔로워의 쓰기 수신에 대한 응답을 기다리지 않는다.
  • 일관성은 떨어지지만, 사용자 응답 지연 시간이 적고 고가용성 제공

반동기식 복제

  • 모든 팔로워가 동기식 복제 방식을 사용할 수는 없다. 하나의 노드만 고장나도 전체 시스템이 마비.
  • 팔로워 하나는 동기식, 나머지는 비동기 식으로 구성하는 것을 의미

리더 기반 복제에서는 보통 완전히 비동기식으로 구성한다.

새로운 팔로워 설정

  • 데이터베이스를 잠그지 않고 리더의 데이터베이스 스냅샷을 일정시점에 가져온다. (MySQL의 경우 innobackupex)
  • 스냅샷을 새로운 팔로워 노드에 복사한다.
  • 팔로워는 리더에 연결해 스냅샷 이후 발생한 모든 데이터 변경 내역을 요청한다.
    PostgresQL -> 로그 일련번호 (log sequence)
    MySQL -> 이진로그 좌표 (binlog coordinate)
  • 요청한 데이터 변경 미처리분(backlog)을 모두 처리하면 팔로워가 리더를 따라잡았다고 말한다.

노드 중단 처리

장애와, 커널 보안 패치와 같은 유지보수로 인해 시스템의 노드가 중단될 수 있다.

팔로워 장애: 따라잡기 복구

로그를 이용하여 복구를 시작할 수 있다.

  • 로그에서 마지막으로 처리된 트랜잭션을 찾는다.
  • 해당 트랜잭션 이후의 데이터 변경 내역을 리더에 요청
  • 요청한 변경 내역을 모두 적용하면 리더를 따라잡아 복구가 완료된다.

리더 장애: 장애 복구

팔로워 중 하나를 새로운 리더로 승격, 클라이언트가 새로운 리더로 쓰기를 전송하기 위해 재설정이 필요하며, 다른 팔로워는 새로운 리더로부터 데이터 변경을 소비하기 시작해야 한다. 이는 매우 까다롭다.

자동 장애 복구를 위한 단계

  1. 리더가 장애인지 판단한다.
  • 판단할 수 있는 확실한 방법은 없음 → 보통 타임아웃을 사용
  1. 새로운 리더를 선택.
  • 복제 노드들이 새로운 리더를 선출
  • 또는 제어 노드(controller node)가 새로운 리더를 임명
  1. 새로운 리더 사용을 위해 시스템 재설정
  • 클라이언트의 쓰기 요청, 팔로워의 데이터 변경 로그 재설정
  • 이전 리더가 복구되는 경우 이전 리더가 새로운 리더를 인식하고 자신은 팔로워

자동 장애복구는 잘못될 수 있는 것 투성이다. 이전에 MySQL의 팔로워가 리더로 승격되었는데 이 팔로워가 이전 리더의 쓰기 내역을 완벽히 갱신하지 못한 Github의 사례가 있다. (레디스와 MySQL간의 데이터 불일치 발생).

또한 비동기식 복제 사용 시 새로운 리더는 이전 리더의 최신 쓰기 중 일부를 받지 못할 수 있는 내구성에 문제가 생길 수 있다.

복제 로그 구현

리더 기반 복제는 내부적으로 어떻게 동작할까?

구문(Statement) 기반 복제

  • 요청받은 구문을 기록하고 쓰기를 실행한 다음 구문을 팔로워에게 전송
    • RDB : INSERT, UPDATE, DELETE …
  • 비결정적인 요인에 의해 복제가 깨질 수 있다.
    • NOW(), RAND() 등은 복제 서버마다 다른 값을 생성할 가능성이 존재
    • 자동증가 컬럼을 사용하거나, 기존 데이터에 의존하는 경우(WHERE) 정확히 같은 순서로 실행
    • 부수효과를 가진 구문의 경우 부수효과가 완벽히 결정적이어야 (비결정적이지 않아야) 모든 팔로워에서 그 효과도 동일하다.
  • 대안은 리더가 구문 기록 시 비결정적 함수 호출을 고정 값을 반환하도록 대체 -> MySQL의 경우는 비결정성 요인이 있으면 로우 기반 복제 방식으로 변경

쓰기 전 로그(WAL, write-ahead log) 배송

  • 일반적으로 데이터베이스의 모든 쓰기는 로그에 기록이 된다.
  • 리더가 로그를 팔로워에게 전송하고, 팔로워는 이 로그를 처리함으로써 복제한다.
  • 로그는 제일 저수준의 데이터를 기술함
    • 디스크 블록에서 어떤 데이터를 변경했는 지와 같은 상세 정보 포함
    • 복제 프로세스가 저장소 엔진과 밀접하게 연관된다. (저수준의 데이터를 기술하는 로그를 사용하므로)
    • 리더와 팔로워가 동일한 소프트웨어 버전에서 실행되어야 한다.
      따라서, 소프트웨어 업그레이드 시 중단 시간이 필요하다.
    • PostgresQL, Oracle에서 이 방식 사용

논리적(로우 기반) 로그 복제

  • 로그를 저장소 엔진과 분리하기 위한 대안으로 복제와 저장소 엔진에 각기 다른 로그 형식을 사용
  • 논리적 로그(logical log)
    • 이와 같이 복제에서 사용하는 로그를 저장소 엔진의 물리적 데이터 표현과 구별하여 부른다.
  • 논리적 로그는 외부 애플리케이션이 파싱하기 쉽다.
    • 데이터 웨어하우스와 같은 외부 시스템에 데이터베이스 내용 전송 시 유용

참조 : 오퍼레이션 로깅(operation logging)으로도 불리는데, 물리적인 로깅이 결과 값을 기록하는 방식이라면 논리적인 로깅은 어떤 일을 했었는가를 기록하는 방식이다. 예를 들어, a = a + 1과 같은 연산을 로깅할 때 이전 값 0, 이후 값 1을 물리적으로 기록할 수도 있고, a = a + 1 이라는 연산 그 자체를 기록
https://d2.naver.com/helloworld/407507

트리거 기반 복제

  • 많은 RDBMS : 트리거, 스토어드 프로시저 제공
  • 트리거(trigger) : 사용자 정의 애플리케이션 코드를 등록할 수 있다, 데이터 변경 시(쓰기 트랜잭션) 자동으로 실행된다.

장단점

  • 오버헤드가 심함. (매번 트리거가 작동되야함)
  • 유연성 때문에 매우 유용 (아마 삭제나 삽입이 편리하기 때문일 것으로 추측)

복제 지연 문제

읽기 확장(read-scaling) 아키텍처

  • 하나의 리더와 여러 팔로워로 구성
  • 리더는 읽기 + 쓰기, 팔로워는 읽기 요청 처리

장점
보통 읽기 요청이 대다수이고 쓰기 요청은 적기 때문에 웹서비스에 적합하고, 복제의 목표 중 확장성과 지연시간 단축 달성이 가능하다.

단점
거의 비동기식 복제 방식에서만 동작 (동기식 복제는 단일 노드의 장애, 네트워크 중단이 전체 시스템의 쓰기를 마비)

  • 비동기 팔로워에서 데이터를 읽을 경우 지난 데이터를 읽을 수 있음 → 데이터 불일치 발생.
  • 불일치는 일시적이다. 결국 팔로워는 리더를 따라잡게 된다. → 최종적 일관성은 이룰수 있게됨.

위와 같은 경우 복제지연의 발생. 복제 지연으로 인해 발생 가능한 사례는 다음과 같다.

자신이 쓴 내용 읽기


쓰기 후 읽기(read-after-write) 일관성

  • 자신의 쓰기 읽기 일관성이 필요함.
    • 사용자가 페이지를 리로딩하면 자신이 제출한 모든 갱신을 볼수 있음을 보장한다.
    • 다른 사용자가 제출한 것에 대해서는 보장 X.

위 사진과 같은 경우는 이것이 가능해야하는데, 어떻게 구현할까?

  • 사용자가 수정한 내용을 읽을 때에는 리더에서 읽는다. 이 외의 경우는 팔로워에서 읽는다.
  • 대부분의 내용에 대해 사용자 편집이 가능하면 첫 번째 방식은 적합하지 않다 (대다수가 리더에서 읽히기 때문). 따라서 레코드의 마지막 갱신 시각을 기준으로(ex. 1분 이내) 시간으로 리더 읽기 여부를 구분한다.
  • 클라이언트가 기억하는 가장 최근 쓰기의 타임스탬프를 사용하기

추가적으로 멀티 디바이스 사용자 환경에서는 다음과 같은 문제를 추가적으로 고려해야한다. (동일 디바이스건 말고도 고려를 해야하는 경우)

  • 복제 서버가 여러 데이터센터 간에 분산된 경우
    • 다른 디바이스의 연결이 동일한 데이터센터로 라우팅된다는 보장이 없기 때문에 사용자 디바이스의 요청을 동일한 데이터센터로 라우팅해야한다.

단조 읽기


팔로워 간에도 동일한 쓰기에 대해 갱신 시점의 차이가 존재하기 때문에, 시간이 거꾸로 흐르는 현상이 일어날 수 있다.

단조 읽기(monotonic read)

  • 위와 같은 종류의 이상 현상이 발생하지 않음을 보장하는 것.
  • 이전에 새로운 데이터를 읽은 후에는 예전 데이터를 읽지 않는다.

단조 읽기를 달성하는 방법

  • 각 사용자의 읽기가 항상 동일한 복제 서버에서 수행되도록 한다.
  • 사용자 ID의 해시를 기반으로 복제 서버를 선택
  • 복제 서버가 고장나면 사용자 질의를 다른 복제 서버로 재라우팅할 필요가 있음

일관된 순서로 읽기


잘못 갱신되는 경우, 관찰자 입장에서 일관되지 않은 순서로 읽힐 수 있다.

  • 이러한 종류의 이상현상을 방지하기 위해 일관된 순서로 읽기와 같은 유형의 보장이 필요하다.
  • 일련의 쓰기가 특정 순서로 발생한 경우 다른 사용자에게도 쓰기에 대해 쓰여진 순서대로 읽는 것을 보장해야한다.
  • 인과성의 위반 : 파티셔닝(샤딩)된 데이터베이스에서 발생하는 특징적인 문제

복제 지연을 위한 해결책

  • 쓰기 후 읽기와 같은 강한 보장을 제공하도록 시스템을 설계
  • 특정 종류의 리더에서 읽기를 수행하는 것처럼 애플리케이션이 데이터베이스보다 더 강력한 보장을 제공

트랜잭션의 사용

애플리케이션이 단순해지기 위해 데이터베이스가 더 강력한 보장을 제공하는 방법이다. 애플리케이션 개발자가 미묘한 복제 문제를 걱정하지 않는다.

분산 시스템에서의 트랜잭션

  • 트랜잭션은 가용성과 성능 측면에서 너무 비싸다.
  • 확장 가능한 시스템에서는 최종적 일관성(결과론적으로 일치하는 것)을 선택하는 것이 불가피하다는 주장도 존재
  • 트랜잭션 및 대안 메커니즘에 대해서는 이후 챕터에서 다룰 것

다중 리더 복제

  • 쓰기 처리를 하는 각 노드는 데이터 변경을 모든 노드에 전달하는데 이를 다중 리더 설정

  • 여기서 각 리더는 동시에 다른 리더의 팔로워 역할

  • 모든 쓰기를 해당 리더를 거쳐야 하고, 리더 연결이 불가능한 경우 쓰기 불가능한 단점 보완

  • 각 센터마다 리더가 있으며, 센터 내에 팔로워를 복제한다.

  • 동일한 데이터를 다른 두 개의 데이터센터에서 동시에 변경 가능하므로 쓰기 충돌은 반드시 해소해야한다.

단일리더 VS 다중리더

단일 리더는 쓰기는 인터넷을 통해 리더가 있는 데이터 센터로 이동해야해서 쓰기 지연 발생하나, 다중 리더는 로컬 데이터센터 처리 후 비동기로 다른 데이터 센터 복제

단일 리더는 리더가 있는 데이터센터가 고장 나면 다른 데이터센터의 팔로워를 리더로 승진하나, 각 데이터센터가 독립적으로 동작.

단일 리더는 데이터 센터 내 쓰기는 동기식이기에 (쓰기는 단일 리더가 처리하기 때문에 동기적으로 처리) 데이터 센터 내 연결 문제에 민감하나, 다중 리더는 비동기 복제를 사용해 네트워크 문제에 보다 잘 견딤, 일시적 네트워크 중단에도 쓰기 처리는 진행되기 때문

오프라인 작업과 협업 편집 상황은 쓰기 충돌과 비슷하다.

쓰기 충돌 다루기

다중 리더 복제에서 가장 큰 문제는 쓰기 충돌이다.

동기 대 비동기 충돌 감지

충돌 감지는 동기식으로 만들 수 있으나, 각 복제 서버가 독립적으로 쓰기를 허용하는 장점을 잃게된다.

충돌 회피

  • 충돌 처리가 어려워 충돌 피하는 것이 자주 권장됨
  • 특정 레코드의 모든 쓰기를 동일한 리더에서 처리

일관된 상태 수렴

  • 모든 복제 서버가 동일해야 함이 원칙으로 한다.
  • 수렴(convergent) : 모든 변경이 복제돼 모든 복제 서버에 동일한 최종 값이 전달되게 해야 함

수렴 충돌 해소 방법들

  • 각 쓰기에 고유 ID를 부여해 가장 높은 ID를 가진 쓰기
  • 타임스탬프를 사용하는 경우 최종 쓰기 승리라고도 한다.
  • 데이터 유실 가능성이 있다 (아마 겹칠 가능성이 있어서)
  • 어떻게든 값을 병합한다. (예를 들어 겹치는 경우 B/C 이런식으로 병합해버림)
  • 명시적 데이터 구조에 충돌을 기록해 모든 정보를 보존

사용자 정의 충돌 해소 로직

충돌 해소의 가장 적합한 방법은 애플리케이션에 따라 다르다. 따라서 대부분 다중 리더 복제도구는 애플리케이션 코드를 사용해 충돌 해소 로직 작성한다.

  • 쓰기 수행 중의 충돌 해소 로직 작성법 : 복제된 변경사항 로그에서 데이터베이스 시스템 충돌 감지되면 충돌 핸들러 호출
  • 읽기 수행 중의 충돌 해소 로직 작성법: 충돌 감지 시 모든 충돌 쓰기 저장. 다음 번 읽기 시 여러 데이터 반환. 애플리케이션은 사용자에게 충돌 내용 보여주거나 자동으로 충돌 해소해 결과를 데이터베이스에 기록. 카우치DB가 이렇다.

다중 리더 복제 토폴로지

  • 원형 토폴로지
    각 노드가 하나의 노드로부터 쓰기를 받고, 이 쓰기를 다른 노드에 전달
    MySQL 에서 기본적으로 제공
    노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌
  • 별 모양 토폴로지
    지정된 루트 노드 하나가 다른 모든 노드에 쓰기 전달
    트리로 일반화 가능
    노드 장애 시 노드 간 복제 메시지 흐름에 방해를 줌
  • 전체 연결 토폴로지
    모든 리더가 각자의 쓰기를 다른 모든 리더에 전송
    가장 일반적인 토폴로지
    내결함성이 상대적으로 좋음


다중 리더 복제에게 일부 복제 서버에 쓰기가 잘못된 순서로 도착할 수 있다.

  • 올바른 이벤트 정렬을 위한 버전 벡터 기법으로 해결 가능하나, MySQL에는 충돌을 감지하기 위한 시도조차 하지않는다.
  • 따라서 다중 리더 복제 시스템을 사용하려면 이런 문제를 인지하고 문서를 주의깊게 읽은 다음 데이터 베이스를 철저하게 테스트해봐야 한다.

리더 없는 복제

아마존이 내부 다이나모 시스템에서 사용한 후, 영감을 얻은 리더 없는 복제 모델의 오픈 소스 데이터스토어이다.

  • 일부 데이터 저장소 시스템은 리더의 개념을 버리고 모든 복제 서버가 클라이언트로부터 쓰기를 직접 하는 방식을 사용하기도 함. (클라이언트에게 복제를 위임하는 형식으로 이해)
  • 다이나모 스타일 DB로 리악, 카산드라, 볼드모트 등 오픈소스 데이터스토어가 있음

노드가 다운됐을 때 데이터베이스에 쓰기


1. 다운된 노드에서는 쓰기가 누락되어 오래된(outdated) 값을 읽게 됨
2. 읽기 요청을 병렬로 여러 노드에 전송해 가장 최신 값을 읽어옴
3. 버전 숫자를 통해 읽어온 값 중 최신 값을 결정하여 다운되었던 데이터베이스에 갱신시켜 데이터베이스를 복구.

읽기 복구와 안티 엔트로피

읽기 복구와 안티 엔트로피 복제 계획은 최종적으로 모든 데이터가 모든 복제 서버에 복사된 것을 보장해야한다.

  • 읽기 복구
    • 클라이언트가 여러 노드에서 병렬로 읽기 수행하면 오래된 응답 감지 가능
    • 복제 서버의 오래된 값을 새로운 값으로 기록
    • 값을 자주 읽는 상황에 적합
  • 안티 엔트로피 처리
    • 백그라운드 프로세스와 복제 서버 간 데이터 차이를 찾아 누락된 데이터를 복사
    • 특정 순서로 쓰기를 복사하기 때문에 지연이 있을 수 있음 (??)

정족수

위 사진과 같은 곳에서 복제 서버 세개 중에 두개만 쓰기를 성공해도 성공한 것으로 간주했다. 세 개의 복제 서버 중 하나만 쓰기를 허용한다면 어떻게 해야할까? 어느 범위까지 이를 허용해야할까?

  • 정족수 : 여러 사람의 합의로 운영되는 의사기관에서 의결을 하는데 필요한 최소한의 참석자 수이다.
  • 유효한 읽기와 쓰기를 위한 복제서버 수, 쓰기 성공 노드 수, 질의 노드 수를 나타냄
  • 다이나모 스타일 DB에서는 복제 서버(n), 쓰기 노드(w), 읽기 노드(r) 설정 가능
  • 일반적으로 n 은 3 또는 5 등의 홀수, w = r = (n+1) / 2 (반올림) 설정

동시쓰기 감지

동시에 같은 키를 쓰는 것을 허용하기 때문에 엄격한 정족수를 사용하라도 충돌이 발생한다.

최종 쓰기 승리 (동시 쓰기 버리기)

  • 복제본을 가장 최신 값으로 덮어 쓰는 방법
  • 쓰기에 타임스탬프를 붙여 최신 값을 선택하는 방법 (LWW)
  • 손실 데이터를 허용하지 않는다면 LWW가 부적합

이전 발생

작업 B가 작업 A에 대해 알거나 A에 의존적이거나, 어떤 방식으로든 A를 기반으로 한다면 작업 A는 작업 B의 이전 발생이라 함
작업이 다른 작업보다 먼저 발생하지 않으면 (어느 작업도 다른 작업에 대해 알지 못하면) 동시 작업이라 일컫음

동시, 이전 발생 결정 알고리즘

06.서버가 모든 키에 대한 버전 번호 유지, 버전 번호 증가하며 키를 기록함
클라이언트가 키를 읽을 때는 최신 버전과 덮어쓰지 않은 모든 값 반환

참조: https://www.devkuma.com/docs/data-intensive-application/05/

profile
노력하는 사람

0개의 댓글