Spring Kafka Consumer 로 컨슈머 애플리케이션을 개발했다. 성능테스트를 하면서 브로커와 컨슈머 간에 무슨 일이 일어나는지 좀 더 자세히 알고 싶어서 모니터링 지표를 수집했는데, 수집하고보니 각 지표가 정확히 어떤걸 의미하는지 도무지 알 수가 없었다. 그래서 모니터링 지표 몇 가지를 컨슈머 설정값과 함께 자세히 알아보았다.

이 글에서는 그라파나 카프카 컨슈머 모니터링 템플릿에 있는 그래프가 어떤 지표를 통해 만들어지고 카프카 컨슈머 설정과는 어떤 관련이 있는지 살펴본다. 살펴볼 그래프 제목은 다음과 같다:
이걸 시간의 흐름에 따라 그림으로 나타낸 것이 위의 다이어그램이다. 카프카 브로커와 컨슈머, 그리고 리스너 컨테이너가 각각 요청/응답을 주고 받으며 어떤 일이 일어나는지 표현한 것이다. 참고로 records lead 와 poll idle time 은 특성상 그림에서 빠져있다.
이번 글에서 살펴볼 지표는 크게 두 종류로 나뉜다. 바로 fetch 에 관한 것과 poll 에 관한 것이다. 우선 fetch 와 poll 이 각각 무엇을 의미하는지 짚고 넘어가자. 아래 그림은 Spring Kafka Consumer 를 사용했을 때 브로커로부터 레코드를 받아와 처리하는 과정을 간략하게 그린 것이다.

Spring Kafka Consumer 는 크게 2개 레이어로 나뉜다. 브로커와 통신을 해서 레코드를 받아오는 Kafka Core Consumer 레이어와 그 레코드 하나를 처리하는 비즈니스 로직을 담고 있는 Spring Listener Container 레이어다. 브로커에게 새 레코드를 요청하는 것을 fetch, 비즈니스 로직 처리를 위해 로컬에 있는 레코드를 요청하는 것을 poll이라고 한다. 따라서 레코드 하나를 처리하는 동안 아래 과정을 거치게 된다.
fetch() 요청을 보내 레코드를 받아온다.poll() 이다.poll()을 통해 로컬 메세지 큐에 저장했던 레코드를 가져와 처리한다.Fetch 와 poll 은 순차적으로 일어나지 않고 각각 독립적으로 이루어진다. 즉, 카프카 브로커로부터 메세지를 받아와 로컬 큐에 저장하는 속도와 레코드 하나하나를 처리하는 속도가 완벽히 일치하지 않아도 된다. 하지만 어느정도 영향은 미칠 것이다. fetch() 는 로컬 메세지 큐가 일정 수준 이상 비워졌을 때 호출되기 때문이다. poll() 이 충분히 빠르게 호출된다면 fetch() 역시 마찬가지일 것이고, 레코드 하나를 처리하는 데 오랜 시간이 걸려서 poll() 이 천천히 호출되어 로컬 큐 소진 속도가 느리다면 fetch() 도 그만큼 드물게 호출될 것이다. 반대로 fetch() 에 너무 오랜 시간이 걸린다면 poll()이 호출되는 간격도 길어질 수 있다.
이제 fetch, poll 과 관련된 지표를 하나씩 살펴보자. 앞서 소개했던 대시보드는 Fetch metrics, Poll metrics 가 따로 나뉘어져있다. 이 글에서는 각 그래프별 제목을 소제목으로 작성했다. 그래프가 어떤 지표로 수집되었는지는 그라파나 그래프에서 Edit 에 들어가면 확인할 수 있으며, 소제목 바로 아래에 그 지표를 적어두었다.

Fetch 는 컨슈머가 브로커에게 새 레코드를 받아오기 위해 보내는 요청이다.

