하드웨어 선정, 클러스터 사이징, OS 튜닝과 프로덕션 고려사항
Kafka: The Definitive Guide (O'Reilly, 2017, 1st Edition) — Neha Narkhede, Gwen Shapira, Todd Palino Chapter 2: Installing Kafka — Hardware Selection ~ Summary (pp.28-39)
브로커 하드웨어를 고를 때 디스크 처리량·용량, 메모리, 네트워크, CPU가 각각 어떤 성능에 대응하는지 정리하고, 클라우드 인스턴스 선택과 클러스터 브로커 대수 산정 방법을 다룬다. 이어서 프로덕션 브로커를 위한 Linux 커널 튜닝(가상 메모리, 파일시스템, 네트워크), G1 GC 옵션, 데이터센터 레이아웃, Zookeeper 공용 사용 시의 주의점을 다룬다.
하드웨어 선정: 디스크, 메모리, 네트워크, CPU
Kafka 브로커에 적합한 하드웨어를 고르는 일은 과학보다 예술에 가깝다. Kafka 자체는 특정 하드웨어 구성을 요구하지 않고 어떤 시스템에서도 문제없이 돌아간다. 다만 성능이 문제가 되기 시작하면 디스크 처리량과 용량, 메모리, 네트워킹, CPU가 전체 성능에 기여한다.
디스크 처리량. 프로듀서 클라이언트의 성능에 가장 직접적으로 영향을 주는 것은 로그 세그먼트를 저장하는 브로커 디스크의 처리량이다. Kafka 메시지는 생성될 때 로컬 저장소에 커밋되어야 하고, 대부분의 클라이언트는 적어도 한 브로커가 메시지 커밋을 확인해 줄 때까지 기다린 뒤에야 전송이 성공했다고 간주한다. 즉 디스크 쓰기가 빠를수록 프로듀스 지연이 낮아진다. 전통적인 회전식 하드디스크(HDD)와 SSD 중 SSD는 탐색·접근 시간이 극적으로 짧아 최고의 성능을 준다. HDD는 더 경제적이고 단위당 용량이 크다. 브로커에 HDD를 여러 개 두거나(다중 데이터 디렉터리) RAID로 구성해 성능을 개선할 수도 있다. 그 밖에 드라이브 기술(SAS냐 SATA냐)이나 드라이브 컨트롤러의 품질도 처리량에 영향을 준다.
디스크 용량. 필요한 용량은 언제든 얼마나 많은 메시지를 보관해야 하는지로 결정된다. 브로커가 하루 1TB의 트래픽을 받고 7일을 보관해야 한다면 로그 세그먼트를 위해 최소 7TB의 사용 가능한 저장 공간이 필요하다. 여기에 다른 파일을 위해 최소 10%의 오버헤드를 더하고, 트래픽 변동이나 시간에 따른 증가에 대비한 버퍼도 고려해야 한다. 저장 용량은 클러스터 크기를 정하고 언제 확장할지 판단하는 요소 중 하나다. 토픽당 파티션을 여러 개 두면 클러스터 전체 트래픽을 분산할 수 있어, 단일 브로커의 밀도로 충분하지 않을 때 브로커를 추가해 가용 용량을 늘릴 수 있다. 필요한 디스크 용량은 클러스터에 선택한 복제 전략에도 좌우된다.
메모리. Kafka 컨슈머의 정상적인 동작 모드는 파티션의 끝에서 읽는 것으로, 컨슈머가 프로듀서를 거의 따라잡은 상태다. 이 상황에서 컨슈머가 읽는 메시지는 시스템의 페이지 캐시에 최적으로 저장되어 있어, 브로커가 디스크에서 다시 읽는 것보다 훨씬 빠르다. 따라서 페이지 캐시로 쓸 메모리가 많을수록 컨슈머 클라이언트 성능이 좋아진다. Kafka 자체는 JVM에 큰 힙 메모리를 설정할 필요가 없다. 초당 X개의 메시지와 X Mbps의 데이터 레이트를 처리하는 브로커도 5GB 힙으로 동작할 수 있다. 나머지 시스템 메모리는 페이지 캐시로 쓰이며 사용 중인 로그 세그먼트를 캐싱해 Kafka에 도움을 준다. 이것이 Kafka를 다른 중요한 애플리케이션과 같은 시스템에 배치하는 것이 권장되지 않는 주된 이유다 — 페이지 캐시를 나눠 쓰게 되어 Kafka의 컨슈머 성능이 떨어지기 때문이다.
네트워킹. 사용 가능한 네트워크 처리량은 Kafka가 다룰 수 있는 최대 트래픽을 규정한다. 디스크 저장 용량과 함께 클러스터 사이징의 결정적 요인이 되는 경우가 많다. 이 문제는 Kafka의 다중 컨슈머 지원이 만들어 내는 인바운드·아웃바운드 네트워크 사용량의 본질적 불균형 때문에 복잡해진다. 프로듀서가 어떤 토픽에 초당 1MB를 쓰더라도 컨슈머 수만큼 아웃바운드 사용량에 배수가 걸린다. 클러스터 복제나 미러링 같은 다른 동작도 요구량을 늘린다. 네트워크 인터페이스가 포화되면 클러스터 복제가 뒤처지는 일이 드물지 않게 발생하며, 이는 클러스터를 취약한 상태에 놓이게 한다.
CPU. 처리 능력은 디스크와 메모리만큼 중요하지는 않지만 브로커 전체 성능에 어느 정도 영향을 준다. 이상적으로는 클라이언트가 메시지를 압축해 네트워크와 디스크 사용을 최적화해야 한다. 그런데 브로커는 개별 메시지의 체크섬을 검증하고 오프셋을 할당하기 위해 모든 메시지 배치의 압축을 풀어야 하고, 그다음 디스크에 저장하기 위해 배치를 다시 압축해야 한다. Kafka가 처리 능력을 요구하는 대부분의 지점이 바로 여기다. 그렇더라도 CPU가 하드웨어 선택의 주된 요소가 되어서는 안 된다.
핵심 포인트
- 디스크 처리량 → 프로듀스 지연 (커밋 확인을 기다리므로 쓰기 속도가 곧 지연)
- 디스크 용량 = 일일 트래픽 × 보관 일수 + 10% 이상 오버헤드 + 변동/증가 버퍼 + 복제 배수
- 메모리는 페이지 캐시용. Kafka 힙은 5GB로도 충분 → 다른 큰 애플리케이션과 코로케이션 비권장
- 네트워크는 인바운드/아웃바운드 불균형이 본질적. 포화 시 복제가 뒤처져 취약해짐
- CPU 사용의 대부분은 배치 압축 해제 후 재압축 (체크섬 검증 + 오프셋 할당 때문)
클라우드에서의 Kafka와 브로커 대수 산정
Kafka는 AWS 같은 클라우드 환경에도 흔히 설치된다. 클라우드는 CPU, 메모리, 디스크의 조합이 서로 다른 인스턴스를 다양하게 제공하므로 Kafka의 성능 특성 중 무엇을 우선할지 정해야 인스턴스를 고를 수 있다. 시작점은 필요한 데이터 리텐션 양이고, 그다음이 프로듀서에게 필요한 성능이다. 아주 낮은 지연이 필요하다면 로컬 SSD 저장소를 갖춘 I/O 최적화 인스턴스가 필요할 수 있고, 그렇지 않다면 AWS Elastic Block Store 같은 저장소로도 충분할 수 있다. 이 결정을 내리고 나면 남은 CPU와 메모리 옵션은 성능에 적절하게 정해진다. 실제로 AWS에서는 m4 또는 r3 인스턴스 타입이 흔한 선택이다. m4는 더 긴 리텐션을 허용하지만 elastic block storage 기반이라 디스크 처리량이 낮고, r3는 로컬 SSD 드라이브로 훨씬 나은 처리량을 내지만 보관 가능한 데이터 양이 제한된다. 두 장점을 모두 원한다면 훨씬 비싼 i2나 d2 인스턴스 타입으로 올라가야 한다.
단일 Kafka 서버는 로컬 개발이나 개념 증명에는 잘 맞지만, 여러 브로커를 클러스터로 구성하면 큰 이점이 있다. 가장 큰 이점은 여러 서버로 부하를 확장할 수 있다는 것이고, 그다음이 복제로 단일 시스템 장애에 의한 데이터 손실을 막는 것이다. 복제는 클라이언트에 대한 가용성을 유지한 채로 Kafka나 기반 시스템의 유지보수 작업을 수행할 수 있게도 해 준다.
브로커 대수는 두 가지 축으로 산정한다.
첫째, 용량 축이다. 메시지를 보관하는 데 필요한 디스크 용량이 얼마이고 브로커 한 대에 사용 가능한 저장 공간이 얼마인지 본다. 클러스터가 10TB를 보관해야 하고 브로커 한 대가 2TB를 저장할 수 있다면 최소 클러스터 크기는 5대다. 여기에 복제를 사용하면 선택한 복제 계수에 따라 저장 요구가 최소 100% 이상 늘어난다. 즉 같은 클러스터라도 복제를 구성하면 최소 10대가 필요해진다.
둘째, 요청을 처리할 수 있는 클러스터의 능력이다. 예를 들어 네트워크 인터페이스의 용량이 얼마이고, 데이터 컨슈머가 여럿이거나 리텐션 기간 동안 트래픽이 일정하지 않을 때(피크 시간대 버스트) 클라이언트 트래픽을 감당할 수 있는지 본다. 단일 브로커의 네트워크 인터페이스가 피크에 80%까지 사용되고 그 데이터의 컨슈머가 둘이라면, 브로커가 두 대가 아닌 한 컨슈머들은 피크 트래픽을 따라갈 수 없다. 클러스터에서 복제를 사용한다면 복제 또한 데이터의 추가 컨슈머이므로 계산에 넣어야 한다. 디스크 처리량이나 시스템 메모리가 부족해 생기는 성능 문제를 해결하기 위해 브로커를 늘리는 것도 바람직할 수 있다.
여러 브로커가 하나의 클러스터에 합류하기 위한 브로커 설정 요구사항은 두 가지뿐이다. 첫째, 모든 브로커의 zookeeper.connect 설정이 동일해야 한다. 이것이 클러스터가 메타데이터를 저장하는 Zookeeper 앙상블과 경로를 지정한다. 둘째, 클러스터 내 모든 브로커의 broker.id 값이 유일해야 한다. 두 브로커가 같은 broker.id로 같은 클러스터에 합류하려 하면 두 번째 브로커는 에러를 로그에 남기고 기동에 실패한다.
핵심 포인트
- AWS: m4는 긴 리텐션 / 낮은 디스크 처리량, r3는 로컬 SSD로 높은 처리량 / 제한된 용량, 둘 다 원하면 i2·d2(고비용)
- 용량 축: 10TB ÷ 브로커당 2TB = 5대, 복제 적용 시 최소 10대
- 요청 처리 축: NIC 피크 80% + 컨슈머 2개 → 2대 필요. 복제도 추가 컨슈머로 계산
- 클러스터 합류 요구사항은 zookeeper.connect 동일 + broker.id 유일, 두 가지뿐
OS 튜닝: 가상 메모리, 디스크, 네트워크
대부분의 Linux 배포판의 기본 커널 튜닝은 대체로 잘 동작하지만, Kafka 브로커를 위해 성능을 개선할 수 있는 변경이 몇 가지 있다. 주로 가상 메모리와 네트워킹 서브시스템, 그리고 로그 세그먼트 저장용 디스크 마운트 포인트에 관한 것이다. 이 값들은 보통 /etc/sysctl.conf에 설정한다.
가상 메모리. 처리량이 중요한 애플리케이션이 대개 그렇듯 스와핑은 거의 모든 비용을 들여서라도 피하는 것이 좋다. 메모리 페이지가 디스크로 스왑되면 Kafka의 모든 성능 측면에 눈에 띄는 영향으로 나타난다. 게다가 Kafka는 시스템 페이지 캐시를 많이 쓰는데, VM 시스템이 디스크로 스왑하고 있다면 페이지 캐시에 충분한 메모리가 할당되지 않고 있다는 뜻이다.
스와핑을 피하는 한 가지 방법은 스왑 공간을 아예 설정하지 않는 것이다. 스왑은 필수가 아니지만, 시스템에 치명적인 일이 생겼을 때 안전망을 제공한다. 스왑이 있으면 OS가 메모리 부족 조건 때문에 프로세스를 갑자기 죽이는 것을 막을 수 있다. 이 때문에 권장되는 것은 vm.swappiness 파라미터를 1처럼 아주 낮은 값으로 설정하는 것이다. 이 값은 VM 서브시스템이 페이지 캐시에서 페이지를 버리는 대신 스왑 공간을 사용할 가능성을 나타내는 백분율이다. 스왑보다는 페이지 캐시 크기를 줄이는 편이 낫다. 참고로 예전에는 vm.swappiness를 0으로 두라는 권장이 일반적이었다. 이 값은 원래 '메모리 부족 조건이 아니면 스왑하지 않음'을 뜻했는데, Linux 커널 3.5-rc1부터 의미가 '어떤 경우에도 절대 스왑하지 않음'으로 바뀌었고, 이 변경은 Red Hat Enterprise Linux 2.6.32-303을 포함한 많은 배포판에 백포트되었다. 그래서 지금은 1이 권장된다.
더티 페이지를 디스크로 플러시하는 방식을 조정하는 것도 이점이 있다. Kafka는 프로듀서에게 좋은 응답 시간을 주기 위해 디스크 I/O 성능에 의존한다. 로그 세그먼트를 보통 빠른 디스크(SSD 같은 응답 시간이 빠른 개별 디스크나 캐싱용 NVRAM이 상당한 RAID 같은 디스크 서브시스템)에 두는 이유도 이것이다. 그 결과 백그라운드 플러시 프로세스가 디스크에 쓰기 시작하기 전에 허용되는 더티 페이지 수를 줄일 수 있다. vm.dirty_background_ratio 값을 기본값 10보다 낮게 설정하면 되며, 이 값은 전체 시스템 메모리에 대한 백분율로 많은 상황에서 5가 적절하다. 다만 0으로 설정해서는 안 된다. 그러면 커널이 페이지를 계속 플러시하게 되어, 기반 장치 성능의 일시적 스파이크에 대해 디스크 쓰기를 버퍼링하는 커널의 능력이 사라진다.
커널이 동기적 플러시를 강제하기 전에 허용되는 더티 페이지 총량은 vm.dirty_ratio를 기본값 20보다 '높여서' 늘릴 수 있다(역시 전체 시스템 메모리에 대한 백분율). 가능한 값의 범위는 넓지만 60에서 80 사이가 합리적인 수치다. 이 설정은 약간의 위험을 동반하는데, 플러시되지 않은 디스크 활동의 양과 동기 플러시가 강제될 때 긴 I/O 정지가 발생할 가능성 양쪽 모두에 관한 것이다. vm.dirty_ratio를 높게 잡는다면 시스템 장애에 대비해 Kafka 클러스터에 복제를 사용하는 것이 강력히 권장된다. 이 파라미터 값을 정할 때는 프로덕션이든 시뮬레이션이든 부하가 걸린 Kafka 클러스터에서 시간에 따른 더티 페이지 수를 검토하는 것이 현명하다. 현재 더티 페이지 수는 /proc/vmstat 파일에서 확인할 수 있다.
cat /proc/vmstat | egrep "dirty|writeback"
# nr_dirty 3875
# nr_writeback 29
# nr_writeback_temp 0
디스크. 디스크 장치 하드웨어 선택과 RAID 구성 다음으로 성능에 큰 영향을 주는 것은 파일시스템 선택이다. 로컬 파일시스템으로 가장 흔한 선택은 EXT4와 XFS다. 최근 XFS가 많은 Linux 배포판의 기본 파일시스템이 되었는데, 그럴 만한 이유가 있다 — 최소한의 튜닝만으로 대부분의 워크로드에서 EXT4보다 나은 성능을 낸다. EXT4도 좋은 성능을 낼 수 있지만 덜 안전하다고 여겨지는 튜닝 파라미터를 써야 한다. 여기에는 플러시 빈도를 줄이기 위해 커밋 간격을 기본 5초보다 길게 설정하는 것이 포함된다. EXT4는 블록의 지연 할당(delayed allocation)도 도입했는데, 시스템 장애 시 데이터 손실과 파일시스템 손상 가능성이 더 커진다. XFS도 지연 할당 알고리즘을 쓰지만 EXT4의 것보다 일반적으로 더 안전하다. XFS는 파일시스템이 수행하는 자동 튜닝 외의 추가 튜닝 없이도 Kafka 워크로드에서 더 나은 성능을 내고, 디스크 쓰기를 배치로 묶는 데도 더 효율적이어서 전반적인 I/O 처리량이 좋다.
로그 세그먼트를 담는 마운트에 어떤 파일시스템을 쓰든, 그 마운트 포인트에 noatime 마운트 옵션을 설정하는 것이 바람직하다. 파일 메타데이터에는 생성 시각(ctime), 마지막 수정 시각(mtime), 마지막 접근 시각(atime) 세 가지 타임스탬프가 있다. 기본적으로 atime은 파일을 읽을 때마다 갱신되는데 이는 대량의 디스크 쓰기를 발생시킨다. atime 속성은 애플리케이션이 파일이 마지막 수정 이후 접근되었는지 알아야 하는 경우가 아니면 일반적으로 별 쓸모가 없다고 여겨진다(그런 경우에는 realtime 옵션을 쓸 수 있다). Kafka는 atime을 전혀 쓰지 않으므로 끄는 것이 안전하다. 마운트에 noatime을 설정하면 이 타임스탬프 갱신이 일어나지 않지만 ctime과 mtime의 정상적 처리에는 영향을 주지 않는다.
네트워킹. 커널이 기본적으로 크고 빠른 데이터 전송에 맞춰 튜닝되어 있지 않기 때문에, 네트워크 트래픽이 많은 애플리케이션에서는 Linux 네트워킹 스택의 기본 튜닝을 조정하는 것이 일반적이다. Kafka에 권장되는 변경은 대부분의 웹 서버나 다른 네트워킹 애플리케이션에 권장되는 것과 같다.
첫 번째 조정은 소켓별 송신·수신 버퍼에 할당되는 메모리의 기본값과 최대값을 바꾸는 것으로, 큰 전송에서 성능을 크게 높인다. 소켓당 송·수신 버퍼 기본 크기 파라미터는 net.core.wmem_default와 net.core.rmem_default이며 131072(128KiB)가 합리적인 설정이다. 최대 크기 파라미터는 net.core.wmem_max와 net.core.rmem_max이며 2097152(2MiB)가 합리적이다. 최대 크기는 모든 소켓이 그만큼의 버퍼를 할당받는다는 뜻이 아니라, 필요할 때 그만큼까지 허용한다는 뜻이다.
소켓 설정에 더해 TCP 소켓의 송·수신 버퍼 크기는 net.ipv4.tcp_wmem과 net.ipv4.tcp_rmem으로 따로 설정해야 한다. 이들은 최소·기본·최대 크기를 공백으로 구분한 세 정수로 지정한다. 최대 크기는 net.core.wmem_max와 net.core.rmem_max로 지정한 전체 소켓 값보다 클 수 없다. 예를 들어 '4096 65536 2048000'은 4KiB 최소, 64KiB 기본, 2MiB 최대 버퍼를 뜻한다. 브로커의 실제 워크로드에 따라 네트워크 연결의 버퍼링을 더 크게 하기 위해 최대값을 올릴 수 있다.
그 밖에 유용한 네트워크 튜닝 파라미터가 몇 가지 더 있다. net.ipv4.tcp_window_scaling을 1로 설정해 TCP 윈도우 스케일링을 켜면 클라이언트가 데이터를 더 효율적으로 전송하고 그 데이터를 브로커 쪽에서 버퍼링할 수 있다. net.ipv4.tcp_max_syn_backlog를 기본값 1024보다 올리면 더 많은 동시 연결을 수락할 수 있다. net.core.netdev_max_backlog를 기본값 1000보다 올리면, 특히 멀티기가비트 네트워크 연결 속도에서 커널이 처리할 패킷을 더 많이 큐에 넣을 수 있어 네트워크 트래픽 버스트에 도움이 된다.
핵심 포인트
- vm.swappiness=1 (0이 아닌 이유: 커널 3.5-rc1 / RHEL 2.6.32-303부터 0의 의미가 '절대 스왑 금지'로 변경)
- vm.dirty_background_ratio: 기본 10 → 5로 '낮춤' (0은 금물)
- vm.dirty_ratio: 기본 20 → 60~80으로 '높임' (높게 잡으면 복제 필수)
- 파일시스템은 XFS 권장 (EXT4는 덜 안전한 튜닝 필요, 지연 할당 위험이 더 큼)
- 로그 세그먼트 마운트에 noatime — Kafka는 atime을 쓰지 않으며 ctime/mtime에 영향 없음
- net.core.{w,r}mem_default=131072(128KiB), net.core.{w,r}mem_max=2097152(2MiB)
- net.ipv4.tcp_{w,r}mem은 '최소 기본 최대' 3개 값 (예: 4096 65536 2048000)
- tcp_window_scaling=1, tcp_max_syn_backlog>1024, netdev_max_backlog>1000
프로덕션 고려사항: GC, 데이터센터 레이아웃, Zookeeper 공유
가비지 컬렉터 옵션. Java GC 옵션 튜닝은 늘 애플리케이션의 메모리 사용 방식에 대한 상세한 정보와 상당한 관찰·시행착오를 요구하는 예술에 가까운 일이었다. Java 7과 G1(Garbage First) 컬렉터의 등장으로 이것이 달라졌다. G1은 서로 다른 워크로드에 자동으로 적응하고 애플리케이션 수명 전반에 걸쳐 일관된 GC 정지 시간을 제공하도록 설계되었다. 또 힙을 더 작은 존으로 분할해 매 정지마다 전체 힙을 수집하지 않기 때문에 큰 힙도 무리 없이 다룬다.
G1은 정상 운영에서 최소한의 설정만으로 이 모든 것을 해낸다. 성능 조정을 위한 설정 옵션은 두 가지다.
- MaxGCPauseMillis: 각 GC 사이클에서 선호되는 정지 시간을 지정한다. 고정된 최대치가 아니며 필요하면 G1이 이 시간을 초과할 수 있다. 기본값은 200밀리초다. G1은 각 사이클이 대략 200ms가 되도록 GC 사이클의 빈도와 매 사이클에서 수집할 존의 개수를 조절하려 시도한다. - InitiatingHeapOccupancyPercent: G1이 수집 사이클을 시작하기 전에 사용 중일 수 있는 전체 힙의 비율을 지정한다. 기본값은 45다. 즉 힙의 45%가 사용되기 전에는 수집 사이클을 시작하지 않는다. 여기에는 new(Eden)와 old 존 사용량이 모두 포함된다.
Kafka 브로커는 힙 메모리 사용과 가비지 객체 생성에서 꽤 효율적이므로 이 옵션들을 더 낮게 설정할 수 있다. 이 절의 GC 튜닝 옵션은 메모리 64GB의 서버에서 5GB 힙으로 Kafka를 운영하는 경우에 적절한 것으로 확인된 값이다. MaxGCPauseMillis는 20ms로 설정할 수 있고, InitiatingHeapOccupancyPercent는 35로 설정해 기본값보다 조금 일찍 GC가 돌게 한다.
Kafka의 시작 스크립트는 G1 컬렉터를 사용하지 않고 parallel new와 concurrent mark and sweep 가비지 컬렉션을 기본으로 사용한다. 환경 변수로 쉽게 바꿀 수 있다.
export JAVA_HOME=/usr/java/jdk1.8.0_51
export KAFKA_JVM_PERFORMANCE_OPTS="-server -XX:+UseG1GC \
-XX:MaxGCPauseMillis=20 -XX:InitiatingHeapOccupancyPercent=35 \
-XX:+DisableExplicitGC -Djava.awt.headless=true"
/usr/local/kafka/bin/kafka-server-start.sh -daemon /usr/local/kafka/config/server.properties
데이터센터 레이아웃. 개발 시스템에서는 브로커의 물리적 위치가 큰 문제가 아니다. 클러스터가 잠깐 부분적으로 또는 전면적으로 사용 불가여도 영향이 심각하지 않기 때문이다. 그러나 프로덕션 트래픽을 처리할 때는 다운타임이 곧 돈의 손실이다. 이때 클러스터 내 복제 구성이 중요해지고, 브로커가 데이터센터의 어느 랙에 물리적으로 놓이는지도 중요해진다. Kafka를 배포하기 전에 다루지 않으면 서버를 옮기는 비싼 유지보수가 필요해질 수 있다.
(집필 시점 기준) Kafka 브로커는 새 파티션을 브로커에 할당할 때 랙 인식(rack awareness)이 없다. 즉 두 브로커가 같은 물리적 랙에 있거나 (AWS 같은 클라우드에서) 같은 가용 영역에 있다는 사실을 고려하지 못하므로, 한 파티션의 모든 복제본이 같은 랙에서 전원과 네트워크를 공유하는 브로커들에 배정되는 일이 쉽게 일어난다. 그 랙에 장애가 나면 해당 파티션들이 오프라인이 되어 클라이언트가 접근할 수 없게 되고, 복구 시 unclean leader election으로 인한 추가 데이터 손실까지 발생할 수 있다.
모범 사례는 클러스터의 각 Kafka 브로커를 서로 다른 랙에 설치하거나, 최소한 전원과 네트워크 같은 인프라 서비스의 단일 장애점을 공유하지 않게 하는 것이다. 보통 이는 브로커를 돌릴 서버를 이중 전원 연결(서로 다른 두 회로)과 이중 네트워크 스위치(서버 자체에는 본딩 인터페이스로 무중단 failover)로 배포하는 것을 뜻한다. 이중화가 되어 있더라도 브로커를 완전히 분리된 랙에 두는 이점은 남는다. 때때로 랙이나 캐비닛을 오프라인으로 만들어야 하는 물리 유지보수(서버 이동, 전원 재배선 등)가 필요하기 때문이다.
Zookeeper에 애플리케이션 함께 두기. Kafka는 브로커, 토픽, 파티션에 대한 메타데이터를 Zookeeper에 저장한다. Zookeeper에 대한 쓰기는 컨슈머 그룹의 멤버십이 바뀌거나 Kafka 클러스터 자체가 바뀔 때만 수행된다. 이 트래픽 양은 매우 적어 단일 Kafka 클러스터를 위해 전용 Zookeeper 앙상블을 두는 것을 정당화하지 못한다. 실제로 많은 배포에서 하나의 Zookeeper 앙상블을 여러 Kafka 클러스터가 사용한다(앞서 설명한 대로 클러스터마다 chroot Zookeeper 경로를 쓴다).
다만 특정 설정에서 컨슈머와 Zookeeper에 관한 우려가 있다. Apache Kafka 0.9.0.0 이전에는 브로커뿐 아니라 컨슈머도 Zookeeper를 직접 사용해 컨슈머 그룹의 구성, 소비 중인 토픽, 소비 중인 각 파티션의 오프셋을 주기적으로 커밋했다(그룹 내 컨슈머 간 failover를 위해). 0.9.0.0에서 새로운 컨슈머 인터페이스가 도입되어 이를 Kafka 브로커와 직접 관리할 수 있게 되었다.
컨슈머는 오프셋 커밋에 Zookeeper와 Kafka 중 무엇을 쓸지 선택할 수 있고 커밋 간격도 설정할 수 있다. 컨슈머가 오프셋에 Zookeeper를 쓰면, 각 컨슈머는 소비 중인 모든 파티션에 대해 매 간격마다 Zookeeper 쓰기를 수행한다. 오프셋 커밋의 합리적인 간격은 1분인데, 이는 컨슈머 장애 시 컨슈머 그룹이 중복 메시지를 읽게 되는 기간이기 때문이다. 이 커밋은 특히 컨슈머가 많은 클러스터에서 상당한 양의 Zookeeper 트래픽이 될 수 있어 고려해야 한다. Zookeeper 앙상블이 그 트래픽을 감당하지 못하면 더 긴 커밋 간격을 써야 할 수도 있다. 다만 최신 Kafka 라이브러리를 쓰는 컨슈머는 오프셋 커밋에 Kafka를 사용해 Zookeeper 의존을 없애는 것이 권장된다.
여러 Kafka 클러스터가 하나의 앙상블을 쓰는 경우를 제외하면, 가능하다면 앙상블을 다른 애플리케이션과 공유하지 않는 것이 권장된다. Kafka는 Zookeeper 지연과 타임아웃에 민감하며, 앙상블과의 통신 중단은 브로커를 예측 불가능하게 동작하게 만든다. 이는 여러 브로커가 동시에 Zookeeper 연결을 잃어 오프라인 파티션을 만드는 상황으로 쉽게 이어질 수 있다. 또한 클러스터 컨트롤러에 부담을 주어, 중단이 지나간 한참 뒤에 브로커의 controlled shutdown을 시도할 때 같은 미묘한 에러로 나타나기도 한다. 과도한 사용이나 부적절한 운영으로 Zookeeper 앙상블에 부담을 주는 다른 애플리케이션은 자체 앙상블로 분리해야 한다.
핵심 포인트
- G1의 두 옵션: MaxGCPauseMillis(기본 200ms, 선호값일 뿐 고정 상한 아님 → 브로커 권장 20ms), InitiatingHeapOccupancyPercent(기본 45 → 권장 35, Eden+old 합산)
- 권장값의 전제: 64GB 메모리 서버에 5GB 힙
- Kafka 시작 스크립트 기본 GC는 G1이 아니라 parallel new + CMS → KAFKA_JVM_PERFORMANCE_OPTS로 변경
- 집필 시점 브로커에는 랙 인식이 없어 한 파티션의 복제본이 같은 랙에 몰릴 수 있다 → 브로커를 서로 다른 랙에
- 여러 Kafka 클러스터의 앙상블 공유는 chroot로 OK. 다른 애플리케이션과의 공유는 비권장
- 0.9.0.0에서 새 컨슈머 인터페이스 도입 — 오프셋은 Zookeeper가 아닌 Kafka에 커밋 권장
이 모듈과 연관된 문항 7개가 문제 은행에 있습니다.
이 내용으로 문제 풀어보기