테마 선택

📚 지식 노트

AI·하드웨어·주변기기·게임·컴퓨터 시스템·이론 컴퓨터 과학·수학·개발과 운영·보안·응용 분야에 대해 알아 두면 좋은 내용을 정리한 노트다. 위에서 분야를 고르고 아래에서 노트를 고른다.

설계 사례: 미디어와 파일

동영상 스트리밍, 파일 동기화, 객체 저장소, 이메일처럼 큰 데이터를 나르고 오래 보관하는 서비스다.

목차 (4)
  1. 동영상 스트리밍
  2. 파일 동기화
  3. S3 같은 오브젝트 저장소
  4. 대규모 이메일

이 노트의 사례는 모두 큰 바이트를 나르고 오래 보관한다. 그래서 공통된 수가 있다. 바이트(본문)와 그 바이트를 설명하는 작은 정보(메타데이터)를 다른 저장소에 나누고, 큰 것은 조각으로 잘라 다룬다. 사례마다 요구와 규모를 직접 어림하고, 핵심 설계, 어려운 점과 선택지, 실제 사례 순서로 본다. 저장 장치 쪽 기초는 블록·파일·오브젝트 스토리지 노트에 있다.

동영상 스트리밍

요구와 규모

사용자가 동영상을 올리면 여러 화질로 바꿔 두고, 시청자의 네트워크 상태에 맞춰 끊기지 않게 재생한다. 하루에 50만 편이 올라오고, 평균 8분짜리 원본이 600MB이며, 저녁에 1천만 명이 동시에 본다고 하자.

  • 원본 저장: 50만 × 600MB = 하루 300TB.
  • 인코딩 결과: 화질 넷(5,000 + 2,800 + 1,400 + 800 = 10,000kbps)을 8분(480초) 동안 만들면 10Mbps × 480초 = 4,800Mb = 600MB. 원본과 비슷한 양이 하루 300TB 더 생긴다.
  • 전송: 1천만 명 × 평균 3Mbps = 30Tbps. 데이터센터 몇 곳에서 감당할 수 있는 양이 아니라서 CDN이 필수이고, 비용의 대부분도 여기서 나온다. 인코딩은 영상마다 한 번, 전송은 시청마다 일어나기 때문이다.

핵심 설계

올리기는 API 서버를 거치지 않는다. 앱이 API에 "올리겠다"고 하면 서버는 오브젝트 저장소에 바로 쓸 수 있는 서명된 URL을 내주고, 앱이 그 주소로 큰 파일을 여러 조각으로 나눠 올린다. 끊기면 그 조각만 다시 올린다. 올리기가 끝나면 처리 파이프라인이 시작된다. 영상을 몇 초짜리 청크로 나누고, 청크 × 화질만큼의 인코딩 작업을 방향 비순환 그래프(DAG)로 묶어 병렬로 돌린다. 8분 영상을 4초 청크로 나누면 120청크 × 4화질 = 480개 작업이라, 기계가 충분하면 몇 분 안에 끝난다. 오디오, 썸네일, 자막은 옆 가지로 돌고, 모두 끝나면 합쳐 패키징한다.

동영상 처리 파이프라인: 앱이 서명된 URL로 원본을 오브젝트 저장소에 올리면, 원본을 몇 초 단위 청크로 나누고 1080p, 720p, 360p 화질마다 청크를 병렬로 인코딩한다. 오디오, 썸네일, 자막은 옆 가지로 처리한다. 결과를 HLS와 DASH 조각과 재생 목록으로 패키징해 오리진에 두고, CDN을 거쳐 플레이어가 받는다올리기와 인코딩(한 번)업로드서명된 URL원본 저장소오브젝트분할몇 초 청크병렬 인코딩: 화질 × 청크1080p720p360p+ 오디오 · 썸네일 · 자막나눠 주기(수백만 번)플레이어화질 고름CDN가까운 곳오리진조각·목록패키징HLS·DASH 조각과 재생 목록인코딩은 한 번, 시청은 수없이: 비용의 대부분은 CDN 전송이다
업로드와 인코딩은 영상마다 한 번이고, 아래 줄의 나눠 주기는 시청마다 일어난다. 인코딩은 청크와 화질로 쪼개 병렬로 돌린다.

