Kafka - 신뢰성 있는 데이터 전달, 복제
이 포스트에서는 아래의 내용에 대해 알아본다.
- 서로 다른 종류의 신뢰성 종류
- 카프카 맥락에서 이것이 어떤 의미를 갖는지?
- 카프카 복제 메커니즘과 이것이 시스템 신뢰성에 어떻게 영향을 미치는지?
- 여러 활용 사례에 대해 카프카 브로커와 토픽들을 어떻게 설정해주어야 하는지?
- 서로 다른 신뢰성 상황에서 클라이언트들(프로듀서와 컨슈머)을 어떻게 사용해야 하는지?
- 시스템의 신뢰성을 검증하는 방법
목차
개발 환경
- mac os
- openjdk 17.0.12
- zookeeper 3.9.2
- apache kafka: 3.8.0 (스칼라 2.13.0 에서 실행되는 3.8.0 버전)
1. 신뢰성 보장
보장이란 서로 다른 상황에서도 시스템이 지킬 것이라 보장되는 행동을 의미한다.
가장 잘 알려진 신뢰성 보장은 RDBMS 가 보편적으로 지원하는 ACID(Atomicity 원자성, Consistency 일관성, Isolation 격리성, Durability 지속성) 일 것이다.
어떤 DB 가 ACID 를 준수한다고 하면 그것은 트랜잭션 처리 관련하여 어떠한 행동을 보장한다는 의미이다.
신뢰성 있는 애플리케이션을 개발하려고 한다면 카프카가 제공하는 보장을 알아야 한다.
<카프카가 보장하는 것들>
- 파티션 안의 메시지들 간에 순서를 보장함
- 메시지 A 다음에 B 가 쓰여졌다면 동일한 프로듀서가 동일한 파티션을 썼을 경우, 카프카는 B 의 오프셋이 A 의 오프셋보다 큰 것을 보장함
- 컨슈머는 A 를 읽어온 다음에 B 를 읽음
- 클라이언트가 쓴 메시지는 모든 ISR(In-Sync Replica) 의 파티션에 쓰여진 뒤에야 ‘커밋’된 것으로 간주됨
- 프로듀서는 메시지가 완전히 커밋된 다음에 응답이 올지, 리더에게 쓰여진 다음 응답이 올지, 네트워크로 전송된 다음 바로 응답이 올지 선택할 수 있음
- 위의 선택은
acks
설정을 통해 선택 가능
- 커밋된 메시지들은 최소 1개의 작동 가능한 레플리카가 남아있는 한 유실되지 않음
- 컨슈머는 커밋된 메시지만 읽을 수 있음
신뢰성 있는 시스템을 구축하는데에는 트레이드 오프가 있고, 이러한 트레이드 오프는 설정 매개변수를 조절함으로써 결정할 수 있다.
보통은 메시지를 저장하는데 있어서의 신뢰성과 일관성이 우선 순위가 높은지 아니면 가용성, 높은 처리량, 낮은 지연, 하드웨어 비용과 같은 다른 고려 사항이 우선 순위가 높은지의 문제인 경우가 많다.
2. 복제
복제에 대한 자세한 설명은 3. 복제: 레플리카 종류 를 참고하세요.
카프카 복제 메커니즘은 파티션별로 다수의 레플리카를 유지한다는 특성과 함께 카프카의 신뢰성 보장의 핵심이다.
하나의 메시지를 여러 개의 레플리카에 씀으로써 카프카는 크래시가 나더라도 메시지의 지속성을 유지한다.
토픽은 파티션으로 이루어지고, 파티션은 하나의 디스크에 저장된다.
카프카는 파티션에 저장된 이벤트들의 순서를 보장하며, 파티션은 온라인 상태(사용 가능)이거나 오프라인 상태(사용 불가능)일 수도 있다.
각 파티션은 다수의 레플리카를 가질 수 있으며, 그 중 하나가 리더가 된다.
모든 이벤트들은 리더 레플리카에 쓰여지고, 대부분 리더 레플리카에서 읽혀진다.
다른 레플리카들은 단순히 리더 레플리카와 동기화를 맞우면서 최신 이벤트를 제 시간에 복사해오기만 하면 된다.
만일 리더 레플리카가 작동 불능 상태가 되면 ISR(In-Sync Replica) 중 하나가 새로운 리더 레플리카가 된다.
파티션의 리더 레플리카이거나, 아래 조건을 만족하는 팔로워 레플리카인 경우 ISR 로 간주한다.
- 주키퍼와의 활성 세션이 있음
- 즉, 최근 6초 사이(설정 가능)에 주키퍼로 하트 비트를 전송함
- 최근 10초 사이(설정 가능)에 리더 레플리카로부터 메시지를 읽어옴
- 최근 10초 사이(설정 가능)에 리더 레플리카로부터 읽어온 메시지들이 가장 최근 메시지임
- 팔로워 레플리카가 리더 레플리카로부터 메시지를 받고 있는 것만으로는 부족함
- 최근 10초 사이에 lag 이 없었던 적이 최소 한 번은 있어야 함
위 조건을 만족하지 못하면 레플리카는 Out-of-sync 상태(= 동기화가 풀린 상태)로 간주된다.
동기화가 풀린 레플리카는 주키퍼와 다시 연결되어 리더 파티션에 쓰여진 가장 최근 메시지까지를 따라잡으면 다시 ISR 이 된다.
동기화가 살짝 늦은 ISR 은 프로듀서와 컨슈머를 느리게 만들 수 있다.
프로듀서와 컨슈머는 메시지가 커밋되기 전에 모든 ISR 가 해당 메시지를 받을 때까지 기다려야 하기 때문이다.
레플리카의 동기화가 풀리면 더 이상 레플리카가 이 메시지를 받을 때까지 기다릴 필요가 없어지므로 더 이상 성능에 영향을 주지는 않는다.
ISR 수가 줄어들면 파티션의 실질적인 복제 팩터가 줄어들면서 중단 시간이 길어지거나 데이터가 유실될 위험성은 높아진다.
3. 신뢰성 있는 브로커 설정
메시지 저장 신뢰성과 관련된 브로커의 설정 매개 변수는 4개가 있다.
- 복제 팩터
replication.factor
,default.replication.factor
- 언클린 리더 선출
unclean.leader.election.enable
- 최소 ISR(In-Sync Replica)
min.insync.replicas
- 레플리카를 In-Sync 상태로 유지
zookeeper.session.timeout.ms
,replica.lag.time.max.ms
위 매개 변수들 역시 브로커 단위에서 적용되어 시스템 안에 모든 토픽들을 제어할 수도 있고, 토픽 단위로 적용되어 특정 토픽의 작동만 제어할 수도 있다.
토픽 단위에서 신뢰성 트레이드오프를 제어할 수 있다는 것은 신뢰성이 필요한 토픽과 아닌 토픽을 같은 카프카 클러스터에 저장할 수 있음을 의미한다.
3.1. 복제 팩터(레플리카 개수): replication.factor
, default.replication.factor
토픽 단위의 설정은 replication.factor
로 하고, 브로커 단위로 설정하여 자동으로 생성되는 토픽들에 적용할 때는 default.replication.factor
로 한다.
토픽의 복제 팩터가 3이라는 것은 각 브로커가 3대의 서로 다른 브로커에 3개 복제된다는 의미이다.
이미 존재하는 토픽일지라도 레플리카를 추가/삭제할 수 있으므로 자연히 복제 팩터 역시 변경이 가능하다.
복제 팩터가 N개이면 N-1개의 브로커가 중단되어도 토픽의 데이터를 읽거나 쓸 수 있다.
따라서 복제 팩터가 클수록 가용성과 신뢰성은 늘어나고 장애가 발생할 가능성은 줄어든다.
반대로 복제 팩터가 N개라는 것은 최소한 N개의 브로커가 필요하고, N개의 복사본을 저장해야 하므로 N배의 디스크 공간이 필요하다.
즉, 복제 팩터는 가용성과 하드웨어 사용량 사이의 트레이드오프가 있다.
<토픽에 레플리카 개수 결정 시 고려사항>
- 가용성
- 레플리카가 하나뿐인 파티션은 브로커를 재시작하기만 해도 작동 불능에 빠짐
- 레플리카 수가 많을수록 가용성은 늘어남
- 지속성
- 각 레플리카는 파티션 안의 모든 데이터의 복사본임
- 파티션에 레플리카가 하나뿐이고 디스크가 사용 불가능하게 될 경우 해당 파티션의 모든 데이터는 유실됨
- 복사본이 더 많을수록 모든 데이터가 유실될 가능성은 줄어듦
- 처리량
- 레플리카가 추가될 때마다 브로커 간 트래픽 역시 늘어남
- 특정 파티션에 10Mbps 의 속도로 쓰는데 레플리카가 하나뿐이라면 복제 트래픽은 없음
- 만일 레플리카가 2개면 복제 트래픽이 10Mbps 가 되고, 3개면 20Mbps 가 됨
- 클러스터의 크기와 용량을 산정할 때 처리량을 고려할 필요가 있음
- 종단 지연
- 쓰여진 메시지를 컨슈머가 읽으려면 모든 ISR 에 복제되어야 함
- 이론적으로 레플리카 수가 많을수록 이들 중 하나가 느려짐으로써 컨슈머까지 함께 느려질 가능성이 높아짐
- 비용
- 중요하지 않은 데이터에 대해 복제 팩터를 3미만으로 잡아주는 일반적이 이유는 바로 비용임
- 레플리카 수가 많을수록 저장소와 네트워크에 들어가는 비용 역시 증가함
레플리카의 위치도 중요하다.
카프카는 언제나 같은 파티션의 레플리카들은 서로 다른 브로커에 저장하는데 이 정도로는 충분히 안전하지 않은 경우가 있다.
같은 파티션의 모든 레플리카들이 같은 rack 에 설치되어 있는 브로커들에 저장되었는데 해당 rack 이 오작동할 경우, 복제 팩터와는 무관하게 해당 파티션을 사용할 수 없게 된다.
rack 단위의 사고를 방지하기 위해 브로커들을 서로 다른 rack 에 배치한 뒤 broker.rack
브로커 설명 매개변수에 rack 이름을 잡아줄 것을 권장한다.
rack 이름을 설정해주면 카프카는 파티션의 레플리카들이 서로 다른 rack 에 분산되어 저장되도록 함으로써 가용성을 높일 수 있다.
rack 에 대한 추가 설명은
2. 파티션 할당,
5.14.client.rack
,replica.selector.class
를 참고하세요.
3.2. 언클린 리더 선출: unclean.leader.election.enable
언클린 리더 선출에 대한 추가 설명은 3.3. 리더 선출:
elecLeader()
을 참고하세요.
언클린 리더 선출 설정은 브로커 단위(실제로는 클러스터 단위)로만 설정 가능하며, unclean.leader.election.enable
로 설정하고 기본값은 false 이다.
파티션의 리더가 더 이상 사용 가능하지 않을 경우 ISR 중 하나가 새로운 리더가 된다.
커밋된 데이터가 모든 ISR 에 존재하는 데이터이기 때문에 아무런 데이터 유실이 없음을 보장한다는 점에서 이러한 리더 선출을 ‘클린’ 하다고 한다.
하지만 작동 불능에 빠진 리더 외에 ISR 이 없다면 어떻게 될까?
위와 같은 상황은 아래 2가지 상황 중 하나에서 발생한다.
① 파티션에 3개의 레플리카가 있고, 팔로워 2개가 작동 불능이 됨 (브로커 크래시)
프로듀서는 리더 레플리카에 쓰기 작업을 계속 할 것이므로 모든 메시지는 커밋되고 응답이 감 (리더가 유일한 ISR 이므로)
이 때 또 브로커가 크래시가 나서 리더 레플리카를 사용할 수 없게됨
이 상황에서 Out-of-sync 레플리카 중 하나가 먼저 시작되면 해당 파티션의 유일한 사용 가능한 레플리카가 Out-of-sync 레플리카가 됨
② 파티션에 3개의 레플리카가 있고, 네트워크 이슈로 팔로워 2개의 복제 작업이 밀림
이 때 복제 작업은 계속되고 있겠지만 더 이상 In-sync 상태는 아님
리더 레플리카는 유일한 ISR 로써 계속해서 메시지를 받음
이 때 리더 레플리카가 작동 불능이 되면 리더가 될 수 있는 레플리카는 Out-of-sync 레플리카밖에 없음
이럴 경우 아래와 같은 쉽지 않은 결정을 해야 한다.
- Out-of-sync 레플리카가 새 리더가 될 수 없도록 함
- 마지막 ISR 이기도 한 예전 리더 레플리카가 복구될 때까지 해당 파티션은 오프라인 상태가 됨
- 이것은 몇 시간이 걸릴수도 있음
- Out-of-sync 레플리카가 새 리더가 될 수 있도록 함
- 새로운 리더가 동기화를 못한 사이 예전 리더 레플리카에 쓰여졌던 모든 메시지들이 유실되고, 컨슈머 입장에서의 일관성 역시 깨짐
- 예) 레플리카 2가 리더이고, 레플리카 0과 1이 사용 불가능한 상황에서 레플리카 2의 오프셋 100~200 에 해당하는 메시지를 썼을 때 레플리카 2가 작동 불능에 빠지고 레플리카 0 이 온라인 상태가 됨. 이 때 레플리카 0 은 0~100 에 해당하는 메시지만 가지고 있고 100~100 에 해당하는 메시지는 없음
- 위와 같은 상황에서 레플리카 0 이 새로운 리더가 된다면 프로듀서들은 새로운 메시지를 레플리카 0 에 쓸 것이고 컨슈머들도 레플리카 0 에서 메시지들을 읽어갈 것임
따라서 이제 새로운 리더는 100~200 에 해당하는 완전히 새로운 메시지들을 갖게 됨 - 일부 컨슈머들은 레플리카 2가 리더인 상태일 때 쓰여진 100~200 에 해당하는 예전 메시지를 읽었겠지만, 일부는 같은 오프셋을 할당받은 새로운 메시지들을 읽을 것이며, 일부는 뒤섞인 채로 읽게 됨
- 만일 레플리카 2가 다시 온라인 상태로 복구되면 새로운 리더인 레플리카 0 의 팔로워가 될 것임
- 이 시점에서 레플리카 2는 자신이 갖고 있는 메시지 중에서 현재의 리더가 갖고 있지 않은 메시지들을 삭제할 것임
- 이 메시지들은 이제 어떤 컨슈머들도 읽을 수 없음
즉, Out-of-sync 레플리카가 리더가 될 수 있도록 허용할 경우 데이터 유실과 일관성 깨짐의 위험성이 있다.
그렇지 않을 경우 파티션이 다시 온라인 상태가 될 때까지 원래 리더가 복구되는 것을 기다려야 하는 만큼 가용성이 줄어든다.
3.3. 최소 ISR(In-Sync Replica): min.insync.replicas
토픽과 브로커 단위 모두 min.insync.replicas
설정으로 잡아줄 수 있다.
토픽 당 3개의 레플리카를 설정해주었더라도 ISR 하나만 남을수도 있다.
만일 이 때 이 레플리카가 작동 불능에 빠질 경우 가용성과 일관성 사이에서 하나를 골라야하는데 이건 결코 쉬운 선택이 아니다.
여기서 문제는 카프카가 보장하는 신뢰성에 따르면 데이터는 모든 ISR 에 쓰여진 시점에서 커밋된 것으로 간주된다는 점인데, ‘모든’ 은 한 개의 레플리카를 의미할 수도 있다는 점이다.
이 레플리카가 작동 불능에 빠지면 데이터를 유실될 수 잇다.
커밋된 데이터를 최소 2개 이상의 레플리카에 쓰고자 한다면 ISR 의 최소값을 더 높게 잡아주면 된다.
만일 토픽에 레플리카가 3개가 있고, min.insync.replicas
를 2로 설정했다면 프로듀서들은 3개의 레플리카 중 최소 2개가 In-Sync 인 상태인 파티션에만 쓸 수 있다.
min.insync.replicas
를 2로 설정한 상태에서 3개의 레플리카가 모두 In-Sync 상태이면 모든 것이 정상적으로 동작한다.
하지만 3개 중 2개의 레플리카가 작동 불능에 빠지게 되면 브로커는 더 이상 쓰기 요청을 받지 않는다.
데이터를 전송하려고 시도하는 프로듀서는 NotEnoughReplicasException
을 받게 된다.
컨슈머는 계속해서 존재하는 데이터를 읽을 수 있다.
이와 같은 설정에서 ISR 이 하나만 남을 경우 해당 레플리카는 사실상 읽기 전용이 된다.
이렇게 하면 언클린 리더 선출이 발생했을 때 사라질 데이터를 쓰거나 읽는 바람직하지 못한 상황을 방지할 수 있다.
레플리카를 읽기 전용 상태에서 회복하려면 2개의 사용 불능 레플리카 중 하나를 복구시킨 뒤(브로커 재시작) 리더 레플리카의 상태를 따라잡아서 In-Sync 상태로 들어갈 때까지 기다려야 한다.
3.4. 레플리카를 In-Sync 상태로 유지: zookeeper.session.timeout.ms
, replica.lag.time.max.ms
2. 복제 에서 본 것처럼 레플리카가 Out-of-sync 상태가 될 수 있는 이유는 2가지가 있다.
- 주키퍼와의 연결이 끊어짐
- 리더 레플리카의 업데이트 내역을 따라가는데 실패해서 복제 lag 이 발생
카프카에서는 이 2가지 조건에 대한 브로커 설정 2개를 제공하고 있다.
zookeeper.session.timeout.ms
- 브로커가 주키퍼로 하트비트 전송을 멈출 수 있는 최대 시간
- 이 간격 안에만 하트비트를 보내면 주키퍼는 브로커가 죽었다고 판단하지 않으므로 클러스터에서 제외되지 않음
- 기본값은 18s 임
- 가비지 수집이나 네트워크 상황과 같은 무작위적인 변동에 영향을 받지 않을만큼 높게, 하지만 실제로 작동이 멈춘 브로커가 적시에 탐지될 수 있도록 충분히 낮게 설정해주어야 함
replica.lag.time.max.ms
- 이 설정값보다 더 오랫동안 리더로부터 데이터를 읽어오지 못하거나, 리더에 쓰인 최신 메시지를 따라잡지 못하는 경우 동기화가 풀린 상태(= Out-of-sync) 가 됨
- 기본값은 30s 임
- 이 설정값은 컨슈머의 최대 지연에도 영향을 줌
즉, 메시지가 모든 레플리카에 도착해서 컨슈머가 메시지를 읽을 수 있게 되는데 최대 30초가 걸릴 수 있음을 의미함
3.5. 디스크에 저장: flush.messages
, flush.ms
카프카는 메시지를 받은 레플리카의 수에만 의존할 뿐 디스크에 저장되지 않은 메시지에 대해서도 응답한다.
카프카는 세그먼트를 교체할 때와 재시작 직전에만 메시지를 디스크로 플러시하며, 그 외엔 리눅스의 페이지 캐시 기능에 의존한다. (페이지 캐시 공간이 다 찼을 때만 메시지를 플러시함)
이렇게 동작하는 이유는 각 데이터의 복제본을 갖고 있는, 서로 다른 rack 이나 가용 영역에 위치한 3대의 장비가 리더의 디스크에 메시지를 쓰는 것보다 더 안전하다고 판단하기 때문이다.
하지만 브로커가 디스크에 더 자주 메시지를 저장하도록 설정하는 것은 가능하다.
flush.messages
- 디스크에 저장되지 않은 최대 메시지 수 설정
flush.ms
- 디스크에 얼마나 자주 메시지를 저장할 지 설정
위 기능을 사용하기 전에 fsync
가 카프카 처리량에 어떤 영향을 미치고 그 단점을 극복하기 위해 어떻게 해야하는지 먼저 알아야 한다.
4. 신뢰성 있는 프로듀서 설정
높은 신뢰성 설정을 브로커에 적용해도 프로듀서 역시 신뢰성이 있도록 설정을 잡아주지 않는다면 시스템 전체로서는 여전히 데이터가 유실될 수 있다.
아래 2가지 예를 보자.
① 토픽별로 3개의 레플리카를 갖도록 브로커를 설정하고, 언클린 리더 선출 기능을 끔
이렇게 하면 카프카 클러스터에 커밋된 메시지를 유실되지 않음
하지만 프로듀서가 메시지를 보낼 때 acks=1
로 보내도록 설정함
프로듀서에서 메시지를 전송해서 리더 레플리카에는 쓰여졌지만 아직 ISR 에 반영되지는 않은 상태임
리더 레플리카가 프로듀서에게 ‘메시지가 성공적으로 쓰여짐’ 응답을 보낸 직후 크래시가 나서 데이터가 레플리카로 복제되지 않음
다른 레플리카들은 여전히 In-Sync 상태로 간주되기 때문(Out-of-sync 상태로 판정될 때까지 약간의 시간이 걸리기 때문)에 그 중 하나가 리더가 될 것임
메시지가 레플리카에 쓰여지지 않았으므로 해당 메시지는 유실됨
하지만 메시지를 쓰고 있는 프로듀서 애플리케이션 입장에서는 성공적으로 쓰여졌다고 판단함
어떤 컨슈머도 이 메시지를 보지 못했기 때문(레플리카 입장에서는 본 적도 없는 메시지이므로 커밋된 것도 아님)에 시스템의 일관성은 유지됨
하지만 프로듀서의 입장에서보면 메시지는 유실된 것임
② 토픽별로 3개의 레플리카를 갖도록 브로커를 설정하고, 언클린 리더 선출 기능을 끔
프로듀서가 메시지를 보낼 때 acks=all
로 보내도록 설정함
카프카에 메시지를 쓰려고 하는데 쓰고 있는 파티션의 리더 브로커가 방금 전 크래시가 나서 새로운 리더가 아직 선출중임(= 브로커는 메시지를 받지도 못함)
그러면 카프카는 Leader not Available
응답을 보낼 것
이 때 만약 프로듀서가 올바르게 에러를 처리하지 않고 쓰기가 성공할 때까지 재시도도 하지 않으면 메시지가 유실될 수 있음
브로커는 메시지를 받지도 않았고, 컨슈머들도 역시 메시지를 받은 적이 없기 때문에 일관성 문제는 아님
하지만 프로듀서가 이 에러를 정확히 처리해주지 않을 경우 메시지 유실을 초래할 수 있음
카프카에 메시지를 쓰는 프로듀서 애플리케이션을 개발할 때는 아래 2가지를 신경써야 한다.
- 신뢰성 요구 조건에 맞는 올바른
acks
설정 사용 - 설정과 코드 모두에서 에러를 올바르게 처리
4. 프로듀서 설정에서 프로듀서 설정에 대해 보았지만 중요한 부분에 대해 다시 한번 살펴본다.
4.1. 응답 보내기: acks
4.2.
acks
를 참고하세요.
4.2. 프로듀서 재시도 설정: delivery.timeout.ms
, enable.idempotence
프로듀서는 재시도 가능한 에러(retriable error) 를 처리할 수 있다.
프로듀서가 브로커에 메시지를 전송하면 브로커는 성공 혹은 에러 코드를 리턴하는데 이 때 에러 코드는 2개의 부류로 나뉜다.
- 전송 재시도 시 해결될 수 있는 에러 코드
- 브로커가
LEADER_NOT_AVAILABLE
에러 코드 리턴- 새로운 브로커가 리더로 선출될 상황일 것이며, 두 번째 시도는 성공할 것이므로 재시도가 가능한 에러임
- 브로커가
- 전송 재시도 시 해결될 수 없는 에러 코드
- 브로커가
INVALID_CONFIG
에러 코드 리턴- 전송 재시도를 한다고 해서 설정이 변경되지는 않으므로 재시도 불가능한 에러임
- 브로커가
프로듀서의 에러 종류에 대한 추가 설명은 3.1.1.
KafkaProducer
에러 종류 를 참고하세요.
메시지가 유실되지 않는 것이 목표일 경우 가장 좋은 방법은 재시도 가능한 에러가 발생한 경우 프로듀서가 계속해서 메시지 전송을 재시도하도록 하는 것이다.
재시도 관련 설정에 대한 추가 설명은
4.3.2.delivery.timeout.ms
,
4.3.4. retries, retry.backoff.ms
를 참고하세요.
재시도에 관한 가장 좋은 방법은 재시도 수인 retries
를 기본 설정값인 MAX_INT (사실상 무한) 으로 내버려두고, 메시지 전송을 포기할 때까지 대기할 수 있는 시간을 지정하는 delivery.timeout.ms
설정값을 최대로 잡아주는 것이다.
즉, 프로듀서는 이 시간 간격 내에 있는 한 메시지 전송을 계속해서 시도한다.
전송 실패한 메시지를 재시도하는 것은 메시지가 중복될 위험을 내포한다.
예) 실패했다고 생각했지만 실제로는 아닌 메시지와 재전송된 메시지가 모두 브로커에 성공적으로 쓰여지는 경우
재시도와 에러 처리는 각 메시지가 ‘최소한 한번’ 저장되도록 보장할 수는 있지만, ‘정확히 한 번’은 보장할 수 없다.
enable.idempotence=true
설정을 통해 프로듀서가 추가적인 정보를 레코드에 포함할 수 있도록 하여 브로커가 재시도로 인한 중복된 메시지는 건너뛸 수 있도록 해야 한다.
멱등성에 대한 좀 더 상세한 내용은 1. 멱등적 프로듀서(idempotent producer) 를 참고하세요.
4.3. 추가적인 에러 처리
3. 카프카로 메시지 전달 을 참고하세요.
5. 신뢰성 있는 컨슈머 설정
컨슈머는 카프카에 커밋된 데이터만 읽을 수 있다. 즉, 모든 ISR 에 쓰여진 다음부터 읽을 수 있는 것이다.
컨슈머는 일관성이 보장되는 데이터만 읽으므로 컨슈머가 해야 할 일은 어느 메시지까지 읽었고, 어디까지는 읽지 않았는데 추적하는 것이다.
파티션으로부터 데이터를 읽어올 때 컨슈머는 메시지를 배치 단위로 읽어온 뒤 배치별로 마지막 오프셋을 확인하여 브로커로부터 받은 마지막 오프셋 값에서 시작하는 다른 메시지 배치를 요청한다.
이렇게 함으로써 컨슈머는 메시지 누락없이 언제나 새로운 데이터를 올바른 순서로 읽어올 수 있다.
특정 컨슈머가 작동을 정지하면 또 다른 컨슈머 입장에서는 어디서부터 작업을 재개해야 하는지 알아야 한다.
바로 이것이 컨슈머가 읽어온 오프셋을 커밋해야 하는 이유이다.
읽고 있던 각 파티션에 대해 어디까지 읽었는지를 저장해두어야 해당 컨슈머나 다른 컨슈머가 재시작한 뒤에도 어디서부터 작업을 계속해야 하는지 알 수 있기 때문이다.
컨슈머가 메시지를 누락할 수 있는 경우는 보통 읽기는 했지만 아직 처리는 완료되지 않은 이벤트들의 오프셋을 커밋하는 경우이다.
이럴 경우 다른 컨슈머가 작업을 물려받았을 때 이 메시지들은 영원히 처리되지 않는다.
이것이 바로 오프셋이 언제, 어떻게 커밋되는지에 대해 신경써야 하는 이유이다.
커밋된 메시지 vs 커밋된 오프셋
- 커밋된 메시지
- 모든 ISR 에 쓰여져서 컨슈머가 읽을 수 있는 메시지
- 커밋된 오프셋
- 컨슈머가 특정 파티션 어느 오프셋까지의 모든 메시지를 받아서 처리를 완료했는지 알리기 위해 카프카에 보낸 오프셋
오프셋을 커밋하는 방법에 대해서는 1. 오프셋과 커밋:
__consumer_offsets
을 참고하세요.
5.1. 신뢰성 있는 처리를 위한 컨슈머 설정
신뢰성 있는 컨슈머 설정을 위한 속성으로는 아래 4가지 속성이 있다.
group.id
auto.offset.reset
enable.auto.commit
auto.commit.interval.ms
이 외에도 컨슈머가 리밸런스를 수행하기 위해 너무 자주 멈추면 신뢰성이 있다고 하기 어렵다.
불필요한 리밸런싱과 리밸런싱이 발생했을 때 멈춤을 최소화하기 위한 컨슈머 설정은 1.2. 컨슈머 그룹과 파티션 리밸런스 를 참고하세요.
5.1.1. group.id
group.id
는 컨슈머가 속하는 컨슈머 그룹을 지정한다.
만일 같은 그룹 ID 를 갖는 2개의 컨슈머가 같은 토픽을 구독할 경우 각 컨슈머는 해당 토픽 전체 파티션의 서로 다른 부분집합이 할당되므로 각각은 서로 다른 부분의 메시지를 읽게 된다. (전체적으로는 컨슈머 그룹이 모든 메시지를 읽고 있는 셈)
만약 컨슈머가 구독한 토픽의 모든 메시지를 읽어야 한다면 고유한 그룹 ID 가 있어야 한다.
5.1.2. auto.offset.reset
5.10.
auto.offset.reset
을 참고하세요.
5.1.3. enable.auto.commit
5.11.
enable.auto.commit
을 참고하세요.
5.1.4. auto.commit.interval.ms
자동으로 오프셋을 커밋할 경우, 이 설정을 사용하여 커밋되는 주기를 설정할 수 있다.
5s 마다 커밋하는 것이 기본값이다.
자주 커밋할수록 오버헤드는 늘어나지만 컨슈머가 정지했을 때 발생할 수 있는 중복의 수는 줄어든다.
5.2. 컨슈머에서 명시적으로 오프셋 커밋
여기서는 데이터를 신뢰성있게 다루는 컨슈머를 개발할 때 고려해야 할 사항들에 대해 알아본다.
5.2.1. 메시지 처리 먼저, 오프셋 커밋은 나중에
폴링 루프에서 모든 처리를 하고 루프 사이의 상태는 저장하지 않는다면 이 때는 아래 중 하나로 하면 된다.
- 자동 오프셋 커밋 설정 (
enable.auto.commit
) - 폴링 루프의 끝에서 오프셋 커밋
- 루프 안에서 일정한 주기로 오프셋을 커밋하여 오버헤으돠 중복 처리 회의 사이의 균형을 맞춤
하지만 스레드가 2개 이상 있거나 상태가 있는 처리가 필요한 경우 컨슈머 객체가 스레드 안전하지 않기 때문에 이 경우엔 복잡해진다.
이럴 때 해결 방법은 4.1. 스레드 안전성 을 참고하세요.
5.2.2. 커밋 빈도는 성능과 크래시 발생 시 중복 개수 사이의 트레이드오프
컨슈머의 폴링 루프 안에서 모든 처리를 수행하고 그 사이에 상태를 전혀 유지하지 않는 가장 단순한 경우더라도 루프 안에서 여러 번 커밋하거나 루프가 몇 번 지나갈 때마다 커밋하는 것 사이에서 선택할 수 있다.
커밋 작업은 상당한 오버헤드를 수반한다.
커밋 주기는 성능과 중복 발생의 요구 조건 사이에서 균형을 맞춰야 한다.
메시지를 읽어올 때마다 커밋하는 방식은 매우 낮은 빈도로 메시지가 들어오는 토픽에나 사용할 수 있다.
5.2.3. 정확한 시점에 정확한 오프셋 커밋 필요
폴링 루프 중간에 커밋할 때 흔히 하는 실수는 마지막으로 처리된 메시지의 오프셋이 아닌 마지막으로 읽어 온 메시지의 오프셋을 커밋하는 것이다.
반드시 처리가 완료된 메시지의 오프셋을 커밋해야 한다.
읽기는 했지만 처리되지 않은 메시지의 오프셋을 커밋할 경우 컨슈머 입장에서는 메시지가 누락될 수 있다.
처리가 완료된 메시지의 오프셋을 커밋하는 예시는 1.2. 현재 오프셋 커밋:
commitSync()
을 참고하세요.
5.2.4. 리밸런스
애플리케이션을 설계할 때는 컨슈머 리밸런스가 발생할 것이라는 것을 예상하여 이를 적절히 처리해 줄 필요가 있다.
이것은 보통 할당된 파티션이 해제되기 전에 오프셋을 커밋하고, 새로운 파티션이 할당되었을 때 애플리케이션이 보유하고 있던 상태를 삭제해주는 작업을 포함한다.
리밸런스에 대한 자세한 내용은 1.2. 컨슈머 그룹과 파티션 리밸런스 를 참고하세요.
5.2.5. 컨슈머의 재시도
상황에 따라 poll()
을 호출하여 레코드를 처리한 뒤 일부 레코드를 처리가 완료되지 않아서 나중에 처리해야할 수도 있다.
예) 카프카에서 읽어온 레코드를 DB 에 쓰는 도중에 DB 오류가 나서 나중에 재시도해야 하는 경우
카프카 컨슈머들은 오프셋을 커밋할 뿐 각각의 메시지에 응답을 보내지 않는다.
즉, 만일 레코드 #10 처리 실패한 상태에서 #11 처리에 성공할 경우 #11 의 오프셋을 커밋하게 되면 #10 의 레코드도 처리되었다고 간주할 것이다.
이럴 때는 아래 2가지 패턴 중 하나를 사용하면 된다.
① 재시도 가능한 에러 발생 시 마지막으로 처리에 성공한 레코드의 오프셋을 커밋함
그리고 나서 나중에 처리해야 할 레코드들을 버퍼에 저장하고(다음 poll()
호출에서 덮어쓰지 않도록 하기 위해), 컨슈머의 pause()
를 호출해서 추가적인 poll()
호출이 데이터를 리턴하지 않도록 한 뒤, 레코드 처리를 계속함
② 재시도해야 하는 레코드들을 별도의 토픽에 쓴 뒤 계속 진행함
별도의 컨슈머 그룹을 사용해서 재시도 토픽에 저장된 레코드들을 처리하거나, 주 토픽과 재시도 토픽을 모두 구독하는 컨슈머를 하나 두어서 재시도 사이에는 재시도 토픽 구독을 잠시 멈추도록 함
이 패턴은 DLT (Dead Letter Queue) 시스템과 비슷함
5.2.6. 컨슈머의 상태 유지
상황에 따라서 poll()
메서드 호출 간에 상태를 유지해야 할 수도 있다.
예) 이동평균을 계산해야 할 경우 카프카에서 새로운 메시지들을 폴링해 올 때마다 평균값을 업데이트해주어야 함
만일 프로세스가 재시작된다면 단순히 마지막 오프셋에서부터 읽기 작업을 재개하는 것만으로는 안되고 여기에 해당하는 이동평균값 역시 복구시켜야 한다.
이럴 때 해결 방법은 애플리케이션이 오프셋을 커밋할 때 마지막으로 누적된 값을 result 토픽에 쓰는 것이다.
이렇게 하면 스레드가 시작될 때 작업이 중단된 시점과 마지막으로 누적된 값을 가져올 수 있다.
단일 트랜잭션 내에서 결과를 쓰는 작업과 오프셋을 커밋하는 작업을 함께 수행하는 방법에 대해서는 2.2. 트랜잭션이 ‘정확히 한 번’을 보장하는 방법 을 참고하세요.
이런 문제는 매우 복잡한 문제이므로 카프카 스트림즈나 플링크(Flink) 와 같이 aggregation(집적), join 등의 복잡한 분석 작업을 위한 DSL 형식 API 를 제공하는 라이브러리를 알아보는 것이 좋다.
6. 시스템 신뢰성 검증
신뢰성 요구 조건 확인, 브로커 설정, 클라이언트(프로듀서, 컨슈머) 설정, 상황에 맞는 API 사용이 완료되면 이제 이벤트가 유실되지 않을 것이라는 확신을 가지고 프로덕션 환경에 적용하는 일만 남는다.
이 때 3개의 계층에 걸쳐서 검증을 수행하는 것이 좋다.
- 설정 검증
- 애플리케이션 검증
- 애플리케이션 모니터링
6.1. 설정 검증
애플리케이션 로직과 격리된 채 브로커와 클라이언트(프로듀서, 컨슈머) 설정을 검증하는 것은 쉽기도 하고, 아래와 같은 이유로 권장된다.
- 선택한 구상이 요구 조건을 충복시킬 수 있는지 확인할 수 있음
- 시스템의 예상 작동을 추론해볼 수 있음
- 카프카는 검증 작업을 위해 org.apache.kafka.tools 패키지에 2개의 툴을 포함하고 있음
VerifiableProducer
(검증용 프로듀서)VerifiableConsumer
(검증용 컨슈머)
- 위 2개의 클래스는 각각 명령줄 형태로든 자동화된 테스팅 프레임워크에 포함된 형태로든 실행 가능함
- 카프카는 검증 작업을 위해 org.apache.kafka.tools 패키지에 2개의 툴을 포함하고 있음
<검증 예시>
- 검증용 프로듀서를 사용해서 1에서부터 선택한 값까지의 숫자를 포함하는 메시지를 순서대로 씀
- 검증용 프로듀서에도 일반적인 프로듀서와 같이
acks
등의 설정값을 잡아줄 수 있으며 메시지를 쓰는 속도 역시 설정 가능함
- 검증용 프로듀서에도 일반적인 프로듀서와 같이
- 검증용 프로듀서를 실행시키면 브로커에 전송된 각 메시지마다 성공 혹은 에러를 출력함
- 검증용 컨슈머는 이벤트를 읽어 온 순서대로 출력함
- 커밋과 리밸런스에 관련된 정보도 함께 출력됨
<테스트 시나리오 종류의 일부>
- 리더 선출
- 리더를 정지시키면 어떻게 되는지?
- 프로듀서와 컨슈머가 정상 작동을 재개하는데까지 얼마나 걸리는지?
- 컨트롤러 선출
- 컨트롤러가 재시작한 뒤 새스템이 재개되는데 얼마나 걸리는지?
- 롤링 재시작
- 메시지 유실없이 브로커들을 하나씩 재시작시킬 수 있는지?
- 언클린 리더 선출
- 각 레플리카의 동기화가 풀리도록 하기 위해 한 파티션의 모든 레플리카들을 하나씩 중단시킨 후 Out-of-sync 상태가 된 브로커를 시작시키면 어떻게 되는지?
- 작업을 재개하려면 어떻게 해야하는지?
- 그것이 용인할 수 있는 수준인지>
이런 시나리오를 하나 고른 후 검증용 프로듀서와 검증용 컨슈머를 실행시키고 해당 시나리오대로 실행해본다.
예) 데이터를 쓰고 있는 파티션의 리더를 정지시킴.
만일 잠깐 중단되었다가 메시지 유실없이 정상적으로 작동이 재개될 것이라고 예상했다면 프로듀서가 쓴 메시지 수와 컨슈머가 읽어 온 메시지 수가 맞는지 확인하면 됨
kafka git test suite 에 포함된 많은 테스트들은 위에서 설명한 것과 같은 원리로 작동한다.
6.2. 애플리케이션 검증
브로커와 클라이언트(프로듀서, 컨슈머)의 설정이 요구 조건과 맞다는 걸 확인했으면 이제 애플리케이션이 우리가 필요로 하는 보장을 해주는 지 확인해야 한다.
이 검증 단계에서는 애플리케이션 로직이 카프카 클라이언트 라이브러리와 상호 작용하는 커스텀 에러 처리 코드, 오프셋 커밋, 리밸런스 리스너와 같은 것들을 확인한다.
- 클라이언트가 브로커 중 하나와 연결이 끊김
- 클라이언트와 브로커 사이의 긴 지연
- 디스크가 꽉 차거나 멈춤
- 리더 선출
- 브로커 롤링 재시작
- 컨슈머 롤링 재시작
- 프로듀서 롤링 재시작
예를 들어 컨슈머 롤링 재시작을 하고자 한다면, 컨슈머 리밸런스 때문에 잠깐 멈춘 뒤 읽기가 재개되어 1,000 개 이하의 중복 메시지를 읽게 될 것이다. 라는 식의 예상ㅇㄹ 갖고 테스트를 할 수 있다.
테스트를 위한 가상의 네트워크나 디스크 장애를 발생시킬 수 있는 좋은 툴들에 대해서는 각자 검토해보세요.
카프카는 장애 주입을 위한 자체적인 프레임워크인 트록도르(Trogdor) 테스트 프레임워크 를 포함한다.
6.3. 프로덕션 환경에서 신뢰성 모니터링
애플리케이션 테스트도 중요하지만 데이터가 예상대로 흐르고 있는지 지속적으로 모니터링하는 것도 중요하다.
클러스터의 상태를 모니터링하는 것 외에 전체 데이터의 흐름 역시 모니터링을 해야 한다.
클러스터 모니터링에 대해서는 추후 다룰 예정입니다. (p. 202)
카프카와 자바 클라이언트들은 클라이언트 쪽 상태와 이벤트를 모니터링 할 수 있게 해주는 JMX 지표를 포함한다.
6.3.1. 프로듀서 모니터링
신뢰성 측면에서 중요한 2 지표는 레코드별 에러율(error-rate) 와 재시도율(retry-rate) 이다.
이벤트 전송 도중에 발생하는 프로듀서 에러 로그도 모니터링해주어야 한다.
- WARN 레벨
- Got error produce response with correlation id 1111 on topic-partition [topic-1,3], retrying (two attempts left)…
- 위와 같은 로그에서 남은 재시도 횟수가 0 인 이벤트가 보인다면 프로듀서에서 재시도 횟수가 고갈된 것임
- 재시도 횟수가 고갈되는 것을 방지하는 것에 대한 내용은 4.2. 프로듀서 재시도 설정:
delivery.timeout.ms
,enable.idempotence
참고
- ERROR 레벨
- 재시도 불가능한 에러, 재시도 횟수가 고갈된 재시도 가능한 에러, 타임아웃으로 인해 메시지 전송이 완전히 실패했음을 가리킬 가능성이 높음
6.3.2. 컨슈머 모니터링
컨슈머에서 가장 중요한 지표는 컨슈머 lag 이다.
이 지표는 컨슈머가 브로커 내 파티션에 커밋된 가장 최신 메시지엣 얼마나 뒤쳐져있는지를 가리킨다.
이상적인 상황은 lag 이 항상 0인 것이지만, 현실에서는 poll()
호출이 여러 개의 메시지를 리터놔면 컨슈머는 다음 번 메시지를 읽어오기 전까지 리턴된 메시지를 처리하는데 시간을 쓰기 마련이므로 lag 은 계속해서 오르락내리락하게 되어있다.
중요한 것은 컨슈머가 점점 더 뒤쳐지는게 아니라 계속해서 따라붙어야 한다는 것이다.
링크드인이 개발한 버로우 를 이용하면 컨슈머 랙을 좀 더 쉽게 확인할 수 있다.
데이터의 흐름을 모니터링한다는 것은 쓰여진 데이터가 적절한 시기에 읽혀진다는 것을 의미한다.
데이터가 적절한 시기에 읽히도록 하려면 데이터가 언제 생성되었는지를 알아야 하는데, 카프카의 모든 메시지는 이벤트가 생성된 시점을 가리키는 타임스탬프를 포함한다.
단, 이 값은 이벤트를 전송 중인 애플리케이션이나 관련 설정이 잡혀 있는 브로커에 의해 재정의될 수 있음을 유의해야 한다.
6.3.2. 브로커 모니터링
카프카 브로커는 브로커가 클라이언트로 보내는 에러 응답률을 보여주는 지표들을 포함한다.
아래 2개의 지표값을 수집할 것을 권장한다.
kafka.server:type=BrokerTopicMetrics,name=FailedProduceRequestsPerSec
kafka.server:type=BrokerTopicMetrics,name=FailedFetchRequestsPerSec
상황에 따라서 일정 수준의 에러 응답이 예상되는 경우도 있다.
예) 유지 관리를 위해 브로커를 꺼서 다른 브로커가 리더로 선출될 경우 프로듀서가 NOT_LEADER_FOR_PARTITION 에러를 받는 거은 예상된 동작
만일 요청 실패가 알 수 없는 이유로 증가할 때는 반드시 원인을 찾아봐야 하는데 이런 작업을 편하게 할 수 있도록 요청 실패 지표에는 브로커가 보낸 에러 응답이 태그 형식으로 달려있다.
참고 사이트 & 함께 보면 좋은 사이트
본 포스트는 김병부 저자의 O’REILLY 카프카 핵심 가이드 2판를 기반으로 스터디하며 정리한 내용들입니다.
- 카프카 핵심 가이드
- 예제 코드 & 오탈자
- Doc:: Kafka
- Git:: Kafka
- confluent blog:: Effect of fsync
- kafka git:: test suite
- git:: 트록도르(Trogdor) 테스트 프레임워크
- git:: Burrow - Kafka Consumer Lag Checking