728x90

하둡 완벽가이드를 읽으며 정리한 내용입니다.


네트워크로 연결된 여러 머신의 스토리지를 관리하는 파일 시스템을 분산 파일시스템이라고 한다. 즉 분산 파일 시스템은 네트워크 기반이므로 네트워크 프로그램의 복잡성을 모두 가지고 있다.

하둡은 HDFS라는 분산 파일 시스템을 제공한다.

3.1 HDFS 설계

HDFS 사용이 적절한 경우는?

  • 매우 큰 파일
    • 수백 메가바이트, 기가바이트, 테라바이트 크기의 파일을 다룬다.
  • 스트리밍 방식의 데이터 접근
    • ‘가장 효율적인 데이터 처리 패턴은 한번쓰고 여러번 읽는 것’. 한번 생성되면 이후에 다양한 목적으로 여러번 읽히게 된다.
    • 특정 데이터 하나를 읽는데 걸리는 지연시간보다, 많은 데이터를 읽을 때 걸리는 시간이 더 중요하다
      • 이 말은 소수의 데이터를 하나하나 읽어들이는 작업과 이러한 데이터를 빨리 읽어들이는 속도보다는, 대부분의 분석은 대량의 데이터를 대상으로 진행되므로, 대량의 많은 데이터를 다루는 것이 더 중요하다는 것을 의미하는 것 같다. 
  • 범용 하드웨어
    • 하둡은 하나의 시스템이 엄청 고가이고 고성능일 필요가 없다.

그렇다면 HDFS가 적합하지 않을 경우는?

  • 빠른 데이터 응답 시간 필요
    • 데이터 접근에 빠른 응답시간을 요구하는 애플리케이션은 HDFS와 맞지 않다. HDFS는 높은 데이터 처리량을 제공하기 위해 최적화되어있고 이를 위해 응답시간을 희생했다.
    • 만약에 하둡시스템의 분산 시스템의 이점을 기반으로 빠른 응답 시간을 원한다면 HBase가 하나의 대안이 될 수 있다.
  • 수많은 작은 파일
  • 다중 라이터와 파일의 임의 수정
    • HDFS는 단일 라이터로 파일을 쓴다. 

3.2 HDFS  개념

3.2.1 블록

블록 크기는 한 번에 읽고 쓸 수 있는 데이터의 최대량이다.

HDFS 블록은 기본적으로 128MB이다(큰단위에 속한다). HDFS의 파일은 단일 디스크를 위한 파일시스템처럼 특정 블록 크기의 청크로 쪼개지고 각 청크는 독립적으로 저장된다.

HDFS 파일은 블록 크기보다 작은 데이터일 경우, 전체 블록 크기에 해당하는 하위 디스크를 모두 점유하지는 않는다. 예를 들어 HDFS의 블록의 크기가 128MB이고 1MB크기의 파일을 저장한다면 128MB의 디스크를 사용하는 것이 아니라 1MB의 디스크만 사용한다.

HDFS 블록이 큰 이유는?

탐색 비용을 최소화하기 위해서다. 블록이 매우 크면 블록을 탐색하는데 걸리는 시간을 줄일 수 있고 데이터를 전송하는데 더 많은 시간을 할애할 수 있다.

 

디스크에서 한 블록을 읽는데 걸리는 시간은 seek time + transfer time 이다.(seek time : 데이터의 시작점을 찾는 시간. transfer time : 블록의 크기 / transfer rate)

 

여기서 transfer rate는 디스크의 물리적인 한계가 있으므로 더 빠르게 만들기는 어려우니 블록의 크기를 크게 만들면 transfer time이 크게 늘어나 한 블록을 읽는 시간에 seek time은 무시해도 될 정도로 작은 값으로 취급할 수 있다.

그러므로 블록을 크게 만들면 대용량 데이터를 읽을 때 seek time의 영향이 줄어들고 디스크의 물리적인 속도인 transfer rate과 데이터를 읽는 속도가 거의 비슷해진다.

 

이를 맵리듀스 개념과 함깨 생각해보면 맵 태스크는 기본적으로 한 번에 하나의 블록을 처리한다. 따라서 만일 태스크 수가 적으면 태스크 수가 많을 때보다 잡이 더 느리게 수행될 것이다.

❓ 태스크 수가 적다는 건 맵 태스크 하나가 작업해야하는 블록의 데이터가 많기 때문에 맵 태스크의 작업이 느리다는 것일까?그렇다면 이게 HDFS블록을 크게 하는 이유가 디스크 탐색시간을 줄이고 전송속도를 빠르게 하기 위한 거랑 무슨 상관이지?

블록의 개념을 도입하면서 얻게된 이득

  1. 파일 하나의 크기가 단일 디스크의 용량보다 더 커질 수 있다는 것이다.
    • 같은 파일의 블록이 반드시 한 디스크 내에 존재하지않아도 되므로
  2. 파일 단위보다 블록 단위로 추상화를 하면 스토리지의 서브 시스템을 단순하게 만들 수 있다.
    • 즉 파일 단위로 추상화 해버리면 매 작업마다 그 단위가 달라져야 할수도 있어서 복잡해지는데, 블록 단위로 추상화하면 그 시스템자체는 단순화된다. 어떤 파일로 작업되어도 정해진 시스템에 맞춰서 작업이 가능하다.
    • 블록은 고정 크기이고 저장에 필요한 디스크 용량만 계산하면 된다.
    • 블록은 단순히 데이터 덩어리일 뿐이니 블록에 대한 접근 권한 같은 메타데이터가 불필요하다
  3. 내고장성과 가용성을 제공하는데 필요한 복제를 구현
    • 물리적으로 분리된 다수의 머신에 각 데이터블록이 복제된다.
    • 하나의 블록을 이용할 수 없다면 다른 머신에 있는 복사본을 읽도록 클라이언트에 알려주면 된다.
