[kafka]카프카 기본 개념
inflearn강의(아파치 카프카 애플리케이션 프로그래밍-데브원영 DVWY)를 들으며 타이핑했던 내용들을 블로그에 기록용으로 남기고자 합니다.강의를 들으며 개인적으로 타이핑한 내용으로 오타나 맞춤법, 띄어쓰기 등이 엉망일 수 있습니다.
카프카 생태계
- 카프카 클러스터가 있고 토픽이 어떤 목적에 따라서 생성되어 있다.
- 기본적으로 데이터를 넣는 역할은 프로듀서고 이 토픽에 데이터가 들어간다. 그리고 데이터를 컨슈머가 가져간다.
- 토픽에 데이터를 처리해서 다시 토픽에 넣고 싶을때는 스트림즈 라이브러리를 사용하면 된다. 스트림즈, 커넥트 등은 오픈소스 아파치 카프카에 포함된 툴이다.
- 커넥트는 데이터 파이프라인을 운영하는 가장 핵심적인 역할이다.
- 커넥트(소스)는 프로듀서 역할을 하고 특정 소스 어플리케이션으로 부터 데이터를 가져와서 토픽에 데이터를 넣는 역할을 한다. 커넥트(싱크) 역할은 컨슈머 연할을 한다. 이를 클러스터로 운영하고 있고 템플릿으로 반복적으로 생성할 수 있다.
카프카 브로커와 클러스터
- 카프카 브로커, 클러스터, 주키퍼
- 주키퍼는 카프카 클러스터를 운영하기 위한 반드시 필요한 어플리케이션이다. 카프카가 2.x버전까지는 주키퍼가 완전히 필요했다. 아직까지는 주키퍼를 대체하지 못하기 때문에 대부분 주키퍼를 사용하고 있다.
- 한개의 카프카 클러스터는 여러개의 브로커로 이루어져있다. 최소한 상용환경에서는 3개의 브로커로 운영되어야 한다.
- 카프카 브로커는 하나하나가 프로세스다. 하나의 서버에는 하나의 카프카 브로커 프로세스가 실행되고 있다.
- 데이터를 안전하게 보관하고 처리하기 위해서 3대 이상의 브로커를 하나의 클러스터로 묶기를 권장한다. 브로커 하나에서 장애가 발생하면 이 브로커를 사용할 수 없고 나머지 브로커에 데이터가 복제되어 있기 때문에 안전하게 분산 저장되기 때문이다.
카프카 클러스터와 주키퍼
- 주키퍼 앙상블이 있을 경우 여러 종류의 클러스터를 운영할 수 있다.
- root znode에 각 클러스터별 znode를 생성하고 클러스터 실행시 root가 아닌 하위 znode로 설정
- 하나의 앙상블로 여러대의 카프카 클러스터를 운영하고 있다. 카프카 클러스터를 운영하기 위해서는 주키퍼가 반드시 필요하다.
카프카 브로커의 역할들
컨트롤러
- 컨트롤러 다수의 브로커중 한 대가 컨트롤러 역할을 한다. 다른 브로커들의 상태를 체크하고 브로커가 클러스터에서 빠지면 해당 브로커에 존재하는 리더 파티션을 재분배한다.
- 카프카는 지속적으로 데이터를 처리해야 하므로 브로커의 상태가 비정상이라면 빠르게 클러스터에서 빼내는 것이 중요하다.
- 컨트롤러 브로커가 장애가 발생하면 다른 브로커가 컨터롤러 역할을 한다.
데이터 삭제
- 브로커가 로그 세그먼트 단위로 데이터를 삭제한다. 카프카는 다른 메시징 플랫폼과 다르게 데이터를 가져가도 토픽의 데이터를 삭제하지 않는다. 원하는 데이터만 콕콕 찝어서 삭제는 불가능하나 로직을 통해서 데이터를 삭제할 수는 있다는 것.
컨슈머 오프셋 저장
- 토픽의 데이터를 컨슈머가 가져갔을떄 이 컨슈머가 어느 오프셋까지 데이터를 가져갔는지 확인하기 위해 오프셋을 특정 토픽에 커밋한다. internal 토픽이라고 __consumer_offsets은 우리가 관리하지 않는다. 컨슈머는 장애가 발생하고 버전 업그레이드를 통한 리스타트를 할 수 있기 때문에 다음 데이터를 알기 위함이다.
그룹 코디네이터
- 컨슈머 그룹 상태를 체크하고 파티션을 컨슈머와 매칭되도록 분배하는 역할을 한다. 각각의 파티션은 컨슈머와 일대일 매핑이 되고 문제가 되는 컨슈머를 삭제하고 나머지 컨슈머가 파티션을 처리하는 과정을 리밸런싱이라고 한다.
브로커 로그와 세그먼트
데이터의 저장
- 카프카를 실행할때 log.dir옵션에 정의한 디렉토리에 데이터를 저장한다. 토픽 이름과 파티션 번호의 조합으로 하위디렉토리를 생성하여 데이터를 저장한다.
- hello.kafka-0 디렉토리 내에는 파티션의 내용이 저장된다. index, log, timeindex 등 파일이 존재한다.
- 파일 시스템을 나뉘어서 저장되는데, 하나는 바이트 단위고 하나는 시간 단위로 나뉘어진다.
- 최대 세그먼트 크기를 지정하면 해당 세그먼트의 크기가 넘어서면 새로 파일이 생성된다.
- 오프셋은 레코드의 고유한 번호이다. 프로듀서가 레코드를 만들어서 브로커로 데이터를 보내게 되면 특정 하티션 중 하나에 데이터가 저장되는데 고유한 번호가 새로 지정된다.
- 총 22개의 레코드가 저장되고 있는 상태라고 할때 레코드가 새로 들어오게 되면 가장 최신의 log파일에 저장된다. 가장 최초의 오프셋 번호가 파일의 이름이 된다.
액티브 세그먼트
- 가장 마지막 세그먼트 파일(스기가 일어나고 있는 파일)을 액티브 세그먼트라고 한다.
- 이 액티브 세그먼트는 브로커의 삭제 대상에 포함되지 않는다. 오프셋 0번의 데이터를 가져가더라도 이 데이터는 삭제되지 않는데 이 데이터는 리텐션 기간에 따라서 삭제 된다.
- 액티브 세그먼트가 아닌 나머지 세그먼트들 중 리텐션 기간에 부합하는 데이터들만 삭제를 한다.
세그먼트와 삭제주기
- retention.ms: 세그먼트를 보유할 최대 기간
- retention.bytes: 파티션당 로그 적재 바이트값이 도달하면 세그먼트 삭제
- log.retention.check.interval.ms: 브로커가 세그먼트가 삭제 영역에 들어왔는지 확인한다
- 파일시스템의 디스크가 용량을 커버가능한지 확인하고 최대 기간을 설정해야한다. 1T * 7days > disk 5T 라고 하면 훨씬 넘어서 써지지 않기 때문에 이 부분을 주의해야한다.
cleanup.policy=delete
- 특정 세그먼트의 특정 오프셋만 삭제하는건 불가능하다.
- 세그먼트 삭제를 수행하면 이 전체 세그먼트 로그가 파일 단위로 삭제가 된다. 삭제가 불가능하기 때문에 이렇게 파일 시스템의 레코드 개별값에 대해서는 수정도 불가능하다.
cleanup.policy=compact
- cleanup.policy=delete는 액티브가 아닌 세그먼트를 기준으로 파일시스템 특정 세그먼트를 삭제되는 것을 알 수 있다.
- 컴팩트는 레코드에는 메시지 키와 값이 있는것을 확인할 수 있다.
- 메시지 키가 다르게 들어갈 수 있다.
- compact로 하게 되면 메시지 키로 삭제된다는 것을 알 수 있다. 가장 마지막 세그먼트 기준으로 가장 최신의 세그먼트를 남긴다.
- 즉 13(k3), 16(k3)가 있을 경우 16(k3)만 남고 나머지는 삭제된다. 가장 최신의 메시지 키가 있는 레코드만 남는다는 것을 알 수 있고 액티브 세그먼트를 제외한 데이터를 대상으로 한다.
테일/헤드영역, 클린/더티 로그
- 액티브를 제외하고 테일 영역과 헤드 영역을 나누어서 관리한다.
- 테일영역은 압축정책에 의해 압축이 완료된 로그를 클린 로그라고 부르고 이 클린 로그가 모인 영역을 테일 영역이라고 한다.
- 그러면 오프셋이 중간중간 빠져 있는것을 확인할 수 있다. 사용자가 삭제한게 아니라 중복된 메시지키를 삭제했기 때문이다.
- 헤드 영역에서 압축이 되기 전 레고크드들을 더티로그라고 부르며 중복된 메시지 키들이 있다. 이를 모여 있는게 헤드 영역에 있다.
min.cleanble.dirty.ratio
- 헤드 영역의 갯수와 테일 영역의 갯수가 비율에 따라 다다랐을때 압축을 수행한다.
- ratio가 0.5이고 클린4개, 더티4개 있을때 50% 비율이 되면 압축을 수행하게 된다.
- 0.9와 같이 크게 설정하면 한번 압축할떄 많은 데이터가 줄어들어 압축효과가 좋지만 더티 레코드가 많아 용량을 많이 차지하니깐 용량 효율이 좋지 않다.
- 0.1로하면 압축이 자주 발생하기 때문에 최신 데이터만 유지할 수 있어 좋긴 하지만 브로커 입장에서는 부담된다.
데이터 복제
- 장애 허용 시스템으로 동작하는 원동력이다. 복제는 클러스터로 묶인 브로커중 일부에 장애가 발생하더라도 데이터를 유실하지 않고 안전하게 사용하기 위함이다.
- 카프카의 데이터 복제단위는 파티션 단위로 이루어진다. 파티션 1번에 대해서 복제가 이루어진다. 파티션 복제의 갯수도 설정하게 되는데 직접 설정하지 않으면 브로커에 설정된 옵션 값을 따라간다. 이 레플리케이션 팩터 최솟값은 1이고 최댓값은 브로커 갯수만큼 설정해서 사용할 수 있다.
- 레플리케이션 팩터가 3으로 설정되면 파티션이 하나있고 복제가 두개가 이루어진다. 그리고 복제된 파티션은 리더와 팔로워로 구성된다.
- 프로듀스, 컨슈머와 직접 통신하는 파티션을 리더, 나머지 복제데이터를 가지고 있는 파티션을 팔로워라고 한다. 즉 리더만 통신하고 나머지는 따라간다.
- 팔로우 파티션은 리더 파티션에 데이터를 추가되는 것을 확인하는데 오프셋을 통해서 확인한다. 자기가 가지고 있는 오프셋과 리더파티션을 비교해서 복제해간다.
- 복제 갯수만큼 저장용량이 증가된다는 단점이 있다. 하지만 복제를 통해 데이터를 안전할 수 있단 장점이 있기 때문에 카프카를 운영할 때는 2 이상의 복제개수를 정하는 것이 중요하다. 디스크를 사용하는 대신 가용성을 높인다는 측면.
ISR(In-Sync-Replicas)
- 리더 파티션과 팔로 파티션이 모두 싱크가 된 상태를 뜻한다. 오프셋의 갯수가 같다는 의미다.
- 리더 파티션의 모든 데이터가 팔로 파티션에 모두 복제가 완료되었다는 것을 의미한다.
- 리더 파티션의 데이터를 모두 복제하지 못한 상태고 장애가 발생했을때 리더 파티션을 지속적으로 처리하기 위해서 새로운 리더를 선출해야하는데 이렇게 싱크 되지 않은경우 팔로 파티션이 리더 파티션이 되면 데이터가 유실될 수 있다.
- unclean,leader.election.enable=true/false 유실을 감수할지 말지의 여부다.
- true이면 유실을 감수하면서 팔로우 파티션을 리더로 승급. false면 절대로 감수하지 않기 때문에 이 브로커가 복구될떄까지 중단된다.
토픽과 파티션
- 토픽은 적어도 한 개 이상의 파티션으로 이루어져있고 각각의 파티션은 큐와 비슷한 구조로 이루어져 있다.
- 프로듀서가 메시지 키와 값이 있는 레코드를 만들어서 각각의 파티션에 데이터를 전송하게 되면 오프셋이 신규로 지속적으로 붙게 된다.
- 일반적인 자료구조의 큐와 다르게 컨슈머가 데이터를 가져가더라도 데이터가 유지된다는 특징을 갖고 있다. 따라서 컨슈머 그룹이 동일한 데이터를 여러번 가져갈 수 있다.
토픽 생성시 파티션이 배치되는 방법
- 파티션이 5개인 토픽을 생성했을때 0번 브로커부터 시작해서 라운드 로빈 방식으로 리더 파티션들이 생성된다.
- 이렇게 리더 파티션이 분배됨으로써 프로듀서가 각각의 리더 파티션과 통신을 할때 균등하게 데이터를 가져갈 수 있다.
- 브로커에 통신이 집중되는 핫스팟 현상을 막고 선형확장을 해서 데이터가 많아지더라도 자연스럽게 대응할 수 있다.
- 리더 파티션이 생성되면 다른 브로커에 팔로우 파티션이 생성된다.
특정 브로커에 파티션이 쏠린 현상
- 리더 파티션이 한개의 브로커에만 존재하기 때문에 프로듀서, 컨슈머 어플리케이션이 하나의 브로커에만 집중하게 되는 현상이 발생한다.
- 그러면 CPU, RAM 리소스가 상당히 많이 차지 하게 된다. 따라서 특정 브로커에 파티션이 몰리는 경우에는 kafka-reassign-partitions.sh명령으로 파티션을 재분배할 수 있다.
파티션 개수와 컨슈머 개수의 처리량
- 파티션과 컨슈머의 관계는 1대1 관계이고 파티션은 최대 한 개의 컨슈머를 가질 수 있다.
- 즉 컨슈머가 더 생성되도 파티션이 연결되지 않는다. 하나의 컨슈머가 장애가 발생했을때 남은 한 개의 컨슈머는 여러개의 파티션을 처리할 수 있다. 컨슈머 개수를 늘림과 동시에 파티션 개수도 느리면 처리량이 증가하는 효과를 볼 수 있다.
- 프로듀서 입장에서 얼마나 데이터가 들어올지 모르는 것. 프로듀서 입장에서 보내는 데이터가 초당 10개라고 할 경우 컨슈머가 초당 1개만 처리하게 된다면 지연이 발생해 컨슈머 랙이 발생한다. 따라서 컨슈머와 파티션 갯수를 늘림으로써 해결할 수 있다.
파티션 개수를 줄이는 것은 불가능
- 한번 토픽에 파티션을 늘려서 운영하고 있을떄 파티션을 다시 줄이는 건 불가능하다.
- 데이터를 세그먼트를 저장하고 있으면 만에 하나 지원한다고 해도 특정 디렉토리에 데이터를 디스크에 저장하고 있는데 디렉토리를 조합하는 과정을 거쳐야 한다. 그렇기 떄문에 아직까지는 파티션 개수를 줄이는 것은 불가능하다
레코드
- 레코드는 타임스탬프, 헤더, 메시지키, 메시지 값 오프셋으로 구성되어 있다. 브로커에 전송되어 저장되면 오프셋과 타임스탬프가 지정되어 저장된다. 한번 적재된 레코드는 수정할 수 없고 로그 리텐션 기간 또는 용량에 따라서만 삭제된다.
타임스탬프
- 타임스템프는 스트림 프로세싱에서 활용되기 위해서 시간을 저장하는 용도다. 유닉스 타임스탬프가 포함되며 프로듀서에서 설정하지 않으면 생성시간이 들어간다. 브로커 적재시간(send())으로 옵션을 변경할 수 있다. 이는 토픽 단위로 설정이 가능하다.
오프셋
- 오프셋은 프로듀서가 생성한 레코드에는 존재하지 않는다. 브로크에 적재될떄 지정이 된다. 0부터 시작되고 1씩 증가된다. 오프셋을 기반으로 처리가 완료된 데이터와 앞으로 처리해야할 데이터를 구분한다. 중복처리를 방지하기 위한 목적으로도 사용한다.
헤더
- 헤더는 키/밸류 데이터를 추가할 수 있으며 레코드 스키마 버전이나 포맷 같은 데이터 프로세싱에 참고할 만한 정보를 담아서 사용할 수 있다.
메시지 키
- 메시지 키는 실제로 데이터를 분류를 위해서 사용하는 용도다. 이를 파티셔닝이라고 부른다. 파티셔너에 따라서 토픽의 파티션 번호가 따라 다르게 된다. 필수값은 아니고 지정하지 않으면 null로 설정된다.
- 기본적으로 null일경우 round-robin에 의해서 순회해서 균등하게 파티션에 저장된다.
- null이 아닌 경우는 해쉬값에 의해서 특정파티션에 매핑되어 전달된다. 특정 문자열 key를 넣으면 해시처리 되어 동일한 파티션에 들어가게 된다. 동일한 데이터는 동일한 위치에 들어가서 append 되기 때문에 순차적이다.
메시지 값
- 메시지 값은 실질적으로 처리할 데이터가 담기는 공간이다. 제네릭으로 사용자에 의해 지정된다.
- 다양한 형태로 지정가능하며 직렬화, 역직렬화 클래스를 만들어 사용할 수 있다. Pojo형태로 만들어서 사용할 수 있다.
- 컨슈머 입장에서는 알 수가 없기 때문에 미리 역직렬화 포맷을 알고 있어야 한다.
- 따라서 대부분 string으로 직렬화/역직렬화를 사용한다. 그러나 스트링은 공간낭비가 많이 된다는 점을 알 고 있어야 한다.
토픽 이름의 제약 조건
- 토픽이름은 영어 대소문자와 숫자0~9그리고 마침표(.), 언더바(_), 하이픈(-) 조합으로 생성할 수 있다. 내부적으로 마침표와 언더바가 동시에 들어간 이름을 사용하지 않는다.
- 한번 만든 토픽이름은 변경을 지원하지 않기 때문에 이름을 모호하게 생성하지 않는다. 삭제 후 다시 생성하는 것 외에는 방법이 없다
클라이언트 메타데이터
- 카프카 클라이언트와 클러스터의 통신하는 방식.
클라이언트에서 클러스터에게 메타데이터를 요청하는데 메타 데이터에는 카프카 브로커의 리더의 위치정보를 요청한다.
- 클러스터로 부터 메타데이터를 받아 리더 파티션의 위치정보를 확인해서 클라이언트(프로듀서 또는 컨슈머)는 직접적으로 브로커와 통신을 하게 된다.
클라이언트 메타데이터가 이슈가 발생한 경우
- 리더 파티션의 위치가 잘못되어 있을 수 있다.
- 카프카 클라이언트의 업데이터 되지 않은 메타데이터를 가지고 잘못된 브로커에 요청을 하게 되면 LEADER_NOT_AVAILABLE익셉션이 발생하게 된다.
- 대부분은 메타데이터가 업데이터 되지 않아서 리프래시 이슈로 발생하게 된다. 만약 자주 발생한다면 메타데이터 리프래시 간격을 확인해야한다.