들어가기 전에
지금까지는 요청과 응답, 질의와 결과에 관한 내용을 주로 다뤘다.
온라인 시스템에 익숙해져있으면, 이러한 것들이 전부라고 착각하기 쉽지만 사실 그렇지 않다.
시스템은 세가지 유형으로 나뉜다.
서비스(온라인 시스템)
- 서비스는 클라이언트로부터 요청이나 지시가 올 때 까지 기다린다.
- http/rest 기반 api덕분에 가장 익숙한 시스템이다.
- 응답시간과 가용성이 가장 주요한 성능 지표로 꼽힌다.
일괄 처리 시스템 (오프라인 시스템)
- 매우 큰 입력데이터를 받아 데이터를 처리하는 작업을 수행하고 결과 데이터를 생산한다.
- 사용자가 대기를 하지는 않으며 그래서 시간당 처리량 같은 것들이 성능 지표로 꼽힌다.
스트림 처리 시스템
- 준 실시간 처리라고 불린다.
- 서비스와 다르게 요청에 응답하지 않으며 입력 데이터를 소비하고 결과 데이터를 생산한다.
- 반대로 배치와는 다르게 특정한 시간이나 데이터 양까지 기다리지 않으며 처리를 시작한다.
결론은 지금까지 알아본 내용은 서비스 위주였고, 이번 장에서는 일괄 처리 시스템에 대해서 알아보고, 다음 장에서는 스트림 처리 시스템에 대해서 알아본다.
유닉스 도구로 일괄 처리하기
cat /var/log/nginx/access.log
awk '{print &7}'
sort
uniq -c
sort -r -n
head -n 5
로그를 읽고,주소를 뽑고, 주소를 기준으로 정렬하고, 중복 제거하면서 카운트를 붙이고, 카운트를 기준으로 정렬하고, 위에서 다섯개를 뽑기!
fn main() -> io::Result<()> {
let log_file_path = "/var/log/nginx/access.log";
let file = File::open(log_file_path)?;
let reader = io::BufReader::new(file);
let mut field_counts: HashMap<String, usize> = HashMap::new();
for line in reader.lines() {
let line = line?;
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 7 {
let field = fields[6].to_string();
*field_counts.entry(field).or_insert(0) += 1;
}
}
let mut sorted_counts: Vec<_> = field_counts.into_iter().collect();
sorted_counts.sort_by(|a, b| b.1.cmp(&a.1));
for (field, count) in sorted_counts.iter().take(5) {
println!("{}: {}", field, count);
}
Ok(())
}
동일한 작업을 하지만, 인메모리 해쉬테이블을 유지하는 방법 2
물론 두번째 방식이 더 익숙하기는 하고, 실제로는 대부분 잘 동작할 것이다.
다만 만약 처리해야할 데이터의 양이 메모리보다 크다면 문제가 생긴다.
두번째 방법은 처리가 불가능하고(추가적인 보완이 필요 하거나 의미없음), 첫번째 방법은 정렬을 기반으로 하기에 디스크를 효율적으로 사용할 수 있다. 청크를 메모리에서 정렬하고 청크를 세그먼트 파일로 디스크에 저장, 그 다음 각각 정렬된 세그먼트 파일 여러개를 한개의 큰 정렬 파일로 병합한다.
실제로 GNU Coreutils에 포함된 sort는 메모리보다 큰 데이터셋을 자동으로 디스크로 보내고 자동으로 여러 코어에서 병렬로 정렬한다고 한다.
디스크를 읽어들이는 병목을 감수하면 아주 잘 동작한다.
가장 중요한 점은 이게 우연이 아니고, 유닉스 철학을 따르기 때문이라고 책에서는 이야기 한다.
유닉스 철학
다른 방법으로 데이터 처리가 필요할 때 정원 호스와 같이 여러 다른 프로그램을 연결하는 방법이 필요하다. 이것은 I/O방식 이기도 하다.
- 각 프로그램이 한 가지 일만 하도록 작성하라. 새 작업을 하려면 기존 프로그램을 고쳐 새로운 기능을 추가해 프로그램을 복잡하게 만들기보다는 새로운 프로그램을 작성하라.
- 모든 프로그램의 출력은 아직 알려지지않은 다른 프로그램의 입력으로 쓰일 수 있다고 생각하라.
- 소프트웨어를 빠르게 써볼 수 있게 설계하고 구축하라. 심지어 운영체제도 마찬가지다. 수 주 안에 끝내는 것이 이상적이다.
- 프로그래밍 작업을 줄이려면 미숙한 도움보다는 도구를 사용하라.
애자일과 데브옵스와 같이 몇십년이 지나도 아이디어가 전혀 바뀌지 않은 탁월한 아이디어이다.
그리고 책에서는 pipe
즉 모든 프로그램의 출력은 아직 알려지지 않은 다른 프로그램의 입력으로 쓰일 수 있다고 생각하라.
에 집중한다.
동일 인터페이스
어떤 프로그램의 출력을 다른 프로그램의 입력으로 쓰고자 한다면 이들 프로그램은 같은 데이터 형식을 사용해야 한다. 즉 호환 가능한 인터페이스를 써야 한다.
유닉스에서의 인터페이스는 파일(파일 디스크럽터)이다. 파일은 단지 순서대로 정렬된 바이트의 연속이다. 그리고 이렇게 단순한 인터페이스를 공유하기에 소켓과 표준 입출력, 드라이버 소켓에서도 다른 여러가지 것들을 표현 할 수 있다.
이처럼 동일 인터페이스를 사용해서 상호 운용하는건 생각하는 것 이상으로 어렵다.
그리고 이러한 프로그램들은 최근에도 많이 없으며 데이터베이스 역시 심지어는 같은 모델을 씀에도 데이터를 한쪽에서 다른 쪽으로 옮기는 것이 쉽지 않다.
유닉스와 같은 통합이 부족했기 때문이다.
로직과 연결의 분리
유닉스는 표준 입출력을 사용한다. stdin
, stdout
그리고 파이프는 다른 프로세스의 stdout
을 stdin
으로 연결한다.
이 때 중간데이터를 디스크에 쓰지 않고 작은 인메모리 버퍼를 사용해 프로세스간 데이터를 전송한다.
위의 것들이 지켜지면 프로그램은 어디서 입력을 받고 내 출력이 어디로 가는지 알 필요도, 알 방법도 없다. (loose coupling, late binding, inversion of control)
투명성과 실험
또한 유닉스 도구가 성공적인 이유는 진행 사항을 파악하기가 쉽다는 점이 있다.
입력이 불변으로 처리된다 -> 여러번 수행해도 ok!
어느 시점이든 파이프라인을 중단하고 출력을 파이프를 통해 원하는 출력이 나오는지 확인 할 수 있다. -> 디버깅 용이
특정 파이프라인 단계의 출력을 파일(디스크)에 쓰고 다음 단계의 입력으로 쓸 수 있다. -> 재실행에 용이
문제는 유닉스도구 는 단일 장비에서만 실행이 가능하다는 것이고 책은 그래서 하둡과 같은 도구가 필요한 이유라고 이야기 한다.
맵리듀스와 분산 파일 시스템
맵리듀스와 유닉스 도구의 공통점과 차이점
공통점
- 하나 이상의 입력을 받아 하나 이상의 출력을 만든다.
- 입력을 수정하지 않는다.
차이점
- 맵리듀스는 수천대의 장비로 분산해서 실행이 가능하다.
- 분산 파일 시스템 상의 파일을 입력과 출력으로 사용한다. -> 잘 모르겠음
무튼 주요한 아이디어는 유닉스도구와 맵리듀스가 동일하고, 분산노드에서 병렬 처리가 가능하다는 이야기 인 것 같다.
그리고 분산 처리를 보완한 내용은 아래와 같다.
- 일단 데몬 프로세스를 통해 노드들끼리 공유가 가능하도록 해두었다.
- 네임노드라고 부르는 중앙 서버는 특정 파일 블록이 어디에 저장됐는지 추적한다.
맵리듀스 작업 실행하기
이것도 순서로 정리 할 수 밖에 없는 것 같다.
- 레코드를 쪼갠다.
- 매퍼함수를 호출한다.
- 정렬한다.
- 리듀스함수를 호출한다.
즉 로그예시에서는 ‘\n’ 으로 로그함수를 쪼개고 , 매퍼함수로 url을 키 값 쌍으로 정리하고 , 정렬하고 , 리듀스 함수로 레코드 수를 읽어간다.
추가적인 내용을 요약하면
- 맵리듀스는 데이터를 파티셔닝하여 병렬로 처리하는데, 병렬처리를 위한 추가적인 로직은 필요없다.
- 입력 데이터는 맵 함수에서 키-값 쌍으로 변환된다.
- 같은 키를 가진 데이터는 항상 같은 리듀서에서 처리된다(해쉬 사용).
- 맵 태스크 수는 입력 파일 블록 수에 따라 결정되고, 리듀스 태스크 수는 사용자가 설정한다.
- 데이터베이스마다 다르기는 한데, 매퍼와 리듀서는 java, js등 어플리케이션 코드로 되어있는 경우가 많다고 한다.
맵리듀스 워크플로
맵리듀스 하나로 해결 할 수 있는 문제는 엄청 제한적이라서 여러 맵리듀스 작업을 연결해 워크플로우를 구성하기도 한다.
다만 유닉스의 파이프라인과는 약간 다른게 실제 메모리 버퍼등을 이용하는것과는 다르게 중간출력파일이 다음 워크플로우의 입력이 되는 형식이라고 한다.
리듀스 사이드 조인과 그룹화
일괄 처리 맥락에서 조인은 데이터셋 내의 모든 연관관계를 다룬다.
그리고 인덱스를 사용하지않고 풀테이블 스캔을 하는 식이다.
당연히 병렬처리로 보완하기도 하고, 애초에 일부 데이터와 그에 대한 처리가 필요하다면 일괄 처리가 아니라 인덱스를 타는 로직을 태우는게 현명하기 때문에 애초에 고려대상이 아니라고 이야기 하는 것 같다.
결론적으로 일괄 처리중에는 실제 조인을 때리는데, 그 과정을 위해 매 데이터마다 필요한 데이터를 얻기 위해 db를 네트워크로 호출은 아니고 사본을 가져와 진행한다고 한다.
(작성중)