읽은 책/[책] 가상 면접 사례로 배우는 대규모 시스템 설계 기초 2권

9장. S3와 유사한 객체 저장소

코드몬스터 2024. 8. 28. 08:56
728x90

 

S3와 유사한 객체 저장소 서비스를 설계한다.

S3는 AWS가 제공하는 서비스로 RESTful API 기반 인터페이스로 이용 가능한 객체 저장소이다.

 

1. 저장소 시스템 101

1) 블록 저장소

  • HDD(Hard Disk Drive)나 SSD(Solid State Drive)처럼 서버에 물리적으로 연결되는 형태의 드라이브는 블록 저장소의 가장 흔한 형태다.
  • 블록 저장소는 원시 블록(raw block)을 서버에 볼륨(volume) 형태로 제공한다.
  • 서버는 원시 블록을 포맷한 다음 파일 시스템으로 이용하거나 애플리케이션에 블록 제어권을 넘겨버릴 수도 있다.
  • 데이터베이스나 가상 머신 엔진 같은 애플리케이션은 원시 블록을 직접 제어하여 최대한의 성능을 끌어낸다.
  • 서버에 물리적으로 직접 연결되는 저장소에 국한되지 않고, 고속 네트워크를 통해 연결할 수도 있고 업계 표준 연결 프로토콜인 FC(Fibre Channel)이나 iSCSI를 통해 연결될 수도 있다.
  • 개념적으로 보자면 네트워크를 통해 연결되는 블록 저장소도 원시 블록을 제공한다는 점에서는 다르지 않다.

2) 파일 저장소

  • 파일 저장소는 블록 저장소 위에 구현된다.
  • 파일과 디렉터리를 손쉽게 다루는데 필요한, 더 높은 수준의 추상화를 제공한다.
  • 데이터는 계층적으로 구성되는 디렉터리 안에 보관된다.
  • SMB/CIFS이나 NFS와 같은 파일 수준 네트워크 프로토콜을 사용하면 하나의 저장소를 여러 서버에 동시에 붙일 수도 있다.
  • 파일 저장소를 사용하는 서버는 블록을 직접 제어하고, 볼륨을 포맷하는 등의 까다로운 작업을 신경 쓸 필요 없기 때문에 단순하여 사용하기 좋다.

3) 객체 저장소

  • 객체 저장소는 새로운 형태의 저장소이다.
  • 데이터 영속성을 높이고 대규모 애플리케이션을 지원하며 비용을 낮추기 위해 의도적으로 성능을 희생한다.
  • 실시간으로 갱신할 필요가 없는 상대적으로 '차가운(cold)' 데이터 보관에 초점을 맞추며 데이터 아카이브나 백업에 주로 쓰인다.
  • 모든 데이터를 수평적 구조 내에 객체로 보관한다.
  • 데이터 접근은 보통 RESTful API를 통한다.
  • 다른 유형의 저장소에 비해 상대적으로 느리다.

4) 용어 정리

버킷(bucket)

  • 객체를 보관하는 논리적 컨테이너.
  • 버킷은 전역적으로 유일해야 하고, S3에 업로드하려면 버킷부터 만들어야 한다.

객체(object)

  • 버킷에 저장하는 개별 데이터를 말한다.
  • 데이터(페이로드(payload))메타데이터를 갖는다.
  • 객체 데이터로는 어떤 것도 가능하다.
  • 메타 데이터는 개체를 기술하는 이름-값 쌍의 집합이다.

버전(versioning)

  • 한 객체의 여러 버전을 같은 버킷 안에 둘 수 있도록 하는 기능이다.
  • 실수로 지웠거나 덮어 쓴 객체를 복구할 수 있도록 한다.

URI(Uniform Resource Identifier)

  • 객체 저장소는 버킷과 객체에 접근할 수 있도록 하는 RESTful API를 제공한다.
  • 각 객체는 해당 API URI를 통해 고유하게 식별할 수 있다.

