Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .idea/.gitignore

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

133 changes: 133 additions & 0 deletions ch04-rate-limiter/week1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Ch.5 — 처리율 제한장치 설계
## 1. 요구사항 해석

### 기능 요구사항

- 다양한 형태의 제어규칙을 정의할 수 있도록 하는 유연한 시스템
- 설정된 처리율을 초과하는 요청은 정확하게 제한

### 비기능 요구사항

- 낮은 응답시간
- 가능한 한 적은 메모리 사용
- 분산 환경에서 동작
- 예외 처리 : 사용자에게 알려야함
- 높은 결함 감내성 : 제한 장치에 장애가 생기더라도 전체 시스템에 영향을 주어서는 안 된다.

---

## 2. 설계 구조

### **2.1 처리율 제한 위치 (서버측 API를 위한 장치 설계)**

- 방법 1) 각 서버 애플리케이션 내부에 구현
- 제어규칙에 대해 서버마다 각자 구현이 필요하고, 분산 환경에서 일관성 관리가 어려울 듯
- `다양한 형태의 제어규칙을 정의할 수 있도록 하는 유연한 시스템` 에 부적합
- `분산 환경에서 동작` 에 부적합
- 방법 2) 클라이언트와 서버 사이에 구현
- 클라이언트와 서버 사이에서 라우팅하며 처리율 체크 (리버스 프록시)
- 서버코드 변경 없이 독립적으로 운영 가능
- `분산 환경에서 동작` 에 적합

방법 2를 채택한다.

```java
클라이언트 → 리버스 프록시 (처리율 제한 장치) → 백엔드 서버
```

---

### **2.2 카운트 저장소**

- 방법 1) DB에 저장
- 디스크 기반 I/O기 때문에 느리고, 동시성 제어 시 추가 병목 발생 가능
- `낮은 응답시간` 부적합
- 방법 2) 처리율 제한 장치 로컬 메모리
- 장치가 1대일 때는 정확, 여러대라면 카운트가 분산되어 부정확함
- 여러대일 경우 IP 마다 지정된 처리율 제한 노드로 가도록 한다면 보완될거 같은데, 노드 추가/삭제 시 매핑이 깨지고 특정 IP에 트래픽이 몰릴 경우 부하 분산 효과가 떨어짐
- `설정된 처리율을 초과하는 요청은 정확하게 제한` 부적합
- 방법 3) Redis
- 응답속도가 빠르고 공유 가능
- `분산 환경에서 동작` 적합
- `낮은 응답시간` 적합
- TTL 기능으로 처리율 제한 시간기준 세팅가능
- 원자적 연산으로 카운터 기능 해결 가능
- `설정된 처리율을 초과하는 요청은 정확하게 제한` 적합

방법 3을 채택한다.

---

### **2.3 설계 철학 — CP vs AP**

- 방법 1) CP (일관성 우선)
- 기능 요구사항(설정된 처리율을 초과하는 요청은 정확하게 제한)과 부합
- 방법 2) AP (가용성 우선)
- 비기능 요구사항(낮은 응답시간, 높은 결함 감내성)과 부합

방법 2 채택: 본 시스템은 **AP 시스템**으로 설계한다.

>
>
>
> 모두 요구사항에 존재하는 부분이라 선택에 어려움이 있었지만, 처리율 제한 미들웨어는 정확한 차단보다 불필요한 차단 방지가 우선이라고 판단했다
>

---

## 3. 핵심 설계 결정

### 다양한 형태의 제어규칙을 정의할 수 있도록 하는 유연한 시스템

제한 기준은 아래 세가지로 조합할 수 있도록 설계

- 시간 (하루, n분 ..)
- 식별자 (사용자 ID, IP, 디바이스 ..)
- 횟수 (윈도우 내 허용 요청 수)

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

만약 동일 ip로 1분에 100개의 요청을 제한 한다고 했을때?

- 단순 카운터를 사용한 방식은 시간마다의 요청 건수 초기화가 어렵다. 그럼 어떻게하냐?
- 윈도우마다 요청을 정확히 카운트할 수 있는 방식이 필요함

방법 1) IP별 사이즈 고정 Set 활용