재생은 적응형 비트레이트(ABR)다. 같은 영상을 화질별로 몇 초짜리 조각으로 잘라 두고, 어떤 화질이 있고 조각이 어디 있는지 적은 재생 목록을 함께 둔다. 플레이어는 조각을 받으며 잰 속도로 다음 조각의 화질을 고른다. Apple의 HLS(RFC 8216)는 재생 목록이 .m3u8 텍스트 파일이고, MPEG-DASH는 XML 매니페스트(MPD)를 쓴다. 둘 다 평범한 HTTP 파일 요청이라 CDN에 그대로 캐시된다.

적응형 비트레이트 그래프: 60초 동안 6초 조각마다 잰 대역폭이 3,000에서 7,200kbps 사이를 오르내린다. 플레이어는 조각마다 잰 대역폭의 80% 안에 드는 가장 높은 화질을 골라, 대역폭이 떨어진 30초 무렵에는 480p와 360p로 내려갔다가 다시 1080p로 올라간다360p480p720p1080p0초12초24초36초48초60초잰 대역폭고른 화질(대역폭의 80% 안)조각 6초마다 다음 조각의 화질을 다시 고른다
플레이어는 조각을 받을 때마다 대역폭을 재고, 그 80% 안에 드는 가장 높은 화질로 다음 조각을 받는다. 화질이 계단처럼 내려갔다 올라가지만 재생은 멈추지 않는다.

어려운 점과 선택지

  • 조각 길이: 조각이 길면 요청 수가 줄고 압축 효율이 좋지만, 화질 전환이 느리고 시작이 늦다. Apple의 HLS 작성 지침은 6초를 권한다. 생방송처럼 지연이 중요하면 조각을 더 잘게 나눠 미리 보내는 저지연 방식을 쓴다.
  • 인코딩 비용: 모든 영상을 모든 화질로 만들면 거의 안 보는 영상에도 같은 계산을 쓴다. 조회가 적은 영상은 낮은 화질 몇 개만 만들고 인기가 오르면 더 만드는 식으로 나눈다. 장면이 단순한 애니메이션과 복잡한 스포츠는 같은 화질에 필요한 비트레이트가 다르므로, 영상마다 화질 사다리를 따로 정하기도 한다.
  • 저작권과 보안: 유료 콘텐츠는 조각을 암호화하고 열쇠는 DRM 서버가 권한을 확인한 뒤 준다. 조각 URL에 만료 서명을 붙여 링크만 퍼 가는 것을 막는다.
  • CDN 배치: 인기 영상은 거의 모든 시청자 근처에 있어야 하고, 대부분의 영상은 가끔만 본다. 인기 순으로 가장자리에 미리 채우고, 나머지는 요청이 올 때 상위 캐시에서 끌어온다.

실제 사례

Netflix는 자체 CDN Open Connect를 운영한다. 저장 장치가 가득 든 전용 서버를 인터넷 서비스 사업자의 망 안이나 교환 지점에 두고, 시청이 적은 새벽 시간에 다음 날 볼 만한 콘텐츠를 미리 채워 둔다. 시청이 몰리는 저녁에 바깥 망으로 나가는 트래픽이 거의 없게 하려는 설계다. 30Tbps 같은 숫자를 감당하는 방법이 "더 큰 데이터센터"가 아니라 "시청자 옆에 놓기"라는 점을 보여 준다.

파일 동기화

요구와 규모

