패스트캠퍼스 9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 초격차 패키지 Online. 수강

본 글은 패스트캠퍼스 ‘9개 프로젝트로 경험하는 대용량 트래픽 & 데이터 처리 초격차 패키지 Online.’ 강의 기반으로 작성 되었습니다.

링크: https://fastcampus.co.kr/dev_online_traffic_data

패스트캠퍼스 대용량 트래픽
패스트캠퍼스 대용량 트래픽

이번에 대용량 트래픽 관련해서 공부가 필요해 마침 패스트 캠퍼스에 좋은 강의가 나와 당장 구입하고 공부 하게 되었습니다.

강의를 듣고 노하우를 100% 흡수 하기 위해 직접 나만의 방식으로 한번 만들어 보았는데요. 이를 진행하면 한번 리뷰 남겨보겠습니다.

예제 소스 코드

이번 블로그에 사용되는 코드는 아래 링크 통해 확인 할 수 있습니다.

https://github.com/syh8088/high_volume_traffic/tree/main/accessor_queuing_system

대용량 트래픽 - 접근자 대기열 시스템 만들기

안녕하세요. 이번에 접근자 대기열 시스템을 한번 만들어 보도록 하겠습니다. 접근자 대기열을 왜 필요한가? 여러가지 이유가 있겠지만 대표적으로

짧은 시간내에 대량의 트래픽이 요청 하게 된다면 해당 서버는 부하에 대해 재성능을 발휘하지 못하게 될것 입니다.

이러한 대규모 트래픽에 대한 부하를 극복하기 위함인데요. 대용량 트래픽 처리 방법에 대해 여러가지 있겠습니다. 서버간의 수평적 확장 및 로드 벨런싱 기술 도입

그리고 자주 사용되는 데이터에 I/O 최소화 하기 위한 캐싱 기술 등 여러가지가 있겠습니다. 그리고 요구사항에 따른 적절한 방법을 선택해 극복 할 수 있겠습니다.

여기서는 접근자 대기열을 통해 대용량 트래픽을 극복하는 방법에 대해 알아보도록 하겠습니다.

접근자 대기열 시스템이란?

대규모 트래픽 예시

단일 서버로 클라이언트로 부터 대규모 트래픽을 받게 된다면 해당 서버는 치명적일 수 있습니다.

대규모 트래픽 예시

이렇게 중간에 대기열 Queue 을 도입하면 어떻게 될까요? 대규모로 트래픽을 요청 받더라도 동시에 처리 할수 있는 요청수를 제한하고 초과된 요청은 대기열에

저장하므로써 순차적으로 처리 (FIFO) 해서 해당 서버의 부하를 최소화 할 수 있습니다. 특히 대기열 시스템은 대규모 트래픽이 예측이 가능하고
짧은 시간내에 대규모 트래픽 요청이 들어올때 효과적 이라고 볼 수 있겠습니다.

일반적인 서버 구성 및 성능 테스트 해보기

일반적인 서버 아키텍처 시퀀스 다이어그램

시스템 시나리오는 client 가 main 페이지를 접속하게 된다면 Spring Mvc 전용 서버에서 Mysql Server 로 공지사항 관련 데이터를 가져오도록 합니다.

가져온 데이터를 가공해 client 에게 응답하는 시나리오 입니다.

1
2
3
4
5
6
7
8
9
10
11
public List<NoticeResponse> selectNotices() {
List<Notice> noticeList = noticeRepository.findAll();

try {
Thread.sleep(100);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}

return NoticeResponse.getInstance(noticeList);
}

조금 더 현실적인 성능 TEST 하기 위해 데이터베이스 서버로 부터 공지사항 데이터 가져오면 “Thread.sleep(100)” 통해 지연을 해볼까 합니다.

일반적인 서버 ngrinder TEST

  • 평균 TPS : 80.8
  • Peek TPS : 94.5
  • Mean Test Time : 11,121 ms
  • Exected Tests : 4064

한번 nGrinder 통해 성능 TEST 해보았는데요. 이렇게 갑작스러운 대규모 트래픽 요청이 들어 올때 만족스러운 성능은 나타나지 않았습니다.

일반적으로 Spring Boot 서버 띄울때 Thread Pool Size 는 100개 입니다. 갑작스럽게 100개 이상 대규모 Thread 가 인입 된다면 각각의 Thread 가

DB 서버로 공지사항 데이터를 가져오기 위해 I/O 발생하게 됩니다. 특히 일반적인 서비스 경우 데이터베이스와 통신을 위해 많은 I/O 많다고 볼수 있는데요.

spring MVC

초기 Client 로부터 요청이 들어오면 이미지 내 보시면 Task 통해 Spring MVC Tomcat 서버 내에 Queue 에 Push 하게 됩니다.

