개발 서적 완독하기/가상 면접 사례로 배우는 대규모 시스템 설계 기초

가상 면접 사례로 배우는 대규모 시스템 설계 기초 - 4장(처리율 제한 장치의 설계)

코드 살인마 2023. 12. 4. 21:41
728x90

처리율 제한 장치

네트워크 시스템에서 처리율 제한 장치는 클라이언트 또는 서비스가 보내는 트래픽의 처리율을 제어하기 위한 장치이다.
API 요청 횟수가 제한 장치에 정의된 임계값을 넘어서면 추가 API들은 버려지거나, 중단된다. 아래에는 몇가지 예시이다.

  • 사용자는 초당 2회 이상의 새 글을 올릴 수 없다.
  • 같은 IP 주소로는 하루에 10개 이상의 계정을 생성할 수 없다.
  • 같은 디바이스로는 주당 5회 이상 리워드를 요청 할 수 없다.

처리율 제한 장치를 두면 좋은점은 아래와 같다.

  • DOS 공격에 의한 자원 고갈을 방지할 수 있다. (트위터는 3시간 동안 300개 트윗 제한, 등등)
  • 비용을 절감한다. 추가요청에 대한 처리를 제한하면 서버를 많이 두지 않아도 되고, 우선순위가 더 높은 API에게 자원을 할당할 수 있다.
  • 서버 과부하를 막는다.

 

문제 이해 및 설계 범위 확정

시스템 요구사항은 다음과 같이 가정한다.

  • 설정된 처리율을 초과하는 요청은 정확하게 제한
  • 낮은 응답시간 : 이 처리율 제한 장치는 HTTP 응답 시간에 영향을 끼쳐서는 안된다.
  • 하나의 처리율 제한 장치를 여러 서버나 프로세스에서 공유 가능
  • 높은 결함 감내성 : 제한 장치에 장애가 생기더라도 전체 시스템에는 영향 X

 

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

기본적으로 클라 - 서버 통신 모델을 사용한다 가정한다.

 

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

일반적으로 클라이언트는 보안의 위험성이 있어 서버에 두고 있다.

서버에 두는 방법은 3가지가 있다.

- API 서버에서 직접 제한장치를 두는 방법

- 미들웨어(Ngnix. Apache)을 이용하여 두는 방법

- 위탁 관리형 서비스에 맡기는 방법(AWS 등..)

프로젝트의 상황을 보고 선택하는 것이 좋은데, 위탁 관리형 서비스에 대한 이점이 많기 때문에 대부분의 사람들이 사용중이다.

 

처리율 제한 알고리즘

대표적인 처리율 제한 알고리즘은 다음과 같다.

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

이 중 가장 보편적으로 사용중인 토큰 버킷 알고리즘에 대해 알아 볼 것이다.

 

토큰 버킷 알고리즘

  • 토큰 버킷은 지정된 용량을 갖는 컨테이너이고, 사전 설정된 양의 토큰이 주기적으로 채워진다. 토큰이 꽉 찬 버킷에는 더 이상의 토큰은 추가되지 않는다. 아래 그림에 설명이 자세히 나와있다.

그림을 보면 자연스레 궁금증이 생길 것이다. 토큰이 채워지는 시간은? 토큰의 최대개수는 조절가능한가?

이는 다음과 같은 파라미터로 설정할 수 있다.

  • 버킷 크기 : 버킷에 담을 수 있는 토큰의 최대 개수
  • 토큰 공급률 : 초당 몇 개의 토큰이 버킷에 공급되는가?

통상적으로 API 엔드포인트마다 별도의 버킷을 두지만, 사례에 따라 달라진다.

  • 사용자는 하루에 한번만 포스팅 할 수 있고(/user/post), 친구는 150명 까지만 추가 할 수 있고,(/user/friend) 좋아요 버튼은 다섯 번까지만 누를 수 있다면(/user/like), 사용자마다 3개의 버킷을 두어야한다.
  • ip 주소 별로 처리율 제한을 적용해야 한다면 IP 주소마다 버킷을 하나씩 할당해야한다.
  • 시스템의 처리율을 초당 10,000개 요청으로 제한하고 싶다면, 모든 요청이 하나의 버킷을 공유하도록 해야 할 것이다.

장점

  • 구현이 쉽다.
  • 메모리 사용 측면에서도 효율적이다.
  • 짧은 시간에 집중되는 트래픽도 처리 가능하다.

단점

  • 이 알고리즘은 버킷 크기, 토큰 공급률 이라는 2개 인자만 가지고 있는데, 이 값을 적절하게 튜닝하기는 까다로울 것이다.

 

개략적인 아키텍처

