4. 처리율 제한 장치

  • 요청 회수가 제한장치에 정의된 임계치를 넘어서면 추가로 도달한 모든 호출에 대해서 처리가 중단됨.

  • 장점

    • DoS 공격 방지

    • 비용 절감

    • 서버 과부하 방지

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

면접관에게 질문

  • 클라이언트 or 서버 측 제한 장치?

    • 서버

  • 어떤 기준을 통해서 호출을 제어해야하나?

    • 다양한 규칙이 있을 수 있음.

  • 시스템 규모, 스타트업 or 대규모?

    • 대규모요청

  • 시스템이 분산 환경에서 동작해야하나?

    • 그렇다.

  • 처리율 서비스가 독립된 서비스?

    • 알아서

  • 사용자의 요청이 처리율 제한 장치에 의해 걸리진 경우, 알려줘야하나?

    • 그렇다.

요구사항

  • 설정된 처 리율을 초과하는 요청은 정확하게 제한

  • 낮은 응답시간

  • 가능한 한 적은 메모리

  • 하나의 처리율 제한 장치를 여러 서버나 프로세스에서 공유

  • 요청이 제한되었을 때는 그 사실을 사용자에게 분명하게 보여줘야함

  • 제한 장치에 장애가 생기더라도 전체 시스템에 영향을 주어서는 안 됨.

2. 개략적 설계안 제시, 동의 구하기

  • 처리율 제한 장치 시스템을 미들웨어에 둘 수도 있고,

  • API 서버와 같은 레벨에 둘 수 도 있음.

처리율 제한 알고리즘

토큰 버킷 (Token Bucket)

  • 미리 정해진 용량의 '버킷'에 주기적으로 '토큰'이 채워짐

    • 토근이 가득차면, 추가되는 토근은 버려짐

  • 토큰을 하나씩 꺼내 사용하며, 토큰이 있어야만 요청이 처리됨.

  • 버킷에 토큰이 없으면 요청은 거부되거나 대기

  • 요청이 없을 때 토큰을 모아둘 수 있어, 일시적인 대량 요청(burst)을 유연하게 처리

  • 버킷 크기, 토큰 공급률 이 값을 적절하게 튜닝하는 것이 어려운 일

  • e.g. Spring resilience4j

누수 버킷 (Leaky Bucket)

  • 요청이 들어오면 '버킷'에 쌓이고, 버킷에서는 정해진 속도(rate)로 요청이 빠져나가 처리됨

  • 보통 FIFO 큐로 구현됨.

    • 큐 크기가 제한되어있기 때문에 메모리 사용량 측면에서 효율적

  • 버킷이 가득 차면 새로 들어오는 요청은 버려짐

  • 요청을 고정된 속도로 처리하므로 안정적인 출력 속도를 보장

  • 일시적인 대량 요청을 처리하기 어렵다는 단점

고정 윈도우 카운터 (Fixed Window Counter)

  • 정해진 시간 간격('윈도우') 동안의 요청 수를 셈

  • 이 숫자가 임계치를 넘으면 해당 윈도우가 끝날 때까지 들어오는 모든 요청을 막음.

  • 알고리즘을 이해하기 쉽다는 장점

  • 윈도우의 경계 시점에서 순간적으로 허용량의 2배에 달하는 요청이 처리될 수 있는 단점

    • ![[Pasted image 20250727150609.png]]

슬라이딩 윈도우 로그 (Sliding Window Log)

  • 각 요청의 타임스탬프를 기록하고, 현재 시간을 기준으로 지난 1분(또는 설정된 시간) 동안의 요청 수를 세어 임계치와 비교

  • 정확하게 처리율을 제어할 수 있지만, 모든 요청의 타임스탬프를 저장해야 하므로 메모리 사용량이 많다는 단점

슬라이딩 윈도우 카운터 (Sliding Window Counter)

  • 고정 윈도우 카운터와 슬라이딩 윈도우 로그의 장점을 결합한 방식

  • 이전 윈도우와 현재 윈도우의 요청 수를 가중 평균하여 현재 요청 가능 여부를 판단

  • 장점 이 전 시간대의 평균 처리율에 기반하여 현재 윈도 상태 판단.

    • 즉 짦은 시간 몰리는 트래픽에도 잘 대응한다.

  • 단점, 추정치 계산이 다소 느슨함

    • 왜?

      • 직전 시갠대에 도착한 요청이 균등하게 분포되어있다고 가정하기 때문에

개략적인 아키텍쳐

![[Pasted image 20250727151013.png]]

  • 클라이언트 -> 미들웨어

  • 미들웨어 -> 레디스

    • 레디스의 지정 버킷에서 카운터를 가져와서 요청 한도에 도달했는지 확인

      • 도달 했다면 요청 거부

      • 도달 하지 않았다면 api 서버로 요청 전달

        • 미들웨어는 카운터 값을 증가 한 다음에 레디스에 다시 저장

3. 상세 설계

  • 처리율 제한 규칙?

  • 처리가 제한된 요청들은 어떻게 처리됨?

