designing-data-intensive-applications

Part2 - 분산 데이터

분산된 데이터베이스를 필요로 하는 이유

고부하로 확장

Scale Up

해당 방식의 문제점은 비용이 선형적인 추세보다 훨씬 빠르게 증가한다는 것이다. 또한 병목 현상 때문에 두 배 크기의 장비가 반드시 두 배의 부하를 처리할 수 있는 것은 아니다.

공유 디스크 아키텍처

비공유 아키텍처(shared-nothing)

Scale Out 각 장비나 가상 장비를 노드라고 부른다.

특별한 하드웨어를 필요로 하지 않으므로 가격 대비 성능이 가장 좋은 시스템을 사용할 수 있다. 또한 여러 지리적인 영역에 걸쳐 데이터를 분산해 지연 시간을 줄일 수 있다. 최근엔 클라우드 배포를 활용하여 소규모 회사라도 다중 지역 분산 아키텍처가 실현 가능하다.

복제 대 파티셔닝

복제

Part5 - 복제

복제란

복제가 필요한 이유

복제중인 데이터가 시간이 지나도 변하지 않는다면 복제는 쉽지만, 데이터는 변경 될 가능성이 있기 매우 높다.

변경 데이터를 복제하기 위한 세 가지 알고리즘을 이번 장에서 얘기한다.

복제에는 동기식 복제와 비동기식 복제 중 어떤 것을 사용할지, 잘못된 복제본을 어떻게 처리할 지 고려해야 한다. 이러한 트레이드 오프는 데이트베이스의 설정 옵션이다.

리더와 팔로워

모든 쓰기는 모든 복제 서버에서 처리되어야 한다. 그렇지 않으면 복제 서버는 더 이상 동일한 데이터를 유지할 수 없다. 이 문제를 해결하기 위한 가장 일반적인 해결책은 리더 기반 복제(leader-based replication) 이다.

서버 중 하나를 마스터로 지정한다.

  1. 클라이언트가 데이터베이스에 쓰기를 할 때 요청을 리더에게 보낸다.
  2. 리더는 먼저 로컬 저장소에 새로운 데이터를 기록한다.
  3. 새로운 데이터를 기록할 때 마다 복제 로그(replication log)변경 스트림(change stream) 을 팔로워에게 전달한다.
  4. 팔로워가 리더로부터 로그를 받으면 동일한 순서로 모든 쓰기를 적용하여 복사본을 갱신한다.

읽기를 할 땐 리더 혹은 팔로워에게 질의할 수 있다. 단, 쓰기는 리더에게만 허용된다.

동기식 vs 비동기식 복제

ex) 프로필 이미지를 갱신하는 상황

  1. 클라이언트가 리더에게 갱신 요청을 전송한다.
  2. 리더는 요청을 받은 후 데이터 변경을 팔로워에게 전달한다.
  3. 리더는 클라이언트에게 갱신이 성공했음을 알려준다.

복제는 매우 빠르지만 얼마 정도의 시간이 걸릴지는 보장할 수 없다. 팔로워가 수 분 이상 리더와 떨어질 수 있다.

동기식 장점

만약 여러 노드를 사용 중일 때 하나의 노드만 장애를 발생해도 전체 시스템을 멈추게한다. 그래서 동기식 복제를 사용하려면 하나만 동기식 복제를 사용하고 나머지는 비동기식으로 하는 것을 의미한다. 이렇게 하면 적어도 두 노드에 데이터의 최신 복사본이 있는 것을 보장한다. 이러한 설정을 반동기식(semi-synchronous) 라고 한다.

리더 기반 복제는 비동기식으로 구성한다. 리더가 잘못되고 복구할 수 없을 경우 팔로워에 아직 복제되지 않은 모든 쓰기는 유실된다. 쓰기가 클라이언트에게 확인된 경우에도 지속성을 보장하지 않는다. 장점

새로운 팔로워 설정

