LabHub
学习 学习路径 课程

Apache Hadoop — HDFS 와 YARN 을 한 파드에 세우고 운영한다 · Hadoop Streaming · 讲解

Hadoop Streaming 은 표준 입출력 한 줄이 곧 계약이다

在 LabHub 中继续学习

한 줄 요약

Hadoop Streaming 은 맵퍼와 리듀서를 표준 입력을 읽고 표준 출력에 쓰는 아무 프로그램으로 바꿔 끼우는 도구다. 계약은 단순하다 — 한 줄이 한 레코드, 첫 탭 앞이 키, 리듀서에는 키로 정렬된 줄이 온다. 단순한 만큼 키가 바뀌는 경계를 스크립트가 직접 찾아야 하고, 실패와 카운터도 종료 코드와 표준 오류로 말해야 한다.

概念图: 표준 입력을 읽고 표준 출력에 쓰는 아무 프로그램 · 키가 바뀌는 경계를 스크립트가 직접 찾아야 · 별도 프로세스 · 첫 탭 문자 앞이 키, 뒤가 값

왜 이게 필요했나

MapReduce 의 원래 계약은 자바 인터페이스다. Mapper 를 상속하고 Writable 타입을 맞추고 jar 로 묶어야 한다. 접근 로그에서 상태 코드별 요청 수를 세는 일에 그만큼의 격식은 무겁다. 게다가 자료를 다루는 사람 대부분은 파이썬이나 셸로 이미 한 대짜리 스크립트를 가지고 있다.

