Backend Developer

목표를 위해 시스템을, 시스템을 위해 회고를

Kafka DefaultErrorHandler 를 이용한 재시도 프로세스 구축


요즘에는 카프카를 이용해서 서비스 간 이벤트 기반으로 시스템을 구현하는 케이스들이 증가하고 있습니다. 서비스가 점점 고도화되고 시스템이 복잡해짐에 따라 서비스간의 느슨한 결합과 독립성이 중요해지고 있으며, 확장성을 고려했을 때도 카프카를 통해 시스템을 구성하는 경우가 많으며 다양한 장점이 있습니다.

하지만, 카프카를 이용해 이벤트를 컨슈밍하여 동기화를 하고 비즈니스 로직을 수행하던 와중에 타임아웃으로 인한 장애가 발생하였고 이번 기회에 장애복구에 대해 다시한번 정리해보았습니다.


장애에는 크게 두가지 종류로 볼 수 있습니다.

  • 시스템 레벨의 장애
    • 네트워크 오류: 네트워크 연결 문제로 인한 장애
    • 서버 오류: 서버나 가상머신의 하드웨어 또는 운영체제 오류로 인한 장애
    • 스토리지 오류: 디스크 고장, 데이터 손상 등으로 인한 장애
    • 리소스 오류: 메모리나 CPU 등의 리소스 고갈로 인한 장애
  • 애플리케이션 레벨의 장애
    • 코드 버그: 개발자가 잘못 구현한 코드로 인한 장애
    • 의존 서비스 문제: 외부 API나 DB, 메시지 브로커 등의 의존 서비스가 중단되거나 오류로 인한 장애
    • 성능 문제: 순간 대규모 트래픽이 몰리거나 무한 루프 등으로 인한 시스템 응답 저하로 인한 장애

서비스를 개발 및 운영하다보면 크게 두가지 종류의 장애를 직면할 수 있습니다. 여기서 장애복구 관점으로 봐라보게 되면 재시도로 장애복구가 가능한 경우별도의 조치를 통해 장애복구가 가능한 경우로 볼 수 있습니다.

별도의 조치를 통해 장애복구가 가능한 경우는 데드레터와 모니터링을 통해 빠른 장애인지와 복구를 유도하는 것이 좋은 방향이지만, 재시도로 장애복구가 가능한 경우는 운영의 피로도를 감소하고자 자동으로 재처리하는 프로세스를 통해서 후처리를 하고도 안될 경우 데드레터와 모니터링 알림을 제공하는 방향이 운영리소스 측면에서 좋다고 생각합니다.

그렇다면, 위에서 이야기한 장애 중에 재시도를 통해 장애복구가 가능한 경우는 아래와 같다고 생각합니다.

  • 네트워크 오류
  • 의존 서비스 문제

이처럼 재시도를 통해 장애복구가 가능하지만, 재시도를 통한 장애복구에서도 고민이 더 필요한 부분들이 있습니다.


재시도를 통해 장애를 복구할 때 고려해야할 포인트들은 무엇일까?

재시도를 통해 장애를 복구할 때 고려해야할 포인트가 많이 있지만, 그 중에서 저는 아래를 중점적으로 생각해보았습니다.

  • 재시도 횟수 및 제한 설정
  • 재시도 간격 설정(백오프 전략)

재시도 횟수 및 제한 설정

재시도를 무한정으로 시도할 수 없으므로, 최대 재시도 횟수에 대한 설정이 필요합니다. 최대 재시도 횟수 초과했을 경우 장애를 다른 방식으로 처리가 필요하며, 빠른 장애인지를 위해 데드레터를 발행하거나 모니터링 알림 구성도 필요합니다.

재시도 간격 설정(백오프 전략)

재시도의 횟수도 중요하지만 얼마큼의 간격으로 재시도하는지도 중요합니다. 재시도를 통해 장애복구가 가능한 경우에서 의존서비스 문제로 인해 발생되는 장애가 있는데, 의존하는 서비스에 재시도 간격없이 재시도를 요청할 경우 의존하는 서비스에 더 많은 부하를 주어 장애복구에 지연을 유발할 수 있습니다.

  • 고정된 백오프(Fixed Backoff)
  • 지수 백오프(Expoenetial Backoff)

보통을 고정된 간격으로 재시도 간격을 설정하거나 재시도 간격을 점진적으로 늘리면서 재시도하여 의존하는 서비스가 회복할 시간을 줄 수 있습니다.

서비스를 운영하다보면, 아무리 테스트를 열심히 해도 장애는 언제든지 발생할 수 있다고 생각합니다. 그렇기 때문에 장애전파와 빠른 장애복구가 중요한데, 재시도 과정에서도 의존하는 서비스의 장애의 빠른 복구에 대해서도 고민해야하는 부분은 중요한 체크포인트라고 생각합니다.
지수 백오프 전략도 빠른 장애복구를 위한 전략 중 하나라고 생각하며, 더 나아가면 서킷 브레이커까지 고민해볼 수 있다고 생각합니다.