파일 시스템에 있는 각 파일을 구성하는 블록의 목록을 출력하는 명령어

 hdfs fsck / -files -blocks

3.2.2 네임노드와 데이터 노드

HDFS클러스터는 두종류의 노드(마스터인 하나의 네임노드 / 워커인 여러개의 데이터노드)로 구성되어있다.

 

네임노드는 파일시스템의 네임스페이스를 관리하며 메타데이터를 유지한다. 이는 네임스페이스 이미지와 에디트 로그라는 두 종류의 파일로 로컬 디스크에 영속적으로 저장된다. 또한 파일에 속한 모든 블록이 어느 데이터 노드에 있는지 파악한다. 하지만 블록의 위치 정보는 시스템이 시작할때 모든 데이터노드로부터 받아서 재구성하기때문에 디스크에 영속적으로 저장하지는 않는다.

 

HDFS 클라이언트는 사용자를 대신해서 네임노드와 데이터 노드 사이에서 통신하고 파일 시스템에 접근한다. 실제로 클라이언트는 인터페이스를 제공하기 때문에 사용자는 각 노드와 관련된 함수를 몰라도 코드를 작성할 수 있다.

 

데이터 노드는 실질적인 일꾼이다. 클라이언트나 네임노드의 요청이 있을때 블록을 저장하고 탐색하며 저장하고 있는 블록의 목록을 주기적으로 네임노드에 보고한다.

 

네임노드가 없으면 파일시스템은 동작하지 않는다. 네임 노드를 실행하는 머신이 손상되면 파일 시스템의 어떤 파일도 찾을 수 없다. 

 

따라서 네임노드의 장애복구 기능은 필수적이다.

  1. 이를 위해 파일 시스템의 메타 데이터를 지속적인 상태로 보존하기 위해 파일로 백업한다. 네임노드가 다수의 파일시스템에 영구적인 상태를 저장하도록 하둡을 구성. 백업 작업은 동기화되고 원자적으로 실행된다. 주로 권장하는 방법은 로컬 디스크와 원격의 NFS 마운트 두곳에 동시에 백업하는 것이다.
  2. 보조 네임노드를 운영하는 것이다. 보조 네임노드의 주 역할은 에디트로그가 너무 커지지 않도록 주기적으로 네임 스페이스 이미지를 에디트 로그와 병합하여 새로운 네임스페이스 이미지를 만드는 것이다. 병합 작업을 수행하기 위해 보조 네임노드는 충분한 CPU와 네임노드와 비슷한 용량의 메모리가 필요하므로 별도의 물리머신에서 실행되는 것이 좋다. 또한 보조 네임노드는 주 네임노드에 장애가 발생할 것을 대비해서 네임 스페이스 이미지의 복제본을 보관한다. 그러나 해당 백업 작업은 약간의 시간차를 두고 진행되므로 주 네임노드에 장애가 발생하면 어느정도의 데이터 손실은 불가피하다.
    • NFS에 저장된 메타데이터 파일을 보조 네임노드로 복사하여 새로 병합된 네임스페이스 이미지를 만들고 그것을 새로운 주 네임노드에 복사한 다음 실행하게 된다.

3.2.3 블록 캐싱

빈번하게 접근하는 블록 파일은 오프힙(off-heap) 블록 캐시라는 데이터 노드의 메모리에 명시적으로 캐싱할 수 있다. 이를 통해 읽기 성능을 높일 수 있다.

3.2.4 HDFS 페더레이션

네임 노드는 파일 시스템의 모든 파일과 각 블록에 대한 참조 정보를 메모리에서 관리한다. 파일이 매우 많은 대형 클러스터에서 가장 큰 걸림돌이 바로 이 메모리다. HDFS v2 이전에는 전체 클러스터에 대한 단일 네임스페이스만 허용했는데, 이러한 확장성 문제를 해결하기 위해 HDFS페더레이션(연합체)를 지원하게 되었다.

HDFS 페더레이션은 디렉토리(네임스페이스) 단위로 네임노드를 등록하여 사용하는 것.

-> 즉 다수의 네임 노드를 두는 것이다.

 

예를 들어 user, hadoop, tmp 세개의 디렉토리가 존재할 때, /user, /hadoop, /tmp 디렉토리 단위로 총 3개의 네임노드를 실행하여 파일을 관리하게 하는 것이다.

 

각 네임노드는 네임 스페이스의 메타 데이터를 구성하는 네임스페이스 볼륨과 네임스페이스에 포함된 파일에 전체 블록을 보관하는 블록 풀을 관리한다.

네임 스페이스 볼륨은 서로 독립되어있으며 네임노드는 서로 통신할 필요가 없고 특정 네임노드에 장애가 발생해도 다른 네임노드가 관리하는 네임스페이스의 가용성에 영향을 주지 않는다.

그러나 블록 풀의 저장소는 분리되어있지 않다. 모든 데이터 노드는 각 네임노드마다 등록되어있고 여러 블록 풀로부터 블록을 저장한다.

3.2.5 HDFS 고가용성

