이번 장에서는 일괄처리 알고리즘인 맵리듀스를 알아보고 다른 일괄 처리 알고리즘과 프레임워크도 살펴볼 것이다.
단순 로그 분석
유닉스 셸(웹사이트에서 가장 인기 높은 페이지 5개 출력)
연쇄 명령 대 맞춤형 프로그램
유닉스 연쇄 명령 대신 같은 작업을 하는 간단한 프로그램을 작성할 수도 있다.
-> 유닉스 연쇄 파이프보다 간결하지는 않지만 더 읽기 쉬우며 뭘 선택하는지는 취향의 문제이다.
정렬 대 인메모리 집계
허용 메머리보다 작업 세트가 크다면 정렬 접근법을 사용하여 디스크를 효율적으로 사용하는 것이 좋다.
유닉스 철학
연쇄 명령을 사용해 쉽게 로그파일을 분석할 수 있었던 것은 유닉스의 핵심 설계 아이디어 중 하나였다.
유닉스에서 빌려올 수 있는 아이디어에는 무엇이 더 있을까
유닉스 파이프: "다른 방법으로 데이터 처리가 필요할 때 정원 호스와 같이 여러 다른 프로그램을 연결하는 방법이 필요하다. 이것은 I/O 방식이기도 하다" -> 배관 공사와 비슷한 점에 착안해 파이프로 프로그램을 연결하는 아이디어이며 이것이 지금은 유닉스 철학의 일부가 됐다.
유닉스 철학
각 프로그램이 한 가지 일만 하도록 작성하라. 새 작업을 하려면 기존 프로그램을 고쳐 새로은 "기능"을 추가해 프로그램을 복잡하게 만들기보다는 새로운 프로그램을 작성하라.
모든 프로그램의 출력은 아직 알려지지 않은 다른 프로그램의 입력으로 쓰일 수 있다고 생각하라. 불필요한 정보로 출력이 너저분해서는 안된다. 입력 형식으로 엄격하게 열을 맞춘다거나 이진형태를 사용하지 마라. 대화형 입력을 고집하지 마라.
소프트웨어를 빠르게 써볼 수 있게 설계하고 구축하라. 심지어 운영체제도 마찬가지다. 수 주 안에 끝내는 것이 이상적이다. 거슬리는 부분은 과감히 버리고 새로 구축하라.
프로그래밍 작업을 줄이려면 미숙한 도움보단 도구를 사용하라. 도구를 빌드하기 위해 한참 둘러가야 하고 게다가 사용 후 바로 버린다고 할지라도 도구를 써라.
동일 인터페이스
특정 프로그램이 다른 어떤 프로그램과도 연결 가능하려면 프로그램 모두가 같은 입출력 인터페이스를 사용해야 한다는 의미.
로직과 연결의 분리
유닉스 도구의 다른 특징으로 표준 입력과 표준 출력을 사용한다는 점이 있다.
입력은 키보드, 출력은 화면으로 설정되어 있다.
파이프는 한 프로세스의 출력을 다른 프로세스의 입력과 연결한다. 이 때 중간 데이터를 디스크에 쓰지 않고 작은 인메모리 버퍼를 사용해 프로세스 간 데이터를 전송한다.
투명성과 실험
유닉스 도구가 성공적인 이유 중 하나는 진행 사항을 파악하기가 상당히 쉽기 때문이다.
단순하지만 놀라울 정도로 유용하다.
맵리듀스와 분산 파일 시스템
맵리듀스는 유닉스 도구와 마찬가지로 상당히 불친절하고 무차별 대입 방법이지만 대신 엄청나게 효율적인 도구다.
단일 맵리듀스 작업은 하나 이상의 입력을 받아 하나 이상의 출력을 만들어 낸다는 점에서 단일 유닉스 프로세스와 유사하다.
유닉스 도구는 stdin과 stdout을 입력과 출력으로 사용하는데 맵리듀스 작업은 분산 파일 시스템상의 파일을 입력과 출력으로 사용한다.
데이터베이스 소프트웨어나 하드웨어는 쓰기 연산이 실행중일 때를 포함해서 언제라도 실패할 수 있다.
애플리케이션은 연속된 연산이 실행되는 도중도 포함해서 언제라도 죽을 수 있다.
네트워크가 끊기면 애플리케이션과 데이터베이스의 연결이 갑자기 끊기거나 데이터베이스 노드 사이의 통신이 안 될 수 있다.
여러 클라이언트가 동시에 데이터베이스에 쓰기를 실행해서 다른 클라이언트가 쓴 내용을 덮어쓸 수 있다.
클라이언트가 부분적으로만 갱신돼서 비정상적인 데이터를 읽을 수 있다.
클라이언트 사이의 경쟁 조건은 예측하지 못한 버그를 유발할 수 있다.
시스템이 신뢰성을 지니려면 이런 결함을 처리해서 전체 시스템의 치명적 장애로 이어지는 것을 막아야 한다.
트랜잭션은 애플리케이션에서 몇 개의 읽기와 쓰기를 하나의 논리적 단위로 묶는 방법이다. 개념적으로 한 트랜잭션 내의 모든 읽기와 쓰기는 한 연산으로 실행되며, 전체가 성공(커밋)하거나 실패(롤백)한다.
트랜잭션 실패시 애플리케이션에서 안전하게 재시도할 수 있다.
트랜잭션은 자연 법칙이 아니며, 프로그래밍 모델을 단순화 하기 위해 만든 것이다.
모든 애플리케이션에서 트랜잭션이 필요하지는 않으며 때로는 트랜잭션적인 보장을 완화하거나 아예 쓰지 않는 게 이득이다.
-> 트랜잭션이 제공하는 안전성 보장에는 어떤 것이 있으며 이와 관련된 비용이 어떻게 되는지 정확히 이해하고 사용하여야 한다.
이번 장에서 배울 점
문제가 생길 수 있는 여러 예를 조사하고 이런 문제를 방지하기 위해 데이터베이스에서 사용하는 알고리즘을 살펴본다.
동시성 제어 분야를 깊게 다룬다.
다양한 종류의 경쟁 조건과 데이터베이스에서 read committed, snapthop isolation, serializability와 같은 격리 수준을 어떻게 구현하는지 설명한다.
단일 노드 데이터베이스와 분산 데이터베이스에 모두 적용되며 분산 시스템에서만 생기는 특정 난제는 8장에서 다룬다.
ACID의 의미
1983년 데이터베이스에서 내결함성 메커니즘을 나타내는 정확한 용어를 확립하기 위해 ACID를 만들었다. 하지만 그 뜻은 모호하며, 데이터베이스마다 ACID 세부 구현은 완전히 다른다.
Atimicity - 원자성: 여러 변경을 적용하는 도중 오류가 발생했을 때, 어보트되고 해당 트랜잭션에서 기록한 모든 내용을 취소한다는 보장이다. 안전한 재시도가 가능하다. (Abortability가 더 나은 단어)
Consistency - 일관성: 데이터가 항상 진실인 불변식(invariant)을 만족한다는 보장이다. 데이터의 유효성 및 애플리케이션의 정책적인 측면과 관련 있으며, ACID 중 유일하게 애플리케이션의 책임이다. (예를 들면 회계 프로그램에서 차변과 대변이 항상 같아야 한다는 정책) 데이터베이스는 불변식을 위반하는 잘못된 데이터를 쓰지 못하게 막을 수 없다. 단 Foreign Key, Unique 등은 데이터베이스에서 보장한다.)
Isolation - 격리성: 여러 트랜잭션이 동시에 같은 레코드에 접근하면 동시성 문제(경쟁 조건)이 생긴다. 이를 해결하기 위해 동시에 실행되는 트랜잭션은 서로 격리된다는 보장이 격리성이다. 트랜잭션은 다른 트랜잭션을 방해할 수 없다.
이는 직렬성 격리와 스냅숏 결리로 구현된다.
직렬성 격리(Serializable Isolation) - 동시에 트랜잭션이 실행되었어도, 순차적으로 실행되었을 때의 결과와 동일하도록 보장한다. 성능 손해가 있기 때문에 Real world에서는 거의 사용되지 않는다.
스냅숏 격리(Snapshot Isolation) - MVCC 등으로 구현한다.
Durability - 지속성: 트랜잭션 커밋이 성공했다면 하드웨어 결함이 발생하거나 데이터베이스가 죽어도 데이터가 손실되지 않는다는 보장이다. write-ahead log, 복제, 백업 등을 통해 구현한다. 지속성을 보장하려면 데이터베이스는 트랜잭션 커밋을 보고하기 전에 쓰기나 복제가 완료될 때까지 기다려야 한다. (6장에서 다뤘듯 완벽한 지속성은 존재하지 않는다.)
단일 객체 연산관 다중 객체 연산
- ACID에서 원자성과 격리성은 클라이언트가 한 트랜잭션 내에서 여러 번의 쓰기를 하면 데이터베이스가 어떻게 해야 하는지를 서술한다.
하드웨어 결함 / 소프트웨어 오류(신속한 해결책이 없음) / 인적 오류(롤백 필요, 테스트 추가, 모니터링 대책, 조기 교육(ㅜㅜ))
신뢰성은 단순한 것이 아니다. 일상적인 애플리케이션 조차도 안정적으로 작동해야 한다.
"중요하지 않은" 애플리케이션도 사용자에 대한 책임이 있어야 한다. 사진 애플리케이션에서 사진이 모두 사라진다면 어떻게 될 것인가? 백업을 복원하는 방법을 알고 있을까?
2. 확장성
시스템의 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 수 있는 적절한 방법이 있어야 한다.
시스템이 현재 안정적으로 동작한다고 해서 미래에도 안정적으로 동작한다는 보장은 없다.
시스템은 전에 처리했던 양보다 더 많은 데이터를 처리하고 있을지도 모른다.
확장성은 증가한 부하에 대처하는 시스템 능력을 말하는데, 이때 고려한 질문은,
"시스템이 특정 방식으로 커지면 이에 대처하기 위한 선택은 무엇인가?", "추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?"라는 구체적인 용어이다.
부하 기술하기
부하 매개변수: 웹 서버의 초당 요청 수, 데이터베이스의 읽기 대 쓰기 비율, 활성 사용자 수, 등
트위터 예시:
사용자는 팔로워에게 새로운 메시지를 게시할 수 있다(평균 초당 4.6k 요청, 피크일 때 초당 12k 요청 이상) 사용자는 팔로우한 사람이 작성한 트윗을 볼 수 있다(초당 300k 요청)
성능 기술하기
1. 부하 매개변수를 증가시키고 시스템 자원은 변경하지 않고 유지하면 시스템 성능은 어떻게 영향을 받을까?
2. 부하 매개변수를 증가시켰을 때 성능이 변하지 않고 유지되길 원한다면 자원을 얼마나 많이 늘려야 할까?
시스템 성능 면에서 일괄 처리 시스템은 처리량, 온라인 시스템은 응답시간이 중요한 성능 지표이다.
평균보다 여러가지 상황을 고려한 백분위 응답시간을 사용하는 것이 좋다.
사용자가 보통 얼마나 오랫동안 기다려야 하는지 알고 싶다면 중앙값이 좋은 지표다.(p50)
응답 시간 지연에 따라 매출에 영향을 주기도 하는 시스템이 있다는 것을 기억하자!
시스템의 확장성을 테스트하려고 인위적으로 부하를 생성하는 경우 부하 생성 클라이언트는 응답 시간과 독립적으로 요청을 지속적으로 보내야 한다. 만약 클라이언트가 다음 요청을 보내기 전에 이전 요청이 완료되길 기다리면 테스트에서 인위적으로 대기 시간을 실제보다 더 짧게 만들어 평가를 왜곡한다.
부하 대응 접근 방식
성능 측정을 위한 부하와 지표 매개변수를 확인했다.
부하 매개변수가 어느 정도 증가하더라도 좋은 성능을 유지하려면 어떻게 해야 할까?
흔히 아는 내용: 스케일 업 / 스케일 아웃
적절한 사양의 장비 몇 대가 다량의 낮은 사양 가상 장비보다 훨씬 간단하고 저렴함
일부 시스템은 탄력적이다. 컴퓨팅 자원을 자동으로 추가할 수 있다는 점.
그렇지 않은 시스템은 수동으로 확장해야 한다.(수동으로 확장하는 시스템이 더 간단하고 운영상 예상치 못한 일이 더 적다. -> 이해 안됨!)
다수의 장비에 stateless 서비스를 배포하는 일은 상당히 간단하지만,
단일 노드에 stateful 데이터 시스템을 분산 설치하는 일은 복잡하다.
그래서 대용량 데이터와 트래픽을 다루지 않는 사용 사례에도 분산 데이터 시스템이 향후 기본 아키텍처로 자리 잡을 가능성이 있다.
특정 애플리케이션에 적합한 확장성을 갖춘 아키텍처는 주요 동작이 무엇이고 잘하지 않는 동작이 무엇인지에 대한 가정을 바탕으로 구축하고 이 가정은 곧 부하 매개변수가 된다.
3. 유지보수성
시간이 지남에 따라 여러 다양한 사람들이 시스템 상에서 작업할 것이기 때문에 모든 사용자가 시스템 상에서 생산적으로 작업할 수 있게 해야 한다.
초기 개발 그 이후 지속해서 이어지는 유지보수에 소프트웨어 비용의 대부분이 들어간다.
레거시 시스템 유지보수 작업은 모두가 싫어하는 일이다(나도..)
그래서 이러한 유지보수 중 고통을 최소화하고 레거시 소프트웨어를 직접 만들지 않게끔 애초에 설계를 잘해야 한다.
이러한 원칙으로는
운용성: 운영팀이 시스템을 원활하게 운영할 수 있게 쉽게 만들어라.
단순성: 시스템에서 복잡도를 최대한 제거해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들어라.
발전성: 엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하라. 그래야 요구사항 변경 같은 예기치 않은 사용 사례를 적용하기가 쉽다. 이 속성은 유연성/수정가능성/적응성으로 알려져 있다.
운용성 책임 작업 중 기억에 남는 작업:
시스템 장애, 성능 저하 등의 문제의 원인을 추적
예측 가능한 운영과 안정적인 서비스 환경을 유지하기 위한 절차 정의
개인 인사 이동에도 시스템에 대한 조직의 지식을 보존함
단순성에서 기억에 남는 내용:
변수 명명, 모듈 간 강한 커플링, 임시방편으로 문제를 해결한 사례, 복잡한 의존성 등등이 복잡도의 다양한 증상이다.
복잡도 때문에 시스템 유지보수가 어려울 때 예산과 일정이 초과되며 버그가 생길 위험이 더 크다.
시스템을 단순하게 ㅁ나든느 일이 반드시 기능을 줄인다는 의미는 아니다. 우발적 복잡도를 줄인다는 뜻일 수 있다.
추상화하면 우발적 복잡도를 제거할 수 있다.
발전성: 변화를 쉽게 만들기
시스템의 요구사항이 영원히 바뀌지 않을 가능성은 매우 적다.
(최근 진행한 업무에서 짠 스크립트는 버전 26까지 갔던 거 보면 사실인 듯하다 ㅋㅋ)
조직 프로세스 측면에서 애자일 작업 패턴은 변화에 적응하기 위한 프레임워크를 제공한다.
애자일 커뮤니티에서는 자주 변화하는 환경에서 소프트웨어를 개발할 때 도움이 되는 기술 도구와 패턴을 개발하고 있다.
정리
데이터 중심 애플리케이션을 생각하는 기본적인 방법 몇 가지를 알아봤다.
애플리케이션이 유용하려면 충족되어야 할 요구사항(비기능적 요구사항, 기능적 요구사항) 중 신뢰성/유지보수성/확장성을 살펴봤다.
신뢰성: 결함이 발생해도 시스템이 올바르게 동작하게 만드는 것.
확장성: 부하가 증가해도 좋은 성능을 유지하기 위한 전략.
유지보수성: 본질은 시스템에서 작업한느 엔지니어와 운영팀의 삶을 개선. 좋은 추상화를 통한 복잡도를 줄이기.
안타깝게도 애플리케이션을 신뢰할 수 있고, 확장 가능하며 유지보수하기 쉽게 만들어주는 간단한 해결책은 없다.