SLA(Service-Level Agreement)

  • 서비스 수준 협약은 서비스 제공자와 클라이언트 사이에 맺어지는 계약이다.

1단계: 설계 범위

1. 기능 요구사항

  • 버킷 생성
  • 객체 업로드 및 다운로드
  • 객체 버전
  • 버킷 내 객체 목록 출력 가능

2. 비기능 요구사항

  • 100PB 데이터
  • 식스 나인(99.9999%) 수준의 데이터 내구성
  • 포 나인(99.99%) 수준의 서비스 가용성
  • 저장소 효율성: 높은 수준의 안정성과 성능은 보증하되 저장소 비용은 최대한 낮추어야 한다.

3. 개략적인 규모 추정

  • 디스크 용량: 객체 크기가 아래 분포를 따른다.
    • 객체 가운데 20%는 크기가 1MB 미만의 작은 객체
    • 60% 정도의 객체는 1MB ~ 64MB 정도 크기의 중간 크기 객체
    • 나머지 20% 정도는 64MB 이상의 대형 객체
  • IOPS: 인터페이스를 탑재하고 7200rmp을 지원하는 하드 디스크 하나가 초당 100~ 150회의 임의 데이터 탐색을 지원할 수 있다고 가정한다.(100 ~150 IOPS)

소형 객체는 0.5MB, 중형 크기 객체는 32MB, 대형 크기 객체는 200MB 용량을 기준으로 계산

  • 6억 8천만개 객체
  • 100PB 
  • 모든 객체의 메타데이터 크기가 대략 1KB 정도라고 가정하면 모든 메타 데이터 정보를 저장하기 위해 0.68TB 정도의 공간이 필요

2단계: 개략적 설계안

객체 불면성(object immutability)

객체 저장소와 다른 두 가지 유형 저장소 시스템의 가장 큰 차이는 객체 저장소에 보관되는 객체들은 변경이 불가능하다.

삭제한 다음 새 버전 객체로 완전히 대체할 수 는 있어도 그 값은 점진적으로 변경이 불가능하다.

 

키-값 저장소(key-value store)

객체 저장소를 사용하는 경우 해당 객체의 URI를 사용하여 데이터를 가져올 수 있다.

이 때 URI는 키이고 데이터는 값에 해당하므로 키-값 저장라고 볼 수 있다.

 

저장은 1회, 읽기는 여러 번:

 

소형 및 대형 객체 동시 지원: 다양한 크기의 객체를 문제 없이 저장할 수 있다.

1. 개략적 설계안

로드 밸런서: RESTful API에 대한 요청을 API 서버들에 분산하는 역할을 담당

API 서비스: IAM(Identity & Access Management) 서비스, 메타데이터 서비스, 저장소 서비스에 대한 호출을 조율하는 역할을 담당

IAM 서비스: 인증, 권한 부여, 접근 제어 등을 중아에서 맡아 처리한다. 인증은 호출 주제차 누구인지 확인하는 작업이고, 권한 부여는 인증된 사용자가 어떤 작업을 수행할 수 있는지 검증하는 과정이다.

데이터 저장소: 실제 데이터를 보관하고 필요할 때마다 읽어가는 장소다.

메타데이터 저장소: 객체 메타데이터를 보관하는 장소다.

 

데이터 자장소와 메타데이터 저장소는 논리적인 구분일 뿐이며 구현 방법은 여러 가지가 있을 수 있다.

2. 객체 업로드

