본문 바로가기
시스템 설계/가상 면접 사례로 배우는 대규모 시스템 설계 기초

4장. 처리율 제한 장치의 설계

by setung 2025. 1. 17.

처리율 제한 장치(rate limi**ter)**는 클라이언트 또는 서비스가 보내는 트래픽의 처리율을 제어하기 위한 장치이다.

HTTP를 예로 들면 이 장치는 특정 기간 내에 전송되는 클라이언트의 요청 횟수를 제한한다.

장점

  • DoS 공격에 의한 자원 고갈을 방지
  • 비용 절감
  • 서버 과부화 방지

처리율 제한 알고리즘

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

분산 환경에서 고려할 점

  • 경쟁 조건(race condition)
  • 동기화(synchronization)
    • 처리율 제한 장치 서버를 여러 대 두게 되면 동기화가 필요하다.
    • 레디스와 같은 중앙 집중형 데이터 저장소를 사용해 해결할 수 있다.

토큰 버킷 기반 처리율 제한 장치 구현 해보기 (gpt 도움)

package ch04_rate_limiter;

import java.util.concurrent.atomic.AtomicInteger;


public class TokenBucketRateLimiter {

    private final int maxTokens; // 최대 토큰 수
    private final int refillRate; // 초당 리필되는 토큰 수
    private final long refillIntervalInMillis; // 리필 간격 (밀리초 단위)
    private final AtomicInteger currentTokens; // 현재 토큰 수
    private volatile long lastRefillTimestamp; // 마지막 리필 시점

    public TokenBucketRateLimiter(int maxTokens, int refillRate, long refillIntervalInMillis) {
        this.maxTokens = maxTokens;
        this.refillRate = refillRate;
        this.refillIntervalInMillis = refillIntervalInMillis;
        this.currentTokens = new AtomicInteger(maxTokens);
        this.lastRefillTimestamp = System.currentTimeMillis();
    }

    // 요청 시 토큰 리필 및 소비
    public boolean tryAcquire() {
        refillTokensIfNeeded();

        if (currentTokens.get() > 0) {
            currentTokens.decrementAndGet();
            return true;
        } else {
            return false;
        }
    }

    // 필요한 경우 토큰 리필
    private void refillTokensIfNeeded() {
        long now = System.currentTimeMillis();
        long elapsedTime = now - lastRefillTimestamp;

        if (elapsedTime > refillIntervalInMillis) {
            int tokensToAdd = (int) (elapsedTime / refillIntervalInMillis * refillRate);
            int newTokenCount = Math.min(maxTokens, currentTokens.get() + tokensToAdd);
            currentTokens.set(newTokenCount); // 토큰 갱신
            lastRefillTimestamp = now; // 리필 시점 갱신
        }
    }

    public static void main(String[] args) throws InterruptedException {

        // 최대 5개의 토큰, 초당 2개 리필
        TokenBucketRateLimiter rateLimiter = new TokenBucketRateLimiter(5, 2, 1000);

        // 테스트 시뮬레이션
        Runnable requestTask = () -> {
            if (rateLimiter.tryAcquire()) {
                System.out.println("Request processed at " + System.currentTimeMillis());
            } else {
                System.out.println("Request denied at " + System.currentTimeMillis());
            }
        };

        // 200ms 간격으로 요청
        for (int i = 0; i < 50; i++) {
            Thread thread = new Thread(requestTask);
            thread.start();
            Thread.sleep(200);
        }
    }
}

private volatile long lastRefillTimestamp

멀티 스레드 환경에서 스레드가 리필 시간을 가져올 때 가시성을 보장하기 위해 volatile을 사용

private final AtomicInteger currentTokens;

토큰 개수를 락없이 스레드 세이프하게 사용하기 위해 AtomicInteger 사용

처음 토큰 5개를 소진한 후 바로 토큰 2개를 보충한 것으로 보인다.

그 이후 일정 시간마나 2개씩 토큰을 소진하는 것을 볼 수 있다.

댓글