분산 시스템에서 일관성은 크게 두 가지 레벨로 나뉩니다:

  • '강한 일관성(Strong Consistency)'
  • '최종 일관성(Eventually Consistency)'.

이 글에서는 우선 '강한 일관성'에 대해 설명하고 그 한계점을 분석한 후, 이러한 한계를 해소할 수 있는 '최종 일관성'에 대해 알아보겠습니다. 그 후 최종적 일관성을 달성하는 방법을 살펴보겠습니다.

 


 

1. Strong Consistency.

1.1 정의:

'강한 일관성 (Strong Consistency)'은 시스템 내 데이터의 '최신성(Timeliness)'과 '무결성(Integrity)'을 보장하는 것을 의미합니다.

 

분산 시스템에서 이는 모든 마이크로서비스가 데이터를 '즉시' 그리고 '정확하게' 업데이트하는 것을 말하며, 데이터베이스 시스템에서는 원본 데이터가 업데이트된 직후 복제 데이터도 동일하게 업데이트 되어지는 것을 의미합니다.

 

1.2 달성하는 방법:

'Strong Consisteny' 을 달성하는 아이디어는 '하나의 데이터에 대한 원자적(Atomic) 업데이트'입니다.

 

하지만 분산 시스템에서 데이터는 한 개가 아니라 기본적으로 복제가 되어 있으며, 각자 서비스에 맞게 원하는 목적에 맞춰서 데이터가 여러 형태로 가공되어 있을 것이기 때문에 이를 고려해서 실행되어야 합니다.

 

따라서 'Strong Consisteny' 를 달성하기 위해서 각 서비스가 자신들의 데이터를 Atomic 업데이트하는 것을 기다려야 합니다.

 

1.3 한계:

여러 벌의 데이터가 업데이트 되는 것을 기다리는 시간도 문제지만 각 데이터 마다의 업데이트 요청은 네트워크 통신을 통해 이뤄진다는 점은 문제입니다.

 

이런 점들을 고려했을 때 '강한 일관성'은 네트워크 지연에 민감하며, 성능 저하를 유발합니다.

 

이는 CAP 이론(Consistency, Availability, Partition tolerance)과 연관이 있으며, '강한 일관성'을 선택한다는 것은 가용성의 희생을 의미합니다.

  • CAP 이론을 잘못알고 있는 사람들이 많은데, 이는 Consistency (일관성), Availability (가용성), Partition tolerance (분단 내성) 중에 두 가지만을 가져갈 수 있다는 뜻이 아닙니다.
  • CAP 이론의 진정한 뜻은 네트워크 결함이 생겼을 때 일관성을 선택할 것인지, 가용성을 선택할 것인지에 대한 이론입니다.
  • 일관성을 이루기 위해서는 다른 노드와의 통신이 필요합니다. 그러므로 일관성을 선택한다는 건 통신이 안되는 상황에서 일관성이 깨지는 연산은 실행하지 않겠다라는 뜻입니다.

 

그럼에도 불구하고 특정 요구사항(예: 리더 선출, Unique Constraint 조건 검사)에서는 필수적입니다.

 

1.4 Strong Consistency 를 구현하는 방법

'강한 일관성'을 달성하기 위한 방법으로는 '2단계 커밋(2PC, Two-Phase-Commit)'과 '합의(Consensus)'가 있습니다.

 

1.4.1 2PC (Two-Phase-Commit)

2PC 은 일관성을 달성하기 위해 여러개의 노드가 동시에 Atomic Commit 을 하는 것입니다.

 

2PC 는 '참가자' 와 '코디네이터' 라는 두 요소를 통해서 이뤄지며, 다음과 같은 2단계 플로우를 통해서 이뤄집니다:

  • 준비 단계 (Preparation Phase):
    • 코디네이터(Coordinator): 트랜잭션을 시작하는 코디네이터는 모든 참여자(Participants)에게 트랜잭션 커밋 준비를 요청하고, 이 때 'Prepare' 메시지를 보냅니다.
    • 참여자(Participants): 각 참여자는 트랜잭션을 커밋할 준비가 되었는지를 확인한다. 준비가 되었으면, 코디네이터에게 'Ready' 응답을 보내고, 준비가 되지 않았다면 'Abort' 응답을 보냅니다.
  • 커밋/중단 단계 (Commit/Abort Phase):
    • 모든 참여자가 'Ready' 응답을 한 경우:
      • 1) 코디네이터는 모든 참여자에게 'Commit' 명령을 보냅니다.
      • 2) 참여자들은 트랜잭션을 커밋하고, 커밋 완료를 코디네이터에게 알립니다.
    • 하나라도 'Abort' 응답을 한 경우:
      • 1) 코디네이터는 모든 참여자에게 'Abort' 명령을 보냅니다.
      • 2) 참여자들은 트랜잭션을 중단하고, 중단 완료를 코디네이터에게 알립니다.

 

 

