리액티브 시스템에서의 카프카
https://developer.ibm.com/articles/kafka-fit-reactive-system/
위 아티클을 읽고 내용 정리..
비동기 메시징 백본으로써의 카프카
다른 리액티브 애플리케이션들의 성능에 영향을 미치는 가장 중요한 기저는 통신에 있어서 비동기적인 메시징 백본을 확보하는 것이다. 이는 무엇을 뜻할까?
메시지는 보통 “특정 위치로 전송된 아이템"으로 정의된다.
리액티브 시스템에서, 메시지 주도의 통신은 어플리케이션의 컴포넌트 간에 중개되는 데이터를 포함한 메시지를 중심으로 이뤄진다.
메시지 드리븐 아키텍쳐는
- 더 비동기적이고
- 스트림 중심의 접근을 허용한다.
- 때문에 데이터는 도달하는 대로 처리될 수 있다.
아파치 카프카는 분산 스트리밍 플랫폼으로써, 이런 종류의 통신에 있어서 메시지 드리븐 백본으로 사용될 수 있다.
어플리케이션은 각 컴포넌트끼리 카프카 토픽으로 발행되는 레코드의 형태로 메시지를 보낼 수 있고, 카프카 토픽으로부터 소비되는 방식으로 메시지를 수신할 수 있다.
어플리케이션의 회복성 (resilience)
리액티브 시스템에서의 어플리케이션은 시스템에 내재되어 있는 잠재적인 위험들을 graceful 하게 다룰 수 있어야 한다. 이것을 회복성 (resiliency)라고 부른다.
회복성은 어플리케이션으로 하여금
- 장애로부터 복구되는 것을 가능하게 하고
- 어플리케이션 각각의 컴포넌트 간의 디커플링을 가능하게 하고
- 리액티브 어플리케이션 내부의 배압 (back pressure)를 사용할 수 있도록 한다.
메시지 리텐션과 데이터 영속성
카프카는 다른 스트림 히스토리를 제공하는 메시지 큐잉 시스템과는 다르다.
- 컨슈머가 카프카로부터 레코드를 읽어갈 때, 레코드는 토픽으로부터 제거되지 않는다.
- 이렇게 토픽에 남아 있는 레코드는 다른 컨슈머가 나중에 재소비할 수도 있게 한다.
- 이런 스트림 히스토리의 구성은 어플리케이션이 장애로부터 복구되는 것을 가능하게 하는 측면에서 용이하다.
컨슈머들은 카프카 스트림 내부에 오프셋의 형태로 자신들의 위치를 기록해 놓는다. 그리고 만약 시스템이 다운되어도 이 오프셋 정보를 토대로 다시 마지막으로 읽었던 곳으로부터 메시지를 읽는 것이 가능하다.
카프카 내부틔 스트림 히스토리는 어플리케이션으로 하여금 추가로 영속적인 저장소를 사용해서 장애로부터 복구되도록 하지 않아도 되도록 한다.
디커플링
시스템 내부의 컴포넌트 간 느슨한 결합을 유지하는 것은 장애를 로컬화 시키기 때문에 더 회복성이 있는 어플리케이션을 만들 수 있게 한다.
- 어플리케이션 내부의 컴포넌트가 강결합되어 있다면, 하나의 컴포넌트에서 발생한 에러는 다른 컴포넌트에 밀접하게 영향을 미칠 것이다.
- 컴포넌트가 약결합 (느슨하게 결합) 되어 있다면, 장애는 전파되지 않을 것이고 에러 컨텍스트는 발생한 컴포넌트 내부에 머물게 된다.
어플리케이션을 성공적으로 디커플링하기 위해서는 응답이 없는 서비스에 대해 민감한 fallback 행동을 미리 작성해놔야 한다. (이런 종류의 fallback behavior는 MicroProfile 이나 Istio와 같은 어플리케이션 라이브러리 레벨에서도 종종 다루어진다.)
카프카를 컴포넌트 간 통신 메커니즘으로 사용하는 것은 디커플링을 달성할 수 있는 한 가지 좋은 방법이다.
- 프로듀서가 다운되더라도, 컨슈머는 존재하는 레코드들을 에러 없이 읽어올 수 있다.
- 프로덕션 환경에서 어플리케이션의 큰 변화 없이 토픽에 대해 새 컨슈머를 추가하거나 기존 컨슈머를 제거하는 것도 가능하다.
카프카를 사용하여 통신하는 마이크로서비스들을 작성할 때 고려해야 할 점은 - 전송 및 수신되는 레코드들의 형태이다.
- 만약 프로듀셔가 생산하는 레코드의 형태를 변경한다면 컨슈머를 고장낼 수 있고, 이는 결합이 강하게 이뤄졌다는 것을 의미한다.
- 이런 종류의 결합을 제공하기 위해서는 스키마와, 스키마 레지스트리를 사용하는 것이 권고된다.
스키마는 레코드 내부의 데이터 구조를 정의한다.
스키마 레지스트리는 스키마들을 저장하기 위해 사용된다.
카프카에서 사용할 수 있는 다양한 솔루션들이 존재하며, 많은 스키마 레지스트리들은 검증과 호환성 검증과 같은 기능을 제공해서 프로듀서와 컨슈머가 새로운 버전의 스키마로 쉽게 업그레이드 할 수 있게 한다.
카프카와 같은 메시징 백본을 사용함으로써, 컴포넌트는 디커플링된다.
fault tolerant한 어플리케이션과 같이 사용되었을 때, 카프카는 시스템에 더 큰 회복성을 제공할 수 있다.
배압 (Backpressure)
배압은 리액티브 시스템을 구현할 때 유용한 패턴이다.
- 배압을 통해 어플리케이션은 잠재적인 병목을 큰 무리 없이 다루게 해주고
- 리소스가 과소비되는 일을 방지하며
- 부하가 가해지는 시스템에게 더 큰 회복성을 제공한다.
배압 패턴은 upstream 컴포넌트들에게 새로운 메시지를 다룰 준비가 되었음을 알려주는, 일종의 피드백 메커니즘으로 동작한다. 이를 통해 컴포넌트는 과도한 부하가 가해지는 일을 사전에 방지할 수 있다.
TCP/IP에서 배압은 별도의 송/수신 버퍼와 수신 버퍼에 가용한 공간이 있을 때에만 데이터를 전송함으로써 달성된다. receive window를 사용하고, downstream 컴포넌트에 여유가 있을 때에만 데이터를 요청할 수도 있지만 (전통적인 TCP/IP에서의 배압) 일종의 무한 버퍼로써 카프카를 도입할 수도 있다. (카프카의 메시지가 영속적이기 떄문)
카프카만 사용하는 간단한 어플리케이션을 작성할 때에는 거의 무한한 버퍼와 같은 동작을 통해 배안에 대한 요구 사항을 달성할 수 있다.
하지만 이는 실 사용 사례에서는 적용될 수 없기 때문에 카프카를 사용하는 시스템이라면 어느 정도의 배압이 필요한 경우가 대부분이다.
카프카 커슈머에서의 배압
표준 자바 카프카 컨슈머 클라이언트는 어플리케이션에서 짧고 불규칙적인 부하를 과도한 스트레스 없이 처리할 수 있다.
카프카 컨슈머는 pull 기반으로 동작하기 때문에 새 메시지를 poll 메서드를 통해 받게 된다. 이 메시지 소비에서의 pull 기반 메커니즘은 컨슈머로 하여금 어플리케이션이 다운되었거나 downstream 컴포넌트가 과도한 부하를 받아 여유가 없을 경우에 새 레코드를 요청하지 않도록 할 수 있다.
가장 간단한 유즈 케이스에서 배압은 모든 이전 레코드들이 처리된 후에만 새 레코드를 poll 하도록 설정하여 달성될 수 있다.
- 반복문 안에서 동기적으로 poll 메서드를 호출하고, 새 레코드가 수신될 때까지 block된다.
- 레코드가 수신되고, 처리된다.
- 모든 레코드들이 처리된다면 반복문을 재시작해서 새로운 레코드를 poll하도록 한다.
이러한 메커니즘의 컨슈밍은 단일 반복문, 싱글 쓰레드를 사용하며, 메시지 처리가 꽤 빠른 간단한 유즈 케이스에서는 잘 동작한다.
하지만 멀티 쓰레드를 사용하는 분산 시스템에서는 위와 같은 방법을 통해 컨슈머 어플리케이션에서 리소스를 사용할 수 없다.
대부분의 리액티브 자바 카프카 클라이언트들은 polling과 메시지 처리를 분리하여 리소스를 더 효율적으로 활용할 수 있게 한다. 배압이 필요할 때 표준 자바 카프카 클라이언트에 존재하는 pause
, resume
함수의 조합을 통해 이를 조절할 수 있다.