Skip to content

Commit

Permalink
Merge pull request #14 from 11th-SSAFY-19/jaeho/chapter4
Browse files Browse the repository at this point in the history
[2주차]_4장_처리율 제한 장치의 설계_변재호
  • Loading branch information
bjho606 authored Jul 4, 2024
2 parents 19cb04f + 27cf846 commit 7425ce0
Showing 1 changed file with 289 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,289 @@
<aside>
💡 처리율 제한 장치 : 클라이언트가 보내는 트래픽의 처리율을 제어하기 위한 장치.
</aside>

- ex) HTTP에서 특정 기간 내에 전송되는 클라이언트의 요청 횟수(threshold)를 제한
- ex1) 사용자는 초당 2회 이상 새글 올릴 수 없음
- ex2) 같은 IP 주소로는 하루에 10개 이상의 계정을 생성할 수 없음
- ex3) 같은 디바이스로는 주당 5회 이상 리워드를 요청할 수 없음
- 처리율 제한 장치의 장점
- Dos(Denial of Service) 공격에 의한 자원 고갈 방지
- 비용 절감
- 서버 비용 하락
- 우선순위가 높은 api에 더 많은 자원 할당
- 서버 과부하 방지

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

> 주체 : 클라이언트 측 제한 vs 서버 측 제한?<br>
> 기준 : api 호출 vs ip 주소 vs 사용자 id vs …?<br>
> 시스템 규모 : 작음 vs 큼?<br>
> 환경 : 분산?<br>
> 독립성 : 독립적 vs 애플리케이션 코드 ?<br>
> 처리 결과 : 요청이 걸러진 결과를 알리기 vs 안 알리기?<br>
>
- 요구사항
- 요청을 정확히 제한
- 낮은 응답시간
- 가능한 적은 메모리
- 분산형 처리율 제한
- 예외 처리
- 높은 결함 감내성 (fault tolerance)

<br>

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

## 1. 처리율 제한 장치를 어디에 둘 것인가?

1. ~~클라이언트 측~~
- 클라이언트 요청은 쉽게 위변조가 가능 ⇒ 부적절
2. 서버 측 : api 서버 측
3. 미들웨어 (middleware) : 클라이언트 - 미들웨어 - api 서버

- 보통 `API 게이트웨이` 라는 컴포넌트에 구현 ⇒ 클라우드 마이크로 서비스
- 처리율 제한
- SSL 종단 (termination)
- 사용자 인증 (authentication)
- IP 허용 목록 (whitelist)

<aside>
💡 처리율 제한 ‘서비스’를 직접 만드는 데 시간이 든다 ⇒ API 게이트웨이가 효과적

</aside>

## 2. 처리율 제한 알고리즘

### 토큰 버킷 알고리즘 (token bucket)

- `토큰 버킷` : 지정된 용량을 갖는 컨테이너

![4-1. 토큰버킷.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/a5b3ee4e-14b3-49e8-a138-aea8e48edd63)

- 방식
1. 토큰 공급기는 정해진 매 초마다 몇 개의 토큰을 공급한다.
1. 토큰이 꽉 차면, 더이 상 토큰이 추가되지 않음
2. 각 요청은 하나의 토큰을 사용한다.
1. 토큰이 없는 경우, 그 요청은 버려짐

![4-2. 토큰버킷2.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/86e080a4-f7dd-4727-a9b1-71101684b408)

- 2개의 인자를 받음
1. 버킷 크기 : 버킷에 담을 수 있는 토큰의 최대 개수
2. 토큰 공급률 (refill rate) : 초당 몇 개의 토큰이 버킷에 공급되는가
- 보통 API 엔드포인트마다 별도의 버킷을 둠
- ex1) 사용자마다 ‘하루에 한번만’ 포스팅 가능, 친구는 ‘150명까지 추가 가능’, 좋아요는 ‘5번만 누를 수 있음’ ⇒ 사용자마다 3개의 버킷이 필요함
- ex2) IP 주소별로 처리율 제한 적용 ⇒ IP 주소마다 버킷이 필요함
- ex3) 시스템의 처리율 : 초당 10000개 요청 제한 ⇒ 모든 요청이 하나의 버킷을 공유해야함
- 장점
- 구현이 쉬움
- 메모리 사용 효율적
- **burst of traffic** (짧은 시간에 집중되는 트래픽) 처리 가능
- 단점
- 두 파라미터 (버킷 크기, 토큰 공급률) 를 적절하게 튜닝하기 어려움

### 누출 버킷 알고리즘 (leaky bucket)

