[Kafka] KRaft 모드에서 컨트롤러의 정체 파헤치기

Broccolism·2024년 10월 6일
4
post-custom-banner

발단: 이 그림 뭐에요?

몇 안되는 최신 카프카 설명서인 카프카 핵심 가이드를 읽으며 스터디를 하고 있었다. 그런데 155쪽에 나온 이 그림을 두고 다들 서로 다르게 이해한 채로 책을 읽고 있었던 것…!

  • KRaft 모드에서는 기존 주키퍼 사용 카프카의 '컨트롤러'가 더이상 존재하지 않는다고 했는데 대체 저건 뭘까? 왜 컨트롤러라는 이름을 하고 있는걸까? 서버 한 대인걸까 프로세스 하나인걸까? 카프카 브로커와 다른 서버인걸까?

아무리 구글링해도 저게 뭔지 안 나오길래 카프카 코드를 직접 구경하고 왔다. 결론부터 말하면, 저 사각형은 그저 프로그램 안의 클래스 인스턴스 하나다.^,^.. 하지만 상황에 따라 서버 한 대를 의미할 수도 있다..! (이건 또 무슨 소리? 자세한것은 아래로 내려보자.)

배경 - KRaft 모드에서의 변화

기존 카프카

151p. 브로커가 컨트롤러가 되면, 클러스터 메타데이터 관리와 리더 선출을 시작하기 전에 먼저 주키퍼로부터 최신 레플리카 상태 맵을 읽어온다.
...
브로커가 클러스터를 나갔다는 사실을 컨트롤러가 알아차리면,
컨트롤러는 해당 브로커가 리더를 맡고 있었던 모든 파티션에 대해 새로운 브로커를 할당해주게 된다.
컨트롤러는 새로운 리더가 필요한 모든 파티션을 순회해 가면서 새로운 리더가 될 브로커를 결정한다.
그러고 나서 새로운 상태를 주키퍼에 쓴 뒤, ...
  • 클러스터 멤버십, 리더 정보 등 메타데이터를 주키퍼가 관리한다.
  • 카프카 브로커 중, 주키퍼와의 소통을 담당하는 브로커를 컨트롤러라고 부른다.
  • 컨트롤러: "내가 리더도 뽑고 이것저것 알아서 다 해야지 !"

그래서 Raft 도입 이전 버전의 카프카 그림을 그려보면 이렇게 된다. 카프카 클러스터와 주키퍼 클러스터가 각각 있을 때, 카프카 클러스터의 프로세스 중 하나가 컨트롤러라는 역할을 맡게 되고 주키퍼와의 통신을 담당한다. 그림에서 3개의 카프카 프로세스 중, 누가 파티션 리더 브로커가 될 지는 주키퍼가 결정하고 주키퍼가 저장한다.

KRaft 에서는 '컨트롤러'의 의미가 바뀐다

여기서부터 조금 헷갈리기 시작한다. KRaft 모드와 그 이전 버전에서 똑같은 단어를 사용하지만 의미가 달라진다. 껍데기만 ‘컨트롤러’로 같은 것이다. 아래는 KRaft 모드에서의 컨트롤러 에 대한 설명이다.

154p. KRaft 이후부터는 주키퍼 프로세스가 제거되기 때문에 카프카 프로세스 외에 다른 프로세스는 없다.
다만 카프카 프로세스가 다음 두 개 중 적어도 하나의 역할(role)을 가지게 된다(즉, 두 역할을 겸할 수도 있다)
: 컨트롤러, 브로커.
- 컨트롤러: 메타데이터관리
- 브로커: 클라이언트 요청을 받아서 카프카 레코드 관리

