이론 정리/Database

분산 트랜잭션에서 2PC, 3PC, Calvin, Spanner, 퍼콜레이터, RAMP 동작 방식에 대한 짧은 글

철매존 2026. 1. 4. 05:16
728x90
반응형

애플리케이션이 아니라 데이터베이스, 인프라 관점에서 이를 처리하는 방법에 대해서 서술(그래서 SAGA가 없었군)

데이터베이스 인터널스 책을 보면서 살짝 고민했던 부분과 거기서 궁금했던 점, 그리고 추가로 동작하는 방식에 대해 공부한 것을 토대로 작성했다.

 

2PC

  • 준비
    • 코디네이터가 코홀트에게 준비되었는지 물어봄
    • 코홀트는 트랜잭션을 실행하고 디스크에 로그 기록한 후에 커밋 준비 되었는지 응답
  • 커밋/중단
    • 준비 단계에서 커밋 준비 안됐으면 ABORT
    • 다 준비 됐으면 커밋 명령
      • 코홀트는 진행/롤백 후 확인 메세지 전달
  • 장애 상황
    • 코호트가 장애
      • prepare 단계의 장애
        • 이건 상관 X (무조건 Abort)
      • YES 이후 장애
        • 코호트가 다시 살아났더니 YES 이후 자신이 어떤 상태인지 모름(Uncertain)
          • 코디네이터에게 질의 필요
      • 커밋 메세지 수신 후 ACK 실패(죽었거나 네트워크 에러)
        • 코디네이터는 ACK 를 못받아서 계속 재전송
        • 다른 코호트는 적용 완료 (일관성이 깨진다.)
    • 코디네이터가 장애
      • prepare 응답 후 결정 전 장애
        • 코호트는 모두 블로킹 (무한 대기)
        • 코디네이터 복구 후에는 로그에 결정 사항이 없기 때문에 Abort
      • 결과 결정 후 전송 이전에 장애
        • YES 를 받고 본인은 COMMIT 로그 적용 후 사망하면?
          • 누구는 받았을수도 있고 누구는 못받았을수도 있고..
          • 일단은 COMMIT 해야 하는데 ACK 를 다 받지 못했으므로 다시 모든 곳에 COMMIT 메세지 전송
          • COMMIT 한 것들은 데이터가 적용되었는데 그렇지 않은 것들은 무한 대기 (일관성이 깨진다.)
        • 이걸 해결하기 위한 방법으로 3PC 가 도입

3PC

  • 2PC 의 블로킹을 해결하기 위해 도입
    • 중간에 PreCommit 을 도입
      • 나는 커밋할거임 이런 식으로 의도를 미리 코홀트에게 전송
        • 이걸 받고 나면 코디네이터가 죽은 경우 커밋해야지? 하고 판단 가능
          • 즉, 코디네이터 장애 혹은 네트워크 이슈 발생 시 무조건 커밋/롤백 가능
    • 문제점
      • PreCommit 단계 이후 DoCommit(실제 커밋) 하다가 코디네이터가 죽는 경우
        • 새로운 코디네이터 선출 후 이전 상태를 모르면 롤백을 결정한다.
          • 그러면 DoCommit 받은 애들은 커밋되고 DoCommit 못받은 애들은 롤백되는 경우가 생긴다.

Calvin

  • 트랜잭션 실행 전에 미리 순서를 확정하는 방식
  • 보통은 실행 중에 락을 잡고 경합한다면 이거는 실행 전에 순서를 정하는것
    • 입력과 순서가 동일하면 결과는 무조건 동일할 것이다.
  • 시퀀서가 트랜잭션 요청에 대해 순서를 매김
    • 그 트랜잭션 목록을 모든 노드에 전달
      • Paxos 같은 합의 알고리즘으로 목록이 맞다는 것만 합의
        • 위의 목록의 내용과 순서가 같으니 알아서 노드들이 수행하면 같은 결과가 나온다.
          • 쓰기를 위한 읽기가 안되기 때문에 읽기 세트, 쓰기 세트를 따로 보내는 방식 사용 (즉 쓰기를 하기 전에 뭐가 대상이 되는지를 미리 읽기 세트로 보내서 처리)
  • 문제점
    • 대화형 트랜잭션이 불가능
      • select -> 애플리케이션 처리 -> update 이런게 안됨
      • stored procedure 같은 식으로 뭉탱이 처리만 가능