객체 업로드 과정

  1. 클라이언트는 bucket-to-share  버킷을 생성하기 위한 HTTP PUT 요청을 보낸다.
  2. API 서비스는 IAM을 호출하여 해당 사용자가 WRITE 권한을 가졌는지 확인한다.
  3. API 서비스는 메타데이터 데이터베이스에 버킷 정보를 등록하기 위해 메타 데이터 저장소를 호출한다.
  4. 버킷이 만들어지고 나면 클라이언트는 script.txt. 객체를 생성하기 위한 HTTP PUT 요청을 보낸다.
  5. API 서비스는 해당 사용자 신원 및 WRITE 권한 소유 여부를 확인한다.
  6. 확인 결과 문제가 없으면 API 서비스는 HTTP PUT 요청 몸체에 실린 객체 데이터를 데이터 저장소로 보낸다.
  7. 데이터 저장소는 해당 데이터를 객체로 저장하고 해당 객체의 UUID를 반환한다.
  8. API 서비스는 메타데이터 저장소를 호출하여 새로운 항목을 등록한다.
    => 항목에는 object_id(UUID), bucket_id(객체가 속한 버킷), object_name 등의 정보가 포함된다.

3. 객체 다운로드

  • 버킷은 디렉토리 같은 계층 구조를 지원하지 않는다.
  • 하지만, 버킷 이름과 객체를 연결하면 폴더 구조를 흉내 내는 논리적 계층을 만들 수 있다.

 

해당 호출 사례

  1. 클라이언트는 GET /bucket-to-share/script.txt 요청을 로드밸런서로 보낸다.
  2. 로드밸런서는 해당 요청을 API 서버로 보낸다.
  3. API 서비스는 IAM을 질의하여 사용자가 해당 버킷에 READ 권한을 가지고 있는지 확인한다.
  4. 권한이 있음을 확인하면 API 서비스는 해당 객체의 UUID를 메타데이터 저장소에 가져온다.
  5. API 서비스는 해당 UUID를 사용해 데이터 저장소에서 객체 데이터를 가져온다.
  6. API 서비스는 HTTP GET 요청에 대한 응답으로 해당 객체 데이터를 반환한다.

3단계: 상세 설계

1. 데이터 저장소

  • API 서비스는 사용자의 요청을 받으면 그 요청을 처리하기 위해 다른 내부 서비스들을 호출한다.
  • 객체를 저장하거나 가져오는 작업은 데이터 저장소를 호출하여 처리한다.

2. 데이터 저장소의 개략적 설계

데이터 저장소 컴포넌트

3. 데이터 라우팅 서비스

데이터 라우팅 서비스는 데이터 노드 클러스터에 접근하기 위한 RESTful 또는 gRPC 서비스를 제공한다.

더많은 서버를 추가하여 쉽게 규모를 확장할 수 있는 무상태 서비스다.

  • 배치 서비스를 호출하여 데이터를 저장할 최적의 데이터 노드를 판단
  • 데이터 노드에서 데이터를 읽어 API 서비스에 반환
  • 데이터 노드에 데이터 기록

4. 배치 서비스

  • 어느 데이터 노드에 데이터를 저장할지 결정하는 역할을 담당한다.
  • 내부적으로 가상 클러스터 지도를 유지하는데, 이 지도에서 클러스터의 물리적 형상 정보가 보관된다.
  • 이 지도에 보관되는 데이터 노드의 위치 정보를 이용하여 데이터 사본이 물리적으로 다른 위치에 놓이도록 한다.
    => 물리적인 분리는 높은 데이터 내구성을 당성하는 핵심 요소이다.
  • 배치 서비스는 아주 중요한 서비스이므로 5개에서 7개의 노드를 갖는 배치 서비스 클러스터를 팩서스나 래프트 같은 합의 프로토콜을 사용하여 구축할 것을 권장한다.
    => 합의 프로토콜은 일부 노드에 장애가 생겨도 건강한 노드 수가 클러스터 크기의 절반 이상이면 서비스를 지속할 수 있도록 보장한다.

5. 데이터 노드

  • 실제 객체 데이터가 보관되는 곳이다.
  • 여러 노드에 데이터를 복제함으로써 데이터의 안정성과 내구성을 보증하는데, 이를 다중화 그룹이라고 부른다.
  • 데이터 노드에는 배치 서비스에 주기적으로 박동 메세지를 보내는 서비스 데몬이 돈다.
    • 해당 데이터 노드에 부착된 디스크 드라이브(HDD/SSD)의 수
    • 각 드라이브에 저장된 데이터의 양
    • 데이터 사본을 보관할 위치