그렇다면 네임노드 장애에 따른 HDFS의 가용성은 어떻게 보장할 수 있을까?

 

네임노드 장애를 복구하기 위해 관리자는 파일 시스템 메타 데이터 복제본을 가진 새로운 네임 노드를 구동하고 모든 데이터 노드와 클라이언트에 새로운 네임 노드를 사용하도록 알려주면 된다.

하지만 1. 네임 스페이스 이미지를 메모리에 로드 2. 에디트 로그를 갱신 3. 전체 데이터 노드에서 충분한 블록 리포트를 받아 안전 모드를 벗어날때까지 어떤 요청도 처리하지 못한다. 실제로 재구동까지 30분이상 걸리기도 한다.

 

이를 해결하기 위해 하둡2버전부터 HDFS고가용성(HA)를 지원한다.

고가용성은 활성 대기(active-standby) 상태로 설정된 한 쌍의 네임노드로 구현된다. 활성 네임노드에 장애가 발생하면 대기 네임노드가 그 역할을 이어받아 큰 중단 없이 클라이언트의 요청을 처리한다.

  • 네임 노드는 에디트 로그를 공유하기 위해 고가용성 공유 스토리지를 반드시 사용해야한다. 대기 네임 노드가 활성화되면 먼저 기존 활성 네임 노드의 상태를 동기화하기 위해 공유 에디트 로그를 읽고, 이어서 활성 네임노드에 새로 추가된 항목도 마저 읽는다.
  • 데이터 노드는 블록 리포트를 두개의 네임 노드에 보내야한다. 블록 매핑정보는 디스크가 아닌 네임노드의 메모리에 보관되기 때문이다.
  • 대기 네임노드는 보조 네임노드의 역할을 포함하고 있으며 활성 네임노드 네임스페이스의 체크 포인트 작업을 주기적으로 수행한다.

고가용성 공유 스토리지를 위해 NFS필러나 QJM(quorum journal manager) 중 하나를 선택할 수 있다.

QJM은 저널 노드 그룹에서 동작하며 각 에디트 로그는 전체 저널노드에 동시에 쓰여진다. 일반적으로 저널 노드는 세개며, 그중 하나가 손상되어도 문제가 없는 시스템이다. 이러한 방식은 주키퍼와 유사하나 QJM은 주키퍼를 사용하지 않고도 이런 기능을 구현했다는 점이 중요하다.

활성과 대기 네임노드는 모두 최신 에디트 로그와 실시간으로 갱신되는 블록 매핑 정보를 메모리에 유지하고 있기 때문에 매우 빠르게 대체가능하다.

 

그러나 실제로 활성 네임노드에 장애판단은 신중해야 하며, 가끔 활성 네임노드에 장애 발생 시에 대기 네임노드가 중단된 상태인 경우도 있다. 그러나 이런 경우 대기 네임노드를 다시 구동하면 그만이다. → 큰문제는 아니다.

장애복구와 펜싱

이러한 전환 작업은 장애복구 컨트롤러라는 새로운 객체로 관리된다. 기본적으로는 단 하나의 네임 노드만 활성상태에 있는 것을 보장하기 위해 주키퍼를 이용한다. 하트비트 방식으로 네임노드의 장애를 감시하고 장애가 발생하면 복구를 지시한다.

장애복구 컨트롤러는 두개의 네임노드가 서로 역할을 바꾸게 하는 방법으로 전환 순서를 제어할 수 잇다. 그러나 이러한 경우 장애발생 네임노드가 현재 실행되지 않고 있다는 것을 확신하기 어렵다. 펜싱이라는 메서드가 이때 사용된다.

 

QJM은 한번에 하나의 네임노드만 에디트 로그에 쓸 수 있도록 보장한다. 그러나 활성 네임노드가 요청에 대해 잘못된 정보를 줄 가능성이 있으므로 SSH펜싱명령어로 네임노드의 프로세스를 확실히 죽이도록 설정하는 것이 가장 좋은 방법이다. 즉 장애가 발생했다고 인지한 네임노드가 사실은 활성화되어있어서 에디트 로그에 작업할 수도 있으므로 이를 펜싱 메소드로 확실히 막아버리는 것이다.

3.3 명령행 인터페이스

HDFS를 위한 인터페이스 중 가장 단순하고 개발자에게 익숙한 방식

3.3.1 기본적인 파일 시스템 연산

# hdfs 명령어에 대한 가이드를 얻을 수 있는 명령어
hdfs fs -help

# 로컬 파일 시스템의 파일 하나를 HDFS로 복사하기
hadoop fs -copyFromLocal input/docs/quangle.txt \ hdfs://localhost/user/tom/quangle.txt

# HDFS 파일을 로컬로 복사하기
hadoop fs -copyToLocal quangle.txt quangle.copy.txt

# 디렉토리 만들기
hadoop fs -mkdir books

# 목록 출력하기
hadoop fs -ls

# 출력 결과
drwxr-xr-x - tom supergroup 0 2014-10-04 13:22 books
-rw-r--r-- 1 tom supergroup 119 2014-10-04 13:21 quangle.txt

첫번째 열은 파일의 모드

두번째 열은 전통적인 유닉스 파일시스템에는 없는 파일의 복제계수다. 디렉터리는 데이터노드에는 저장되지 않고 네임노드에만 저장되고 메타데이터로 관리되므로 복제의 개념이 없다.(그래서 -(공란) 로 출력된다)

세번째와 네번째 열은 파일의 소유자와 그룹니다.

