IT/DevOps

Logstash의 Kafka Input 성능 개선 이야기

Aaron's papa 2021. 10. 28. 23:01
반응형

오늘은 Logstash의 Kafka Input 성능 개선과 관련된 이야기를 해보겠습니다. 어떤 문제가 있었고 어떻게 해결했는지 그 과정에 대해 살펴보겠습니다. 참고로 이 글에 있는 작업은 박병진 님의 작품 입니다. 저는 그저 사람들에게 알리기 위해 대신해서 글을 쓸 뿐입니다. ^^


문제의 발단

일부 서비스를 오픈하고 트래픽을 처리하고 있던 중 아래와 같은 모습의 Kafka Lag 지표를 발견했습니다. 피크 시에는 3천을 육박할 정도로 높은 수준의 Lag이 감지되고 있었습니다.

Kafka Consumer Lag 그래프

초당 7백여개의 로그가 인입되고 있는 상황이었기 때문에 3천대의 Lag이라면 로그가 ES에 실리기까지 약 4~5초 정도 밀린다는 의미가 됩니다. 물론 4~5초 정도의 로그 적재 지연이 크리티컬 한 상황은 아니었지만, 좀 더 빠르고 정확한 모니터링 시스템을 구현하기 위해서는 Lag을 최소화해야 할 필요가 있었습니다. 그래서 Lag을 1천 미만으로 떨어뜨리기 위한 성능 개선 작업이 시작되었습니다.


첫 번째 시도 - 파티션 수 증가

Kafka Lag이 발생하면 가장 먼저 시도해 볼 수 있는 작업이 파티션 수를 증가시키는 것입니다. 그래서 가장 많은 Lag이 발생하고 있는 토픽의 파티션 수를 증가시켜 보기로 했습니다. 다른 토픽들은 파티션의 수가 10이었지만 가장 많은 Lag이 발생하고 있는 토픽의 파티션 수를 30까지 늘려 보기로 했습니다.

파티션 수 변경 후의 Lag

하지만 해당 토픽의 파티션을 증가시켜도 상황은 나아지지 않았습니다. 파티션의 수를 증가 시켜도 나아지지 않은 게 이상해서 자연스럽게 이런 의문이 들기 시작했습니다. Logstash 컨슈머들이 토픽들의 파티션 별로 잘 분배되어 있는 걸까? 그래서 이걸 먼저 확인해 보기로 했습니다.


Logstash 컨슈머의 분배 확인

Logstash가 읽어야 할 토픽들의 수는 총 10개, 토픽이 각각 가지고 있는 파티션 수는 위에서 30개로 수정한 2개의 토픽을 제외하고는 각각 10개씩이었습니다. 그래서 읽어야 할 모든 파티션의 수는 8 * 10 + 2 * 30 = 140개입니다. 그리고 아래는 Logstash Kafka Input의 설정 파일 중 일부입니다.

input {
  kafka {
    consumer_threads => 30
    decorate_events => true
    max_poll_records => "5000"
    topics_pattern => "log\.[^.]*\.k8s-pod\..*"
  }
}

Logstash 애플리케이션을 총 5개 운영 중이었으니 총 컨슈머는 150개입니다. 컨슈머의 개수가 전체 파티션의 수보다 조금 많긴 하지만 많이 차이가 나지 않았기 때문에 의도했던 대로 Consumer가 고르게 분배되었는지가 궁금했습니다. 그리고 이것을 확인하기 위해 kaf라는 CLI를 설치했습니다. 

