ImportTsv 는 MapReduce 잡이다. 그래서 느릴 때 볼 곳이 HBase 인지 YARN·MapReduce 인지부터 가려야 한다.
| 관찰 | 병목 |
|---|---|
| map 진행률이 좀처럼 오르지 않는다 | 입력 분할·매퍼 수·읽기 성능 |
| map 은 금방 100% 인데 그 뒤로 오래 걸린다 | 출력 커밋(파일 이동) 또는 reduce 단계 |
bulk.output 없이 직접 적재하며 느리다 |
RegionServer 쓰기 경로 |
bulk.output 을 주면 HFile 만 만들고 적재는 나중에 하므로, HBase 쓰기 경로가 완전히 빠진다. HBase 쪽 지표(memstore · compaction queue)가 멀쩡한데 잡이 느리다면 HBase 를 더 볼 이유가 없다.
같은 데이터인데 어떤 때는 진행률이 0%→100% 로 한 번에 튀고 어떤 때는 39%→67%→100% 로 오른다. 둘 다 매퍼가 하나여도 그렇다. 진행률은 매퍼가 읽은 입력 바이트 비율로 보고되는데, 입력이 분할 불가능하면 보고 지점이 성겨져 계단식으로 튄다.
매퍼 수를 결정하는 것은 입력 파일의 분할 가능 여부와 크기다.
hdfs dfs -ls -h /input/path
hdfs dfs -du -s -h /input/path
파일이 하나이고 gzip 으로 압축돼 있으면 분할할 수 없어 매퍼가 무조건 하나다. 압축을 풀거나, bzip2·LZO 인덱스처럼 분할 가능한 형식으로 바꾸거나, 파일을 여러 개로 나눈다.
분할 크기는 다음으로 조정한다. 값은 바이트다.
-Dmapreduce.input.fileinputformat.split.maxsize=134217728
-Dmapreduce.input.fileinputformat.split.minsize=1
파일이 여러 개인데도 매퍼가 하나라면 CombineFileInputFormat 계열 설정이 작은 파일을 하나로 묶은 것일 수 있다.
map 100% 가 찍히고 한참 뒤에 Job completed successfully 가 나온다면 출력 커밋 단계다. 기본 출력 커미터는 각 태스크의 결과를 임시 디렉터리에 쓴 뒤, 잡이 끝날 때 최종 디렉터리로 옮긴다. HDFS 에서 rename 은 메타데이터 연산이라 보통 빠르지만, 파일 수가 많거나 NameNode 가 바쁘면 눈에 띄게 느려진다.
-Dmapreduce.fileoutputcommitter.algorithm.version=2
버전 2 는 태스크가 끝날 때마다 최종 위치로 옮겨 마지막 단계의 일괄 이동을 없앤다. 대신 잡이 중간에 실패하면 최종 디렉터리에 부분 결과가 남으므로, 실패 시 출력 디렉터리를 지우고 다시 돌리는 절차를 전제로 쓴다. 객체 스토리지(S3 등)에서는 rename 이 복사라서 이야기가 또 다르므로 전용 커미터를 쓴다.
bulk.output 으로 만든 HFile 을 적재하는 단계도 별도로 시간이 든다. 이 단계는 MapReduce 가 아니다.
hbase org.apache.hadoop.tools.LoadIncrementalHFiles /staging/hfiles/<TABLE> <TABLE>
TSV 라고 하지만 실제로는 \x01 같은 제어 문자를 쓰는 경우가 많다. 셸에서 그대로 넘긴다.
hbase org.apache.hadoop.hbase.mapreduce.ImportTsv \
-Dimporttsv.separator="$(printf '\x01')" \
-Dimporttsv.columns=HBASE_ROW_KEY,cf:col1,cf:col2 \
-Dimporttsv.bulk.output=/staging/hfiles/<TABLE> \
<TABLE> /input/path
echo -e 는 셸에 따라 동작이 다르므로 printf 가 안전하다. 구분자가 잘못 넘어가면 파싱 실패로 모든 행이 Bad Line 이 된다.
-Dimporttsv.skip.bad.lines=false
기본값은 건너뛰기이므로, 검증 단계에서는 명시적으로 false 로 두어 잘못된 행을 실패로 만든다.
클러스터에 여러 JDK 가 설치돼 있으면 명령이 의도하지 않은 버전으로 뜬다. 잡 실패 로그에 java.version=11 처럼 찍혀 있으면 그 명령 하나에만 다른 JDK 를 지정한다.
JAVA_HOME=/usr/lib/jvm/java-1.8.0 \
hbase org.apache.hadoop.hbase.mapreduce.ImportTsv ...
이것은 클라이언트(드라이버) 쪽 JVM 만 바꾼다. 실제 매퍼와 리듀서는 NodeManager 가 띄우므로 컨테이너 쪽도 함께 지정해야 한다.
-Dmapreduce.map.env=JAVA_HOME=/usr/lib/jvm/java-1.8.0 \
-Dmapreduce.reduce.env=JAVA_HOME=/usr/lib/jvm/java-1.8.0 \
-Dyarn.app.mapreduce.am.env=JAVA_HOME=/usr/lib/jvm/java-1.8.0
지정한 경로는 모든 노드에 같은 자리에 있어야 한다. 한 노드라도 없으면 그 노드에 배치된 태스크만 실패한다.
IPC Server handler 1 on 36731 같은 줄에서 36731 은 그 데몬이 listen 하는 포트이고 1 은 핸들러 스레드 번호다. 특정 핸들러 번호가 계속 등장한다고 해서 문제의 원인은 아니다. 이 줄이 많이 보이면서 태스크 진행이 더디면 그 포트를 쓰는 데몬을 찾아 그쪽 부하를 본다.
ss -lntp | grep 36731
ClientCnxn: EventThread shut down for session 은 ZooKeeper 세션이 정상 종료될 때 남는 줄이다. 이 줄과 태스크 커밋 사이에 시간 차가 크다면 ZooKeeper 가 아니라 그 사이의 다른 단계(출력 커밋 등)를 본다.