[데이터중심 어플리케이션 설계] 10장 정리
현대 시스템은 크게 세 가지 유형으로 구분된다
- 서비스(온라인 시스템) : 클라이언트로부터 요청이나 지시가 올 때까지 기다림. 요청 하나가 들어오면 가능한 빨리 요청을 처리해 응답을 내보냄
- 일괄 처리 시스템(오프라인 시스템) : 매우 큰 입력데이터를 받아 데이터를 처리하는 작업을 수행해 결과를 생산. 수 분에서 때론 수 일이 걸리기 때문에 사용자는 대기하지 않음
- 스트림 처리 시스템(준실시간 시스템) : 온라인/일괄 처리 사이 어딘가. 입력 이벤트가 발생한 직후 바로 작동
일괄 처리는 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션을 구축하는 데 매우 중요한 구성요소다.
여러 일괄 처리 알고리즘과 프레임워크를 알아보기 전에 표준 유닉스 도구를 사용한 데이터 처리를 알아보자.
유닉스 도구로 일괄 처리하기
다음과 같은 형식의 액세스 로그가 있다고 가정하자
$remote_addr - $remote_user [$time_local] "$request"
$status $body_bytes_send "$http_referer" "$http_user_agent"
위 형식에서 가장 인기가 높은 페이지 5개를 뽑는다고 하면 다음과 같은 스크립트를 짤수 있다.
cat /var/log/nginx/access.log |
awk '{print $7}' | 줄마다 공백으로 분리해 7번째 필드 출력
sort | 알파벳 순 정렬
uniq -c | 중복 횟수 출력하며 중복 제거
sort -r -n | 내림차순 정렬
head -n 5
이 스크립트에서 유닉스의 철학을 엿볼수 있다.
1978년 기술된 유닉스 철학은 아래와 같다.
- 각 프로그램이 한 가지 일만 하도록 작성하라. 새 작업을 하려면 기존 프로그램을 고쳐 새로운 "기능"을 추가해 프로그램을 복잡하게 만들기보다는 새로운 프로그램을 작성하라
- 모든 프로그램의 출력은 아직 알려지지 않은 다른 프로그램의 입력으로 쓰일 수 있다고 생각하라. 불필요한 정보로 출력이 너저분해서는 안 된다. 입력 형식으로 엄격하게 열을 맞춘다거나 이진 형태를 사용하지 마라. 대화형 입력을 고집하지 마라
- 소프트웨어를 빠르게 써볼 수 있게 설계하고 구축하라. 심지어 운영체제도 마찬가지다. 수 주 안에 끝내는 것이 이상적이다. 거슬리는 부분은 과감히 버리고 새로 구축하라
- 프로그래밍 작업을 줄이려면 미숙한 도움보단 도구를 사용하라. 도구를 빌드하기 위해 한참 둘러가야 하고 사용 후 바로 버린다고 할지라도 도구를 써라
이 철학은 오늘날의 애자일과 매우 흡사하다.
유닉스 셸이 강력한 데이터 처리 작업을 쉽게 할수 있는 이유는 무엇일까?
- 동일 인터페이스
프로그램의 출력을 다른 프로그램의 입력으로 쓰고자 한다면 이 프로그램들은 같은 데이터 형식을 사용해야 한다.
유닉스에서의 인터페이스는 파일 디스크립터이다.
파일은 단지 순서대로 정렬된 바이트의 연속이어서, 실제 파일, 프로세스간의 통신 채널(소켓, 표준입/출력), 장치 드라이버 등 여러 가지를 표현할 수 있다. - 로직과 연결의 분리
프로그램은 입력이 어디서부터 들어오는지, 출력이 어디로 나가는지 신경쓸 필요가 없다.
느슨한 결합, 지연 바인딩, 제어 반전의 개념이다. - 투명성과 실험
진행상황을 파악하기가 상당히 쉽다.
유닉스 명령에 들어가는 입력 파일은 일반적으로 불변이다. 다양한 명령을 실행하더라도 입력 파일에는 변화가 없다.
어느 시점이든 파이프라인을 중단하고 출력을 확인할 수 있다.
특정 파이프라인 단계 출력을 파일에 쓰고, 그 파일을 다음 단계의 입력으로 사용할 수 있다.
하지만 유닉스 도구에도 단점은 존재한다.
- 여러 입력을 받거나 여러 출력이 필요한 경우에 까다롭다
- 출력을 네트워크와 연결하지는 못한다
- 단일 장비에서만 실행 가능하다.
맵리듀스와 분산 파일 시스템
맵리듀스는 유닉스 도구와 비슷한 면이 있지만 수천 대의 장비로 분산해서 실행 가능하다.
유닉스 도구와 마찬가지로 입력을 수정하지 않기 때문에 출력을 생산하는 것 외에 다른 부수 효과는 없다.
맵리듀스 작업은 분산 파일 시스템상의 파일을 입/출력으로 사용한다.
하둡 맵리듀스 구현에서 이 파일 시스템은 HDFS (Hadoop Distributed File System) 이라 한다.
맵리듀스는 HDFS 같은 분산 파일 시스템 위에서 대용량 데이터셋을 처리하는 코드를 작성하는 프레임워크다.
맵리듀스 작업 실행하기
맵리듀스 데이터 처리 패턴은 위의 로그 분석 예제와 비슷하다
- 입력 파일을 읽고 레코드로 쪼갠다
- 각 입력 레코드마다 매퍼 함수를 호출해 키-값을 추출한다
- 키를 기준으로 키-값 쌍을 정렬한다
- 정렬된 키-값 쌍 전체를 대상으로 리듀스 함수를 호출한다.
2단계(맵)과 4단계(리듀스)는 사용자가 작성한 데이터 처리 코드다.
- 매퍼 : 모든 입력 레코드마다 한번 호출. 입력 레코드로부터 키-값을 추출하는 작업
- 리듀서 : 매퍼가 생산한 키-값 쌍을 받아 같은 키를 가진 레코드를 모음. 출력 레코르를 생산
맵리듀스 분산 실행
유닉스 명령어 파이프라인과 가장 큰 차이점은 병렬 수행 코드를 직접 작성하지 않아도 여러 장비에서 동시 처리가 가능하다.
매퍼/리듀서는 한 번에 하나의 레코드만 처리하고 입력이 어디서 오는지/출력이 어디로 가는지 신경 쓰지 않는다.
맵리듀스 작업의 병렬 실행은 파티셔닝을 기반으로 한다.
입력 파일을 읽기 시작하면 입력 파일에서 한 번에 레코드 하나씩 읽어 매퍼 콜백 함수로 전달하고, 출력은 키-값 쌍으로 구성된다.
리듀서 측 연산도 파티셔닝 된다. 맵 태스크 수는 입력 파일의 블록 수로 결정되지만 리듀스 태스크 수는 사용자가 설정한다.
같은 키를 가진 모든 키-값 쌍은 같은 리듀서에서 처리하는 것을 보장하는데, 특정 키-값 쌍이 어느 리듀스에서 수행될지 결정하기 위해 키 해시값을 사용한다.
맵리듀스 작업들을 연결해 워크플로 로 구성하는 방식이 일반적이다.
리듀스 사이드 조인과 그룹화
데이터셋에서 한 레코드가 다른 레코드와 연관이 있는 것은 일반적이다.
맵리듀스에서는 일반적인 색인 개념이 없기 때문에, 이런 데이터셋이 있다면 입력 파일 전체 내용을 읽는다.
분석 질의에서는 대량 데이터를 대상으로 집계 연산을 하는 것이 일반적이기 때문에 전체 데이터를 스캔하는건 합리적이다.
웹사이트 이벤트 로그 예제로 살펴보자
이 태스크는 사용자 활동과 프로필 정보를 연관시켜야 한다.
사용자 데이터베이스 사본을 가져와 활동 이벤트 로그가 저장된 분산 파일 시스템에 넣으면,
사용자 데이터베이스와 활동 레코드가 같은 HDFS 상에 존재하기 때문에 효율적으로 처리가 가능하다.
정렬 병합 조인
키로 매퍼의 출력을 파티셔닝해 키-값 쌍으로 정렬한다면 같은 사용자의 활동 이벤트와 사용자 레코드는 리듀서의 입력으로 인접해서 들어간다.
리듀서는 특정 사용자 ID의 모든 레코드를 한번에 처리하므로 한번에 사용자 한명의 레코드만 메모리에 유지하면 된다.
이 알고리즘을 정렬 병합 조인이라 한다.
쏠림 다루기
키 하나에 너무 많은 데이터가 연관되면 "같은 키를 가지는 모든 레코드를 같은 장소로 모으는" 패턴은 제대로 동작하지 않는다.
불균형한 데이터 레코드를 린치핀 객체 또는 핫키라 한다.
이런 데이터를 리듀서 한개에서 모은다면 상당한 쏠림 현상이 생기고 이를 핫스팟이라 한다.
핫스팟을 완화할 몇가지 알고리즘이 있다.
쏠린 조인 (skewed join)
어떤 키가 핫키인지 결정하기 위해 샘플링 작업을 수행한다.
실제 조인할때 매퍼는 핫 키를 가진 레코드는 여러 리듀서 중 임의로 선택한 하나로 보낸다.
핫키로 조인할 다른 입력은 전송된 모든 리듀서에 복제한다.공유 조인 (shared join)
위와 비슷하지만 샘플링 작업 대신 핫키를 명시적으로 지정한다.
지금까지 설명한 조인 알고리즘은 실제 조인을 리듀서에서 수행하기 때문에 리듀스 사이드 조인 이라 한다.
장점은 입력 데이터에 대한 특정 가정이 필요 없다.
하지만 정렬 후 리듀서로 복사한뒤 리듀서 입력을 병합하는 모든 과정에 드는 비용이 크다.
맵 사이드 조인
입력 데이터에 대해 특정 가능이 가능하다면 맵사이드 조인을 이용해 조인을 더 빠르게 할수 있다.
브로드캐스트 해시 조인
작은 데이터셋과 매우 큰 데이터셋을 조인하는 경우에 간단하게 적용할수 있다.
이때 작은 데이터셋은 전체를 각 매퍼 메모리에 적재 가능할 정도로 충분히 작아야 한다.
매퍼가 시작할때 분산 파일 시스템에서 작은 데이터셋을 읽어 인메모리 해시 테이블에 넣는다.
이후 매퍼는 모든 데이터를 스캔할수 있고 해시 테이블에서 간단하게 조회할 수 있다.
이런 방식을 브로드캐스트 해시 조인이라 한다
파티션 해시 조인
맵 사이드 조인 입력을 파티셔닝 한다면 각 파티션에 독립적으로 적용할 수 있다.
파티셔닝이 잘 작동했다면 조인할 레코드 모두가 같은 번호의 파티션에 위치한다.
각 매퍼는 각 입력 데이터셋 중 파티션 한 개만 읽어도 충분하다.