다섯번째 열은 바이트 단위의 파일크기인데 디렉터리는 0으로 표시된다.

여섯번째와 일곱번째 열은 마지막으로 수정된 날짜와 시간이다.

HDFS 파일 권한

읽기(r), 쓰기(w), 실행(x) 권한 등 세가지 종류의 파일 권한이 존재한다. 그러나 HDFS는 파일의 실행을 지원하지 않으므로 파일에 대한 실행권한은 무시되지만 디렉터리에 대한 실행권한은 하위 디렉터리의 접근을 위해 필요하다.

3.4 하둡 파일 시스템

HDFS외에도 하둡 파일시스템 내에서의 여러 구현체가 존재한다.

파일 시스템  URI 스킴 자바구현체 설명
Local file fs.LocalFileSystem 로컬티스크를 위한 파일 시스템. 클라이언트의 체크성을 위함
HDFS hdfs hdfs.DistributedFileSystem 하둡 분산 파일 시스템
WebHDFS webhdfs hdfs.web.WebHdfsFileSystem HTTP를 통해 HDFS에 인증을 통한 읽기/ 쓰기를 제공하는 파일 시스템
SecureWebHDFS swebhdfs hdfs.web.SWebHdfsFileSystem WebHDFS의 HTTPS 버전
HAR har fs.HarFileSystem 아카이브 파일을 위한 파일시스템. 하둡 아카이브는 네임노드의 메모리 용량을 줄이기 위해 HDFS에 수많은 파일을 묶어놓은 파일이다.
View viewfs viewfs.ViewFileSystem 클라이언트 측 마운트 테이블. 페더레이션 네임노드의 마운트 지점을 생성할 때 주로 사용
FTP ftp fs.ftp.FTPFileSystem  
S3 s3a fs.s3a.S3AFileSystem s3n(구버전)을 대체
Azure wasb fs.azure.NativeAzureFileSystem  
Swift swift fs.swift.snative.SwiftNativeFileSystem  

앞에서 살펴본 하둡 명령행 인터페이스는 모든 하둡 파일시스템에서 잘 동작한다.

맵리듀스 프로그램은 어떠한 파일 시스템도 접근할 수 있고 쉽게 실행할 수 있지만 대용량 데이터를 처리할 때는 데이터 지역성 최적화가 가능한 HDFS와 같은 분산 파일 시스템을 선택하는 것이 좋다

3.4.1 인터페이스

하둡은 자바로 작성되었기 때문에 자바 API를 통해 하둡 파일시스템과 연동할 수 있다.

HTTP

webHDFS 프로토콜을 이용한 HTTP REST API를 사용하면 다른 언어도 HDFS에 쉽게 접근할 수 있다. 

HTTP 로 HDFS에 접근하는 두가지 방식이 있다.

  1. 클라이언트의 HTTP 요청을 HDFS데몬이 직접 처리하는 방식이다. → WebHDFS
    • 파일에 대한 메타데이터 연산은 네임노드가 처리하지만 파일을 읽고 쓰는 연산은 먼저 네임노드로 전달되고 네임노드는 파일데이터를 스트리밍할 데이터 노드를 알려주는 HTTP 리다이렉트를 클라이언트에 보낸다.
  2. 클라이언트 대신 DistributedFileSystem API로 HDFS에 접근하는 프록시를 경유하는 방식이다. → HttpFS프록시
    • 독립 프록시 서버를 통하는 것
    • 클라이언트는 네임노드와 데이터 노드에 직접 접근할 필요가 없다.
    • 엄격한 방화벽이나 대역폭 제한 정책을 적용하기 쉽다.
    • 서로 다른 데이터 센터에 있는 하둡 클러스터 사이의 데이터 전송이나 외부네트워크에 있는 클라우드에서 운영되는 하둡 클러스터에 접근할 때 일반적으로 이용된다.

HttpFS 프록시는 WebHDFS와 동일한 HTTP인터페이스를 제공하므로 클라이언트는 webhdfs URI로 둘 다 접근할 수 있다.

HttpFS프록시는 네임노드나 데이터 노드 데몬과는 독립적이다.

C

하둡은 자바 인터페이스를 모방한 libhdfs라는 C라이브러리를 제공하며 이는 모든 하둡 파일시스템에 접근할 수 있다.

자바 API보다 한발 늦게 개발된다

NFS

하둡의 NFSv3 게이트웨이를 이용하면 로컬 클라이언트 파일 시스템에 HDFS를 마운트할 수 있다.

FUSE

Filesystem in Userspace(사용자공간에서의 파일시스템)

3.5 자바 인터페이스

참고로 하둡 버전2부터는 동시에 여러 파일시스템을 다룰 수 있는 FileContext라는 새로운 파일시스템 인터페이스를 제공한다.

3.5.1 하둡 URL로 데이터 읽기

FsUrlStreamHandlerFactory의 인스턴스와 함께 URL 클래스의 setURLStreamHandlerFactory() 메서드를 호출하여 수행된다. 이 메서드는 JVM 하나당 한번씩만 호출할 수 있기 때문에 일반적으로 정적블록에 실행된다.

이러한 한계는 프로그램의 다른 부분에서 URLStreamHandlerFactory를 설정하면 하둡 URL로부터 데이터를 읽을 수 없게됨을 의미한다.

// 하둡 파일 시스템의 파일을 URLStreamHandler를 사용하여 표준출력으로 보여주기