Kraft 모드에서는 주키퍼가 사라진다. 따라서 주키퍼 클러스터는 없어지고, 카프카 클러스터만 그림에 남게 된다.

  • 주키퍼가 들고 있던 메타데이터는 __cluster_metadata 라는 카프카 토픽에서 관리하게 된다.

    In KRaft mode, cluster metadata, reflecting the current state of all controller managed resources, is stored in a single partition Kafka topic called __cluster_metadata. (https://developer.confluent.io/courses/architecture/control-plane/)

주키퍼가 없어지니 자연스럽게 주키퍼와의 통신을 담당하는 역할을 의미하던 “컨트롤러 브로커”는 사라지게 된다. 대신 이제 “컨트롤러”라는 용어는 다른 의미로 사용된다. 바로 카프카 프로세스가 가질 수 있는 역할의 일종이 되는 것이다. 책에 나온 것처럼 카프카 프로세스는 컨트롤러 이거나 브로커 이거나 컨트롤러 이면서 브로커 역할을 맡게된다. 더이상 카프카 프로세스 == 브로커 라는 공식이 통하지 않게 되는 것이다.

컨트롤러 역할을 맡은 모두가 리더 선출에 관여한다.(이제 컨트롤러라고 불리는 프로세스는 더이상 1개가 아니다!) 컨트롤러 역할은 브로커 역할과 겸임할 수 있다. 예를 들어, 카프카 프로세스 5개로 구성된 클러스터가 있을 때 이런 역할을 맡을 수 있다.

  • 카프카 프로세스 1 = 컨트롤러 1: "내가 리더 선출할거야"
  • 카프카 프로세스 2 = 컨트롤러 2: "나도 투표할거야
  • 카프카 프로세스 3 = 컨트롤러 3 & 브로커 1: "나도"
  • 카프카 프로세스 4 = 브로커 2: "나는 데이터 처리만 할거야"
  • 카프카 프로세스 5 = 브로커 3: "나도 데이터 처리만 할거야"

컨트롤러 1, 2, 3 즉, 카프카 프로세스 1, 2, 3으로 구성된 서버 풀을 '컨트롤러 풀' 이라고 부른다.

KRaft 모드에 대한 질문

질문 1. 카프카 프로세스이면서 브로커가 아닐 수도 있는가?

  • YES.
    https://developer.confluent.io/courses/architecture/control-plane/ 에 나와있는 그림을 보자.
  • 아래 그림의 윗 부분은 컨트롤러와 브로커 역할을 맡는 노드가 각각 따로 있는 클러스터를, 아랫부분은 컨트롤러와 브로커 역할을 겸임하는 노드가 있는 클러스터를 각각 나타내고 있다.
  • 노드 셋팅을 할 때 설정값인 process.rolescontroller 라고만 설정하면 브로커 역할은 안 하고 컨트롤러 역할만 하는 노드를 만들 수 있다.

질문 2. 맨 처음 봤던 그림에서 '컨트롤러'의 정체는 뭘까?

  • 클래스 인스턴스다!
  • 카프카 코드를 살펴보자: https://github.com/apache/kafka
  • 일단 방금 등장한 process.roles 를 설정값으로 받고
// KRaftConfigs.java

public class KRaftConfigs {
    /** KRaft mode configs */
    public static final String PROCESS_ROLES_CONFIG = "process.roles";
  • KafkaConfig 를 만든다. parseProcessRoles() 에서 위 설정값을 보고 역할을 정해준다. 브로커이거나, 컨트롤러이거나, 혹은 둘 다가 될 수 있다.
// KafkaConfig.scala

class KafkaConfig private(doLog: Boolean, val props: util.Map[_, _])
  extends AbstractKafkaConfig(KafkaConfig.configDef, props, Utils.castToStringObjectMap(props), doLog) with Logging {

  // ...
  val processRoles: Set[ProcessRole] = parseProcessRoles()
  // ...
  private def parseProcessRoles(): Set[ProcessRole] = {
    val roles = getList(KRaftConfigs.PROCESS_ROLES_CONFIG).asScala.map {
      case "broker" => ProcessRole.BrokerRole
      case "controller" => ProcessRole.ControllerRole
      case role => throw new ConfigException(s"Unknown process role '$role'" +
        " (only 'broker' and 'controller' are allowed roles)")
    }

    val distinctRoles: Set[ProcessRole] = roles.toSet

    if (distinctRoles.size != roles.size) {
      throw new ConfigException(s"Duplicate role names found in `${KRaftConfigs.PROCESS_ROLES_CONFIG}`: $roles")
    }

    distinctRoles
  }
  • 이렇게 셋팅한 processRoles 를 보고 주키퍼가 필요한지 아닌지 플래그를 세워놓고
// KafkaConfig.scala
// ...
def requiresZookeeper: Boolean = processRoles.isEmpty // (설정값으로 아무 역할도 지정 안 했다면 KRaft 쓰는게 아닌거임!)
def usesSelfManagedQuorum: Boolean = processRoles.nonEmpty
  • 주키퍼가 필요하면 KafkaServer, 필요 없으면 KafkaRaftServer 를 만든다.
// Kafka.scala

private def buildServer(props: Properties): Server = {
  val config = KafkaConfig.fromProps(props, doLog = false)
  if (config.requiresZookeeper) {
    new KafkaServer(
      config,
      Time.SYSTEM,
      threadNamePrefix = None,
      enableForwarding = enableApiForwarding(config)
    )
  } else {
    new KafkaRaftServer(
      config,
      Time.SYSTEM,
    )
  }
}
  • 그리고 KafkaRaftServer 시작, 종료할 때 순서대로 startup, shutdown 해준다.
// KafkaRaftServer.scala

override def startup(): Unit = {
  Mx4jLoader.maybeLoad()
  // Controller component must be started before the broker component so that
  // the controller endpoints are passed to the KRaft manager
  controller.foreach(_.startup())
  broker.foreach(_.startup())
  AppInfoParser.registerAppInfo(Server.MetricsPrefix, config.brokerId.toString, metrics, time.milliseconds())
  info(KafkaBroker.STARTED_MESSAGE)
}

override def shutdown(): Unit = {
  // In combined mode, we want to shut down the broker first, since the controller may be
  // needed for controlled shutdown. Additionally, the controller shutdown process currently
  // stops the raft client early on, which would disrupt broker shutdown.
  broker.foreach(_.shutdown())
  controller.foreach(_.shutdown())
  CoreUtils.swallow(AppInfoParser.unregisterAppInfo(Server.MetricsPrefix, config.brokerId.toString, metrics), this)
}

정리

  • 맨 처음, 그림에서 컨트롤러는 상황에 따라 서버 한 대를 의미할 수 있다고 한 이유: 브로커나 컨트롤러 둘 중 하나만 선택할 수도 있으니까. 서버를 띄워놓고 컨트롤러 역할만 맡기면 그대로 서버 하나가 컨트롤러 하나가 되는 셈이다.
  • 카프카 내부 구현에서 컨트롤러, 브로커는 KafkaRaftServer 의 필드 중 하나로 등록된다.

  • 사실 하위호환을 생각하면 이런식으로 구성하는게 아주 자연스럽다! 하지만 클래스 필드 하나일 것이라고는 상상도 못 한 정체… 오늘도 하나 배웠다.

  • +) 번외: 추가로 든 생각과 스터디원의 대답

    • 🤷‍♀️: 어… 컨트롤러 역할만 가져가서 데이터 처리 안 하고 리더 선출만 할거면 왜 카프카 노드가 된 겁니까? 당연히 브로커이면서 컨트롤러 역할을 겸임하거나, 브로커 역할만 하는게 자연스럽지 않나요?
    • 👩🏻: 어차피 주키퍼 때에도 메타데이터 관리만 하던 서버가 따로 있었으니 크게 부자연스러울건 없죠. 브로커와 컨트롤러 프로세스 둘 중 한 군데에 이슈가 발생했을 때 이 둘을 하나의 서버에 같이 두는 것보다 떨어뜨려놓는게 더 좋지 않겠어요?
    • 🤷‍♀️: 흠… 그럴 수 있겠군요. !!
profile
설계를 좋아합니다. 코드도 적고 그림도 그리고 글도 씁니다. 넓고 얕은 경험을 쌓고 있습니다.
post-custom-banner

2개의 댓글

comment-user-thumbnail
2024년 10월 7일

정리 잘 해주셔서 너무 잘 읽었습니다! 차라리 과거의 카프카를 몰랏다면,, 싶은 생각도 드네요

1개의 답글