Thread Pool 에서 Queue 에 있는 Task 를 POP 해서 처리 하는 구조를 갖고 있습니다.

미리 만들어진 Thread 를 만들어서 대규모로 발생하는 트래픽을 처리 할 수 있도록 구성 되어 있는데요. 이것은 Client 로 부터 요청이 들어 올때마다

Thread 를 만들어서 처리하는 과정은 비싼 비용이 발생 되기 때문 입니다.

하지만 이러한 구조로 대비 했는데 더 많은 요청이 들어온다면 어떻게 될까요? 앞서 nGrinder 통해 성능 TEST 지표를 자세히 확인 해보면 Error 가 발생

된 부분이 있습니다. 이것은 Spring MVC 내에 있는 Queue 가 많은 Task 로 인해 꽉 차여 있다면 이후 요청 들어오는 Task 는 대기 상태가 됩니다.

이 대기 상태가 일정 시간 넘어버리면 TimeOutException 이 발생 됩니다.

앞서 사용한 Thread 가 최대한 빨리 사용을 하고 반납 하면 좋을텐데 많은 I/O 가 발생되는 시점에 해당 Thread 는 대기 상태로 이어 지고 이러한 문제가

점점 쌓여 결국은 전체적으로 트래픽을 처리 하지 못 하는 상황까지 오게 된것 입니다.

Spring WebFlux 를 도입 해보자

출처 https://spring.io/reactive

Spring 에서는 ‘Servlet Stack’, ‘Reactive Stack’ 두 가지 웹 스택을 제공 하게 됩니다.

‘Servlet Stack’ 은 Spring MVC 등 구성되어 있습니다.

Spring MVC 에서 ‘thread-per-request’ 모델 기반으로 구성되어 있는데 1개의 Request 당 단 1개의 Thread 를 사용하는 웹 요청 처리 모델 입니다.

이와 같은 방식은 단점이 존재하는데요. 첫번째로는 일반적으로 Blocking 방식으로 처리되고 있기 때문에 I/O 처리 발생시 해당 Thread 는

아무것도 하지 않고 대기 상태에 됩니다. 대기 상태 (Idle) 가 길어지면 길어 질수록 많은 요청이 불가능 하고 병목 현상이 발생 될 수 있습니다.

즉 대규모 트래픽 요청이 올때 각각의 Thread 의 작업 단위는 요청이 완료 되어야 다른 task 가 그 다음 처리를 할 수가 있어서 이를 극복하기 위해 추가적인 Thread 가 필요하는 점 입니다.

Thread pool 을 늘리는 방법으로 대신 Core 수에 대비 너무 많은 Thread pool 를 구성하게 된다면 Context Swiching 빈도수가 증가 하게 되면서 성능 문제가 발생 될 수 있습니다.

이에 반해 ‘Reactive stack’ 경우는 Non Blocking 동작으로 구성 되어 있어 대규모 트래픽을 동시에 처리 할 수 있어 적절한 동작이라고 볼 수 있겠습니다.

Spring Webflux 는 대량의 동시 커넥션이 가능한 Non-Blocking 웹 프레임워크 이라고 보면 되는데 여기서 가장 중요하다고 강조한 Netty 에 대해 자세히 알아봅시다. 한마리로 핵심은 Event Loop 입니다.

Event Loop

Event Loop 란 동시성 처리를 위한 싱글 스레드 기반 스케쥴러 입니다.

요청이 들어오면 동시성 처리를 위해 Event Queue 에 적재 되고 Event Loop 는 반복해서 Event Queue 에 꺼내서 그에 따른 Event handler 에 실행을 과정을 반복 하게 됩니다.

Spring WebFlux 를 도입 해서 부하 TEST 해보자

1
2
3
4
5
6
7
8
public Flux<NoticeResponse> selectNotices() {

Flux<Notice> noticeFlux = noticeR2DbcRepository.findAll();
return noticeFlux.flatMap(notice -> {
NoticeResponse instance = NoticeResponse.getInstance(notice);
return Flux.just(instance);
}).delayElements(Duration.ofMillis(100L));
}

이전 MVC 코드와 비슷하게 모든 공지사항 글을 조회해서 100 millis 정도 지연 해서 응답 하도록 구현 하였습니다.
delayElements 기능은 최소 delay 만큼 간격을 두고 onNext 이벤트 발행 하게 됩니다. onNext 이벤트가 발행된 후 더 늦게 다음 onNext 이벤트가 전달되면 즉시 전파
즉, 이를 사용하면 처리량을 제한 할 수 있습니다.