6. 데이터 저장 흐름

데이터를 영속적으로 보관하는 흐름

 

  1. API 서비스는 객체 데이터를 데이터 자장소로 포워딩 한다.
  2. 데이터 라우팅 서비스는 해당 객체에 UUID를 할당하고 배치 서비스에 해당 객체를 보관할 데이터 노드를 질의한다.
    배치 서비스는 가상 클러스터 지도를 확인하여 데이터를 보관할 주 데이터 노드를 반환한다.
  3. 데이터 라우팅 서비스는 저장할 데이터를 UUID와 함께 주 데이터 노드에 직접 전송한다.
  4. 주 데이터 노드는 데이터를 자기 노드에 지역적으로 저장하는 한편, 두 개의 부 데이터 노드에 다중화한다.
    주 데이터 노드는 모든 부 데이터 노드에 성공적으로 다중화하고 나면 응답을 보낸다.
  5. 객체의 UUID, 즉 객체 ID를 API 서비스에 반환한다.

 

배치 서비스는 UUID를 입력으로 주고 질의하면 해당 객체에 대한 다중화 그룹이 반환된다는 뜻인데 어떻게 계산을 수행할까?

=> 조회 연산 구현에는 보통 안전 해시를 사용한다.

 

4단계는 응답을 반환하기 전에 데이터를 모두 부노드에 다중화한다는 뜻이다.

  • 따라서 모든 데이터 노드에 강력한 데이터 일관성이 보장된다.
  • 하지만 가장 느린 사본에 대한 작업이 완료될 때까지 응답을 반환하지 못하므로, 지연 시간 측면에서는 손해다.
  • 데이터 일관성과 지연 시간 사이에 타협적 관계(trade-off)가 있다.

7. 데이터는 어떻게 저장되는가

가장 단순한 방안은 객체를 개별 파일로 저장하는 것이지만, 작은 파일들이 많아지면 성능이 떨어진다.

  1. 낭비되는 데이터 블록 수가 늘어난다.
    • 파일 시스템은 파일을 별도의 디스크 블록으로 저장한다.
    • 작은 파일을 저장할 때도 블록 하나를 온전히 쓰기 때문에 작은 파일이 많아지면 낭비되는 블록이 늘어난다.
  2. 시스템의 아이노드(inode) 용량 한계를 초고하는 문제다.
    • 파일 시스템은 파일 위치 등의 정보를 아이노드라는 특별한 유형의 블록에 저장한다.
    • 사용 가능한 아이노드의 수는 디스크가 초기화되는 순간에 결정된다.
    • 작은 파일의 수가 수백만에 달하게 되면 아이노드가 전부 소진될 가능성이 생긴다.

작은 객체들을 큰 파일 하나로 모아서 해결하는 방법은 WAL(Write-Ahead Log)와 같이 객체를 저장할 때 이미 존재하는 파일에 추가하는 방식이다.

  • 용량 임계치에 도달한 파일은 읽기 전용 파일로 변경하고 새로 파일을 만든다.
  • 읽기 전용으로 변경된 파일은 오직 읽기 요청만 처리한다.
  • 읽기-쓰기 파일에 대한 쓰기 연산은 순차적으로 이루어져야 한다는 것에 유의하자.
  • 객체는 파일에 일렬로 저장되는데, 이러한 레이아웃을 유지하려면 여러 CPU 코어가 쓰기 연산을 병렬로 진행하더라도 객체 내용이 뒤섞이는 일은 없어야 한다.
    • 파일에 객체를 기록하기 위해서는 자기 순서를 기다려야 한다는 뜻이기도 하다.
    • 많은 코어를 갖는 현대적 서버 시스템의 경우, 이렇게 하면 쓰기 대역폭이 심각하게 줄어든다는 문제가 있다.
    • 따라서, 코어별로 전담 읽기-쓰기 파일을 두어야 한다.

8. 객체 소재 확인