kaf는 Go 언어로 만들어진 Kafka CLI입니다. (https://github.com/birdayz/kaf)

그리고 kaf 명령으로 컨슈머들의 상태를 확인해 본 결과 생각지도 못한 결과를 볼 수 있었습니다.

  logstash-v7.13-1:
    Host:  /10.255.146.50
    Assignments:
      Topic                                                      Partitions
      -----                                                      ----------
      log.apne2-karrotpay-prod.k8s-pod.observability-v1          [8]
      log.apne2-ops-prod.k8s-pod.argo-workflow-v1                [8]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-user-v1         [8]
      log.apne2-karrotpay-prod.k8s-pod.filebeat-v1               [8]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-banking-v1      [8]
      log.apne2-ops-prod.k8s-pod.filebeat-v1                     [8]
      log.apne2-karrotpay-prod.k8s-pod.kube-system-v1            [8]
      log.apne2-karrotpay-prod.k8s-pod.cert-manager-v1           [8]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-money-v1        [8]
      log.apne2-ops-prod.k8s-pod.observability-v1                [8]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-api-gateway-v1  [8]
      log.apne2-ops-prod.k8s-pod.kube-system-v1                  [8]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-partner-v1      [8]
      log.apne2-ops-prod.k8s-pod.argo-cd-v1                      [8]
      log.apne2-karrotpay-prod.k8s-pod.argo-cd-v1                [8]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-payment-v1      [8]
  logstash-v7.13-0:
    Host:  /10.255.129.182
    Assignments:
      Topic                                                      Partitions
      -----                                                      ----------
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-partner-v1      [2]
      log.apne2-ops-prod.k8s-pod.kube-system-v1                  [2]
      log.apne2-karrotpay-prod.k8s-pod.filebeat-v1               [2]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-payment-v1      [2]
      log.apne2-ops-prod.k8s-pod.observability-v1                [2]
      log.apne2-ops-prod.k8s-pod.filebeat-v1                     [2]
      log.apne2-ops-prod.k8s-pod.argo-cd-v1                      [2]
      log.apne2-karrotpay-prod.k8s-pod.argo-cd-v1                [2]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-user-v1         [2]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-api-gateway-v1  [2]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-money-v1        [2]
      log.apne2-karrotpay-prod.k8s-pod.kube-system-v1            [2]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-banking-v1      [2]
      log.apne2-karrotpay-prod.k8s-pod.cert-manager-v1           [2]
      log.apne2-karrotpay-prod.k8s-pod.observability-v1          [2]
      log.apne2-ops-prod.k8s-pod.argo-workflow-v1                [2]
  logstash-v7.13-3:
    Host:  /10.255.146.50
    Assignments:
      Topic                                                      Partitions
      -----                                                      ----------
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-user-v1         [18]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-api-gateway-v1  [18]
  logstash-v7.13-2:
    Host:  /10.255.134.52
    Assignments:
      Topic                                                      Partitions
      -----                                                      ----------
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-api-gateway-v1  [12]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-user-v1         [12]

컨슈머들이 파티션에 붙어 있는 상태가 이상 했습니다. 고르게 분배되어 있지 않았고 파티션 번호를 하나 정해서 모든 토픽의 해당 파티션 번호를 컨슈밍 하고 있었습니다. 예를 들어 logstash-v7.13-0 컨슈머를 보면 모든 토픽의 2번 파티션을 컨슈밍 하고 있고, logstash-v7.13-3 컨슈머를 보면 모든 토픽의 18번 파티션을 컨슈밍 하고 있습니다. 그럼 왜 이런 현상이 발생했을까요?


topics_pattern 설정

앞에서 살펴본 파티션 번호를 기준으로 한 분배의 원인은 바로 topics_pattern 설정 때문이었습니다. topics_pattern에 의해 패턴에 맞는 토픽을 컨슈밍 하도록 되어 있는데 이 설정은 해당하는 토픽들을 마치 하나의 토픽처럼 파티션 번호를 기준으로 나누기 때문입니다. 만약 topics_pattern으로 사용하지 않고 아래와 같이 컨슈밍 해야 하는 토픽들을 topic 설정으로 개별 설정했다면 위와 같은 분배 문제는 발생하지 않았을 것입니다.

input {
  kafka {
    consumer_threads => 30
    decorate_events => true
    max_poll_records => "5000"
    topic => "log.apne2-karrotpay-prod.k8s-pod.karrotpay-api-gateway-v1"
  }
}

input {
  kafka {
    consumer_threads => 30
    decorate_events => true
    max_poll_records => "5000"
    topic => "log.apne2-karrotpay-prod.k8s-pod.karrotpay-user-v1"
  }
}

위와 같이 설정했다면 총 60개의 컨슈머가 만들어졌을 거고 우리가 의도했던 대로 각각에 30개씩 파티션을 고르게 나눠 가졌을 것이기 때문입니다. 하지만 토픽의 수가 많고 언제 토픽이 추가될지 모르기 때문에 토픽 별로 설정하는 것은 운영 효율성을 떨어지게 만듭니다. 토픽이 추가될 때 마다 Logstash 설정을 변경해야 하기 때문 입니다. 결국 운영 효율성을 위해 topics_pattern을 계속해서 사용할 수밖에 없었습니다. 이 문제를 어떻게 해결해야 할지 계속해서 찾던 중 Logstash의 Kafka Input 설정 중 partition_assignment_strategy 항목을 발견했습니다.


partition_assignment_strategy 설정

Logstash의 공식 홈페이지에 가면 partition_assignment_strategy 설정을 찾을 수 있습니다.

이 항목은 기본값이 없습니다. 아마도 파티션 번호를 기준으로 나누는 게 기본값일 거라 추측합니다.

partition_assignment_strategy 설명 (https://www.elastic.co/guide/en/logstash/current/plugins-inputs-kafka.html#plugins-inputs-kafka-partition_assignment_strategy)

그래서 모든 토픽의 모든 파티션을 고르게 분배하도록 이 값을 round_robin으로 변경해 보기로 했습니다. Logstash 설정은 아래와 같이 변경합니다. 거기에 추가로 consumer_threads 값을 기존 30에서 8로 줄였습니다. 컨슈머가 너무 많은 것이 문제를 일으킬 수도 있어서 개별 컨슈머의 수를 줄이고 Logstash 자체를 늘렸습니다.

input {
  kafka {
    consumer_threads => 8
    decorate_events => true
    max_poll_records => "5000"
    topics_pattern => "log\.[^.]*\.k8s-pod\..*"
    partition_assignment_strategy => "round_robin"
  }
}

그리고 Logstash를 재시작하자 아래와 같이 의도했던 대로 컨슈머들이 분배되기 시작했습니다.

logstash-v7.13-1:
    Host:  /10.255.145.135
    Assignments:
      Topic                                                  Partitions
      -----                                                  ----------
      log.apne2-karrotpay-prod.k8s-pod.argo-cd-v1            [8]
      log.apne2-ops-prod.k8s-pod.filebeat-v1                 [2]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-banking-v1  [6]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-user-v1     [14]
  logstash-v7.13-0:
    Host:  /10.255.148.230
    Assignments:
      Topic                                                  Partitions
      -----                                                  ----------
      log.apne2-karrotpay-prod.k8s-pod.argo-cd-v1            [3]
      log.apne2-ops-prod.k8s-pod.argo-workflow-v1            [7]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-banking-v1  [1]
      log.apne2-karrotpay-prod.k8s-pod.karrotpay-user-v1     [9]

결과적으로 Lag도 훨씬 안정적으로 줄어들기 시작했습니다. 이것으로 문제가 해결 될 것이라 생각하고 퇴근을 했으나.. 트래픽이 올라가기 시작하는 저녁 무렵이 되자 문제가 다시 발생하기 시작 했습니다.

 

저녁 시간이 되면서 보인 Lag 그래프

18시 30분경에 Lag이 확 줄어든 게 보이시나요? 저 시간이 바로 partition_assignment_strategy 설정을 round_robin으로 변경한 시간입니다. 저렇게 확 줄어드는 것을 보고 퇴근했으나 19시를 기점으로 해서 Lag이 다시 증가하는 것을 볼 수 있습니다. 하지만 기존처럼 들쑥날쑥 하지 않고 그래프가 제법 평탄화가 되었다는 게 차이점이긴 했습니다.

결국 트래픽의 증가에 따라 (저녁 시간이라 트래픽이 증가하는 시간입니다.) Lag이 다시 증가하고 있기 때문에 이것을 해결해야 했습니다. 그리고 어떻게 해결할 수 있을까에 대해 고민하다가 문득 과연 Lag이란 무엇인가 라는 근본적인 질문에 다다르게 됩니다.


Lag 이란 무엇인가?

카프카에서 이야기하는 Lag이란 무엇일까요? 단순히 메시지가 밀리는 것을 확인하는 지표 정도로 알고 있었지만 좀 더 근본적인 물음과 그에 대한 답이 필요했습니다. 그리고 찾아보니 Lag에 대해 아래와 같이 표현하고 있었습니다.

Lag에 대한 설명 (https://www.confluent.io/blog/kafka-lag-monitoring-and-metrics-at-appsflyer/)

여기서 주목한 것은 consumer's committed offset입니다. 그냥 consumer's offset 이 아니고 consumer's committed offset. 컨슈머가 가져갔다고 표시한 오프셋이라는 의미입니다. 그리고 거기서 힌트를 얻었습니다.


auto_commit_interval_ms 설정

consumer's committed offset이라는 단어에서 힌트를 얻어 찾아보니 auto_commit_interval_ms 설정이 있었습니다. 이 설정의 기본값은 5000 ms, 즉 5초입니다. Logstash는 카프카로부터 메시지를 꺼내간 후 내가 어디까지 꺼내갔는지를 커밋하는데 그 주기가 5초였던 것입니다. 그래서 이 값을 1초로 줄여 보기로 했습니다. 그때 적용한 Logstash 설정은 아래와 같습니다.

input {
  kafka {
    consumer_threads => 8
    decorate_events => true
    max_poll_records => "5000"
    topics_pattern => "log\.[^.]*\.k8s-pod\..*"
    partition_assignment_strategy => "round_robin"
    auto_commit_interval_ms => 1000
  }
}

그리고 결과는 아래와 같았습니다.

auto_commit_interval_ms 적용 후 Lag

의도했던 대로 Lag이 뚝 떨어진 것입니다. 그리고 저희는 편안한 밤을 보낼 수 있었습니다.


마치며

이렇게 하루 반나절을 들였던 성능 개선은 마무리되었습니다. Lag을 줄이기 위해 했던 시도 중 의미 있는 결과를 보였던 것은 partition_assignment_strategy와 auto_commit_interval_ms 두 설정이었습니다. partition_assignment_strategy를 통해 컨슈머들을 각 토픽의 파티션들에 고르게 분배하고 auto_commit_interval_ms을 통해 커밋을 더 자주 빨리 할 수 있도록 했습니다. 이 글이 저희와 비슷한 이슈를 겪고 계신 분들에게 도움이 되었으면 좋겠습니다.

 

ps. 아마 이 글에 있는 내용들 중 잘못된 내용이 있을 수도 있습니다. 그에 대해선 언제든지 댓글 부탁드립니다. 

 

ps2. 당근페이에서 서버 개발자분들을 모십니다. https://team.daangn.com/jobs/4511184003/

 

당근페이 서버 개발자 (Java, Kotlin) | 당근마켓 채용

당근마켓과 함께 할 멋진 동료를 찾고 있어요!

team.daangn.com

 

반응형