여러 기기의 폴더를 같게 유지한다. 한 기기에서 고치면 몇 초 안에 다른 기기에 보이고, 오프라인에서 고친 것도 나중에 맞춘다. 유료 사용자 1천만 명이 평균 40GB를 쓰고, 기기 2천만 대가 하루 열 번 무언가를 바꾼다고 하자.

  • 저장: 1천만 × 40GB = 400PB. 같은 파일(설치 파일, 공유받은 문서)이 여러 사람에게 있으므로 중복 제거의 효과가 크다.
  • 변경: 2천만 × 10 = 하루 2억 건, ÷ 86,400 ≈ 초당 2,300건의 메타데이터 커밋이다.
  • 대역폭: 100MB 파일의 한 군데를 고쳤을 때 파일 전체를 다시 올리면 100MB, 4MB 블록 하나만 올리면 4MB다. 25배 차이가 하루 2억 번 쌓인다.

핵심 설계

파일을 고정 크기 블록으로 나누고 블록마다 해시를 구한다. 파일은 "블록 해시의 목록"이 되고, 블록은 해시를 이름으로 저장한다. 같은 내용의 블록은 이름이 같으니 한 번만 저장되고(중복 제거), 바뀐 블록만 오간다. 저장소는 둘로 나뉜다. 블록 자체를 담는 블록 저장소(크고 바뀌지 않는 바이트)와, 어떤 파일이 어떤 블록 목록으로 몇 번째 버전인지 담는 메타데이터 DB(작고 자주 바뀌며 트랜잭션이 필요한 정보)다.

파일 동기화 단계 그림: 기기 1이 파일을 블록 넷으로 나눠 해시하고, 메타데이터 서버에 없는 블록을 물어 바뀐 b3 블록만 블록 저장소에 올린 뒤 새 블록 목록을 버전 8로 커밋한다. 롱 폴링으로 기다리던 기기 2가 알림을 받고 바뀐 블록만 내려받는다기기 1 (고친 쪽)b1b2b3′b44MB 블록 × SHA-256기기 2b1b2b3b3′b44MB 블록 × SHA-256블록 저장소해시 → 내용메타데이터 서버파일 버전 7파일 버전 8알림 서버"없는 것은 b3′뿐"b3′ 4MB만 올림목록 커밋 → 버전 8롱 폴링 대기"바뀜"b3′만 받음
1 / 5

기기 1에서 100MB 파일의 가운데를 고쳤다. 앱이 파일을 4MB 블록으로 나눠 각각 SHA-256으로 해시하니 세 번째 블록(b3′)의 해시만 바뀌었다.

파일 동기화. 파일을 블록과 해시로 다루면 바뀐 블록만 오가고, 같은 블록은 한 번만 저장된다. 메타데이터 커밋이 버전의 경계다.

순서가 중요하다. 블록을 다 올린 뒤에 메타데이터를 커밋하므로, 다른 기기가 새 버전을 보는 순간 필요한 블록은 이미 저장소에 있다. 다른 기기가 바뀐 것을 아는 방법으로는 롱 폴링이 흔하다. 기기는 알림 서버에 요청을 걸어 두고, 서버는 변경이 생기면 그때 응답한다. 응답에는 "바뀌었다"는 사실만 담고, 실제 변경 목록은 기기가 "버전 7 이후"처럼 자기가 아는 마지막 지점을 대고 따로 받아 간다. 알림을 놓쳐도 다음 요청에서 따라잡으므로 알림 채널은 단순하게 둘 수 있다.