각각의 데이터 파일 안에 많은 작은 객체가 들어 있다면 데이터 노드는 어떻게 UUID로 객체 위치를 찾을 수 있을까?

  • 객체가 보관된 데이터 파일
  • 데이터 파일 내 객체 오프셋
  • 객체 크기

object_mapping 테이블 필드 의미

필드 설명
object_id 객체의 UUID
file_name 객체를 보관하는 파일의 이름
start_offset 파일 내 객체의 시작 주소
object_size 객체의 바이트 단위 크기

 

해당 정보를 저장하는 두 가지 방법

  1. RocksDB 같은 파일 기반 키-값 저장소를 이용
    • RocksDB 는 SSTable에 기반한 방법으로, 쓰기 연산 성능은 아주 좋지만 읽기 성능은 느리다.
  2. 관계형 데이터베이스를 이용
    • B+ 트리 기반 저장 엔진을 이용하며 읽기 연산 성능은 좋지만 쓰기 성능은 느리다.

object_mapping 에 저장되는 데이터는 한 번 기록된 후에는 변경되지 않으며, 읽기 연산이 아주 빈번하게 발생한다.

관계형 데이터베이스가 나은 선택이다.

 

그러면 막대한 양의 데이터를 어떻게 구성하면 좋을까?

  • 데이터 노드에 저장되는 위치 데이터를 다른 데이터 노드와 공유할 필요가 없다는 점이다.
  • 따라서, 데이터 노드마다 관계형 데이터베이스를 설치하면 된다.

9. 개선된 데이터 저장 흐름

  1. API 서비스는 새로운 객체를 저장하는 요청을 데이터 노드 서비스에 전송한다.
  2. 데이터 노드 서비스는 읽기-쓰기 파일의 마지막 부분에 추가한다.
  3. 해당 객체에 대한 새로운 레코드를 object_mapping 테이블에 추가한다.
  4. 데이터 노드 서비스는 API 서비스에 해당 객체의 UUID를 반환한다.

10. 데이터 내구성

99.9999% 수준의 데이터 내구성을 제공하는 저장소 시스템을 만들려면 무엇이 필요한가? 

장애가 ㅂ라생할 모든 경우를 세심하게 살핀 다음 데이터를 적절히 다중화할 필요가 있다.

 

1) 하드웨어 장애와 장애 도메인

  • 드라이브 한대로 원하는 내구성 목표를 달성하기 불가능하기 때문에, 데이터를 여러 대의 하드 드라이브에 복제하여 어떤 드라이브에서 발생한 장애가 전체 데이터 가용성에 영향을 주지 않도록 하는 것이다.
  • 데이터를 3중 복제하면 내구성은 개략적 수치로 0.999999이다.
  • 완전한 내구성 평가를 위해서는 여러 장애 도메인의 영향을 복합적으록 고려해야한다.
  • 장애 도메인은 중요한 서비스에 문제가 발생했을 때 부정적인 영향을 받는 물리적 또는 논리적 구획을 일컫는다.

 

2) 소거 코드

  • 소거 코드는 데이터 내구성을 다른 관점에서 달성하려 시도한다.
  • 데이터를 작은 단위로 분할하여 다른 서버에 배치하는 한편, 가운데 일부가 소실되었을 때 복구하기 위한 패리티(parity)라는 정보를 만들어 중복성(redundancy)을 확보하는 것이다.
  • 장애가 생기면 남은 데이터와 패리티를 조합하여 소실된 부분을 복구한다.
  • 소커 코드 사례
    1. 데이터를 네 개의 같은 크기 단위로 분할(d1, d2, d3, d4)
    2. 수학 공식을 사용하여 패리티 p1, p2를 계산한다.
    3. 데이터 d3와 d4가 노드 장애로 소실되었다고 가정
    4. 남은 값 d1, d2, p1, p2와 패리티 계산에 쓰인 수식을 결합하면 d3와 d4를 복원할 수 있다.
  • 단점으로 데이터 다중화의 경우 하나의 겅강한 노드에서 읽으면 충분하지만 소거 코드를 사용하면 최대 8개의 노드에서 데이터를 가져와야 한다.
    => 응답 지연은 높이지는 대신 내구성은 향상되고 저장소 비용은 낮아진다.
  • 소거 코드를 사용하면 2개 데이터 블록에 하나의 패리티 블록이 필요하므로 오버헤드는 50%이다.
  • 3개 중 복제 다중화 방안을 채택하는 경우에는 200%이다.

 

