[kafka] 카프카란 무엇이며 어떻게 동작하는가.
카프카란
카프카는 Pub/Sub 모델 기반의 메시징 서버로, 링크드인에서 자체적으로 사용하기 위해 만든 분산 데이터 스트림 플랫폼을 말한다. 기존 메세징 시스템에 비해 처리량이 높고, 파티셔닝이나 복제기능이 있어 대규모 메시지 처리에 적합하다.
일반적인 서비스 회사에서 서비스를 운영할 때 다음과 같은 데이터 시스템들을 필요로 하게된다.
1. 사용량, 응답시간, 에러 카운트등의 메트릭 모니터링용 데이터 시스템
2. 배치/분석을 위한 로그 저장용 데이터 시스템
3. 서비스에 필요한 메인 데이터 시스템
4. key/value 저장소
5. 기타 등등..
처음엔 단순한 구조로 시작하지만 서비스 규모가 커질수록 다음 모습처럼 복잡한 구조로 변형되게 된다.
위와 같은 복잡한 구조에서는 데이터 파이프라인별로 데이터 포맷과 처리 방법들이 달라 확장하기 어렵고, 다양한 서비스와 연결된 만큼 관리도 어려워진다. 게다가 시스템간의 데이터가 불일치 하여 신뢰도까지 떨어질 수 있다.
카프카는 이러한 문제를 해결하기 위해 개발 되었고, 아래 그림과 같이 모든 이벤트와 데이터의 흐름을 관리하는 허브 역할을 함으로써 복잡한 서비스 아키텍처를 단순화 시켜준다.
Kafka 특징
· 프로듀서와 컨슈머의 분리(pub/sub)
프로듀서와 컨슈머가 분리되어 있지 않고 일대일로 통신하는 경우에는, 통신하던 모니터링 서버에 문제가 생겨 응답이 늦어지면,
연쇄작용으로 해당 모니터링 서버와 연결된 다른 서비스들에서도 지연이슈가 발생한다는 치명적인 단점이 있다.
카프카는 중앙에서 여러 시스템과 애플리케이션 사이에서 데이터 스트림을 주고 받는 구조로, 프로듀서와 컨슈머가 확실히 구분되어 있다.
각 서비스들은 모니터링이나 분석 시스템의 상태와 관계 없이 카프카로 메시지를 보내는 역할만 하면되고,
마찬가지로 모니터링이나 분석 시스템들도 서비스 서버들의 상태에 상관 없이 카프카에 저장된 메시지만 가져오면 된다.
· 멀티 프로듀서, 멀티 컨슈머
프로듀서는 하나 이상의 토픽에 메세지를 보낼수 있고, 컨슈머 역시 하나 이상의 토픽으로부터 메세지를 읽어올 수 있다.
하나의 토픽에 여러 프로듀서나 컨슈머가 접근이 가능하므로 하나의 데이터를 다양한 용도로 사용하기에 좋다.
토픽: 데이터가 저장되는 일종의 카테고리
· 메세지 영속성
일반적인 메시징 시스템에서는 컨슈머가 메시지를 읽어가면 큐에서 바로 메시지를 삭제하지만, 카프카는 일정 주기만큼 메시지를 디스크에 저장해둔다.
따라서 컨슈머의 메시지 처리가 늦어지거나 컨슈머의 중단이 발생하더라도 메시지의 손실이 없으며, 한번 읽은 메시지를 여러번 다시 읽을 수 있다.
· 확장성(Scalability)
카프카 클러스터는 3개(권장)의 브로커에서 수십대의 브로커로 확장이 가능하다. 확장 작업도 서비스 중단없이 비교적 간단하게 진행할 수 있다.
· 빠른 처리 속도
OS의 페이지 캐시를 사용해 데이터를 읽고 쓰기 때문에 속도가 빠르다.
또한, 카프카는 작은 메시지들을 묶어서 배치로 처리하기 때문에 I/O로 인한 속도 저하가 적은편이다.
· 고가용성(High availability), 결함에 강함(Fault-tolerant)
카프카는 분산 시스템을 사용하기 때문에 단일 시스템보다 더 높은 성능을 얻을 수 있고 시스템 확장이 용이하다. 또한 분산시스템 중 하나의 서버에 장애가 발생해도 다른 서버가 데이터 손실 없이 작업을 대신한다. 심지어 한대의 서버만 남아도 서비스가 가능하다.
Kafka 구성 요소와 역할
Zookeeper(주키퍼)
- 카프카는 분산 애플리케이션을 관리하는 코디네이션으로 주키퍼를 사용한다.
- 주키퍼는 분산 각 애플리케이션의 정보를 관리하고, 동기화등을 처리한다.
- 분산 애플리케이션들은 각각 클라이언트가 되어 주키퍼 서버들과 커넥션을 맺어 상태정보 등을 주고 받는다.
- 상태 정보들은 주키퍼 지노드(znode)라 불리는곳에 key-value 형태로 저장되고 이값을 가지고 분산 애플리케이션들은 서로 데이터를 주고 받는다.
Broker(브로커)
- 카프카 애플리케이션이 설치되어 있는 서버로 데이터 저장소 역할을 한다.
Topic(토픽) 과 Partition(파티션)
- 토픽은 데이터를 구분하는 논리적 개념이고, 파티션은 토픽을 분할한 데이터 저장소 이다.
- 카프카의 리플리케이션은 토픽이 아닌 파티션을 리플리케이션 하는 것이다.
- 파티션 수는 한 번 설정하면 증가만 가능하고 줄일 수는 없다. 토픽을 삭제 해야 한다.
Producer(프로듀서)
- 메시지를 생산하는 애플리케이션
- 각 메시지를 토픽 파티션에 매핑하고 파티션의 리더에 요청을 보낸다.
Consumer(컨슈머)
- 저장된 메시지를 가져가서 사용하는 서버 또는 애플리케이션
- 특정 파티션을 관리하고 있는 파티션 리더에게 메시지 가져오기 요청을 한다.
- 각 요청에 오프셋을 명시할 수 있어, 컨슈머는 가져올 메시지의 위치를 조정할 수 있고, 이미 가져온 데이터도 다시 가져올 수 있다.
Kafka 데이터 모델
토픽
토픽은 메시지가 저장되는 카테고리와 같다.
카프카 클러스터는 토픽이란 곳에 데이터를 저장하고,
프로듀서들은 원하는 토픽에 메시지를 보내고, 컨슈머들도 원하는 토픽에 접근하여 메시지를 가져온다.
파티션
파티션은 토픽을 분할한 대상이다. 카프카의 복제 기능은 이 파티션 단위로 이루어지며, 카프카의 병렬 처리를 가능하게 해
효율적인 메시지 전송을 하도록 한다.
위 그림에서 각 메시지 전송 속도가 1초일 때 파티션이 하나 뿐이라면 3개의 메시지를 전달하는데 총 3초가 걸리게 된다.
토픽을 파티션을 3개로 나누면 3초를 1초로 줄일 수 있다. 파티션 수가 많아지면 그만큼 빠른 전송이 가능 하다.
하지만 그에 따른 단점도 있다.
파티션 수가 많아질 때 단점
- 파일 핸들러 낭비
각 파티션은 브로커의 디렉토리와 매핑이 되고, 저장되는 데이터마다 2개의 파일이 있다.
카프카는 모든 디렉토리에 대해 파일 핸들을 열게 되어 파티션 수가 많을 수록 파일 핸들 수 역시 많아져
리소스를 낭비하게 된다.
- 장애 복구 시간 증가
카프카의 브로커가 다운될 때 해당 브로커에 리더 파티션이 있다면, 파티션이 많을수록 그만큼 리더선출 시간이 오래 걸리게 된다. 또한 컨트롤러 역할을 하는 브로커가 다운될 경우 모든 파티션의 데이터를 읽어야 하는데 그만큼 장애 시간이 늘어나게 된다.
* 카프카 브로커 장애 시
카프카는 파티션 단위로 데이터를 복제하는데, 이들 파티션중 하나는 리더역할을 하고 나머지는 팔로워가 된다.
카프카의 브로커가 다운이 되면 다운된 브로커에 리더 파티션이 있는 경우, 다른 브로커로 리더를 이동시키는 작업이 진행된다.
컨트롤러로 지정된 브로커가 리더의 이동작업을 수행하는데, 컨트롤러 브로커가 다운된 경우에는 컨트롤러의 페일오버가 작동하고
새 컨트롤러가 초기화 하는동안 주키퍼에서 모든 파티션의 데이터를 읽어야 한다.
적절한 파티션의 수
적절한 파티션 수는 서비스에서 처리할 데이터량을 기준으로 잡는것이 좋다. 주의할 점은 프로듀서, 파티션, 컨슈머가 처리하는 메시지 양이 비슷해야 메시지가 원하는만큼 효율적으로 처리 된다는 것이다.
또한 파티션수 증가는 아무때나 할 수 있지만, 줄이는 방법은 토픽 삭제 외에는 없으므로 한번에 많은 파티션 수를 설정하기 보다는 운영을 하다가 병목현상이 발생할 때 하나씩 늘려가는 방법이 좋다.
카프카 데이터 저장 방식
파티션에는 메시지가 저장되는 위치인 오프셋(offset)이 존재 한다. 오프셋은 순차적으로 증가하는 숫자이며 파티션 내에서 유일한 값이다.
하나의 토픽을 3개의 파티션으로 나눈 모습으로, 파티션0, 파티션1, 파티션2 두모두 0번 오프셋을 가지고 있다.
카프카는 이 오프셋을 가지고 메시지의 순서를 보장한다.
컨슈머가 데이터를 가져갈 때는 오프셋 순서대로만 가져갈 수 있다.
리플리케이션 동작방식
리플리케이션 팩터
카프카의 리플리케이션 팩터(Replication Factor)를 설정하여 파티션의 복제수를 결정할 수 있다.
기본값은 1이며, 토픽별로 서로다른 리플리케이션 팩터 값을 설정할 수 있고, 운영 중에도 토픽의 리플리케이션 팩터 값은 변경 할 수 있다. 단, 클러스터 내 모든 브로커에 동일하게 설정해야 한다.
구성된 리플리케이션들 중 원본을 가진 파티션을 리더, 복제한 파티션을 팔로워라 부른다.
모든 읽기와 쓰기는 리더에서만 이루어지고 팔로워는 리더의 데이터를 복제만 한다.
만약에 리더 파티션이 있는 브로커가 다운된다면 팔로워들중 하나가 새로운 리더가 되어 요청에 응답한다.
리플리케이션 수 결정하기
리플리케이션을 사용하게 되면 필요한 저장소의 크키도 배가 된다.
예를 들어 100G의 메시지를 저장하는 파티션의 복제수를 3으로 했다면 총 300G의 저장소가 필요하게 된다.
또한 리플리케이션 수가 늘어나면 리플리케이션에 대한 상태 확인을 하는 백그라운드 작업이 차지하는 리소스 사용량이 증가하게 된다.
따라서 데이터의 중요도에 따라 팩터를 2 또는 3으로 결정하는 것이 좋다.
리더와 팔로워 관리
카프카는 ISR(In Sync Replica)를 통해 파티션의 리더와 팔로워의 데이터 불일치 현상을 방지하고 있다. ISR은 파티션 리더와 리더후보 파티션의 그룹을 말한다.
리더는 팔로워들이 주기적으로 데이터를 확인을 하고 있는지 체크해 일정 주기만큼 확인 요청이 오지 않는다면, 해당 팔로워의 이상을 감지하고 ISR 그룹에서 해당 팔로워를 추방시킨다.
1) 한개의 리더와 두개의 팔로워로 ISR을 이루고 있음
2) 팔로워1은 리더의 데이터를 확인해 저장하지만, 팔로워2는 리더에게 데이터 확인 요청을 보내지 않음
3) 일정주기(replica.lag.time.max.ms)만큼 확인 요청이 들어오지 않아, 리더는 이를 감지하고 해당 팔로워는 더이상 리더의 역할을 대신할 수 없다고 판단해 ISR 그룹에서 추방함
카프카 모든 브로커가 다운 되었을 때 리더 선출 방법
다음과 같이 세개의 브로커가 모두 다운되었을때의 최악의 시나리오를 가정해보자
1) ISR 그룹에 리더와 팔로워1, 팔로워2가 있고 A라는 메시지 복제가 잘 이루어졌다.
2) 팔로워2가 다운되어 데이터 확인을 못하게 되자 ISR 그룹에서 제외되고 B메시지는 리더와 팔로워 1에만 저장되게 된다.
3) 후에 팔로워1도 다운되어 데이터 확인요청을 못해 ISR 그룹에서 제외되고 C 메시지는 리더에만 저장된다.
4) 그 후 리더마저 다운되 메시지를 받을 수 없는 상황이 된다.
현재 모든 브로커가 다운된 상황이며 각 브로커마다 가지고 있는 데이터 값도 모두 다르다.
이때 선택할 수 있는 옵션은 두가지가 있다.
1) 마지막 리더가 살아나기를 기다린다.
unclean.leader.election.enable=false
2) ISR에서 추방되었지만 먼저 살아나면 자동으로 리더가 된다.
unclean.leader.election.enable=true
데이터의 중요도에 따라 두가지 방법중 하나를 선택하면 되는데,
1)번 옵션을 선택할 때 마지막 리더는 모든 브로커가 다운되기 전 리더로 모든 메시지를 가지고 있다.
따라서 이 리더가 살아날 때까지 기다린다면 데이터 유실을 막을 수는 있지만, 해당 리더가 다운되는 시간이 길어질 경우, 장애상황 역시 실어진다는 단점이 있다.
2)번 옵션은 선택 했을때 팔로워2가 가장 먼서 살아나 리더가 되어 다음 메시지를 이어받게 되면 B메시지와 C메시지의 누락이 발생한다.
따라서 이 옵션은 메시지 유실이 일부 발생 되어도 서비스를 빠르게 정상화 시키고 싶을때 사용 할 수 있다.
프로듀서(Producer)
프로듀서는 각각의 메시지를 토픽 파티션에 매핑하고 파티션의 리더에 요청을 보내는 역할을 한다.
키 값을 정해 해당 키를 가진 모든 메시지를 특정 파티션으로 전송 할 수도 있고,
키 값을 입력하지 않으면 파티션은 라운드 로빈 방식으로 파티션에 균등하게 분배 된다.
일반적으로 서버로 메시지를 보내고 성공적으로 도착했는지는 확인하지 않지만, get()메소드를 통해 브로커에 전송한 메시지의 성공 실패 여부를 확인 할 수 있다.
(메시지가 성공적으로 전송되지 않으면 예외가 발생하고, 에러가 없다면 RecordMetadata를 얻을 수 있다.
RecordMetadata 를 이용해 메시지가 저장된 파티션과 오프셋을 알 수 있다.)
메시지 전송 방법
프로듀서 옵션중 acks 옵션 설정에 따라 메시지 손실 여부나 전송 속도 등이 달라지게 된다.
- 메시지 손실 가능성이 높지만 빠른 전송을 원할 때 (acks = 0)
프로듀서가 메시지를 전송한 후 카프카의 응답을 확인하지 않고, 다음 메시지를 보낼 준비가 되는 즉시 다음 요청을 보낸다.
프로듀서만 준비되면 바로 보내므로 매우 빠르게 메시지 전송을 할수 있다
- 메시지 손실 가능성이 적으면서 적당한 전송을 원할 때 (acks = 1)
프로듀서가 카프카로 메시지를 보낸 후 보낸 메시지를 카프카가 잘 받았는지 확인을 한다.
응답을 기다리는 시간만큼 속도는 저하된다.
브로커 프로듀서로부터 메시지를 받고 프로듀서에게 acks를 보낸후 바로 장애가 발생하는 경우, 팔로워들이 데이터를 복제하기 전이라면
새로운 리더가 선출되는 과정에서 데이터 유실이 있을 수 있다.
- 전송 속도는 느리지만 메시지 손실이 없어야 할 때 (acks = all)
프로듀서가 메시지를 전송하고 난 후 리더가 메시지를 받았는지 확인을 하고 추가로 팔로워까지 메시지를 받았는지 확인하는 방법이다. 속도는 가장 느리지만 팔로워들의 메시지 저장까지 확인하므로 데이터 손실이 없다.
이 기능을 완벽히 사용하려면 브로커의 설정도 같이 조정해줘야 한다.
《프로듀서의 acks=all 과 브로커의 min.insync.replicas》
min.insync.replicas 는 프로듀서의 설정이 acks=all 일 때 메시지 성공처리를 위해 확인하는 리플리케이션 수이다.
1(디폴트)로 설정되어 있을 경우 하나의 리플리케이션만 확인하므로 리더만 메시지를 저장하게 되면 성공으로 인식한다.
만약 min.insync.replicas 값을 브로커 수와 동일하게 설정하게 되면 모든 리플리케이션이 메시지를 받았는지 확인하게 되므로, 브로커가 한대만 다운되도 카프카 클러스터 전체 장애와 비슷한 상황이 발생하게 된다.
컨슈머(Consumer)
프로듀서가 카프카 토픽으로 메시지를 보내면 그 토픽의 메시지를 가져와서 소비하는 역할을 하는 애플리케이션이다.
컨슈머 주요 기능은 특정 파티션을 관리하고 있는 파티션 리더에게 메시지 가져오기 요청을 하는 것이다.
각 요청은 오프셋을 명시하여 그 위치의 메시지를 수신하고, 특정 파티션을 지정해 데이터를 가져올 수도 있다.
또한 가져왔던 메시지들을 다시 가져 올 수도 있고, 한꺼번에 여러 토픽으로부터 데이터를 읽어올 수도 있다.
파티션과 메시지 순서
카프카에서 토픽의 파티션이 여러개인 경우, 메시지의 순서를 보장할 수 없다.
프로듀서에서 "abcde12345" 순서대로 메시지로 보냈다고 가정해보자.
카프카는 기본적으로 라운드로빈 방식으로 파티션에 데이터를 저장하므로 아래와 같은 모습으로 데이터가 저장된다.
컨슈머를 통해 데이터를 읽어온다면 "ad14be25c3"순으로 읽어오게 된다.
컨슈머에서의 메시지 순서는 동일한 파티션 내에서는 프로듀서가 생성한 순서와 동일하게 처리되지만,
파티션과 파티션 사이에서는 순서가 보장되지 않는다는걸 알 수 있다.
메세지의 순서를 완전히 보장 받고 싶다면 토픽의 파티션 수를 1로 설정해 사용하면 되지만 분산 처리를 할 수 없고, 하나의 컨슈머에서만 처리할 수 있기 때문에 처리량이 높지 않다.
컨슈머 그룹
하나의 토픽에는 여러 컨슈머 그룹이 동시에 접속해 메시지를 가져올 수 있다.
컨슈머 그룹 안에서 컨슈머들은 메시지를 가져오는 토픽의 파티션들의 담당을 공유한다.
다음 그림은 파티션 수가 3인 토픽에서 컨슈머가 하나인 컨슈머 그룹이 메시지를 가져오는 모습이다.
프로듀서가 쌓는 메시지 양이 많아져 컨슈머가 읽어가지 못하는 메시지들이 쌓이면, 다음과 같이 컨슈머를 추가해 확장해야 한다.
추가 컨슈머인 컨슈머02, 컨슈머03을 시작할 때 컨슈머 그룹 아이디만 컨슈머1과 동일하게 설정하면 위 그림과 같은 구성이 되고, 파티션1의 소유권은 컨슈머02로, 파티션2의 소유권은 컨슈머 03으로 이동한다.
이렇게 소유권을 이동하는 것을 리밸런스(rebalance)라고 하고,
(리밸런스를 하는 동안 일시적으로 컨슈머는 메시지를 읽어 올 수 없다.
※토픽의 파티션에는 동일 컨슈머 그룹 내에서 하나의 컨슈머만 연결 할 수 있기 때문에 파티션 수를 초과하는 컨슈머를 추가 하려면 반드시 파티션 수도 함께 늘려줘야 한다.
컨슈머가 일정한 주기로 하트비트를 보냄으로써 이 소유권을 유지한다.
하트비트는 컨슈머가 poll 할 때와 가져간 메시지의 오프셋을 커밋할 때 보낸다.
오랫동안 컨슈머가 하트비트를 보내지 않으면 세션은 타임아웃되고 해당 컨슈머가 다운되었다고 판단하여 리밸런스를 진행하고 해당 컨슈머를 컨슈머 그룹에서 제외 시킨다.
이렇게 되면 제외된 컨슈머가 소유하고 있던 파티션은 다른 컨슈머에게 넘어가게 되고,
다음 그림처럼 하나의 컨슈머가 두개의 파티션으로부터 메시지를 가져오는 불균형한 상황이 발생할 수 도 있다.
여러 컨슈머 그룹들이 하나의 토픽에서 메시지를 가져갈 수 있는데 그 이유는 컨슈머 그룹마다 각자의 오프셋을 별도로 관리하기 때문이다.
컨슈머 그룹아이디만 중복되지 않게 조심하면 된다.
커밋과 오프셋
컨슈머 그룹의 컨슈머들은 각각의 파티션에 자신이 가져간 메시지의 오프셋을 기록하고 있다.
각 파티션에 대해 현재 위치를 업데이트 하는 동작을 '커밋한다'고 표현한다.
컨슈머 그룹내에 컨슈머가 추가되거나 삭제될 때 그룹 내에서 리밸런스가 일어나고, 리밸런스가 일어난 후 각각의 컨슈머는 이전에 처리한 파티션에 상관없이 새로운 파티션을 할당 받는다.
새로운 파티션에서 가장 최근에 커밋된 오프셋을 읽고 그 이후 데이터를 가져오는데,
만약 커밋된 오프셋이 실제 처리한 오프셋보다 작으면 커밋된 오프셋부터 실제 처리한 오프셋까지의 메시지는 중복으로 처리가 된다.
반대로 커밋된 오프셋이 실제 처리한 오프셋보다 크면 커밋된 오프셋부터 실제 처리한 오프셋까지의 메시지는 누락된다.
커밋 방법에는 자동커밋과 수동커밋 두가지 방법이 있다.
1) 자동커밋 : 5초마다 컨슈머는 poll()을 호출할 때 가장 마지막 오프셋을 커밋한다. 커밋되기전 리밸런스가 일어나면 중복 데이터가 발생 할 수 있다.
2) 수동커밋 : 커밋을 원하는 시점에 commitSync() 를 호출해 커밋한다. 역시나 중복 데이터가 발생할 수 있지만 손실은 없다.