어려운 점과 선택지

  • 충돌: 두 기기가 오프라인에서 같은 파일을 고치면 둘 다 "버전 7을 고쳐 버전 8"을 만든다. 메타데이터 커밋을 "지금 버전이 7일 때만"이라는 조건부 쓰기로 하면 두 번째 커밋이 실패하므로 충돌을 알아챌 수 있다. 일반 파일은 내용을 합칠 수 없으니 보통 한쪽을 "충돌 사본"으로 남기고 사람에게 맡긴다.
  • 무엇이 바뀌었나: 로컬 파일이 사라졌을 때 "내가 지웠다"인지 "서버에 새로 생겼는데 아직 안 받았다"인지 구별하려면, 마지막으로 서로 같았던 상태를 기억해야 한다. 버전 관리의 공통 조상(merge base)과 같은 생각이다.
  • 블록 크기: 블록이 작을수록 고친 부분만 정확히 보내지만 해시와 목록이 늘어난다. 고정 크기 블록은 파일 앞에 한 바이트만 끼워도 뒤의 블록 경계가 모두 밀린다. 내용으로 경계를 정하는 가변 블록(content-defined chunking)은 이를 줄이는 대신 계산이 늘어난다.
  • 보안: 해시로 중복을 확인하는 서버는 "이 해시의 블록이 이미 있다"는 답으로 남의 파일이 있는지 알려 줄 수 있다. 사용자 사이 중복 제거를 할지, 블록을 올릴 때 실제 내용을 증명하게 할지 정해야 한다.

실제 사례

Dropbox의 Streaming File Synchronization은 파일을 4MB 블록으로 나눠 SHA-256으로 해시하고, 쉬는 기기는 알림 서버에 롱 폴링 연결을 걸어 둔다고 설명한다. 이 글의 요점은 올리기 커밋이 끝나기 전에 받는 기기가 블록을 미리 가져오게(prefetch) 해서 여러 기기 동기화 시간을 최대 두 배 줄인 것이다. API 문서의 content hash도 같은 4MB 블록 해시들을 이어 다시 해시한 값이다. 동기화 엔진을 Rust로 다시 쓴 이야기는 Rewriting the heart of our sync engine, 그 엔진이 서버 상태·로컬 상태·마지막으로 맞춰진 상태의 세 트리를 비교해 할 일을 정하고 이를 무작위 테스트로 검증한 이야기는 Testing sync at Dropbox에 있다. 블록을 담는 자체 저장 시스템은 Inside the Magic Pocket이 소개한다.

S3 같은 오브젝트 저장소

요구와 규모

버킷과 키로 객체를 넣고 꺼내는 HTTP 저장소다. 객체는 통째로 쓰고 고치지 않으며(덮어쓰기는 새 객체), 아주 오래 잃지 않아야 한다. 논리 용량 100PB, 객체 1천억 개를 담는다고 하자.

  • 원본을 세 벌 복제하면 300PB의 디스크가 든다. 데이터 6조각에 패리티 3조각을 더하는 소거 코딩(6+3)이면 100 × 9/6 = 150PB다. 디스크 150PB를 아끼는 선택이다.
  • 메타데이터: 객체마다 키, 크기, 조각 위치, 권한을 합쳐 300바이트면 1천억 × 300B = 30TB. 이것도 한 대에 담을 수 없으니 키 범위로 샤딩한다.

핵심 설계

요청은 상태 없는 API 프런트가 받아 인증과 권한을 확인한다. 그 뒤가 둘로 나뉜다. 메타데이터 서비스는 "버킷 + 키 → 객체 ID, 크기, 조각이 어느 노드에 있나"를 담는 샤딩된 DB이고, 데이터 서비스는 바이트를 조각내고 소거 코딩해 저장 노드들에 흩는다. 쓰기는 조각을 다 쓴 뒤 메타데이터를 커밋해야 끝난다. 그래야 메타데이터가 가리키는 바이트가 늘 존재한다.

