카프카 개념 정리

최창효·2023년 12월 11일
0
post-thumbnail

오프셋

각 파티션마다 메시지가 저장되는 위치를 오프셋이라 합니다.

컨슈머 그룹은 __consumer_offsets라는 곳에 각 파티션별 offset을 어디까지 소비했는지 저장합니다.

이를 오프셋 커밋이라 하며, 오프셋 커밋을 통해 __consumer_offsets에는 groupId : topic : partition별로 다음으로 전달해야 할 offset이 저장됩니다.

카프카는 레코드를 개별적으로 커밋하지 않고 성공적으로 처리해낸 마지막 메시지를 커밋함으로써 그 앞의 모든 메시지들 역시 성공적으로 처리된 것으로 간주합니다.

리밸런싱 작업이 발생하면 컨슈머는 이전에 처리하고 있던 것과는 다른 파티션을 할당받게 될 수 있고, 이때 새로운 작업을 어디서부터 재개해야 하는지 알아내기 위해 오프셋을 활용하게 됩니다.

리밸런스

컨슈머 그룹에 속한 컨슈머들은 자신이 구독하는 토픽의 파티션들에 대한 소유권을 공유하고 있습니다. 즉, 같은 그룹 내에서 컨슈머들은 누가 어떤 파티션을 소유하고 있는지를 알고 있습니다.

이러한 상황에서 할당된 파티션을 다른 컨슈머에게 할당해주는 작업을 리밸런스라고 합니다.

새로운 컨슈머를 컨슈머 그룹에 추가하거나, 컨슈머 종료 혹은 크래시가 발생하거나, 컨슈머 그룹이 읽고 있는 토픽에 변경이 발생했을 때 리밸런스 작업이 필요합니다.

복제

복제는 카프카가 높은 신뢰성을 보장하기 위한 핵심 기능입니다. 하나의 파티션은 다수의 레플리카를 가질 수 있습니다.

하나의 메시지를 여러 레플리카가 함께 보관함으로써 일부 파티션에 크래시가 발생해도 메시지의 지속성을 유지할 수 있게 됩니다.

대부분의 이벤트를 처리하는(읽기 및 쓰기 작업을 진행하는) Partition을 리더 레플리카, 리더를 복사하는 작업만 하는 Partition을 팔로워 레플리카라고 합니다.

팔로워 레플리카가 리더 레플리카와 동기화를 유지하기 위해 보내는 요청은 컨슈머가 메시지를 읽어오기 위해 사용하는 요청과 동일합니다.

리더가 동작 불능 상태가 되었을 때는 인-싱크 레플리카 중 하나가 새로운 리더가 됩니다.

카프카에서는 같은 클러스터에 속한 노드(파티션)간의 데이터 교환을 복제라고 하며, 이와 구분하기 위해 카프카 클러스터 간의 데이터 복제를 미러링이라고 합니다.

인-싱크 레플리카

인-싱크 레플리카(ISR)란 간단히 말해 리더를 대신할 수 있는 상태의 레플리카라고 할 수 있습니다.

다음 조건을 만족하는 팔로워 레플리카는 인-싱크 상태인 것으로 간주합니다.

  • 주키퍼와의 활성 세션이 존재해 최근 6초 사이에 주키퍼로 하트비트를 전송한 경우
  • 최근 10초 사이에 리더로부터 메시지를 읽어왔으며, 그 메시지들이 가장 최근 메시지인 경우

그렇기 때문에 아웃-오브-싱크상태가 된 레플리카더라도 리더 파티션에 쓰여진 가장 최근 메시지까지를 따라잡기만 하면 얼마든지 다시 인-싱크 레플리카가 될 수 있습니다.

선호 리더 선출

각 파티션은 ‘현재 리더’외에 ‘선호 리더’를 가지게 됩니다. 선호 리더란 토픽이 처음 생성되었을 때 리더였던(처음에 설정을 통해 미리 지정한 리더) 레플리카를 가리킵니다.

파티션이 처음 생성되던 시점에는 리더 레플리카가 모든 브로커에 걸쳐 균등하게 분포되기 때문에 선호(preferred)라는 표현이 붙었습니다.

클러스터 내의 파티션들이 가진 선호 리더가 모두 실제 리더로 선출될 경우 이는 모든 부하가 브로커 사이에 균등하게 분배될 것이라고 생각할 수 있습니다.

선호 리더를 식별하는 가장 좋은 방법은 파티션의 레플리카 목록을 살펴보는 것입니다. 레플리카 목록에 있는 첫 번째 인-싱크 레플리카가 최초에 리더 레플리카로 선출되기 때문에 이 레플리카가 곧 선호 리더입니다.

