[식구하자_MSA] API Gateway 제대로 쓰고 있는 거 맞아? - 1편: Rate Limiter를 통한 안정화

이민우·2024년 8월 7일
5

🍀 식구하자_MSA

목록 보기
19/21

배경


[이전 포스팅]에서 언급했듯이, 식구하자 프로젝트에서는 Spring Cloud Gateway(SCG)를 API Gateway로 사용하고 있습니다. 이 게이트웨이는 주로 JWT 토큰 유효성 검증과 라우팅 기능을 담당하고 있습니다.

프로젝트 초기 설계 단계에서 SCG 도입은 깊은 고민 끝에 이루어진 선택이 아니었습니다. MSA 환경에서 Spring Cloud를 사용하면서 자연스럽게 선택하게 된 것이었습니다. Gateway를 생각 없이? 남들이 쓰니까 써야지 하고 서버의 장애 대응을 하지 않으면 전체 서비스 장애로 이어질 수 있습니다. 이는 MSA의 장점을 무색하게 만들 수 있음을 유념해야 합니다.

그러나 프로젝트 마무리 단계에 이르러 서버 아키텍처의 장애 대응 능력을 개선하는 과정에서 몇 가지 우려 사항이 있었습니다….

  1. 병목 현상 발생 가능성
  2. 전체적인 응답 시간 증가
  3. API Gateway 장애 시 전체 서비스 중단 위험

이러한 문제들을 해결하기 위한 가장 이상적인 방법은 게이트웨이 서버를 이중화 또는 삼중화하는 것입니다. 그러나 MSA에서 여러 대의 AWS 서버를 쓰고 있어서 과금이 많이 되는 입장에서 현실적으로 추가적인 인프라 비용 부담이 크기 때문에, 이는 현재 상황에서 적용하기 어려운 대안이였습니다,,,,

따라서 Spring Cloud Gateway를 사용하면서도 더욱 안정적인 서비스를 제공하기 위해 다음 두 가지 방법을 구현하기로 결정했습니다:

  1. Rate Limiter
  2. Circuit Breaker를 통한 장애 전파 방지

이 두 가지 주제에 대해 총 2편의 포스팅으로 나누어 다룰 예정이며, 본 포스팅에서는 Rate Limiter의 구현 과정에 대해 자세히 설명하고자 합니다!!

🤨 Rate Limiter가 뭔데!

🧭 Rate Limiter(처리율 제한 장치란 ?)

처리율 제한 장비(Rate Limiter)는 클라이언트가 보내는 트래픽의 처리율(Rate)을 제어하기 위한 장치다.
일반적으로 정의된 임계치(Threshold)를 넘어가면 추가로 들어온 모든 호출은 처리를 중단한다.
ex) 사용자 는 초당 2회 이상 새 글을 올릴 수 없다.
처리율 제한 장치를 사용할 때의 장점은

  • Dos 공격에 의한 자원 고갈 방지
  • 비용 절감
  • 서버 과부하를 방지
<대규모 시스템 설계 기초> "4장 처리율 제한 장치" 참조

limiter가 왜 필요할까에 대한 이유는, 정말 간단하게 생각해 보면 10초 이상 걸리는 api가 있을 때 client들이 짧은 시간에 API를 무분별하게 요청하게 되면 저희의 서버는 끔찍한 결말을 맞게 될 것이고 이는 서비스 전체의 장애를 일으킬 것입니다.,,

이를 방지하기 위해 처리율 제한 장치를 사용하는 것입니다!!

🤔 어떻게 Limiter를 만들건데!


다행히도 우리 복덩이 Spring Cloud Gateway에서는 이미 Request Rate Limiting 기능을 기본적으로 제공하고 있습니다. 이는 우리가 별도의 복잡한 구현 없이도 효과적인 Rate Limiter를 사용할 수 있습니다!!

Spring Gateway에서는 처리율 제한 알고리즘을 토큰 버킷(token bucket) 알고리즘을 사용중 입니다. 구현하는 방법을 보기 앞서, 토큰 버킷 알고리즘이 무엇인지 간단히 알아보도록 하겠습니다.

🪣 토큰 버킷 알고리즘

[동작 원리]

토큰 버킷은 지정된 용량을 갖는 컨테이너입니다. 이 버킷에는 사전 설정된 양의 토큰이 주기적으로 채워지는데 꽉차면 더 이상의 토큰은 추가되지 않습니다.

아래 그림은 용량이 10인 버킷이고, 토큰 공급기는 버킷에 매초 1개의 토큰을 추가합니다


각 요청은 처리될 때마다 하나의 토큰을 사용하고 요청이 도착하면 버킷에 충분한 토큰이 있는지 검사를 먼저 합니다
충분한 토큰이 있으면 버킷에서 토큰을 하나 꺼내 요청을 마이크로서비스들에게 전달하고 충분한 토큰이 없으면, 해당 요청은 버려집니다!