오브젝트 저장소 구조: 클라이언트 요청을 API 프런트가 받아, 메타데이터 서비스에서 버킷과 키로 객체 ID와 조각 위치를 찾고 데이터 서비스로 읽고 쓴다. 데이터 서비스는 객체를 데이터 조각 여섯과 패리티 조각 셋으로 나눠 AZ 세 곳에 셋씩 둔다. 아홉 조각 중 아무 여섯 조각이면 복원되므로 AZ 하나를 통째로 잃어도 읽을 수 있고, 저장 배율은 1.5배다클라이언트PUT·GETAPI 프런트인증·권한·라우팅메타데이터 서비스 (키로 샤딩)버킷 + 키 → 객체 ID · 크기 · 조각 위치데이터 서비스: 6+3 소거 코딩·배치AZ 1AZ 2AZ 3d1d2d3d4d5d6p1p2p39조각 중 아무 6조각이면 복원 · AZ 하나(3조각)를 잃어도 읽힌다 · 저장 배율 1.5배 (3벌 복제는 3배)
AZ마다 조각을 셋씩 두면 AZ 하나를 통째로 잃어도 남은 여섯 조각으로 복원된다. 조각을 넷씩 두면 패리티(셋)보다 많이 잃어 AZ 장애를 견디지 못한다.

어려운 점과 선택지

  • 배치: 어떤 조각을 어느 디스크에 둘지 중앙 표에 모두 적으면 표가 병목이 된다. Ceph의 CRUSH처럼 클러스터 지도만 공유하고 위치는 해시 함수로 계산하는 방법, 또는 위치를 메타데이터에 적되 메타데이터를 샤딩하는 방법이 있다. 어느 쪽이든 같은 장애 영역(디스크, 서버, 랙, AZ)에 조각이 몰리지 않게 해야 한다.
  • 소거 코딩의 대가: 저장은 싸지만 읽기 하나가 여러 노드에 걸치고, 조각 하나를 잃으면 복구하려 다른 여러 조각을 읽어야 한다. 작은 객체는 조각 오버헤드가 커서 여러 작은 객체를 큰 덩어리에 모아 한꺼번에 부호화한다. 원리와 비교는 중복과 분산 노트에 있다.
  • 뜨거운 디스크: 한 고객의 객체가 디스크 몇 개에 몰리면 그 디스크의 초당 요청 한계가 곧 그 고객의 한계가 된다. 객체를 아주 많은 디스크에 흩어 두면 고객 한 명의 몰림을 수백만 디스크가 나눠 받는다.
  • 일관성: 쓰고 바로 읽거나 목록을 볼 때 새 객체가 보이는지가 메타데이터 계층의 일관성에 달려 있다.

실제 사례

Amazon S3 팀의 Andy Warfield가 쓴 Building and operating a pretty big storage system called S3는 수백만 개의 하드 디스크 위에서 "열"(heat), 곧 디스크마다 몰리는 요청을 어떻게 고르게 펴는지, 소거 코딩 조각을 넓게 흩어 한 고객의 갑작스러운 몰림을 백만 개 넘는 디스크가 나눠 받는지를 설명한다. 맨 아래 저장 계층을 Rust로 다시 쓴 ShardStore와 그 경량 정형 검증 이야기도 있다. S3는 2020년 말부터 쓴 직후 읽기와 목록에서 강한 일관성을 보장한다. Facebook은 사진처럼 작은 객체를 큰 파일에 이어 붙이고 메모리 색인으로 디스크 읽기를 한 번으로 줄인 Haystack, 자주 읽지 않는 객체를 리드-솔로몬(10,4)과 데이터센터 사이 XOR로 옮겨 실제 저장 배율을 3.6에서 2.1로 낮춘 f4를 논문으로 공개했다.

대규모 이메일

요구와 규모

메일을 받고, 보내고, 폴더와 검색으로 찾는다. 웹뿐 아니라 표준 프로토콜로 붙는 메일 앱도 지원한다. 사용자 1억 명이 하루 60통을 받고, 한 통이 평균 50KB라고 하자.

  • 수신: 1억 × 60 = 하루 60억 통, ÷ 86,400 ≈ 초당 7만 통. 저장은 60억 × 50KB = 하루 300TB이고, 메일은 몇 년씩 보관한다.
  • 수신 메일의 상당수는 스팸이라 저장하기 전에 걸러야 비용이 준다.

핵심 설계

