Part 3 파생 데이터
레코드 시스템과 파생 데이터 시스템
- 데이터를 저장하고 처리하는 시스템은 크게 두 분류로 나눌 수 있다.
- 레코드 시스템
- 진실의 근원(source of truth) 라고도 하며 새로운 데이터는 먼저 레코드 시스템에 저장된다.
- 일반적으로 정규화를 거쳐 정확하게 한 번 표현된다.
- 파생 데이터 시스템
- 다른 시스템에 존재하는 데이터를 가져와 특정 방식으로 변환하고 처리한 결과다.
- 파생 데이터를 잃더라도 원천 데이터로부터 다시 생성할 수 있다.
- 기존 데이터를 복제한다는 의미에서 중복(redundant) 으로 볼 수 있다.
- 하지만 파생 데이터는 읽기 질의 성능을 높이는데 종종 필수적이다.
- 본통 비정규화 과정을 통해 생성한다.
- 시스템 아키텍처가 명료성을 갖추기 위해서는 데이터가 어떤 데이터로부터 파생됐는지를 명확히 해야 한다.
10장 - 일괄 처리
- 시스템을 구축하는 세 가지 유형
- 서비스(온라인 시스템)
- 서비스는 클라이언트로부터 요청, 지시를 기다린다.
- 가능한 빨리 요청을 처리한다.
- 중요한 성능 지표는 응답 시간이다.
- 일괄 처리 시스템(오프라인 시스템)
- 매우 큰 입력 데이터를 받아 처리하고 결과 데이터를 생산한다.
- 매우 오래 걸리므로 사용자가 작업이 끝나길 대기하지 않는다.
- 일배치 등 반복적으로 수행한다.
- 주요한 성능 지표는 처리량이다.
- 스트림 처리 시스템(준실시간 시스템)
- 온라인과 오프라인, 일괄 처리 사이의 어딘가에 있다.
- 입력 데이터를 소비하고 출력 데이터를 생산한다.
- 일괄 처리 시스템보다 지연 시간이 낮다.
- 일괄 처리는 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션을 구축하는데 매우 중요한 요소
- 2004년 발표된 맵리듀스가 대표적
유닉스 도구로 일괄 처리하기
- nginx 기본 액세스 로그 형식을 분석하는 예시를 살펴보자
- 로그의 포맷

단순 로그 분석
- 유닉스 도구를 사용해 로그를 처리하여 웹사이트 트래픽에 관한 보고서를 작성한다고 가정해보자.

연쇄 명령 대 맞춤형 프로그램
- 유닉스 연쇄 명령 대신 루비 코드로 작성해보자.
- 문법 외 차이점은 실행 흐름이 크게 다르단 점이다. 대용량 파일을 분석해보면 차이가 확연히 드러난다.