응답 지연이 중요한 애플리케이션에는 다중화 방안이 좋고 저장소 비용이 중요한 애플리케이션에는 소거 코드가 좋다.

  다중화 소거 코드
내구성 99.9999%(3중 복제의 경우) 99.9999999999%(8+4 소커 코드를 사용하는 경우)
저장소 효율성 200%의 저장 용량 오버헤드 50%의 저장 용량 오버헤드
계산 자원 계산이 필요 없음 패리티 계산에 많은 자원 소몬
쓰기 성능 데이터를 여러 노드에 복제, 추가로 필요한 계산이 없음 데이터를 디스크에 기록하기 전에 패리티 계산이 필요하므로 쓰기 연산의 응답 지연이 증가
읽기 성능 장애가 발생하지 않는 노드에서 데이터를 읽음 데이터를 읽어야 할 때마다 클러스터 내의 여러 노드에서 데이터를 가져와야함.  지연시간 증가

 

3) 정확성 검증

대구모 시스템의 경우, 데이터 훼손 문제는 디스크에 국한되지 않는다. 메모리의 데이터가 망가지는 일도 자주 일어난다.

 

  • 메모리 데이터가 훼손되는 문제는 프로세스 경계에 데이터 검증을 위한 체크섬(checksum)을 두어 해결할 수 있다.
  • 체크섬은 데이터 에러를 발견하는데 사용되는 작은 크기의 블록이다.

 

원본 데이터의 체크섬을 알면 전송 받은 데이터의 정확성은 해당 데이터의 체크섬을 다시 계산한 후 아래와 같은 절차로 확인 가능하다.

  • 새로 계산한 체크섬이 원본 체크섬과 다르면 데이터가 망가진 것이다.
  • 같은 경우에는 아주 높은 확률로 데이터는 온전하다고 볼 수 있다.
  • 체크섬 알고리즘은 MD5, SHA1, HMAC 등 다양하다.

(8+4) 소거 코드와 체크섬 확인 메커니즘을 동시에 활용하는 절차는 다음과 같다.

  1. 객체 데이터와 체크섬을 가져온다.
  2. 수신된 데이터의 체크섬을 계산한다.
    1. 두 체크섬이 일치하면 데이터에는 에러가 없다고 간주한다.
    2. 체크섬이 다르면 데이터는 망가진 것이므로 다른 장애 도메인에서 데이터를 가져와 복구를 시도한다.
    3. 데이터 8조각을 전부 수신할 때까지 1과 2를 반복한다. 그런 다음 원래 객체를 복원한 다음 클라이언트에게 보낸다.

11. 메타데이터 모델

1) 스키마

스키마는 3가지 질의를 지원할 수 있어야 한다.

  1. 객체 이름으로 객체 ID 찾기
  2. 객체 이름에 기반하여 객체 삽입 또는 삭제
  3. 같은 접두어를 갖는 버킷 내의 모든 객체 목록 확인

메타데이터 데이터베이스 스키마

 

2) bucket 테이블의 규모 확장

  • 사용자가 만들 수 있는 버킷의 수에는 제한이 있으므로, 테이블의 크기는 작다.
  • 전체 테이블은 최신 데이터베이스 서버 한 대에 충분히 저장할 수 있지만, 모든 읽기 요청을 처리하기에는 CPU 용량이나 네트워크 대역폭이 부족할 수 있다.
  • 이런 경우에는 데이터베이스 사본을 만들어 일기 부하를 분산한다.