클라이언트는 지속적으로 데이터베이스에 기록하고 데이터는 항상 유동적이기 때문에 표준 파일 복사본은 다른 시점에 데이터베이스의 다른 부분을 보게 된다.

새로운 팔로워 노드 설정은 대개 중단시간 없이 수행할 수 있다.

  1. 데이터베이스를 잠그지 않고, 리더의 데이터베이스 스냅숏을 일정 시점에 가져온다.
  2. 스냅숏을 팔로워 노드에 복사한다.
  3. 팔로워는 리더에 연결해 스냅숏 이후 발생한 모든 데이터 변경을 요청한다.
    • 스냅숏이 리더의 복제 로그의 정확한 위치와 연관되어야 한다.
      • postgresql : 로그 일련번호(log sequence number)
      • MySQL : 이진로그 좌표(binlog coordinate)
  4. 팔로워가 스냅숏 이후 데이터 변경의 미처리분을 모두 처리했을 때 따라잡았다고 말한다.
    • 리더에 발생하는 데이터 변화를 이어서 처리할 수 있다.

노드 중단 처리

모든 노드는 중단 시간이 발생할 수 있다.

중단시간 없이 개별 노드를 재부팅할 수 있다는 점은 운영과 유지보수에 큰 장점이다. 리더 기반 복제에서 HA는 어떻게 달성 할까?

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

팔로워는 리더로부터 수신한 데이터 변경 로그를 로컬 디스크에 보관한다. 이렇게 하게되면 복구를 쉽게 진행할 수 있다.

  1. 보관된 로그에서 결함이 발생하기 전에 처리한 마지막 트랜잭션을 확인한다.
  2. 팔로워는 리더에 연결해 연결이 끊어진 동안 발생한 데이터 변경을 모두 요청한다.
  3. 변경이 다 적용되면 데이터 변경의 스트림을 계속 받을 수 있다.

리더 장애: 장애 복구

  1. 리더가 장애인지 판단한다.
    1. 대부분 시스템은 TimeOut을 통해 판별한다.
  2. 새로운 리더를 선택한다.
    1. 선출 과정을 통해 이전에 선출된 제어 노드에 의해 새로운 리더가 임명될 수 있다.
  3. 새로운 리더 사용을 위해 시스템을 재설정 한다.
    1. 새로운 쓰기 요청을 새로운 리더에게 보내야 한다.

장애 복구 과정은 잘못될 수 있는 것 투성이다.

복제 로그 구현

구문 기반 복제

리더는 모든 쓰기 요청을 기록하고 쓰기를 실행한 다음 구문 로그를 팔로워에게 전송한다. 관계형 데이터베이스는 모든 INSERT, UPDATE, DELETE 구문을 팔로워에게 전달하고, 팔로워는 클라이언트에게 직접 받은 것처럼 SQL 구문을 파싱하고 실행한다.

하지만 이 방법엔 복제가 깨질 수 있다.

해결책

쓰기 전 로그 배송

로그는 데이터베이스의 모든 쓰기를 포함하는 추가 전용(append-only) 바이트 열이다. 완전히 동일한 로그를 사용해 다른 노드에서 복제 서버를 구축할 수 있다. 리더는 디스크에 로그를 기록하는 일 외에도 팔로워에게 네트워크로 로그를 전송한다.

팔로워가 이 로그를 처리하게 되면 정확히 동일한 데이터 구조의 복제본이 만들어진다.

큰 단점 로그가 제일 저수준의 데이터를 기술한다. WAL은 어떤 디스크 블록에서 어떤 바이트를 변경했는지와 같은 상세 정보를 포함한다. 이렇게 하면 복제가 저장소 엔진과 밀접하게 엮인다. 다른 버전으로 변경한다면 리더와 팔로워의 데이터베이스 소프트웨어 버전을 다르게 실행할 수 없다.

만약 이것이 허용된다면 팔로워를 먼저 업그레이드 하고 레이드함으로써 중단시간 없이 데이터베이스 소프트웨어 업그레이드 수행이 가능하다. WAL배송과 같이 복제 프로토콜이 버전의 불일치를 허용하지 않는다면 중단시간이 필요하다.

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

