본문 바로가기
Project

[Project] 인스턴스 서버에 너무 많은 부하가 쏠리다면?

by 육빔 2025. 4. 17.
728x90

지금부터 우리 서비스의 아키텍쳐를 우선 설명하겟다.

 

초기 아키텍쳐고 서버에선 jwt을 엑세스토큰과 리프레쉬토큰로 검증해서 사용하고 있는데 문제점이 발생했다.

토큰이 유효한지 확인하려면 아래와 같이 계속해서 데이터베이스를 조회하는 과정이 있었다.

 

이렇게 계속해서 데이터베이스를 거치는 과정은 비용이 클 뿐만 아니라 사용자에게 안좋은 영향을 끼치고 있었다.

이러한 문제점을 데이터베이스를 거치는 대신 아래와 같이 Redis를 사용하여 서비스 로직을 단축시키는 아이디어 적용해 우리 서비스에 적용할 수 있었다.

 

그래서 그다음 아키텍쳐 구조는 아래와 같이 진화할 수 있었다.

 

 

 

우리 서버에서는 자주 조회되는 데이터에 대해서 로컬캐시로 저장하고 있었다. 캐싱전략은 Cash Aside 전략을 사용해 아래와 같은 흐름으로 동작하고 있었다.

 

 

그래서 최종적으로 아래와 같은 형태의 아키텍쳐가 되었다.

 

이제 여기서 가정을 해보자

 

갑자기 서버의 유저가 폭등하여 현재 TPS를 훨씬~ 넘어서 서버가 요청을 처리하지 못하는 상황에 놓였다고 해보자. 이럴땐 어떻게 해야하는가??

 

보통은 이럴때 보편적인 해결책은 로드벨런서를 앞에 붙여 서버를 스케일 아웃을 진행한다. 스케일 업은 돈이 많이 나가고 한계가 있어서 인프라관점에서는 고려해볼만하지만 우리는 서버개발자이기에 돈을 최적으로 하는 방법을 공부하자 ^.^ 그리고 잠시 로컬 캐시를 이용하면 데이터 일관성을 깨질 수 있고 다양한 전략이 있는데 일단은 주제랑 어긋나서 글로벌 캐시로 컨트롤 하는 가정이라고 하자 ㅎㅎ

 

 

로드벨런서를 적용한 서버의 상태이다. application 서버는 이제 로드벨런서가 있기에 여러대 둘수 있는 구조이다. 그로 인해 부하가 분산될 수 있는 구조다. 하지만 이제 서버의 부하는 분산이 되고 있지만 Redis 캐시의 부하가 상당히 많이 갈 것이다. 이걸 해결하고자 Redis도 여러대를 두어서 분산하게끔 설계를 해야된다. 여기선 방법이 2가지가 있다. 첫번째는 Sentinel, 두번째는 cluster 방법이 있다.

 

 

하지만 위의 문제점은 Redis의 SPOF문제는 해결됐지만 운영의 복잡성이 증가하면서 데이터의 정합성이 깨질수 있는 문제점이 있다. 쓰기는 Master, 읽기는 Slave에서 가능하게 할시 읽기 데이터의 지연이 발생할 수 있다. 하지만 여기서 아까와 마찬가지로 DB의 SPOF를 해결할 수 없기에 데이터베이스 또한 분산할 수 있게 처리를 진행한다. Master-slave 구조로 읽기 / 쓰기를 분리하면 아래와 같은 구조가 된다.

 

 

이제 모든 SPOF가 해결됐다! 하지만 이렇게 되면 우리 시스템에서 고려해야될 부분이 몇개있다.

 

우리 시스템은 세미나실 예약이 있는데 어떤 흐름으로 흘러가게될까?

우선 요청을 날리면 이런식으로 날아갈것이다. 

Client A → PUT /rooms/301/reserve
          { date: "2025-04-20", time: "14:00–16:00", userId: "A" }
           ↓
    Load Balancer → App

 

로드벨런서 -> 어플리케이션 -> 캐시 -> DB

그런데 문제점은 이 비즈니스 로직은 동시성, 데이터의 정합성가 필수로 되야되는 api이다. 그걸 해결하기 위해 현재는 비관적락을 사용하여 룸을 막고 있는 상황이다. 흠.. 그런데 우리의 가정은 현재 너무 많은 사용자가 몰려서 처리를 해야되는 상황인데 Master-slave 구조에서 우리가 비관적락을 걸면 읽기 요청은 원래라면 slave로 가는게 맞지만 select for update 구문은 무조건 Master DB에서 동작한다. 트랜잭션을 시작해서 다른 slave에 뿌려줘야되기에 ㅎㅎ; 그래서 DB Master가 바로 병목 현상이 발생할 것이다. 이럴때 해결책은 데이터베이스를 Master-slave 구조에서 데이터베이스 샤딩을 해 각 쓰기, 읽기 요청을 각 Master의 부하를 줄이는 방식으로 진행할 수 있다.

 

아래 그림에선 1개 Master 2개 Slave 구조로 그렸다. 

 

하지만 아직 락을 하는 경우엔 부하 문제가 해결이 안됐다. 그 이유는
1. 샤드 A에서 락을 검

2. 샤드 B에서 락을 걸기위해 시도

3. 이때 샤드A의 락을 해제하기 전까지 B작업은 대기하게 된다. 그 이유는 

 

 

문제 1: 부분 적용

  • 만약 B 트랜잭션이 샤드 B만 바꾸고 샤드 A가 풀리기 전에 실패하면,
  • 샤드 B만 바뀌고 샤드 A는 그대로 → 데이터 정합성 깨짐

문제 2: 데드락 위험

  • 다른 트랜잭션이 샤드 A만 잡고 있다가, 동시에 샤드 B를 잡으려 한다면,
  • 서로가 상대 락을 풀기 전까지 꼼짝 못 하는 교착 상황(데드락)이 생길 수 있음

 

이러한 이유때문에 대기하게 된다.

 

4. 결국 샤드A트랙잭션이 길어지면 다른 샤드 트랜잭션도 대기를 해 타임아웃이 발생.

즉 샤드마다 트랜잭션을 걸어야되는 상황은 순서, 실패하면 롤백이 상당히 어려워지는 문제가 발생한다.

 

그래서 이런 문제를 해결하기 위해 

 

글로벌 락을 도입할 수 있다!!

 

글로벌 락을 사용하면 중앙에서 글로벌 락을 적용하기 때문에 락이 확보된 상태에서만 샤드 A,B에 순서대로 트랜잭션을 걸고 작업을 수행할 수 있어서 샤딩 상황에서 유리하게 취할 수 있다! 또한 지금 아키텍쳐에서 레디스 클러스터를 사용중이므로 같이 사용할 수 있기에 인프라를 따로 띄우지 않아도 돼서 비용을 감소할 수 있다.

 

추가로 lettuce, redisson 등 다양한 Redlock 알고리즘이 있다.

 

 

이제 우리는 분산환경에서 조회, 데이터 문제를 해결한 것 같다. 이제 아까 우리 서버에서 빼고 생각했던 로컬 캐시를 다시 한 번 생각해보자.

우선 우리는 사실 보통 캐시를 생각할때 로컬캐시(Caffeine Cache, ehCache)같은 것보다 Reddis를 먼저 생각하게 된다. 그 이유는 왤까? 사실 우리가 쓰는 큰 규모의 사이트는 분산환경이 대부분이므로 데이터의 정합성을 맞추기 위해 Redis를 캐시서버로 많이들 사용한다.

 

분산환경에서 캐시를 적용하는 방식은 다음에 이어서 작성하겠다.

728x90