2PC 의 단점:

  • 성능:
    • 여러번의 네트워크 통신으로 인한 성능 감소
    • Lock 까지 이용한다면 동시성 처리는 더욱 감소
  • Fault Tolerance 하지 않음:
    • 커밋 phase 에서 커밋하는 노드가 죽는다면 장기간 대기할 수 있습니다.
    • 참가자가 Prepare 단계에서 커밋할 수 있다고 응답을 했는데 코디네이터로부터 응답이 안오면 장기간 대기해야할 수 있습니다.
  • Single point of failure:
    • 코디네티어라는 단일 장애점이 존재합니다.
  • 코디네이터는 Stateful 한 서비스입니다:
    • 2PC 을 하기 위해서 어떤 노드가 참여하고 있고, 현재 트랜잭션 처리는 어떤 상탠지 기록하는 '상태 정보' 가 필요합니다. 이는 Stateless 한 어플리케이션을 주로 배포하는 클라우드 환경과는 맞지 않을 수 있습니다. 

 

1.4.2 Consensus (합의)

분산 시스템에서 Consensus 또한 Strong Consistency 를 달성하는 방법입니다.

 

합의는 모든 노드가 어떤 사항에 '동의' 하는 것을 말하며 '충돌' 이 발생했을 때 누가 승자인지 알려줍니다.

 

간단한 합의 방식은 단일 복제 리더와 유사하게 동작합니다. 리더가 제안하고, 팔로워들이 이를 승인하거나 거부하는 방식으로 이루어집니다.

 

과반수의 동의만 있으면 합의를 진행할 수 있으며, 2PC와 달리 가용성을 고려할 수 있는 장점이 있습니다

 

즉 합의는 2PC 와 달리 가용성 또한 챙길 수 있다는 점이 혁신적입니다.

 

그러나 합의 또한 네트워크에 민감하고, 단일 리더 위주로 처리된다는 점에서 Scalability 에 한계가 있습니다.

 


 

2. Eventually Consistency

Eventually Consistency 는 이전에 Strong Consistency 가 가지고 있는 두 가지 속성 중 무결성 (Integrity) 만 있는 것입니다.

 

이는 Timeliness 를 보장하지 않아도 되기 때문에 Strong Consistency 가 가지지 못했던 Scalability 를 가질 수 있습니다.

 

최종 일관성의 대표적인 아키텍처 예로는 이벤트 중심 아키텍처(Event-Driven Architecture, EDA)를 들 수 있습니다.

 

EDA에서는 서비스가 발생시킨 이벤트의 처리 여부에 대해 신경 쓰지 않습니다.

 

이는 강한 일관성에서 다른 데이터들의 업데이트를 기다리는 것과 대조적으로, 성능적 이점을 제공합니다.

 

그리고 이벤트는 메시지 브로커에 저장되어 유실되지 않으므로, Consumer가 장애가 발생해도 재가동하여 처리를 재개할 수 있습니다

 

그러나 EDA 자체만으로는 '무결성 (Integrity)'을 완전히 만족시키지는 못합니다. 무결성을 달성하기 위해서는 아래와 같은 요소들을 고려해야 합니다.

 

2.1 멱등성 처리 (idempotent)

멱등성 처리는 데이터 무결성 (Integrity) 을 유지하는 데 필수적입니다.

 

예를 들어, 데이터베이스에 잔액을 1000원 증가시키는 업데이트 연산을 커밋했을 때, TImeout 인해 응답을 받지 못하고 커넥션이 종료되었다면 해당 연산이 성공했는지 여부를 알 수 없습니다.

 

이 경우 재시도(Retry)를 함부로 하면 데이터 무결성이 깨질 수 있습니다.

 

따라서 모든 상황에서 안전한 재시도를 위해 멱등성 연산을 만들어야 합니다.

 

멱등성 연산을 만드는 방법에는 연산 자체를 멱등하게 만들거나, 이미 처리된 연산을 재처리하지 않도록 필터링하는 방법이 있습니다.

 

필터링은 일반적으로 모든 연산에 적용할 수 있는 방법이므로, 이를 살펴봅시다.

 

2.1.1 이벤트 기반 Dataflow 시스템에서 멱등성 연산 만들기

멱등성 연산을 만드는 방법은 간단합니다. 요청에 '고유 요청 식별자(Unique Request Id)' 를 포함하여 전송하는 것입니다.

 

중요한 것은 요청이 시작되는 지점부터 종료되는 지점까지, 즉 엔드-투-엔드(end-to-end)에서 이 Unique Request Id 를 관리하는 것입니다.

 

Unique Request Id 는 클라이언트에서 시작되거나 서버 측에서 요청을 처음 수신할 때 생성될 수 있습니다.

 

