07. 신뢰성 있는 데이터 전달
시스템에 있어 신뢰성은 매우 중요한 속성 중 하나이다. 웹사이트의 클릭 추적, 신용카드 결제 등 카프카를 통한 신뢰성 있는 데이터 전달에 많은 활용 사례가 있다. 최고의 신뢰성 / 처리속도와 단순성을 유연하게 처리하는 카프카를 알아보자
# 신뢰성 보장
보장 (guarantee) : 서로 다른 상황에서도 시스템이 지킬 것이라 보장되는 행동
- e.g. 관계형 데이터베이스
- ACID를 통해 신뢰성 보장
- 어떤 DB가 ACID를 준수한다고 하면, 트랜잭션 처리 관련하여 어떠한 행동을 보장한다
카프카가 보장하는 것
- 파티션 안의 메세지들 간에 순서 보장 (메세지 A 다음 B가 쓰여졌다면,
B의 오프셋 > A의 오프셋
보장) - 클라이언트가 쓴 메세지는 모든 인-싱크 레플리카의 파티션에 쓰여진 뒤에야 커밋된 것으로 간주
- 프로듀서는 아래의 옵션 선택 가능
- 메세지가 완전히 커밋된 다음 응답이 올지
- 파티션 리더에게 쓰여진 다음 응답이 올지
- 네트워크로 전송된 다음 바로 응답이 올지
- 프로듀서는 아래의 옵션 선택 가능
- 커밋된 메세지들은 최소 1개의 작동 가능한 레플리카가 남아있으면 유실되지 않음
- 컨슈머는 커밋된 메세지만 읽을 수 있음
카프카는 트레이드오프를 고려하여 어느 정도의 신뢰성이 필요한지 결정할 수 있도록 개발됨
- 신뢰성과 일관성 우선
- 가용성
- 높은 처리량 (high throughput)
- 낮은 지연 (low latency)
- 하드웨어 비용
# 복제
카프카의 신뢰성 보장 핵심
- 파티션별로 다수의 레플리카를 유지하는 특성
- 복제 메커니즘
# 메커니즘
복제 메커니즘 : 하나의 메세지를 여러 개의 레플리카에 씀으로써 카프카는 크래시가 나더라도 메세지의 지속성을 유지
6장 리마인드
- 토픽 > 파티션 > 메세지
- 하나의 파티션은 하나의 디스크에 저장
- 파티션에 저장된 이벤트(메세지)들의 순서 보장
- 파티션은 온라인 상태(사용 가능 🟢) / 오프라인 상태(사용 가능❌)
- 각 파티션은 다수의 레플리카 가질 수 있고, 그중 하나가 리더가 됨
- 모든 이벤트들은 리더 레플리카에 write (대체로)
- 팔로워 레플리카들은 리더와 동기화 맞추면서 최신 이벤트를 재시간에 복사해오면 됨
- 리더가 작동 불능 상태가 되면, 인-싱크 레플리카 중 하나가 새로운 리더로 선출
인-싱크 레플리카(ISR) 상태로 간주되는 기준
- 파티션의 리더 레플리카
- 아래의 조건을 만족하는 팔로워 레플리카
- 주키퍼와의 활성 세션 존재 (최근 6초 내에 주키퍼로 하트비트 전송한적 있는지)
- 최근 10초 내에 리더로부터 메세지 읽어온적 있는지
- 최근 10초 내에 리더로부터 읽어온 메세지들이 가장 최근 메세지인지
최근 10초 내에 랙이 없었던 적이 한 번은 있어야한다의 의미
- 랙 (lag) : 팔로워가 리더로부터 데이터를 얼마나 지연되게 받고 있는지에 대한 지표
- e.g. 리더에 쓰여진 최신 메세지의 offset이 105, 팔로워가 읽어온 메세지의 offset이 102이면, lag은 3
- 즉, 팔로워 레플리카가최근 10초 동안 리더 레플리카의 업데이트 내역을 따라가지 못한다는 의미
- 랙이 있다면 이미 뒤쳐졌다는 의미이니까
아웃-오브-싱크 레플리카
- 인-싱크 레플리카가 아니면 아웃-오브-싱크 레플리카
- 레플리카와 주키퍼 사이의 연결이 끊어짐
- 새 메세지 읽어오는 것을 주단
- 최근 10초 동안의 업데이트 내역을 따라가지 못함
- 카프카 예전 버전에서는 인 - 아웃 싱크 상태를 빠르게 교차하는 것이 매우 흔한 일이었음
- 최대 요청 크기가 커서 JVM 힙을 많이 잡고 있는 경우, 튜닝을 안해주면 브로커의 GC 시간이 오래 걸리면서 순간적으로 주키퍼와의 연결이 끊어질 수 있었음
- 큰 메세지를 사용할 경우 튜닝이 필요
결국은 트레이드오프 !
- 동기화가 살짝 늦은 인-싱크 레플리카
- 프로듀서와 컨슈머를 느리게 만들 수 있음
- 메세지가 커밋되기 전, 모든 인-싱크 레플리카가 해당 메세지를 받을 때까지 프로듀서와 컨슈머가 기다려야함
- 아웃-오브-싱크 레플리카
- 성능에 영향 없음
- 더이상 이 레플리카가 메세지를 받을 때까지 기다릴 필요 없으니까
- 성능에 영향 없음
- 인-싱크 레플리카의 수 ↓ + 아웃-오브-싱크 레플리카의 수 ↑
- 파티션의 실질적인 복제 팩터가 줄어들면서 중단 시간 길어짐
- 데이터 유실 위험성 높아짐
# 브로커 설정
메세지 저장 신뢰성 관련된 브로커의 설정 매개변수 3개 존재
- 브로커 단위로 제어 가능 (시스템 내부 모든 토픽 단위로 제어 가능)
- 특정 토픽 단위로 제어 가능
즉, 신뢰성이 필요한 토픽, 필요하지 않은 토픽을 같은 카프카 클러스터에 저장할 수 있음
# 복제 팩터
replication.factor
: 토픽 단위 설정default.replication.factor
: 자동으로 생성되는 토픽들에 적용되는 브로커 단위 설정
복제 팩터 (Replication Factor) : 토픽의 파티션의 복제본을 몇 개를 생성할 지
- e.g. 복제 팩터가 3이다 → 각 브로커가 3대의 서로 다른 브로커에 3개 복제된다는 것
- 복제 팩터가
N
이면N - 1
개의 브로커가 중단되더라도 토픽의 데이터를 읽거나 쓰기 가능 🟢 - 복제 팩터가 클수록
- 가용성과 신뢰성 ↑
- 장애가 발생할 가능성 ↓
- 필요한 브로커 수 ↑
- 필요한 디스크 공간 ↑
- e.g. N개의 브로커가 필요하다 → N개의 복사본 저장해야한다 → N배의 디스크 공간 필요하다
- 가용성 - 하드웨어 사용량 사이 트레이드오프 존재
Q. 토픽에 몇 개의 레플리카가 적절할까? 아래의 핵심 사항을 고려
가용성 (availability)
- 레플리카가 1개인 파티션 → 브로커를 재시작하기만 해도 작동 불능
- 레플리카 수 많을수록 → 가용성 ↑
지속성 (durability)
- 각 레플리카는 파티션 내부의 모든 데이터(메세지)에 대한 복사본
- 레플리카가 1개이고 디스크가 사용 불가능한 상태 → 해당 파티션의 모든 데이터 유실됨
처리량 (throughput)
- 클러스터의 크기와 용량 산정시 고려 필요
- 레플리카 수 많을수록 → 브로커 간 트래픽 ↑
- 복제 트래픽 : 데이터 복제 과정에서 발생하는 네트워크 트래픽
- e.g. 특정 파티션에 10Mbps 속도로 쓰는 상태
- 레플리카 1개 → 복제 트래픽 X
- 레플리카 2개 → 복제 트래픽 10Mbps
- 레플리카 3개 → 복제 트래픽 20Mbps
- e.g. 특정 파티션에 10Mbps 속도로 쓰는 상태
종단 지연 (end-to-end delay, OWD : one-way delay)
- 네트워크의 출발지에서 목적지까지 도달하는데 걸리는 시간
- RTT(round-trip time)는 왕복 시간을 의미
- RTT와는 다르게 단방향으로 측정
- 쓰여진 메세지를 컨슈머가 읽으려면 모든 인-싱크 레플리카에 복제되어야 함
- 레플리카 수 많을수록 → 하나가 느려져서 컨슈머까지 함께 느려질 가능성 ↑
- 사실, 복제 팩터와 무관하게 특정 브로커 느려지면 다 느려짐
- 특정 브로커 느려짐 → 해당 브로커 사용하는 모든 클라이언트 느려짐
비용
- 레플리카 수 많을수록 → 저장소, 네트워크에 들어가는 비용 ↑
- 중요하지 않은 데이터는 복제 팩터 3 미만으로 잡자
- 복제 팩터를 2로 잡아서 저장 비용을 합리적으로 줄이기
레플리카의 위치 또한 중요 !
- 카프카는 같은 파티션의 레플리카들을 언제나 서로 다른 브로커에 저장하는데, 이러한 레플리카의 위치는 안전하지 않을 수 있음
- e.g. 같은 파티션의 모든 레플리카들이 같은 랙에 설치되어 있는 브로커들에 저장됨
- 랙 스위치 오작동 → 복제 팩터와 상관없이 해당 파티션 사용 ❌
- 랙 단위 사고 방지 위해 브로커를 서로 다른 랙에 배치하고
broker.rack
매개변수에 랙 이름 잡아서 방지하자- 랙 이름 설정하면 서로 다른 랙에 분산되어 저장되도록 하여 가용성 ↑
# 언클린 리더 선출
- 브로커 단위에서만 제어 가능 (실제로는 클러스터 단위)
unclean.leaderelection.enable
- default : false
클린 리더 선출
- 파티션의 리더가 더이상 작동 불능하여 인-싱크 레플리카 중 하나가 새로운 리더가 되는 경우
- 커밋된 데이터에 아무런 유실이 없음을 보장
- 커밋된 데이터의 정의가 모든 인-싱크 레플리카에 존재하는 데이터니까
언클린 리더 선출
- 작동 불능한 리더 외에 인-싱크 레플리카가 없는 경우
- (1) 아웃-오브-싱크 레플리카를 리더로 선출
- (2) 원래 리더가 복구될 때까지 대기
언클린 리더 선출 시나리오 (1)
e.g. 파티션에 3개의 레플리카 존재, 팔로워 2개가 작동 불능 (e.g. 브로커가 크래시 난 경우)
- 프로듀서는 리더에 쓰기 작업을 계속 진행할 것
- 모든 메세지는 커밋되고 응답이 감 (리더가 유일한 ISR 이니까)
- 이후, 브로커 크래시로 리더 또한 작동 불능이 됨
- → 아웃-오브-싱크 레플리카 중 하나가 먼저 시작되면, 해당 파티션에서 유일하게 사용 가능한 레플리카가 아웃-오브-싱크 레플리카가 됨
언클린 리더 선출 시나리오 (2)
e.g. 파티션에 3개의 레플리카 존재, 팔로워 2개의 복제 작업이 뒤쳐짐 (e.g. 네트워크 문제)
- 복제 작업은 계속 되지만, 뒤쳐져서 인-싱크 상태는 아님
- 리더 레플리카만 유일한 ISR이라 계속 메세지 받음
- 이후, 브로커 크래시로 리더 또한 작동 불능이 됨
- → 이러한 경우 리더가 될 수 있는 레플리카는 아웃-오브-싱크 레플리카밖에 없음
언클린 리더를 선출하기 위한 결정
(1) unclean.leader.election.enable=false
- 아웃-오브-싱크 레플리카가 새로운 리더가 될 수 있도록 허용 ❌
- 예전 리더가 복구될 때까지 대기
- 해당 파티션은 오프라인 상태
(2) unclean.leader.election.enable=true
- 아웃-오브-싱크 레플리카가 새로운 리더가 될 수 있도록 허용 🟢
- 새로운 리더가 동기화를 못한 사이에 예전 리더에 쓰여졌던 모든 메세지들이 유실됨
- 컨슈머 입장에서의 일관성 깨짐
일관성 깨지는 이유
e.g. 레플리카 1(리더), 2(팔로워), 3(팔로워)가 있고, 2와 3이 작동 불능 상태라고 하자
- 작동 불능 상태 이후 1이 오프셋 100~200에 해당하는 메세지를 작성함
- 그러면 2와 3은 0~99만 있고 100~200은 없음
- 이후 1이 작동 불능되고 2가 새로운 리더가 되었다고 하자
- 2는 오프셋 100~200에 해당하는 새로운 메세지 작성함
- 오프셋 100~200에 해당하는 메세지가 다르군요 ~
# 최소 인-싱크 레플리카
min.insync.replicas
: 토픽 단위, 브로커 단위 모두 설정 가능- default : 1
데이터가 커밋되었다 → 모든 인-싱크 레플리카에 쓰여졌다
- 그런데, 이때 모든은 단 1개의 레플리카를 의미할 수도 있음
- 이 단 1개의 레플리카가 작동 불능에 빠짐 → 데이터 유실
- 그래서 최소 인-싱크 레플리카 수를 충족하는 파티션에만 메세지 쓰기가 가능하도록 위와 같은 옵션이 생김
e.g. 토픽에 레플리카가 3개 있고, min.insync.replicas = 2
- 프로듀서들은 3개의 레플리카 중 최소 2개가 인-싱크 상태인 파티션에만 쓰기 가능
- 2개의 레플리카가 작동 불능에 빠지면 브로커는 더이상 쓰기 요청 받을 수 없음
- 이때 데이터를 전송하려고 시도한 프로듀서는
NotEnoughReplicasException
예외 받음 - 이때 존재하는 데이터에 한해서 컨슈머는 계속 읽기 작업 가능
- 즉, 사실상 살아남은 1개의 레플리카는 읽기 전용이 되어버림
- 이때 데이터를 전송하려고 시도한 프로듀서는
- 읽기 전용 상태에서 회복하려면, 최소 인-싱크 레플리카 수를 충족할 때까지 대기해야함
- 아웃-오브-싱크 레플리카가 브로커 재시작하고 복구 후에 리더 레플리카 상태 따라잡을 때까지
# 레플리카 인-싱크 상태 유지하기
- 브로커 단위에서만 제어 가능 (실제로는 클러스터 단위)
zookeeper.session.timeout.ms
- default : 1800ms (18s) (2.5.0 버전부터, 이전 버전까진 6초였음)
replica.lag.time.max.ms
- default : 3000ms (30s) (2.5.0 버전부터, 이전 버전까진 10초였음)
아웃-오브-싱크 레플리카 상태가 될 수 있는 2가지 이유
- 주키퍼와의 연결 끊어짐
- 리더 업데이트 내역을 따라가는데 실패하여 복제 랙 발생
zookeeper.session.timeout.ms
- 카프카 브로커 → 주키퍼으로 하트비트 전송을 멈출 수 있는 최대 시간
- 이 간격 안에만 하트비트를 보내면 주키퍼는 브로커가 죽었다고 판단 ❌ → 클러스터에서도 제외하지 않음
- 클라우드 환경은 네트워크 지연 변동폭이 커서, 이러한 안정성 증대를 위해 최신 버전부터 default 값 증가
- 값 설정 가이드
- GC나 네트워크 상황 같이 무작위적인 변동에 영향 받지 않을 만큼 높게
- 실제로 작동이 멈춘 브로커가 적시에 탐지될 수 있을 만큼 충분히 낮게
replica.lag.time.max.ms
- 이 값보다 더 오랫동안 리더로부터 데이터를 읽어오지 못함 + 리더에 쓰여진 최신 메세지를 따라잡지 못함
- → 아웃-오브-싱크 레플리카
- 클러스터의 회복 탄력성 증대 + 불필요한 변동 피하기 위해 최신 버전부터 default 값 증가
- 컨슈머의 최대 지연에도 영향을 줌
- 해당 설정값 넘기기 전에는 인-싱크 레플리카라는 의미
- 메세지가 모든 레플리카에 도착하여 커밋된 데이터가 되어야 컨슈머가 읽을 수 있음
- default가 30s니까 컨슈머가 메세지 읽으려면 최대 30초까지 기다릴 수도 있다는 의미
# 디스크에 저장
flush.messages
: 디스크에 저장되지 않은 최대 메세지 수를 조절flush.ms
: 얼마나 자주 디스크에 메세지를 저장하는지를 조절
메세지를 디스크로 플러시하는 경우
- 카프카가 직접 메세지를 디스크로 플러시
- 세그먼트를 교체할 때 (default : 1GB)
- 재시작 직전
- 그외에는 리눅스의 페이지 캐시 기능에 의존
- 페이지 캐시 공간이 다 찼을 경우에만 메세지를 플러시
Q. 위처럼 하는 이유?
A. 보통 서로 다른 랙이나 가용 영역에 레플리카가 위치하게 되는데, 리더의 디스크에 메세지를 쓰는 것보다 더 안전하다는 판단이 있어서
# 신뢰성 있는 시스템에서 프로듀서
- 브로커에 사용 가능한 가장 높은 신뢰성 설정 적용한 경우
- 프로듀서 역시 신뢰성 있도록 설정하지 않으면 시스템 전체적으로 봤을 때 여전히 데이터 유실될 수 있음
# 신뢰성 없는 시스템 예시
- 프로듀서를 신뢰성 있게 설정하지 못하여, 시스템 전체적으로 영향을 끼칠 수 있는 문제를 살펴보자
첫번째 시나리오
(1) 토픽별로 3개의 레플리카를 가지도록 브로커 설정, 언클린 리더 선출 기능 false
- 카프카 클러스터에 커밋된 메세지 유실되지 않음
(2) 프로듀서가 메세지를 보내는 과정에서 acks=1
설정으로 보냄
(3) 프로듀서에서 메세지 전송하여 리더에 쓰여짐
- 인-싱크 레플리카에 반영되지는 않은 상태
(4) 리더가 프로듀서에게 성공 응답 보낸 직후, 크래시가 나서 데이터가 레플리카로 복제 ❌
(5) 다른 레플리카들은 여전히 인-싱크 상태니까 클린 리더 선출로, 이 중 하나가 리더로 됨
- (4)에서 크래시 난 리더가 아웃-오브-싱크 상태 될 때까지는 시간이 좀 걸려서 아직 인-싱크인거
(6) 메세지가 레플리카에 반영되지 않은 만큼 메세지 유실
(7) 메세지를 쓰고 있는 애플리케이션 입장에서는 성공적으로 썼다고 착각
- 어떤 컨슈머도 해당 메세지를 못봄
- 레플리카 입장에서는 본 적 없는 메세지라서 커밋된 것도 아님
- → 따라서, 시스템의 일관성은 유지됨
- → 그러나 프로듀서의 입장에서 보면 메세지는 유실된 것
두번째 시나리오
(1) 토픽별로 3개의 레플리카를 가지도록 브로커 설정, 언클린 리더 선출 기능 false
- 카프카 클러스터에 커밋된 메세지 유실되지 않음
(2) 프로듀서가 메세지를 보내는 과정에서 acks=all
설정으로 보냄
(3) 카프카에 메세지를 쓰려고 하는데, 쓰고 있는 파티션의 리더 브로커가 크래시나고 새 리더는 아직 선출중이라고 가정
(4) 카프카는 “Leader not Available” 응답 보냄
(5) 프로듀서가 올바르게 에러 처리 안함 + 성공할 때까지 재시도 안함 → 메세지 유실될 수 있음
- 브로커는 메세지를 받지도 않았으니까 브로커의 신뢰성과 관련 ❌
- 컨슈머 역시 메세지 받지도 않았으니까 일관성 문제 ❌
결론
- 신뢰성 요구 조건에 맞게 프로듀서에 올바른 acks 설정하기
- 설정과 코드 모두에서 에러를 올바르게 처리하기
# 프로듀서 응답 보내기
프로듀서가 선택 가능한 세 가지 응답 모드
acks=0
- 프로듀서가 네트워크로 메세지를 전송한 시점에서 메세지가 성공적으로 쓰여진 것으로 간주
- 에러 발생하는 경우
- 전송하는 객체가 직렬화 될 수 없음
- 네트워크 카드 오작동
- 에러 발생하지 않는 경우
- 파티션 오프라인
- 리더 선출이 진행중
- 전체 카프카 클러스터가 작동 불능
- 특징
- 지연 낮음
- 지연이 낮아서 이 설정으로 벤치마크 돌릴 때 많음
- 종단 지연이 개선되지는 않음
- 컨슈머는 메세지가 모든 인-싱크 레플리카로 복제되지 않는 한 해당 메세지를 볼 수 없으니까
- 지연 낮음
acks=1
- 리더가 메세지를 받아서 파티션 데이터 파일에 쓴 직후 응답 또는 에러 보냄 (디스크에 반영될 필요 ❌)
- 데이터 유실될 수 있는 경우
- 일부 메세지가 리더에 성공적으로 쓰여져서 클라이언트로 응답이 간 상태
- 팔로워로 복제 완료되기 전에 리더가 정지하거나 크래시 날 경우 데이터가 유실될 수 있음
- 불완전 복제 파티션 (under-replicated partition, URP)
메세지를 복제하는 속도 < 리더에 메세지를 쓰는 속도
가 가능해서 발생하는 문제- 리더 입장에서는 팔로워로 복제 완료 이전에 프로듀서에게 응답 먼저 하기 때문
acks=all
- 리더가 모든 인-싱크 레플리카가 메세지를 받아갈 때까지 대기 후 응답 또는 에러 보냄
- 브로커의
min.insync.replicas
설정과 함께, 응답이 오기 전까지 얼마나 많은 레플리카에 메세지가 복제될 것인지를 조절할 수 있게 해줌 - 특징
- 가장 안전한 옵션
- 프로듀서는 메세지가 완전히 커밋될 때까지 계속해서 메세지 재전송
- 프로듀서 지연이 가장 길어지는 옵션
- 프로듀서는 모든 인-싱크 레플리카가 메세지 받을 때까지 대기 → 이후 해당 메세지 배치에 완료 표시하고 작업 짆애
# 프로듀서 재시도 설정
프로듀서의 에러 처리
- 프로듀서가 자동으로 처리해주는 에러
- 프로듀서 라이브러리 사용하는 개발자들이 처리해야 하는 에러
프로듀서는 재시도 가능한 에러(retriable error)를 자동으로 처리 가능
- 프로듀서가 브로커에 메세지를 전송하면 브로커는 성공 혹은 에러 코드 리턴
LEADER_NOT_AVAILABLE
예외 리턴- 프로듀서는 전송 재시도 가능 🟢
- 새로운 브로커가 리더로 선출된 상황이라 두 번쨰 시도는 성공할 것
INVALID_CONFIG
예외 리턴- 프로듀서는 전송 재시도 가능 ❌
- 재전송한다고 설정이 바뀌지 않으니까
메세지가 유실되지 않는 것이 목표
- 재시도 가능 에러 발생 시, 프로듀서가 계속해서 메세지 전송을 재시도하도록 설정
- 3장에서 권장한 것처럼 재시도 수를 기본 설정값(
MAX_INT
, 사실상 무한)으로 두기 - 3장에서 권장한 것처럼 메세지 전송 포기까지 대기할 수 있는 시간
delivery.timeout.ms
설정값을 최대로 잡기
전송 실패한 메세지를 재시도하는 것은 메세지가 중복될 위험 내포
- 실패했다고 생각했지만 실제로 실패가 아닌 메세지
- 재전송된 메세지
- 위의 두 메세지가 모두 브로커에 성공적으로 쓰여지는 경우 있을 수 있음
즉, 각 메세지가 최소 한 번 저장되도록 보장할 수 있지만, 정확히 한 번은 보장할 수 없음
enable.idempotence=true
설정으로 프로듀서가 추가적인 정보를 레코드에 포함할 수 있도록 하고 이를 활용하여 브로커가 재시도로 인한 중복된 메세지를 건너뛸 수 있도록 설정 가능
# 추가적인 에러 처리
프로듀서가 재시도 불가능한 에러에 대해 개발자의 수동 처리 필요
- e.g.
- 메세지 크기와 관련되었거나 인가 관련 에러와 같이 재시도 불가능한 브로커 에러
- 메세지 → 브로커 전송 이전 발생한 에러 (e.g. 객체 직렬화 에러)
- 프로듀서가 모든 재전송 시도를 소진했거나, 재시도 과정에서 프로듀서가 사용하는 가용 메모리가 메세지로 가득 차서 발생하는 에러
- 타임아웃
수동 처리는 애플리케이션의 조건에 따라 적절한 방식 선택
- 잘못된 메세지는 폐기해야 하는가?
- 에러를 로깅해야 하는가?
- 원본 시스템으로부터 메세지를 읽는 것을 중단해야 하는가?
- 원본 시스템에 백프레셔를 적용하여 잠시 메세지 전송을 멈추도록 해야하는가?
- 이 메세지를 로컬 디스크의 디렉토리에 저장해야 하는가?
에러 핸들러로 제어하는게 메세지 재전송이 전부에요
- 그럼 그냥 프로듀서의 재전송 기능 쓰는게 나아요
# 신뢰성 있는 시스템에서 컨슈머
- 컨슈머는 일관성이 보장되는 데이터만 읽음 (커밋된 데이터, 즉, 모든 인-싱크 레플리카에 쓰여진 이후에만 읽으니까)
- 컨슈머가 해야할 일은 어느 메세지까지 읽었고 어디까지 읽지 않았는지를 추적하는 것
- 이것 때문에 컨슈머는 메세지의 오프셋을 커밋
- 이유 : 해당 컨슈머나 다른 컨슈머가 재시작 이후에도 어디부터 작업을 계속할 지 알 수 있어서
- 컨슈머가 메세지를 누락할 수 있는 경우
- 읽기는 했지만, 아직 처리는 완료되지 않은 이벤트들의 오프셋을 커민하는 경우
- 다른 컨슈머가 작업 이어받았을 때, 이 메세지들은 건너뛰고 영원히 처리 ❌
- 컨슈머가 언제 어떻게 커밋되는지 신경써야함
커밋된 메세지 vs 커밋된 오프셋
- 커밋된 메세지 : 모든 인-싱크 레플리카에 쓰여져서 컨슈머가 읽을 수 있는 메세지
- 커밋된 오프셋 : 컨슈머가 특정 파티션 어느 오프셋까지의 모든 메세지를 받아서 처리 완료했는지 알리기 위해 카프카에 보낸 오프셋
# 컨슈머 설정
신뢰성을 갖는 컨슈머를 설정하기 위한 컨슈머 속성
group.id
auto.offset.reset
enable.auto.commit
auto.commit.interval.ms
group.id
- 컨슈머가 구독한 토픽의 모든 메세지를 읽어야한다면 고유한 그룹 ID 필요
- e.g. 2개의 컨슈머가 같은 그룹 ID 가지고 같은 토픽 구독하는 경우
- 해당 토픽 전체 파티션의 서로 다른 부분을 각각 읽음
- 즉, 컨슈머그룹 전체적으로 모든 메세지를 읽고 있는 셈
- 2개의 컨슈머가 반띵해서 읽고 있는 것
- 컨슈머 1개가 구독한 토픽 다 읽으려면 고유한 그룹id 가져야함
auto.offset.reset
- 커밋된 오프셋이 없을 때, 컨슈머가 브로커에 없는 오프셋을 요청할 때 컨슈머가 어떻게 해야할지 결정
earliest
: 유효한 오프셋이 없는 한, 컨슈머는 파티션의 맨 앞에서부터 읽기 시작- 컨슈머는 많은 메세지들을 중복 처리하게 될 수 있음
- 데이터 유실은 최소화 할 수 있음
latest
: 컨슈머는 파티션의 끝에서부터 읽기 시작- 중복 처리 최소화
- 컨슈머가 일부 데이터 누락할 것이 거의 확실
enable.auto.commit
true
: 일정한 시간에 맞춰 컨슈머가 알아서 오프셋 커밋하게 함faule
: 코드에서 직접 오프셋 커밋할지- 자동 오프셋 커밋 기능 장점
- 애플리케이션에서 컨슈머 사용할 때 걱정거리 줄어듬
- 우리가 처리하지 않은 오프셋을 실수로 커밋하는 사태가 발생하지 않도록 보장
- 자동 오프셋 커밋 기능 단점
- 메세지 중복 처리를 개발자가 제어할 수 없음
- 자동 커밋되지 않은 상태에서 컨슈머 멈추면, 컨슈머 재시작 이후 메세지 중복처리 피할 수 없음
auto.commit.interval.ms
- 오프셋 자동 커밋 시, 커밋되는 주기 설정 (default : 5s)
- 더 자주 커밋할수록
- 오버헤드 ↑
- 컨슈머 정지 시 발생할 수 있는 중복의 수 ↓
추가 내용) 컨슈머의 빈번한 리밸런싱
- 신뢰성과 직접적으로 연관된 것은 ❌
- 그러나, 컨슈머가 리밸런스 수행을 위해 너무 자주 멈추면 신뢰성 있다고 하기에는 어려울 것
- 불필요한 리밸런싱과 리밸런싱 발생 시 멈춤을 최소화 하는 방법 고려하자 (4장에서 설명함)
- 불필요한 리밸런싱 최소화 : e.g. 정적 그룹 멤버쉽
- 리밸런싱 발생 시 멈춤 최소화 : e.g. 협력적 리밸런스
# 명시적으로 오프셋 커밋
오프셋을 자동 커밋하지 않고, 직접 수행했을 때 고려해야하는 부분들
(1) 메세지 처리 먼저, 오프셋 커밋은 나중에
- 폴링 루프에서 모든 처리를 하고 루프 사이의 상태는 저장하지 않으면 괜찮음
- 자동 오프셋 커밋 설정 키거나
- 폴링 루프 끝에서 오프셋 커밋하거나
- 루프 안에서 일정 주기로 오프셋 커밋
- 이러면 오버헤드와 중복 처리 회피 사이의 요구 조건 균형 맞추면 됨
- 스레드가 2개 이상 있거나 stateful 처리가 필요한 경우는 컨슈머 객체 스레드가 안전하지 않아서 복잡해짐
(2) 커밋 빈도는 성능과 크래시 발생시 중복 개수 사이의 트레이드 오프다
- 폴링 루프에서 모든 처리를 하고 루프 사이의 상태는 저장하지 않는 경우 아래 옵션 선택 가능
- 루프 안에서 여러 번 커밋하거나
- 루프가 몇 번 지나갈 때마다 커밋하거나
- 커밋 작업 → 높은 오버헤드
- 프로듀서의
acks=all
설정과 함께 쓰기 작업 수행하는 것과 비슷 - 특정 컨슈머 그룹의 모든 오프셋 커밋이 동일한 브로커로 간다는 점이 다르긴 함
- 프로듀서의
- 커밋 주기 → 성능과 중복 발생의 요구 조건 사이에서 균형 맞추기
- 메세지 읽어올 때마다 커밋하는건 매우 드물게 메세지 들어오는 토픽에나 쓸 수 있음
(3) 정확한 시점에 정확한 오프셋을 커밋하자
- 폴링 루프 중간에서 커밋할 때 흔히 하는 실수
- 원래는, 마지막으로 처리된 메세지의 오프셋 가져와야함
- 근데 마지막으로 읽어온 메세지의 오프셋을 커밋하는 경우 많음
- 읽기는 했지만 처리되지 않은 메세지의 오프셋을 커밋하지 말라는 말
- → 컨슈머 입장에서 메세지가 누락될 수 있음
(4) 리밸런스
- 컨슈머 리밸런스가 발생할 것이라는 것을 기억해야 함
- 할당된 파티션 해제 되기 전에 오프셋을 커밋
- 새로운 파티션이 할당되었을 때 애플리케이션이 보유하고 있던 상태를 삭제해주는 작업
- 이러한 작업들이 포함됨
(5) 컨슈머는 재시도를 해야 할 수도 있다
poll()
호출하고 레코드 처리 → 처리가 완료되지 않은 일부 레코드는 나중에 처리되어야 할 수 있음- 전통적인 발행/구독 메세지 전달 시스템 → 응답을 보냄
- 카프카 컨슈머 → 오프셋을 커밋할 뿐, 각각의 메세지에 응답을 보내지 않음
- 방식
- 동기 방식 재시도
- 재시도 가능한 에러가 발생했을 경우 → 마지막으로 처리에 성공한 레코드의 오프셋을 커밋
- 나중에 처리해야 할 레코드들을 버퍼에 저장하고, 컨슈머의
pause()
메서드를 호출해 추가적인poll()
호출이 데이터를 리턴하지 않도록 한 뒤, 레코드 처리를 계속 진행
- 비동기 방식 재시도
- dead-letter-queue 방식
- 별도의 재시도 토픽에 메시지를 발행한 뒤 기존 메시지 처리는 계속 진행
- 동기 방식 재시도
(6) 컨슈머가 상태를 유지해야 할 수도 있다
poll()
메서드 호출 간 상태를 유지해야 할 수도 있음- e.g. 이동평균을 계산하는 경우
- 카프카에서 새 메시지를 폴링해 올 때마다 평균값을 업데이트 해줘야 함
- 컨슈머가 재시작되면 단순히 마지막 오프셋에서부터 읽기 작업 재개하는 것으로는 안되고, 이동평균값을 복구시켜야 함
- e.g. 이동평균값 복구하는 방법
- 마지막으로 누적된 값을 애플리케이션이 오프셋을 커밋할 때
results
토픽에 쓰는 것 - 스레드가 시작될 때, 작업 중단 시점 + 마지막으로 누적된 값 가져올 수 있음
- 마지막으로 누적된 값을 애플리케이션이 오프셋을 커밋할 때
# 시스템 신뢰성 검증
# 설정 검증
- 브로커와 클라이언트 (프로듀서, 컨슈머) 설정 검증하는 것은 쉬움
- 아래의 이유로 설정 검증을 권장
- 우리가 선택한 구성이 요구 조건을 충족시킬 수 있는지 확인하는데 도움됨
- 시스템의 예상 작동 추론하기 좋음
검증용 프로듀서 (org.apache.kafka.tools.VerifiableProducer
), 검증용 컨슈머 (org.apache.kafka.tools.VerifiableConsumer
) 활용하여 CLI든 테스팅 프레임워크든 활용하기 좋음
어떤 테스트를 할 것인지에 대한 고려
- 리더 선출
- 리더 정지시키면 어떻게 되나?
- 프로듀서/컨슈머가 정상 작동 재개하는데까지 얼마나 걸리나?
- 컨트롤러 선출
- 컨트롤러 재시작 후, 시스템 재개까지 얼마나 걸리나?
- 롤링 재시작
- 메세지 유실 없이 브로커들을 하나씩 재시작시킬 수 있나?
- 언클린 리더 선출 테스트
- 한 파티션의 모든 레플리카들을 하나씩 중단시킨 후, 다음 아웃-오브-싱크 상태가 된 브로커를 시작시키면 어떻게 되나?
- 작업을 재개하려면 어떻게 해야할까?
# 애플리케이션 검증
애플리케이션 로직이 카프카의 클라이언트 라이브러리와 상호작용 하는 부분에 대한 테스트
- 아래와와 같은 곳들을 확인
- 커스텀 에러 처리 코드
- 오프셋 커밋
- 리밸런스 리스너
다양한 장애 상황에 대한 테스트도 해보기
- 클라이언트가 브로커 중 하나와 연결 끊어짐
- 클러이언트와 브로커 사이의 긴 지연
- 디스크 꽉 참
- 디스크 멈춤
- 리더 선출
- 브로커 롤링 재시작
- 컨슈머 롤링 재시작
- 프로듀서 롤링 재시작
장애 주입을 위한 카프카의 자체적인 프레임워크
- 트록도르 (Trogdor) 테스트 프레임워크
# 프로덕션 환경 모니터링
- 클러스터 상태 모니터링
- 클라이언트와 전체 데이터의 흐름 모니터링
카프카의 자바 클라이언트들은 클라이언트 쪽 상태/이벤트를 모니터링 할 수 있게 해주는 JMX 지표 포함
- 각각 신뢰성에서 가장 중요한 지표를 알아보자
- 프로듀서
- 레코드별 에러율 (error-rate)
- 재시도율 (retry-rate)
- 이벤트 전송 도중 발생하는 에러 로그 모니터링
- WARN 레벨
- “Got error produce response with correlation id 5689 on topic-partition ~, retrying ~”
- ERROR 레벨
- 재시도 불가능한 에러
- 재시도 횟수가 고갈된 재시도 가능 에러
- 타임아웃으로 메세지 전송 완전히 실패했음
- WARN 레벨
- 컨슈머
- 컨슈머 랙 (consumer lag)
- 컨슈머가 브로커 내 파티션에 커밋된 가장 최신 메세지에서 얼마나 뒤떨어져 있는지
- 이상적인 상황
- 랙은 0, 컨슈머는 계속해서 최신 메세지만 읽음
- 현실
poll()
호출이 여러 메세지를 리턴 → 컨슈머는 다음 번 메세지를 읽어오기 전까지 리턴된 메세지를 처리하는데 시간 씀 → 랙은 계속해서 올랐다 내렸다 반복- 중요한건 컨슈머가 계속 뒤쳐지는게 아니라, 계속 따라붙는 것
- 링크드인이 개발한 버로우 사용하면 컨슈머 랙 확인하는 작업 수월함
- 컨슈머 랙 (consumer lag)
데이터의 흐름을 모니터링 하는 것 → 모든 쓰여진 데이터가 적절한 시기에 읽혀진다
- 모든 메세지는 이벤트가 생성된 시점인 타임스탬프를 포함함
- 타임스탬프는 애플리케이션이나 브로커에 의해 재정의될 수 있음
모든 쓰여진 메세지가 적절한 시간 내에 읽히도록 하기 위한 방법
- 애플리케이션
- 자신이 쓴 이벤트의 수를 메세지 형태로 작성 (보통, 초당 이벤트 수 형태)
- 컨슈머
- 단위 시간당 읽은 이벤트 수, 이벤트가 쓰여진 시점과 읽힌 시점 사이의 랙을 기록
- 프로듀서 - 컨슈머 양쪽에서 들어온 초당 이벤트 수를 서로 맞춘 뒤, 이벤트가 쓰여진 시점과 읽힌 시점 사이의 간격이 적절함을 확인할 수 있는 시스템 필요
- 이런 종단 모니터링 시스템은 컨플루언트 컨트롤 센터의 일부인 제품을 쓰자
카프카 브로커가 클라이언트로 보내는 에러 응답률을 보여주는 지푯값 수집하기
kafka.server:type=BrokerTopicMetrics,name=FailedProduceRequestsPerSec
kafka.server:type=BrokerTopicMetrics,name=FailedFetchRequestsPerSec
- 때때로 일정 수준의 에러 응답이 예상되는 경우
- e.g. 유지관리 작업을 위해 브로커를 꺼서 다른 브로커가 리더로 선출될 경우
- 프로듀서가
NOT_LEADER_FOR_PARTITION
에러 받는 것은 예상된 작동
- 프로듀서가
- e.g. 유지관리 작업을 위해 브로커를 꺼서 다른 브로커가 리더로 선출될 경우