- 토큰 버킷과 유사 + 요청 처리율 고정
- FIFO (First-In-First-Out) 큐로 구현

![4-3. 누출 버킷.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/cc4e5424-e389-4f55-aff6-c95a5fcda239)

- 방식
1. 요청이 도착하면 큐가 가득 차 있는지 확인
1. 빈자리가 있으면, 큐에 요청을 추가
2. 가득 차 있으면, 요청 버림
2. 지정된 시간마다 큐에서 요청을 꺼내어 처리
- 2개의 인자를 받음
1. 버킷 크기 : 큐 (처리될 항목들) 사이즈
2. 처리율 (outflow rate) : 지정된 시간당 몇 개의 항목을 처리할 수 있는가
- 장점
- 메모리 사용량 효율적 - 큐의 크기가 제한
- 안정적 출력 - 고정된 처리율
- 단점
- burst of traffic 에 취약 - 오래된 요청이 쌓이고, 새로운 요청이 버려짐
- 두 파라미터 튜닝 어려움

### 고정 윈도 카운터 (fixed window counter)

![ex) 초당 3개의 요청만 허용하는 경우](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/e505f2d3-36ed-454e-93c1-7c5fd970407c)

ex) 초당 3개의 요청만 허용하는 경우

- 방식
1. 타임라인을 고정된 간격의 윈도로 나누고, 각 윈도마다 카운터를 붙인다.
2. 요청이 접수될 때마다, 카운터 값이 1씩 증가
3. 카운터 값이 사전에 설정된 임계치 (threshold)에 도달하면, 새로운 요청은 새 윈도가 열릴 때까지 버려짐
- 장점
- 메모리 효율성
- 이해하기 쉬움
- 윈도가 닫히는 시점에 카운터 초기화 ⇒ 특정 트래픽 패턴을 처리하기 적합
- 단점
- 윈도 경계 부근에서 burst of traffic에 취약
- ex)
2:00 ~ 2:10 : 5개 & 2:10 ~ 2:20 : 5개 이지만,
(윈도 위치를 옮기면) 2:05 ~ 2:15 : 10개 가 되어버릴 수 있음

### 이동 윈도 로그 (sliding window log)

- 방식
1. 요청의 타임스탬프 (timestamp)를 추적
1. 이 데이터는 보통 redis의 정렬 집합 (sorted set) 같은 캐시에 보관
2. 새 요청이 오면 만료된 타임스탬프(=현재 윈도의 시작시점보다 오래된 타임스탬프) 제거
3. 새 요청의 타임스탬프를 로그(log)에 추가
4. 로그의 크기가 허용치보다 같거나 작으면 시스템에 전달. 아니면 처리 거부

![4-5. 이동 윈도 로그.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/82aa011f-af0d-4286-ba71-e34ec9bdb8d9)

- 장점
- 메커니즘이 정교
- 어느 순간의 윈도를 보더라도, 허용되는 요청의 개수는 시스템 처리율 한도를 넘지 않음
- 단점
- 다량의 메모리 사용 - 거부된 요청의 타임스탬프도 보관하므로

### 이동 윈도 카운터 (sliding window counter)

- 고정 윈도 카운터 + 이동 윈도 로그

![4-6. 이동윈도카운터.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/563657ec-d46b-4d88-ade8-5354d7594f4c)

- 방식
- 이전 시간대의 평균 처리율에 따라 현재 윈도 상태를 계산
- ex) 현재 1분간의 요청 수 + 직전 1분간의 요청 수 x 이동 윈도와 직전 1분이 겹치는 비율
- 장점
- burst of traffic 취약점 강화
- 메모리 효율성
- 단점
- ~~다소 느슨~~ - 직전 시간대에 도착한 요청이 균등하게 분포되어 있다고 가정한 상태에서 추정치를 계산하므로

## 개략적인 아키텍처

<aside>
💡 1. 요청 접수 개수를 추적할 카운터
2. 추적 대상 설정 (사용자별? IP주소별? API엔드포인트별? …)
3. 시스템 허용 threshold 넘어가면 요청 거부

</aside>

- 카운터 보관 ⇒ 캐시 (ex. Redis)
- 데이터베이스는 너무 느림 - 디스크 접근
- 시간에 기반한 만료 정책 지원
- Redis
- INCR : 메모리에 저장된 카운터 값을 1만큼 증가
- EXPIRE : 카운터에 타임아웃 값을 설정. 설정 시간 지나면 카운터 자동 삭제

![4-7. 개략.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/dd40cd15-e4b0-4ddf-8a4a-fa2c34f4a02c)