```java
1. 새 요청 들어옴
2. Set이 가득 차지 않았으면 → 삽입 → 통과
3. Set이 가득 찼으면 → 첫 번째(가장 오래된) 요소 확인
→ 윈도우(1분) 밖이면 제거 후 삽입 → 통과
→ 윈도우 안이면 → 차단
```

- 매 요청마다 첫번째 요소 하나만 비교하면 됨 O(1)
- 단점 : 만료된 요소라도 Set이 가득 차지 않았으면 계속 저장하고 있음
- `가능한 한 적은 메모리 사용` 부적합

방법 2) 만료 요소 전체제거

```java
1. 새 요청 들어옴
2. 윈도우(1분) 밖의 요소 전부 제거
3. 현재 카운트 확인
4. 처리율 미만이면 삽입 → 통과
5. 처리율 이상이면 → 차단
```

- 요청이 들어올때마다 만료된걸 체크해서 삭제해서 위 방식보다 적은 메모리 사용
- `가능한 한 적은 메모리 사용` 적합
- 최악의 경우에 삭제 시 O(N)
- 다만, 책의 예시를 봤을때 N이 작을수록 O(1)에 수렴해 응답시간에 문제는 없을 듯

방법 2 채택: N이 수십만 이상일 경우 다른 방식을 생각해봐야함. 너무 느려서 ..

---

## 4. 병목 & 장애 포인트

### 레디스를 사용하는데, 레디스 장애 시 어떻게 함?

1. 처리율 제한을 타지 않고 바로 통과시키는 방법
2. 레디스 클러스터 내 이중화/샤딩해서 복제/부하 분산을 해두는 방법 (1로 가는걸 최소화)
Empty file added ch06-key-value-store/.gitkeep
Empty file.
143 changes: 143 additions & 0 deletions ch06-key-value-store/week1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
## 1. 요구사항 해석

### 기능 요구사항

- 키-값 쌍을 저장하고 조회할 수 있어야 한다
- 값은 어떤 타입이든 가능 (문자열, 객체 등)
- 키 쌍 값의 크기는 10KB 이하

### 비기능 요구사항

- 가용성: 장애 발생 시에도 빠르게 응답
- 확장성: 트래픽에 따라 서버 자동 증설/삭제, 대용량 데이터 저장 가능
- 지연시간: 낮은 레이턴시
- 일관성: 조정 가능한 수준

---

## 2. 설계 구조

단일 서버에 설계하면 기능 요구사항은 구현 가능하지만 비기능 요구사항을 충족하지 못한다.

- 가용성 : 단일 서버 장애 시 전체 서비스 다운 (SPOF)
- 확장성 : 단일서버 증설은 스케일업만 가능해 확장에 한계가 있음. 자동 삭제도 불가. 대용량 데이터 저장에도 한계가 있음
- 지연시간은 데이터가 늘어날수록 압축/디스크 이동 등 방법이 있겠지만, 디스크로 이동한 데이터에 대한 지연시간이 발생함
- 일관성 : 단일이니 일관됨

따라서 분산 키-값 저장소가 필요하다.

분산 키-값 저장소로 가정하고 요구사항을 다시 본다.

- 가용성 : 서버가 여러 대이므로 일부 서버가 죽어도 다른 서버가 응답 가능 (SPOF 제거). 일부 서버가 죽었을 때 응답 가능하도록 데이터가 복제되어있어야함?
- 확장성 : 트래픽에 따라 서버를 자동 증설/삭제하면 됨. 대용량 데이터도 저장 가능함. 다만 자동 증설/삭제 시 해시 값의 변동이 있을텐데 이걸 해결할 수 있어야함.
- 지연시간 : 요청을 처리할 서버가 여러 개라 부하가 분산됨. 단, 서버간 통신이 이루어진다면 느릴 수 있음.
- 일관성 : 서버가 분산되어있어 데이터 동기화가 이루어져야함.

### CAP란?

> 어떤 두가지를 지키려면 나머지 하나는 희생되어야함을 의미
>
- Consistency : 일관성 (조정가능)
- Avalability : 가용성
- Partition Tolerance : 네트워크 장애로 파티션이 발생해도 시스템 계속 동작

P는 무조건 지켜져야 하기 때문에 대개는 CP 시스템과 AP 시스템으로 설계한다