3) ojbect 테이블의 규모 확장

  • ojbect 테이블에는 객체 메타데이터를 보관한다.
  • 객체 메타데이터를 데이터베이스 서버 한대에 보관하기는 불가능하다.
  • 테이블을 샤딩하는 방법
    1. bucket_id를 기준으로 삼아 같은 버킷내 객체는 같은 샤드에 배치되도록 하는 것이다.
      => 버킷 안에 수십 억개의 객체가 있는 핫스팟 샤드를 지원하지 못하므로 좋은 방법이 아니다.
    2. object_id를 기준으로 샤딩하는 것
      => 부하를 균등하게 분산한다는 측면에서는 괜찮은 방법이다.
  • 본 설계안에서는 bucket_name과 object_name을 결합하여 샤딩에 사용한다.
    => 대부분의 메타데이터 관련 연산이 객체 URI를 기준으로 하기 때문이다.

4) 버킷 내 객체 목록 확인
객체 저장소는 객체를 파일 시스템처럼 게층적 구조로 보관하지 않는다.

객체는 s3://<버킷 이름>/<객체 이름>의 수평적 경로로 접근하다.

ex) s3://mybucket/abc/d/e/f/file.txt

  • mybueckt은 버킷 이름이다.
  • abc/d/e/f/file.txt는 개체 이름이다.

사용자가 버킷 내 객체들을 잘 정리할 수 있도록 하기 위해 s3는 접두어(prefix) 라는 개념을 지원한다.

접두어를 잘 사용하면 디렉터리와 비슷하게 데이터를 정리할 수 있다.

 

asw s3가 제공하는 목록 출력 명령어 예시

  1. 어떤 사용자가 가진 모든 버킷 목록 출력
    • aws s3 list-buckets
  2. 주어진 접두어를 가진, 같은 버킷 내 모든 객체 목록 출력
    • aws s3 ls s3://mybucket/abc/
  3. 주어진 접두어를 가진, 같은 버킷 내 모든 객체를 재귀적으로 출력.
    • aws s3 ls s3://mybucket/abc/ --recursive

12. 단일 데이터베이스 서버

  1. 특정 사용자가 가진 모든 버킷을 출력하기 위한 질의
    • SELECT * FROM bucket WHERE owner_id={id}
  2. 같은 접두어를 갖는, 버킷 내 모든 객체를 출력하려면 다음 질의
    • SELECT * FROM obejct WHERE bucket_id = "123" AND object_name LIKE `abc/%`
    • bucket_id 값이 123이며 abc/를 공통 접두어로 갖는 모든 객체를 찾는다.

13. 분산 데이터베이스

메타데이터 테이블을 샤딩하면 어떤 샤드에 데이터가 있는지 모르므로 목록 출력 기능을 구현하기 어렵다.

가장 단순한 해결책은 검색 질의를 모든 샤드에 돌린 다음 결과를 취합하는 것이다.

 

  1. 메타데이터 세비스는 모든 샤드에 질의를 돌림
    SELECT * FROM obejct
    WHERE bueckt_id = "123" AND object_name LIKE `a/b/%`
  2. 메타 데이터 서비스는 각 샤드가 반환한 객체들을 취합하여 그 결과를 호출 클라이언트에 반환
    • 페이지 나눔(patination) 기능을 구현하기 복잡하다.
    • 다음 10개를 얻고자 할 때 사용자는 요청에 힌트를 담아 서버로 하여금 OFFSET의 값을 10으로 설정하여 두 번 째 페이지에 보일 객체에 대한 질의문을 만들도록 한다.
    • 힌트는 서버가 클라이언트에 각 페이지를 보낼 때 붙여 보내는 커서(cursor)를 말한다.
      SELECT * FROM obejct
      WHERE bueckt_id = "123" AND object_name LIKE `a/b/%`
      ORDER BY object_name OFFSET 0 LIMIT 10