카프카 컨슈머가 브로커로부터 데이터를 요청했을 때, 요청에 대한 응답을 받기까지 걸린 시간을 의미한다. 컨슈머와 브로커 간 rount-trip latency 라고 보면 된다. 그림에서는 파란색으로 표시한 시간이다.
Fetch latency 를 분석할 때는 단순 지연 시간이라고만 생각하지 말고 좀 더 따져봐야 할 것이 있다. 앞서 언급했듯이 브로커가 컨슈머에게 레코드를 전달할 때는 여러 레코드를 묶어서 한 번에 보내기 때문이다.
이 때 컨슈머 설정값에 따라 fetch latency가 달라질 수 있다. 여기에는 fetch.min.bytes, fetch.max.wait.ms 가 영향을 미친다. 브로커는 컨슈머가 fetch 요청을 보냈다고 해서 바로 응답하지 않고 둘 중 한가지 조건을 만족했을 때 응답을 보내기 때문이다.
fetch.min.bytes 로 설정한 양만큼의 레코드를 모았거나,fetch.max.wait.ms 로 설정한 시간만큼 기다렸을 때따라서 위 값을 크게 설정할수록 기본 fetch latency 가 늘어날 수 있다. 또한 producer 의 속도가 빠를수록 fetch.min.bytes 조건을 빠르게 만족할테니 fetch latency 가 줄어들 것이다.
브로커가 컨슈머의 fetch 요청에 대한 응답을 의도적으로 지연시켰을 때, 그 지연 시간을 의미한다. 브로커가 의도적으로 응답을 지연시키는 경우는 컨슈머가 과도한 요청을 보냈거나, 대역폭 제한이 걸려있을 때 등이 있다.
컨슈머의 fetch 요청 발생 빈도를 의미한다. 즉, 컨슈머가 브로커에게 데이터를 요청하는 속도를 알 수 있는 지표다. 컨슈머는 자신의 로컬 메세지 큐에 쌓인 레코드가 일정 수준 이하로 떨어졌을 때 브로커로부터 새 레코드를 요청할 것이기 때문에 이 로컬 메세지 큐가 비워지는 속도를 알 수 있는 지표이기도 하다.
컨슈머가 브로커에게 보낸 fetch 요청 1회당 받아온 레코드의 평균 개수를 의미한다. 첫 번째 지표였던 fetch latency 값과 반대 방향으로 움직일 수 있다. 예를 들어 producer 가 빠르게 레코드를 밀어넣는 상황이라면, fetch.min.bytes 로 설정한 양만큼의 레코드를 빠르게 모을 수 있기 때문에 fetch latency 는 낮아질 것이다. 반면 그만큼 한 번에 받아오는 레코드 수가 많아지기 때문에 records per requeset 는 높아질 것이다. 반대로 producer 가 아주 가끔 - 100초에 1개 정도 - 레코드를 밀어넣는 상황이라면 fetch.max.wait.ms 로 설정한 시간이 지날만큼 기다려도 0~1개 레코드 밖에 도착하지 않기 때문에 fetch latency 값은 높아진다. 이 때 fetch 요청 1회 당 받아온 레코드 수는 0~1개이기 때문에 records per request 는 낮아진다.
이 지표는 그래프 상으로 나타내지 못한 지표인데, 카프카 컨슈머의 로컬 메세지 큐와 관계 있는 지표이기 때문이다. 현재 fetch 된 레코드 중에서 아직 poll 되지 않은 레코드의 수를 의미한다. 그러니까 카프카 컨슈머의 로컬 메세지 큐에 남아있는 레코드 수라고 보면 된다.
만약 비즈니스 로직 특성상 레코드 하나를 처리하는데 오랜 시간이 걸린다면 이 값이 증가한 채로 유지될 것이고, fetch 하는 속도에 맞춰 poll 도 빠르게 일어난다면 값은 딱히 증가하지 않을 것이다.
이제 거의 다 왔다! Poll 과 관련된 지표 2개를 살펴보자.
Poll 은 컨슈머 애플리케이션이 컨슈머 로컬 메세지 큐에 있는 레코드를 받아오기 위해 보내는 요청이다.

컨슈머 애플리케이션이 poll 을 호출한 평균 시간 간격을 의미한다. 이 때, 컨슈머 애플리케이션도 poll 요청 1회당 여러개의 레코드를 한번에 받게 된다. 이 값은 컨슈머 설정값 중 max.poll.records 를 최댓값으로 한다. 컨슈머 애플리케이션의 poll 요청은 주기적으로 이루어지기 때문에 특별히 poll 할게 없더라도 계속 호출된다. 컨슈머 설정값 중 max.poll.interval.ms 로 설정한 시간 안에는 반드시 poll 을 호출하게 되어있다.
만약 time between poll 의 값이 증가했다면 컨슈머 애플리케이션이 레코드를 처리하는 데 걸리는 시간이 늘어난 것으로 볼 수 있다. 하지만 항상 이렇게만 해석할 수 있는건 아니다. 아래에 time between poll 값이 커질 수 있는 예시를 가져와봤다.
max.poll.records 값이 너무 큼: 한 번에 너무 많은 레코드를 처리하다보니 poll 사이 간격이 늘어날 수 있다.컨슈머 애플리케이션이 poll loop 를 수행하는 동안 아무 일도 하지 않고 idle 상태로 있었던 시간의 비율이다. 컨슈머가 얼마나 바쁘게 일하고 있는지를 의미하며, 값이 높으면 처리할 레코드가 거의 없거나 fetch 지연이 있다는 뜻이고 값이 낮으면 컨슈머가 바쁘게 처리하고 있다는 뜻이다.
Spring Kafka Consumer 내부의 2가지 레이어와 각 레이어에서 보내는 fetch/poll 요청, 그리고 이와 관련된 지표를 살펴봤다. Fetch latency 와 time between poll 을 보면 알 수 있듯이 각 지표 값이 달라지는 데에는 여러가지 원인이 있기 때문에 하나의 지표만 확인하기보다는 여러가지 지표를 종합적으로 분석하는게 필요하다. 보통 컨슈머 설정값은 기본값으로 두는 것이 권장사항이지만 설정값을 몇가지 조절했다면 그 값도 함께 확인하는게 좋다. 브로커와 컨슈머, 컨슈머 애플리케이션 간 어떤 일이 일어났는지 보다 정확하게 확인하고 싶다면 이러한 지표를 수집하고 분석해보자.