아웃-오브-싱크 레플리카 중 하나를 리더로 선출하는 언클린 리더 선출 방식도 존재합니다.

acks

프로듀서가 쓰기 작업이 성공했다고 판단하기 위해 얼마나 많은 파티션 레플리카가 해당 레코드를 받아야 하는지를 결정하는 값입니다.

acks = 0

  • 프로듀서가 네트워크로 메시지를 전송한 시점에 성공적으로 쓰여진 것으로 간주하며 브로커의 응답을 기다리지 않습니다.
    • 전송하는 객체가 직렬화될 수 없거나, 네트워크 카드가 오작동하는 경우에는 에러를 받게 됩니다. (네트워크에 메시지 자체를 전송하지 못함)
    • 파티션이 오프라인이거나, 리더 선출이 진행중이거나, 전체 카프카 클러스터가 작동 불능인 경우에도 에러가 발생하지 않습니다. (네트워크에 메시지가 전송됐기 때문)
  • 브로커가 메시지를 받지 못했더라도 프로듀서는 이 상황에 대해 알지 못하고, 메시지를 재전송하지 않기 때문에 메시지가 그대로 유실됩니다.
  • 매우 높은 처리량이 필요할 때 사용합니다.

acks = 1

  • 프로듀서는 리더 레플리카가 메시지를 받는 순간 브로커로부터 성공했다는 응답을 받습니다.
  • 만약 리더에 메시지를 쓸 수 없다면(리더에 크래시가 났는데 새 리더는 아직 선출되지 않음) 프로듀서는 에러 응답을 받을 것이고, 데이터 유실을 피하기 위해 메시지 재전송을 시도합니다.
  • 리더가 메시지를 받아서 응답을 받았는데 해당 리더가 팔로워에 복제하기 전에 크래쉬가 날 경우 데이터가 유실될 수 있습니다.

acks = all

  • 리더 및 모든 인-싱크 레플리카가 메시지를 받았을 때 성공했다는 응답을 받습니다.
  • acks=all에서 프로듀서는 메시지가 완전히 커밋될 때까지 계속해서 메시지를 재전송합니다.

멱등적 프로듀서

카프카는 acks값을 조절하고 해당 브로커가 메시지를 받지 못했을 경우 다시 재전송을 통해 ‘최소 한 번’처리를 보장할 수 있습니다.

하지만 이러한 ‘최소 한 번’처리는 중복의 가능성을 내포하고 있습니다. ‘계좌 잔액 차감하기’와 같은 일부 행동은 중복 처리되면 치명적인 문제가 발생할 수도 있습니다.

동일한 작업을 여러 번 실행해도 한 번 실행한 것과 결과가 동일한 것을 멱등하다고 합니다. 카프카의 멱등적 프로듀서는 자동으로 중복을 탐지하고 이를 처리해 줍니다. 이를 ‘정확히 한 번’이라고 합니다.

프로듀서 설정에 enable.idempotence=true를 추가함으로써 멱등적 프로듀서를 사용할 수 있습니다.

멱등적 프로듀서 기능을 켜면 모든 메시지는 고유한 프로듀서 ID(PID)와 시퀀스 넘버를 가지게 됩니다. PID와 시퀀스 넘버, 대상 토픽 및 파티션을 모두 합치면 각 메시지의 고유한 식별자가 됩니다.

각 브로커는 이 고유 식별자를 사용해 자신의 파티션들에 쓰여진 마지막 5개(변경 가능)의 메시지를 추적합니다.

추적을 통해 이전에 받은 적 있는 메시지를 또 받았다는 사실을 알게 될 경우 적절한 에러를 발생시킴으로써 중복 메시지를 거부합니다. 이 에러는 피로듀서에 로깅도 되고 지표값에도 반영되지만 예외가 발생하지는 않습니다. 따라서 사용자에게 별도의 경보가 보내지지는 않습니다.

하지만 카프카의 멱등적 프로듀서는 프로듀서의 내부 로직으로 인한 재시도가 발생할 경우 생기는 중복만을 방지합니다.

동일한 메시지(내용은 같지만 ID값은 다름)를 가지고 producer.send()를 두 번 호출했을 때는 명등적 프로듀서가 개입하지 않으므로 중복을 방지할 수 없습니다. 또한 여러 개의 인스턴스를 띄우거나 하나의 인스턴스에서 여러 개의 프로듀서를 띄우는 경우, 이 프로듀서들 중 두 개가 동일한 메시지를 전송하려 시도해도 마찬가지로 멱등적 프로듀서는 중복을 방지하지 못합니다.