---

## 3. 핵심 설계 결정

### 가용성 설계

특정 서버가 죽었을 때 다른 서버가 응답하기 위해 데이터가 복제 되어있어야함

→ 근데 전체를 다 복제하면 의미가 없음

→ 몇개에만 복제하자 서버1은 서버2 서버4에 복제해두고.. 이런식?

→ 복제 대상 서버는 원본 서버과 물리적인 위치가 분리되어있어야할듯 (장애 대비)

데이터를 N개의 서버에 복제하는 방식 채택

- 복제본이 많을수록 가용성은 높아지지만 저장 비용이 증가하고 일관성 복잡도가 높아짐
- 적당하게 복제본 개수를 세팅해서 가용성을 챙기고, 다만 일관성 부분을 따로 보완해야함

### 확장성 설계

단순하게 hash(key) % 서버수로 서버를 선택한다

→ 이 방법은 서버 증설/삭제 시 서버수의 영향을 받아 선택되는 서버 결과가 달라지게 됨

그럼 변동이 없는 서버의 해시값은 유지하면서, 삭제된 서버의 데이터만 서버를 재분배하거나 서버 추가시에는 그럼 따로 재분배를 해주는 방식? (안정 해싱이라 함)

→ 재분배 중에 해당 데이터 조회 요청이 오면 지연 발생 가능

라우팅 테이블을 별도로 두고 어느 키가 어느 서버에 있는지 저장해두면?

→ 테이블 관련 장애시 SPOF, 재분배 시 지연 없음

**안정 해싱 채택**

- SPOF 없고 재분배 최소화, 다만 재분배 시 일시적 지연 발생 감수

### 일관성 설계

일관성은 조정 가능한 수준? 조정이 가능하게 설계하라는건가? 더 어렵다

1. 일관성 높은 시스템 : 쓰기 요청이 오면 모든 복제본에 반영되고 완료 응답.
2. 일관성 낮은 시스템 : 서버1에 저장되면 완료 응답. 복제본 동기화는 따로진행.
3. 결과적 정합성이 맞는 시스템 : 서버1에 저장되면 복제본 반영 보장.

이 세가지 시스템을 운영 중 선택해서 쓸 수 있도록 해야함 ..

요청 데이터가 저장된 서버가 N개라고 하면

1. 반영 응답 N개 전부
2. 반영 응답 1개
3. 반영 응답 1개 (반영보장)

**결과적 일관성 채택**

- 가용성을 챙기지만 일관성도 어느정도 챙김, 트레이드 오프 … 일시적으로 복제본값과 불일치 할 수 있음

### 지연시간 설계

지연시간은 서버간 통신이 잦을 수록 느려진다

→ 그럼 서버간 통신을 줄이자

→ 언제 서버간 통신이 발생하지? 서버 증설/삭제 시 데이터를 찾아 서버1로 갔는데 데이터가 없는 경우

**안정 해싱 채택**

- 처음부터 맞는 서버로 라우팅해서 평상시 서버 간 통신 없음
- 서버 증설/삭제 시 재분배 중 일시적 지연 발생 감수

---

## 4. 병목 & 장애 포인트

### 트래픽 급증 시 가장 먼저 무너질 곳

- 특정 키에 요청이 몰리게 되면 해당 서버만 과부하가 올 수 있음

→ 복제본 서버로도 요청이 분산될 수 있도록 랜덤하게 서버를 선택

### 데이터 일관성이 깨질 수 있는 시나리오

- 위에 복제본 서버로도 요청이 올 수 있다고 했으니 결과적 일관성 채택 시 아직 동기화 안된 복제본으로 읽기 요청이 오면 일관성이 깨질 수 있음

→ 모르겠음

### 장애 발생 감지

카프카의 컨슈머가 heartbeat를 보내는 것 처럼 서버가 heartbeat를 보내다가 일정 시간 안보내는걸 감지하면 장애로 간주

### 장애 발생 재처리

재처리는 복제본을 통해 자동으로 처리됨

근데 계속 장애 서버로 요청이 가면 지연시간 발생함 → 안정 해싱 링에서 제거 (서버 삭제처럼 동작되도록)