정렬 대 인메모리 집계
- 루비 스크립트는 URL 해시 테이블을 메모리에 유지한다.
- 허용 메모리보다 작업 세트가 크다면 정렬 접근법을 사용하는 것이 좋다.
- 정렬 접근법은 디스크를 효율적으로 사용한다. (SS테이블과 LSM 트리)
- 데이터 청크를 메모리에서 정렬하고 청크를 세그먼트 파일로 디스크에 저장한다.
- 정렬된 세그먼트 파일 여러 개를 한 정렬 파일로 병합한다.
- 병합 정렬은 순차적 접근 패턴을 따르고 디스크 상에서 좋은 성능을 낸다.
- 리눅스의 sort 유틸리티는 메모리보다 큰 데이터셋을 자동으로 디스크로 보내고 여러 CPU 코어에서 병렬로 정렬한다.
- 이는 유닉스 연쇄 명령이 메모리 부족 없이 손쉽게 큰 데이터셋으로 확장 가능하다는 의미이다.
유닉스 철학
- 1978년에 기술된 유닉스 철학은 아래와 같다.
- 각 프로그램이 한 가지 일만 하도록 작성하라.
- 모든 프로그램의 출력은 다른 프로그램의 입력으로 쓰일 수 있다고 생각하라.
- 소프트웨어를 빠르게 써볼 수 있게 설계하고 구축하라.
- 프로그래밍 작업을 줄이려면 도구를 사용하라. (도구 빌드에 한참 둘러가야 하고 사용 후 바로 버린다 할지라도)
- sort의 경우 한 가지 일을 잘 해내는 좋은 예시다. 단, 단독으론 그다지 유용하지 않다. uniq 같은 다른 유닉스 도구와 조합할 때 강력해진다.
- 유닉스에서 이런 결합성을 부여하는 것은 무엇일까?
동일 인터페이스
- 어떤 프로그램의 출력을 다른 프로그램의 입력으로 쓰려면 프로그램들이 같은 데이터 형식을 사용해야 한다.
- 즉 호환 가능한 인터페이스를 써야 한다.
- 유닉스에서 인터페이스는 파일이다.
- 파일은 단지 순서대로 정렬된 바이트의 연속이다.
- 많은 유닉스 프로그램은 연속된 바이트를 아스키 텍스트로 취급한다.
- awk, sort, uniq, head는 입력 파일을
\n 문자로 분리된 레코드로 다룬다.
- 완벽하진 않음에도 유닉스의 동일 인터페이스는 여전히 상호 운용, 구성 면에서 대단하다.
로직과 연결의 분리
- 유닉스 도구의 다른 특징으론 표준 입력(stdin) 과 표준 출력(stdout) 을 사용한다는 점이 있다.
- 혹은 파일에서 입력을 가져와 다른 파일로 출력을 재전송할 수도 있다.
- 파이프는 한 프로세스의 stdout을 다른 프로세스의 stdin과 연결한다.
- 프로그램은 입력이 어디서부터 들어오는지, 출력이 어디로 나가는지 신경쓸 필요가 없다.
- 느슨한 결합(loose coupling) , 지연 바인딩(late binding) , 제어 반전(inversion of control) 이라고도 한다.
- 프로그램에서 입출력을 연결하는 부분을 분리하면 작은 도구로 큰 시스템을 구성하기 수월해진다.
- 그러나 stdin, stdout을 사용할 때 제약사항이 있다.
- 여러 개의 입력, 출력이 필요한 때 까다로워진다.
- 프로그램의 출력을 파이프를 이용해 네트워크와 연결하진 못한다.
- 프로그램이 직접 파일을 열어 읽고 쓰거나, 서브프로세스로 다른 프로그램을 구동하거나, 네트워크 연결을 한다면 → 프로그램의 IO는 그 프로그램 자체와 서로 묶이게 된다.
- 즉, 입출력을 연결하는 유연함이 감소한다.
투명성과 실험
- 유닉스 도구가 성공적인 이유 중 하나는 진행 사항을 파악하기 쉽기 때문
- 유닉스 명령의 입력 파일은 보통 불변으로 처리된다.
- 명령을 수행하더라도 입력 파일에는 손상을 주지 않는다.
- 파이프라인의 어느 시점이든 출력을 less로 보내 원하는 형태의 출력이 나오는지 확인할 수 있다.
- 파이프라인의 특정 단계의 출력을 파일로 쓰고 그 파일을 다음 단계의 입력으로 사용할 수 있다.
- 이렇게 하면 전체 파이프라인을 다시 시작하지 않고 다음 단계부터 재시작할 수 있다.
- 유닉스 도구를 사용하는 데 가장 큰 제약은 단일 장비에서만 실행된다는 점이다.
맵리듀스와 분산 파일 시스템
- 맵리듀스는 수천 대의 장비로 분산해서 실행 가능하다.
- 입력을 수정하지 않으므로 출력 생산 외 다른 부수 효과가 없다.
- 출력 파일은 순차적으로 한 번만 쓰여지고 이미 쓰여진 부분은 고치지 않는다.
- HDFS와 같은 분산 파일 시스템 상의 파일을 입력, 출력으로 사용한다.
- HDFS 분산 파일 시스템
- NAS(Network Attached Storage), SAN(Storage Area Network) 등 공유 디스크 방식과는 반대로 비공유 원칙을 기반으로 하여 맞춤형 HW나 특별한 네트워크 인프라를 필요로 하지 않는다.
- 각 장비에서 실행되는 데몬 프로세스로 구성되고 중앙 서버인 NameNode가 각 파일 블록이 어떤 장비에 저장됐는지 추적한다. (개념적으로 매우 큰 하나의 파일 시스템)
- 파일 블록은 장애 복구를 위해 단순히 여러 장비에 동일하게 복사되거나 erasure coding 방식으로 분산 저장되기도 한다.
- 확장성이 뛰어나다. 수만대 장비를 묶어 용량이 수백 페타바이트에 이를 수 있다.
맵리듀스 작업 실행하기
- 맵리듀스의 데이터 처리 패턴
- 입력 파일을 읽고 레코드로 쪼갠다. 한 로그 라인이 한 레코드가 된다.
- 각 입력 레코드마다 매퍼 함수를 호출해 키와 값을 추출한다.
- 키를 기준으로 키-값 쌍을 모두 정렬한다.
- 정렬된 키-값 쌍 전체에 대해 리듀스 함수를 호출한다.
- 맵리듀스 작업을 생성하려면 매퍼와 리듀서라는 콜백 함수를 구현해야 한다.
- 매퍼(Mapper)
- 모든 입력 레코드마다 한 번씩만 호출된다.
- 입력 레코드로부터 키와 값을 추출한다.
- 각 레코드를 독립적으로 처리한다.
- 리듀서(Reducer)
- 매퍼가 생산한 키-값 쌍을 받아 같은 키를 가진 레코드를 모은다.
- 출력 레코드를 생산한다.
맵리듀스의 분산 실행
- 매퍼와 리듀서로 표준 유닉스 도구를 사용할 수도 있으나 보통 프로그래밍 언어로 함수를 작성한다.
- 매퍼 입력 파일의 복제본이 있는 장비에 리소스가 충분하다면 해당 장비에서 작업을 수행하려 한다. → 데이터 가까이에서 연산하기
- 네트워크로 파일을 복사하는 부담 감소, 지역성은 증가
- 애플리케이션 코드(자바라면 jar 파일)를 작업을 수행하기 적절한 장비로 복사한다.
- 리듀서 측 연산도 파티셔닝된다.
- 맵 태스크 수 : 입력 파일의 블록 수로 결정됨
- 리듀서 태스크 수 : 사용자 설정값에 의해 결정한다.
- 즉, 맵과 리듀스의 태스크 수가 다를 수 있다.
- 키-값 쌍이 어느 리듀스 태스크에서 수행될지 결정하기 위해 키의 해시값을 사용한다.
- 키-값 쌍은 반드시 정렬돼야 하지만 데이터셋이 매우 크면 한 장비에서 모두 정렬하기 어려우므로 여러 단계로 나누어 정렬을 수행한다.
- 맵 태스크는 출력을 리듀서로 파티셔닝하여 로컬 디스크에 정렬된 파일로 기록한다.
- 리듀서는 담당 파티션에 해당하는 키-값 쌍 파일을 다운로드한다.
- 리듀서를 기준으로 파티셔닝, 정렬한 뒤 매퍼로부터 데이터 파티션을 복사하는 과정을 셔플(shuffle) 이라 한다.
- 리듀스 태스크는 매퍼로부터 파일을 가져와 정렬된 순서를 유지하며 병합한다.
맵리듀스 워크플로
- 단일 맵리듀스 작업으로 해결할 수 있는 문제의 범위는 제한적이므로 여러 맵리듀스 작업을 연결해 워크플로(workflow) 를 구성하는 방식이 일반적이다.
- 한 MR 작업의 출력을 다른 MR의 입력으로 사용하는 방식
- 하둡 맵리듀스 작업 간 수행 의존성을 관리하기 위해 다양한 스케줄러가 개발됐다.
- Oozie, Azkaban, Luigi, Airflow, Pinball 등
리듀스 사이드 조인과 그룹화
- 연관된 레코드 양쪽 모두에 접근해야 하는 코드가 있다면 조인이 필수이고, DB에서 적은 수의 레코드만 질의할 경우 색인을 쓰는게 효율적이다.
- 그러나 맵리듀스에선 색인 개념이 없으며 입력 파일 전체를 읽는 전체 테이블 스캔(full table scan) 을 사용한다.
- 일괄 처리 맥락에서 조인은 데이터셋 내 모든 연관 관계를 다룬다는 뜻이다.
- 즉, 모든 사용자 데이터를 동시에 처리한다.
- 사용자 활동 이벤트 분석 예제
- 웹사이트의 로그인 사용자 이벤트 로그와 사용자 DB를 연관시키는 예제
- 가장 단순한 조인 구현 방법 :
- 이벤트 로그를 훑으면서 모든 사용자 ID마다 사용자 DB에 질의 보내기
- (단점1) DB 통신의 왕복 시간으로 처리량이 제한되고 DB가 과부하되기 쉽다. 즉 성능이 나쁘다.
- (단점2) 원격 DB에 질의하면 일괄처리가 비결정적이 된다. DB 데이터는 도중에 변경될 수 있기 때문
- 더 좋은 방법 :
- DB의 사본을 가져와 이벤트 로그와 함께 HDFS에 같이 저장하고
- 이를 맵리듀스를 사용해 연관된 레코드끼리 모두 모아 효율적으로 일괄처리하는 것
- 정렬 병합 조인 (sort-merge join)
- 매퍼가 키로 정렬된 출력을 생산하고, 리듀서는 정렬된 레코드 목록을 병합하는 방식으로 조인이 수행된다.
- 리듀서가 읽을 키-값 쌍을 정렬해 특정 값을 먼저 보도록 하는 방식을 보조 정렬(secondary sort) 이라 한다. (e.g. 사용자 DB가 활동 이벤트보다 먼저 오도록)