public class URLCat{
	static {
		URL.setURLStreamHandlerFactory(new FsUrlStreamHandlerFactory());
	}
	public static void main(Strong[] args) throws Exception {
		InputStream in = null;
		try {
			in = new URL(args[0]).openStream();
			IOUtils.copyBytes(in, System.out, 4096, false);
		} finally {
			IOUtils.closeStream(in);
		}
	}
}

finally구문에서 스트림을 닫고 입력스트림에서 출력스트림으로 바이트를 복사하기 위해 하둡 클래스와 함께 IOUtils 클래스를 사용했다.

copyBytes()메서드의 세번째 인자는 버퍼 크기를, 네번째 인자는 복사완료시 스트림의 닫기 여부를 의미한다.

3.5.2 파일시스템 API로 데이터 읽기

하둡 파일 시스템의 파일은 하둡 path객체로 표현된다. 여기서 path는 hdfs://localhost/user/tom/quangle.txt 같은 하둡 파일시스템URL을 의미한다.

 

접근할 파일시스템(예시는 HDFS)에 대한 인스턴스를 얻기

// FileSystem인스턴스를 얻을 수 있는 정적팩토리 메서드

public static FileSystem get(Configuration conf) throws IOExeption
public static FileSystem get(URI uri, Configuration conf) throws IOExeption
public static FileSystem get(URI uri, Configuration conf, String user) throws IOExeption

Configuration 객체는 클라이언트나 서버의 환경설정을 포함. 설정파일에서 관련 설정을 읽어들인다.

첫번째 메서드는 기본파일시스템(core-site.xml파일에 설정하며 설정하지 않으면 기본 로컬 파일시스템이 사용됨)을 반환한다.

두번째 메서드는 주어진 URI 스킴과 권한으로 파일시스템을 결정

세번째 메서드는 특정 사용자를 명시하여 파일시스템을 추출 → 보안차원에서 중요

 

로컬파일시스템 인스턴스만 얻을때는 getLocal()메서드를 사용하는 것이 가장 편리

public static LocalFileSystem getLocal(Configuration conf) throws IOExeption

FileSystem 인스턴스를 얻으면 open() 메서드를 호출하여 파일에 대한 입력 스트림을 열 수 있다.

public FSDataInputStream open(Path f) throws IOExeption
public abstract FSDataInputStream open(Path f, input bufferSize) throws IOExeption

 

//하둡 파일 시스템의 파일을 FileSystem API를 사용하여 표준출력으로 보여주기
public class FileSystemCat{
	public static void main(String[] args) throws Exeption {
		String uri = args[0];
		Configuration conf = new Configuration();
		FileSystem fs = FileSystem.get(URI.create(uri), conf);
		InputStream in = null;
		try {
			in = fs.open(new Path(uri));
			IOUtils.copyBytes(in, System.out, 4096, false);
		} finally {
			IOUtils.closeStream(in);
			}
		}
	}

FSDataInputStream

FileSystem의 open() 메서드는 표준 java.io클래스가 아닌 FSDataInputStream 클래스를 반환한다.

package org.apache.hadoop.fs;

public class FSDataInputStream extends DataInputStream
	implements Seekable, PositionedReadable {
//...
}

위의 Seekable 인터페이스는 파일에서 특정위치로 이동하는 것을 허용한다.

그리고 이 인터페이스에서 쓰이는 seek()메서드는 파일에서 임의 위치나 절대위치로 이동할 수 있다.

이를 통해 FileSystem API로 파일을 읽어서 한번 출력하고, 파일의 시작점으로 이동한 다음 스트림을 다시 출력할 수 있다.

즉 파일을 표준출력으로 두번 보여준다.

이때 주어진 오프셋에서 파일의 일부를 읽기 위한 PositionedReadable 인터페이스를 제공한다.

이 인터페이스에서 사용되는 read()메서드는 파일의 주어진 position에서 length만큼의 바이트를 읽어서 buffer의 주어진 오프셋에 그 내용을 복사한다. 반환값은 실제로 읽은 바이트 크기다.

이러한 모든 메서드는 파일의 현재 오프셋을 유지하고 스레드 안전(thread safe)를 지원한다. (동시접속을 고려하여 FSDataInputStream을 설계하지 않았기 때문에 다중 인스턴스를 생성하는 방법을 권장)

따라서 파일 전체를 읽는 중이더라도 메타데이터와 같은 파일의 다른 부분에 쉽게 접근할 수 있다.

 

seek()메서드를 호출하는 것은 상대적으로 비용이 많이 드는 연산이다. 애플리케이션이 다수의 seek()를 수행하는 방식이 아닌 맵리듀스와 같은 스트리밍 데이터 기반의 접근 패턴을 사용하도록 구조화하는 것을 권장한다

3.5.3 데이터쓰기

가장 간단한 메서드는 같이 생성할 파일을 path 객체로 받아서 출력 스트림으로 쓰는 것

public FSDataOutputStream create(Path f)throws IOExeption

이 메서드는 기존의 파일을 강제로 덮어쓰기, 파일의 복제계수, 파일쓰기의 버퍼크기, 파일의 블록 크기, 파일권한을 명시할 수 있는 오버로드 버전을 허용한다.

 

💡 create()메서드는 파일을 쓸때 부모 디렉터리가 없으면 자동으로 생성한다. 부모 디렉터리가 없을때 쓰기 실패를 원한다면 먼저 exists()메서드를 호출하여 부모 디렉터리가 존재하는지 미리 확인해야 한다. 그 대안으로 FileContext는 부모 디렉터리의 생성여부를 제어하는 기능을 제공한다.

 

