들어가기
알림 시스템을 구축했으니 이제 RabbitMQ가 감당할 수 있는 메세지 양을 측정해보고 Prefetch Size와 Concurrent Consumer의 관계와 해당 값들이 성능에 미치는 영향을 알아보려합니다.
서버 구조
서버 구조는 위처럼 구성돼있습니다. 각 Compute Engine 인스턴스를 모니터링하기 위해 Prometheus, Grafana를 위한 인스턴스를 별도로 구성했습니다.
Grafana, Prometheus를 제외한 Spring Boot, RabbitMQ는 도커로 실행중입니다.
이번 글에서는 RabbitMQ의 지표를 살펴볼 예정입니다.
RabbitMQ 인스턴스 스펙
GCP E2-Small
- vCPU 2
- 코어 1
- RAM 2GB
- Ubuntu 20.04 LTS
K6 Script
부하는 K6를 이용해 생성했습니다.
import http from "k6/http";
import { check, sleep } from "k6";
export const options = {
stages: [
{ duration: "0.m", target: 100 },
{ duration: "1m", target: 300 },
{ duration: "1m", target: 500 },
],
};
export default function () {
const headers = {
headers: {
"Content-Type": "application/json",
Authorization: `Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiIxIiwiaWF0IjoxNzM1OTk2Njg0LCJleHAiOjE3MzYwMDM4ODR9.8GhstHS0_yvQ1sjBJGRDZZaLIc5Kf7I7BGLlBks2qelTqFqBmWUIHNqaZVeyGRUGGOf5VXMvRS5yULDBWd07ug`,
},
};
const payload = JSON.stringify({ minute: 15 });
const res = http.post(
"https://amorgakco.store/api/group-participants/groups/1/tardiness",
payload,
headers
);
check(res, {
"status is 200": (r) => r.status === 200,
});
sleep(Math.random() * 2); // 요청 간격
}
30초 동안 100명의 가상유저, 그 후 1분동안 300명까지 증가, 그 후 1분동안 500명까지 증가시킵니다.
각 요청은 0~2초 사이의 랜덤한 대기를 거치게됩니다.
요청하는 API는 모각코에 지각을 요청하는 api입니다. RabbitMQ로 알림을 전송하고 Consumer가 FCM을 통해 WebPush를 전송합니다.
Prefetch와 Concurrent Consumer
먼저 Prefetch는 컨슈머가 한 번에 메모리에 올려놓고 처리할 메세지들의 개수입니다.
Prefetch가 250이라면 하나의 컨슈머는 큐에서 250개의 메세지를 몽땅 들고와서 메모리에 올려놓고 하나씩 처리하게 됩니다.
Concurrent Consumer는 컨슈머 병렬성을 뜻하는데요. 멀티스레드를 이용해서 동시에 Consume 할 수 있습니다.
멀티 스레드환경이 항상 그렇지만 스레드가 과도하게 많으면 CPU Context Switching 관점에서도 좋지 못하기 때문에 성능은 오히려 떨어질 수 있습니다. 그렇기 때문에 Concurrent Consumer 설정은 하드웨어 스펙과 서비스에서 감당해야할 트래픽을 기준으로 테스트를 통해 결정해야합니다.
RabbitMQ Performance Measurements, part 2 | RabbitMQ
Welcome back! Last time we talked about flow control and
www.rabbitmq.com
- Prefetch 값이 너무 작으면(예를들어 1,2) 성능이 떨어진다.
- Prefetch 값이 클 땐 Concurrent Consumer(스레드)를 늘리는 것은 그다지 도움이 되지 않는다.
- Prefetch 값이 작을 땐 Concurrent Consumer를 늘리는 것이 성능에 도움이 된다
- ConcurrentConsumer를 무작정 늘린다해서 성능이 향상되는 것은 아니다.
위 벤치 마크에서 가장 좋은 성능을 내는 Concurrent Consumer 10, Prefetch 50 이 모든 상황에서 가장 좋은 성능을 보이는 것은 아닙니다.
서버의 하드웨어 스펙, 트래픽, 메세지의 크기, 메세지 처리 시간 등 여러가지를 고려하고 테스트를 통해 결정해야 할 것으로 보입니다.
Concurrent Consumer 1 , Prefetch 5
Consumer는 1개, Prefetch Size는 5로 설정돼 있습니다.
결과
- Consumer 초당 10.1개 메세지 처리 (최고 처리량)
들어오는 메세지 속도(Incoming messages 63.2/s)에 비해 처리하는 양(Outgoing messages 10.1)이 턱 없이 작습니다. 이런 상황이라면 큐에 메세지가 쌓일 것이고 지연은 길어집니다.
QUEUED MESSAGES 지표가 나타내는 것이 큐에 쌓여서 컨슈머에게 처리되길 기다리는 양입니다.(누적치)
INCOMING MESSAGES는 초당 발행되는 메세지의 양입니다. 컨슈머의 처리량과는 무관합니다.
이렇게 메세지가 쌓이는 이유는 FCM 발송에 소요되는 시간(129ms)이 길기 때문에 하나의 컨슈머가 초당 처리할 수 있는 메세지양이 작다고 유추할 수 있습니다.
따라서 뒤이은 테스트에서 컨슈머는 고정시키고 Prefetch 값을 늘려 보겠지만 처리량이 변하지 않을 것이라 추측됩니다.
Concurrent Consumer 1 , Prefetch 20
이번엔 Prefetch를 20으로 설정했습니다.
결과
예상한대로 처리량은 늘지 않았고 메세지 발행이 끝난 뒤에도 쌓여있는 메세지를 처리하고 있습니다.
Concurrent Consumer 1 , Prefetch 250 (Spring AMQP Default)
Prefetch Size는 기본값 250으로 설정돼 있습니다.
결과
- Consumer 초당 10.2개 메세지 처리 (최고 처리량 기준)
역시 마찬가지 입니다. 컨슈머 처리량 자체가 작기때문에 Prefetch 값은 영향이 없습니다. 더 이상 하나의 컨슈머에 Prefetch값을 조정하는 것은 의미가 없어 보입니다.
Concurrent Consumer 2 , Prefetch 5
2개의 Consumer가 실행중입니다.
결과
- Consumer 초당 20개 메세지 처리 (최고 처리량 기준)
Peak Time에 큐에 쌓인 메세지 양이 기존엔 10K에서 8K로 감소했습니다.
CPU 사용량
Concurrent Consumer 3 , Prefetch 5
3개의 Consumer가 실행중입니다.
- Consumer 초당 31.4개 메세지 처리 (최고 처리량 기준)
Peak Time에 큐에 쌓인 메세지 양이 기존엔 직전 테스트 8K에서 6K로 감소하고 쌓여있던 메세지를 처리하는 시간 자체도 줄어들었습니다.
지금보니 컨슈머 1개를 추가할 때 마다 10만큼의 처리량이 늘어나는 것으로 보입니다. FCM 전송 자체가 빨라지지 않는 한 하나의 컨슈머가 처리할 수 있는 처리량이 변하지 않을 것으로 보입니다.
CPU 사용량
Concurrent Consumer 10 , Prefetch 5
- Consumer 초당 85.5개 메세지 처리 (최고 처리량 기준)
처음으로 최대치의 요청을 모두 즉각적으로 처리해내고 있습니다. 큐에 쌓여있는 양은 아예 존재하지 않습니다.
CPU 사용량
Concurrent Consumer가 3일때 비해 약 5%가량 CPU를 더 소모해 Peak Time에 22%가량 점유하고 있습니다.
Concurrent Consumer 50 , Prefetch 5
50개의 컨슈머도 10개일 때와 처리량은 같습니다. 이 테스트의 경우 CPU 사용량만 올릴 뿐이지 들어오는 메세지에 비해 과분한 스레드 설정입니다.
결론
위 테스트에선 FCM만 테스트했지만 현재 모각코 서비스에선 SMS까지 전송해야합니다. FCM에만 서버 자원을 몰아 줄 수 없기 때문에
위 테스트를 바탕으로 Concurrent Consumer를 평상시 6개, Max 10개로 설정하고 Prefetch 값은 5로 설정하기로 결정했습니다.
@Bean
public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory(
ConnectionFactory connectionFactory) {
final SimpleRabbitListenerContainerFactory factory =
new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setConcurrentConsumers(6);
factory.setMaxConcurrentConsumers(10);
factory.setPrefetchCount(5);
return factory;
}
'Spring' 카테고리의 다른 글
Hibernate ORM User Guide 오픈소스 기여 (0) | 2024.12.30 |
---|---|
동시성 문제 해결하기 (3) | 2024.10.26 |
정렬 알고리즘 [선택정렬, 버블정렬, 삽입정렬] (0) | 2024.09.27 |
Spring OAuth2 Client 흐름 개선시키기 (0) | 2024.09.14 |
Google S2 (3) | 2024.09.11 |