✅ Spring Gateway Limiter 구현


기본적으로 Spring Gateway는 Limiter를 Redislua를 사용하여 구현을 해놔서 저희는 이걸 이용하기만 하면 됩나다.👍 나이스
위에 사진 3번 빨간색 글씨에 해당하는 부분(Limiter)를 구현해보도록 하겠습니다.

🤔 좀 더 알아보기

그럼 Redis에 어떻게 데이터를 저장해서 처리율을 제한할까요?
Redis에는 request_rate_limiter.{key}.tokens, request_rate_limiter.{key}.timestamp 이렇게 2개의 Key가 저장되어 있습니다
request_rate_limiter.{key}.tokens : 해당 key(ex: memberNo)에 해당하는 bucket의 token이 얼마나 남았는지를 저장합니다. bucket size인 burstCapacity를 10으로 지정하면 ,요청 시 사용되는 token의 수인 requestedTokens를 1으로 지정하면 최초로 1회 요청을 보냈을 때 bucket에 남은 token은 10 - 1 = 9가 됩니다

1️⃣ 의존성 추가

위에서 Spring Cloud Gateway는 기본적으로 Redis를 사용한다고 했습니다

Spring Data Redis dependency를 추가하도록 해야하고 Spring Cloud Gateway는 Webflux 기반이기 때문에 reactive로 추가해줍니다

implementation("org.springframework.boot:spring-boot-starter-data-redis-reactive")

2️⃣ KeyResolver 추가하기

[이전 포스팅]에서 만든 AuthorizationHeaderFilter에서 jwt 토큰에 대한 유효성을 검증한 후, 인증이 완료가 되면 memberNo를 (위 시퀀스 다이어그램에서 2) 요청 제한을 위한 Key로써 사용하도록 KeyResolver를 구현한 custom KeyResolver를 만들어줍니다.

@Configuration
public class CustomUserKeyResolver {
    @Bean
    KeyResolver userKeyResolver() {
        return exchange -> {
            String memberNo = exchange.getRequest().getHeaders().getFirst("X-Member-No");
            return Mono.just(memberNo);
        };
    }
}

AuthorizationHeaderFilter의 일부를 보여드리자면,토큰에서 memberNo을 추출하고 헤더에 추가해서 넘겨주게 됩니다.

// 새 요청 객체 생성 및 memberNo 헤더 추가
            ServerHttpRequest newRequest = request.mutate()
                    .header("X-Member-No", memberNo)
                    .build();

            // 수정된 요청으로 교환 객체 업데이트
            return chain.filter(exchange.mutate().request(newRequest).build());

3️⃣ Spring Gateway YML 설정

spring:
  cloud:
    gateway:
      routes:
      - id: rate_limiter
        uri: []
        filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 1
                redis-rate-limiter.burstCapacity: 10
                redis-rate-limiter.requestedTokens: 1
                key-resolver: "#{@userKeyResolver}"
  • key-resolver : 위에서 CustomUserKeyResolver에서 선언한 bean이름을 주입
  • requestedTokens : 요청시에 소모되는 토큰의 갯수(보통 1개)
  • burstCapacity : 버킷의 담겨져있는 최대용량
    • 저는 10개로 설정했습니다. 각자의 프로젝트 요구사항에 맞게 설정하시면 됩니다!
  • replenishRate: 초당 버킷 회복량

📉 광클 테스트


이제 모든 설정과 구축이 끝났으니 초과 요청을 보냈을 때는 어떻게 응답이 나오는지 알아보도록 하겠습니다

빠른 테스트를 위해 burstCapacity는 1로 바꿔서 진행하겠습니다!

API를 여러 번 광클해봅시다! 🚨🚨

그러면 아래와 같이 429 Too Many Requests 상태 코드와 Rate Limit 관련된 header가 응답으로 오는 것을 확인할 수 있을 것입니다!

마무리


이번 포스팅에서는 Spring Cloud Gateway의 RequestRateFilter를 활용하여 Rate Limiter를 구현하는 과정을 살펴보았습니다.

Rate Limiter 구현을 통해 서버의 안정성을 높이고 과도한 요청으로 인한 서비스 중단을 예방할 수 있습니다. 각 프로젝트의 요구사항에 맞게 설정을 조정하여 사용하시기 바랍니다!

다음 포스팅에서는 Gateway에 Circuit Breaker패턴을 통한 장애 전파 방지하는 방법에 대해서 알아보도록 하겠습니다.

오늘도 긴 글 읽어주셔서 감사합니다😄😄

참고


https://toss.tech/article/slash23-server
https://www.yes24.com/Product/Goods/102819435
https://dgle.dev/RateLimiter1/

profile
백엔드 공부중입니다!

0개의 댓글

관련 채용 정보