append()메서드로 기존 파일에 데이터를 추가할 수도 있다.

public FSDataOutputStream append(Path f) throws IOExeption>

파일 추가 연산은 기존 파일을 열고 파일의 마지막 오프셋에 데이터를 쓸 수 있는 단일 쓰기를 허용한다. 이를 통해 파일을 닫은 후에도 기존 파일에 쓸수있으므로 로그파일과 같은 무제한 파일을 생성할 수 있다.

 

그러나 모든 하둡파일시스템에 구현된 것은 아니다.

// 로컬파일을 하둡파일시스템으로 복사하기

public class FileCopyWithProgress {
	public static void main(String[] args) throws Exception {
		String localSrc = args[0];
		String dst = args[1];

		InputStream in = new BufferedInputStream(new FileInputStream(localSrc));

		Configuration conf = new Configuration();
		FileSystem fs = FileSystem.get(URI.create(dst), conf);
		OutputStream out = fs.create(new Path(dst), new Progressable() {
			public void progress(){
				System.out.print(".");
			}
		});
		IOUtils.copyBytes(in, out, 4096, true);
		}
	}

FSDataOutputStream

이는 FSDataInputStream과 같이 파일에서 현재 위치를 얻기위한 메서드를 지원한다.

그러나 seek기능(파일탐색)은 지원하지 않는다. HDFS는 열려있는 파일에는 순차쓰기를, 기존 파일에는 추가 연산만 지원하기 때문이다. 즉 파일의 끝 외의 다른 부분에 쓰는 것은 지원하지 않으므로 쓰는 중에 탐색은 불가능하다.

3.5.4 디렉터리

mkdir()메서드를 제공한다.

파일을 쓸때 자동으로 create()메서드를 호출하여 부모 디렉터리를 생성하므로, 명시적으로 디렉터리를 생성할 필요는 없다.

3.5.5 파일 시스템 질의

파일 메타데이터 : FileStatus

모든 파일 시스템은 디렉터리 구조를 탐색하고 저장된 파일과 디렉터리에 대한 정보를 추출하는 기능을 제공한다. 해당 클래스는 파일길이, 블록크기, 복제, 수정시간, 소유권, 권한 정보와 같은 파일과 디렉터리에 대한 파일 시스템 메타데이터를 포함하고 있다.

getFileStatus()메소드를 호출하면 단일 파일 또는 디렉터리에 대한 FileStatus 객체를 얻을 수 있다.

파일 목록 조회

특정 디렉터리의 내용(파일이나 서브 디렉터리 목록)을 조회해야할 때 listStatus()메서드를 사용한다.

public FileStatus[] listStatus(Path f) throws IOExeption
public FileStatus[] listStatus(Path f, PathFilter filter) throws IOExeption
public FileStatus[] listStatus(Path[] files) throws IOExeption
public FileStatus[] listStatus(Path[] files, PathFilter filter) throws IOExeption

PathFilter를 인자로 가지는 메서드는 특정 패턴에 일치하는 파일과 디렉터리만 처리한다.

파일패턴

파일의 집합을 처리해야 할때도 있다. 다중파일을 처리할때 일일이 모든 파일과 디렉터리를 나열하기보다는 단일 표현식으로 와일드카드 문자를 이용하여 다중파일을 매칭하는 글로빙(globbing)으로 알려진 연산을 사용하는 것이 더 편리하다.

public FileStatus[] globStatus(Path pathPattern) throws IOExeption
public FileStatus[] globStatus(Path pathPattern, PathFilter filter) throws IOExeption

PathFilter

위의 방법으로도 접근하고싶은 파일의 집합을 완전히 표현할 수 없다.

가령 특정 파일을 제거하는 것은 불가능하다.

listStatus()와 globStatus()메서드는 프로그래밍 방식으로 매칭을 허용하는 PathFilter를 옵션으로 지원한다.

 

// 정규표현식에 매칭되는 특정 경로를 제외하기 위한 PathFilter

public class RegexExcludePathFilter implements PathFilter {
	private final String regex;

	public RegexExcludePathFilter(String regex) {
		this.regex = regex;
	}
	public boolean accept(Path path){
		return !path.toString().matches(regex);
	}
}

fs.globStatus(new Path("/2007/*/*"), new RegexExcludeFilter("^.*/2007/12/31$"))

가장 마지막 줄인 예제를 실행하면 2007년 1월 1일부터 12월 30일까지의 데이터만 반환된다.

필터는 파일 생성시간과 같은 파일 속성은 사용할 수 없다.

그러나 글로빙패턴이나 정규표현식으로 수행할 수 없는 매칭작업도 할 수 있다.

3.5.6 데이터 삭제

delete()메서드를 사용하면 파일과 디렉터리를 영구적으로 삭제할 수 있다.

public boolean delete(Path f, boolean recursive) throws IOExeption

여기서 경로 f가 파일이거나, 비어있는 디렉터리면 recursive인자는 무시된다.

3.6 데이터 흐름

