분산 messaging system, message broker (kafka, rabbitmq을 이해하기 앞서 기초)

안녕하세요 codeshow입니다.
이번 시간에는 분산 메세징 시스템을 학습하겠습니다.
메세징 시스템에서 메세지란, 둘 이상의 관계에서 주고받을수 있는 데이터입니다.
여기서 데이터는, 일반적으로 바이너리 데이터입니다. json 이나 Protocol Buffers 와 같은 serializer 를 사용해서 데이터를 주고 받습니다.
메세징 시스템을 가운데 두고 한쪽은 publisher, 다른 한쪽은 subscriber 관계를 맺습니다.
publisher는 메시지를 생성하고 메시지 큐에 전송하는 클라이언트입니다. 메시지를 큐에 보내는 것을 publish 라고 합니다.
subscriber는 메시지 큐에서 메시지를 받는 클라이언트입니다. 메시지를 큐에서 가져오는 것을 consume 이라고 합니다.
메세지는 publisher 가 publish 하고 메세징 시스템을 거쳐 subscriber로 메세지가 이동합니다.
메세지는 항상 publisher, 메세징 시스템, subscriber로 항상 단방향으로 흐릅니다.
참고로 publisher 대신 producer, subscriber 대신 consumer 용어를 사용합니다.
그리고 publisher와 subscriber를 줄여서 pub, sub를 사용합니다.

publisher와 subscriber 관계를 이해하기 위해 코드를 예를 들어 설명드리겠습니다.

package main

import "time"

func main() {
    c := make(chan string)
    go pub(c)
    go sub(c)
    select {}
}

func sub(c <-chan string) {
    for {
        message := <-c
        println(message)
    }
}

func pub(c chan<- string) {
    c <- "Hello World 1"
    time.Sleep(1 * time.Second)
    c <- "Hello World 2"
    time.Sleep(1 * time.Second)
    c <- "Hello World 3"
    time.Sleep(1 * time.Second)
    c <- "Hello World 4"
    time.Sleep(24 * time.Hour)
}

이 코드에서는 main 함수에서 string 타입의 channel을 만듭니다.
pub 함수는 “Hello World” 문자를 publish 합니다.
그리고 sub는 for 무한루프 안에서 channel에 메세지가 도착하면 “Hello World”를 출력합니다.
pub 함수 안에서는 channel에 string을 publish하고,
sub 함수에서는 channel을 통해 string을 consume 합니다.
메세지를 consume해서 처리하는 모듈을 worker라고도 부릅니다.

앞서 publisher, 메세징 시스템, subscriber의 관계를
위의 고언어 코드에서는 pub 함수는 publisher, sub 함수는 subscriber 역할을 합니다.
그리고 이 pub, sub 사이에 있는 string channel 타입의 c 변수를 메세징 시스템으로 생각할 수 있습니다.

만약 고언어처럼 언어에서 메세징을 제공하지 않는다면 프레임워크에서 제공합니다.
자바의 스프링프레임워크를 예를 든다면 ApplicationEvent 타입의 메세지를 발행합니다.
그리고 EventListener annotation을 통해 publish 된 메세지를 subscribe 할 수 있습니다.
이때 메세징 시스템은 스프링 프레임워크가 담당하게 됩니다.
참고로 메세지와 event는 비슷합니다.
둘의 차이는, 메세징은 보통 Queue라고 불리는 저장소에 메세지를 저장해둬서 subscriber가 즉시 처리하지 않아고 나중에 처리가 가능하지만
이벤트는 발생한 순간에 subscriber에게 전달되고 이벤트는 소멸됩니다.

위에서 설명한 것처럼 메세징은 Queue라는 저장소가 있습니다.
그래서 Message Queue라고 부릅니다.

일반적으로 메세징 시스템은 분산환경에서 메세지 전달과 처리를 위해 만들어졌습니다.
그래서 고 언어의 channel이나 스프링프레임워크의 EventListener는 한정된 문맥에서는 메세징 시스템이 아닙니다.
하지만 앞서 이야기한 것이나 뒤에 이야기할 것 모두 event driven architecture를 따르며 넓은 의미로 메세징 시스템이라고 볼 수 있습니다.

그럼 지금부터는 분산 메세징 시스템에서 가장 많이 사용되는 kafka와 rabbitmq 살펴보겠습니다.

먼저 kafka와 rabbitmq는 둘다 분산환경에서 동작하는 메세징 시스템입니다.
또한 message broker 라고 부릅니다.

분산 메세징 시스템들은 일반적으로 확장성, 고가용성, 신뢰성을 제공합니다.

  1. 확장성은 kafka와 rabbitmq는 cluster를 구성할 수 있습니다.
    이를 통해 노드수를 늘려서 수평확장을 할 수 있습니다.
    수평 확장을 통해 처리량을 늘리 수 있습니다.
  2. 고가용성은 일부 노드가 다운되더라도 메세징 시스템은 동작을 할 수 있어야합니다.
  3. 신뢰성은 발행된 메시지를 유실하지 않아야하며 subscriber에게 메세지를 중복으로 보내지 않는등 메세지 전달에 대한 신뢰성 있는 동작을 합니다.

