앞선 챕터에서 분산 파일 시스템(DFS)라는 운영체제에 대한 공부를 했습니다. GFS와 HDFS의 근간이 되는 개념이죠. 그럼 또 한가지 의문이 생깁니다. HDFS는 GFS 논문을 보고 만들었고 GFS는 DFS 개념을 토대로 작성되었으니, 이제는 ‘GFS는 과연 무엇일까’하는 의문이 말이죠. 이 의문은 너무나도 자연스러워서 거부할 수 없는 흐름을 만들어냈습니다. 그렇기에 전 이번 챕터에서 HDFS를 알기위한 그 두번째 여정으로 GFS가 무엇인지에 대해 알아보려합니다. 다소 너무 돌아가는 듯한 느낌이 들 수도 있지만, Deep Inside라는 취지에 맞춰서 끈기있게 진행해 보도록 하겠습니다.
1. Google File Systme(GFS) 개요
구글 파일 시스템(이하 GFS)의 등장에는 2000년 전후에 기하급수적으로 성장하기 시작한 인터넷 시장이 있습니다. 인터넷 시장이 커짐에 따라 구글의 데이터 처리 요구 사항이 급증했고, 구글은 해당 욕구를 충족하기 위해 GFS를 설계하고 구현한 것이죠.
GFS는 기본적으로 이전의 분산 파일 시스템(이하 DFS)과 같은 목표들을 많이 공유하고 있습니다. 성능, 신뢰성, 확장성, 가용성, 내고장성같은 목표들말이죠. 하지만 구글은 동시에 그들의 애플리케이션의 워크로드나 자사의 기술적 환경을 관찰하여 그 결과를 GFS 설계에 반영했습니다. 초기 DFS의 가정이 구글이 현재 처한 상황과 확연히 다르다고 여겼기 때문입니다. 이에 따라 구글은 DFS 기존 특징들에 대해 재검토하고 근본적으로 다른 점을 탐구했습니다.
첫째, 컴포넌트(부품) 고장은 예외라기보다는 일반적이다.
구글 서버의 많은 컴퓨터들은 비교적 싸고, 평범한 범용 부품들로 구축된 수백, 수천대의 스토리지 시스템으로 구성됩니다. 이는 컴퓨터 중 일부는 작동하지 않을 수 있고, 고장으로부터 회복되지 않을 가능성이 높다는 것을 시사합니다. 실제로 구글은 어플리케이션 에러, OS 에러, 사람이 촉발한 에러, 디스크/메모리/커넥터/네트워크 및 전원 공급장치 에러가 발생하는 것을 관찰했습니다.
구글은 이러한 컴포넌트들의 고장 및 장애를 예외적인 상황으로 간주하는 대신, 일반적인 상황으로 산정했습니다. 그래서 구글은 지속적인 모니터링, 에러 탐지, 내고장성 등이 필수적으로 시스템에 적용되어야 한다고 여겼습니다.
둘째, 파일은 전통적인 기준으로 봤을 때 거대하며, 다중 GB(기가바이트) 파일은 일반적이다.
인터넷 시장이 커져감에 따라 구글이 처리해야했던 파일의 크기 또한 비대해져갔습니다. 다중 GB파일이 일반적이다라는 말은 GB(기가바이트. 우리가 아는 그파일단위 맞음 kb < mb < gb)단위의 파일들이 많아졌다는 정도로 이해하면 됩니다. 특히나 구글같이 웹 기반의 거대 IT 기업은 관리해야 하는 파일에 웹 문서나 응용 프로그램 객체가 포함되어 있기 때문에 해당 파일의 용량이 큰 건 일반적인 것이라고 할 수 있습니다.
하지만 이러한 거대한 파일을 전통적인 DFS처럼 KB(키로바이트)단위로 나누어서 다루는 것은 매우 까다로운 일입니다. 구글이 정기적으로 처리해야하는 데이터의 크기가 GB를 넘어 TB단위였고, 파일 시스템이 이를 지원할 수 있다고 하더라도 수십억개에 달하는 KB단위의 파일을 관리한다는건 너무나도 어렵기 때문입니다. 따라서 구글은 거대한 파일을 효율적으로 다루기 위해선, I/O 오퍼레이션이나 블록 사이즈같은 시스템의 전반적인 설계 가정이나 파라미터에 대한 재검토가 필요하다고 여겼습니다.
- I/O Operation
- 주변 장치와 주 메모리간에 데이터를 전송하고 중앙 처리 장치(CPU)가 연결된 주변 장치를 제어할 수 있도록 하는 일련의 I/O 작업(명령)
- 블록 사이즈
- 파일을 어느정도의 단위로 쪼개서 분산 시킬 것인가에 대한 것
셋째, 대부분의 파일은 기존 데이터를 덮어쓰기보단 새로운 데이터를 추가하는 방식으로 변경된다.
구글은 대부분의 파일이 기존 데이터를 덮어쓰기(Overwrite)보단 새로운 데이터를 추가(Append)하는 방식으로 변경된다는 것을 관찰했습니다. 또한 파일 내에서 임의 쓰기는 사실상 존재하지 않으며, 한번 작성된 파일은 보통 읽기 전용이고 순차적으로 읽힌다는 것을 알았습니다. 그리고 데이터 스트림, 아카이브 데이터, 중간 결과물 형태의 데이터 등과 같은 다양한 데이터들이 이러한 특성을 공유한다는 것을 알게됐습니다.
이로써 구글은 이러한 파일 접근 방식이 대용량 파일에 적용했을 때를 가정했을 때, 기능 향상과 원자성 보장(Atomicity Guarantee)을 위해서 중점을 두어야 할 부분이 클라이언트의 데이터 블록 캐싱 알고리즘이 아니라 파일의 추가(append) 알고리즘이라고 결론지었습니다.
넷째, 어플리케이션과 파일 시스템 API를 공동 설계하면 유연성을 높여 전체 시스템이 도움이 된다.
예를 들어 GFS의 일관성 모델은 에플리케이션 쪽의 부담을 줄여주는 단순한 방식으로 구현되었으며, 클라이언트에서 동시에 하나의 파일에 추가(append) 하는 작업에서 클라이언트가 별도의 동기화 과정없이 가능하게 하기 위해 어토믹 어펜드(atomic append) 오퍼레이션을 도입했습니다.
- API(Application Programming Interface) \
- 애플리케이션에서 사용할 수 있도록 운영 체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스
2. GFS의 설계
1) GFS의 가정들
GFS의 설계자들은 앞선 관찰을 통해 주요 사실들에 대해 인식했고, 이를 토대로 GFS 설계를 위한 가정을 구체화 했습니다.
- 시스템은 종종 고장나는 많은 저렴한 상품 컴포넌트들로 구축된다. 지속적으로 모니터링하여 컴포넌트 고장을 감지(detect), 허용(tolerate) 및 신속하게 복구해야 한다.
- 멀티 GB 파일이 일반적인 경우이므로 이를 효율적으로 관리해야 한다.
워크로드(workload)
는 주로 큰 스트리밍 읽기와 작은 랜덤 읽기, 두 가지 종류의 읽기로 구성된다.- 워크로드(workload)
- 주어진 기간에 시스템에 의해 실행되어야 할 작업의 할당량
동시적으로 같은 파일에 append 작업을 하는 많은 클라이언트를 고려해야한다. 생산자-소비자 큐와 같은 자료구조를 도입하여 수 많은 생산자(클라이언트)들이 동시적으로 파일에 append하게 한다.
- 낮은 지연 시간(latency)보다 높은 지속적인 대역폭(bandwidth)이 더 중요하다.
1
- 대부분의 구글 에플리케이션은 대량의 데이터 처리 속도를 높이면서, 개별 읽기 또는 쓰기에 대한 엄격한 응답 시간을 요구하지 않기 때문
2) 인터페이스
- GFS에서 파일은 디렉토리에서 계층적으로 구성되고 경로 이름(pathname)으로 식별된다.
create
,delete
,open
,close
,read
,write
동작을 지원한다.
GFS는
snapshot
과record append
동작을 가지고 있다.- 스냅샷(snapshot) \
- 파일 또는 디렉토리 트리의 복사본을 저렴한 비용으로 생성
- 레코드 추가(record append)
- 레코드 추가를 사용하면 여러 클라이언트가 동일한 파일에 데이터를 동시에 추가할 수 있으며, 각 개별 클라이언트의 추가 데이터의 원자성을 보장할 수 있다.
3) 아키텍쳐
GFS의 클러스터는 하나의 마스터와 복수의 청크서버(Chunk Server)로 구성되어 있고, 각 요소들은 평범한 리눅스 머신에 불과합니다. 그리고 클라이언트들은 각 요소(마스터와 청크서버들)에 접근이 가능합니다.
GFS에 저장되는 파일은 고정 크기 청크(Fixed-size chunks)로 나누어져서 저장됩니다. 각 청크는 불변하고 전역적으로 고유한 64비트의 ‘청크 핸들(Chunk-handle)’로 식별될 수 있고, 이는 청크가 생성될 때 마스터로부터 부여되는 일종의 고유식별번호입니다.
청크서버는 각 청크를 로컬 디스크에 리눅스 파일로 저장하고, 청크 핸들 및 바이트 범위에서 원하는 청크 데이터를 식별할 수 있습니다. 이를 통해 읽기(Read) 또는 쓰기(Write) 작업이 이루어지는 것이죠.
각 청크는 신뢰성(Reliability)을 위해 여러 청크서버에 복제됩니다. 기본적으로 세개의 복제본을 저장하지만, 사용자가 원한다면 설정을 통해 복제본의 수를 지정할 수 있습니다.
마스터는 모든 파일 시스템의 메타 데이터를 유지 관리합니다. 여기서 메타데이터는 파일 네임 스페이스(name space), 접근 제어 정보, 파일과 청크의 매핑 정보, 청크의 위치 등을 말합니다.
또한 마스터는 청크 리스 관리, 가비지 컬렉션, 청크 마이그레이션과 같은 시스템 전반의 활동을 제어하는 역할도 수행합니다. 이를 위해 마스터는 청크서버와 주기적으로 통신하여 명령을 내리거나 상태 정보 등을 받는데, 이러한 메커니즘을 하트비트(heartbeat)라고 합니다. 마치 심장박동을 확인하는 것 처럼 정기적으로 신호를 주고받는다는 것이죠.
클라이언트는 메타데이터 작업을 위해서 마스터에 접근하기도 하지만, 데이터를 포함한 모든 통신은 청크서버로 직접 전달되는 형태입니다. 즉, 모든 데이터의 읽기나 쓰기 작업은 클라이언트와 청크서버간의 통신을 통해 이루어진다는 것입니다.
청크가 로컬 파일로 청크서버에 저장되기 때문에 청크서버는 파일 데이터를 굳이 캐시할 필요가 없고, 리눅스의 버퍼 캐시는 이미 자주 액세스하는 데이터를 메모리에 보관하고 있습니다. 여기서 캐시는 데이터나 값을 미리 복사하놓는 임시 저장소라고 생각하시면 됩니다.
전체적인 아키텍쳐를 봤으니, 이제는 아키텍쳐를 구성하는 개개의 요소들을 하나씩 살펴볼 차례입니다.
(3-1) 단일 마스터(Single Master)
GFS 클러스터의 아키텍쳐를 보면, 단일 마스터를 사용하고 있습니다. 단일 마스터를 사용하면 설계가 크게 간소화되고, 마스터가 클러스터 전역의 지식을 사용하여 정교한 청크 배치 및 복제 결정을 내릴 수 있기 때문이죠. 그러나 단일 마스터는 클라이언트나 청크서버와의 교신에서 병목현상으로 인한 레이턴시가 발생할 우려가 있습니다. 그래서 GFS의 마스터는 이러한 병목현상을 줄이기 위해 위에 설명한 내용대로 데이터 읽기와 쓰기에 개입하지 않는 것이죠. 즉, 마스터는 최대한 부하 부담을 줄이는 형태로 설계가 되어 있다는 것입니다.
대신 클라이언트는 마스터에게 어떤 청크서버에 연결해야하는지를 요청합니다. 아래의 그림을 통해 마스터와 클라이언트가 상호작용하는 과정을 좀 더 자세하게 설명해보죠.
- 읽기 작동메커니즘 (Read Operation)
- 우선 클라이언트는 고정 크기 청크(Fixed-size chunks)를 사용하여 어플리케이션에서 지정한 파일 이름과 바이트 오프셋을 파일 내의 청크 인덱스로 변환합니다.
- 그런 다음 파일 이름(File name)과 청크 인덱스(Chunk index)가 포함된 요청을 마스터에 보냅니다.
- 그러면 마스터는 청크 핸들(Chunk handle)과 청크의 위치(Chunk location)를 클라이언트에게 전달합니다.
- 클라이언트는 파일 이름과 청크 인덱스를 키로 사용하여 이 정보를 캐싱합니다. 그런 다음 클라이언트는 가장 가까운 복제본 중 하나로 요청을 보냅니다. 요청은 청크 핸들(Chunk handle) 및 해당 청크 내의 바이트 범위(Byte range)를 지정하는 것입니다.
- 그럼 청크서버는 지정된 데이터를 클라이언트에게 전달하고
- 클라이언트는 그 데이터를 다시 어플리케이션에 전달합니다.
- 쓰기 작동 메커니즘(Write Operation)
(3-2) 청크 크기(Chunk size)
GFS는 기본 청크 크기로 64MB를 설정했고, 이는 일반적인 DFS의 블록 크기보다 훨씬 큰 단위입니다. 각 청크 복제본은 청크 서버에 일반 리눅스 파일로 저장되며 필요한 경우에만 확장됩니다.
이렇게 크게 청크 크기를 할당하는 것에는 두가지 이점이 존재합니다.
첫째, 클라이언트와 마스터 간의 통신을 줄여줍니다. 동일한 청크의 읽기 및 쓰기에 있어, 단위가 크면 클수록 자연스레 초기 요청의 응답으로부터 오는 메타데이터가 큰 데이터를 아우르기 때문이죠. 특히나 어플리케이션이 대부분 대용량 파일을 순차적으로 읽고 쓴다는 점을 감안하면 이러한 통신의 감소는 워크로드에도 유리합니다.
둘째, 청크 크기가 크다는 것은 클라이언트가 주어진 청크서버에서 많은 작업을 수행할 수 있다는 것을 의미합니다. 즉, 클라이언트가 오랜 시간동안 청크 서버에 대한 지속적인 TCP 연결을 유지함으로써 네트워크 오버헤드를 줄일 수 있다는 것입니다.
셋째, 마스터에 저장된 메타데이터의 크기를 줄여줍니다. 청크사이즈가 큰 만큼 마스터가 저장해야될 메타데이터의 양이 감소하기 때문입니다.
반면, 큰 청크 크기에 따른 단점도 존재합니다. 많은 클라이언트들이 하나의 파일 청크에 접근하여 ‘핫 스팟(Hot spot)’이 될 수 있다는 점입니다. 하지만 이 또한 큰 문제는 되지 않는게 구글의 어플리케이션들은 대부분 거대한 멀티 청크 파일을 순차적으로 읽기 때문입니다.
(3-3) 메타데이터(Metadata)
마스터는 크게 세가지 유형의 메타데이터를 저장하고 있습니다. 그리고 모든 메타데이터는 마스터의 메모리에 저장됩니다.
세가지 유형의 메타 데이터는 다음과 같습니다.
- 파일 및 청크 네임 스페이스
- 파일과 청크의 매핑 정보
- 각 청크의 복제본 위치
청크 네임 스페이스와 파일-청크 매핑 정보는 변화가 생기면, 마스터가 로깅하면서 유지됩니다. 이 작업 로그는 마스터의 로컬 디스크에 저장되고 원격 머신에 복제됩니다. 이 로그를 사용하면 마스터의 충돌 시 발생하는 부정합 위험없이 안정적으로 마스터의 상태를 업데이트 할 수 있습니다.
마스터는 청크 위치정보를 영구히 가지고 있지 않습니다. 대신 마스터가 시작할 때와 청크 서버가 클러스터에 들어올 때마다 각 청크 서버에 청크를 묻게 되어있습니다.
(3-4) 인메모리 방식의 데이터 구조(In-Memory Data Structures)
메타 데이터가 마스터에 메모리에 저장되기 때문에 마스터의 작동은 빠릅니다. 또한 마스터는 백그라운드에서 전체 상태를 주기적으로 스캔하는 것이 쉽고 효율적입니다. 마스터는 이 주기적인 스캔작업을 통해 시스템 전반에 대한 제어를 관장하는데 가비지 컬렉션, 청크 재복제, 청크 마이그래이션이 대표적입니다.
- 마이그레이션(migration) \
- 대량의 데이터를 옮기는 프로세스를 말함
(3-5) 청크 위치(Chunk Locations)
네임스페이스와 파일-정크 매핑정보에 대한 작업 로그는 마스터의 로컬 디스크에 영구적으로 저장되는 반면, 복제본이 있는 청크서버에 대한 기록은 영구적으로 저장되지 않습니다. 대신 마스터의 시작 시 해당 정보를 위해 청크서버를 폴링할 뿐입니다.
- 폴링(pooling) \
- 프로그램에 의한 입출력 방식. 데이터의 입출력이 CPU가 수행하는 프로그램의 입출력 명령에 의해 실행되고 입출력을 수행할 준비가 되었는지 알기 위해 CPU가 주변장치의 상태를 계속 감시한다.
이 후 마스터는 청크 배치(Chunk placement)와 청크 서버의 상태를 하트비트 메세지를 통해 지속적으로 모니터링하는데, 이러한 방식은 마스터와 청크 서버 사이의 동기화(synchronization)를 유지해야하는 수고를 덜어줍니다.
(3-6) 작업 로그(Operation Log)
작업 로그는 중요한 메타데이터의 변경 내역을 포함하고 있습니다. 메타데이터에 대한 유일한 영구 레코드일 뿐 아니라 동시적인 작업의 순서를 정의하는 논리적 타임라인 역할도 합니다. 파일 및 청크는 모두 고유하며 생성되었을 때의 논리적 시간에 의해 식별됩니다.
작업 로그는 중요하므로 안정적으로 저장해야 하며, 메타데이터 변경이 영속적이어질 때까지 클라이어트에게 보이지 않게 합니다. 로컬 및 원격의 디스크에 해당 로그 레코드를 플러시한 후에만 클라이언트의 작업에 응답합니다.
마스터는 이 작업 로그를 다시 재생함으로써 파일 시스템 상태를 복구하는데, 시작 시간(startup time)을 최소화하기 위해서 로그를 작게 유지합니다. 추가로 이 로그를 작성하는 중에서도 에러가 발생할 수 있는데, 복구 코드는 자동으로 불완전하게 종료된 로그들을 무시하여 데이터 무결성을 유지합니다.
(3-7) 연속성 모델(Consistency Model)
GFS는 고도로 분산된 어플리케이션을 잘 지원하지만, 상대적으로 단순하고 효율적으로 구현하는 완화된 일관성 모델을 가지고 있습니다. 이 단락에서는 GFS의 보증(Guarantees)와 어플리케이션의 의미를 알아보겠습니다.
- Gureantees by GFS
파일 네임스페이스의 변경(ex. 파일 생성)은 원자적입니다. 이것은 전적으로 마스터에 의해 수행되며, 네임스페이스 ‘locking’은 원자성과 정확성을 보증합니다. 마스터의 작업 로그는 이러한 작업의 전체 순서를 정합니다.
데이터 변화(mutation) 후 파일 영역(file region)의 상태는 변화의 유형, 성공 또는 실패 여부, 동시 변화가 있는지 여부에 따라 달라집니다. 위의 표는 그 결과를 요약한 것입니다.
몇 가지 상황을 나누어 살펴보자면,
(1) 만약 모든 클라이언트가 언제나 같은 데이터만을 읽는다면, 어떤 복제본을 읽는지와 상관없이 파일 영역은 일관될 것이다.
(2) 파일 데이터가 변화되고, 이것이 일관적이라면 해당 파일 영역은 ‘defined’ 상태가 되고, 클라이언트들은 변화를 통해 전체에 쓰여진 내용을 볼 수 있다.
(3) 동시 작성자(concurrent writers)로부터 같은 파일 영역에 간섭없이 변화가 성공한다면, 해당 영역의 상태는 마찬가지로 ‘defined’ 상태가 되며, 또한 마찬가지로 모든 클라이언트들은 그 변화를 볼 수 있다.
(3vs4) 그러나 동시에 성공한 변화는 영역을 defined 하지않고, 다만 consistency 상태로 남긴다. 이는 모든 클라이언트는 같은 데이터를 볼 수 있지만, 어떤 변화에 의해 쓰여진 것을 반영하지 못할 수 있다.( it may not reflect what any one mutation has written) 일반적으로 이러한 변화는 여러 변화의 혼합 조각(mingled fragments)로 구성된다.
(5) 실패한 변화는 해당 영역을 비일관된 상태(inconsistency)로 만든다(and alse undefined). 따라서 서로 다른 클라이언트는 다른 데이터를 다른 시간 때마다 읽게 된다.
데이터 변화는 ‘write’와 ‘record append’가 있습니다. 쓰기의 경우 애플리케이션이 명시한 파일 오프셋에 데이터를 쓰는 것이고, append의 경우 원자적으로 데이터가 appended하게 합니다. 일반적인 append의 경우 클라이언트가 생각하는 파일의 끝 부분 offset에 추가됩니다.
반면, record append operation의 경우 GFS가 선택합니다. 선택된 오프셋은 클라이언트로 반환되고, 레코드를 포함하는 정의된 영역의 시작을 표시합니다.
일련의 변화를 성공적으로 마친 후 변화된 파일 영역은 ‘defined’ 상태로 보장되며, 마지막 변화에 의해 쓰여진 데이터를 포함합니다. GFS는 모든 변화를 동일한 순서로 청크에 적용하면서 보장합니다. 그리고 청크 버전 번호를 사용하여 ‘stale’한(오래된) 상태의 복제본을 탐지하는데, stale 상태란 청크서버가 다운되면서 변화를 적용하지 못하게 된 상태를 뜻합니다.
stale 복제본은 변화 속에서 돌연변이에 관여되지 않거나 청크 위치를 요청하는 클라이언트에 제공되지 않는다. 이러한 복제본은 가능한 빨리 가비지 컬렉트됩니다.
- Implications for Applications
GFS 애플리케이션은 이미 다른 목적에 필요한 몇 가지 간단한 기술을 사용하여 완화된 일관성 모델을 수용할 수 있습니다. 덮어쓰기, 체크포인트, 자기확인 및 자기 식별 기록 작성 대신 append에 의존합니다. 실제로 모든 어플리케이션은 덮어쓰기가 아닌 append 방식으로 파일을 변형합니다.
체크포인트에는 어플리케이션 수준의 체크섬(Checksum)도 포함될 수 있습니다. writer는 정의된 상태로 알려진 마지막 체크포인트까지 파일 영역만 확인하고 처리합니다. append는 랜덤 쓰기보다 훨씬 효율적이고 어플리케이션 장애에 더 탄력적입니다.