처리율 제한 알고리즘의 기본 아이디어는 단순하다. 얼마나 많은 요청이 접수되었는지를 추적할 수 있는 카운터를 추적 대상(API 엔드포인트? 사용자? IP? )별로 두고 카운터의 값이 한도가 넘어 도착한 요청은 거부하는 것이다.

그럼 이제 이 카운터를 보관하는 장소를 정해야한다.

  • 데이터 베이스는 디스크 접근 때문에 느리니까 사용하면 안될 것 같다.

그렇다면 메모리상에서 동작하는 캐시가 바람직해보인다. 속도도 빠르고, 시간에 기반한 만료 정책을 지원해준다. 일례로 레디스는 처리율 제한 장치를 구현할 때 자주 사용되는 메모리 기반 저장 장치로서 INCR과 EXPIRE의 두가지 명령어를 지원한다.

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

다시 돌아와 개략적인 아키텍처는 다음과 같다

  • 클라이언트가 처리율 제한 미들웨어에게 요청을 보낸다.
  • 처리율 제한 미들웨어는 레디스의 지정 버킷에서 카운터를 가져와서 한도에 도달했는지 아닌지를 검사한다.
    • 한도에 도달 했다면 요청은 거부된다.
  • 한도에 도달하지 않았다면 요청은 API 서버로 전달된다. 한편 미들웨어는 카운터 값을 증가시킨 후 다시 레디스에 저장한다.

 

상세 설계

이전에서 했던 처리율 제한 규칙은 설정파일 형태로 디스크에 저장된다.

domain: messaging
descriptors:
    - key: message_type
    Value: marketing
    rate_limit:
        unit: day
        requests_per_unit: 5

 

처리율 제한 장치에서 사용하는 HTTP 헤더

  • X-Ratelimit-Remaining : 윈도우에 남은 처리 가능한 요청 수
  • X-Ratelimit-Limit : 매 윈도우마다 클라이언트가 전송 가능한 요청 수
  • X-Ratelimit-Retry-After : 한도 제한에 걸리지 않으려면 몇 초 뒤에 요청을 다시 보내야 하는지 알림

 

상세 설계

아래는 상세설계 이미지이다.

  • 처리율 제한 규칙은 디스크에 보관하고, 작업 프로세스는 수시로 규칙을 디스크에서 읽어 캐시에 저장한다.
  • 클라이언트가 요청을 서버에 보내면 먼저 처리율 제한 미들웨어에 간다.
  • 처리율 미들웨어는 제한 규칙을 캐시에서 가져오고, 제한에 걸리지 않으면 API 서버, 걸리면 429 too may request 을 반환한다.

 

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

여러대의 서버와 병렬 스레드를 지원하도록 시스템을 확장하기전에 2가지 큰 문제를 해결해야한다.

  • 경쟁 조건(race condition)
  • 동기화(synchronization)

경쟁조건

처리율 제한 장치 간략한 알고리즘이다.

  1. 레디스에서 카운터의 값을 읽는다.
  2. counter + 1 값이 임계치를 넘는지 본다
  3. 넘지 않는다면 레디스에 보관된 카운터 값을 1만큼 증가시킨다.
    병행성이 심한 환경에서는 경쟁 조건 이슈가 발생할 수 있다.

경쟁 조건을 해결 할 수 있는 가장 대표적인 방법은 Lock 이다. 하지만 성능 이슈가 발생할 수 있음으로, 락 대신 아래 2가지를 사용할 수 있다.

  • 루아 스크립트
  • 정렬 집합 (레디스 자료구조)

동기화 이슈

처리율 제한 장치를 여러개 둘 경우, 여러대의 값이 동기화 되어야 한다.
이를 가장 쉽게 해결 할 수 있는 방법은 중앙 집중형 데이터 저장소인 레디스를 이용하는 것이다.

 

마무리

시간이 허락한다면 다음과 같은 주제로 얘기해보면 좋다.

  • hard or soft 처리율 제한
    • hard 처리율 제한 : 요청의 개수는 임계치를 절대 넘어설 수 없다.
    • soft 처리율 제한 : 요청 개수는 잠시 동안은 임계치를 넘어설 수 있다.
  • 다양한 계층에서의 처리율 제한
    • 이번장에서는 HTTP 즉 7계층에서만 해당 문제를 다루고 있었다. 하지만 다른 계층도 가능하다.
  • 처리율 제한을 회피하는 방법, 클라이언트를 어떻게 설계하는 것이 최선인가?
    • 클라이언트 측 캐시를 사용하여 API 호출 횟수를 줄인다.
    • 예외나 에러를 처리하는 코드 도입하여, 우아하게 복구
    • 재시도 로직을 구현할 때는 충분한 백오프 시간을 둔다.