브로커 지표 상세 — 컨트롤러, 요청 핸들러, 트래픽, 요청 타이밍
Kafka: The Definitive Guide (O'Reilly, 2017, 1st Edition) — Neha Narkhede, Gwen Shapira, Todd Palino Chapter 10: Monitoring Kafka — Broker Metrics / Request metrics / Topic and Partition Metrics (pp.220-231)
under-replicated partitions 외에 대시보드에 반드시 올려야 할 브로커 지표들을 정리한다. 컨트롤러 존재 여부(active controller count), 브로커 부하의 지표인 request handler idle ratio, 트래픽 지표(bytes in/out, messages in)와 rate 지표의 속성 구조, 파티션/리더 수, offline partitions, 요청 타입별 8개 지표, 그리고 토픽·파티션 단위 지표를 다룬다.
Active controller count — 정확히 하나여야 한다
active controller count는 이 브로커가 현재 클러스터의 컨트롤러인지를 나타낸다. 값은 0 또는 1이며, 1이면 그 브로커가 컨트롤러다.
- JMX MBean: kafka.controller:type=KafkaController,name=ActiveControllerCount
항상 오직 한 브로커만 컨트롤러여야 하고, 동시에 클러스터에는 반드시 컨트롤러가 한 대 있어야 한다. 두 브로커가 자신이 컨트롤러라고 보고한다면, 종료됐어야 할 컨트롤러 스레드가 멈춰 있는 상태다. 이 경우 파티션 이동 같은 관리 작업이 제대로 실행되지 않을 수 있고, 최소한 두 브로커를 재시작해야 한다. 여분의 컨트롤러가 있으면 브로커의 controlled shutdown도 자주 실패한다.
반대로 어떤 브로커도 컨트롤러라고 주장하지 않으면, 토픽·파티션 생성이나 브로커 장애 같은 상태 변경에 클러스터가 제대로 반응하지 못한다. 예를 들어 ZooKeeper 클러스터와의 네트워크 분단이 이런 문제를 일으킬 수 있다. 근본 원인을 고친 뒤에는 컨트롤러 스레드 상태를 초기화하기 위해 클러스터의 모든 브로커를 재시작하는 것이 현명하다.
핵심 포인트
- 값은 0 또는 1, 클러스터 전체 합계는 항상 정확히 1이어야 한다
- 2가 되면 컨트롤러 스레드가 멈춘 상태 — 해당 브로커들 재시작 필요, controlled shutdown도 실패하기 쉽다
- 0이면 상태 변경(토픽 생성, 브로커 장애 처리)이 동작하지 않는다
Request handler idle ratio — 브로커 부하의 척도
Kafka는 클라이언트 요청 처리에 두 개의 스레드 풀을 쓴다. 네트워크 핸들러와 요청 핸들러다. 네트워크 핸들러 스레드는 네트워크를 통해 클라이언트와 데이터를 읽고 쓰는 역할만 하고 큰 처리를 하지 않아서 고갈 우려가 적다. 반면 요청 핸들러 스레드는 요청 자체를 처리하며 여기에는 메시지를 디스크에 읽고 쓰는 일이 포함된다. 그래서 브로커 부하가 커지면 이 풀이 직접 영향을 받는다.
- 지표 이름: Request handler average idle percentage - JMX MBean: kafka.server:type=KafkaRequestHandlerPool,name=RequestHandlerAvgIdlePercent - 값 범위: 0과 1 사이의 실수(양끝 포함)
이 지표는 요청 핸들러가 사용되지 않는 시간의 비율이다. 값이 낮을수록 브로커 부하가 크다. 경험적으로 유휴 비율이 20% 미만이면 문제 소지가 있고, 10% 미만이면 대개 이미 진행 중인 성능 문제다.
스레드 사용률이 높은 원인은 클러스터 규모 부족 외에 두 가지다. 첫째, 풀의 스레드가 부족한 경우다. 다만 수백 개가 필요할 것 같지만 실제로는 브로커의 CPU 수보다 많은 스레드를 설정할 필요가 없다. Kafka는 오래 걸릴 요청을 purgatory로 넘겨 두는 식으로 요청 핸들러를 영리하게 쓰기 때문이다(쿼터로 요청이 지연될 때나 produce 요청에 여러 개의 확인이 필요할 때 등). 일반적으로 요청 핸들러 스레드 수는 시스템의 프로세서 수(하이퍼스레딩 포함)와 같게 잡는다.
둘째, 스레드가 요청마다 불필요한 일을 하는 경우다. Kafka 0.10 이전에는 요청 핸들러 스레드가 들어오는 모든 메시지 배치를 압축 해제하고, 메시지를 검증하고 오프셋을 부여한 다음, 디스크에 쓰기 전에 다시 압축했다. 게다가 압축 메서드가 모두 동기 락 뒤에 있었다. 0.10부터는 메시지 배치 안에서 상대 오프셋을 쓸 수 있는 새 메시지 포맷이 도입되어, 신규 프로듀서가 배치를 보내기 전에 상대 오프셋을 채워 두면 브로커가 재압축을 건너뛸 수 있다. 그래서 모든 프로듀서·컨슈머 클라이언트가 0.10 메시지 포맷을 지원하게 하고 브로커의 메시지 포맷 버전도 0.10으로 바꾸는 것이, 할 수 있는 단일 성능 개선 중 가장 큰 것에 속한다.
핵심 포인트
- 네트워크 핸들러보다 요청 핸들러 풀이 부하에 민감하다
- 유휴 비율 20% 미만은 문제 소지, 10% 미만은 진행 중인 성능 문제
- 요청 핸들러 스레드 수는 CPU 수(하이퍼스레딩 포함)를 넘길 필요가 없다 — 긴 요청은 purgatory로 넘어간다
- 0.10 메시지 포맷의 상대 오프셋으로 브로커의 재압축을 제거하는 것이 큰 성능 개선
트래픽 지표와 rate 지표의 속성 구조
**All topics bytes in**(kafka.server:type=BrokerTopicMetrics,name=BytesInPerSec)은 프로듀싱 클라이언트로부터 브로커가 받는 메시지 트래픽의 양을 초당 바이트로 보여 준다. 클러스터 확장 시점을 판단하는 추세 지표로 좋고, 특정 브로커가 다른 브로커보다 트래픽을 많이 받는지 확인해 파티션 재배치 필요성을 판단하는 데도 쓴다.
여기서 rate 지표의 속성 구조를 알아 둘 필요가 있다. 모든 rate 지표에는 7개의 속성이 있다.
- EventType: 측정 단위. 이 지표에서는 'bytes' - RateUnit: rate 속성의 시간 단위. 여기서는 'SECONDS' - OneMinuteRate: 직전 1분 평균 - FiveMinuteRate: 직전 5분 평균 - FifteenMinuteRate: 직전 15분 평균 - MeanRate: 브로커 시작 이후의 평균 - Count: 브로커 시작 이후 계속 증가하는 절대 누적값
OneMinuteRate는 빠르게 출렁이며 '순간' 관점을 준다. 짧은 트래픽 스파이크를 볼 때 유용하다. MeanRate는 거의 변하지 않아 전체 추세를 보여 주지만 알람을 걸 지표로는 부적절하다. FiveMinuteRate와 FifteenMinuteRate가 둘 사이의 절충이다. Count는 카운터 지표를 지원하는 모니터링 시스템과 함께 쓰면 평균 rate 대신 절대량 관점을 준다.
**All topics bytes out**(name=BytesOutPerSec)은 컨슈머가 메시지를 읽어 가는 속도다. Kafka는 컨슈머를 여럿 붙이기 쉬워서 아웃바운드가 인바운드와 다르게 스케일하며, 아웃바운드가 인바운드의 6배가 되는 배포도 흔하다. 그래서 따로 관찰하고 추세를 봐야 한다.
주의할 점은 아웃바운드 바이트 레이트에 복제 트래픽이 포함된다는 것이다. 모든 토픽의 replication factor가 2라면, 컨슈머 클라이언트가 하나도 없어도 bytes out이 bytes in과 같게 나온다. 컨슈머 하나가 모든 메시지를 읽으면 bytes out은 bytes in의 두 배가 된다. 이 사실을 모르면 지표 해석이 혼란스러워진다.
**All topics messages in**(name=MessagesInPerSec)은 크기와 무관하게 초당 생산된 개별 메시지 수다. bytes in과 함께 쓰면 평균 메시지 크기를 구할 수 있다.
반대로 'messages out' 지표는 존재하지 않는다. 메시지가 소비될 때 브로커는 다음 배치를 그대로 컨슈머에게 보낼 뿐, 배치를 펼쳐서 안에 몇 건이 들어 있는지 세지 않기 때문이다. 그래서 브로커는 몇 건을 내보냈는지 알지 못하고, 제공 가능한 것은 초당 fetch 수라는 요청 rate뿐이다.
핵심 포인트
- rate 지표 속성 7개: EventType, RateUnit, OneMinuteRate, FiveMinuteRate, FifteenMinuteRate, MeanRate, Count
- MeanRate는 브로커 시작 이후 평균이라 알람용으로 부적절
- bytes out에는 팔로워 복제 트래픽이 포함된다 (RF=2, 컨슈머 0이면 bytes out ≈ bytes in)
- messages out 지표는 없다 — 브로커가 배치를 펼치지 않기 때문
- bytes in ÷ messages in 으로 평균 메시지 크기를 얻을 수 있다
Partition count, Leader count, Offline partitions
**Partition count**(kafka.server:type=ReplicaManager,name=PartitionCount)는 그 브로커에 할당된 전체 파티션 수로, 리더든 팔로워든 브로커가 가진 모든 복제본을 포함한다. 값이 자주 변하지는 않지만, 자동 토픽 생성이 켜진 클러스터에서는 토픽 생성이 운영자 통제 밖에서 일어나므로 관찰할 가치가 있다.
**Leader count**(kafka.server:type=ReplicaManager,name=LeaderCount)는 현재 그 브로커가 리더인 파티션 수다. 이 값은 브로커 간에 고르게 나와야 하며, 복제본 수와 크기가 완벽히 균형 잡혀 있어도 리더십이 기울어질 수 있으므로 정기적으로 확인하고 알람을 거는 편이 좋다. ZooKeeper 세션 만료 등 여러 이유로 브로커가 리더십을 잃을 수 있고, 자동 리더 리밸런싱을 켜지 않았다면 회복 후에도 리더십을 되찾아 오지 않기 때문이다. 이 지표가 낮거나 0이면 preferred replica election을 돌려 리더십을 재분배해야 한다는 신호다.
유용한 활용법은 partition count와 함께 써서 '이 브로커가 리더인 파티션의 비율'을 보는 것이다. 균형 잡힌 클러스터에서 replication factor가 2라면 모든 브로커가 자기 파티션의 약 50%에서 리더여야 하고, replication factor가 3이면 이 비율은 약 33%로 떨어진다.
**Offline partitions**(kafka.controller:type=KafkaController,name=OfflinePartitionsCount)는 under-replicated partitions와 함께 가장 중요한 지표다. 이 값은 클러스터의 컨트롤러인 브로커만 제공하고 나머지 브로커는 모두 0을 보고하며, 현재 리더가 없는 파티션 수를 보여 준다. 리더가 없어지는 이유는 두 가지다. 이 파티션의 복제본을 가진 브로커가 모두 다운되었거나, unclean leader election이 꺼진 상태에서 메시지 수 불일치 때문에 어떤 in-sync replica도 리더십을 가져갈 수 없는 경우다.
프로덕션에서 offline partition은 프로듀서 클라이언트에 직접 영향을 주어 메시지를 잃거나 애플리케이션에 백프레셔를 일으킨다. 대개 '사이트 다운' 급 문제이며 즉시 대응해야 한다.
핵심 포인트
- partition count는 리더/팔로워를 모두 포함한 복제본 총수
- RF=2면 리더 비율 약 50%, RF=3이면 약 33%가 균형의 기준
- offline partitions는 컨트롤러 브로커만 보고하고 나머지는 0을 보고한다
- 리더 없는 파티션의 원인: 복제본 브로커 전부 다운, 또는 unclean election 비활성 상태에서 승격 가능한 ISR 부재
- offline partition은 즉시 대응이 필요한 '사이트 다운' 급 문제
요청 타입별 지표 — 8개 지표와 백분위수
Kafka 프로토콜에는 여러 요청 타입이 있고 각각에 대해 지표가 제공된다. ApiVersions, ControlledShutdown, CreateTopics, DeleteTopics, DescribeGroups, Fetch, FetchConsumer, FetchFollower, GroupCoordinator, Heartbeat, JoinGroup, LeaderAndIsr, LeaveGroup, ListGroups, Metadata, OffsetCommit, OffsetFetch, Offsets, Produce, SaslHandshake, StopReplica, SyncGroup, UpdateMetadata 등이다.
각 요청 타입마다 8개의 지표가 제공된다. 요청 처리 단계별 7개의 타이밍 지표와 요청 rate 하나다. 예를 들어 Fetch 요청이라면 kafka.network:type=RequestMetrics,name=TotalTimeMs,request=Fetch 형태다.
- Total time: 요청을 받아서 응답을 돌려보낼 때까지 브로커가 쓴 전체 시간 - Request queue time: 요청이 접수된 뒤 처리 시작 전까지 큐에서 기다린 시간 - Local time: 파티션 리더가 요청을 처리하는 데 쓴 시간(디스크로 보내는 것 포함, 다만 flush까지는 아님) - Remote time: 요청 처리 완료를 위해 팔로워를 기다린 시간 - Throttle time: 클라이언트 쿼터 설정을 지키기 위해 응답을 붙잡아 둔 시간 - Response queue time: 응답이 전송되기 전 큐에서 기다린 시간 - Response send time: 실제로 응답을 보내는 데 쓴 시간 - Requests per second: 해당 요청 타입의 초당 처리 건수(rate 지표)
7개 타이밍 지표는 각각 백분위수 집합과 별도의 Count 속성을 제공한다. 속성은 50thPercentile, 75thPercentile, 95thPercentile, 98thPercentile, 99thPercentile, 999thPercentile과 Count, Min, Max, Mean, StdDev다. 이 값들은 모두 브로커 시작 이후 기준으로 계산되므로, 브로커가 오래 떠 있을수록 숫자가 안정적으로 변한다는 점을 감안해야 한다.
백분위수의 의미는, 99번째 백분위수 값이라면 표본(여기서는 요청 소요 시간)의 99%가 그 값보다 작다는 뜻이다. 즉 1%가 그 값보다 크다. 평균값과 99% 또는 99.9% 값을 함께 보는 것이 흔한 패턴으로, 평균적인 요청 성능과 이상치를 동시에 파악할 수 있다.
무엇을 수집할 것인가? 최소한 모든 요청 타입에 대해 total time의 평균과 높은 백분위수(99% 또는 99.9%) 하나, 그리고 requests per second를 모아야 한다. 여력이 되면 나머지 6개 타이밍 지표까지 모으면 성능 문제를 요청 처리의 특정 단계로 좁힐 수 있다.
타이밍 지표에 임계값을 잡는 일은 까다롭다. 예를 들어 Fetch 요청 시간은 클라이언트가 메시지를 얼마나 기다릴지에 대한 설정, 해당 토픽이 얼마나 바쁜지, 클라이언트와 브로커 간 네트워크 속도에 따라 크게 달라진다. 그래도 최소한 Produce 요청의 total time 99.9번째 백분위수에 대해 기준선을 만들고 알람을 걸어 두면 매우 유용하다. under-replicated partitions와 마찬가지로, Produce 요청의 99.9% 값이 급격히 오르는 것은 폭넓은 성능 문제를 알려 주는 신호다.
핵심 포인트
- 요청 타입별로 8개 지표 = 7개 타이밍 + requests per second
- 타이밍 단계: total / request queue / local / remote / throttle / response queue / response send
- 백분위수 속성은 50/75/95/98/99/999, 그 외 Count·Min·Max·Mean·StdDev
- 모든 지표는 브로커 시작 이후 누적 기준으로 계산된다
- 최소 수집: 요청 타입별 total time 평균 + 99(또는 99.9)% + requests per second
- Produce 요청 total time의 99.9%는 알람 걸기 좋은 대표 지표
토픽·파티션 단위 지표
브로커 전반 지표 외에 토픽별·파티션별 지표도 있다. 큰 클러스터에서는 수가 너무 많아 상시 수집이 어렵지만, 특정 클라이언트 문제를 디버깅할 때 매우 유용하다. 예를 들어 토픽 지표로 클러스터 트래픽을 급증시킨 토픽을 특정할 수 있다.
**토픽별 지표**는 브로커 지표에 topic=TOPICNAME이 붙은 형태로, 내용은 브로커 지표와 동일하다. BytesInPerSec, BytesOutPerSec, FailedFetchRequestsPerSec, FailedProduceRequestsPerSec, MessagesInPerSec, TotalFetchRequestsPerSec, TotalProduceRequestsPerSec 등이다. 개수가 많아 운영자가 알람을 걸 대상은 아니지만, 클라이언트들이 자기 사용량을 평가하고 디버깅하도록 제공해 주면 좋다.
**파티션별 지표**는 토픽 지표보다 상시 유용성이 떨어진다. 토픽 수백 개면 파티션은 수천 개가 되기 때문이다. 그래도 쓸모 있는 것들이 있다.
- Partition size: kafka.log:type=Log,name=Size,topic=TOPICNAME,partition=0 — 그 파티션이 현재 디스크에 보존 중인 데이터 양(바이트). 합치면 토픽별 보존량이 되어 클라이언트별 비용 배분에 쓸 수 있다. 같은 토픽의 두 파티션 크기가 크게 다르면 프로듀싱에 쓰는 키에 대해 메시지가 고르게 분산되지 않는 문제를 뜻한다. - Log segment count: name=NumLogSegments — 파티션의 디스크 상 로그 세그먼트 파일 수. 파티션 크기와 함께 자원 추적에 유용하다. - Log end offset / Log start offset: name=LogEndOffset / LogStartOffset — 파티션 안 메시지의 가장 높은 오프셋과 가장 낮은 오프셋.
다만 log end offset과 log start offset의 차이가 곧 파티션 안의 메시지 수는 아니다. 로그 컴팩션 때문에 같은 키의 더 새로운 메시지가 들어와 제거된, '비어 있는' 오프셋이 생길 수 있기 때문이다. 이 오프셋들을 추적하면 타임스탬프-오프셋 매핑을 더 세밀하게 제공해 컨슈머가 특정 시점으로 쉽게 롤백하게 할 수 있지만, Kafka 0.10.1에서 시간 기반 인덱스 검색이 도입되면서 중요도는 낮아졌다.
마지막으로 파티션 단위의 under-replicated 여부 지표도 존재하지만, 일상 운영에서는 수집·관찰할 지표가 너무 많아져 유용하지 않다. 브로커 전체의 under-replicated partition 카운트를 모니터링하고, 구체적인 파티션은 커맨드라인 도구로 찾는 편이 훨씬 쉽다.
핵심 포인트
- 토픽별 지표는 브로커 지표에 topic=... 이 붙은 형태 — 운영자 알람보다 클라이언트 제공용
- 같은 토픽의 파티션 크기 편차는 키 분포 불균형의 신호
- log end offset - log start offset ≠ 메시지 수 (컴팩션으로 빈 오프셋 발생)
- 파티션 단위 under-replicated 지표보다 브로커 전체 카운트 + CLI 조회가 실전적
이 모듈과 연관된 문항 3개가 문제 은행에 있습니다.
이 내용으로 문제 풀어보기