오늘날의 대부분의 애플리케이션은 계산중심이 아닌 데이터중심적이다. CPU 성능은 성능을 제한하는 요소가 아니고, 문제는 데이터의양, 복잡도, 변화 속도 이다.
많은 app은 다음의 기능을 필요로 한다.
•
Database: 앱이 나중에 다시 데이터를 찾을 수 있게 데이터를 저장한다.
•
Cache: 읽기 속도 향상을 위해 값비싼 수행 결과를 기억한다.
•
Search Index: 키워드로 검색하거나, 필터링 할 수 있게 제공한다.
•
Stream processing: 비동기 처리를 위해 다른 프로세스로 메세지 보내기
•
Batch processing: 주기적으로 대량의 누적된 데이터를 분석한다.
많은 생각 없이 application 을 만들 수 있는 이유는 데이터 시스템이 충분히 추상화가 되어있기 때문이다.
하지만, 간단한 일이 아니다. application 의 요구사항은 다르고 각 시스템마다 다양한 특성을 가지고 있기 때문에 어떤 도구와 어떤 접근 방식이 수행중인 작업에 적합한지 생각해야 한다.
이 책은 시스템의 원칙과 실용성, 그리고 이를 활용한 데이터 중심 애플리케이션을 개발하는 방법을 담고있다.
신뢰할 수 있고, 확장 가능하며, 유지보수하기 쉽게 만들기 위한 기초적인 노력을 먼저 살펴본다.
데이터 시스템에 대한 생각 (Thinking About Data Systems)
DB, queue, cache 등 매우 다른 범주에 속하는 도구이지만, 각각은 다른 접근 패턴을 가지고 있으면서도 그 경계가 조금씩 허물어지고 있다.
message queue
점점 application 은 단일 도구로 데이터 처리와 저장을 모두 만족시킬 수 없다. 대신에 작업(work)을 하나의 도구에 효율적으로 수행할 수 있는 task 로 쪼개고, 그 각 도구들을 application code 를 이용해 연결한다.
다양한 구성요소를 결합한 데이터 시스템의 예
이때 개발자는 code 를 통해 단순히 도구들을 결합하는 것 뿐만 아니라. 복합 데이터 시스템의 결과를 일관되게 볼 수 있도록 캐시를 무효화 하거나 갱신하는 등 특정 보장 기능을 제공할 수 있다.
따라서 개발자는 application 개발자 뿐만 아니라 data system 의 설계자이기도 하다.
이 책은 데이터 시스템을 설계하는데 다음의 3가지를 중점으로 다룬다.
1.
신뢰성 (Reliability)
•
software 결함, 인적 오류와 같은 역경에도 시스템은 지속적으로 올바르게 동작해야 한다.
2.
확장성 (Scalability)
•
데이터의 양, 트래픽의 양, 복잡도가 증가 하면서 이를 처리할 수 있는 적절한 방법이 있어야 한다.
3.
유지보수성 (Maintainability)
•
시간이 지남에 따라 다양한 사람들이 작업하고, 새로운 사례를 적용하는 운영이 실행될 때 생산적으로 작업할 수 있어야 한다.
신뢰성
소프트웨어에서 신뢰성에 대한 기대치는 다음과 같다.
•
application 은 사용자가 기대한 기능을 수행한다.
•
시스템은 사용자가 범한 실수나 예상치 못한 소프트웨어 사용법을 허용할 수 있다.
•
시스템 성능은 예상된 부하와 데이터 양에서 필수적인 사용 사례를 충분히 만족한다.
•
시스템은 허가되지 않은 접근과 오남용을 방지한다.
결함: 잘못될 수 있는 일(사양에서 벗어난 시스템의 한 구성 요소)
내결함성(fault-tolerant): 결함을 예측하고 대처할 수 있는 시스템
→ 하지만 모든 종류의 결함을 견딜 수는 없다. (블랙홀이 서버를 빨아들인다면?)
장애: 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우
결함으로 인한 장애가 발생하지 않도록 내결함성 구조를 설계하는 것이 가장 좋다.
또한 내결함성 시스템에 경고 없이 개별 프로세스를 무작위로 죽여 고의적으로 결함을 일으키고 시스템을 지속적으로 훈련하고 테스트 하여 결함이 자연적으로 발생했을 때 올바르게 처리할 수 있다는 자신감을 갖는것도 좋은 방법이다. (neflix의 Chaos monkey)
하드웨어 결함
하드디스크가 고장나고, 램에 결함이 생기고, 대규모 정전 사태가 발생하고, 네트워크 케이블을 잘못 뽑는
규모가 큰 data center 에서 일하는 사람은 많은 장비를 다룰 경우 이 같은 일은 늘상 일어난다.
10,000개 디스크로 구성된 cluster 는 평균적으로 하루에 하나의 디스크가 죽는다.
해결방법은 하드웨어 구성 요소에 중복(redundancy)를 추가하는 방법이 일반적이다.
•
디스크는 RAID 구성으로
•
서버는 이중 전원 디바이스
•
hot swap 가능한 CPU
•
데이터 센터에 예비 전원용 디젤 발전기
이러한 접근 방식은 하드웨어 문제로 장애가 발생하는 것을 완전히 막을 수는 없지만 수년간 장비가 죽지 않고 계속 동작하게 만들 수 있다.
최근까지는 단일 장비에 전체 장애가 매우 드물었기 때문에 구성 요소의 중복으로 해결할 수 있었다.
새 장비에 백업을 빠르게 복원할 수 만 있다면, 중단 시간은 치명적이지 않다.
따라서 다중 장비 중복은 고가용성이 절대적으로 필수적인 소수의 애플리케이션에만 필요했으나 AWS 와 같이 가상 장비 instance 가 별도의 경고 없이 사용할 수 없게 되는 상황이 요즘에는 매우 일반적이다.
이러한 이유는 단일 장비의 신뢰성보다 flexibility(유연성)과 elasticity (탄력성)을 우선으로 처리하게끔 설계되었기 때문이다.
따라서 내결함성 기술을 사용하거나 하드웨어의 중복성을 통해 전체 장비의 손실을 견딜 수 있는 시스템으로 점점 옮겨가고 있다. 이러한 발전은 rolling update 까지 가능하도록 영향을 주었다.
소프트웨어 오류 (Software Errors)
시스템 내 체계적 오류(systematic error) 는 예상하기도 어렵고 node 간 상관관계 때문에 하드웨어 결함보다 시스템의 오유를 더욱 많이 유발하는 경향이 있다.
•
잘못된 특정 입력이 있을 때 모든 application 서버 instance 가 죽는 소프트웨어 버그
•
CPU 시간, 메모리, 디스크 공간, 네트워크 대역폭 처럼 공유 자원을 과도하게 사용하는 일부 프로세스
•
한 시스템의 속도가 느려져 반응이 없거나 잘못된 응답을 반환하는 서비스
•
한 구성요소의 결함이 다른 구성 요소의 결함을 야기하고 차례차례 더 많은 결함이 발생하는 연쇄 장애
이러한 장애의 특징은 특정 상황이 발생하기 전까지 오랫동안 나타나지 않는다.
이러한 문제는 특별한 신속한 해결책이 없다.
시스템의 가정과 상호작용에 대해 주의 깊게 생각하기, 빈틈없는 테스트, 프로세스 격리(process isolation), 죽은 프로세스의 재시작 허용, 프로덕션 환경에서 시스템 동작의 측정, 모니터링, 분석하기와 같은 여러 작은 일들이 문제 해결에 도움을 줄 수 있다.
만약 시스템이 무언가 보장하길 기대한다면 수행중에 지속적으로 확인해 차이가 생기면 경고를 발생하는 것을 고려할 수 있다.
인적 오류 (Human Errors)
사람은 최선의 의도를 갖고 있어도 미덥지 않다고 알려져 있다. 대규모 인터넷 서비스에 대한 연구에 의하면 운영자의 설정 오류 중단의 주요 원인이고 H/W (서버나 네트워크) 결함은 중단 원인의 10~25% 라고 한다.
그러면 어떻게 이것을 신뢰성 있게 만들까? 다양한 접근 방식을 결합한다.
•
오류의 가능성을 최소화 하는 방향으로 시스템을 설계하라
◦
잘 설계된 추상화 API, 관리 interface 를 사용하면 "옳은 일"은 쉽고, "잘못된 일"은 막을 수 있다.
◦
하지만 지나치게 제한적이라면, 이것을 피해 더 쉬운 interface 를 사용하게 될 수 있다.
•
사람이 가장 많이 실수하는 부분에서 사람의 실수로 장애가 발생할 수 잇는 부분을 분리한다.
◦
실제 데이터를 사용해 안전하게 살펴보고 실험할 수 있지만, production 에 영향이 없는 sandbox 를 제공하라.
•
단위 테스트부터 전체 시스템 통합 테스트와 수동 테스트까지 모든 수준에서 철저하게 테스트하라.
◦
이 방식은 corner case 를 다루는데 유용하다.
•
장애 발생의 영향을 최소화하기 위해 인적 오류를 빠르고 쉽게 복구할 수 있게 하라.
◦
rollback, roll out(새로운 코드를 서서히 배포한다 - rolling update? 일부 사용자에게만 영향이 미친다.)
•
성능 비표와 오류율 같은 상세하고 명확한 모니터링 대책을 마련하라.
◦
원격 측정(telemetry) 문제가 발생했을 때 지표(metrics)는 문제를 분석하는데 중요하다.
신뢰성은 얼마나 중요할까? (How Important Is Reliability?)
증명되지 않은 시장을 위해 시제품을 개발하는 비용이나 매우 작은 이익률의 서비스를 운영하는 비용을 줄이려 신뢰성을 희생해야 하는 상황이 있다. 하지만, 이 경우에 비용을 줄여야 하는 시점을 알아야 한다.
이것도 어느정도 trade off 를 감안해야 한다.
확장성 (Scalability)
시스템이 현재 제대로 동작한다고 해서 미래에도 제대로 동작할까? 성능 저하를 유발하는 흔한 이유는 부하 증가 이다.
"확장성" 은 증가 부하에 대처하는 시스템 능력을 설명하는데 사용하는 용어이다. 하지만, 시스템에 부여하는 일차원적인 표식이 아니다("X는 확장 가능하다" 같은 말은 의미가 없다.)
시스템이 특정 방식으로 커지면 이에 대처하기 위한 선택은 무엇인가?
추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?
부하 매개변수와 부하를 측정하기 위한 방법을 토대로 접근방식까지 살펴보자
부하 기술하기 (Describing Load) - 부하 매개변수
시스템의 현재 부하를 명료하게 기술해야 한다. - 그래야 부하 질문을 논의 할 수 있다.(부하가 두배로 되면?)
부하 매개변수(load paramter) 라 부르는 몇개의 숫자로 나타낼 수 있다.
웹 서버의 초당 요청 수, Database 의 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자, 캐시 적중률 등이 있다.
평균적인 경우가 중요할 수도 있고, 소수의 극단적인 경우가 병목 현상의 원인일 수도 있다.
fan-out: 트랜잭션 처리 시스템에서 하나의 수신 요청을 처리하는 데 필요한 다른 서비스의 요청 수
트위터의 경우 트윗 작성과 홈 타임라인의 두 가지 핵심 기능을 가지고 있다.
•
작성: 평균 초당 4.6k의 요청, 피그일 때 초당 12k 요청 이상.
•
읽기: 초당 300k 요청
평균적으로 약 65배의 차이가 있다.
1.
트윗 작성은 전역 collection 에 삽입하고 사용자가 자신의 timeline 을 요청하면 팔로우중인 모든 사람들의 트윗을 찾아 시간으로 정렬하여 merge 한다.
SELECT tweets.*, users.* FROM tweets
JOIN users ON tweets.sender_id = users.id
JOIN follows ON follows.followee_id = users.id
WHERE follows.follower_id = current_user;
SQL
복사
트위터 홈 타임라인 구현을 위한 간략한 관계형 스키마
2.
각 수신자용 트윗을 개별 홈 타임라인 캐시로 유지한다. 사용자가 트윗을 작성하면 작성한 사용자를 follow 하는 사람들을 모두 찾아 각자의 home timeline 에 새로운 트윗을 삽입한다.
그러면 home timeline 의 읽기 요청은 결과를 미리 계산했기 대문에 비용이 저렴한다.
2012년 11월 부하 매개변수와 함께 follower 에게 트윗을 정송하기 위한 트위터 데이터 파이프라인
트위터의 첫 버젼은 접근 방식 1번 (RDB join) 을 사용했는데 질의 부하를 버텨내기 위해 고군분투 해야했다.
그결과 2로 전환했는데 게시 요청량이 home timeline 읽기 요청에 비해 수백 배 적기 때문에 방식 2가 훨씬 잘 동작한다.
그러나 2번 방식은 불리하다
트윗 작성이 많은 부가 작업을 필요로 한다는 점인데 평균적으로 75명의 팔로워에게 전달되므로 초당 4.6k 트윗은 345k가(75 * 4.6k) 된다
그러나 어떤 사용자는 팔로워가 3천만명이 넘는데, 이러면 쓰기 작업은 3천만건 이상이 될 수 있다.
트위터 사례에서 사용자당 분포는 fan-out 부하를 결정하기 때문에 확장성을 논의할 때 핵심 부하 매개변수가 된다.
트위터의 최종 전개
성능 기술하기
부하가 증가할 때 어떤 일이 일어나는지 조사해야 한다.
•
부하 매개변수를 증가시키고 시스템 자원(CPU, 메모리, 네트워크 대역폭 등)은 유지하면 시스템의 성능은 어떻게 될까?
•
부하 매개변수를 증가시켰을 때 성능이 유지되려면 자원을 얼마나 많이 늘려야 할까?
이 질문에 대답하기 위해 수치가 필요하다
처리량(throughput): 초당 처리할 수 있는 record 수 혹은 일정 크기의 데이터 작업을 수행할 때 걸리는 전체 시간
응답 시간(response time): client 가 요청을 보내고 응답을 받는 사이의 시간
지연시간(latency) 과 응답시간 (response time)
이 둘은 서로 다르다.
요청을 처리하는 실제 시간 외에도 네트워크 지연과 큐 지연도 포함한다.
지연 시간은 요청이 처리되길 기다리는 시간으로, 서비스를 기다리며 휴지 상태인 시간을 말한다.
이러한 수치는 동일한 요청이라도 매번 응답시간이 다를 수 있으므로 분포로 생각해야 한다.
평균과 백분위 예시: 서비스에 대한 100건의 샘플 요청에 대한 응답 시간
대부분의 요청이 꽤 빠르지만, 특이 값(outlier) 가 있다.
(back ground 의 context switch, network packet 손실과 TCP 재전송, GC 등등) 여러 비싼 작업을 같이 수행하는 등 추가 지연이 생길 수 있다.
그래서 평균을 다루는 것이 일반적이지만, 백분위(percentile)를 사용하는 편이 더 좋다. 예를들어 응답 시간 목록을 가지고 가장 빠른 시간부터 가장 느린 시간까지 정렬하면 중간 지점이 중간값(median) 이 된다.
이 중간값은 p50 으로 줄여서 표현하기도 한다.
얼마나 안좋은지 확인하기 위해선 95, 99, 99.9 분위를 살펴보는 것이 좋다.
95분위: 95분위 응답시간이 1.5초 라고 하면 100개 요청중 95개는 1.5초 미만이고, 100개 요청중 5개는 1.5초 보다 더 걸린다.
이러한 시간을 꼬리 지연 시간(tail latency) 라고 하며 아마존은 99.9분위로 기술한다.
이러한 지표는 서비스 수준 목표(service level objective, SLO) 와 서비스 수준 협약서(service level agreement, SLA) 에서 자주 사용하여 계약서에 등장한다.
큐 대기 지연은 응답에 상당한 부분을 차지한다. 서버는 병렬로 소수의 작업만 처리할 수 있기 때문인데 소수의 느린 요청이 큐에 있고, 그 연산이 시작되면 다른 작업은 큐에서 대기하게 된다.
이러한 현상을 선두 차단(head-of-line blocking) 이라 한다.
후속 요청이 빨리 처리 되더라도 선두 차단이 발생하면 client 는 대기 시간이 길어질 수 있기 때문에 client 측 응답 시간도 중요하게 생각해야 한다.
부하 대응 접근 방식 (Approaches for Coping with Load)
부하를 알기 위해 어떤것이 부하인지 정의(부하 매개변수)했고, 그 부하를 측정하기 위해 무엇을 살펴야 하는지(99분위) 접근했다. 이제 본질적으로 확장성에 대한 논의를 시작한다.
부하 수준이 1단계에 적합한 아키텍쳐로 10배에 해당하는 부하를 견딜 수 없다. 급성장 하는 서비스를 경험한다면 scale up(용량 확장, 수직 확장, vertical scaling)과 scaling out(규모 확장, 수평 확장, horizontal scaling) 을 구분해서 말한다.
다수의 장비에 부하를 분산하는 아키텍쳐를 shared-nothing 비공유 아키텍쳐라 부른다.
일부 시스템은 탄력적(elastic)이기 때문에 부하 증가를 감지하면 자동으로 자원이 추가되고, 그렇지 않은 시스템은 수동으로 확장하는데 수동으로 확장할 때 운영상 예상치 못한 일이 더 적고 scale out 은 예측할 수 없을 만큼 높은 경우에 유용하다.
분산 시스템을 위한 도구와 추상화가 좋아지면서 대용량 트래픽을 사용하지 않는 사례에도 분산 데이터 시스템이 향우 기본 아키텍쳐로 자리 잡을 가능성이 있으며, 이 책은 확장성뿐만 아니라 손쉬운 사용과 유지보수를 어떻게 달성하는지 설명한다.
범용적이고 모든 상황에 맞는 확장 아키텍쳐(one-size-fits-all)은 없다.
아키텍쳐를 결정할 때 읽기의 양, 쓰기의 양, 저장할 데이터 양, 데이터의 복잡도, 응답시간 요구사항, 접근 패턴등을 고려해야 한다.(각 크기가 1kb인 초당 100,000건 요청을 처리하는 아키텍쳐와 각 크기가 2GB인 분당 3건의 요청을 처리하기 위해 설계한 시스템은 처리량이 동일해도 매우 다르다)
적합한 확장성을 갖추기 위해선 주요 동작이 무엇이고, 잘 하지 않는 동작이 무엇인지에 대한 가정을 바탕으로 구축한다. → 부하 매개변수
스타트업 같이 미래를 가정한 부하에 대비하여 확장하기 보다는 빠르게 반복해서 제품 기능을 개선하는 작업이 좀 더 중요하다.
유지보수성 (Maintainability)
소프트웨어 비용의 대부분은 초기 개발이 아니라 지속해서 이어지는 유지보수에 들어간다.
(버그 수정, 시스템 운영 유지, 장애 조사, 새로운 플랫폼 적용, 새 사용 사례를 위한 변경, 기술 채무 상환, 새로운 기능 추가)
소프트웨어를 개발하는 사람은 레거시 다루기를 그리 좋아하지 않는다. 하지만 희망적인 것은 유지보수의 고통을 최소화 하고 레거시를 만들지 않게끔 소프트웨어를 설계할 수 있다. 그러기 위해 지켜야 할 원칙은 다음과 같다.
1.
운용성(operability): 운영팀이 시스템을 원활하게 운영할 수 있게 쉽게 만들어라.
2.
단순성(simplicity): 복잡도를 최대한 제거해 새로운 엔지니어가 이해하기 쉽게 만들어라
3.
발전성(evolvability): 시스템을 쉽게 변경할 수 있게 하라그래야 변경 같은 예기치 않은 사례를 적용하기 쉽다. (유연성, 수정 가능성, 적응성)
운용성: 운영의 편리함 만들기 (Operability: Making Life Easy for Operations)
운영은 나쁜 소프트웨어의 제약을 피하는 대안이 될 수 있지만, 좋은 소프트웨어라도 나쁘게 운영한다면 작동을 신뢰할 수 없다.
운영 중 일부 측면은 자동화할 수 있고, 자동화 해야 한다. 그러나 그 자동화를 확인하는 것은 사람의 몫이다.
원활하게 작동하려면 운영팀은 필수이고, 다음의 작업을 책임져야 한다.
•
시스템 상태를 모니터링 하고 상태가 좋지 않다면 빠르게 복원
•
시스템 장애, 성능 저하 등 문제의 원인 추적
•
보안 패치를 포함해 최신 상태로 유지
•
다른 시스템이 서로 주는 영향을 파악하고, 문제가 생길 수 있는 변경 사항을 손상 전에 차단
•
미래에 발생 가능한 문제를 예측해 문제 발생 전에 해결(용량)
•
모번 사례와 도구를 마련
•
애플리케이션을 다른 플랫폼으로 이동하는 등의 복잡한 유지보수 task 수행
•
설정 변경으로 생기는 보안 유지보수
•
안정적인 서비스를 유지하기 위한 절차 정의
•
개인 인사 이동에도 조직의 지식 보존
좋은 운영은 동일하게 반복되는 task 를 쉽게 수행하게끔 만들어 운영팀이 고부가가치 활동에 노력을 집중하도록 해야 한다. 동일 태스크를 쉽게 하기 위해 다음의 항목을 포함한 다양한 일을 할 수 있다.
•
runtime 동작과 시스템 내부에 대한 가시성 제공
•
자동화와 통합을 위한 우수한 지원
•
개별 장비 의존성 회피, 유지보수를 위해 장비를 내리더라도 전체에 영향을 주지 않고 운영 가능하도록
•
좋은 문서와 이해하기 쉬운 운영 모델(X하면 Y가 발생한다)
•
기본동작 제공 및 필요할 때 기본값 변경
•
적절한 자기 회복, 관리자의 시스템 수동 제어
•
예측 가능한 동작, 예기치 않은 상황 최소화
단순성: 복잡도 관리 (Simplicity: Managing Complexity)
프로젝트가 커짐에 따라 시스템은 복잡해지는데 이 복잡함은 모든 사람의 진행을 느리게 하고 유지보수의 비용을 증가시킨다. 복잡도의 증상으로 상태 공간 급증, 모듈 간 강한 커플링, 복잡한 의존성, 일관성 없는 네이밍, 성능 문제를 위한 해킹, 임시방편으로 사용한 사례 등이 있다.
복잡도는 예산과 일정을 초과 시켜 비용이 증대된다. 이러한 복잡도를 우발적 복잡도를 의미한다.
하지만 모슬리와 마크스는 소프트웨어가 풀어야 할 문제에 내재하고 있지 않고, 구현에서만 발생하는 것으로 정의 했다.
이러한 우발적 복잡도를 제거할 수 있는 방법이 추상화 이다. 좋은 추상화는 복잡한 구현을 깔끔하고 간단하게 노출시켜 다양한 곳에서 재사용이 가능하게 한다.
예를들어, SQL은 디스크에 기록하고 메모리에 저장한 복잡한 데이터 구조와 다른 clinet 의 동시 요청, 고정 후 불일치를 숨긴 추상화 이다. 물론 내부적으론 기계 언어, CPU, 시스템 호출을 숨겼다.
단지 직접 사용하지만 않을 뿐이다.
분산 시스템 분야에는 여러 좋은 알고리즘이 있다. 하지만, 관리 가능한 수준에서 복잡도를 유지하는데 도움이 되는 추상화로 이런 알고리즘을 묶는 방법은 명확하지 않다.
발전성: 변화를 쉽게 만들기 (Evolvability: Making Change Easy)
시스템의 요구사항이 영원히 바뀌지 않을 가능성은 매우 적다. 시스템의 요구사항은 계속 변하고 새로운 사실을 배워 적용하게 되고, 예기치 않은 사례가 나타나며 비즈니스의 우선순위가 바뀌기 때문이다.
조직 프로세스 측며에서 agile 작업 패턴은 변화에 적응하기 위한 프레임워크를 제공한다
또한 TDD, 리팩토링과 같이 자주 변화하는 환경에서 소프트웨어를 개발할 때 필요한 기술 도구와 패턴이 있다.
데이터 중심 애플리케이션이 표준 구성 요소
•
데이터베이스: 애플리케이션이 나중에 다시 데이터를 찾을 수 있게 데이터를 저장
•
캐시: 읽기 속도 향상을 위해 값비싼 수행 결과를 기억
•
검색 색인: 키워드로 검색하거나 다양한 방법으로 필터링 할 수 있게 제공
•
스트림 처리: 비동기 처리를 위해 다른 프로세스로 메시지 보내기
•
일괄 처리: 주기적으로 대량의 누적 데이터를 분석
뻔한 말처럼 들린다면 데이터 시스템이 성공적으로 추상화 되었기 때문이다.