[Book - 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2] 9. S3와 유사한 객체 저장소
S3는 AWS가 제공하는 서비스로 RESTful API 기반 인터페이스로 이용 가능한 객체 저장소다.
저장소 시스템 101
블록 저장소
HDD나 SSD처럼 서버에 물리적으로 연결되는 형태의 드라이브는 블록 저장소의 가장 흔한 형태다.
블록 저장소는 원시 블록을 서버에 볼륨 형태로 제공한다.
가장 유연하고 융통성이 높은 저장소다.
서버는 원시 블록을 포맷한 다음 파일 시스템으로 이용하거나 애플리케이션에 블록 제어권을 넘겨버릴 수도 있다.
DB나 VM 엔진 같은 애플리케이션은 원시 블록을 직접 제어하여 최대한의 성능을 끌어낸다.
파일 저장소
파일 저장소는 블록 저장소 위에 구현된다.
파일과 디렉터리를 손쉽게 다루는 데 필요한, 더 높은 수준의 추상화를 제공한다.
데이터는 계층적으로 구성되는 디렉터리 안에 보관된다.
파일 저장소는 가장 널리 사용되는 범용 저장소 솔루션이다.
객체 저장소
객체 저장소는 새로운 형태의 저장소다.
데이터 영속성을 높이고 대규모 애플리케이션을 지원하며 비용을 낮추기 위해 의도적으로 성능을 희생한다.
실시간으로 갱신할 필요가 없는 상대적으로 ‘차가운(cold)’ 데이터 보관에 초점을 맞추며 데이터 아카이브나 백업에 주로 쓰인다.
모든 데이터를 수평적 구조 내에 객체로 보관한다.
계층적 디렉터리 구조는 제공하지 않는다.
데이터 접근은 RESTful API를 통한다.
다른 유형의 저장소에 비해 사애적으로 느리다.
비교
용어 정리
- 버킷(bucket)
- 객체를 보관하는 논리적 컨테이너
- 버킷 이름은 전역적으로 유일
- 데이터를 S3에 업로드하려면 우선 버킷 생성
- 객체(object)
- 버킷에 저장하는 개별 데이터
- 객체는 데이터(또는 payload)와 메타데이터를 갖음
- 객체 데이터로는 어떤 것도 가능
- 메타데이터는 객체를 기술하는 이름-값 쌍의 집합
- 버전(version)
- 한 객체의 여러 버번을 같은 버킷 안에 둘 수 있도록 하는 기능
- 버킷마다 별도 설정 가능
- 시수로 지웠거나 덮어 쓴 객체 복구 가능
- URI
- 객체 저장소는 버킷과 객체에 접근할 수 있도록 하는 RESTful API 제공
- 각 객체는 해당 API URI를 통해 고유하게 식별 가능
- SLA(Service-Level Agreement)
- 서비스 수준 협약(SLA)은 서비스 제공자와 클라이언트 사이에 맺어지는 계약
- 여러 AZ에 걸쳐 99.999999999%의 객체 내구성을 제공하도록 설계
- 하나의 AZ 전체가 소실되어도 데이터 복원 가능
- 연간 99.9%의 가용성 제공
1단계: 문제 이해 및 설계 범위 확정
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
어떤 기능을 지원해야 하나요?
>> 다음 기능을 제공하는 S3와 유사한 객체 저장소 시스템을 설계
- 버킷 생성
- 객체 업로드 및 다운로드
- 객체 버전
- 버킷 내 객체 목록 출력 기능. aws s3 ls 명령어와 유사
데이터의 크기는?
>> 아주 큰 객체(수 GB 이상)와 다량의 소형 객체(수 KB 정도)를 효율적으로 저장
내년 추가되는 데이터는 어느 정도?
>> 100PB
99.9999%의 데이터 내구성과 99.99%의 서비스 가용성을 보장한다고 가정 가능?
>> 네
비기능 요구사항
- 100PB 데이터
- 시스 나인(six nines, 99.9999%) 수준의 데이터 내구성
- 포 나인(four nines, 99.99%) 수준의 서비스 가용성
- 저장소 효율성: 높은 수준의 안정성과 성능은 보증하되 저장소 비용은 최대한 낮춰야 한다.
대략적인 규모 추정
객체 저장소는 디스크 용량이나 초당 디스크 IO(IOPS)가 병목이 도리 가능성이 높다.
- 디스크 용량: 객체 크기가 다은 분포를 따른다고 가정
- 객체 가운뎅 20%는 그 크기가 1MB 미만의 작은 객체
- 60% 정도의 객체는 1MB~64MB 정도 크기의 중간 크기 객체
- 나머지 20% 정도는 64MB 이상의 대형 객체
- IOPS: SATA 인터페이스를 탑재하고 7200rpm을 지원하는 하드 디스크 하나가 초당 100~150회의 임의 데이터 탐색을 지원할수 있다고 가정(100~150 IOPS)
2단계: 개략적 설계안 제시 및 동의 구하기
객체 저장소의 몇 가지 흥미로운 속성을 알아보자.
- 객체 불변성(object immutability)
- 객체 저장소에 보관되는 객체들은 변경이 불가능
- 키-값 저장소(key-value store)
- 객체 저장소를 사용하는 경우 해당 객체의 URI를 사용하여 데이터 조회 가능
- 저장은 1회, 읽기는 여러 번
- 소형 및 대형 객체 동시 지원
객체 저장소의 설계 철학은 UNIX 파일 시스템의 설계 철학과 아주 비슷하다.
UNIX의 경우 파일을 로컬 파일 시스템에 저장하면, 파일의 이름과 데이터는 같은 곳에 저장되지 않는다.
대신 파일 이름은 ‘inode’라고 불리는 자료 구조에 보관되고 파일의 데이터는 디스크의 다른 위치에 들어간다.
아이노드에는 파일의 데이터가 실제로 보관되는 디스크 상의 위치를 가리키는 파일 블록 포인터 목록이 저장된다.
따라서 로컬 파일을 읽으려고 하면 우선 아이노드에 기록된 메타데이터를 읽어서 파일 블록 포인터 목록을 확보한 다음 그 포인터를 일일이 따라가 데이터를 읽어야 한다.
객체 저장소의 동작 방식도 비슷하다.
객체 저장소의 메타데이터 저장소는 아이노드에 해당하고, 객체 데이터가 저장되는 데이터 저장소는 하드 디스크에 해당한다.
다만 메타데이터 저장소에는 파일 블록 포인터 대신 네트워크를 통해 데이터 저장소에 보관ㄴ된 객체를 요청하는 데 필요한 식별자(ID)가 보관된다.
메타데이터와 객체의 실제 데이터를 분리하면 설계까 단순해진다.
데이터 저장소에 보관되는 데이터는 불변이고, 메타데이터 저장소에 보관되는 데이터는 변경 가능하다.
이렇게 분리해 두면 그 두 컴포넌트를 독립적으로 구현하고 최적화할 수 있다.
개략적 설계안
- 로드밸런서
- RESTful API에 대한 요청을 API 서버들에 분산하는 역할
- API 서비스
- IAM 서비스, 메타데이터 서비스, 저장소 서비스에 대한 호출을 조율하는 역할
- 무상태 서비스이므로 수평적인 규모 확장 가능
- IAM 서비스
- 인증, 권한 부여, 접근 제어 등을 중앙에서 맡아 처리
- 데이터 저장소
- 실제 데치터를 보관하고 필요할 때마다 읽어가는 장소
- 모든 데이터 관련 연산은 객체 ID(UUID)를 통함
- 메타데이터 저장소
- 객체 메타데이터를 보관하는 장소
객체 저장소가 지원해야 하는 가장 중요한 작업 흐름을 살펴보자.
객체 업로드
객체는 버킷 안에 두어야 한다.
- 클라이언트는 bucket-to-share 버킷을 생성하기 위한 HTTP PUT 요청 -> API 서비스로 전달
- API 서비스는 IAM을 호출하여 해당 사용자가 WRITE 권한을 가졌는지 확인
- API 서비스는 메타데이터 DB에 버킷 정보를 등록하기 위해 메타데이터 저장소를 호출
- 버킷 정보가 만들어지면 그 사실을 알리는 메시지가 클라이언트에 전송
- 버킷이 만들어지고 나면 클라이언트는 script.txt 객체를 생성하기 위한 HTTP PUT 요청
- API 서비스는 해당 사용자 신원 및 WRITE 권한 소유 여부 확인
- 확인 결과 문제가 없으면 API 서비스는 HTTP PUT 요청 몸체에 실린 객체 데이터를 데이터 저장소로 보냄
- 데이터 저장소는 해당 데이터를 객체로 저장하고 해당 객체의 UUID를 반환
- API 서비스는 메타데이터 저장소를 호출하여 새로운 항목을 등록
- object_id(UUID), bucket_id(해당 객체가 속한 버킷), object_name 등
객체 다운로드
버킷은 디렉터리 같은 계층 구조를 지원하지 않는다.
하지만 버킷 이름과 객체 이름을 연결하면 폴더 구조를 흉내 내는 논리적 계층을 만들 수는 있다.
예를 들어 객체 이름을 script.txt 대신 bucket-to-share/script.txt와 같이 짓는 것이다.
- 클라이언트는 GET /bucket-to-share/script.txt 요청을 로드밸러서로 보냄 -> API 서버로 전달
- API 서비스는 IAM을 질의하여 사용자가 해당 버킷에 READ 권한을 가지고 있는지 확인
- 권한이 있음을 확인하면 API 서비스는 해당 객체의 UUID를 메타데이터 저장소에서 가져옴
- API 서비스는 해당 UUID를 사용해 데이터 저장소에서 객체 데이터를 가져옴
- API 서비스는 HTTP GET 요청에 대한 응답으로 해당 객체 데이터를 반환
3단계: 상세 설계
데이터 저장소
API 서비스는 사용자의 요청을 받으면 그 요청을 처리하기 위해 다른 내부 서비스들을 호출한다.
객체를 저장하거나 가져오는 작업은 데이터 저장소를 호출하여 처리한다.
객체 업로드/다운로드를 처리하기 위해 API 서비스와 데이터 저장소가 어떻게 연동하는지 보자.
데이터 저장소의 개략적 설계
데이터 저장소는 세 가지 주요 컴포넌트로 구성된다.
데이터 라우팅 서비스
데이터 라우팅 서비스는 데이터 노드 클러스터에 접근하기 위한 RESTful 또는 gRPC 서비스를 제공한다.
더 많은 서버를 추가하여 쉽게 규모를 확장할 수 있는 무상태 서비스다.
이 서비스는 아래와 같은 역할을 한다.
- 배치 서비스를 호출하여 데이터를 저장할 최적의 데이터 노드를 판단
- 데이터 노드에서 데이터를 읽어 API 서비스에 반환
- 데이터 노드에 데이터 기록
배치 서비스
배치 서비스는 어느 데이터 노드에 데이터를 저장할지 결정하는 역할을 담당한다.
데이터 노드에는 주 데이터 노드와 부 데이터 노드가 있다.
배치 서비스는 내부적으로 가상 클러스터 지도를 유지하는데, 이 지도에는 클러스터의 물리적 형상 정보가 보관된다.
배치 서비스는 이 지도에 보관되는 데이터 노드의 위치 정보를 이용하여 데이터 사본이 물리적으로 다른 위치에 놓이도록 한다.
이 물리적인 분리는 높은 데이터 내구성을 달성하는 핵심 요소다.
배치 서비스는 모든 데이터 노드와 지속적으로 박동 메시지를 주고받으며 상태를 모니터링한다.
15초의 유예 기간 동안 박동 메시지에 응답하지 않는 데이터 노드는 지도에 죽은 노드로 표시한다.
데이터 노드
데이터 노드는 실제 객체 데이터가 보관되는 곳이다.
여러 노드에 데이터를 복제함으로써 데이터의 안정성ㅇ과 내구성을 보증하는데, 이를 다중화 그룹이라 부른다.
각 데이터 노드에는 배치 서비스에 주기적으로 박동 메시지를 보내는 서비스 데몬이 돈다.
박동 메시지에는 다음과 같은 중요 정보가 들어 있다.
- 해당 데이터 노드에 부착된 디스크 드라이브(HDD/SDD)의 수
- 각 드라이브에 저장된 데이터의 양
배치 서비스는 못 보던 데이터 노드에서 박동 메시지를 처응ㅁ 받으면 해당 노드에 ID를 부여하고 가상 클러스터 지도에 추가한 다음, 아래 정보를 반환한다.
- 해당 데이터 노드에 부여한 고유 식별자
- 가상 클러스터 지도
- 데이터 사본을 보관할 위치
데이터 저장 흐름
이제 데이터가 어떻게 데이터 노드에 영속적으로 보관되는지 살펴보자.
- API 서비스는 객체 데이터를 데이터 저장소로 포워딩
- 데이터 라우팅 서비스는 해당 객체에 UUID를 할당하고 배치 서비스에 해당 객체를 보관할 데이터 노드를 질의
- 배치 서비스는 가상 클러스터 지도를 확인하여 데이터를 보관할 주 데이터 노드를 반환
- 데이터 라우팅 서비스는 저장할 데이터를 UUID와 함께 주 데이터 노드에 직접 전송
- 주 데이터 노드는 데이터를 자기 노드에 지역적으로 저장하는 한편, 두 개의 부 데이터 노드에 다중화
- 주 데이터 노드는 데이터를 모든 부 데이터 노드에 성공적으로 다중화하고 나면 데이터 라우팅 서비스에 응답
- 객체의 UUID, 즉 객체의 ID를 API 서비스에 반환
데이터는 어떻게 저장되는가
가장 단순한 방안으로 각각의 객체를 개별 파일로 저장하면, 동작은 하지만 작은 파일이 많아질수록 성능이 떨어진다.
- 낭비되는 데이터 블록수 증가
- 파일 시스템은 파일을 별도의 디스크 블록으로 저장
- 디스크 블록의 크기는 전부 같으며 볼륨을 초기화할 때 결정(보통 4KB)
- 크기가 4KB 보다 작은 파일을 저장할 때도 블록 하나를 온전히 사용
- 작은 파일 증가 -> 낭비되는 블로 수 증가
- 시스템의 inode 용량 한계를 초과하는 문제
- 파일 시스템은 파일 위치 등의 정보를 아이노드라는 특별한 유형의 블록에 저장
- 대부분의 파일 시스템의 경우, 사용 가능한 아이노드의 수는 디스크가 쵝화 되는 순간에 결정
- 작은 파일의 수가 수백만에 달하게 되면 이 아이노드가 전부 소진될 가능성 생김
- 운영체제는 파일 시스템 메타데이터를 공격적으로 캐싱하는 전략을 취하더라도 아주 많은 양의 아이노드를 효과적으로 처리 X
- 작은 객체를 개별 파일 형태로 저장하는 방안은 현실에서 쓸모 없음
이 문제는 작은 객체들을 큰 파일 하나로 모아서 해결할 수 있다.
개념적으로는 WAL(Write-Ahead Log)와 같이 객체를 저장할 때 이미 존재하는 파일에 추가하는 방식이다.
용량 임계치에 도달한 파일은 읽지 전용 파일로 변경하고 새로운 파일로 만든다.
읽기 전용으로 변경된 파일은 오직 읽기 요청만 처리한다.
읽기-쓰기 파일에 대한 쓰기 연산은 순차적으로 이루어져야 한다는 것에 유의하자.
객체는 파일에 일렬로 저장된다.
이 레이아웃을 유지하려면 여러 CPU 코어가 쓰기 연산을 병렬로 진행하더라도 객체 내용이 뒤섞이는 읽은 없어야 한다.
파일에 객체를 기록하기 위해서는 자기 순서를 기다려야 한다는 뜻이기도 하다.
객체 소재 확인
각각의 데이터 파일 안에 많은 작은 객체가 들어 있을 경우, 데이터 노드가 UUID로 객체 위치를 찾기 위해 아래와 같은 정보가 필요하다.
- 객체가 보관된 데이터 파일
- 데이터 파일 내 객체 오프셋
- 객체 크기
이 데이터는 한 번 기록된 후에 변경되지 않기 때문에 읽기 연산 성능이 좋은 RDB가 좋은 선택이다.
데이터 노드에 저장되는 위치 데이터를 다른 데이터 노드와 공유할 필요가 없는 점을 활용하면 데이터 노드마다 RDB를 설치하는 방안이 가능하다.
개선된 데이터 저장 흐름
데이터 노드의 설계를 변경했으므로, 새로운 객체를 데이터 노드에 저장하는 절차를 다시 짚어보자.
- API 서비스는 새로운 객체를 저장하는 요청을 데이터 노드 서비스에 전송
- 데이터 노드 서비스는 객체 4를 읽기-쓰기 파일 /data/c의 마지막 부분에 추가
- 해당 객체에 대한 새로운 레코드를 object_mapping 테이블에 추가
- 데이터 노드 서비스는 API 서비스에 해당 객체의 UUID 반환
데이터 내구성
데이터의 안정성은 데이터 저장 시스템에 아주 중요하다.
99.9999% 수준의 데이터 내구성을 제공하는 저장소 시스템을 만들려면 장애가 발생할 모든 경우를 세심하게 살핀 다음 데이터를 적절히 다중화할 필요가 있다.
하드웨어 장애와 장애 도메인
기록 매체 종류와 관계없이, 하드 디스크 장애는 피할 수 없다.
들라이브 한 대로 원하는 내구성 목표를 달성하기는 불가능하다.
내구성을 높이는 검증된 방법은 데이터를 여러 대의 하드 드라이브에 복제하여 어떤 드라이브에서 발생한 장애가 전체 데이터 가용성에 영향을 주지 않도록 하는 것이다.
본 설계안에서는 데이터를 3중 복제한다.
대규모의 장애 도메인 사례로는 데이터센터의 AZ를 예로 들 수 있다.
가용성 구역은 보통 다른 데이터센터와 물리적 인프라를 공유하지 않는 독립적 데이터센터 하나다.
데이터를 여러 AZ에 복제해 놓으면 장애 여파를 최소화할 수 있다.
소거 코드
데이터를 3중으로 다중화하면 대략 99.9999%의 내구성을 달성할 수 있다.
소거 코드는 데이터 내구성을 다른 관점에서 달성하려 시도한다.
데이터를 작은 단위로 분할하여 다른 서버에 배치하는 한편, 그 가운데 일부가 소실되었을 때 복구하기 위한 parity라는 정보를 만들어 중복성을 확보하는 것이다.
장애가 생기면 남은 데이터와 패리티를 조합하여 소실된 부분을 복구한다.
4+2 소거 코드 사례를 살펴보자.
- 데이터를 네 개의 같은 크기 단위로 분할(d1, d2, d3, d4)
- 수학 공식을 사용하여 패리티 p1, p2 계산
- p1 = d1 + 2 x d2 - d3 + 4 x d5
- p2 = -d1 + 5 x d2 + d3 - 3 x d
- 데이터 d3와 d4가 노드 장애로 소실되었다고 가정
- 남은 값 d1, d2, p1, p2와 패리티 계산에 쓰인 수식을 결합하면 d3와 d4 복원 가능
8+4 소거 코드 사례를 살펴보자.
데이터를 다중화 할 경우 데이터 라우터는 객체 데이터를 하나의 건강한 노드에서 읽으면 충분했지만 소거 코드를 사용하면 최대 8개의 건강한 노드에서 가져와야 한다.
소거 코드의 구조적 단점이다.
정리하자면 응답 지연은 높아지는 대신 내구성은 향상되고 저장소 비용은 낮아진다.
객체 저장소는 저장 비용이 대부분이므로 이런 타협적 측면은 고려할 가치가 있다.
데이터 다중화와 소거 코드의 장단점
구분 다중화 소거 코드 내구성 99.9999%(3중 복제의 경우) 99.999999999%(8+4 소거 코드를 사용하는 경우), 따라서 내구성은 소거 코드가 우월 저장소 효율성 200%의 저장 용량 오버헤드 50%의 저장 용량 오버헤드, 따라서 소거 코드가 우월 계산 자원 계산이 필요 없음, 다중화가 우월 패리티 계산에 많은 계산 자원 소모 쓰기 성능 데이터를 여러 노드에 복제, 추가 계산 없음 → 다중화가 우월 데이터를 디스크에 기록하기 전에 패리티 계산 필요 → 쓰기 지연 증가 읽기 성능 장애 발생 시 남은 노드에서 읽음 → 다중화가 우월 읽을 때 클러스터 내 여러 노드에서 데이터 가져와야 함, 장애 시 복구 필요 → 지연 증가
정확성 검증
소거 코드를 사용하면 적당한 비용으로 높은 데이터 내구성을 달성할 수 있다.
가령 어떤 디스크에 장애가 생긴 사실이 발견되면 해당 데이터 노드에 장애가 생긴 것으로 간주하고 훼손된 데이터는 parity 데이터 블록을 통해 복구할 수 있다.
메모리 데이터가 훼손되는 문제는 프로세스 경계에 데이터 검증을 위한 checksum을 두어 해결할 수 있다.
checksum은 데이터 에러를 발견하는 데 사용되는 작은 크기의 데이터 블록이다.
원본 데이터의 체크섬을 알면 전송 받은 데이터의 정확성은 해당 데이터의 체크섬을 다시 계산한 후 다음과 절차로 확인 가능하다.
- 새로 계산한 체크섬이 원본 체크섬과 다르면 데이터가 망가진 것
- 같은 경우에는 아주 높은 확률로 데이터는 온전하다고 봄
파일을 읽기 전용으로 전환하기 직전에 전체 파일의 체크섬을 계산한 다음에 파일 말미에 추가한다.
(8 + 4) 소거 코드와 체크섬 확인 메커니즘을 동시에 활용하는 경우에는 객체 데이터를 읽을 때마다 다음의 절차를 수행한다.
- 객체 데이터와 체크섬을 가져옴
- 수신된 데이터의 체크섬 계산
- 두 체크섬이 일치하면 데이터에는 에러가 없다고 간주
- 체크섬이 다르면 데이터는 망가진 것이므로 다른 장애 도메인에서 데이터를 가져와 복구 시도
- 데이터 8조각을 전부 수신할 때까지 1과 2를 반복 -> 원래 객체를 복원한 다음 클라이언트에게 전송
메타데이터 모델
스키마
DB 스키마는 다음 3가지 질의를 지원할 수 있어야 한다.
- 질의 1: 객체 이름으로 객체 ID 찾기
- 질의 2: 객체 이름에 기반하여 객체 삽입 또는 삭제
- 질의 3: 같은 접두어를 갖는 버킷 내의 모든 객체 목록 확인
bucket 테이블의 규모 확장
보통 한 사용자가 만들 수 있는 버킷의 수에는 제한이 있으므로, 이 테이블의 크기는 작다.
모든 읽기 요청을 처리하기에 CPU 용량이나 네트워크 대역폭이 부족하다면 DB 사본을 만들어 읽기 부하를 분산하면 된다.
object 테이블 규모 확장
object 테이블에는 객체 메타데이터를 보관한다.
샤딩을 통해 객체 메타데이터 테이블의 규모를 확장한다.
bucket_id를 기준으로 샤딩하면 핫스팟 샤드를 지원하지 못한다.
object_id를 기준으로 샤딩하면 질의 1과 2를 효율적으로 지원하지는 못한다.(두 질의는 URI를 기준으로 함)
bucket_id와 object_id를 결합하여 샤딩에 사용한다.
대부분의 메타데이터 관련 연산이 객체 URI를 기준으로 한다.
객체 ID를 찾는 연산도 입력이 URI이고, 객체를 업로드하는 연산도 마찬가지다.
bucket_name을 데이터와 균등하게 분산하려면 object_name의 순서쌍을 해싱한 값을 샤딩키로 사용하면 된다.
이 샤딩 방안을 활용하면 첫 두 질의는 간단히 지원할 수 있지만 세 번째 질의는 다소 애매하다.
버킷 내 객체 목록 확인
객체 저장소는 객체를 파일 시스템처럼 계층적 구조로 보관하지 않는다.
객체는 s3://<버킷 이름="">/<객체 이름="">의 수평적 경로로 접근한다.객체>버킷>
S3가 제공하는 목록 출력 명령어는 보통 다음과 같이 쓰인다.
- 어떤 사용자가 가진 모든 버킷 목록 출력
- 주어진 접두어를 가진, 같은 버킷 내 모든 객체 목록 출력
- 주어진 접두어를 가진, 같은 버킷 내 모든 객체를 재귀적으로 출력
분산 DB
메타데이터 테이블을 샤딩하면 어떤 샤드에 데이터가 있는지 모르므로 목록 출력 기능을 구현하기가 어렵다.
가장 단순한 해결책은 검색 질의를 모든 샤드에 돌린 다음 결과를 취합하는 것이다.
객체 버전
객체 버전은 버킷 안에 한 객체의 여러 버전을 둘 수 있도록 하는 기능이다.
이 기능이 있으면 실수로 지우거나 덮어 쓴 객체를 쉽게 복구할 수 있다.
- 클라이언트는 script.txt 객체를 업로드하기 위한 HTTP PUT 요청 전송
- API 서비스는 사용자의 신원을 확인하고 해당 사용자가 해당 버킷에 쓰기 권한을 가지고 있는지 확인
- 확인 결과 문제가 없으면 API 서비스는 데이터를 데이터 저장소에 업로드
- 데이터 저장소는 새 객체를 만들어 데이터를 영속적으로 저장하고 API 서비스에 새로운 UUID를 반환
- API 서비스는 메타데이터 저장소를 호출하여 새 객체의 메타데이터 정보를 보관
- 객체 메타데이터 생성
- 버전 기능을 지원하기 위해 메타데이터 저장소의 객체 테이블에는 object_version이라는 이름의 열 존재
- object_version 열은 버전 기능이 활성화 되었을 경우에만 사용
- 기존 레코드를 덮어쓰는 대신, bucket_id와 object_name은 같지만 object_id, object_version은 새로운 값인 레코드를 추가
- object_id는 3단계에서 반환된 새 객체의 UUID
큰 파일의 업로드 성능 최적화
큰 객체는 작게 쪼갠 다음 독립적으로 업로드한다.
모든 조각이 업로드되고 나면 객체 저장소는 그 조각을 모아서 원본 객체를 복원한다.
이 과정을 multipart 업로드라고 부른다.
- 클라이언트가 멀티파트 업로드를 시작하기 위해 겍체 저장소 호출
- 데이터 저장소가 uploadId 반환
- 클라이언트는 파일을 작은 객체로 분할한 뒤에 업로드 시작
- 조각 하나가 업로드 될 때마다 데이터 저장소는 ETag를 반환
- ETag는 기본적으로 해당 조각에 대한 MD5 해시 체크섬으로 멀티파트 업로드가 정상적으로 되었는지 검사할 때 이용
- 모든 조각을 업로드하고 나면 클라이언트는 멀티파트 업로드를 종료하라는 요청 전송
- 데이터 저장소는 전송 받은 조각 번호 목록을 사용해 원본 객체를 복원
이 접근법에 생길 수 있는 한 가지 문제는 객체 조립이 끝난 뒤에는 조각들은 더 이상 쓸모가 없다는 것이다.
이런 조각을 삭제하여 저장 용량을 확보하는 쓰레기 수집 프로세슬르 구현할 필요가 있을 수 있다.
쓰레기 수집
쓰레기 수집(garbage collection)은 더 이상 사용되지 않는 데이터에 할당된 저장 공간을 자동으로 회수하는 절차다.
- 객체의 지연된 삭제: 삭제했다고 표시는 하지만 실제로 삭제 X
- 갈 곳 없는 데이터: 반쯤 업로드된 데이터, 또는 취소된 멀티파트 업로드 데이터
- 훼손된 데이터: 체크섬 검사에 실패한 데이터
쓰레기 수집기는 개체를 데이터 저장소에서 바로 지우지 않는다.
삭제된 객체는 정리 메커니즘을 주기적으로 실행하여 지운다.
- 쓰레기 수집기는 /data/b의 객체를 /data/d로 복사
- 그 과정에서 삭제된 객체임을 알리는 플래그 값이 참인 객체들인 ‘객체 2’와 ‘객체 5’는 건너뛴다는 것에 유의
- 모든 객체를 복사한 다음 쓰레기 수집기는 object_mapping 테이블을 갱신
- 데이터의 일관성을 보장하기 위해 file_name과 start_offset에 대한 갱신 연산은 같은 트랜잭션 안에서 수행하는 것이 바람직