이번 글에서는 실제로 Kafka 를 이용할 때 모르고 쓰면 문제를 일으킬 수 있는 부분들에 대해 이해하기 위한 기록이다.
이 글의 주제는 멱등성 Producer에 대해 기록한다.
개념 정리 – (1) 멱등성과 Transaction
우선 멱등성( Idempotence )에 대해서 먼저 알아보고 가자.
멱등성이란 첫 번째 수행을 한 뒤 여러 차례 적용해도 결과를 변경시키지 않는 것을 말한다. 즉, 멱등한 작업의 결과는 1번 수행한 것과 N번 수행한 것의 결과가 서로 동일하다는것을 말한다.
Http 에서는 멱등성을 보장하려면 Header 에 멱등성 키 ( “Idempotency-Key” ) 를 포함해서 요청을 전송하게 함으로써 멱등성을 보장할 수 있다. ( 물론 이를 처리하는 Server 는 멱등함을 보장하기 위한 추가적인 로직이 필요하다. )
Transaction 은 개별적이고 나눌 수 없는 작업으로 구분되는 정보 처리 작업을 말한다. 즉, 나눌 수 없으므로 일부 정보만 처리되고 나머지 정보는 처리되지 않는 것은 올바르지 못한 정보처리 방법이다. 오직 모두 처리되거나, 모두 처리되지 않거나 딱 두가지의 상태로만 존재하게 하는 것이 핵심이다.
또한, 재시도( Retry ) 에 관한 문제도 고민해볼 필요가 있다.
Transaction Processing 과정이 되었든, Rest API 요청에 대한 처리가 되었든 재시도는 굉장히 중요하다. 예를 들어 주변에 시끄러운 이웃이 있어서 일시적인 Network 과부하로 문제가 생겼다고 하면, 이런 문제들은 재시도를 통해 얼마든지 해결할 수 있기 때문이다.
이번 글에서는 재시도 ( Retry ) 를 처리하는 방법과, 멱등성 Producer 에 대해서 살펴본다.
Producer – Message Delivery Semantics
기본적으로 Producer 는 발행한 메시지에 대한 Broker로 부터 오는 응답( ack )에 의존하여 메시지가 제대로 전달되었는지 판단한다. 이 응답을 받지 못했을경우에 재시도 하는 등 Kafka Producer 는 기본적으로 신뢰성을 보장하기 위한 기능들을 이미 가지고 있다.
- 같은 파티션내의 메시지들에 대한 순차 처리 보장.
- Replica 를 이용한 메시지 유실 방지
이 둘은 기본적으로 Kafka 가 보장하는 방법이며, 이 둘만 이용해도 어느정도의 메시지 신뢰성은 보장할 수 있다.
하지만 Broker 에게 Message 를 발행하는 역할을 하는 Producer 는 메시지를 보낼 때 윗절에서 말한 멱등성과 Transaction 에 대해 꼭 고민해보아야 한다.
일반적으로 많이 등장하는 예제를 통해 자세히 이해해보자. 예를 들어 우리의 시스템이 주문을 처리하는 Order Service 와 결제를 담당하는 Payment Service 가 있다고 생각하자. 그리고 이 둘은 Kafka 를 통해 메시지를 주고받는다.
Order Service 가 고객의 주문에 대한 요청을 정상적으로 처리하였고, 결제 요청을 보내기 위해 PaymentEvent 를 발행했다고 가정해보자.
만약, 이 상황에서 여러 이유( 일시적인 Broker 장애, Network 통신 에러.. ) 등으로 요청을 응답받지 못하면 Producer 는 자동적으로 요청을 다시 보내게 된다.
이 때 만약 ack 를 받기 이전에 보낸 Event가 해당 Partition 에 게시조차 되지 못한 상황이라면, Retry 를 이용하여 재시도하면 해결되거나, 혹은 계속해서 반영되지 못하게 될 것이므로 PaymentService의 Consumer 가 PaymentEvent 를 2번 받는 상황이 나타날 수 있다.
그런데, ack 를 받기 이전에 보낸 Event 는 정상적으로 게시되었으나 ack 를 돌려주는 과정에서 문제가 발생한 경우라면? 이러면 Retry 를 통해 PaymentEvent 가 2번 발행될 수 있고, 이중결제가 되는 심각한 결함을 발생시킬 수 있다.
그렇다면 이 문제는 어떻게 해결할 수 있을까?
차악을 택하는 방법으로 이중결제가 될 바에는 아예 결제를 실패하게 만드는 것이다.
Producer가 ack를 받았든, ack를 받지 않았든 메시지를 무조건 딱 한번만 발행하는것이다. 이렇게 되면 결제가 이중으로 되는 일은 절대로 일어나지 않는다, ( 그 외에, Kafka를 없에버리고 PaymentService의 기능을 OrderService 에 흡수시켜 하나의 Application 으로 만들어 단일 Transaction 으로 처리하는 방법도 분명 가능하나, 이건 다른 문제들을 많이 일으킬 수 있다. 그리고 이 글은 Kafka 에 대한 글이므로 이 방법은 생각하지 않는다. )
그러나 이 경우는 ack 를 받지 못한 PaymentEvent 가 Partition 에 게시조차 되지 못했다면 결제가 이루어지지 않는 경우가 발생하게 된다.
이렇게 만들고 OrderService 에서 PaymentEvent History 를 관찰하다가 일정 시간이 지났는데도 응답을 받지 못한 Order 를 Failed 처리 해버리는 방법이다.
위의 2가지 예제는 Kafka 에서 Message 전달 수준에서 2가지를 나타낸 것이다.
위의 것은 최소 한번( At Least Once )을 보장하는 방법으로 Retry 를 이용한 중복메시지가 생길수 있는 리스크와 ack 확인 검증 + Retry 를 통한 퍼포먼스 희생을 감수하더라도 높은 신뢰성이 요구된다면 반영한다.
아래 예제는 최대 한번( At Most Once )을 보장하는 방법으로 Producer 는 응답과 관련없이 최대 한번만 발행하는 방식이다. 이때 Producer 는 요청에 대한 ack 를 전혀 고려하지 않는다.( Send and Forgot ) 이 방법은 메시지의 신뢰성은 딱히 중요하지 않을 때, 쉽게 높은 처리량과 짧은 응답 시간을 챙길 수 있는 방법이다.
한가지가 더 있는데, 바로 어떤 상황이든 정확하게 한번만 전달( Exactly Once )하는 방법이다.
즉 메시지가 여러번 전송되더라도 멱등하게 처리할 수 있도록 하는 것이다.
Kafka 는 멱등한 Producer를 제공한다. 윗절의 At Least Once 와 같은 경우가 생겼을 때 Retry 를 진행하더라도 모든 메시지는 로그에 정확히 한 번만 기록되는 것을 말한다. 그렇다면 어떻게 Exactly Once 하게 보장할 수 있을까?
각 Producer 는 고유의 Producer ID 를 부여받고, 프로듀서는 메시지를 브로커에게 보낼 때마다 이 PID를 포함한다.
각 메시지는 순차적으로 증가하는 Sequence 번호를 부여받게 된다. 이 번호는 각 토픽 파티션마다 각각의 유지되고, Broker는 파티션별로 이 PID와 Sequence의 쌍을 계속해서 추적한다.
그래서, Retry로 인한 동일한 Sequence 를 가진 메시지가 또 전달되더라도 이 Sequence가 정확히 1차이가 아니라면 반영하지 않는 식으로 멱등함을 보장하는 것이다.
( PID – Sequence 를 쌍으로 관리하는것에 주의해야 한다. 만약, Producer 가 새롭게 재시작되거나 하면 새로운 PID 가 부여될것이고, 이전의 세션과는 연속성이 사라진다. 따라서 이 멱등성 Producer는 같은 PID로 유지된 단일 세션 내에서만 멱등성이 보장된다.
따라서 Exactly Once 방법을 이용하면 메시지 전송에 높은 신뢰성을 달성할 수 있지만, 이를 제대로 사용하려면 몇가지 설정들을 함께 알아야 한다.
- 적절한 복제 : 1편에서 설명한 replication factor 를 적절한 수준으로 설정해야 하고, 최소 Sync replica( min.insync.repliacs )의 개수도 적절한 수준으로 조절해야 한다.
- acks = ( 0, 1, all ) : 메시지가 브로커에 복제될때 까지 기다리는 수준을 설정한다. 0은 ACK 를 기다리지 않는 것이고, 1은 Leader Partition 에만, all 은 min.insync.replicas와 Leader 까지 전부를 말한다.
- deliver.timeout.ms : 재시도를 할 때 정해진 시간이다. 이 시간을 넘어가면 Timeout이 된다.
- enable.idempotency = ( true/false ) : Producer 를 Exactly Once 를 보장하는 멱등성 프로듀서로 설정한다.
- max.in.flight.requests.per.connection : 한번에 메시지를 보내는 양을 말한다. 이 값이 클수록 여러 메시지를 동시에 보내기 때문에 전송 속도가 빨라질 수 있지만, 메시지의 순서와 중복 문제가 발생할 수 있다.
만약 값이 5 이하로 설정되어 있다면, Kafka는 하나의 요청이 실패할 경우 이후의 메시지를 기다리게 된다. 이렇게 하면 재시도할 때도 이전 메시지보다 순서가 뒤바뀌지 않도록 관리할 수 있다.
반대로, 이 값이 5보다 크면 메시지 하나가 실패해도 다른 메시지들은 서버로 전송될 수 있고, 실패한 메시지를 재전송할 때 순서가 어긋날 위험이 존재한다.
예를 들어 메시지 1이 실패하고 메시지 2, 3이 성공했다면, 재전송 시 메시지 순서가 [2, 3, 1]로 뒤섞일 수도 있다는 얘기다.
정리
Producer 는 다음과 같은 전략을 이용해서 메시지의 유실을 방지하고 신뢰성을 높일 수 있다.
기본적으로 Exactly Once 를 사용하면 너무 좋겠지만, 무조건 Exactly Once 가 좋은것은 아니고, At most once 방법이 나쁜것도 아니다.
Kafka Producer 를 사용함에 있어서 어떤 전략을 통해서 메시지를 어떻게 전달할 것인지 꼭 잘 이해하고 사용하자.
만약 신뢰성이 정말 중요하다면, ( 여기서와 같이 결제와 같은 높은 신뢰성이 요구되는 환경이라면 ) 여기서 더 생각해 보아야 하는 것이 남아있는데, 메시지의 Transaction과 관련된 이야기이다.
다음 글에서는 메시지의 Transaction 에 대해서도 한번 알아보자.