메뉴

문서정보

목차

kafka

IoT 관련일을 하고 있다. 클라이언트로 부터 메시지를 수신하는 메시지 게이트웨이, 수집한 메시지를 백앤드 서버로 안전하게 보내기 위한 메시지 큐, 메시지큐에서 데이터를 처리하고 저장하는 부분에 관심을 가지고 있다.

IoT 메시지 인프라는 비교적 작은 크기의 메시지를 대량으로처리할 수 있어야 한다. 사람과 사람과의 메시지 특성과는 차이가 있다. 이 분야는 대략 아래와 같이 정형화된 구성이 있다.

 전형적인 구성

대략 위 구성에서 서비스 목적에 따라서 구성요소들이 추가되거나 빠지는 등의 변화가 있을 것이다. 어떤 식의 변화가 있더라도 kafkaspark는 거의 기본으로 들어가는게 지금(2017년 6월)의 추세다.

하여 kafka를 좀 더 자세히 살펴보기로 했다. 그 중 Exactly-once에 대해서 고찰해 보려한다.

분산환경에서의 Exactly-once은 매우 어렵다.

메시지 시스템은 몇 가지 레벨의 메시지 보증전략을 가지고 있다.

at-most-once. 실패, 타임아웃등이 발생하면 메시지를 버릴 수도 있다. 쓸모 없는 시스템 같지만, 짧은 간격으로 주기적으로 발생하는 센서 데이터의 경우, 중간에 이빨이 빠지더라도 대세에는 영향이 없고 대량처리가 용이하므로 선택 할 수 있는 방법이다.

exactly-once. 정확하게 한번의 메시지 전송을 보장한다. 중복과 유실 모두 허용하지 않는 방식인데, 당연히 구현하기 힘들다. 분산 시스템은 여러 노드와 여러 계층으로 구분이 된다. 노드가 아예 실패하거나 타임아웃의 발생 혹은 지연이 발생 할 수 있다. 아예 메시지가 실패 해버리면 오히려 처리하기가 쉽겠지만, 타임아웃이나 메시지 지연이 발생 할 경우, 메시지는 나중에 도착 할 수 도 있으며 중복 될 수 있다. 다양한 모든 경우에 대해서 exactly once는 어려울 수 밖에 없다.

at-least-once. 메시지는 최소 1회 전달되야 한다. 전송한 메시지는 반드시 상대편에게 전달이 되야 한다. 메시지 시스템은 실패 할 수 있기 때문에, 데이터는 중복해서 전달될 수 있다. 메시지를 전송이 응답지연등으로 실패했다고 판단하면 다시 메시지를 전송 하는데, 실패했다고 판단했던 메시지가 (사실은 실패가 아니라서) 나중에 전송될 수 있기 때문이다. 마찬가지로 메시지의 순서도 보장 할 수 없다.

일반적으로 at-least-once 방식 정도가 적당한 타협점이다. 중복되는 메시지는 메시지 ID나 시간 등으로 어느 정도 보정이 가능하기 때문이다.

어쨋든 이론적으로는 exactly-once가 가장 좋은 메시지 처리 방식임에는 분명하다. 이런 시스템을 만들 수 있을까. ? 내 생각에 exactly-once한 메시지 처리는 불가능하다. 불가능 하다라는 의미는 기술적으로 할 수 없다는 의미가 아니다. 실용화 하기에 너무 많은 비용이 든다는 점이다. 분산 시스템에서 가장 어려운 문제는
  1. Exactly-once delivery
  2. 메시지의 순서 보장
  3. Exactly-once delivery
이다.

messaging semantics

분산 시스템을 구성하는 노드(컴퓨터)들의 실패는 일상적으로 발생한다. 카프카 시스템의 경우 브로커가 실패 할 수 있으며, 프로듀서와 컨슈머가 메시지를 주고 받는 동안에 네트워크 장애가 발생 할 수 있다. 각 실패가 메시지 전송 방식에 어떤 의미를 가질 수 있는지 살펴보자.

At least once semantics: 프로듀서가 메시지를 전송하고 카프카 브로커로 부터 acks=all 응답을 받으면, 카프카 토픽에 메시지를 썼다는 것을 의미한다. 만약 시간이 초과되거나 오류가 발생하면, 프로듀서는 메시지를 쓰지 못했다고 가정하여 메시지를 다시 전송하려 할 것이다. 하지만 실제로는 (특히 시간초과시에) 메시지를 썼을 수도 있는데, 이 경우 메시지가 중복전달 될 것이다. 중복전달된 메시지는 잘못된 결과를 초래 할 수도 있다.

At most once semantics: 프로듀서가 시간초과 혹은 오류를 감지했을 때, 메시지를 쓰지 않는다. 결국 메시지가 컨슈머에게 전달되지 않을 수도 있다.

Exactly once semantics: 프로듀서가 메시지 실패 혹은 타임아웃으로 메시지를 다시 전송하더라도 최종 소비자에게 정확히 한번 전달된다. 누구나 바라는 것이지만 달성하기는 힘들다. 프로듀서와 컨슈머의 협업이 필요하기 때문이다.

실패의 제어

정확히 한번 메시지를 전달(Exactly once semantics)하는게 왜 어려운지 예를 들어서 설명해보려 한다.

EOP라는 단일 파티션 토픽에 "Hello Kafka"라는 메시지를 보내는 단일 프로듀서가 있다고 가정해보자. 반대편에는 EOP 토픽으로 부터 데이터를 읽는 단일 컨슈머가 있다. 각 컴포넌트들에 문제가 없는 한, Hello Kafka는 EOP 파티션에 한번만 기록이 되며, 소비자는 메시지를 읽어서 오프셋을 커밋하여 처리를 완료한다. 오프셋이 커밋됐으므로, 컨슈머가 실패하고 다시 시작하더라도 메시지를 중복해서 받지는 않을 것이다.

