7장 : 신뢰성 있는 데이터 전달에서는 ‘최소 한 번’ 전달에 초점을 맞췄다면, 이번 장에서는 ‘정확히 한 번’에 초점을 맞춘다. 신뢰성을 보장한다고 하더라도 여전히 메시지 중복의 가능성은 남아있다.
하지만, 이벤트를 읽어서 평균을 구하고 결과값을 산출하는 애플리케이션을 예로 들면, 특정 이벤트가 중복해서 쓰여짐으로써 평균값이 잘못 계산되는 오류가 발생될 수 있고, 이로 인해서 “더 강력한 보장(정확히 한 번 exactly-once semantics)”을 제공할 필요가 있다.
이번 장에서는 카프카의 ‘정확히 한 번’의미구조를 사용하는 방법과, 권장되는 활용 사례, 그리고 그 한계에 대해서 살펴본다.
카프카의 ‘정확히 한 번’의미구조는 두 개의 핵심 기능(명등적 프로듀서 idempotent producer & 트랙잭션 의미구조)의 합으로 이루어진다.
- 멱등적 프로듀서 : 프로듀서 재시도로 인해 발생하는 중복 방지
- 트랜젝션 의미구조 : 스트림 처리 애플리케이션에서 ‘정확히 한 번’ 처리 보장
8.1. 멱등적 프로듀서
- 멱등적(idempotent)이다 : 동일한 작업을 여러 번 실행해도 한 번 실행한 것과 결과가 같다.
우리가 멱증성 의미 구조가 아닌 ‘최소 한 번’ 의미구조를 가지도록 프로듀서를 설정한다면, 프로듀서가 메시지 전송을 재시도함으로써, 메시지가 최소 한 번 이상 도착할 수 있는 불확실성이 존재하게 됨. 즉, 재시도는 메시지 중복을 발생시킬 수 있음.
- 파티션 리더가 프로듀서로부터 레코드를 받아서 팔로워들에게 성공적으로 복제한다.
- 프로듀서에게 응답을 보내기 전, 파티션 리더가 있는 브로커에 크래시가 발생한다.
- 프로듀서 입장에서는 응답을 받지 못한 채 타임아웃이 발생하고, 메시지를 재전송한다.
- 재전송된 메시지가 새 리더에 도착한다. 하지만 이 메시지는 이미 저장되어 있다(결과적 중복).
어떤 어플리케이션에서는 중복이 크게 문제 되지 않는다. 하지만 어떤 애플리케이션에서는 재고가 맞지 않는다던가, 재무제표가 잘못된다던가, 우산을 하나 구매한 고객에게 2개가 배송된다던가 하는 문제가 발생될 수 있다.
카프카의 멱증적 프로듀서 기능은 자동으로 이러한 중복을 탐지하고 처리함으로써 이 문제를 해결한다.
8.1.1. 멱등적 프로듀서의 작동 원리
멱등적 프로듀서 기능 ON : 모든 메시지는 고유한 프로듀서 ID(Producer ID, PID)와 시퀀스 넘버 sequence number를 가지게 됨.
각 메시지의 고유한 식별자 : 대상 토픽 및 파티션 + PID + Sequence number.
각 브로커는 해당 브로커에 할당된 모든 파티션들에 쓰여진 마지막 5개 메시지들을 추적하기 위해 이 고유 식별자를 사용.
max.in.flights.requests.per.connection(def=5, max=5) : 파티션별로 추적되어야 하는 시퀀스 넘버의 수를 제한하고 싶다면 해당 설정을 5 이하로 잡으면 됨.
브로커가 예전에 받은 적 있는 메시지를 받게 된 경우, 적절한 에러를 발생시킴으로써 중복 메시지를 거부.
해당 에러는 프로듀서에 로깅되고, 지푯값에도 반영되지만, 사용자에게 경보를 보내지는 않음(예외가 발생하는 것이 아니기 때문)
프로듀서 클라이언트에서는 record-error-rate 지푯값을 확인함으로써 에러를 확인할 수 있음.
브로커의 경우 RequestMetrics 유형의 ErrorPerSec지푯값에 기록됨.
만약 브로커가 예상보다 높은 시퀀스 넘버를 받게 된다면, ‘out of order sequence number’ 에러를 발생시킴.
만약 트랜잭션 기능 없이 멱등적 프로듀서만 사용하고 있다면, 이 에러는 무시해도 좋음.
💡 ‘out of order sequence number’ 해당 에러가 발생한 뒤에도 프로듀서가 정상 작동한다면, 이 에러는 보통 프로듀서와 브로커 사이에 메시지 유실이 있었음을 의미함. 만약 로그에 이러한 에러가 찍힌다면,
1. 프로듀서와 브로커 설정을 재점검하고
2. 프로듀서 설정이 고신뢰성을 위해 권장되는 값으로 잡혀있는지 확인하고
3. 언클린 리더 선출이 발생했는지 여부를 확인해 볼 필요가 있음.
작동이 실패했을 경우 멱등적 프로듀서가 어떻게 처리하는지 생각해 볼 필요가 있음. 프로듀서 재시작과 브로커 장애 두 가지 경우를 살펴보면 아래와 같음.
1. 프로듀서 재시작
프로듀서에 장애가 발생할 경우, 보통 새 프로듀서를 생성해서 장애가 난 프로듀서를 대체함.
멱등적 프로듀서기능이 켜있는 경우, 프로듀서는 프로듀서가 시작될 때 초기화 과정에서 카프카 브로커로부터 프로듀서 ID를 생성받음.
트랙잭션 기능을 켜지 않은 경우, 프로듀서를 초기화할 때마다 새로운 PID 가 생성됨.
즉, 프로듀서에 장애가 발생해서 새로 투입된 프로듀서가, 기존 프로듀서가 이미 전송한 메시지를 재전송하는 경우, 브로커는 메시지에 중복이 발생했음을 알아차리지 못함.
기존 프로듀서가 작동을 멈췄다가 새 프로듀서가 투입된 이후 작동을 재개해도, 서로 다른 PID를 가졌기 때문에 기존 프로듀서는 좀비로 취급되지 않음.
2. 브로커 장애
브로커에 장애가 발생한 경우, 컨트롤러는 장애가 난 브로커가 리더를 맡고 있던 파티션들에 대해 새 리더를 선출함.
리더는 새 메시지가 쓰여질 때마다 인-메모리 프로듀서 상태에 저장된 최근 5개의 시퀀스 넘버를 업데이트함. 팔로워 레플리카는 리더로부터 새로운 메시지를 복제할 때마다 자체적인 인-메모리 버퍼를 업데이트함.
즉, 팔로워가 리더가 된 시점에서는 이미 메모리 안에 최근 5개 시퀀스 넘버를 가지고 있는 것.
만약, 예전 리더가 복구되어 돌아온다면?
재시작 후에는 인-메모리 프로듀서 상태는 더 이상 메모리 안에 저장되어있지 않음.
복구 과정에 도움이 될 수 있도록, 브로커는 종료되거나 새 세그먼트가 생성될 때마다 프로듀서 상태에 대한 스냅샷을 파일 형태로 저장함.
브로커가 시작되면 → 일단 파일에서 최신 상태를 읽어옴 → 현재 리더로부터 복제한 레코드를 사용해서 프로듀서 상태를 업데이트 함으로써 최신상태를 복구함 → 해당 브로커가 다시 리더를 맡을 준비가 된 시점에서는 최신 시퀀스 넘버를 가지고 있게 됨.
만약, 브로커가 크래시 나서 최신 스냅샷이 업데이트되지 않는다면?
PID와 시퀀스 넘버는 둘 다 카프카 로그에 저장되는 메시지 형식의 일부임.
크래시 복구 작업이 진행되는 동안 프로듀서 상태는 더 오래된 스냅샷 뿐만 아니라, 각 파티션 최신 세그먼트의 메시지들 역시 사용해서 복구됨.
복구작업이 완료되는 대로 새로운 스냅샷 파일이 저장됨.
만약, 메시지가 없다면?
보존기한은 2시간인데, 지난 2시간 동안 메시지가 하나도 들어오지 않은 토픽이 있다.
(브로커가 크래시 날 경우, 프로듀서 상태를 복구하기 위해 사용할 수 있는 메시지 역시 없을 것).
다행히 메시지가 없다는 얘기는 중복이 없다는 이야기이다.
즉, 이 경우 즉시 새 메시지를 받기 시작해서(프로듀서 상태가 없다는 경고가 로그에 찍힐 것), 새로 들어오는 메시지들을 기준으로 프로듀서 상태를 생성할 수 있다.
8.1.2. 멱등적 프로듀서의 한계
카프카의 멱등적 프로듀서는 프로듀서 내부 로직으로 인한 재시도가 발생한 경우 생기는 중복만을 방지.
동일한 메시지를 가지고 producer.send()를 두 번 호출하면, 멱등적 프로듀서가 개입하지 않는 만큼 중복된 메시지가 생성됨. (프로듀서 입장에선, 전송된 두 개의 레코드가 실제로 동일한 레코드인지 확인할 방법이 없기 때문)
프로듀서 예외를 잡아서 애플리케이션이 직접 재시도하는 것보다는, 프로듀서에 탑재된 재시도 메커니즘을 사용하는 것이 언제다 더 낫다.
멱등적 프로듀서는 이 패턴을 더 편리하게 해 준다.(재시도할 때 중복을 피할 수 있는 가장 쉬운 방법)
여러 개의 프로듀서들 중 두 개가 동일한 메시지를 전송하려 시도하는 경우, 멱등적 프로듀서는 중복을 잡아내지 못함. (PID가 다르기 때문)
이러한 사례는 파일 디렉터리와 같은 원본 데이터를 읽어서 카프카로 쓰는 애플리케이션에서는 흔한 상황.
만약, 동일한 파일을 읽어서 카프카에 레코드를 쓰는 두 개의 애플리케이션 인스턴스가 뜨게 되면, 해당 파일의 레코드는 2번 이상 쓰여질 것.
💡 멱등적 프로듀서는 프로듀서 자체의 (프로듀서, 네트워크, 브로커 에러로 인해 발생하)재시도 매커니즘에 의한 중복만 방지함.
8.1.3. 멱등적 프로듀서 사용법
프로듀서 설정에 enable.idempotence=true를 추가해 주면 됨.
만약 프로듀서에서 acks=all 설정이 이미 잡혀있다면, 성능에는 차이가 없음.
멱등적 프로듀서 기능을 활성화시킬 경우, 아래와 같은 것들이 변화함.
PID를 받아오기 위해 프로듀서 시동 과정에서 API를 하나 더 호출함
전송되는 각각의 레코드 배치에는 PID(Long)와 배치 내 첫 메시지의 시퀀스 넘버(integer)가 포함되어, 각 메시지 배치에 96비트가 추가됨.
각각의 메시지 시퀀스 넘버 = 첫 메시지 시퀀스 넘버 + 변화량.
작업 부하에 어떠한 오버헤드도 되지 않는 정도의 용량.
브로커들은 모든 프로듀서 인스턴스에서 들어온 레코드 배치의 시퀀스 넘버를 검증해서 메시지 중복을 방지함.
장애가 발생하더라도, 각 파티션에 쓰이는 메시지의 순서는 보장됨.
max.in.flight.request.per.connection 설정값이 1보다 큰 값으로 잡혀도 마찬가지.
8.2. 트랜잭션
아파치 카프카의 트랜잭션 기능은 스트림즈를 사용해서 개발된 애플리케이션에 정확성을 보장하기 위해 도입됨.
스트림 처리 애플리케이션이 정확한 결과를 산출하도록 하기 위해, 각 입력 레코드는 정확히 한 번만 처리되어야 하며, 그 처리 결과 역시 (장애 상황에서도) 정확히 한 번만 반영되어야 함.
따라서, 카프카의 트랜잭션 기능은 스트림 처리 애플리케이션의 기본 패턴인 ‘읽기-처리-쓰기’ 패턴에서 사용하도록 개발됨.
트랜잭션 기능은 이런 맥락에서 ‘정확히 한 번’ 의미구조를 보장할 수 있음.
각 입력 레코드의 처리는 애플리케이션의 내부 상태가 업데이트되고 결과가 출력 토픽에 성공적으로 쓰였을 때에야 완료된 것으로 간주됨.
💡 트랜잭션은 근본적인 메커니즘의 이름. ‘정확히 한 번’ 의미구조 혹은 ‘정확히 한 번’ 보장은 스트림 처리 애플리케이션의 작동을 가리킴. 카프카 스트림즈는 ‘정확히 한 번’ 보장을 구현하기 위해 트랜잭션 기능을 사용함. 스파크 스트리밍 spark streaming이나 플링크 Flink 와 같은 다른 스트림 처리 프레임워크의 경우 사용자에게 ‘정확히 한 번’ 의미구조를 제공하기 위해 다른 메커니즘을 사용함.
8.2.1. 트랜잭션 활용 사례
트랜잭션은 정확성이 중요한 스트림 처리 애플리케이션에 도움이 되며, 스트림 처리 로직에 집적이나 조인이 포함되어 있는 경우 특히 그러함.
만약 스트림 처리 애플리케이션이 개별 레코드 변환과, 필터만을 수행한다면 업데이트할 상태 자체가 없는 만큼 처리과정에서 중복이 발생하더라도 출력 스트림에서 걸러내는 것은 매우 단순함.
스트림 처리 애플리케이션이 다수의 레코드를 집적해서 하나로 만들 경우, 결과 레코드가 잘못되었는지 여부를 판단하는 것은 훨씬 어려움.
몇 개의 입력 레코드가 한 번 이상 처리되었는지 알 수 없기 때문
주어진 입력을 다시 처리하지 않는 한 결과를 교정하는 것은 불가능함.
금융 애플리케이션은 ‘정확히 한 번’ 기능이 정확한 집적 결과를 보장하는데 쓰이는 복잡한 스트림 처리 애플리케이션의 전형적인 예시임.
하지만, 카프카 스트림즈 애플리케이션이 ‘정확히 한 번’ 보장을 제공하도록 설정하는 것이 상당히 단순한 만큼, 챗봇과 같이 더 흔한 활용사례에서도 이 기능이 활용되는 것을 볼 수 있음.
8.2.2. 트랜잭션이 해결하는 문제
원본 토픽으로부터 이벤트를 읽어서, (아마도) 처리를 한 다음 결과를 다른 토픽에 쓰는 단순한 스트림 처리 애플리케이션을 예시로 들어보자.
우리가 처리하는 각 메시지에 대해 결과가 정확히 한 번만 쓰여지도록 하고 싶다. 무엇이 잘못될 수 있을까?
1. 애플리케이션 크래시로 인한 재처리
원본 클러스터로부터 메시지를 읽어서 처리한 뒤, 애플리케이션은 두 가지 작업을 해야 함.
- 결과를 출력토픽에 쓰기.
- 우리가 읽어온 메시지의 오프셋을 커밋하기.
두 작업이 순서대로 실행되었다고 가정할 때, 만약 출력 토픽에는 이미 썼는데, 오프셋은 커밋되기 전에 애플리케이션이 크래시가 난다면?
컨슈머가 크래시 날 경우 → 몇 초가 지난 뒤 하트비트가 끊어짐 → 리밸런스 발생 → 컨슈머가 읽어오고 있던 파티션들은 다른 컨슈머로 재할당됨 → 컨슈머는 새로 할당된 파티션의 마지막으로 커밋된 오프셋으로부터 레코드를 읽어오기 시작.
즉, 마지막으로 커밋된 오프셋으로부터 크래시가 난 시점까지, 애플리케이션에 의해 처리된 모든 레코드는 다시 처리될 것. 결과 역시 출력 토픽에 다시 쓰여질 것.(즉, 중복 발생)
2. 좀비 애플리케이션에 의해 발생하는 재처리
만약, 애플리케이션이 카프카로부터 레코드 배치를 읽어온 직후 뭔가를 하기 전에 멈추거나, 카프카로부터의 연결이 끊긴다면?
하트비트가 끊어짐 → 애플리케이션은 죽은 것으로 간주됨 → 해당 컨슈머에 할당되어 있던 파티션들은 컨슈머 그룹 내 다른 컨슈머에게 재할당됨 → 파티션을 재할당 받은 컨슈머가 레코드 배치는 다시 읽어서 처리 → ㅋ꙼̈출력토픽에 결과를 쓰면서 작업을 계속함.
그 사이 작동을 멈췄던 애플리케이션이 다시 작동하게 되면, 마지막으로 읽어왔던 레코드 배치를 처리하고 결과를 출력 토픽에 작성함.
레코드를 읽어오기 위해 새로 카프카를 폴링 하거나, 하트비트를 보냈다가 자기가 죽은 것으로 판정되어 다른 인스턴스들이 현재 해당 파티션들을 할당받은 상태란걸 알아차릴 때까지 해당 작업은 계속될 수 있음.
스스로가 죽은 지 모르는 컨슈머 = 좀비.
이러한 상황에서 추가적 보장이 없는 경우, 좀비는 출력토픽으로 데이터를 쓸 수 있음.
따라서 중복된 결과가 발생될 수 있음.
8.2.3. 트랜잭션은 어떻게 ‘정확히 한 번’을 보장하는가?
스트림 처리 애플리케이션은 토픽에서 1) 데이터를 읽고 2) 처리하고 3) 결과를 다른 토픽에 쓴다.
‘정확히 한 번’ 처리함의 의미는 1,2,3의 모든 작업이 원자적으로 이루어진다는 말임.
즉, 부분적인 결과가 결코 발생하지 않을 거라는 보장이 필요함.
이러한 작동을 지원하기 위해, 카프카 트랜잭션은 원자적 다수 파티션 쓰기(atomic multipartition write) 기능을 도입함.
오프셋을 커밋하는 것과 결과를 쓰는 것은 결과는 출력 토픽에, 오프셋은 _consumeroffset 토픽에 쓰인다는 점만 다를 뿐, 둘 다 파티션에 메시지를 쓰는 과정을 수반한다는 점에서 착안됨.
만약, 우리가 트랜잭션을 시작해서 양쪽에 메시지를 쓰고, 둘 다 성공해서 커밋할 수만 있다면(혹은 재시도하기 위해 중단할 수 있다면), 그다음부터는 ‘정확히 한 번’ 의미구조가 알아서 해줌.
트랜잭션을 사용해서 원자적 다수 파티션 쓰기를 수행하려면, 트랜잭션적 프로듀서를 사용해야 함.
트랜잭션적 프로듀서와, 보통 프로듀서의 차이 : transactional.id 설정이 잡혀있고 initTransactions()를 호출해 초기화해주었다는 것뿐.
카프카 브로커에 의해 자동 생성되는 producer.id와 transactional.id는 프로듀서 설정의 일부이며, 재시작을 하더라도 값이 유지됨.
transactional.id의 주 용도 : 재시작 후에도 동일한 프로듀서를 식별하는 것.
카프카 브로커는 transactional.id에서 producer.id로의 대응관계를 유지하고 있다가, 만약 이미 있는 transactional.id프로듀서가 initTransactions()를 다시 호출하면 새로운 랜덤값이 아닌 이전에 쓰던 producer.id 값을 할당해 줌.
애플리케이션의 좀비 인스턴스가 중복 프로듀서를 생성하는 것을 방지하려면 좀비펜싱 zombie fencing, 혹은 어플리테이션의 좀비 인스턴스가 출력 스트림에 결과를 쓰는 것을 방지할 필요가 있다.
가장 일반적인 좀비 펜싱 방법인 에포크 epoch를 사용하는 방식이 쓰인다.
카프카는 트랜잭션적 프로듀서가 초기화를 위해 initTransactional()을 호출하면, transactional.id 에 해당하는 에포크 값을 증가시킨다.
같은 transactional.id 값을 가지고 있지만, 에포크 값은 낮은 프로듀서가 메시지 전송, 트랜잭션 커밋, 트랜잭션 중단 요청을 보낼 경우 FencedProducer에러가 발생하면서 거부된다.
이렇게 오래된 프로듀서는 출력 스트림을 쓰는 것이 불가능 하기 때문에 close()를 호출해서 닫아주는것 이외에는 방법이 없다. 즉, 좀비가 중복된 레코드를 쓰는것이 불가능해졌다.
아파치 카프카 2.5 이후로부터는 트랜잭션 메타데이터에 컨슈머 그룹 메타데이터를 추가할 수 있는 옵션이 생김.
이 메타데이터 역시 펜싱에 사용됨 → 좀비 인스턴스를 펜싱 하면서, 서로 다른 트랜잭션 id를 가지는 프로듀서들이 같은 파티션들에 레코드를 쓸 수 있게 됨.
트랜잭션은 대부분 프로듀서 쪽 기능.
즉, 트랜잭션적 프로듀서를 생성하고, 트랜잭션을 시작하고, 다수의 파티션에 레코드를 쓰고, 이미 처리된 레코드들을 표시하기 위해 오프셋을 쓰고, 트랜잭션을 커밋하거나 중단하는 모든 작업이 프로듀서에서 이루어짐.
중단된 트랜잭션에 속할지라도, 트랜잭션 기능을 사용해서 쓰인 레코드는 다른 레코드들과 마찬가지로 파티션에 쓰여짐.
즉, 컨슈머에 올바른 격리 수준이 설정되지 않을 경우 ‘정확히 한 번’은 보장되지 않을 것.
isolation.level 설정값을 잡아줌으로써, 트랜잭션 기능을 써서 쓰여진 메시지들을 읽어오는 방식을 제어.
read_committed : 토픽을 구독한 위 consumer.poll()을 호출하면 커밋된 트랜잭션에 속한 메시지나 처음부터 트랜잭션에 속하지 않은 메시지만 리턴됨.(중단되거나, 진행 중인 트랜잭션의 메시지는 리턴되지 않음)
read_committed라고 해서 특정 트랜잭션에 속한 모든 메시지가 리턴되는 것이 보장되진 않음.
트랜잭션에 속하는 토픽의 일부만 구독했기 때문에, 일부 메시지만 리턴 받을 수 있음.
read_committed인 경우 LSO last stable offset(현재 진행 중인 트랜잭션이 처음으로 시작된 시점) 이후의 메시지는 읽지 않음
프로듀서에 의해 트랙잭션이 중단 혹은 커밋되거나, transaction.timeout.ms 설정값(def:15분)만큼 시간이 지나 브로커가 자동으로 트랜잭션을 중단시킬 때까지 보류됨.
이렇게 트랜잭션이 오랫동안 닫히지 않으면, 컨슈머들이 지체되면서 종단지연이 길어짐.
read_uncommitted(def) : 진행 중이거나 중단된 트랜잭션에 속하는 것들을 포함한 모든 메시지가 리턴.
트랜잭션이 언제 시작되고 끝날 지, 어느 메시지가 어느 트랜잭션에 속하는지에 대해 애플리케이션은 알 수 없음.
스트림 처리 애플리케이션은 입력 토픽이 트랜잭션 없이 쓰여진 경우에도 ‘정확히 한 번’ 의미구조를 보장함.
원자적 다수 파티션 쓰기 기능은, 만약 출력 레코드가 출력토픽에 커밋되었을 경우, 입력 레코드의 오프셋 역시 해당 컨슈머에 대해 커밋되는 것을 보장함. 따라서 중복처리되지 않음.
8.2.4. 트랜잭션으로 해결할 수 없는 문제들
트랜잭션 : 다수의 파티션에 대한 원자적 쓰기 기능과, 스트림 처리 애플리케이션에서 좀비 프로듀서를 방지하기 위해 추가됨.
‘정확히 한 번’ 보장은 카프카에 대한 쓰기 이외의 동작에서는 보장되지 않음.
하나의 컨슈머가 항상 전체 트랜잭션을 읽어온다고 가정하면 안 됨.
카프카 트랜잭션 기능이 ‘정확히 한 번’ 보장에 도움이 되지 않는 몇 가지 경우 예시
1. 스트림 처리에 있어서 부수 효과 side effect
스트림 처리 애플리케이션의 처리단계에서 사용자에게 이메일을 보내는 작업이 포함되어 있는 경우.
‘정확히 한 번’ 의미구조를 활성화한다고 해서 이메일이 한 번만 발송되는 것은 아님.
해당 기능은 ‘카프카에 쓰여지는 레코드’에만 적용되기 때문.
즉, 트랜잭션을 중단 혹은 취소하기 위해 마커를 사용하는 것은 카프카 안에서 만작동하는 것이지, 이미 발송된 이메일을 되돌리지는 않음.
2. 카프카 토픽에서 읽어서 DB에 쓰는 경우
이 경우 애플리케이션은 카프카가 아닌 외부 DB에 결과물을 씀.
여기서는 프로듀서가 사용되지 않음.
즉, 레코드는 JDBC 같은 DB 드라이버를 통해 데이터베이스에 쓰여지고, 오프셋은 컨슈머에 의해 카프카에 커밋됨.
3. 데이터베이스에서 읽어서 카프카에 쓰고 여기서 다시 다른 DB에 쓰는 경우
카프카 트랜잭션은 이러한 종류의 종단보장에 필요한 기능을 가지고 있지 않음.
하나의 트랜잭션 안에서 레코드와 오프셋을 함께 커밋하는 문제 외에도 다른 문제가 있기 때문.
카프카 컨슈머의 read_committed보장은 데이터베이스 트랜잭션을 보존하기엔 너무 약함.
컨슈머가 아직 커밋되지 않은 레코드를 볼 수 없으며, 일부 토픽에서 랙이 발생했을 수도 있을 만큼 이미 커밋된 레코드를 모두 봤을 거라는 보장 또한 없기 때문.
트랜잭션의 경계를 알 수 있는 방법 또한 없기 때문에, 언제 트랜잭션이 시작/중단/읽었는지 알 수 없음.
4. 한 클러스터에서 다른 클러스터로 데이터 복제
5. 발행/구독 패턴
8.2.5 트랜잭션 사용법
가장 일반적이고 권고되는 방법 1.
카프카 스트림즈에서 exactly-once보장을 활성화하는 것.
processing.guarantee설정을 exactly_once 나 excatly_once_beta로 잡아주면 됨.
💡 excatly_once_beta : 크래시가 나거나 트랜잭션 전송 중에 멈춘 애플리케이션 인스턴스를 처리하는 방법이 다름. 카프카 브로커 2.5, 카프카 스트림즈 2.6에 도입됨 하나의 트랜잭션적 프로듀서에서 더 많은 파티션을 효율적으로 다룰 수 있음
트랜잭션 기능을 직접적으로 사용할 일은 전혀 없지만, 카프카 스트림즈가 대신 해당 기능을 사용해서 우리가 필요로 하는 보장을 제공함.
카프카 스트림즈를 사용하지 않으면서, ‘정확히 한 번’ 의미구조를 보장하고 싶다면
트랜잭션 API를 직접 사용함.
8.2.6. 트랜잭션 ID와 팬싱
트랜잭션 ID : 동일 애플리케이션 인스턴스가 재시작했을 때에는 일관적으로 유지되는 반면, 서로 다른 애플리케이션 인스턴스에 대해서는 서로 달라야 한다는 점이 중요함.
2.5 버전까지 : 펜싱을 보장하는 유일한 방법은 트랜잭션 ID를 파티션에 정적으로 대응시켜 보는 것뿐.
각 파티션이 항상 단 하나의 트랜잭션 ID에 의해 읽혀짐을 보장.
우리가 원하는 것 : 트랜잭션 ID는 똑같지만, 에포크 값은 더 높은 새로운 프로듀서에 의해 대체되는 것.
2.5 버전까지 : 스레드에 할당되는 트랜잭션 ID는 랜덤 하게 결정되고, 동일한 파티션에 쓰기 작업을 할 때 언제나 동일한 트랜잭션 ID가 쓰일 거라는 보장이 없었음.
현재 : KIP-447은 트랜잭션 ID와 컨슈머 그룹 메타데이터를 함께 사용하는 펜싱을 도입.
프로듀서의 오프셋 커밋 메서드를 호출할 때 단순한 컨슈머 ID 가 아닌, 컨슈머 그룹 메타데이터를 인수로 전달.
8.2.7. 트랜잭션의 작동 원리
8.3. 트랜잭션 성능
트랜잭션은 프로듀서에 약간의 오버헤드를 발생시킴.
프로듀서를 생성해서 사용하는 동안 트랜잭션 ID 등록 요청은 단 한번 발생
트랜잭션의 일부로서 파티션들을 등록하는 추가적인 호출은 각 트랜잭션에 있어서 파티션별로 최대 한 번씩만 이루어짐
각 트랜잭션이 커밋요청을 전송하면, 파티션마다 커밋 마터가 추가됨.
트랜잭션 초기화와 커밋 요청은 동기적으로 작동하기 때문에 성공/실해/타임아웃 할 때까지 어떤 데이터도 전송하지 않음.
즉, 오버헤드는 더 증가함.
트랜잭션마다 많은 수의 메시지를 넣는 쪽이 상대적으로 오버헤드가 적음
컨슈머 쪽에서는 커밋 마커를 읽어오는 작업과 관련된 약간의 오버헤드 존재.(read_committed)
컨슈머는 아직 완료되지 않은 트랜잭션에 속하는 메시지를 버퍼링 할 필요 없음.
브로커는 컨슈머가 보낸 읽기 요청을 받는다고 해서 이 메시지를 리턴하지 않음.
따라서, 트랜잭션 데이터를 읽을 때 컨슈머 쪽에 추가적인 작업은 없음.
'Web_Backend > Kafka' 카테고리의 다른 글
[Kafka] 6장 카프카 내부 매커니즘 (1) | 2023.12.26 |
---|---|
[Kafka] 4장. 카프카 컨슈머 (0) | 2023.11.02 |
[Kafka] 3장. 카프카 프로듀서 : 카프카에 메시지 쓰기 (1) | 2023.11.02 |
[Kafka] 1. 카프카 시작하기 (0) | 2023.11.02 |
야나의 코딩 일기장 :) #코딩블로그 #기술블로그 #코딩 #조금씩,꾸준히
포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!