3.6.1 파일읽기 상세

  1. 클라이언트는 HDFS가 DistributedFileSystem 인스턴스인 FileSystem 객체의 open() 메서드를 호출하여 원하는 파일을 연다
  2. DistributedFileSystem은 파일의 첫번째 블록 위치를 파악하기 위해 RPC를 사용하여 네임노드를 호출하고 네임노드는 데이터 노드의 주소를 반환
    • 이때 클러스터의 네트워크 위상에 따라 클라이언트와 가까운 순으로 데이터 노드가 정렬된다.
    • 클라이언트 자체가 데이터노드고 해당 블록의 복제본을 가지고 있으면 클라이언트는 로컬 데이터 노드에서 데이터를 읽는다.
  3. DistributedFileSystem은 클라이언트가 데이터를 읽을수 있도록 FSDataInputStream(파일 탐색을 지원하는 입력스트림)을 반환한다.
    • FSDataInputStream은 데이터노드와 네임노드의 I/O를 관리하는 DFSInputStream을 래핑한다.
    • 클라이언트는 스트림을 읽이 위해 read()메서드를 호출한다.
  4. 데이터 노드 주소를 저장하고 있는 DFSInputStream은 가장 가까운(첫번째)데이터 노드와 연결한다.
    • 해당 스트림에 대해 read()메서드를 반복적으로 호출하면 데이터노드에서 클라이언트로 모든 데이터가 전송된다.
  5. 블록의 끝에 도달하면 데이터 노드의 연결을 닫고 다음 블록의 데이터노드를 찾는다.
    • 이는 마치 연속적인 스트림을 읽는 것처럼 느낀다.
    • 실제로 클라이언트는 스트림을 통해 블록을 순서대로 하나씩 읽는다. 이때마다 블록마다 데이터노드와 새로운 연결을 맺는다.
  6. 모든 블록에 대한 읽기가 끝나면 클라이언트는 FSDataInputStream의 close()메서드를 호출한다.

데이터를 읽는 중에 데이터 노드와의 통신에 장애가 발생하면 DFSInputStream은 해당 블록을 저장하고 있는 다른 데이터노드와 연결을 시도한다. 또한 이후 불필요한 재시도를 방지하기 위해 장애가 발생한 데이터노드를 기억해둔다.

 

그리고 stream은 전송된 데이터의 체크섬도 검증한다. 만약 블록이 손상되었으면 다른 데이터노드에 있는 블록의 복제본을 읽으려 시도한다. 이때 손상된 블록에 대한 정보는 네임노드에 보고된다.

 

이러한 설계의 핵심은 클라이언트는 데이터를 얻기위해 데이터노드에 직접적으로 접촉하고 네임노드는 각 블록에 적합한 데이터노드를 안내해준다는 것이다.

 

네임노드는 효율적인 서비스를 위해 메타 데이터를 메모리에 저장하고 단순히 블록의 우치 정보 요청만 처리하며 데이터를 저장하거나 전송하는 역할은 맡지 않으므로 클라이언트가 많아져도 병목현상은 거의 발생하지 않는다.

네트워크 토플로지와 하둡

로컬 네트워크에서 두개의 노드가 서로 가깝다는 것은 어떤의미인가?
노드간 데이터 전송의 제약요소는 바로 전송률이며, 대역폭은 한정 자원이다. 즉 네드워크 대역폭을 거리 측정에 이용한다.
하둡은 네트워크 전체르 하나의 트리로 표현하고 두 노드의 거리는 가장 가까운 공통조상과의 거리를 합산하여 계산한다.

데이터 센터 > 랙 > 노드 순으로 구성되며,
동일 노드 > 동일 랙의 다른 노드 > 동일 데이터센터에 있는 다른 랙의 노드 > 다른데이터 센터의 노드 순으로 거리가 멀어진다.

3.6.2 파일쓰기 상세

  1. create() 호출
  2. 파일 시스템의 네임스페이스에 새로운 파일을 생성하기 위해 네임노드에 RPC를 요청.
    • 블록에 대한 정보는 보내지 않는다.
    • 네임노드는 요청한 파일과 동일파일이 존재하는지, 클라이언트가 파일을 생성할 권한을 가지고 있는지 등 다양한 검사 수행
    • DistributedFileSystem은 데이터를 쓸수있도록 클라이언트에 FSDataOutputStream을 반환한다.
    • DFSOutputStream으로 래핑된다.
  3. 스트림을 통해 클라이언트가 데이터를 쓸때
    • 스트림은 데이터를 패킷으로 분리하고 데이터큐라 불리는 내부큐로 패킷을 보낸다.
  4. DataStreamer는 데이터큐에 있는 패킷을 처리한다.
    • 데이터노드의 목록을 요청
    • 파이프라인을 통해 첫번째 데이터노드부터 차례로 패킷을 전송한다.
    • 큐에 있는 패킷은 파이프라인의 모든 데이터노드로부터 ack 응답을 받아야 제거된다.
    • 데이터를 쓰는 중에 장애가 발생하면 장애복구 작업이 시작
      1. 먼저 파이프라인이 닫히고 ack큐에 있는 모든 패킷은 데이터큐 앞쪽에 다시 추가된다.
      2. 이러면 패킷이 하나도 유실되지 않는다.
      3. 정상 데이터노드는 네임노드로부터 새로운 ID를 다시 받는다.
      4. 장애가 발생한 데이터노드가 복구되면 불완전한 블록은 삭제된다.
      5. 장애 데이터노드는 파이프라인에서 제거된다.
      6. 두개 이상의 데이터노드에 장애가발생할 확률은 희박하다
    • dfs.namenode.replication.min에 설정된 복제소에만 블록이 저장되면 쓰기 작업은 성공한 것으로 간주DFSOutputStream은 데이터노드의 승인여부를 기다리는 ack큐라 불리는 내부 패킷큐를 유지한다.
    • 목표 복제계수에 도달할때까지 비동기로 복제가 수행된다.
  5. 데이터 쓰기를 완료할때 스트림에 close()메서드를 호출한다.
    • 파이프라인에 남아있는 모든 패킷을 플러시하고 승인이 나기를 기다린다.
  6. 모든 패킷이 완전히 전송되면 네임노드에 파일완료 신호를 보낸다.