하지만 시스템은 실패하기 마련이다. 시스템의 실패는 막을 수 없다.
  1. 브로커의 실패: 카프카는 고가용성, persistent, 내구성(durable) 시스템으로 모든 메시지는 (일정시간 동안)유지되며, n개의 노드에 복제저장된다. 결과적으로 카프카는 n-1 개의 브로커 오류를 용인한다. 즉, 사용가능한 브로커가 하나라도 있으면 파티션을 사용 할 수 있다.
  2. 프로듀서와 브로커간의 RPC 실패: 카프카의 경고함은 프로듀서가 브로커로 부터 ACK를 받는 메커니즘에 의존한다. 문제는 ACK를 받지 못했다고 요청이 실패했다고 단정 할 수 없다는데 있다. 메시지를 토픽에 성공적으로 기록했지만 브로커와 프로듀서의 충돌, 네트워크의 문제로 ACK를 받지 못할 수 있다. 프로듀서는 실패의 원인을 정확히 알 수 없기 때문에, 일단 실패했다고 가정하고 다시 메시지를 전송한다. 결국 컨슈머는 두 개의 중복된 메시지를 수신하게 된다.
  3. 클라이언트의 실패: 정확히 한번 메시지를 전달하기 위해서는 클라이언트의 오류도 고려해야 한다. 클라이언트가 영구적으로 (완벽하게)실패했는지, 아니면 느슨하게 실패했는지 그 차이를 알 수 없다. 일단 새로운 클라이언트가 실행되면, 실패한 인스턴스의 최근 상태를 복구하고 안전한 지점에서 처리를 시작해야 한다. 즉 처리한 오프셋을 항상 동기화 할 수 있어야 한다.

카프카의 Exactly-once semantics

카프카 0.11.x(2017년 7월 16일 현재 버전은 0.11.0.0)이전 버전에서는 각 파티션에 대해서 least once을 지원했다. 이는 프로듀서가 메시지를 중복해서 보낼 수 있음을 의미한다. 새로 추가된 exactly-once semantics를 아래 설명하는 장치들을 이용해서 지원하고 있다.

멱등성

멱등성은 연산을 여러 번 적용하더라도 결과가 달라지지 않는 성질이다. REST 연산을 예로 들자면, GET, HEAD, DELETE 등은 연산을 여러번 적용하더라도 결과가 달라지지 않는다. 하지만 POST는 상태를 변경하므로 멱등성이 없다. 이제 카프카는 프로듀서의 메시지 전송이 멱등성을 가진다. 프로듀서가 동일한 메시지를 여러 번 보낼 경우 이 정보는 브로커의 카프카 로그에만 기록이 된다. 카파카는 단일 파티션에서 프로듀서 혹은 브로커의 오류로 인한 중복 메시지가 발생할 가능성을 제거 한다. Exactly-once를 적용하려면 "enable.idempotence=true"로 설정하면 된다.

이 기능은 TCP와 유사하게 작동한다. TCP는 패킷에 일련번호(Sequence number)를 포함하는 것으로 세션내에서의 패킷 흐름을 제어한다. 카프카도 각 메시지에 일련번호를 두는 것으로 중복 메시지를 처리 할 수 있다. 프로듀서가 메시지를 중복해서 보낼경우 브로커는 일련번호를 검사하는 것으로 중복 메시지라는 것을 알 수 있다. 이 일련번호는 복제된 로그에 유지되므로 리더가 실패하더라도, 인계한 브로커는 메시지의 중복여부를 확인 할 수 있다.

트랜잭션

이제 카프카는 분산된 파티션에서 atomic write를 위한 트랜잭션 API를 제공한다. 프로듀서는 이 기능을 이용해서 여러 개의 메시지를 여러 파티션으로 보낸 후 해당 배치작업 안에서 모든 메시지가 컨슈머에서 표시되거나 표시되지 않도록 할 수 있다. 또한 이 기능을 이용하면 트랜잭션에서 처리해야 할 데이터들의 오프셋을 커밋 함으로써, 프로듀서와 컨슈머 사이에 정확한 메시지 전달이 가능하다. 아래는 트랜잭션 API의 사용법을 보여준다.
producer.initTransactions();
try {
  producer.beginTransaction();
  producer.send(record1);
  producer.send(record2);
  producer.commitTransaction();
} catch(ProducerFencedException e) {
  producer.close();
} catch(KafkaException e) {
  producer.abortTransaction();
}

Exactly-once stream processing

멱등성과 원자성을 바탕으로 Exactly-once stream 처리를 할 수 있다. 스트림 애플리케이션이 Eactly-once 하게 만들려면 "processing_guarantee=exactly_once"를 설정하면 된다. 이제 모든 메시지는 정확히 한번만 전달된다.

카프카의 streams API가 제공하는 exactly-once는 지금까지의 어떤 스트림 처리 시스템이 제공하는 것보다 더 나은 기능을 제공한다. 카프카에 메시지를 쓰는 것에서, 메시지 처리 애플리케이션에 메시지가 도착하기 까지, 정확히 한번 메시지가 전달 될 것을 보장한다. 하지만 모든 경우에 대해서 exactly-once를 보장하는 것은 아니다. 카프카로 부터 데이터를 읽다가 실패했을 경우에는 카프카 offset을 이용해서, 메시지를 다시 처리 할 수는 있지만, 외부 소프트웨어의 상태를 롤백할 수는 없기 때문에 잘못된 결과가 발생 할 수도 있다.

참고