DefaultErrorHandler를 활용한 재시도 프로세스 구축방안

재시도 프로세스를 구축하기 위해 고민해야되는 내용에 대해서는 이미 선배 개발자분들이 Spring Kafka 통해 API 를 제공해주고 있습니다. 기존에는 SeekToCurrentErrorHandler 를 제공해주었지만, Spring kafka 2.8 부터 Deprecated 되고 DefaultErrorHandler 를 사용하도록 가이드하고 있습니다.

SeekToCurrentErrorHandler 의 경우 백오프 설정이 FixedBackOff 만 적용이 되어 지수 백오프를 필요할 경우 커스텀 BackOff 를 구현해야하는 단점이 있었습니다.

    ... 
    
    final DefaultErrorHandler errorHandler =
        new DefaultErrorHandler((record, exception) -> {
            // recover after 3 failures, with no back off - e.g. send to a dead-letter topic
        }, new FixedBackOff(0L, 2L));
    
    ...

보통 데드데터를 발행하기 위해 Recoverer 과 함께 BackOff 를 지정해줄수 있는데 자주 사용하는 BackOff는 다음과 같습니다.

  • FixedBackOff
    • 생성자를 통해 고정된 재시도 간격과 최대 시도횟수를 지정해줄 수 있습니다.
    • setter 를 이용해 재시도 간격과 최대 시도회수를 지정해줄 수 있습니다.
  • ExponentialBackOff
    • 생성자를 통해 초기 재시도 간격과 지연 배수를 설정해줄 수 있습니다.
    • setter 를 이용해 초기재시도 간격과 지연배수를 설정해 줄 수 있으며, 최대 시도 횟수 설정, 최대 지연간격, 최대 경과시간을 지정할 수 있습니다.

      Tip)
      ExponentialBackOff 의 경우 버전에 따라 최대 시도횟수를 지정해주는 API 가 없을 수 있습니다. 이때 setMaxElapsedTime 를 이용해 해결할 수 도 있지만, org.springframework.kafka.support 패키지에 제공하는 ExponentialBackOffWithMaxRetries 을 활용하면 최대 시도횟수를 손쉽게 지정이 가능하며 가독성 또한 증가할 수 있습니다.


FixedBackOff 보다 ExpoenetialBackOff 만 사용해도 되는거 아닌가?

ExponentialBackOff 에 대해 알아보다보면 FixedBackOff 에 대해서는 고려하지 않아도 될 것 같지만 상황에 따라 FixedBackOff 를 활용하는 것이 더 적절할 수 있습니다. 지수 백오프 전략을 가져가게 되었을 때, 만약 외부 서비스의 장애로 인해 타임 아웃이 발생했는데 외부서비스 복구가 늦어지면 하나의 이벤트를 처리하는데 처리속도가 매우 늦려지게 됩니다.

예를들어, 아래와 같이 구성을 했고 타임아웃 설정이 1초로 구성되어 있을 경우

...

final ExponentialBackOff backOff = new ExponentialBackOff(1_000L, 2.0);
backOff.setMaxAttempts(3);

final DefaultErrorHandler defaultErrorHandler = new DefaultErrorHandler(recoverer, backOff);

...

컨슈머 로직(1.xx초) -> 백오프 지연(1초) -> 컨슈머 로직(1.xx초) -> 백오프 지연(2초) -> 컨슈머 로직(1.xx초) -> 백오프 지연(4초) -> 컨슈머 로직(1.xx초) -> 데드레터 발행 및 모니터링 알림 제공

위와 같은 프로세스로 진행되며, 총 11.xx 초 정도 소요시간이 걸립니다. 이렇게 될 경우 이벤트 발행이 많았을 때 해당 이벤트의 파티션에 있는 이벤트들이 재시도 처리로 인해 블로킹 되어 렉이 다 차버리는 이슈도 발생할 수 있습니다.

결국 각각의 차이점을 알고 상황에 맞게 활용하시는 것이 가장 좋으며, 컨슈머에서 재시도로 인한 장애복구 프로새스를 구축하면서 운영리소스로부터 자유로움을 얻기를 바랍니다.


마치며

DefaultErrorHandler 를 이용해 재시도 프로세스를 구축하는 기본적인 방안에 대해 글을 작성했습니다. DefaultErrorHandler 를 활용했을 때 별도 설정을 안해주면 치명적인 예외로 재시도가 필요없는 특정 예외를 제외하고는 다 재시도 처리를 진행하게 되는데 이부분에 대한 커스텀 방안과 기본적으로 재시도 제외처리되는 예외에 대해 다루겠습니다.


레퍼런스