장애 복구 후엔 서버 증설하듯 링에 다시 추가하여 데이터 재분배 → 데이터가 많으면 재분배 비용이 클듯
87 changes: 87 additions & 0 deletions ch06-key-value-store/week2.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
## 1. 요구사항별 설계

- 가용성
- 읽기 가용성 : 데이터를 N개 서버에 복제
- 쓰기 가용성 : 낮은 W(quorum)을 설정해 일부 복제본만 응답해도 성공 처리
- 확장성
- 서버 증설 삭제 시 재배치 최소화 : 안정 해시
- 서버 데이터 균등 분배 : 안정 해시 + 가상노드
- 대용량 데이터 저장 가능 : 분산시스템 적용, 안정 해시로 부하 분산
- 지연시간: 낮은 레이턴시
- 빠른 라우팅 처리 : 안정 해시
- 특정 핫키에 요청 집중 : 복제본으로 읽기 요청 랜덤 분산
- 일관성
- 조정 가능한 수준 : R/W quorum 값 조정하여 사용
- 결과적 일관성 보장 형태를 채택
- 복제로 인한 데이터 불일치 처리
- 데이터 손실 감수 가능 → LWW (Last-Write-Wins)
- 데이터 손실 감수 불가 → 버저닝 + 벡터 시계 (채택)

---

## 2. 요청 처리 흐름

### 7.1 GET 동작

```
1. 클라이언트 GET(key) 호출
-> 내부적으로 hash(key) 계산 -> N개 서버 식별 -> 중재자(첫 번째 서버)에 GET(key) 전송

2. 중재자가 N개 서버 중 R개에 읽기 요청
-> 각 서버는 캐시를 확인하고 없으면 디스크에서 조회
-> 조회된 데이터와 벡터시계값을 응답

3. R개 응답이 모이면, 충돌 비교
-> 벡터 시계 비교 후 이전 버전은 무시

4. 결과를 클라이언트에게 반환

5. 벡터 시계 사용 시 클라이언트가 충돌 병합 후 저장
```

Q. 근데 중재자가 선택되는 방식이 단순 첫번째로 고정되는게 맞을지? → 코디네이션 역할 . .

### 7.2 PUT 동작

```
1. 클라이언트 PUT(key, value, clock) 호출
-> 내부적으로 hash(key) 계산 -> N개 서버 식별 -> 중재자(첫 번째 서버)에 PUT(key, value, clock) 전송
-> clock에는 이전에 받은 벡터시계값을 같이 전달

2. 중재자가 clock 값을 1 증가시킨 새 벡터시계값 생성

3. N개 서버에 데이터 전파

4. W개 응답이 오면, 클라이언트에 성공 응답
-> 오지않으면 timeout 처리
```

---

## 3. 장애 감지

### 일시적 장애

각 서버는 주기적으로 heartbeat를 주고 받는다.

응답이 없을 시 나머지 복제본으로 처리하지만, 일정 시간 동안 heartbeat가 없으면 영구 장애로 넘어간다.

### 영구적 장애

영구 장애로 판단되면 반-엔트로피 프로토콜 + 머클트리를 사용해 사본 동기화한다고 함

왜 머클트리까지 쓰냐?

트리로 구성해서 실제 차이분만 동기화할 수 있도록 함 → 동기화 비용이 축소

---

## 4. 1주차 대비 보완된 점

| 항목 | 1주차 | 2주차 |
| --- | --- | --- |
| 일관성 충돌 처리 | "결과적 일관성 채택"까지만, 구체적 메커니즘 없음 | LWW vs 벡터 시계 비교, 벡터 시계(+앱서버 병합) 채택 및 동작 원리 구체화 |
| 쓰기 가용성 | 별도 항목 없음 | 낮은 W(quorum)로 일부 응답만으로 성공 처리 |
| 요청 처리 흐름 | 명시 안 됨 | 코디네이터(중재자) 개념 도입, GET/PUT 단계별 동작 정의 |
| 데이터 일관성이 깨질 수 있는 시나리오 | "모르겠음" | 벡터 시계 비교 + R/W quorum(R+W>N) 조합으로 해결됨을 확인 |
| 장애 복구 방식 | "재분배" (모호) | 일시/영구 장애 구분, 영구 장애 시 머클 트리로 차이만 동기화 |