- 같은 곳으로 연관된 데이터 가져오기
- 병합 정렬 조인은 (사용자 ID로 조인 연산할 때) 필요한 데이터를 미리 한 곳으로 모아 리듀서를 사용자 ID 별로 한 번만 호출한다.
- 맵리듀스는 데이터를 모으는 네트워크 통신 측면 과 데이터 처리 로직 을 분리한다.
- 장비 다운 등 부분 실패가 있더라도 실패한 태스크를 확실하게 재시도
- 그룹화
- 조인 외에도 SQL의
GROUP BY 절과 같이 집계 연산(레코드 수 카운트, 값 더하기 등)을 위해 특정 키로 레코드를 그룹화할 수 있다.
- 맵리듀스 상에서 그룹화와 조인은 구현이 상당히 유사하다.
- 세션화(sessionization) : 사용자의 일련의 활동을 찾기 위해 세션별 활동을 분석할 때에도 그룹화를 사용한다.
- 쏠림 다루기
- 키 하나에 너무 많은 레코드가 쏠리면 맵리듀스가 제대로 작동하지 않는다.
- 한 리듀서만 매우 많은 레코드를 처리하느라 느려짐, 마지막 리듀서가 완료될 때까지 후속 작업 딜레이
- 핫 키(hot key) : 불균형한 활성 데이터베이스 레코드 (또는 린치핀 객체)
- 핫스팟 : 리듀서 한 개에 쏠림 현상이 발생하는 것
- 핫스팟을 완화할 알고리즘들
- Pig의 쏠린 조인(skewed join) 메서드, Crunch의 공유 조인(shared join) 메서드… → 현업에서 잘 안 쓰이니 skip
- Hive는 맵 사이드 조인(map-side join) 을 사용해 처리한다.
- 핫 키로 레코드를 그룹화, 집계하는 작업은 두 단계로 수행된다.
- 레코드를 임의의 리듀서로 보내고, 각 리듀서는 핫 키 레코드의 일부를 그룹화, 집계하여 간소화된 집계 결과를 출력한다.
- 두 번째 단계에선 첫 단계의 모든 리듀서 출력을 결합해 하나의 값으로 만든다.
맵 사이드 조인
- 앞의 리듀스 사이드 조인(reduce-side join) 은 데이터를 정렬 후 리듀서로 복사한 뒤, 리듀서 입력을 병합하는 과정에서 드는 비용이 크다는 단점이 있다.
- 반면 맵사이드 조인(map-side join) 은 리듀서도 정렬 작업도 필요 없는 축소된 맵리듀스 작업으로, 조인을 더 빠르게 수행할 수 있다.
- 브로드캐스트 해시 조인
- 작은 데이터셋과 매우 큰 데이터셋을 조인하는 경우 적용 가능
- 모든 매퍼는 작은 데이터셋 전체를 인메모리 해시 테이블에 적재한다.
- 즉, 작은 입력을 큰 입력의 모든 파티션에 브로드캐스트한다.
- 인메모리 해시 테이블 대신 로컬 디스크에 읽기 전용 색인으로 저장할 수도 있다.
- 데이터셋 전체가 메모리에 들어오지 않을 경우에도 빠르게 임의 접근이 가능한 유용한 방법
- 파티션 해시 조인
- 해시 조인 접근법을 각 파티션에 독립적으로 적용할 수 있다.
- 조인할 두 데이터셋을 조인 키(사용자ID) 를 기준으로 파티셔닝하여 각 매퍼에 재배열할 수 있다.
- ex) 3번 매퍼가 ID가 3으로 끝나는 사용자들을 해시 테이블에 올리고, ID가 3으로 끝나는 활동 이벤트를 모두 스캔하는 식
- 조인할 레코드 모두가 같은 번호의 파티션에 위치하게 된다.
- 각 매퍼의 해시 테이블에 적재할 데이터 양을 줄일 수 있다는 장점이 있다.
- Hive에서는 버킷 맵 조인(bucketed map join) 이라 한다.
- 맵 사이드 병합 조인
- 입력 데이터셋이 이미 같은 키로 파티셔닝 및 정렬됐다면 맵 사이드에서 병합 조인을 수행할 수 있다.
- 매퍼도 리듀서와 동일하게 병합 연산을 수행할 수 있다.
- 보통 선행 맵리듀스 작업이 이미 입력 데이터셋을 파티셔닝하고 정렬했을 때 가능하다.
- 리듀스 사이드가 아닌 맵 사이드에서 병합 정렬이 유용할 때가 있다.
- 파티셔닝, 정렬된 데이터셋이 바로 조인 외 다른 용도로 필요한 경우 등..
- 맵 사이드 조인을 사용하는 맵리듀스 워크플로
- MR 조인의 출력을 하위 작업에서 입력으로 사용할 때, 맵 사이드 조인과 리듀스 사이드 조인은 출력 구조가 다르다.
- 리듀스 사이드 조인 : 조인 키로 파티셔닝, 정렬해서 출력
- 맵 사이드 조인 : 큰 입력과 동일한 방법으로 파티셔닝, 정렬
- 큰 조인 입력의 파일 블록마다 맵 태스크가 실행되기 때문
- 하둡 생태계에서 데이터셋 파티셔닝 관련 메타데이터 관리
- HCatalog나 Hive 메타스토어를 사용하여 관리
일괄 처리 워크플로의 출력
- 맵리듀스와 같은 일괄처리의 결과는 어떻게 나오고 수행하는 이유는 무엇인가?
- OLTP 질의 : 색인을 사용해 소량의 레코드만 특정 키로 조회하는 것이 일반적
- 분석 질의 : 대량의 레코드를 스캔해 그룹화, 집계 연산을 수행한 결과를 보고서 형태로 출력
- 일괄 처리 : 트랜잭션 처리도, 분석도 아니다. 일괄 처리의 출력은 흔히 보고서가 아닌 다른 형태의 구조다.
검색 색인 구축
- 맵리듀스는 구글에서 검색 엔진에 사용할 색인을 구축하기 위해 처음 사용됐다.
- 일괄 처리는 색인을 구축하는데 매우 효율적이다.
- 매퍼는 문서 집합을 파티셔닝하고, 리듀서는 파티션 별 색인을 구축, 색인 파일은 분산 파일 시스템에 저장
일괄 처리의 출력으로 키-값을 저장
- 검색 색인 외에도 분류기와 같은 머신러닝 시스템(e.g. 스팸 필터, 이상 검출, 이미지 인식 등)을 구축하거나 추천 시스템을 구축하는데에도 일괄 처리는 유용하다.
- 데이터베이스 파일을 생성하는 작업도 굉장히 좋은 맵리듀스 활용법이다.
- 매퍼로 키를 추출한 다음, 키로 정렬하는 과정은 색인을 만들 때에도 필요한 작업임
- 키-값 저장소는 대부분 읽기 전용이므로 자료 구조가 단순함
- 크게 의미있는 내용은 아니므로 자세한건 skip
일괄 처리 출력에 관한 철학
- 앞서 살펴본 유닉스 철학은 데이터플로가 “프로그램이 입력을 읽어 출력을 내놓는다” 로 명확하며 과정에서 아무런 부수 효과가 없다.
- 맵리듀스 작업도 이런 철학과 마찬가지로 입력을 불변으로 처리하고, 외부 DB에 기록하는 등 부수 효과를 피한다.
- 따라서 일괄 처리 작업은 좋은 성능을 내면서도 유지보수가 간단하다.
- 다만 입력 파싱 부분에서 유닉스와 하둡은 다르다.
- 유닉스 도구는 타입이 없는 텍스트 파일을 가정, 처리하므로 입력을 파싱해야하는 부담이 있다.
- 하둡에선 구조화된 파일 형식을 사용하여 저수준 구문 변환 작업을 하지 않아도 된다. (Avro, Parquet)
하둡과 분산 데이터베이스의 비교
- 맵리듀스 논문이 발간될 당시에 맵리듀스는 전혀 새로운 개념이 아니었다.
- 대규모 병렬 처리(massively parallel processing; MPP) 데이터베이스에서 이미 맵리듀스의 처리 알고리즘과 병렬 조인 알고리즘을 구현한 바 있다.
- 맵리듀스와의 차이점
- MPP 데이터베이스는 장비 클러스터에서 분석 SQL 질의를 병렬로 수행하는 것에 초점을 둠
- 맵리듀스와 분산 파일 시스템의 조합은 아무 프로그램이나 실행할 수 있는 운영체제와 비슷한 속성을 제공함
저장소의 다양성
- MPP 데이터베이스는 특화된 저장 형태로 데이터를 가져오기 때문에 가져오기 전 신중하게 모델링해야 한다.
- 이는 중앙 집중식 데이터 수집을 느리게 만든다.
- 반면 하둡과 같은 분산 파일시스템은 데이터가 어떤 형태라도 수집 가능하며, 데이터를 어떻게 처리할지(스키마 설계 등)는 덤프 이후에 고려한다.
- 이런 속성 덕에 데이터 수집 속도가 올라간다.
- data lake 또는 enterprise data hub 라고 알려진 개념
- 현실에선 이상적인 데이터 모델을 만드는 것보다 데이터를 빨리 사용 가능하게 만드는 것이 더 가치있다.
- 초밥 원리(sushi principle) : 원시 데이터를 덤프하는 것 만으로도 여러 변환이 가능하다. “원시 데이터가 더 좋다.”
- 데이터 생산자에게 데이터셋 표준 형식을 강제하는 대신, 소비자에게 데이터 해석의 부담을 이전시킨다. → 스키마 온 리드(schema-on-read) 접근법
처리 모델의 다양성
- MPP 데이터베이스는 SQL과 같은 설계된 질의 유형으로 좋은 성능을 얻을 수 있으나, SQL 질의만으로 모든 종류의 처리를 표현할 수 없다.
- 머신러닝, 추천 시스템, 랭킹 모델을 탑재한 전문 검색 색인 구축 등
- 이러한 한계를 넘으려면 잔순한 질의 작성이 아닌 코드 작성이 반드시 필요하다.
- 맵리듀스와 HDFS를 사용하면 그 위에서 SQL 질의 실행 엔진도 사용할 수 있다. → Apache Hive
- 물론 SQL 질의로 표현하기 어려운 다양한 일괄 처리 코드도 직접 작성할 수 있다.
- 결정적으로 이런 다양한 처리 모델은 모두 단일 공유 클러스터 장비에서 실행된다.
- 여러 서비스들이 HDFS의 동일한 파일들에 접근 가능하므로 특정 처리를 위해 데이터를 다른 시스템으로 보낼 필요가 없다.
- 예를 들어, 임의 접근 가능한 OLTP DB인 HBase, MPP 스타일의 분석 데이터베이스인 Impala 모두 HDFS를 저장소로 사용한다.
빈번하게 발생하는 결함을 줄이는 설계
- 맵리듀스와 MPP 데이터베이스는 아래 두 설계 방식에서 큰 차이점이 있다.
- 결함을 다루는 방식
- 메모리, 디스크를 사용하는 방식
- 맵리듀스는 실패를 잘 견딜 수 있게 설계되었다.
- 개별 태스크 수준에서 작업을 재실행하기 때문에 전체 작업은 영향을 받지 않는다.
- 내결함성 확보와 큰 데이터셋 이슈로 데이터를 되도록 디스크에 기록한다.
- 많은 데이터를 처리하고 오랜 시간 수행하는 작업일 수록 태스크가 실패할 가능성이 높으므로, 맵리듀스는 대용량 작업에 적합하다.
- 맵리듀스가 잦은 실패에도 견딜 수 있도록 설계된 이유
- 하드웨어를 신뢰할 수 없기 때문이 아니라, 프로세스를 임의로 종료할 수 있으면 연산 클러스터에서 자원 활용도를 높일 수 있기 때문이다.
- 구글에서 개발할 때 장시간 수행되는 맵리듀스 작업은 언제든 (효율적인 리소스 사용을 위해) 선점될 수 있도록 염두에 뒀기 때문
- 실제로 하둡 YARN의 CapacityScheduler는 다른 큐 간의 리소스 선점을 지원한다.
- (단 YARN, Mesos, K8S를 작성할 당시엔 우선순위 선점 방식은 지원하지 않았다고 함)
맵리듀스를 넘어
- 맵리듀스는 분산 파일 시스템 상에서 상당히 단순 명료하게 추상화된 모델로, 무엇을 하고 있는지 이해하기 쉽다.
- 맵리듀스를 직접 사용하는건 어렵기 때문에 맵리듀스 상에서 추상화된 다양한 고수준 프로그래밍 모델이 개발됐다. → 피그, 하이브, 캐스캐이딩, 크런치
- 맵리듀스 동작 원리를 이해하면 배우기 쉬고, 일괄 처리 태스크를 구현하기 편해진다.
- 그러나 맵리듀스 모델 자체에도 문제가 있으며 이는 추상화 단계를 올린다고 해결되지 않는다. → 일괄 처리 방법의 대안을 알아보자.
중간 상태 구체화
- 중간 상태(Intermediate state) :
- 맵리듀스에서 보통 한 작업의 출력은 다른 작업의 입력으로만 사용된다.
- 즉 분산 파일 시스템 상의 파일들은 단순히 데이터를 다른 작업으로 옮기는 수단에 불과하며 이를 중간 상태라 한다.
- 구체화(materialization) :
- 중간 상태를 파일로 기록하는 과정
- 구체화는 요청이 왔을 때 계산을 시작하는 것이 아니라, 미리 특정 연산 결과를 만들어 둔다는 의미다. (ad-hoc 쿼리와 반대)
- 맵리듀스의 중간 상태 구체화는 유닉스 파이프에 비해 여러 단점이 있다.
- 선행 작업이 완료되어야만 작업이 가능하다. 즉, 모든 선행 작업이 종료될 때까지 전체 워크플로 수행시간이 지연되므로 느리다.
- 각 맵리듀스 작업이 중복으로 매퍼를 갖는다. 즉, 앞 작업의 리듀서의 일부가 될 수 있는 매퍼를 굳이 끼워넣어야 한다.
- 중간 상태 파일도 분산 파일 시스템에 저장하므로 복제가 된다. 임시 데이터에 대한 과잉조치다.
데이터플로 엔진
- 이러한 맵리듀스의 문제를 해결하기 위해 분산 일괄 처리 연산을 수행하는 새로운 엔진들이 개발되었다.
- Spark, Tez, Flink 등
- 이들의 공통점은 전체 워크플로를 독립된 하위 작업으로 나누지 않고, 작업 하나로서 다룬다는 점이다.
- 데이터플로 엔진(dataflow engine) :
- 여러 처리 단계를 통해 데이터 흐름을 명시적으로 모델링하는 시스템
- 단일 스레드에서 UDF를 반복 호출해 한 번에 레코드 한 개씩 처리
- 입력을 파티셔닝해 병렬화
- 한 함수의 출력을 다른 함수의 입력으로 사용
- 맵과 리듀스를 번갈아 수행해야하는 맵리듀스와 달리, 연산자(operator) 라는 함수들을 조합하는 유연한 방식을 사용
- 맵리듀스 모델과 비교했을 때 장점
- 값비싼 작업(정렬 등)은 실제로 필요할 때만 수행한다.
- 필요없는 맵 태스크는 없다.
- 워크플로에 모든 조인, 데이터 의존 관계가 명시되므로 지역성 최적화가 가능하다.
- 연산자 간 중간 상태를 메모리나 로컬 디스크에 기록하여 HDFS에 기록할 때보다 I/O가 훨씬 적게 든다. (맵리듀스는 무조건 HDFS에 중간 상태 기록함)
- 연산자들은 입력이 준비되는 즉시 실행을 시작할 수 있다. (앞 태스크가 완료되길 기다릴 필요 없음)
- 워크플로가 하나의 작업으로 다뤄지므로, 새로운 연산자를 실행할 때마다 새로운 JVM을 구동할 필요 없이 이미 존재하는 JVM을 사용한다.
내결함성
- 맵리듀스는 중간 상태를 HDFS에 구체화하는 것으로 내구성을 확보한다.
- 반면 Spark, Flink, Tez는 중간 상태를 HDFS에 쓰지 않는다.
- 따라서 중간 상태가 유실되면 유효한 데이터로부터 계산을 다시 수행해 복구한다.
- Spark는 데이터의 조상을 추적하기 위해 RDD(resilient distributed dataset) 추상화를 사용한다.
- Flink는 연산자 상태를 체크포인트로 남겨 실패한 연산자 수행을 재개할 수 있다.
- 데이터를 재연산할 때 중요한 점은 해당 연산이 결정적인지 여부이다.
- 동일한 입력 데이터가 주어지면 항상 같은 출력을 생산하는가?
- 전파되는 결함을 피하려면 연산자를 결정적으로 만들어야 함
구체화에 대한 논의
- 대부분의 경우, 출력 또한 입력과 마찬가지로 HDFS에 기록하게 된다.
- 입력은 불변히고 최종 출력은 완전히 교체하는 방식인 점에서 맵리듀스와 비슷하다.
- 맵리듀스보다 개선된 점은, 모든 중간 상태를 HDFS에 기록하는 수고를 덜어준다는 것이다.
그래프와 반복 처리
- 그래프형 데이터 모델에 대한 내용은 불필요하므로 전부 skip
고수준 API와 언어
- 맵리듀스 작업을 직접 작성하는 일은 상당히 어렵기 때문에 하이브 등 고수준 언어와 API가 인기를 끌었다.
- 이런 데이터플로 API는 일반적으로 관계형 스타일의 빌딩 블록을 사용해 연산을 표현한다.
- 특정 필드의 값을 기준으로 데이터셋을 조인
- 키로 튜플을 그룹화하고 특정 조건으로 필터링하거나 튜플을 카운트 등 집계
- 고수준 인터페이스의 장점
- 코드를 적게 사용한다.
- 대화식 사용도 지원 → 셸에서 코드 동작을 바로 확인할 수 있다.
- 사용자가 시스템을 생산성 높게 사용할 수 있다.
선언형 질의 언어로 전환
- 코드를 작성하는 방식에 비해 관계형 연산자로 조인을 나타내면 최적화가 가능하다는 장점이 있다.
- Hive, Spark, Flink는 비용 기반의 질의 최적화기를 내장하고 있다.
- 질의 최적화기는 중간 상태를 최소화하기 위해 조인 순서를 바꾸기도 한다.
- 위와 같은 최적화가 이뤄지려면 선언적인 방법으로 조인을 지정해야 한다.
- 고수준 API에서 선언적 방식과 질의 최적화기를 가진다면 MPP 데이터베이스와 견줄만한 성능을 낼 수 있게 된다.
- 임의의 코드를 실행하고
- 임의 형식의 데이터를 읽을 수 있는 확장성을 지니고
- 일괄 처리 프레임워크의 장점인 유연성은 그대로 유지할 수 있다.
정리
- 분산 일괄 처리 프레임워크가 해결해야 할 두 가지 중요한 문제
- 파티셔닝
- 맵리듀스의 매퍼는 입력 파일 블록에 따라 파티셔닝되고 리듀서 파티션 개수는 사용자가 설정할 수 있다.
- 이 파티셔닝의 목적은 같은 키를 갖는 모든 레코드를 같은 장소(리듀서)로 모으는 것이다.
- 내결함성
- 맵리듀스는 개별 태스크가 실패하더라도 전체 작업을 재수행하지 않도록 중간 상태를 빈번히 디스크에 기록한다. (그래서 느리다)
- 반면 데이터플로 엔진은 메모리에 중간 상태를 유지한다. 내결함성은 결정적인 구조와 RDD, 체크포인트 등으로 확보한다.
- 맵리듀스에서 사용하는 조인 알고리즘
- 정렬 병합 조인 (리듀스 사이드 조인)
- 기본적인 조인으로, 같은 키를 갖는 모든 레코드는 하나의 리듀서로 모인다.
- 이 과정을 위해 매퍼가 조인 키를 추출하고 레코드는 파티셔닝, 정렬, 병합 과정을 거친다.
- 브로드캐스트 해시 조인 (맵 사이드 조인)
- 두 조인 데이터셋 중 상대적으로 작은 것을 파티셔닝하지 않고 해시 테이블에 통째로 적재하여 사용하는 방식
- 조인 연산 시작 전에 각 매퍼가 작은 데이터셋을 브로드캐스트받고 자신의 해시 테이블에 올려놓고 조인에 사용한다.
- 파티션 해시 조인 (맵 사이드 조인)
- 조인 입력 두 개를 같은 방식으로 파티셔닝(키, 해시 함수, 파티션 수가 모두 동일하게 사용)
- 해시 테이블 방식을 각 파티션 별로 독립적으로 사용할 수 있다.