멱등적 프로듀서는 프로듀서 자체의 재시도 메커니즘(프로듀서, 네트워크, 브로커 에러로 인해 발생하는)에 의한 중복만을 방지할 뿐 그 이상의 중복 방지는 하지 못합니다.

트랜잭션

카프카의 트랜잭션은 카프카 스트림즈를 사용해서 개발된 애플리케이션에 정확성을 보장하기 위해 만들어진 기능입니다. 그런 만큼 스트림 처리 애플리케이션의 기본 패턴인 '읽기-처리-쓰기'패턴에서만 ‘정확히 한 번’을 보장할 수 있습니다.

카프카 트랜잭션은 원자적 다수 파티션 쓰기를 통해 ‘정확히 한 번’을 보장합니다. '읽기-처리-쓰기'패턴에서 마지막 쓰기 작업으로 결과를 토픽에 쓰는 것과, 오프셋을 __consumer_offsets 토픽에 쓰는 것 둘 다 쓰기 작업을 진행하는 것입니다. 두 쓰기 작업을 원자적 다수 파티션 쓰기로 실행함으로써 두 작업 사이의 all or nothing을 보장합니다.

트랜잭션을 사용해 원자적 다수 파티션 쓰기를 수행하려면 트랜잭션적 프로듀서를 사용해야 합니다. 트랜잭션적 프로듀서는 transactional.id설정이 잡혀 있고 initTransactions()을 호출해서 초기화해 줍니다.

카프카 브로커에 의해 자동으로 설정되는 producer.id와 달리 transactional.id는 프로듀서 설정의 일부이며, 재시작을 하더라도 값이 유지됩니다. 카프카 브로커는 transactional.id에서 producer.id로의 대응 관계를 유지하고 있다가 만약 이미 있는 transactional.id프로듀서가 initTransactions()를 다시 호출하면 새로운 랜덤값이 아닌 이전에 쓰던 producer.id값을 할당해 줍니다.

트랜잭션 기능을 사용해서 쓰여진 레코드는 비록 결과적으로 중단된 트랜잭션에 속할지라도 다른 레코드들과 마찬가지로 파티션에 쓰여지게 됩니다. 그렇기 때문에 올바른 격리수준을 통해 메시지를 읽어드리는 방식을 제어해야 합니다.

카프카의 트랜잭션 기능은 다음과 같은 상황의 ‘정확히 한 번’은 보장하지 못합니다.

  1. 스트림 처리에 있어서의 부수 효과
    • 스트림 처리 단계에 사용자에게 이메일을 보내는 작업이 포함되어 있다면, 트랜잭션을 롤백하더라도 이메일 발송이 취소되는 건 아닙니다.
  2. 카프카 토픽에서 읽어서 DB에 쓰는 경우
    • 외부 DB에 결과물을 쓰기 때문에 프로듀서가 사용되지 않습니다. 즉, 레코드는 외부 DB에 쓰여지고 오프셋은 컨슈머에 의해 카프카에 커밋되는 상황입니다. 이때 하나의 트랜잭션에서 외부 데이터베이스에는 결과를 쓰고 카프카에는 오프셋을 커밋할 수 있도록 해주는 메커니즘 같은 건 존재하지 않습니다.
  3. DB에서 읽어서, 카프카에 쓰고, 이걸 다시 다른 DB에 쓰는 경우
    • 카프카는 종단 보장에 필요한 기능을 가지고 있지 않습니다.
  4. 한 클러스터에서 다른 클러스터로의 데이터 복제(미러링)
    • 하나의 클러스터에서 다른 클러스터로 데이터를 복사할 때 미러메이커 2.0을 이용하면 ‘정확히 한 번’을 보장할 수 있습니다. 하지만 이것이 트랜잭션의 원자성을 보장하지는 않습니다.
    • 만약 어플리케이션이 여러 개의 레코드와 오프셋을 트랜잭션적으로 쓰고, 미러메이커 2.0이 이 레코드들을 다른 카프카 클러스터에 복사한다면, 복사 과정에서 트랜잭션 속성이나 보장 같은 것은 유실됩니다.
  5. 발행/구독 패턴
    • 발행/구독 패턴에서 read_committed를 사용한다고 해서 '정확히 한 번'을 보장받지 못합니다. 발행/구독 패턴의 오프셋 커밋 로직이 어떻게 되어있는지에 따라 컨슈머들은 메시지를 한 번 이상 처리할 가능성이 충분히 존재합니다.

References

profile
기록하고 정리하는 걸 좋아하는 개발자.

0개의 댓글