Spanner

  • 기본적으로 2PC 기반
  • 2PC 에서 코홀트가 하나 죽으면 대기해야 하는데 여기서는 그걸 해결
    • 코홀트 하나가 아니라 Paxos 그룹이 대기자가 된다.
      • 즉 뭉탱이가 수신자기 때문에 하나 죽어도 ㄱㅊ
  • 순서를 확인하는 방법 : TrueTime 사용
    • 무조건 시간의 오차 범위를 파악하여 그보다 늦게 응답을 보냄
      • 트랜잭션 T1 의 커밋 타임스탬프 S1이 있을 때, 수신 그룹에서는 S1만큼 기다린 후에 커밋 성공 응답
        • 그러면 T1, T2 가 있었을 때 무조건 T1이 T2 보다 먼저 끝남(인과관계 보장)
  • 읽기 단계에서 락을 걸지 않는다.
    • 위의 타임스탬프가 있기 때문에 읽기 요청 시 그 타임스탬프 이전의 스냅샷을 그냥 보내주면 해당 시간의 데이터를 보내주기 가능
      • 즉 쓰기 단계 때문에 읽기가 방해받지 않는다.
  • 여기서 가장 궁금한건... 시간은 무조건 맞지 않는다고 했던 것 같은데?
    • 해결법 1 : 클라이언트의 시계는 믿지 않음
      • 커밋 시간의 경우는 수신을 받은 리더가 자신의 TrueTime 기준으로 타임스탬프를 찍는다.
    • 해결법 2 : 스패너 간의 시간 차이 해결법은 시간을 구간으로 설정 : 시간은 요 구간 안이야, 라고 보내준다.
      • 그래서 그 최대 시간 이후에 응답을 주면 된다.
      • 구글에서는 이 오차 범위를 잘 잡나봄.
        • 읽기 요청의 경우는 읽기 기준 시간을 정해서 보내주면 된다. 시간이 좀 애매하면 기다렸다가 읽으면 무조건 해당 과거의 데이터 전달 가능

퍼콜레이터

  • Prewite
    • 클라이언트가 직접 코디네이터 역할을 하는거인듯?
    • 클라이언트가 TSO 라는 전역적으로 증가하는 시간을 발급하는 곳에서 시작 시간 받아옴
      • 수정하려는 대상 중 하나를 Primary 로 두고 클라이언트 Lock 컬럼에 입력
        • 나머지를 Secondary 로 두고 Lock 에서 secondary 검색 시 primary 로 가도록 작성
          • 실제 데이터는 Data 컬럼에 적어둔다 (이 때는 다른곳에서 참조 불가)
  • Commit
    • TSO 에서 커밋 시간 받아옴
      • Primary 의 Write 컬럼에 위의 prewrite 시간의 데이터는 Commit 단계 시간에서 유효하다고 기록
        • Primary 의 Lock 제거 (이 순간 트랜잭션 성공)
          • 나머지 Secondary 애들도 Write 기록 후 Lock 제거
            • 다른 클라이언트에서 읽기 요청 시 Lock 컬럼을 확인하여 락이 있으면 풀릴 때까지 대기하면 됨
              • 그러면 Lock 을 건 클라이언트가 죽으면?
                • 이 때 Lazy 로 관련 처리 진행
                  • 커밋 된 경우 : 커밋은 했는데 락은 남아있으니 문제 없다고 판단해서 락 제거
                  • Write 에 아무 기록 없는 경우 : 문제 있다고 생각해서 락 지우면 rollback 처리 (왜냐면 Data 컬럼에 있는 데이터는 안보이니까 의미 X)
                    • 문제는... 위의 방식을 보면 락 충돌 시 기다리거나 청소하는 과정이 느리다.
                      • 그래서 이거는 실시간성으로는 부적합해서 그런 곳에는 Spanner 쓴다.
                        • 유튜브 조회수 집계, 검색 인덱스 업데이트 같이 사람이 볼 필요는 없지만 정확해야 하는 경우 필요한 작업에 필수적인 친구임

코디네이터 생략

  • 대표적으로 RAMP 트랜잭션
    • 데이터를 쓸 때 메타데이터를 같이 보낸다.
      • 데이터를 읽었을 때, 다른 쪽의 메타데이터와 일치하지 않으면 데이터 불일치 발생했다고 확인
        • 그 순간 클라이언트는 다른 노드에게 관련 데이터 다시 확인
          • 그래서 결국 일치하는 데이터를 받을 수 있음
            • 그래서 결국 원하는 데이터들이 있을 때 전체적으로 다 반영된 것을 볼 수 있다. 불일치 해결될 때까지 다른 노드를 통해 확인할 것이기 때문에
            • 그리고 쓰기 단계에서 따로 뭔가 처리 불필요. 내꺼 할만큼 했으면 된것이기 때문.
            • 읽기도 락은 걸지 않고, 이상하면 다른쪽에 또 요청
            • 그대신 메타데이터 용량이 더 들고, 읽기를 여러 번 요청할 수 있다.
반응형