다른 로그 형식을 사용하는 방법이다. 복제 로그를 저장소 엔진의 데이터 표현과 구별하기 위해 논리적 로그(logical log)라고 부른다.

여러 로우를 수정하는 트랜잭션은 여러 로그 레코드를 생성한 다음 트랜잭션이 커밋됐음을 레코드에 표시한다.

논리적 로그를 내부와 분리했기 때문에 호환성을 더 쉽게 유지할 수 있고, 다른 버전의 데이터베이스 소프트웨어나 다 른 저장소 엔진을 실행할 수 있다.

또한 논리적 로그 형식은 외부 애플리케이션이 파싱하기 더 쉽다. 이런 측면은 외부 시스템에 데이터베이스 내용을 전송하고자 할 때 유용하다.

트리거 기반 복제

조금 더 유연한 상황이 필요할 때 트리거나 프로시저를 사용하여 복제할 수 있다.

트리거는 애플리케이션 코드를 등록할 수 있게 하여 데이터가 변경되면 자동으로 실행된다. 데이터 변경을 분리된 테이블에 로깅할 수 있는 기회를 가진다.

그러나 많은 오버헤드가 있다. 하지만 유연성이 미쳐서 많이 사용한다.

복제 지연 문제

복제는 내결함성 뿐만 아니라 확장성과 지연시간 때문에 한다.

리더 기반 복제는 모든 쓰기가 단일 노드를 거쳐야 하지만, 읽기 전용 질의는 어떤 복제 서버에서도 가능하다. 대부분이 읽기 요청이고 쓰기가 작은 비율로 구성된 작업부하라면 많은 팔로워를 만들어 읽기 요청을 분산시킨다.

읽기 확장(read-sacling) 아키텍처는 팔로워를 더 추가함으로써 읽기 전용 요청을 처리하기 위한 용량을 늘릴 수 있다. 이러한 것은 비동기식 복제에서만 동작한다. 동기식에서 한다면 단일 노드 장애나 네트워크 중단으로 전체 시스템의 쓰기가 불가능해진다.

하지만 비동기 팔로워에서 데이터를 읽을 때 이전 데이터를 읽어올 수도 있다. 동일한 질의를 수행하면 모든 쓰기가 반영되지 않았을 때 다른 결과를 얻을 수 있다. 하지만 이러한 상태는 일시적인 상태에 불과하다. 쓰기를 멈추고 잠시 기다리면 결국 따라잡게 되고 리더와 일치하게 된다. 이런 효과를 최종적 일관성이라고 한다.

하지만 지연 시간이 얼마나 길어질 지 확신할 수 없기 때문에 이러할 경우 큰 문제가 될 수 있다.

자신이 쓴 내용 읽기

데이터가 제출되면 리더에게 전송해야 하지만, 데이터를 볼 때는 팔로워에서 읽을 수 있다.

사용자가 쓰기를 수행한 직후 데이터를 본다면 복제 서버에 반영되지 않았을 수 있다. 이러할 경우 유실된 것처럼 보이기 때문에 당연히 불만족스러운 동작이다.

이런 상황에서는 읽기 일관성 이 필요하다. 페이지를 재로딩 했을 때 모든 갱신을 볼 수 있음을 보장하며 다른 사용자에 대해서는 보장하지 않는다.

동일한 사용자가 여러 디바이스로 접근할 시엔 또 다른 문제가 발생한다. 이러한 경우엔 디바이스 간 쓰기 후 읽기 일관성이 제공되어야 한다.

단조 읽기

시간이 거꾸로 흐르는 현상이 있을 수 있다.

단조 읽기(monotonic read) 는 이상 현상이 발생하지 않음을 보장한다. 강한 일관성보단 덜 하지만 최종적 일관성보다는 더 강한 보장이다. 새로운 데이터를 읽은 후에는 예전 데이터를 읽지 않는다.