Spring Webflux 부하 테스트

  • 평균 TPS : { 80.8 } → { 628.4 } 약 8배 개선
  • Peek TPS : { 94.5 } → { 999.5 }
  • Mean Test Time : { 11,121 } ms → { 1,514 } ms 약 7배 단축
  • Exected Tests : { 4064 } → { 32,178 } 약 8배 실행

접속자 대기열 시스템 도입

앞써 이야기 한대로 접속자 대기열 시스템이란 많은 요청에 대해 접속자 대기열을 만들어서 요청 접근을 조절하는 시스템 입니다.

접속자 대기열 시스템 Architecture

Spring Webflux 부하 테스트

기존에는 Main Server 만 있었지만 이번에 Accessor Queuing Server 가 구축 되어 있습니다. 해당 서버 역활은 접속자 대기열 생성 및 관리 역활을 맡은 서버 입니다.

Client 가 Main Server 로부터 요청이 들어오면 Main Server 는 해당 사용자가 접근이 가능한지 Accessor Queuing Server 로 요청 하게 됩니다.

바로 접근이 불가능 한다면 Accessor Queuing Server 로 해당 사용자를 대기열에 추가 하도록 합니다. 정해진 일정 시간 마다 해당 순차적으로 대기열에 있는 task 를 POP 하게 되고

Waiting 대기열에 해당 사용자를 POP 시키고 Proceed Queue 에 Push 하도록 합니다. 그런 후 프론트에서 Main Server 로 접근 해서 공지사항 데이터를 출력 하도록 합니다.

이때 대기열 기능을 구현하기 위해 Redis 의 SADD 명령어인 Sorted Set 기능 통해 구현 했습니다. Redis 로 선택한 이유는 각각의 서버 마다 스케일 아웃 통해 확장시

단일 대기열 큐로 관리하기 위함 입니다.

접속자 대기열 시스템 Sequence Diagram

Spring Webflux 부하 테스트

  • (1) Client → Main Server: Client 가 Main 페이지를 접속 합니다.
  • (2) Main Server → AccessorQueuing Server: MainServer 에서는 AccessorQueuingServer 로 “/api/accessor-queuing/allowed-user” API 호출 하므로써 해당 접속자가 접근이 가능한지 판단 합니다.
  • (3) Main Server → AccessorQueuing Server(Front): 만약 접근이 불가능 한다면 “redirect:/waiting-room” 페이지를 접속 하도록 합니다.
  • (4) AccessorQueuing Server → Redis Server: 해당 접근자는 요청 받은 idempotencyKey (멱등성 키값) 값 이용해 Redis Server 에 “ZADD {waitKey} UnixTime idempotencyKey” 명령어를 이용해서 대기열 Queue 에 저장 하도록 합니다.
  • (5) AccessorQueuing Server → Redis Server: Redis Server 에 “ZRANK {접근자 대기열 Key값} idempotencyKey” 명령어를 이용해 해당 사용자의 대기열 순번 가져오도록 합니다.
  • (6) AccessorQueuing Server → Redis Server: Main 페이지에 접근이 가능하도록 대기열에 POP 해서 접근 하도록 합니다. 이때 Redis Server 에 사용되는 명령어는 “ZPOPMIN 100 ZPOPMIN {waitKey} 100” 입니다. 정해진 시간 간격으로 반복 하면서 실행 됩니다.
  • (7) AccessorQueuing Server → Redis Server: 대기열에 POP 통해 빠져나온 task 는 “ZADD {ProceedKey} UnixTime idempotencyKey” 이용해 “ProceedKey” 로 구성된 대기열에 저장 하도록 합니다.
  • (8) AccessorQueuing Server(Front) → AccessorQueuing Server: 프론트 서버는 정해진 일정 시간이 지나면 스케쥴링 기능 통해 ‘/api/accessor-queuing/rank’ API 호출 통해 순번 데이터가 존재하지 않을시 즉 waitingQueue 에 데이터가 존재하지 않으면 ‘/api/accessor-queuing/touch’ API 호출 해서 대기열 이탈 방지 하기 위한 토큰 생성 합니다.
  • (9) AccessorQueuing Server → Main Server: Main 페이지에 접근 하도록 합니다.

접속자 대기열 시스템 이용해 부하 TEST 해보자

접속자 대기열 시스템 부하 테스트

  • 평균 TPS : { 628.4 } → { 2059.2 } 약 3배 개선
  • Peek TPS : { 999.5 } → { 6035.0 }
  • Mean Test Time : { 1,514 } ms → { 462 } 약 3배 단축
  • Exected Tests : { 32,178 } → { 105,602 } 약 3배 실행

‘본 게시물은 패스트캠퍼스 수강 후기 이벤트 참여를 위해 작성되었습니다’


Copyright 201- syh8088. 무단 전재 및 재배포 금지. 출처 표기 시 인용 가능.

💰

×

Help us with donation