기술면접 중, 현재 시스템에 kafka 를 적용한 이유에 대해서 물어보셨다.
내부적인 문제를 조리있게 설명할 자신이 없었는데,(실수할까봐;)
그냥 넘어가주셔서, 다음에는 조리있게 설명하기 위해 해당 내용을 정리합니다.

기존 시스템

기존 레거시 시스템은 JAVA의 Bean 객체에 데이터를 담는 것입니다.
어플리케이션에 데이터를 담고, 해당 데이터에 Spring Job Scheduler 을 통해, 10초 or 20초 간격마다,
데이터를 insert 하는 로직이었습니다.

스케일업과 스케일아웃

영업과 운영이 확장되면서 기존 시스템으로 버거운 상황이 생기기 시작했습니다.
단순하게,
스케일 아웃이란 서버를 여러 대 추가하여 시스템을 확장하는 방법이며,
스케일 업은 서버에 CPU나 RAM 등을 추가하여 고성능 부품, 서버로 교환하는 방식이었습니다.

서비스가 고도화 됨에 따라, 스케일업과 스케일아웃이 동시다발적으로 진행되었고,
그로인해 기존 덤프로직으로는 점점 버티기 힘들게 되었습니다.

장애 상황

첫번째 장애의 경우 Major GC로 어플리케이션이 순간 지연이 발생하는 것이었습니다.
유입량의 증가로 인해, 10초 와 20 초 정도 담는 Bean 객체의 Heap 영역이 점점 늘어났습니다.
해당 데이터를 Major GC를 유발하였고, “Java GC Stop The World time” 이 발생이 되었습니다.
(etc. 이때 해당 문제를 해결하기 위해 단순 패러랠 GC에서 CMS 75%를 적용하게 됨)

두번째 장애는 유실률 증가입니다.
덤프의 경우, Bean 에 담긴 객체를 temp 객체에 담게, 다시금 해당 Bean 을 초기화하는 작업을 반복했습니다. 이 경우,
초기화 하는 그 순간의 유입되는 데이터가 삭제되는 문제가 생길 수 밖에 없었습니다.
해당 기능을 해결하기 위해, ConcurrentHashMap 를 적용하거나 구아바(Guava) 등을 고려했으나,
순간 유실되더라도 ad tech 는 Thread-Safe 를 보장하지 않더라도 속도를 해결해야하는 숙명을 고려해야했습니다.

세번째는 db lock 상황이 증가되는 이슈였습니다.
스케일 아웃이 발생하면서, 동일 DB에 동일 row에 insert 하는 현상이 빈번하게 발생했습니다.
이 이슈는 위의 2가지 이슈보다 더 큰 장애상황을 야기했습니다.
dead lock 이 걸리는 순간 해당 lock 상황을 DBA에 프로세스 킬을 하지 않는한, 서비스를 잠시 중단해야하는 경우가 생긴 적도 있을 정도로 크리티컬한 상황을 예상할 수 밖에 없었습니다.
(feat. 긴급하게, master 와 slave 구조를 만들어, select 는 slave로 하여 dead lock 상황이 발생하더라도 서비스는 안정적으로 운영가능하게 작업. )

메시지 큐 적용을 고려

기존 덤프로직들을 jvm 의 Bean 객체가 아닌, 다른 곳에 저장 후 처리방식에 대해 논의가 있었습니다.

1차적으로 몽고 시스템을 적용했으나, MongoDB 영속성 (Persistence) 문제가 있었습니다.
Bean 에 담는 객체보다는 데이터 유실률은 좋았으나, 중요 순간 순간, 데이터 검증에 실패하게 되어, 메시지 큐에 대해서 고민하게 되었습니다.

CTO 님은 스파크를 사용하고 싶었고, 휼룡한 개발자분들은 Nosql 쪽보다는 메시지 큐 방식을 더 선호하였습니다.

1 차적으로 몽고를 테스트 했던 이유는 단순히 현재 몽고가 운영되고 있었기 때문이었으니까요! 모든 것이 도전인 상황이었습니다.

좋은 개발자분들이 해당 상황은 메시지 큐 방식이 정당하다는 것을 몽고를 1차적으로 테스트성으로 적용하여, 어설프게나마 증명을 했습니다.
남은 것은 좋은 큐를 선택하는 것이 었습니다.

Kafka, RabbitMQ, Active MQ 중 카프카를 선택한 이유

