[카프카 핵심 가이드] Chapter 08. '정확히 한 번' 의미 구조

Falco·2024년 1월 13일
0
post-thumbnail

'정확히 한 번' 보내는 카프카

카프카에서 정확히 한 번 메시지를 전송하는 방법과 활용 사례, 그리고 한계에 대해 알아본다.

정확히 한 번 이란?

  • 멱등적 프로듀서
  • 트랜잭션

으로 이루어진다.

8.1 멱등적 프로듀서란?

동일한 작업을 여러번 실행해도 한 번 실행한 것과 같은 서비스를 멱등적이라고 한다.

데이터베이스에서는 다음과 같이 설명된다.

// 멱등적 X
UPDATE t Set x=x+1 where y=5
// 멱등적 O
UPDATE t Set x=18 where y=5

카프카에선 메시지 전송을 재시도함으로써 동일한 작업을 여러번 수행하는 결과가 나올 수 있다.

  1. 파티션 리더가 프로듀서로부터 레코드를 받아서 팔로워들에게 성공적으로 복제한다.
  2. 프로듀서에게 응답을 보내기 전, 파티션 리더가 있는 브로커에게 크래시가 발생한다.
  3. 프로듀서 입장에서는 응답을 받지 못한 채 타임아웃이 발생하고, 메시지를 재전송한다.
  4. 재전송된 메시지가 새 리더에게 도착한다. 하지만 이 메시지는 이미 저장되어 있다.(결과적으로 중복 발생)

8.1.1 카프카 프로듀서의 멱등적 작동 원리

멱등적 프로듀서 기능을 켜면 메시지는 고유한 프로듀서 ID시퀀스 ID를 가지게 된다.

따라서 메시지마다 고유한 식별자가 추가됨

각 브로커느 할당된 모든 파티션들에 쓰여진 마지막 5개 메시지를 이 고유 식별자로 추적한다. (max.in.flights.requests.per.connection : 추적 메시지 개수)

따라서 이런 고유 식별자를 가지고 중복처리를 수행한다.

+) 예상보다 높은 시퀀스 넘버의 메시지를 받게된다면 out of order sequence number에러를 발생시킨다. (2번 메시지가 와야하는데 23번 메시지가 온 상황)

작동이 실패했을 경우

  • 프로듀서 재시작

프로듀서에 장애가 발생할 경우, 보통 새 프로듀서를 생성해서 장애가 난 프로듀서를 대체한다.(쿠버네티스와 같은 장애 복구 프레임워크 사용)

멱등적 프로듀서기능이 켜있을 경우 브로커로부터 프로듀서 ID를 생성받는다. (꺼져있을 경우 새로운 ID발급) 따라서 프로듀서ID와 시퀀스 넘버를 가지고 메시지 중복체크를 진행한다.

  • 브로커 장애

브로커에 장애가 발생할 경우 파티션에 대해 새 리더를 선출하게 된다.

리더의 경우 새 메시지가 쓰여질 때 마다 인-메모리 프로듀서 상태에 최근 N개의 시퀀스 넘버를 가지고 있다.

팔로워 레플리카는 리더로부터 새로운 메시지를 받을 때 마다 자체적인 인-메모리 버퍼를 업데이트한다. 즉, 팔로워가 리더가 되어도 이 버퍼를 활용해 중복처리를 진행한다.

+) 예전 리더가 다시 돌아와서 인-메모리 버퍼가 없어도, 상태에 대한 스냅샷을 파일형태로 저장해서 복구작업을 진행한다.

8.1.2 멱등적 프로듀서의 한계

카프카의 멱등적 프로듀서는 프로듀서 내부의 중복 로직만을 방지한다. 따라서 producer.send()로 똑같은 메시지를 2번 보내면 메시지 중복이 발생하게 된다.

이러한 예외처리는 어플리케이션이 하는 것 보단 카프카 프로듀서의 멱등적 기능을 활용하는 것이 낫다.

8.1.3 멱등적 프로듀서 사용법

프로듀서 설정에 enable.idempotence=true를 추가한다. 만약 acks=all이면 성능에는 큰 차이가 없다.

멱등적 프로듀서 기능의 특징으로써는

  • 프로듀서 ID를 받아오기 위해 프로듀서 시동 과정에서 API를 하나 더 호출
  • 각각의 레코드에는 프로듀서ID와 메시지 시퀀스 넘버가 포함됨
  • 각각의 레코드 시퀀스 넘버 예외처리(중복 방지)
  • 장애가 발생해도 파티션에 쓰여진 메시지들의 순서는 보장된다.

8.2 트랜잭션

트랜잭션 기능은 카프카 스트림즈를 사용해서 개발된 어플리케이션에 정확성을 보장하기 위해 도입되었다.

8.2.1 트랜잭션 활용사례

주로 금융 어플리케이션에서의 복잡한 스트림 처리 어플리케이션에는 트랜잭션이 활용된다.

8.2.2 트랜잭션이 해결하는 문제

보통 플로우는 다음과 같다.

  1. 이벤트를 읽는다.
  2. 처리를 한다.
  3. 다른 토픽에 쓴다.

이 과정에서 다음과 같은 에러가 발생할 수 있다.

1. 어플리케이션 크래시로 인한 재처리