메일 서버끼리는 SMTP(RFC 5321)로 주고받는다. 보내는 쪽은 받는 도메인의 MX 레코드를 조회해 우리 수신 SMTP 서버로 접속한다. 수신 서버는 스팸과 바이러스 검사, 보낸 도메인이 진짜인지 확인하는 SPF·DKIM·DMARC 검사를 거쳐 저장 서비스에 넘긴다. 저장은 동영상·파일과 같은 원칙으로 나눈다. 폴더, 읽음 표시, 스레드 같은 메타데이터는 사용자 ID로 샤딩한 DB에, 본문과 첨부는 블롭 저장소에, 그리고 사용자마다 검색 색인을 둔다.

이메일 서비스 구조: 외부 메일 서버가 MX 레코드로 찾은 수신 SMTP 서버에 메일을 넘기면, 스팸과 SPF·DKIM 인증 필터를 거쳐 메일 저장 서비스가 메타데이터, 본문 블롭, 사용자별 검색 색인에 나눠 쓴다. 웹·IMAP·JMAP 클라이언트는 메일 API로 읽고, 보낸 메일은 발송 큐를 거쳐 송신 SMTP가 재시도하며 내보낸다외부 메일 서버수신 SMTPMX 레코드필터스팸·SPF·DKIM메일 저장 서비스받은 편지 쓰기메타데이터폴더·플래그본문 블롭첨부 포함검색 색인사용자별메타데이터는 사용자 ID로 샤딩한다.같은 첨부는 한 번만 저장한다.클라이언트웹 · IMAP · JMAP메일 API읽기보내기발송 큐송신 SMTP실패하면 며칠 재시도
받은 메일은 필터를 거쳐 메타데이터·본문·검색 색인으로 나뉘어 저장된다. 읽기는 메일 API가, 보내기는 발송 큐와 송신 SMTP가 맡는다.

어려운 점과 선택지

  • 클라이언트 프로토콜: 메일 앱은 IMAP(최신판 RFC 9051)으로 서버의 폴더를 직접 다룬다. 연결을 오래 붙잡고 명령이 많아 모바일에서 무겁다. 이를 JSON over HTTP로 다시 설계한 것이 JMAP(RFC 8620)이다. 여러 동작을 한 요청에 묶고 "이 상태 이후의 변경"을 받는 방식이라, 파일 동기화의 버전 기반 따라잡기와 같다.
  • 검색: 모든 사용자의 메일을 한 색인에 넣으면 거대하고, 검색은 늘 한 사람의 메일 안에서만 한다. 그래서 사용자별(또는 사용자 묶음별) 색인을 두고 메일이 들어올 때 비동기로 색인한다. 검색 엔진의 구조는 분석과 검색 노트에 있다.
  • 보내기의 평판: 우리 서버에서 스팸이 나가면 다른 메일 회사가 우리 IP를 막아 모든 사용자의 메일이 막힌다. 발송량 제한, 새 계정의 발송 한도, 보내는 IP 묶음 분리가 필요하다. 받는 쪽이 일시 오류를 내면 SMTP 관례대로 간격을 늘려 며칠 동안 재시도한다.
  • 중복 저장: 같은 첨부가 사내 수천 명에게 가면 해시로 한 번만 저장할 수 있다. 다만 지우기가 어려워진다. 마지막 참조가 사라질 때까지 남겨야 하므로 참조 수를 따로 관리한다.

실제 사례

이메일은 다른 사례와 달리 바깥과 맞닿는 부분이 모두 공개 표준이다. 수신은 SMTP, 클라이언트는 IMAP과 JMAP으로 고정되어 있고, 서비스마다 다른 것은 그 안쪽의 저장과 검색 구조다. 앞의 세 사례에서 본 "메타데이터와 본문을 나누고, 큰 것은 블롭 저장소에, 바뀐 것만 따라잡기"가 이메일에서도 그대로 되풀이된다.