[Hadoop Streaming 문서](https://hadoop.apache.org/docs/r3.5.0/hadoop-streaming/HadoopStreaming.html)는 이 틈을 메운다. 실행 파일이나 스크립트라면 무엇이든 맵퍼와 리듀서로 쓸 수 있고, 문서의 첫 예는 맵퍼로 /bin/cat, 리듀서로 /usr/bin/wc 를 쓴다. 한 대에서 cat log | map.py | sort | reduce.py 로 돌던 파이프라인이 거의 그대로 수백 대로 퍼진다. 가운데의 sort 를 MapReduce 의 셔플이 대신하는 셈이다.

어떻게 동작하나

문서에 따르면 맵 태스크는 시작할 때 지정한 실행 파일을 별도 프로세스로 띄운다. 입력 조각을 줄로 바꿔 그 프로세스의 표준 입력에 흘려 넣고, 표준 출력에서 나오는 줄을 모아 키와 값의 쌍으로 바꾼다. 기본 규칙은 첫 탭 문자 앞이 키, 뒤가 값이고, 탭이 없으면 줄 전체가 키이고 값은 비어 있다. 리듀서 쪽도 같다. 프레임워크가 정렬·병합한 쌍을 다시 키\t값 줄로 풀어 리듀서 프로세스의 표준 입력에 넣는다.

이 계약에서 가장 중요한 사실은 리듀서가 받는 것이 키별 묶음이 아니라 정렬된 줄의 흐름이라는 점이다. 자바 리듀서는 reduce(키, 값 목록) 을 키마다 한 번씩 불려 받지만, 스트리밍 리듀서는 줄을 하나씩 읽으며 "키가 바뀌었는가" 를 스스로 판단해야 한다. [MapReduce 튜토리얼](https://hadoop.apache.org/docs/r3.5.0/hadoop-mapreduce-client/hadoop-mapreduce-client-core/MapReduceTutorial.html)이 말하는 대로 맵 출력은 정렬된 뒤 리듀서별로 나뉘어 오므로, 같은 키의 줄은 반드시 붙어서 온다. 리듀서는 그 보장 하나에 기대어 앞 줄의 키를 기억해 두고, 키가 바뀌는 순간 합계를 내보내고 초기화한다.

import syscurrent, total = None, 0for line in sys.stdin:    key, _, value = line.rstrip("\n").partition("\t")    if key != current:        if current is not None:            print(f"{current}\t{total}")        current, total = key, 0    total += int(value)if current is not None:    print(f"{current}\t{total}")

마지막 키를 루프 뒤에서 한 번 더 내보내는 줄을 빼먹는 것이 가장 흔한 실수다. 그러면 결과에서 정렬상 맨 마지막 키 하나가 조용히 사라진다.

맵퍼와 리듀서 스크립트는 작업 노드에 있어야 한다. -files 로 넘기면 태스크의 작업 폴더에 같은 이름의 심볼릭 링크가 생긴다. 문서는 -files-D 같은 일반 옵션을 스트리밍 옵션보다 앞에 두지 않으면 명령이 실패한다고 경고한다.

셔플을 다루는 손잡이

컴바이너. -combiner 로 실행 파일을 하나 더 줄 수 있다. 상태 코드별로 1 을 내놓는 맵퍼라면 리듀서와 같은 합산 스크립트를 컴바이너로 걸어 셔플로 넘어가는 줄 수를 크게 줄일 수 있다. 컴바이너가 몇 번 돌지는 보장되지 않으므로 여러 번 적용해도 결과가 같은 합산류만 건다.

키 필드와 분할. 키가 2026-09-19.404 처럼 여러 필드로 되어 있을 때, 정렬은 전체 키로 하되 분할은 앞 필드만으로 하고 싶을 때가 있다. 문서의 KeyFieldBasedPartitioner 예가 그것이다. stream.num.map.output.key.fields 로 키가 몇 필드인지, map.output.key.field.separator 로 필드 구분자를, mapreduce.partition.keypartitioner.options=-k1,1 처럼 분할에 쓸 필드를 준다. 그러면 같은 날짜의 줄은 모두 같은 리듀서로 가고, 그 안에서는 전체 키 차례로 정렬되어 온다. 날짜별 결과 파일을 원하거나, 한 리듀서 안에서 날짜별 소계를 내고 싶을 때 쓴다.

리듀서 수와 맵만 있는 잡. 기본 리듀서 수는 [mapred-default.xml](https://hadoop.apache.org/docs/r3.5.0/hadoop-mapreduce-client/hadoop-mapreduce-client-core/mapred-default.xml)의 mapreduce.job.reduces 기본값 1 이다. 문서에 따르면 0 으로 두면 리듀서 없이 맵 출력이 곧 결과가 된다. 줄을 거르거나 형식만 바꾸는 일이면 셔플 자체를 없애는 편이 가장 빠르다.

실패와 카운터를 말하는 법

스트리밍 스크립트는 자바 API 가 없으니 프레임워크와 두 통로로만 말한다. 하나는 종료 코드다. 문서에 따르면 기본으로 0 이 아닌 종료 코드로 끝난 태스크는 실패로 처리된다. 실패한 태스크는 다시 시도되고, mapreduce.map.maxattempts 기본값이 4 이므로 같은 맵이 네 번 실패하면 잡 전체가 실패한다. 깨진 줄 하나에 예외를 던지는 맵퍼는 그 줄이 든 조각에서 네 번 죽고 잡을 끌고 내려간다.

다른 하나는 표준 오류다. reporter:counter:<그룹>,<카운터>,<양> 형식의 줄을 stderr 에 쓰면 카운터가 올라가고, reporter:status:<메시지> 는 상태 문구를 바꾼다. 그래서 깨진 줄은 예외로 죽지 말고 건너뛰면서 reporter:counter:logs,malformed,1 을 쓰는 편이 낫다. 잡은 끝까지 돌고, 몇 줄이 깨졌는지는 카운터에 숫자로 남는다. 표준 출력에 섞어 쓰면 그것이 결과 레코드가 되어 버리니 반드시 stderr 로 보낸다.

설정값도 환경 변수로 읽을 수 있다. 문서에 따르면 설정 이름의 점이 밑줄로 바뀌어, 예를 들어 mapreduce.job.idmapreduce_job_id 로 보인다.

현장에서 만나는 모습

첫째, 로컬에서는 맞는데 클러스터에서 마지막 키가 빠진다. 리듀서가 루프 뒤에서 마지막 묶음을 내보내지 않은 경우다. 한 대에서 sort 로 시험할 때 작은 자료로는 우연히 안 드러나기도 한다.

둘째, 잡이 네 번 재시도하고 죽는다. 로그 한 줄의 인코딩이 깨졌거나 칸 수가 모자란 줄이 있다. 파이썬 맵퍼가 그 줄에서 예외를 던지면 같은 조각의 재시도도 같은 줄에서 죽는다. 재시도는 일시적 장애를 위한 장치지 나쁜 자료를 위한 장치가 아니다.

셋째, 카운터가 결과 파일에 찍혀 나온다. reporter:counter 를 print 로 표준 출력에 썼다. 결과에 reporter:counter:... 로 시작하는 키가 생긴다.

넷째, 스크립트가 노드에 없다는 오류. -files 를 빼먹었거나 스트리밍 옵션 뒤에 두었다. 실행 권한과 첫 줄의 인터프리터 지정도 함께 확인한다.

실무에서 진짜 중요한 것

다음 실습에서 할 것

접근 로그에서 상태 코드마다 1 을 내놓는 파이썬 맵퍼와, 정렬된 입력을 이어 읽으며 합을 내는 리듀서를 만들어 스트리밍 잡으로 돌린다. 같은 합산 스크립트를 컴바이너로 걸어 맵 출력과 리듀스 입력 레코드 수를 카운터로 견주고, 날짜와 경로를 탭으로 이은 두 칸짜리 키를 KeyFieldBasedPartitioner 로 첫 칸(날짜)만 보고 리듀서 둘에 나눈다. 깨진 줄은 표준 오류로 사용자 카운터를 올려 세고, 특정 경로에서 일부러 죽는 맵퍼를 맵 시도 1회로 돌려 잡이 곧바로 실패하는 모습을 본 뒤 같은 이름의 잡을 정상 맵퍼로 다시 돌려 성공시킨다.