📌 프로세스 상호작용
프로세스들이 서로 상호작용할 때 필요한 두 가지 기본적인 요구사항은
동기화(synchronous)와 통신(communication)이다.
동기화는 상호배제를 보장하기 위해 필요하고,
통신은 프로세스들 간에 정보 교환을 위해 필요하다.
➡️동기화, 통신을 동시에 제공하는 대표적인 방법이 메시지 전달이다.
▪️메시지 전달을 위해 제공되는 가장 기본적인 primitive
send : 프로세스는 전송하려는 정보를 메시지 형태로 만들어 destination(목적 프로세스)로 보낸다.
receive : 프로세스는 receive를 호출하여 source(메시지를 보낸 프로세스)가 보낸 정보를 수신할 수 있다.
📌 설계 이슈
RabbitMQ, ActiveMQ, Kafka의 특징, 장단점은 아래의 메시지 전달 설계 이슈와 관련되어 있다.
메시지 큐 개념과 관련이 깊은 4가지의 설계 이슈에 대해 서술하려고 한다.
➡️ RabbitMQ, ActiveMQ, Kafka가 아래의 설계 이슈 중 어떤 전략을 택했는지 알아보기 위함이다.
1. 동기화
수신자는 송신자가 메시지를 보낸 후에 메세지를 받을 수 있다.
메시지가 도착하지 않으면 수신자는 기다려야 한다.
즉, 두 프로세스 사이의 메시지 통신은 이들 간에 기본적인 동기화(순서 관계)가 이루어짐을 의미한다.
따라서 send, receive가 호출된 이후에 프로세스에 어떤 일이 발생하는지 명시해야 한다.
▪️send (메시지 전송)
프로세스가 send를 호출하면,
1️⃣ 메세지가 전송되기까지 기다릴 수도 있고,
2️⃣ 바로 다른 작업을 할 수도 있다.
- 블로킹 송신: 메세지를 보낼 때 상대가 받을 때까지 기다린다.
- 논블로킹 송신: 메세지를 보낸 후 바로 다른 작업을 한다.
▪️receive (메시지 수신)
receive를 호출한 프로세스는
메세지가 이미 도착했다면 바로 메세지를 받고,
1️⃣ 도착하지 않으면 기다리거나,
2️⃣ 아예 기다리지 않고 다른 작업을 할 수 있다.
- 블로킹 수신: 메시지가 도착할 때까지 기다린다.
- 논블로킹 수신: 메시지가 도착하지 않으면 바로 다른 작업을 한다.
송신 수신이 각각 어떤 유형이냐에 따라 세 가지 조합이 가능하다.
동기화 유형
▪️블로킹 송신 / 블로킹 수신
- 송신자와 수신자 모두 메시지가 전달될 때까지 대기 (랑데뷰)
- 이 방식은 두 프로세스가 서로 밀접하게 연결되어 있어서,
다른 방식보다 더 높은 결합도를 가진다. - 프로세스 간의 긴밀한 동기화를 제공한다.
▪️논블로킹 송신 / 블로킹 수신
- 송신자는 메시지를 보낸 후 기다리지 않고 계속 진행하지만, 수신자는 메시지가 도착할 때까지 기다린다.
- 하나의 송신자가 다수의 수신자에게 빠르게 하나 이상의 메시지를 보낼 수 있도록 해준다.
- 이 방식은 메시지가 도착한 이후에 작업을 시작할 수 있는 프로세스에서 사용한다.
- 요청에 따른 서비스 or 자원을 제공하는 서버처럼 요청이 많을 때 유용하다.
➡️ RabbitMQ가 구현하고 있는 프로토콜과 ActiveMQ의 Queue/Topic 모델 모두
하나의 Producer에 대한 다수의 Consumer에게 메시지를 전달할 수 있다.
▪️논블로킹 송신 / 논블로킹 수신
- 송신자와 수신자 모두 기다리지 않고 각자 일을 계속한다.
- 이 방식은 효율적이지만, 메시지 전송의 제어가 어려워 자원 낭비가 발생할 수 있다.
병행 처리 (concurrent processing)
병행 처리는 여러 작업을 동시에 처리하는 방식이다.
▪️논블로킹 송신
병행 처리에서의 송신은 논블로킹 송신이 자연스럽다.
예를 들어 알림 요청을 보내는 경우, 요청 프로세스는 요청을 전달하고 나서 다른 작업을 바로 진행할 수 있다.
➡️ RabbitMQ에서 요청 프로세스인 Producer가 요청을 메시지 형태로 만들고 Broker로 전달한 후,
다음 작업(메시지 형태로 만들고 Broker 전달)을 반복한다.
그러나 논블로킹 송신은 한 프로세스가 많은 메시지를 반복해서 보내기 때문에
시스템 자원(CPU, 버퍼 등)의 낭비를 가져와 다른 프로세스의 성능이 저하된다.
따라서 너무 많은 메시지를 보내면 시스템 자원이 낭비될 수 있기 때문에, 이를 제어하는 방법이 필요하다.
➡️ RabbitMQ에서 Producer가 Broker에 메시지를 전달하는 것을 담당하면,
프로그래머는 응답 메시지로 메시지 수신 확인 부분 구현을 통해 문제를 해결한다.
▪️블로킹 수신
수신의 경우 보통 메세지를 받은 후에야 작업을 시작하기 때문에 블로킹 수신이 자연스럽다.
일반적으로 메세지 consumer 수신이 필요한 프로세스의 경우 메세지가 수신된 이후 작업을 시작한다.
2. 주소 지정
메세지를 보낼 때 어디로 보내야하는지 결정하는 방식에 대해 알아보자.
프로세스를 명시하는 방법은 크게 직접 주소 지정과 간접 주소 지정으로 구분된다.
▪️직접 주소 지정
직접 주소 지정은 송신자가 수신자를 정확하게 알고 있을 때 사용한다.
수신자 프로세스의 식별자(주소)를 명확하게 지정한다.
이처럼 destination, source를 명확히 알 수 있는 경우 명시적으로 지정한다.
하지만 수신자가 불확실한 경우 암묵적으로 지정되거나, 수신자가 나중에 메시지를 확인하게 될 수 있다.
▪️간접 주소 지정
송신자가 수신자에게 메세지를 직접 수신자에게 보내는 것이 아니라,
잠시 메시지를 보관하는 공유 큐(메일 박스)로 보내는 방식이다.
두 프로세스가 통신을 할 때, 송신자 프로세스는 큐에 메세지를 넣고
수신자 프로세스는 큐에서 메세지를 꺼내는 방식으로 메세지를 받는다.
➡️ AMQP 프로토콜(RabbitMQ가 구현하고 있는 프로토콜)의 구조 참고
(송신자가 메세지를 큐에 보내고, 여러 수신자들이 그 큐에서 메세지를 꺼내는 방식)
✅ 간접 주소 지정의 장점
송신자와 수신자를 분리를 통해 메시지 전달이 유연해진다.
송신자는 수신자의 상태를 몰라도 되고, 수신자는 메시지가 언제 도착할지 몰라도 되기 때문이다.
RabbitMQ, ActiveMQ와 같은 메세지 큐 시스템이 이런 장점을 가지고 있다.
✅ 간접 주소 지정에서 송신자와 수신자의 관계
일대일 (One-to-One) : 하나의 송신자가 하나의 수신자에게 메세지를 보내는 경우
- 두 개의 프로세스만 연결되는 개인 통신로
- 큐는 해당 프로세스에게 정적이고 영구적으로 연결
다대일 (Many-to-One) : 여러 송신자가 하나의 수신자에게 메시지를 보내는 경우
- 송신자는 여러 개일 수 있고 수신자는 하나의 프로세스인 서버/클라이언트 모델에 적합함
- 이때 메일 박스는 포트라고 함
- 생성된 포트는 특정 프로세스와 정적인 관계
일대다 (One-to-Many) : 하나의 송신자가 여러 수신자에게 메세지를 보내는 경우
- 하나의 송신자 서버가 다수의 수신자 프로세스 집합에 메시지나 정보를 방송하는 경우 유용
- 이때 메세지는 여러 수신자의 메일박스에 복제되어 저장될 수 있음
다대다 (Many-to-Many) : 여러 송신자가 여러 수신자에게 메세지를 보내는 경우
- 여러 개의 서버 프로세스가 동시에 여러 클라이언트에게 서비스를 제공할 때 사용
- 여러 송신자인 경우 송신자와 메일박스의 관계는 동적으로 연결되거나 해제
- 동적으로 프로세스와 메일박스를 연결하고 해제할 때 사용하는 primitive가 connect, disconnect
✅ 메일박스의 소유권
송신자와 수신자의 관계, 프로세스와 메일박스의 관계는 메일박스의 소유권과 연관되어 있다.
메일박스의 소유자는 그 큐를 삭제하거나 관리할 수 있는 권한을 가진다.
다대일에서 포트(메일박스)의 경우, 수신자가 생성하므로 결국 메일박스의 소유권도 수신자가 가진다.
따라서 수신자 프로세스가 종료되면 포트도 해제된다.
일반적인 메일박스의 경우 운영체제가 관리할 수 있다.
이때 메일박스의 소유권은 생성 프로세스에게 주어지거나 운영체제에게 주어진다.
생성 프로세스가 소유권을 가지는 경우, 프로세스가 종료될 때 메일박스도 소멸된다.
운영체제가 소유권을 가지는 경우, 메일박스를 소멸시키기 위한 명시적인 명령어가 제공된다.
➡️ RabbitMQ에서는 큐가 운영체제의 소유일 때, 시스템 종료 후 큐 내용이 삭제되는 문제(손실 위험)가 발생할 수 있다. RabbitMQ의 단점인 메시지 큐 서버 종료 후 재가동 시 큐 내용을 모두 삭제하는 손실 위험성이 여기에서 발생함을 짐작할 수 있다. 이는 메일박스의 소유권이 어떻게 관리되는지와 관련이 있다.
3. 메세지 형식
메세지 형식은 메세지 전달 시스템의 목적에 따라 다르고,
메세지 전달이 단일 컴퓨터인지 분산 시스템인지 따라 다르다.
유연하게 메세지를 작성하는 방법은 가변 길이 메세지를 이용하는 것이다.
메시지의 내용이 얼마나 길어질지 미리 예측할 수 없기 때문에,
길이를 동적으로 조정할 수 있는 구조를 사용하는 것이 좋다.
- 헤더 : 메세지에 대한 정보
- 몸체 : 메세지의 실제 내용
스프링과 RabbitMQ에서의 메세지 형식
스프링은 RabbitMQ와의 통합을 위해 여러 가지 도구와 설정을 제공한다.
메세지를 발생시키는 메서드는 헤더와 몸체를 어떻게 구성하는지 명시적으로 지정할 수 있다.
예를 들어, 스프링에서 메세지를 보낼 때는 메세지 객체를 만들고,
그 안에 헤더와 본문을 설정한 후 RabbitMQ로 보낸다.
이 과정에서 메세지의 헤더에는 라우팅 정보나 타임스탬프 등의 메타데이터가 담기고,
몸체에는 실제 데이터가 포함된다.
4. 큐잉 정책
가장 간단한 큐잉 정책은 FIFO이다.
긴급 메시지 지원이 필요한 경우, 메시지 우선순위를 명시할 수 있다.
➡️ RabbitMQ는 큐 / 우선순위 큐를 모두 생성할 수 있는 기능을 제공한다.
우선순위 큐를 사용하는 경우 메시지를 입력할 때 priority 옵션을 주면 된다.
📌 설계 이슈 키워드 정리
동기화
#블로킹 #논블로킹 #도착 여부 확인
주소 지정
#직접 송수신 #명시적 #암묵적
#간접 송수신 #정적 #동적 #소유관계
메세지 형식
#내용 #고정 #가변
큐잉방식
#선입선출(FIFO) #우선순위
📌 메세지 큐 시스템이 사용한 전략
▪️RabbitMQ 전략
송신 : 논블로킹
수신 : 블로킹 basic.get / 논블로킹 basic.consume (선택 가능)
간접 주소 지정:
송신자는 Exchange에 메시지를 보내고, Queue와의 라우팅은 Exchange가 관리
메시지는 Exchange를 통해 라우팅되며, Direct, Fanout, Topic, Headers와 같은 라우팅 방식을 사용
- Direct Exchange: 메시지를 특정 큐로 전달 (다대일)
➡️ 라우팅 키와 일치하는 하나의 큐로 메시지가 전달됨. 여러 프로듀서가 하나의 큐에 메시지를 보냄. - Fanout Exchange: 메시지를 모든 바인딩된 큐로 전달 (일대다)
➡️ 하나의 프로듀서가 메시지를 보내면 모든 바인딩된 큐에 메시지가 전달됨. - Topic Exchange: 메시지를 주제 패턴에 맞는 큐로 전달 (다대다)
➡️ 라우팅 키 패턴에 맞는 여러 큐로 메시지가 전달됨. 와일드카드를 사용하여 다수 큐로 전달 가능. - Headers Exchange: 메시지 헤더를 기준으로 큐에 라우팅 (다대다)
➡️ 메시지의 헤더를 기준으로 여러 큐로 메시지가 전달됨. 여러 큐가 동일한 헤더 조건에 매핑될 수 있음.
▪️Kafka 전략
송신 : 논블로킹
수신 : 논블로킹 poll()
(기본적으로 논블로킹 방식으로 설계되어 있어 처리량 높고 지연 시간 낮음)
간접 주소 지정:
Producer는 메시지를 특정 Topic에 발행하고, Consumer는 그 Topic을 구독하여 메시지를 받음
- 다대다(Producer → Consumer Group)
➡️ 메시지가 Consumer 그룹에 의해 소비되며, 각 Consumer는 해당 그룹 내에서 특정 파티션을 처리
이렇게 해서 하나의 Consumer 그룹 내에서 다수의 Consumer가 병렬로 데이터를 처리할 수 있음 - 메일박스 소유권
➡️ Consumer 그룹이 관리
각 Consumer는 자신에게 할당된 파티션을 처리하며, Consumer 그룹 내에서 메시지는 중복 처리되지 않음
메시지 소유권은 Offset으로 관리되며, 각 소비자는 자신이 읽은 마지막 메시지의 오프셋을 기록하여 중복 없이 데이터를 처리
▪️ActiveMQ 전략
송신: 블로킹
수신: 블로킹 receive() / 논블로킹 onMessage() (선택 가능)
둘 다 지원하나 기본적으로 블로킹 처리 방식이 많이 사용
간접 주소 지정:
메시지 소유권을 Consumer가 가지며, Consumer Group을 통한 메시지 분배 방식을 지원
Destination(Queue 또는 Topic)에 발행. 송신자는 메시지를 지정된 Queue나 Topic에 보내고, 소비자는 이를 수신.
큐나 토픽은 메시지를 전달하는 간접적 경로로 동작.
- Queue (다대일): 여러 생산자가 하나의 큐에 메시지를 보낼 수 있지만, 각 메시지는 하나의 소비자만 처리.
- Topic (일대다): 하나의 메시지가 여러 소비자에게 전달.
- 다대다 모델: 여러 소비자가 여러 Topic을 구독하고 처리 가능
- 일대일 모델: 큐나 토픽을 사용하는 대신, 한 송신자와 한 수신자가 메시지를 주고받는 일대일 통신 구현.
메일박스 소유권: 메시지는 Queue에 저장되고, Consumer가 이를 처리.
➡️ 소비자는 메시지를 Ack(acknowledgement)로 처리 완료를 알리며, 큐에 있는 메시지는 소유권을 Consumer가 가짐.
📌 설계 이슈 정리 및 비교
설계 이슈 | RabbitMQ | Kafka | ActiveMQ |
1. 동기화 | 송신: 논블로킹 수신: 블로킹/논블로킹 |
송신: 논블로킹 수신: 논블로킹 |
송신: 블로킹 수신: 블로킹/논블로킹 |
2. 주소 지정 | 간접 주소 지정(Exchange → Queue) 다대일, 일대다, 다대다 지원 |
간접 주소 지정(Topic) 다대다 지원 (Producer → Consumer Group) |
간접 주소 지정(Queue 또는 Topic) 다대일, 일대다 지원 |
3. 메시지 형식 | 자유로운 형식 (Text, JSON, XML, 바이너리 등) | 바이트 배열 형식 (직렬화 가능: Avro, Protobuf 등) | JMS 메시지 표준 (TextMessage, ObjectMessage, BytesMessage 등) |
4. 큐잉 정책 | 메시지 TTL, 우선순위 큐, DLX(Dead Letter Exchange), 내구성, 메시지 보장 | 메시지 보존 기간(TTL), 로그 압축(Log Compaction), 오프셋 관리 | 메시지 TTL, 우선순위, 재전송 정책, DLQ(Dead Letter Queue), 내구성 |
기본 메시징 시스템(rabbitMQ, ActiveMQ)에서는 브로커(Broker)가 컨슈머(consumer)에게 메시지를 push해 주는 방식이다.
그런데 카프카는 컨슈머(Consumer)가 브로커(Broker)로부터 메시지를 직접 가져가는 PULL 방식으로 동작한다.
그렇기 때문에 컨슈머는 자신의 처리 능력만큼의 메시지만 가져와 최적의 성능을 낼 수 있다.
대용량처리에 특화 되었다는 것은 이러한 구조로 설계가 되어 가능한 것으로 보인다.
'Backend' 카테고리의 다른 글
[MQ] RabbitMQ, Kafka, ActiveMQ 비교 (3) (0) | 2024.12.13 |
---|---|
[MQ] Message Queue 관련 기본 개념 정리 (2) (0) | 2024.12.13 |