J juno.log
글 목록

카프카 너는 도대체 누구냐? 1 / 1

카프카를 이해해봅시다.

회사에서 ‘AI Native’를 이야기하다가도, 결국 시스템을 제대로 이해하지 못하면 아무것도 그릴 수 없다는 생각이 들었다. 그래서 평소에 이름만 듣고 어렴풋이 알던 카프카(Apache Kafka)를 제대로 파보기로 했다. 누가 설명해주는 걸 받아 적는 대신, 개념을 하나씩 듣고 내 언어로 다시 풀어서 이해한 걸 기록해보려고 한다.

이름부터

사실 프란츠 카프카의 소설을 볼 때마다 “어, 아파치 카프카~” 하고 혼자 농담을 했었다. 그런데 그게 진짜였다.

카프카를 만든 Jay Kreps가, “쓰기에 최적화된 시스템이라 작가의 이름을 붙였다”며 소설가 프란츠 카프카에서 이름을 따왔다고 한다. 농담인 줄 알았던 게 진짜였다는 게 좀 웃겼다.

왜 생겼을까

카프카가 푸는 문제는 ‘시스템 사이의 데이터 연결’이다.

회사에 서비스가 늘어난다고 생각해보자. 주문이 하나 들어오면 정산도 해야 하고, 알림도 보내야 하고, 검색 색인도 갱신하고, 로그도 분석하고, 추천에도 반영해야 한다. 이걸 일일이 직접 연결하면 연결선이 N×M개로 폭발한다. 게다가 연결마다 데이터 포맷도, 장애 처리 방식도 제각각이 된다.

카프카의 답은 단순하다. 가운데에 거대한 로그(log) 하나를 두는 것이다. 데이터를 만드는 쪽(producer)은 로그에 쓰고, 가져다 쓰는 쪽(consumer)은 로그에서 읽는다. 한 서비스가 양쪽 역할을 다 하기도 하지만, 일단 쓰는 역할과 읽는 역할로 나눠서 보자. 그러면 연결이 N×M에서 N+M으로 줄어든다. 생산자와 소비자가 서로를 몰라도 되는 구조가 된다.

graph LR
  P1[주문] --> K[(Kafka 로그)]
  P2[결제] --> K
  K --> C1[정산]
  K --> C2[알림]
  K --> C3[검색]
  K --> C4[분석]

큐가 아니라 로그라고요?

여기서 가장 중요한 개념 하나. 카프카는 ‘추가만 되는 로그(append-only log)’ 다. 메시지가 순서대로 차곡차곡 쌓이고, 각 소비자는 자기가 어디까지 읽었는지(offset) 를 기억하면서 읽어 나간다.

graph LR
  subgraph 토픽["토픽 (append-only log)"]
    M0[0]-->M1[1]-->M2[2]-->M3[3]-->M4[4]
  end
  CA["정산 consumer · offset=4"] -.-> M4
  CB["분석 consumer · offset=2"] -.-> M2

같은 로그를 정산 consumer는 offset 4까지, 분석 consumer는 offset 2까지 읽은 식으로 각자 자기 위치를 들고 읽는다.

내가 알던 메시지 큐(RabbitMQ 같은)와 결정적으로 다른 부분이 여기다.

  • 보통 큐: 메시지를 꺼내 읽으면 사라진다. 소비가 곧 삭제다.
  • 카프카: 읽어도 사라지지 않는다. 보존 기간 동안 로그에 그대로 남는다.

이 차이를 듣고, 나는 ‘읽어도 안 사라지는 로그’가 있으면 무엇이 가능해질지 혼자 떠올려봤다. 크게 세 가지였다.

  1. 재시도. 이벤트를 소비한 쪽이 뭔가를 수행하다 실패했을 때, 로그가 남아 있으니 그 자리부터 다시 시도할 수 있다.
  2. 여러 소비자가 같은 이벤트를 소비. 정산팀, 알림팀, 분석팀이 같은 로그를 각자 독립적으로 읽을 수 있다.
  3. 영속화. 기존 큐의 휘발성에 한계를 느끼고, 발행/소비라는 행위 자체에 영속화까지 고안한 게 아닐까.

이 세 가지가 사실 카프카의 핵심 장점 그대로였다.