- 과정
1. 클라이언트 → 처리율 제한 미들웨어 : 요청 보냄
2. 처리율 제한 미들웨어 ↔ 레디스 : 카운터 가져와서 threshold 확인
1. 안되면 거부
3. 처리율 제한 미들웨어 → API 서버 : 요청 전달
1. 처리율 제한 미들웨어 → 레디스 : 카운터 값 증가

<br>

# 3단계. 상세 설계

<aside>
💡 처리율 제한 규칙은 어떻게 만들어지고 어디에 저장?
처리가 제한된 요청들은 어떻게 처리?

</aside>

## 1. 처리율 제한 규칙

- 보통 `설정 파일 (configuration file)` 형태로 디스크에 저장
- ex)
```yaml
# 마케팅 메시지는 하루 5개까지만 허용한다.
domain: messaging
descriptors:
- key: message_type
value: marketing
rate_limit:
unit: day
requests_per_unit: 5
```
## 2. 처리율 한도 초과 트래픽의 처리
- 응답 : HTTP 429 (too many requests)
- HTTP 헤더 : 클라이언트가 자기 요청이 제한에 걸렸는지 어떻게 확인하나?
- X-Ratelimit-Remaining : 윈도 내에 남은 처리 가능 요청 수
- X-Ratelimit-Limit : 매 윈도마다 클라이언트가 전송할 수 있는 요청 수
- X-Ratelimit-Retry-After : 한도 제한에 걸리지 않으려면 몇 초 뒤에 다시 보내야하는가
- 처리 : 제한 걸린 메시지를 큐에 보관했다 나중에 처리
## 상세 설계
![4-8. 상세.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/c1ddcfc2-564c-4e61-b871-448ad66869b2)
1. 처리율 제한 규칙 : 디스크에 보관
2. 작업 프로세스는 이 규칙을 수시로 디스크에서 읽어 → 캐시에 저장
3. 클라이언트 요청 → 처리율 제한 미들웨어 : 요청 전송
4. 미들웨어 ↔ 캐시 : 제한 규칙 가져옴
미들웨어 ↔ 레디스 : 카운터, 마지막 요청의 타임스탬프 가져옴
5. 가져온 값들에 근거하여 요청 통과 or 거부
1. 거부할 땐 429 (too many requests) 에러 or 메시지 큐에 보관
## 분산 환경에서의 설계
> 경쟁 조건
> 동기화
### 경쟁 조건 (race condition)
- 레디스에서 카운터를 읽고 쓰는 상황에서, 병렬적인 요청 처리 상황
- 해결
- ~~락 (lock)~~
- 성능이 매우 안 좋아짐
- 루아 스크립트 (Lua Script)
- 정렬 집합 (Sorted Set)
### 동기화 이슈 (synchronization)
- 처리율 제한 장치를 여러개 두는 상황
- 해결
- ~~고정 세션 (sticky session)~~ : 같은 클라이언트의 요청은 항상 같은 장치로 보내기
- 확장성 x, 유연성 x
- 중앙 집중형 데이터 저장소 사용 (like Redis)
![4-9. redis.png](https://github.com/11th-SSAFY-19/large-scale-system-design/assets/17850099/55aca6f5-fbf9-4bc5-b1be-4576829d65b6)
### 성능 최적화
1. 지연시간 (latency) 줄이기
→ 세계 곳곳에 에지 서버 (edge server) 심기
2. 제한 장치 간 데이터 동기화
→ 최종 일관성 모델 (eventual consistency model) 사용하기
### 모니터링 (monitoring)
1. 채택된 처리율 제한 알고리즘이 효과적인가
2. 정의한 처리율 제한 규칙이 효과적인가
<br>
# furthermore
## 경성(hard) vs 연성(soft) 처리율 제한
- hard : 요청의 개수는 임계치를 절대 넘으면 안됨
- soft : 요청 개수는 잠시 동안은 임계치를 넘을 수 있음
## 다양한 계층에서의 처리율 제한
- 현재까지는 7계층에서의 처리율 제한
- 3계층 처리율 제한도 가능
- ex) IP table → IP 주소에 처리율 제한 적용
## 처리율 제한을 회피하는 방법 (클라이언트 설계)
- 클라이언트 측 캐시 사용 ⇒ API 호출 횟수 줄이기
- threshold 를 생각하며, 짧은 시간 동안 너무 많은 메시지 보내지 않기
- 예외, 에러 처리 코드 도입 ⇒ 복구 가능하도록
- 재시도 로직 구현할 때, 충분한 백오프(back-off) 시간 두기

0 comments on commit 7425ce0

Please sign in to comment.