데이터베이스를 샤딩하면 왜 페이지 나눔 기능을 구현하기 어려운지 살펴보자.

  • 객체가 여러 샤드에 나눠져 있으므로, 샤드마다 반환하는 객체 수는 제각가이다.
  • 애플리케이션 코드는 모든 샤드의 질의 결과를 받아 취합한 다음 정렬하여 그 중 10개만 추려야 한다.
  • 반환할 페이지에 포함되지 못한 객체는 다음에 다시 고려해야 한다.
    => 즉, 샤드마다 추적해야 하는 오프셋이 달라질 수 있다.
    => 서버는 모든 샤드의 오프셋을 추적하여 커서에 결부시킬 수 있어야 한다.
  • 객체 저장소는 규모와 내구성 최적화에 치중하고, 객체 목록 출력 명령의 성능을 보장하는 것은 우선위가 높지 않다.
  • 버킷 ID로 샤딩하는 별도 테이블에 목록 데이터를 비정규화하는 것도 한 가지 방법이다.

14. 객체 버전

  • 객체 버전은 버킷 안에 한 객체의 여러 버전을 둘 수 있도록 하는 기능이다.
  • 해당 기능이 있으면 실수로 지우거나 덮어 쓴 객체를 쉽게 복구할 수 있다.
  • 객체를 삭제할 떄는 해당 객체의 모든 버전을 버킷 안에 그대로 둔 채 단순히 삭제 표식(delete marker)만 추가한다.
  • 삭제 표식은 객체의 새로운 버전이다. 따라서 삽입되는 순간에 해당 객체의 새로운 '현재 버전'이 된다.
  • 현재 버전 객체를 가져오는 GET요청을 보내면 404 Object Not Found 오류가 반환된다.

15. 큰 파일의 업로드 성능 최적화

  • 객체 가운데 20% 정도는 크기가 크다고 가정한다.
  • 몇 GB 이상인 객체를 직접 업로드하는 것도 가능하지만 시간이 오래 걸린다.
  • 네트워크에 문제가 생기면 처음부터 다시 업로드해야한다.
  • 객체를 작게 쪼갠 다음 독립적으로 업로드하는 것이다.
    => 모든 조각이 업로드되고 나면 객체 저장소는 그 조각을 모아서 원본 객체로 복원하는데 이 과정을 멀티파트(multipart) 업로드라고 부른다.

16. 쓰레기 수집

  • 더 이상 사요되지 않는 데이터에 할당된 저장 공간을 자동으로 회수하는 절차이다.
  • 아래와 같은 쓰레기 데이터가 생길 수 있다.
    • 객체의 지연된 삭제: 삭제했다고 표시는 하지만 실제로 지우지는 않는다.
    • 갈 곳 없는 데이터: 반쯤 업로드된 데이터, 또는 취소된 멀티파트 업로드 데이터.
    • 훼손된 데이터: 체크섬 검사에 실패한 데이터.
  • 쓰레기 수집기는 바로 지우지 않고, 삭제된 객체는 정리 메커니즘을 주기적으로 실행하여 지운다.
  • 사용되지 않는 사본에 할당된 저장 공간을 회수하는 역할도 담당한다.
    • 주 저장소 노드뿐 아니라 부 저장소 노드에서도 지워야 한다.
    • (8+4) 소거 코드를 사용하는 경우에는 개체 하나를 지울 때 12개 노드에서 전부 지워야 한다.

4단계: 마무리

  • 이번 장의 핵심은 객체 저장소를 설계하는 것으로 객체 업로드, 다운로드, 버킷 내 객체 목록 표시, 객체 버전 등의 기능이 어떻게 구현되는지 살펴보았다.
  • 상세 설계를 진행하는 동안에는 데이터 저장소와 메타데이터 저장소가 어떻게 구현되는지 살펴보았다.
  • 데이터가 어떻게 영속적으로 저장되는지, 안정성과 내구성을 높이는 두 가지 방안(다중화 소거 코드)
  • 메타데이터 저장소에 대해서는 멀티파트 업로드가 어떻게 실행되는지 보았으며 쓰레기 수집 방법에 대해서도 살펴보았다.