kafka와 rabbitmq 는 앞의 분산 메세지 시스템으로서의 조건을 만족합니다.

그리고 publisher는 한번에 하나의 메세지를 publish하지만 메세징 시스템을 통해 이 메세지는 복제되어 다수의 queue로 전달할 수 있습니다.

예를 들면,

kafka는 topic이란 단위로 메세지를 나눕니다.
그리고 이 topic은 다시 여러개의 partition으로 나뉩니다.
그리고 subscriber쪽은 consumer group을 통해 하나의 메세지를 다수의 worker가 사용할 수 있습니다.

rabbitmq는 exchange와 queue로 구성이 됩니다.
exchange는 어느 queue로 메세지를 전달할지 라우팅을 결정합니다.
rabbitmq는 direct, fanout, topic, headers 이 4가지 타입의 exchange가 있습니다.
다양한 exchange로 메세지를 전달 할 수 있을 뿐만 아니라 exchange에서 exchange로도 라우팅을 할 수 있기에
rabbitmq는 kafka에 비해 메세지 라우팅 설계가 매우 유연하고 편리합니다.

대신 rabbitmq는 queue이기 때문에 queue에서 메세지를 consumer가 dequeue하게 된다면 queue에서 해당 메세지가 제거 됩니다.
반면에 kafka는 저장되는 단위가 queue가 아니라 topic과 partition입니다.
topic은 메세지를 구별하는 논리적인 단위이고 실제로 저장되는 단위는 partition입니다.
그리고 consumer가 partition에서 메세지를 소비해도 queue처럼 메세지를 삭제하지 않습니다.
특정 offset을 기억해두고 offset 이후의 데이터를 subscribe 합니다. 이는 배열과 비슷합니다.

이런 차이에서 rabbitmq는 하나의 queue에 여러개의 subscriber를 붙일 수 있는데, kafka는 하나의 partition에는 하나의 consumer group만 붙일 수 있습니다.

둘의 차이는 저장소에도 차이가 있습니다.
rabbitmq는 주로 메모리에 kafka는 디스크에 저장합니다.

그래서 kafka를 database와 같이 영속 데이터 저장소로 사용합니다.

이러한 동작의 차이는 메세징 시스템이 추구하는 것에 따라 메세징 시스템 설계에 반영되었습니다.
rabbit m q 는 message queueing에 목적이 있습니다.
그래서 kafka에 비해 메세지큐잉에 라우팅과 queue 생성 삭제에 대해 매우 유연합니다.
kafka는 rabbitmq에 비해 대규모 메세지 처리에 목적이 있습니다.
그래서 rabbitmq에 비해 대량의 배치작업에 우월한 성능을 갖고 있습니다.

이상으로 분산 메세징 시스템에 대해 살펴 보았고 rabbitmq와 kafka에 대해 간단히 살펴 보았습니다.
앞으로 코드를 통해 kafka와 rabbitmq에 대해 더 자세히 살펴보겠습니다.

마지막으로 우리가 메세징 시스템을 사용하는 이유를 5가지로 정리했습니다.

  1. 통신: publisher와 subscriber 사이에 데이터를 전달 할 수 있습니다. 이는 다른 언어, 플랫폼, 이기종간에 제약으로부터 자유로워 집니다.
  2. 비동기성: 동기 통신에 비해 비동기 통신은 절차적인 통신에 비해 성능을 높일 수 있습니다.
  3. 고가용성: 분산 메세징 시스템에서는 일부 메세징 시스템이 다운되더라도 성능은 저하되겠지만 정상적으로 동작을 보장합니다.
  4. 재시도: worker가 다운되더라도 메세지는 queue에 남아 있기 때문에 worker가 다시 살아난다면 이전에 실패한 작업 이후를 이어서 할 수 있습니다. 실시간성은 떨어지더라도 데이터에 대한 정합성을 신뢰할 수 있게됩니다.
  5. 느슨한 결합: 직접적인 호출이 아닌 메세징 시스템을 통한 간접적인 호출을 통해 publisher쪽은 메세지 발행 이후 모든 비지니스 로직을 알 필요가 없습니다. 발행까지만 publisher의 역할입니다. 이후는 메세징 시스템을 통해 이 메세지는 subscriber에게 전달됩니다. 이제 메세지 처리에 책임은 subscriber입니다. publisher의 직접 호출이 아닌 간접호출을 통해 얻게되는 느슨한 결합을 통해 우리의 소프트웨어는 유연함과 확장성을 높일 수 있습니다.

구독과 좋아요 알림 설정은 컨텐츠 제작자에게 많은 도움이 됩니다.

감사합니다.