카프카가 유명해서?
보편적으로 레빗보다는 카프카를 운영에서 많이 쓴다고 하지요.

두개의 장단점은 다음과 같습니다.
RabbitMQ 는 broker가 워커(kafka의 컨슈머)에서 메시지를 push 하는 방식이고,
kafka는 컨슈머가 broker 로부터 메시지를 pull 하는 방식입니다.
(feat. 카프카의 경우도 RabbitMQ처럼 메시지를 push 하는 방법이 있다고 하셨는데, 그 부분은 조금더 문서를 찾아봐야겠습니다. 제가 모르는 부분이었습니다!)

RabbitMQ 아키텍처

범용 메시지 브로커 — 다양한 요청 / 응답, 지점 간 및 게시-구독 통신 패턴을 사용합니다.
스마트 브로커 / 단순한 소비자 모델 — 브로커가 소비자 상태를 모니터링하는 것과 거의 같은 속도로 소비자에게 일관된 메시지 전달.
성숙한 플랫폼 — 잘 지원되며 Java, 클라이언트 라이브러리, .NET, Ruby, node.js에서 사용할 수 있습니다. 수십 개의 플러그인을 제공합니다.
통신 — 동기식 또는 비동기식 일 수 있습니다.
배포 시나리오 — 분산 배포 시나리오를 제공합니다.
다중 노드 클러스터 - 클러스터 연합 — 외부 서비스에 의존하지 않지만 특정 클러스터 형성 플러그인은 DNS, API, Consul 등을 사용할 수 있습니다.

Apache Kafka 아키텍처

대용량 발행-구독 메시지 및 스트림 플랫폼 — 내구성, 속도 및 확장 성.
내구성있는 메시지 저장소 — 로그와 같이 서버 클러스터에서 실행되며 주제 (범주)의 레코드 스트림을 유지합니다.
메시지 — 값, 키 및 타임 스탬프로 구성됩니다.
Dumb Broker / 스마트 소비자 모델 — 소비자 가 읽은 메시지를 추적하지 않고 읽지 않은 메시지 만 보관합니다. Kafka는 일정 기간 동안 모든 메시지를 보관합니다.
실행하려면 외부 서비스가 필요합니다 . 일부 경우 Apache Zookeeper.

실질적으로 저희 서버의 경우, 초당 처리되야하는 데이터가 엄청 많아야 하고, Consumer가 처리할 수 있을 때 메시지를 가져오므로 자원을 효율적으로 사용 가능하고, 항시적으로 데이터를 가지고 있으니까, 결과적으로 카프카를 적용하게 되었습니다.

물론, 카프카도 문제점이 있습니다.
pull 방식이라서 생기는 문제로, 데이터가 없음에도 정기적인 polling으로 인해 자원을 낭비하는 문제입니다.
컨슈머 서버가 계속 while(true) 로 기다리고 있는거지요.

이러한 단점을 보완하기 위해 실제 데이터가 도착할 때까지 long poll 대기를 할 수 있는 parameter를 지원하기도 합니다.
이번 프로젝트의 카프카 리스너에는 해당값을 적용햇습니다.

그 이후…

제가 그 후 추가하는 모든 서비스는 새로운 서비스를 구축하기보다, 기존 서비스를 사용하는 경우가 많았고, 완벽하게 별도로 구축한 카프카 서버는, 단순 토픽만을 저장하는 용도로 사용하여!!!

신규 서버 구축보다는 기존 카프카를 이용하는 형태로 운영중입니다!! (다만, 제가 구성할때, 주키퍼 서버 3대를 구축했는데 사용 CPU가 1~3%만 사용하고 있어, 해당 서버를 별도로 이용하는 방법을 계속 고민중입니다. )

결론

  • 하나의 토픽으로 다중 consumer이 받아갈수 있는 구조
  • 대량 지연 실시간 처리가능 및 데이터 공유

추가적으로 나중에 그룹으로 별도의 동일 데이터를 가져가 처리할 수 있는 기능이 필요하게 되었을 때,
정말 카프카를 유용하게 사용했습니다!
다른 메시지 큐보다 지금 운영하는 시스템에 딱 맞었다고 봅니다.

참고 주소 : https://dhsim86.github.io/web/2017/04/03/Apache_kafka-post.html
https://dhsim86.github.io/web/2017/03/22/spring_boot_features_06-post.html