1번은 좀 더 정확히 말하면 ‘언제 읽었다고 표시(offset commit)하느냐’와 맞물린다. 처리에 성공한 뒤에 표시하면, 중간에 실패해도 표시 안 된 자리부터 다시 읽으니 적어도 한 번은 전달(at-least-once) 되는 셈이다. (반대로 처리 전에 미리 표시해버리면 그 사이 실패한 건 유실될 수도 있다.) 그러니까 ‘재시도가 되니까 at-least-once’가 아니라, 이 commit 시점이 전달 보장의 진짜 핵심이다. 이건 뒤 편에서 더 파볼 생각이다.

2번은 생산자가 누가 읽는지조차 모르는 느슨한 결합(decoupling) 의 힘이다. 그리고 3번처럼, 카프카는 실제로 메시지 큐와 저장소의 경계를 허물고 ‘로그 자체를 저장소’로 본다.

영속화에서 한 발 더 나가면 이런 것도 된다. 로그가 안 사라지니까, 나중에 새로 붙는 소비자도 과거를 처음부터 다시 읽을 수 있다. 예를 들어 반년 뒤에 추천 시스템을 새로 만들어도, 그동안 쌓인 주문 이벤트를 맨 앞에서부터 재생(replay)해서 학습시킬 수 있다. 소비하면 사라지는 큐에서는 상상하기 어려운 일이다.

사실 나는 이 replay를 의도치 않게 한 번 겪어봤다. 컨슈머가 새로 뜨면 그 전에 쌓인 메시지는 가져오지 않을 거라고 생각했는데, 새 컨슈머를 띄우자 그동안 쌓여 있던 메시지를 전부 다시 컨슘해버린 것이다. 메시지가 영속화되어 있다는 걸 염두에 두지 않아서 생긴 일이었다. 새로 뜬 컨슈머는 ‘어디까지 읽었다’는 기록(offset)이 없으니, 설정에 따라 로그의 맨 앞에서부터 읽어버린다. 큐라고 생각하고 접근했다가, 카프카가 큐가 아니라 로그라는 걸 몸으로 배운 셈이다.

그럼 로그가 무한히 쌓이면?

여기까지 듣고 나니 자연스럽게 걱정이 됐다. 로그가 계속 쌓이기만 하면 부하가 클 텐데, 기본 저장 기간 같은 게 있으려나?

있었다. 카프카 토픽은 기본적으로 7일이 지나면 오래된 로그를 지운다. 시간 말고 크기 기준으로도 정리할 수 있고, 둘 중 먼저 도달하는 쪽이 적용된다. 그래서 무한정 쌓이지 않고, 토픽마다 “우린 3일이면 충분”, “우린 30일은 봐야 해” 식으로 정할 수 있다.

그리고 만약 정말로 무한히 적재해야 한다면, 어느 정도 쌓인 뒤 S3 같은 스토리지로 백업해두면 되지 않을까 생각했다. 그런데 이것도 이미 기능으로 있었다. Tiered Storage(계층형 저장소) 라고, 최근 데이터는 빠른 로컬 디스크에 두고 오래된 데이터는 S3 같은 오브젝트 스토리지로 자동으로 내려보낸다. 덕분에 브로커 디스크 부담 없이 사실상 무한에 가까운 보존이 가능하다. 내가 떠올린 백업 아이디어를 카프카가 이미 정식 기능으로 갖고 있던 셈이다.

전부 다 지우긴 싫은 경우를 위한 방식도 있다. 로그 압축(log compaction) 은 시간이 지나도 ‘키별 최신 값’은 남긴다. 예를 들어 ‘사용자 123의 최신 프로필 상태’처럼, 과거 변경 이력은 지우더라도 마지막 상태 하나는 보존하는 식이다.

1부를 정리하면

카프카는 결국, 시스템 사이의 N×M 연결을 가운데 로그 하나로 N+M으로 줄이고, 그 로그를 ‘읽어도 사라지지 않게’ 만든 시스템이다. 그 하나의 선택에서 재시도, 다중 소비, 재생, 영속화가 줄줄이 따라 나온다.

여기까지가 “카프카가 누구냐”였다.

그런데 정리하고 보니 걸리는 게 생겼다. 소비자가 ‘어디까지 읽었는지(offset)‘를 각자 들고 읽는다고 했는데, 그러면 그 소비자가 죽으면 그 위치는 누가 기억해줄까? 또 한 로그를 여러 명이 나눠 읽으려면 어떻게 쪼개야 할까?

다음 편에서는 이 로그를 토픽, 파티션, 오프셋, 컨슈머 그룹으로 해부하면서 그 답을 찾아보려 한다.