사용자의 읽기가 항상 동일한 복제 서버에서 수행되게끔 하는 방법이다. 예를 들어 임의 선택보다는 사용자 ID의 해시를 기반으로 복제 서버를 선택한다. 그러나 복제 서버가 고장 나면 질의를 다른 복제서버로 재라우팅 할 필요가 있다.

일관된 순서로 읽기

B는 지연없이 바로 전달되더라도 A는 지연이 발생할 수 있다. 이러하면 B -> A 순서로 화면에 보여질 수 있다.

https://www.notion.so

이러한 현상은 일관된 순서로 읽기(Consistent Prefix Read) 와 같은 다른 유형의 보장이 필요하다. 일련의 쓰기가 특정 순서로 발생한다면 쓰기를 읽는 모든 사용자는 같은 순서로 쓰여진 내용을 보게 됨을 보장한다.

파티셔닝된 데이터베이스에서 발생하는 특징적인 문제다. 같은 순서로 쓰기를 적용한다면 읽기 또한 일관된 순서를 보기 때문에 이러한 현상은 일어나지 않는다.

복제 지연을 위한 해결책

최종적 일관성 시스템으로 작업할 때 지연이 몇 분이나 몇 시간으로 증가한다면 문제가 생긴다. 이러한 상황이 중요하다면 쓰기 후 읽기와 같은 강한 보장을 제공하게끔 설계해야 한다.

또한 복제가 비동기식으로 동작하지만 동기식으로 동작하는 척 하는 것도 문제 해결 방안이다.

이러한 이유로 트랜잭션을 사용한다,

다중 리더 복제

다중 리더 복제의 사용 사례

  1. 다중 데이터센터 운영

쓰기 충돌 다루기

다중 리더 복제에서 제일 큰 문제는 쓰기 충돌이 발생한다는 점 → 충돌 해소가 필요하다

동기 대 비동기 충돌 감지

충돌 회피

일관된 상태 수렴

사용자 정의 충돌 해소 로직

다중 리더 복제 토폴로지

리더 없는 복제

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

읽기 복구와 안티 엔트로피

사용 불가능한 노드가 온라인 상태가 된 후 누락된 쓰기를 따라잡는데 주로 사용하는 두 가지 메커니즘

읽기와 쓰기를 위한 정족수

정족수 일관성의 한계

최신성 모니터링

느슨한 정족수와 암시된 핸드오프

다중 데이터센터 운영

동시 쓰기 감지

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

“이전 발생” 관계와 동시성

이전 발생 관계 파악하기

  1. 클라이언트 1은 장바구니에 우유를 추가. 버전 1을 할당받는다.
  2. 클라이언트 2는 우유에 대해 모르고 달걀을 추가. 버전 2을 할당하고 [우유], [달걀]을 반환받는다.
  3. 클라이언트 1은 달걀에 대해 모르고 밀가루를 추가. 버전 1과 함께 데이터를 전송하면 이 버전 번호로 [우유]를 , [우유, 밀가루]로 대체. 버전 3을 할당받는다. 서버는 [우유, 밀가루]와 [달걀]이 동시라는 사실을 안다. [우유, 밀가루]와 [달걀]을 반환받는다.
  4. 클라이언트 2는 클라이언트 1의 작업을 모른 채 햄을 추가. [달걀, 우유, 햄]으로 데이터를 합쳐 버전 2를 가지고 서버에 전송. 서버는 버전 2로 [달걀]을 덮어쓰고, [달걀, 우유, 햄]에 버전 4를 할당한다.
  5. 마지막으로 클라이언트 1이 베이컨을 추가. 이전에 버전 3을 받았으므로 그 데이터에 베이컨을 추가해 버전 3으로 전송. 이 값은 [우유, 밀가루]를 덮어쓰지만 [달걀, 우유, 햄]은 동시에 수행됐기 때문에 서버는 이 두개의 동시 수행된 값을 유지한다.

⇒ 전체적으로 이해가 잘 안됨

동시에 쓴 값 병합

버전 벡터