어플리케이션은 특정 처리를 하고, 오프셋을 커밋해야 한다. 하지만, 특정 처리를 하고 어플리케이션이 종료된다면? 하트비트가 끊어지면서 리밸런스가 발생하고, 다른 컨슈머가 새로운 파티션을 할당받아 중복된 처리를 진행한다.

2. 좀비 어플리케이션에 의해 발생하는 재처리

위의 상황을 겪었다가 갑자기 어플리케이션이 다시 작동한다면?(오프셋을 또 커밋해버린다면) 다른 컨슈머가 해당 파티션이 할당받은 상태를 알아차릴 때 까지 이 작업을 계속한다면 메시지 중복이 발생할 수 있다.

8.2.3 트랜잭션은 어떻게 정확히 한 번을 보장하는가?

읽기, 처리, 쓰기 작업이 원자적으로 이루어져야 한다.

카프타 트랜잭션은 원자적 다수 파티션 쓰기기능을 도입했다.

이는 메시지 커밋을 다수 파티션에 원자적으로 쓰는 것을 의미한다. (커밋 메시지에 consumer-offsets를 같이 넣음)

이를 활용하기 위해서는 트랜잭션적 프로듀서를 활용해야 한다. 트랜잭션적 프로듀서와 보통 프로듀서의 차이점이란 transactional.id 설정이 잡혀있어 내부적으로 프로듀서의 아이디와 비교하여 메시지 처리를 진행하게 된다.

프로듀서 뿐만 아니라 컨슈머 격리 수준을 조절해야 한다.(isolation.level 설정 값)

  • read_committed
    • 커밋된 트랜잭션에 속한 메시지나 처음부터 트랜잭션에 속하지 않은 메시지만 리턴(트랜잭션진행 중 메시지는 리턴 X)
  • read_uncommitted(default)
    • 트랜잭션에 속하고 있는 메시지도 리턴

read_comitted 모드로 작동중인 컨슈머는 read_uncommitted로 작동중인 컨슈머보다 더 뒤에 있는 메시지를 할당 받는다.

8.2.4 트랜잭션으로 해결할 수 없는 것들

원자적 쓰기 기능, 멱등적 프로듀서를 활용해도 해결할 수 없는 것이 몇 가지 있다.

1. 스트림 처리에 따른 부수효과

어플리케이션 처리 중간에 이메일을 보내는 작업이 포함되어 있다고 하자. 멱등적 프로듀서 기능을 활용한다고 이메일이 한 번만 보내질 것이라 장담할 수 없다. (이는 카프카 레코드에만 적용되는 기능이기에)

따라서 스트림 처리의 외부효과는 정확히 한 번을 제공할 수 없다.

2. 카프카 토픽에서 읽어서 데이터베이스에 쓰는 경우

위와 비슷한 맥락이다. 외부 데이터베이스에는 결과를 쓰고 카프카에는 오프셋을 커밋하는 이 두작업을 트랜잭션을 활용해 구현할 수는 없다.(데이터베이스의 트랜잭션 보장에 달렸다.)

이러한 작업을 수행하기 위해서는 아웃 박스 패턴에 대해 공부해보자.

3. 데이터베이스에서 읽어서, 카프카에 쓰고, 여기서 다른 데이터베이스에 쓰는 경우

이러한 종류의 종단 보장은 제공하지 않는다.

4. 한 클러스터에서 다른 클러스터로 데이터 복제

이는 카프카 트랜잭션이 아니라 미러메이커를 호라용해야 한다.

5. 발행/구독 패턴

메시지를 쓰고 나서 커밋하기 전 다른 어플리케이션이 응답하기를 기다리는 패턴은 지양해야한다. 결과적으로 데드락이 발생하기 때문이다.

read_committed모드를 활용하면 중단된 트랜잭션에 대한 메시지를 모두가 컨슘하지 못하고 데드락이 발생한다.

8.2.5 트랜잭션 활용 법

가장 권장되는 방법은 카프카 스트림즈에서 exactly-once를 활성화 하는 것이다. 이렇게 하면 트랜잭션 기능을 직접적으로 사용할 일은 없지만, 알아서 보장을 해준다.

카프카 스트림즈란??

토픽(topic)에 있는 데이터를 낮은 지연과 빠른 속도로 처리
토픽에 적재된 데이터를 실시간으로 변환하여 다른 토픽에 적재하는 라이브러리

하지만, 카프카 스트림즈를 활용하지 않고 정확히 한번을 보장하고자 한다면 트랜잭션 API를 직접 사용해야한다.

8.3 트랜잭션 성능

트랜잭션은 어쩔 수 없이 약간의 오버헤드를 발생시킨다.

프로듀서를 생성해서 사용하는 동안 트랜잭션 ID 등록 요청은 단 한 번 발생한다. 트랜잭션의 일부로써 파티션들을 등록하는 추가적인 호출은 각 트랜잭션에 있어서 파티션 별로 최대 한 번씩만 이루어진다.

  • 프로듀서에 있어서 트랜잭션 오버헤드는 트랜잭션에 포함된 메시지수와 무관하다.
  • 컨슈머는 read_committed모드로 읽기에 약간 지연이 생긴다.
profile
강단있는 개발자가 되기위하여

0개의 댓글