Created
June 21, 2024
Created by
D
DaEun KimTags
system-design
Property
Functional Requirement
https://some-commerce-system.com/soLongUrl?key1=val1&key2=val2...
와 같이 긴 url 을 짧은 URL 으로 단축시켜 제공해야 한다.- shortened url 은 원천 url 으로 리다이렉트 시켜야 한다.
- 한번 생성된 shortened url 은 영구적으로 원천 url 에 1:1 대응되어 쓰여야 한다.
- shortened url 에는 숫자와 영어 대소문자 조합만 허용한다. (특수문자는 허용하지 않는다.)
Non-Functional Requirements
- 고가용성, 규모 확장성이 가능해야 한다.
Estimates
- 매일 1억개의 고유한 shortened url 생성 필요
- 단축 URL 생성 요청 수 대비 읽기 요청 수는 10배 정도
- shortened url 서비스를 10년 간 운영한다면 총 3,650억개의 shortened url 생성 필요
→ 쓰기 요청 : 1억 / 60 / 60 / 24 = 약 1,157 TPS
→ 매일 10억개의 shortened url 방문
→ 읽기 요청 : 10억 / 60 / 60 / 24 = 약 11,157 TPS
Number of characters in shortened url
- 알파벳 대소문자 + 숫자 → 62개
- 62 ^ N ≥ 3,650억 을 만족하는 최소 N = 7
→ 따라서 shortened url 의 길이 = 7
예) input → <progress> → output X1yE2z3
길이가 7인 문자열을 만드는 데 필요한 인코딩 : Base62
- 바이너리 데이터를 62개 문자(a~z, A~Z, 0-9)로 표현
- 6 bit 씩 끊어서 인코딩한다.
- 이유) bit(이진수)로만 구성된 데이터를 62개 문자들로 표현하려면 2 ^ 6 = 64 → 6 bit 필요
- https://en.wikipedia.org/wiki/Base62
- 예) 101010 111000 000001 → g u 1
- a~z, A~Z, 0-9 으로 구성한 7개의 고유한 문자열을 만들려면 6 bit X 7 = 42 bit 필요
→ 42 bit 짜리 unique ID 를 만들어주는 ID generator 가 필요하다.
Data Persistence
DBMS
- Nosql (MongoDB, Casssandra…)
- 천억 단위의 레코드 저장 & 조회 → data intensive DBMS 필요
Schema
column | type | meaning | constraint |
long_url | varchar | 긴 url (ex. /soLongUrl?key1=val1&key2=val2... ) | |
shortened_url | varchar | shortened url (ex. /Xp3g0zy ) | unique |
created_at | datetime | 단축 url 생성시각 | |
expired_at | datetime | 단축 url 유효기한 |
System Design
- url shortening 서비스는 쿠버네티스에서 서빙하는 것을 가정
- consistent hashing-ring 으로 확장 가능한 data persistance 구축
- 빈번하게 발생하는 동일한 요청은 redis 캐싱 디비에서 담당
설명 | |
1. ask IP of shortening-url-service.com | DNS 에 url shortening 서비스의 IP 요청 |
2. provide IP | url shortening 서비스의 IP 제공 |
3. request POST /shorten | { long_url: ‘https://blah.com/soLongUrl?key1=val1&key2=val2…’} 와 같은 요청 바디 형태로 긴 url 에 대한 단축 url 요청 |
4. generate shortened url | shortened url 생성 |
5. insert shortened url | 생성한 shortened url 을 샤드에 저장 |
6. return | shortened url 을 클라이언트에게 제공 |
7. request GET /Xp3g0zy | shortened url 으로 원천 url 요청 |
8. get long url & validate expired_at | 디비에서 shortened url 에 대응하는 원천 url 조회 & 사용기한의 만료여부 확인 |
9. cache result | 이후 동일한 요청이 올 것을 감안하여 redis 에 캐싱 |
10. return | 원천 url 을 클라이언트에게 제공 |
💡 10.return 단계에서 클라이언트에게 상태코드 몇 번을 주는 게 적절할까?
상태코드 | 의미 |
301 (Moved Permanently) | • 브라우저가 응답을 캐싱 & 이후 shortened url 방문 시 브라우저 단에서 원천 url 으로 직접 리다이렉션
• 리다이렉션 과정에서 HTTP 메서드가 변경될 수 있음 (ex. POST → GET) |
307 (Temporary Redirect) | 브라우저가 응답ㅇㄹ 캐싱하지 않고 shortened url 방문 시 매번 서버에게 원천 url 요청 |
308 (Permanent Redirect) | 301 과 유사하지만 HTTP 메서드 유지 |
애플리케이션의 리소스를 절약하려면 301 또는 308, 원천 url 요청에 대한 별도의 데이터분석을 요구한다면 307 이 적절
How to generate shortened url
후보1) 해싱 함수 활용
해싱 함수 : MD5, SHA
- MD5 : 인풋에 대해 128bit 짜리 해시값 생성 → 캐릭터 길이가 7인 shortened url 만드려면 앞에서부터 잘라야 함 → collision 발생
- SHA256 : 인풋에 대해 256bit 짜리 해시값 생성 → 캐릭터 길이가 7인 shortened url 만드려면 앞에서부터 잘라야 함 → collision 발생 (MD5 보다 colliision 확률 높음)
후보2) Twitter’s Snowflake ID generator 으로 input key 생성
base62 기반으로 shortened url 만드려면 42 비트 필요
→ snowflake ID 는 64 비트로 구성
→ 앞에 42 비트 잘라서 쓰면 분산처리 환경에서는 collision 발생하므로 snowflake ID 는 적절하지 않음
collision 을 보완하는 수단 : bloom filter
해싱 알고리즘 또는 snowflake ID 단독으로만 적용해서는 collision 을 피할 수 없다
→ collision 을 방지하는 다른 수단이 필요하다
- bloom filter : 시간복잡도 O(1) 에 달하는 멤버십 체크로 collision 방지 가능하나, false-positive
- false-positive : 없는데 있다고 오판 (collision 이 없으나 있다고 판단 가능)
- consistent hashing ring : shard scale-out 으로 인한 해싱 미스 방지
ID generating service
- 길이가 7인 문자열을 만드는 데 필요한 42bit (=ID) 를 생성하는 서비스
- 각 파드에서 ID 생성 & 블룸필터 셋에 ID 저장
- 만약 생성한 ID 가 블룸필터 셋에 이미 있다고 판단하면 random bit 를 더해 ID 재생성 후 필터링 재시도
- 고가용성 유지를 위해 블룸필터 셋을 관리하는 스토리지는 다른 AZ 에 복제 인스턴스를 여러 개 둔다
참고하기 좋은 기술 사례
기타 레퍼런스