Apache Hadoop — HDFS 와 YARN 을 한 파드에 세우고 운영한다 · 작은 파일과 HAR · 讲解
작은 파일은 디스크가 아니라 NameNode 의 메모리를 먹는다
한 줄 요약
HDFS 에서 파일 하나의 비용은 바이트가 아니라 NameNode 메모리 속 객체 수로 매겨진다. 1KB 파일 2,000개는 디스크로는 2MB 지만 NameNode 에는 파일 2,000개와 블록 2,000개의 객체다. 하둡 아카이브(HAR)는 그 객체를 몇 개로 접어 이름공간을 가볍게 하고, 원본을 지우는 것은 사람의 몫이다.
왜 이게 문제가 되나
[HDFS 설계 문서](https://hadoop.apache.org/docs/r3.5.0/hadoop-project-dist/hadoop-hdfs/HdfsDesign.html)는 NameNode 가 이름공간 전체와 블록 대응표를 메모리에 들고 있다고 적는다. 디스크는 DataNode 를 늘리면 얼마든지 늘지만, 이름공간은 NameNode 한 프로세스의 힙 안에 산다. 파일 하나, 디렉터리 하나, 블록 하나가 모두 그 힙 안의 객체다. 그래서 HDFS 의 용량에는 두 축이 있다. 바이트로 재는 디스크 용량과, 객체 수로 재는 이름공간 용량이다.
같은 문서는 HDFS 가 큰 파일에 맞춰져 있다고 분명히 한다. 전형적인 파일은 기가바이트에서 테라바이트이고, 한 인스턴스가 수천만 개의 파일을 지원해야 한다고 적는다. 수천만이라는 규모가 설계가 염두에 둔 크기다. 같은 수천만 개라도 파일이 평균 1GB 면 수십 페타바이트를 담지만, 평균 10KB 면 수백 기가바이트를 담는 사이에 이름공간이 먼저 찬다. [Federation 문서](https://hadoop.apache.org/docs/r3.5.0/hadoop-project-dist/hadoop-hdfs/Federation.html)가 NameNode 를 여럿 두는 이유로 "작은 파일이 많은 배포" 를 직접 드는 것도 이 때문이다.
작은 파일은 계산 쪽에서도 손해다. [MapReduce 튜토리얼](https://hadoop.apache.org/docs/r3.5.0/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html)에 따르면 맵 수는 입력 블록 수가 좌우하고, 태스크를 준비하는 데 시간이 들어 맵 하나가 적어도 1분은 도는 것이 좋다. 1KB 파일 2,000개는 블록 2,000개이고, 준비에 몇 초씩 드는 맵 태스크가 1초도 안 걸릴 일을 하러 수천 번 뜬다.
어떻게 동작하나
객체 수는 숫자로 볼 수 있다. [지표 문서](https://hadoop.apache.org/docs/r3.5.0/hadoop-project-dist/hadoop-common/Metrics.html)의 FSNamesystem 항목에서 FilesTotal 은 파일과 디렉터리 수, BlocksTotal 은 할당된 블록 수다. NameNode 웹 주소(기본 9870 포트)의 /jmx 로 바로 읽힌다. 빈 디렉터리 하나에 작은 파일 2,000개를 올리면 두 값은 이런 식으로 움직인다(앞의 숫자는 예시다).
put 전 FilesTotal= 41 BlocksTotal= 12put 2,000 FilesTotal=2042 BlocksTotal=2012 <- 파일 2,000 + 디렉터리 1, 블록 2,000작은 파일 하나는 블록 하나를 만든다. [hdfs-default.xml](https://hadoop.apache.org/docs/r3.5.0/hadoop-project-dist/hadoop-hdfs/hdfs-default.xml)의 dfs.blocksize 기본은 128MB 지만, 그것은 블록의 최대 크기일 뿐이다. 1KB 파일의 블록은 DataNode 디스크에서 1KB 남짓만 차지한다. 디스크는 낭비되지 않는다. 낭비되는 것은 그 블록 하나를 기억하는 NameNode 의 객체와, 그 블록을 보고하고 관리하는 일이다. 작은 파일 문제를 "디스크가 아깝다" 로 설명하면 틀린 설명이 된다.
하둡 아카이브(HAR) 는 이 객체들을 접는다. [Hadoop Archives 안내](https://hadoop.apache.org/docs/r3.5.0/hadoop-archives/HadoopArchives.html)에 따르면 .har 는 파일 시스템 위의 디렉터리 하나이고, 안에 메타데이터 _index·_masterindex 와 자료 파일 part-* 가 들어 있다. 원본 파일들의 내용은 몇 개의 part 파일에 이어 붙여지고, _index 가 각 원본 파일의 이름과 part 파일 안의 위치를 기억한다. 2,000개의 파일이 NameNode 에게는 몇 개의 파일이 되는 것이다.
hadoop archive -archiveName logs.har -p /data/raw day1 /data/archivehdfs dfs -ls har:///data/archive/logs.har/day1HAR 은 파일 시스템 층으로 자신을 드러낸다. har:// 주소로 ls·cat 같은 셸 명령이 그대로 되고, MapReduce 입력으로도 쓸 수 있다. 대신 몇 가지 대가가 있다.
- 불변이다. 문서대로 HAR 안에서 이름 바꾸기·지우기·만들기는 오류를 낸다. 하루치를 고치려면 아카이브를 다시 만든다.
- 만드는 데 MapReduce 잡이 돈다. 보통은 YARN 위에서 돌고, YARN 을 켜지 않았다면
-D mapreduce.framework.name=local로 클라이언트 JVM 안에서 돌린다. distcp 도 같다. - 원본을 지우지 않는다. 문서가 강조하듯 이름공간을 줄이려면 아카이브를 확인한 뒤 원본을 직접 지워야 한다. 지우기 전까지 객체 수는 오히려 늘어 있다.
- 복제 수 기본이 3 이다.
-r을 주지 않으면 복제 3 으로 만든다. DataNode 가 하나인 이 실습 환경에서는-r 1을 주지 않으면 복제 부족 블록이 생긴다. - 읽기는 한 단계 더 거친다. 파일 하나를 읽으려면 색인에서 위치를 찾은 뒤 part 파일의 그 구간을 읽는다. 무작위로 조금씩 자주 읽는 용도가 아니라 보관용이다.
HAR 이 줄이는 것은 이름공간의 객체 수다. HAR 을 MapReduce 입력으로 넣어도 안의 논리 파일들은 여전히 파일로 보이므로, 맵 수 문제까지 저절로 풀리지는 않는다. 계산이 문제라면 애초에 큰 파일로 합쳐 쓰는 편이 낫다.
distcp 로 옮기고 푼다
대량 복사는 [DistCp](https://hadoop.apache.org/docs/r3.5.0/hadoop-distcp/DistCp.html)로 한다. 이것도 MapReduce 잡이다. 복사할 파일 목록을 펼쳐 맵 태스크들에 나눠 주고, 맵마다 자기 몫을 복사한다. 문서에 따르면 맵마다 비슷한 바이트를 맡도록 나누지만 파일이 가장 작은 단위라서, 작은 파일이 많으면 복사 역시 파일 수에 끌려간다. -update 는 대상에 없거나 다른 파일만 복사해 두 번째 실행부터 빠르다. HAR 을 푸는 것도 복사다. har:// 주소를 원본으로 hdfs dfs -cp 하면 차례로, distcp 하면 병렬로 풀린다.
현장에서 만나는 모습
첫째, NameNode 가 GC 로 멈춘다. 디스크는 절반이 비어 있는데 NameNode 힙이 가득 차 긴 GC 가 반복된다. 원인을 추적하면 누군가 5분마다 수천 개씩 작은 파일을 떨어뜨리는 수집기다. FilesTotal 의 추세를 감시 항목에 넣어야 하는 이유다.
둘째, 디렉터리 하나에 파일이 너무 많다. dfs.namenode.fs-limits.max-directory-items 의 기본은 1,048,576 이다. 한 디렉터리에 계속 떨어뜨리는 수집기는 언젠가 이 벽에 부딪혀 쓰기가 실패한다.
셋째, 아카이브를 만들었는데 객체가 줄지 않았다. 원본을 지우지 않았다. 아카이브를 har 주소로 읽어 확인하고, 그다음에 원본을 지운다. 순서를 바꾸면 되돌릴 길이 없다.
넷째, 진짜 해결은 쓰는 쪽에 있다. HAR 은 이미 쌓인 것을 치우는 도구다. 새로 쌓이는 것은 수집 단계에서 묶어 쓰거나, 정기적으로 큰 파일로 합치는 작업을 둬야 문제가 다시 자라지 않는다.
실무에서 진짜 중요한 것
- 파일의 비용은 객체 수다. 파일·디렉터리·블록이 모두 NameNode 힙의 객체다.
- 작은 블록은 디스크를 낭비하지 않는다. 낭비되는 것은 NameNode 의 기억과 태스크 수다.
- FilesTotal·BlocksTotal 을 본다. 디스크 사용률만 보면 이 문제는 보이지 않는다.
- HAR 은 원본을 지우지 않는다. 확인한 뒤 직접 지워야 객체가 줄어든다.
- HAR 은 불변이고 복제 기본이 3 이다. 고칠 일이 있는 자료에는 맞지 않고, 작은 클러스터에서는 -r 을 준다.
- 근본 해결은 쓰는 쪽에서 묶는 것이다. 치우는 도구로 새는 곳을 막을 수는 없다.
다음 실습에서 할 것
작은 파일 2,000개를 HDFS 에 올리고 count 와 fsck 로 파일·디렉터리·블록 수를 세어 NameNode 가 떠안은 객체 수를 잰다. YARN 을 켜지 않고 로컬 모드로 hadoop archive 를 돌려 그 파일들을 복제 1 짜리 HAR 하나로 묶고, har 주소로 목록을 보아 안의 파일 수를 센 뒤 파일 하나를 읽어 로컬로 꺼낸다. 원본 디렉터리와 HAR 의 객체 수를 나란히 견주어 얼마나 줄어드는지 숫자로 확인하고, 마지막으로 distcp 로 아카이브의 백업 사본을 만든다.