클라이언트에서는 hidden form 을 통해 생성하고 서버에서는 요청 본문을 해싱하여 생성할 수 있습니다.

 

이렇게 생성된 식별자를 데이터베이스에 저장하고 Unique Constraint 조건을 적용함으로써, 동일한 요청이 여러 번 오더라도 연산이 중복 적용되지 않도록 할 수 있습니다.

 

2.2 Multi Partition 을 이용한 연산 순서 보장

요청의 처리 순서는 데이터 무결성 (Integrity) 을 달성하는 데 중요한 요소입니다.

 

즉 연산의 순서가 다르면 결과가 달라질 수 있습니다. 연산의 순서를 보존하는 방법 중 하나는 로그 기반의 데이터 구조를 사용하는 메시지 큐 서비스를 활용하는 것입니다 (e.g Apache Kafka).

 

Apache Kafka는 토픽의 파티션별로 데이터를 로그 형태로 저장합니다.

 

로그는 Append-Only 구조로 되어 있어서 연산의 순서를 보장하는 데 적합합니다.

 

따라서 이 방식은 상태를 유지해야 하는 데이터에 대한 연산들을 각각 지정된 파티션으로 보내서 연산의 처리 순서를 보장하고, 충돌이 발생하면 거부(Reject)하는 방식으로 작동합니다.

 

2.3 예시: 멱등성 처리와 Multi Partition 이용한 무결성 보장

예를 들어, A 계좌에서 B 계좌로 송금하는 트랜잭션을 처리하는 과정을 살펴봅시다:

  • Step 1: 서버가 송금 메시지를 처음 수신하면 Unique Request Id 룰 할당하고 메시지 큐 서비스에 넣습니다. 이렇게 하면 메시지는 유실되지 않습니다.
  • Step 2: 송금 메시지를 처리하여 각 상태에 맞는 연산을 생성합니다. A 계좌에서는 잔액이 감소하는 연산이, B 계좌에서는 잔액이 증가하는 연산이 생성됩니다. 이 연산들은 다시 메시지 큐의 파티션으로 들어가며, 한 계좌의 모든 연산은 항상 지정된 동일한 파티션으로 들어갑니다.
  • Step 3: 연산이 들어간 긱각의 파티션에서 메시지를 소비하여 처리합니다. 중요한 것은 메시지를 중복 처리하지 않고 단일 스레드로 처리하는 것입니다, 이는 중복 처리로 인한 사이드 이펙트와 동시성 문제를 피하기 위함입니다.

이 과정에서 2단계에서 메시지를 여러 번 전송하는 문제가 발생할 수 있습니다. 그러나 3단계에서 한 번 처리된 메시지는 여러 번 처리되지 않도록 함으로써 문제를 방지합니다.

 

만약 지불하는 계좌에서 자신이 가진 잔고보다 오버해서 송금되지 않게 하려면, Step 1 이전의 첫 메시지는 지불하는 계좌의 파티션으로 향하게 만들고, 이 메시지를 처리하는 Prcoessor 내부의 Local Database 에 잔고를 유지하도록 해서 유효성 검사 (Validation check) 를 하면 됩니다. (이는 카프카 스트림즈를 사용하고 있다고 생각해보면 이해하기 쉬울 수 있습니다.)

 

그러면 Step 1 으로 들어오는 메시지는 송금할 수 있는 메시지만이 들어오게 될 것입니다.

 

2.4 보상 트랜잭션 (Compensating Transaction)

Multi Partition 을 통해서 연산의 순서를 보장하는 방식은 2PC나 합의(Consensus)에 비해 Scalability 가 뛰어나면서도 데이터 무결성을 달성할 수 있습니다.

 

그러나 엄격한 제약 조건을 검사하게 되면 시스템의 병목 현상이 발생하여 확장성을 얻기 어려울 수 있습니다.

 

이런 경우, 일시적으로 무결성이 훼손될 수 있지만 '보상 트랜잭션 (Compensating Transaction)' 을 통해 문제를 해결할 수 있습니다.

 

예를 들어, 주문 처리 시스템에서 재고를 엄격하게 검사하지 않고 주문이 이루어진 후, 재고 소진이 확인되면 주문을 취소하고 결제를 환불하는 방식으로 처리할 수 있습니다.

 

이러한 시스템은 고객 경험을 다소 떨어뜨릴 수 있지만, 오히려 처리 지연으로 인한 고객의 불편함을 감소시킬 수 있습니다.

 

비즈니스 상황에 따라 Compensating Transaction 의 비용이 높거나 엄격한 제약 조건 검사가 필요한 경우도 있지만, 그렇지 않은 경우에는 확장성을 높이기 위해 이와 같은 방법을 고려할 수 있습니다.

 

이 방법은 실시간 처리 요구 사항이 높고, 대규모 트랜잭션을 처리해야 하는 시스템에서 유용하게 사용될 수 있습니다.

+ Recent posts