처리율 한도 초과 트래픽은 어떻게 처리되는가?

  • 429 응답을 보내는데,

  • HTTP 헤더에 특정 값과 같이 보낼 수 있음.

    • X-Ratelimit-Remaining: 윈도 내 에 남은 처리 가능 요청 의 수

    • X-Ratelimit-Limit: 매 윈도마다 클라이 언트가 전송할 수 있는 요청 의 수

    • X-Ratelimit-Retry-After: 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 다시 보내야 하는지 알림

![[Pasted image 20250727151459.png]]

  • 429 응답을 보낼때도 2가지 옵션

    • 요청을 버리거나?

    • 혹은 해당 옵션을 메세지 큐에 보관 -> 나중에 처리..하는 방법도 있을듯?

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

  • 분산 환경에서는 문제가 있음.

    • 경쟁 조건(race condition)

    • 동기화 (synchronization)

경쟁 조건

![[Pasted image 20250727151718.png]]

  • 락을 사용할 수 있겠지만, 성능에 이슈 있음.

    • 해결할 수 있는 방법?

      • Lua script

        • 레디스가 단일 트랜잭션으로 실행하는 것을 보장하는 것.

        • 트랜잭션 실행하는 동안, 다른 클라이언트 명령이 중간에 끼어들 수 없음.

      • sorted set

        • 슬라이딩 윈도우 로그 알고리즘을 구현함

        • 이걸 루아 스크립트로 묶으면 경쟁조건을 완벽하게 해결할 수 있음.

-- KEYS[1]: ratelimit 키 (예: "ratelimit:user123")
-- ARGV[1]: 현재 시간 (타임스탬프)
-- ARGV[2]: 윈도우 크기 (예: 60000)
-- ARGV[3]: 최대 허용량 (예: 5)

local key = KEYS[1]
local now = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local limit = tonumber(ARGV[3])

-- 1단계: 윈도우 바깥의 오래된 기록 삭제
local clear_before = now - window
redis.call('ZREMRANGEBYSCORE', key, '-inf', clear_before)

-- 2단계: 현재 윈도우 내의 요청 수 확인
local count = redis.call('ZCARD', key)

-- 3단계: 허용 여부 결정
if count < limit then
  -- 허용: 현재 요청을 기록에 추가
  redis.call('ZADD', key, now, now)
  -- TTL(만료 시간)을 설정하여 메모리 관리
  redis.call('EXPIRE', key, window / 1000) -- 초 단위로 설정
  return 1 -- 성공
else
  return 0 -- 실패
end

동기화 이슈

  • 처리율 제한 장치 서버를 여러대 두개 되면 동기화가 필요해질 수 있음.

    • 예시

      • 동시 읽기: 서버 A와 서버 B가 거의 동시에 Redis에서 카운터 값 9를 읽습니다.

      • 잘못된 판단:

        • 서버 A는 9 < 10 이므로 요청을 허용하기로 결정합니다.

        • 서버 B도 9 < 10 이므로 요청을 허용하기로 결정합니다.

      • 덮어쓰기:

        • 서버 A가 카운터를 10으로 업데이트합니다.

        • 거의 직후에 서버 B도 카운터를 10으로 업데이트합니다.

      • 결과:

        • 실제로는 11개의 요청이 시스템에 들어왔지만, 카운터에는 10이라고 기록됩니다.

        • 이렇게 제한이 뚫리는 것이 바로 동기화 이슈입니다.

  • 어떻게 해결?

    • 읽고 수정하고 쓰는 것을 단일 작업으로 만듬 -> Redis lua script

![[Pasted image 20250727152855.png]]

성능 최적화

  • 지연 시간 이슈

  • 최종 일관성

모니터링

  • 선택한 처리율 제한 규칙이 너무 빡빡하다면, 많은 유효한 요청이 처리되지 못하고 버려질 것.

  • 그러면 규칙을 완화해야함

4. 마무리

  • 처리율 제한 알고리즘

    • 토큰버킷

      • 티켓 발급기

      • 요청이 없을 때는 토큰이 버킷에 쌓임. 갑자기 요청이 몰려와도 쌓아둔 요청 만큼 토큰을 빠르게 처리 가능

    • 누출버킷

      • 바닥에 작은 구멍이 뚫린 물통

      • 물통의 크기와 상관 없이 물은 바닥의 구멍을 통해 항상 일정한 속도로 빠져나감

      • 고정된 속도로 요청을 꾸준히 처리

    • 고정 윈도 카운터

      • 매시간 정각에 리셋되는 출입 게이트

      • 1시 59분 100명, 2시 00분 100명 들어오면 순가적으로 200명 요청이 처리될 수 있음.

    • 이동 윈도 로그

      • CCTV 녹화 기록을 돌려보는 것

      • 모든 사람의 출입 시간을 초 단위로 기록

      • 처리율을 아주 정확하게 제한할 수 있음.

      • 모든 요청 시간을 저장해야하므로 메모리 사용 크다

    • 이동 윈도 카운터

      • 이전 시간대의 입장객 수를 참고하는 똑똑한 게이트

      • 고정 윈도우 카운터 + 이동 윈도 로그 의 장점만 합침

  • 경성, 연성 처리율 제한

    • 경성 -> hard, 요청 의 갯수는 임계치를 절대 넘어설 수 없음

    • 연성 -> soft, 요청 개수는 잠시 동안 임계치를 넘어설 수 있음.

  • 다양한 계층

Last updated

Was this helpful?