복제본 배치

네임노드는 복제본을 저장할 데이터노드를 어떻게 선택할까?
신뢰성과 쓰기 대역폭과 읽기 대역폭은 서로 트레이드오프관계다.
가령 쓰기 대역폭을 줄이기 위해 단일 노드에 모든 복제본을 배치하면 중복성은 제공되지 않는다. 해당 노드에 장애가 발생하면 데이터는 유실된다. 외부랙에서 읽기를 수행할때 읽기 대역폭이 증가한다.

하둡의 전략은 첫번째 복제본을 클라이언트와 같은 노드에 배치한다. 그러나 클라이언트가 클러스터 외부에 있으면(데이너노드가 아니면) 무작위로 노드를 선택한다. 두번째 복제본은 첫번째 노드와 다른 랙의 노드에 배치된다., 세번째 복제본은 두번째 노드와 같은 랙의 다른 노드에 배치된다. 그 이상의 복제본은 클러스터에서 무작위로 선택하여 배치한다.

하둡의 기본 전략은 신뢰성(블록을 두 랙에 저장), 쓰기 대역폭(쓰기는 하나의 네트워크 스위치만 통과), 읽기 성능(두 랙에서 가까운 랙을 선택), 블록의 분산 사이의 균형이다.

3.6.3 일관성 모델

파일에 대한 읽기와 쓰기의 데이터 가시성은 파일 시스템의 일관성 모델로 설명가능하다.

일단 파일을 생성하면 파일 시스템의 네임스페이스에서 그 파일을 볼 수 있다.

Path p = new Path("p");
fs. create(p);
assertThat(fs.exists(p), is(true));

그러나 스트림이 플러시된 상황에서 파일에 저장된 내용을 바로 볼수있지는 않다.

한 블록 이상 데이터가 기록되었을 때 새로운 리더는 기록 완료된 블록의 내용을 볼 수 있다.

 

따라서 HDFS는 FSDataOuputStream의 hflush() 메서드를 통해 데이터 노드에 있는 모든 버퍼의 내용이 플러시되도록 강제하는 방법을 제공한다. hflush()가 성공했다는 것은 파이프라인에 있는 모든 데이터노드가 파일의 특정부분까지 데이터쓰기를 완료했다는 것을 의미한다.

그러나 데이터노드가 디스크에 데이터를 썼다는 것을 보장하지는 않는다. 이렇게 메모리에 데이터를 쓰기 때문에 데이터 센터의 전원이 중단되면 유실 가능성이 있다.

 

강한 신뢰성을 보장받기 위해서는 hflush()대신 hsync()메서드를 사용해야 한다.

 

HDFS의 파일을 닫는 것은 암묵적으로 hflush()를 수행하는 것이다.

애플리케이션 설계를 위한 결론

hflush()나 hsync()를 호출하지 않은 상황에서 클라이언트나 시스템에 장애가 발생하면 데이터 블록을 잃어버릴수도 있다.

따라서 어느정도 레코드나 바이트를 쓴 후 적절한 시점에 반드시 hflust()를 호출해야한다. 이 연산은 어느정도 오버헤드가 있으며 hsync()가 좀 더 크다.

3.7 distcp로 병렬 복사하기

하둡은 병렬로 다량의 데이터를 하둡 파일시스템으로부터 복사하기 위한 distcp라는 유용한 프로그램을 제공한다.

hadoop distcp file1 file2
hadoop distcp dir1 dir2
  • dir2이 존재하지 않는다면 새로 생성되고 복사
  • dir2이 존재한다면 dir2/dir1 형태로 하위 디렉토리 생성
  • 덮어쓰고 싶다면 -overwrite 사용
  • -update옵션으로 변경 부분만 업데이트 가능

기본적으로 맵리듀스 잡으로 구현되어있고 전반에 병렬로 수행되는 맵 태스크를 이용하여 복사작업을 한다.(리듀서는 없다)

가장 일반적인 사용 사례는 두 HDFS 클러스터 간의 데이터 이동이다.

hadoop distcp -update -delete -p hdfs://namenode1/foo hdfs://namenode2/foo

// 호환되지 않는 HDFS 버전으로 운영중인 경우 아래처럼 webhdfs 프로토콜이나 HttpFS 프록시 방식 활용 가능

hadoop distcp webhdfs://namenode1:50070/foo webhdfs://namenode2:50070/foo

3.7.1 HDFS 클러스터 균형 유지

HDFS는 클러스터 전반에 걸쳐 파일 블록이 고르게 분산되었을때 가장 잘 동작한다. 따라서 distcp가 이러한 클러스터의 균형유지를 방해하지 않도록 보장해야한다.

 

가령 -m옵션값으로 1을 설정한 경우,. 단일 맵이 복사를 수행하는데 이는 느리고 비효율적인 자원 활용일 뿐만 아니라, 복사과정은 각 블록의 첫번째 복제본이 단일 맵을 실행하는 노드에 저장되어야 한다.

 

 

Reference

728x90

+ Recent posts