[책][대규모 시스템 설계] 처리율 제한 장치의 설계

유기훈·2025년 10월 9일

처리율 제한 장치

네트워크 시스템에서 처리율 제한 장치는 클라이언트 또는 서비스가 보내는 트래픽의 처리율을 제어하기 위한 장치이다.

처리율 제한 장치가 필요한 이유

  • DoS 공격에 의한 자원 고갈을 방지할 수 있다.
  • 비용을 절감한다. 비용이 큰 API의 호출을 제한함으로써 비용을 절감할 수 있다.
  • 서버 과부하를 막는다.

1단계 문제 이해 및 설계 범위 확정

면접관과의 소통을 통해 알아낼 수 있는 내용은 다음과 같다.
1. 클라이언트측 제한 장치인지, 서버측 제한 장치인지
2. IP주소를 기준으로 제한할지, 사용자 ID 기준으로 제한할지
3. 시스템 규모는 어느 정도인지
4. 분산 환경에서 동작해야 하는지
5. 처리율 제한 장치는 독립된 서비스인지, 애플리케이션 코드에 포함되는지

2단계 개략적 설계안 제시 및 동의 구하기

처리율 제한 장치를 어디에 둘 것인가

클라이언트측

클라이언트측에 위치하면 얼마든지 위변조 당할 수 있기 때문에 클라이언트측은 좋지 않다.

애플리케이션에서 처리

서버측에서는 애플리케이션에서 각각 사용량 제한을 처리 할 수 있다. 이러한 경우에는 애플리케이션에서 각각 처리하므로 사용량 제한에 필요한 데이터를 Redis와 같은 데이터 저장소에 저장해야 한다.

API 게이트웨이에서 처리

MSA의 경우, 처리율 제한 정치는 보통API 게이트웨이라 불리는 컴포넌트에 구현된다. API 게이트웨이는 다음과 같은 기능을 제공하는 서비스다.

  • 처리율 제한
  • SSL 종단
  • 사용자 인증
  • IP 허용 목록 관리
    API 게이트웨이는 보통 클라우드 업체가 유지 보수를 담당한다.

처리율 제한 알고리즘

처리율 제한 알고리즘은 다음과 같다. 알고리즘에 대해서는 따로 정리하지 않는다.

  • 토큰 버킷
  • 누출 버킷
  • 고정 윈도 카운터
  • 이동 윈도 로그
  • 이동 윈도 카운터

개략적인 아키턱처

얼마나 많은 요청이 접수되었는지를 추적할 수 있는 카운터를 추적 대상별로 두고(사용자별로 추적 or IP 등), 이 카운터의 값이 어떤 한도를 넘어서면 한도를 넘어 도착한 요청은 거부하는 것이다.

처리율 제한 처리는 API 게이트웨이와 같은 미들웨어에서 하고, 카운터는 레디스에서 관리한다.

레디스를 사용하는 경우에 동시성 문제가 있을 수 있다. 동시성 문제는 레디스의 원자적 연산인 INCR, EXPIRE를 사용하면 해결 가능하다.

  • INCR: 메모리에 저장된 카운터의 값을 1만큼 증가시킨다.
  • EXPIRE: 카운터에 타임아웃 값을 설정한다. 설정된 시간이 지나면 카운터는 자동으로 삭제된다.
@Service
public class RateLimiterService {
    private final StringRedisTemplate redisTemplate;

    public boolean isAllowed(String userId, int limit, int windowSeconds) {
        String key = "rate:" + userId;
        Long count = redisTemplate.opsForValue().increment(key);

		// 메모리에 키가 없었으면 1을 반환함. 따라서 count가 1이면 ttl 설정 필요.
        if (count == 1) {
            redisTemplate.expire(key, Duration.ofSeconds(windowSeconds));
        }
        return count <= limit;
    }
}

3단계 상세 설계

처리율 한도 초과 트래픽의 처리

어떤 요청이 한도 제한에 걸리면 API는 HTTP 429 응답을 클라이언트에게 보낸다. 경우에 따라서는 한도 제한에 걸린 메시지를 나중에 처리하기 위해 큐에 보관할 수 있다.

분산 환경에서의 처리율 제한 장치의 구현

분산 환경에서 처리율 제한 장치를 구현할 때는 경쟁 조건과 동기화 이슈를 고려해야 한다.

경쟁 조건 해결 방법

  • INCR, EXPIRE 사용
  • redis의 루아 스크립트 활용

루아 스크립트를 활용하는 예시 코드는 다음과 같다.

String script = """
    local key = KEYS[1]
    local limit = tonumber(ARGV[1])
    local ttl = tonumber(ARGV[2])
    local count = redis.call('INCR', key)
    if count == 1 then
        redis.call('EXPIRE', key, ttl)
    end
    return count <= limit
""";
Boolean allowed = redisTemplate.execute(
    new DefaultRedisScript<>(script, Boolean.class),
    Collections.singletonList(key),
    limit, windowSeconds
);

동기화 이슈 해결 방법
처리율 제한 장치를 여러 개 사용해야 하는 경우를 대비하여 카운터는 처리율 조절 장치에 관리하지 않고, 레디스와 같은 중앙 집중형 데이터 저장소를 사용해야 한다.

성능 최적화

  • 처리율 제한 장치는 여러 데이터 센터를 지원해야 한다.
    - 여러 데이터 센터를 지원하지 않으면 데이터 센터에서 멀리 떨어진 곳에서는 속도가 현저히 느려진다.
  • 제한 장치 간에 데이터를 동기화할 때 최종 일관성 모델을 사용해야 한다.
    - 최종 일관성 모델: 분산 환경에서 가용성을 높이기 위해 즉각적 일관성을 희생하지만, 결국 모든 노드가 동일한 상태로 수렴하도록 보장하는 모델 (예: Redis Cluster)

모니터링

처리율 제한 장치가 효과적으로 동작하고 있는지 보기 위해 데이터를 모아야 한다. 모니터링을 통해 확인하려는 것은 다음 두가지다.

  • 채택된 처리율 제한 알고리즘이 효과적이다.
  • 정의한 처리율 제한 규칙이 효과적이다.
    예를 들어 처리율 제한 규칙이 너무 빡빡하게 설정되었다면 많은 유효 요청이 처리되지 못하고 버려질 것이다. 그런 일이 벌어진다면 규칙을 다소 완화할 필요가 있다.
profile
개발 블로그

0개의 댓글