카프카는 사용자의 클릭 추적부터 신용카드 결제까지 신뢰성에 있어서 매우 다양한 활용 사례가 존재한다. 카프카가 어떻게 신뢰성있게 데이터를 전달할 수 있는지 알아보자.
어떤 행동이 보장된다는 것은 시스템을 신뢰할 수 있게 만들고 에러 발생에도 유연하게 대처할 수 있게 만든다.
카프카의 신뢰성 보장
파티션 안에 들어간 메시지들 간의 순서는 보장된다.
클라이언트가 쓴 메시지는 설정한 시점(인싱크 레플리카가 모두 응답/바로 응답/리더만 응답)에 따라 응답하는 것이 보장된다.
최소 1개의 레플리카가 남아 있는 한 메시지는 유실되지 않는다.
컨슈머는 커밋한 메시지만 읽을 수 있다.
카프카는 이러한 기능들이 있지만 이것만으로 완전한 신뢰성 보장이라고 보기는 어렵다. 그러나, 개발자가 설정을 통해 높은 신뢰도를 보장하는 카프카를 사용할 수 있다. 하지만, 높은 신뢰도를 보장하기 위해서는 그에 따른 트레이드오프를 감수할 필요가 있다.
복제는 카프카의 신뢰성 보장에 있어서 핵심 기능으로 크래시가 발생해도 메시지가 보존되도록 보장한다.
신뢰성 보장에 영향을 미치는 브로커 설정들에 대해 알아보자.
복제 팩터
default.replication.factor 설정을 통해 토픽의 레플리카 수 기본값을 설정해줄 수 있고, 토픽 단위로 replication.factor를 지정해줄 수도 있다.
N개의 레플리카가 존재할 경우, N-1개의 브로커에서 크래시가 나도 데이터는 유실되지 않는다.
N개의 레플리카가 존재할 경우, N배의 디스크 공간이 필요하며, 최소 N개의 브로커가 필요하여 하드웨어 사용량이 늘어난다.
레플리카 수를 결정할 때 고려할 요소
가용성 -> 레플리카가 적으면 레플리카 브로커들에 크래시가 발생할 경우 해당 토픽은 동작하지 못한다.
지속성 -> 레플리카가 적으면 레플리카를 저장한 디스크들에 문제가 발생할 경우 메시지는 유실된다.
처리량 -> 레플리카가 늘어나면 그에 따라 복제 트래픽도 선형적으로 증가한다.
종단 지연 -> 클라이언트가 쓴 메시지는 모든 인싱크 레플리카에 쓰여져야 컨슈머가 읽을 수 있으므로, 레플리카가 늘어나면 종단 지연도 길어진다.
비용 -> 레플리카가 많을 수록, 저장소와 네트워크 비용이 증가한다.
언클린 리더 선출
unclean.leader.election.enable 설정값으로 설정할 수 있고, 기본값은 false 이다.
언클린 리더 선출이란 리더 외에 인싱크 레플리카가 존재하지 않아 아웃오브싱크 레플리카를 리더로 선출해야 하는 상황을 말한다.
언클린 리더 선출이 가능하게 하면, 인싱크 레플리카가 없어도 동작할 수 있으므로 가용성이 좋아지지만, 아웃오브싱크 레플리카가 리더가 되면 발생하는 데이터 유실과 트레이드오프 관계에 있다.
최소 인싱크 레플리카
토픽, 브로커 단위로 min.insync.replicas 설정으로 설정할 수 있다.
카프카 메시지는 모든 인싱크 레플리카에 쓰여지는 것이 보장되는데, 인싱크 레플리카가 1개면 1번만 복제될 뿐이다. 따라서 일관성을 높이고자 한다면 최소 인싱크 레플리카 값을 설정하여 최소 인싱크 레플리카의 수를 잡아줄 수 있다.
최소 인싱크 레플리카의 수보다 적은 인싱크 레플리카가 존재하는 파티션에는 메시지를 쓸 수 없다. 따라서 가용성과 일관성이 트레이드오프 관계에 있다.
레플리카를 인싱크 상태로 유지하기
zookeeper.session.timeout.ms 설정을 통해 하트비트 전송하지 않아 브로커가 죽었다고 판단하는 시간을 늘려서 잡아주거나, replica.lag.time.max.ms 설정을 높게 잡아 리더로부터 데이터를 읽어오지 않아도 아웃오브싱크로 판단하는 시간을 늘려주어 인싱크 상태를 유지하도록 설정할 수 있다.
위 설정을 통해 인싱크 레플리카가 되기 쉽도록 하여 가용성을 높여줄 수 있으나, 크래시가 나도 아웃오브싱크로 판단하는 데 오래걸리기 때문에 최대 지연 시간이 더 길어질 수 있다.
디스크에 저장하기
기본적으로 브로커는 세그먼트를 변경하는 상황이 아닌 이상 메시지를 디스크에 직접 저장하지 않고, 리눅스 페이지 캐시에 의존한다. 리더 디스크에 쓰는 것보다 여러 대의 페이지 캐시에 있는 것이 더 안전하다는 생각에서 비롯된 설계이다.
flush.messages 로 디스크에 저장되지 않은 최대 메시지 수를 설정하거나, flush.ms 로 자주 디스크에 메시지를 저장하도록 설정하도록 할 수 있다.
디스크에 자주 저장하는 방식은 캐시를 잘 활용할 수 없어 처리량에 영향을 미치지만, 일관성을 높여주어 트레이드오프 관계에 있다.
브로커를 신뢰성있게 설정한다고 하더라도, 프로듀서에서 브로커의 예외를 잘 처리하지 못하거나, 실제 카프카에서는 메시지가 쓰여지지 않았는데 쓰여진 줄 알고 넘어가는 등의 문제가 발생할 수 있으므로 프로듀서 애플리케이션도 신뢰성을 지킬 수 있게 코드를 짜야한다.
응답 보내기
acks=all 설정을 통해 모든 인싱크 레플리카에 메시지가 쓰이지 않으면 재시도하여 신뢰성 있게 메시지를 저장할 수 있다.
acks=0, 1 설정을 통해 프로듀서 지연을 줄일 수는 있지만, 컨슈머는 어차피 모든 인싱크 레플리카에 메시지가 쓰여야만 읽을 수 있어 종단 지연은 줄어들지 않으며, 프로듀서는 실제로 브로커에서 에러가 발생해 유실된 메시지도 쓰여졌다고 착각할 수 있다.
프로듀서 재시도 설정하기
앞서 3장에서 살펴본대로 재시도는 무한대로 할 수 있도록 하고, 최대 재시도 시도 시간을 크게 잡아주어 자동으로 재시도가 일어나도록 할 수 있다.
일시적인 에러에 대해 재시도를 하는 것은 메시지가 최소 한번 쓰여지는 것을 보장할 수 있으나, 정확히 한번 쓰여지는 것을 보장할 수는 없다. 따라서 enable.idempotence=true 설정을 통해 정확히 한 번 쓰여지도록 설정하는 것이 좋다.
추가적인 에러 처리
프로듀서가 기본적으로 수행하는 재시도 가능한 에러에 대한 재시도는 쉽게 사용할 수 있는 좋은 방법이지만, 그외의 에러에 대해서도 애플리케이션 개발자가 처리해줄 수 있어야 한다.
메시지의 크기, 인증 등 재시도가 불가능한 에러에 대해 별도로 로깅을 수행한다거나, 메시지 전송을 일시적으로 멈추게 하는 등의 추가적인 작업을 필요 시 해줄 수 있다.
컨슈머는 모든 인싱크 레플리카에 복제된 안전한 메시지만 읽을 수 있도록 설계되어 있다. 따라서 컨슈머에서는 메시지의 오프셋만 잘 관리하여 중복되지 않게, 유실되지 않게 메시지를 잘 읽어오도록 하는 것이 중요하다.
컨슈머에서 명시적으로 오프셋 커밋하기
메시지 처리 먼저, 오프셋 커밋은 나중에
오프셋 커밋을 먼저하게 되면 받아온 메시지 배치를 처리하는 도중에 크래시가 발생하면 메시지가 유실되게 된다.
오프셋 커밋을 나중에 하게 되면 메시지 배치를 처리하고 오프셋 커밋 하기 전에 크래시가 발생하면 메시지가 중복되게 된다.
대부분 데이터 유실보다는 중복이 낫다.
커밋 빈도는 성능과 크래시 발생시 중복 개수 사이의 트레이드오프다.
메시지 배치를 처리하는 폴링 루프에서 루프가 돌때마다 해당하는 오프셋을 커밋하는 등 커밋 빈도를 더 자주 잡아줄 수 있다.
오프셋을 커밋하는 것은 오프셋을 저장하는 토픽에 메시지를 쓰는 것이기 때문에 오버헤드가 크다. 따라서 성능과 메시지 중복 사이에서 적절한 타협점을 찾아야 한다.
정확한 시점에 정확한 오프셋을 커밋하자.
폴링 루프 내에서 메시지를 커밋할 때 자주하는 실수는 마지막 처리한 오프셋이 아닌 읽어온 최신 오프셋을 커밋하는 실수이다.
읽고 처리가 완료된 메시지의 오프셋을 커밋해야만 데이터 유실이 발생하지 않는다.
리밸런스
리밸런스 리스너를 통해 리밸런스가 발생할 경우에 적절하게 오프셋을 커밋해주고, 애플리케이션의 상태를 초기화하는 등의 작업을 해주어야 한다.
컨슈머는 재시도를 해야 할 수도 있다
컨슈머는 메시지를 읽어와 처리하다가 에러가 발생해서 특정 메시지까지만 처리했을 경우, 읽어온 메시지의 최신 오프셋을 커밋하는 것이 아니라 폴링을 멈추고 메시지 처리를 재시도하는 방법을 사용해야 한다.
메시지의 처리 순서가 중요하지 않은 시스템이라면 에러가 발생한 메시지들을 별도에 토픽에 넣어놓고 계속 진행하고 별도의 컨슈머 그룹을 통해 재시도해야할 토픽을 구독하여 메시지를 처리하는 방법을 사용할 수도 있다.
컨슈머가 상태를 유지해야 할 수도 있다
컨슈머가 폴링 루프 밖에서도 상태를 유지해야하는 상황(ex. 평균값 구하기)이 있을 수 있는데, 이런 경우에는 오프셋 커밋 시 별도의 results 토픽에 상태 메시지를 쓰는 방법으로 처리할 수 있다.
브로커 설정, 프로듀서, 컨슈머를 통해 신뢰성을 보장하도록 설계했다면 실제로 신뢰성이 보장되는지 검증해야 한다. 카프카에서 추천하는 세 단계 검증 방법에 대해 알아보자.
설정 검증하기
애플리케이션 로직과 분리하여 브로커와 클라이언트 설정만 검증한다.
카프카에서 제공하는 VerifiableProducer, VerifiableConsumer 를 통해 간편하게 검증을 수행해볼 수 있다.
리더 선출, 롤링 재시작, 컨트롤러 선출 등의 시나리오를 가정하고 메시지 유실이나 중복이 발생하지 않고 프로듀서와 컨슈머가 정상동작하는지 검증한다.
아파치 카프카 소스코드 저장소에는 방대한 테스트 스위트가 포함되어 있어 실행해볼 수 있다.
애플리케이션 검증하기
설정에 대한 검증이 끝났다면 애플리케이션 로직만을 대상으로 테스트를 수행해볼 수 있다. 다양한 상황을 가정해 에러를 발생시키고 의도대로 동작하는지 검증한다.
애플리케이션 단에 검증해볼 수 있는 것은 에러 처리, 리밸런스 처리, 오프셋 처리 등이 정상적으로 되는지 검증하는 것들이 있을 것이다.
에러를 발생시키는 좋은 툴들이 많이 있고, 카프카에도 Trogdor 이라는 테스트 프레임워크가 포함되어 있다.
프로덕션 환경에서 신뢰성 모니터링하기
클러스터의 상태를 모니터링하는 것도 중요하지만, 전체 데이터의 흐름을 모니터링하는 것도 중요하다.
카프카 자바 클라이언트는 JMX 지표를 통해 error-rate, retry-rate 등을 확인할 수 있으며, 프로듀서 에러 로그를 확인하여 메시지 유실이 발생하지는 않는지 확인해야 한다.
consumer lag 을 확인하여 쓰여지고 있는 메시지에 비해 읽는 속도가 너무 뒤쳐지지 않는지 확인하고, 쓰여진 메시지가 적절한 시점에 처리될 수 있도록 하는 것이 중요하다. consumer lag 을 확인하는 방법으로는 링크드인이 개발한 버로우를 사용하는 방법이 있다.