오늘날 많은 애플리케이션은 계산 중심(compute-intensive)과는 다르게 데이터 중심(data-intensive)적이다. 이제부터 개발자는 애플리케이션 개발자뿐만 아니라 데이터 시스템 설계자이기도 하다.

점점 더 많은 애플리케이션이 단일 도구로는 더 이상 데이터 처리와 저장 모두를 만족시킬 수 없는 과도하고 광범위한 요구사항을 갖고 있기 때문이다.

사용자 요청에 따라

  1. 기본 데이터베이스에 데이터를 저장하고
  2. 데이터 변경을 포착하여 검색 색인에 갱신하고
  3. 캐시 무효화나 갱신을 한다.
  4. 비동기 태스크를 위해 메시지 큐에 전달한다.

데이터 시스템이나 서비스를 설계할 떄 까다로운 문제가 많이 생긴다. 이 책에서는 대부분의 소프트웨어 시스템에서 중요하게 여기는 세 가지 관심사에 중점을 둔다.

  • 신뢰성 (Reliability)

하드웨어나 소프트웨어 결함, 인적 오류 같은 문제가 발생해도 시스템은 지속적으로 올바르게 동작해 야한다.

  • 확장성 (Scalability)

시스템의 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 수 있는 적절한 방법이 있어야 한다.

  • 유지보수성 (Maintainability)

모든 사용자가 시스템 상에서 생산적으로 작업할 수 있게 해야 한다.

신뢰성

“무언가 잘못되더라도 지속적으로 올바르게 동작함”을 신뢰성의 의미로 이해할 수 있다. 잘못될 수 있는 일을 결함(fault)이라 함. 결함을 예측하고 대처할 수 있는 시스템을 내결함성 또는 탄력성이라고 함

내결함성은 특정 유형의 결함 내성에 대해서만 이야기 하는 것이 타당함. 예를 들어, 블랙홀이 지구와 지구 상의 모든 서버를 삼켜버려도 웹 호스팅이 가능한 내결함성을 지닐 순 없다.

결함은 장애(failure)와 다름. 결함은 사양에서 벗어난 시스템의 한 구성 요소로 정의함. 장애는 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 것을 의미함.

하드웨어 결함

하드디스크 고장. 램 결함. 대규모 정전. 네트워크 케이블을 잘못 뽑는 것. 등 규모가 큰 데이터센터에서 일하는 사람은 많은 장비를 다룰 경우 이 같은 일은 늘상 일어난다.

장애율을 줄이기 위해 하드웨어 구성 요소에 중복을 추가하는 방법이 일반적이다. 디스크는 RAID 구성으로 설치 가능. 서버는 이중 전원 디바이스와 핫 스왑 가능한 CPU 구성. 데이터센터는 예비 전원용 발전기 갖추기.

AWS 같은 일부 클라우드 플랫폼은 단일 장비 신뢰성보다 유연성과 탄력성을 우선적으로 처리하게끔 설계됐기 때문에 가상 장비 인스턴스가 별도의 경고 없이 사용할 수 없게 되는 상황이 상당히 일반적임.

소프트웨어 오류

소프트웨어 오류는 하드웨어 결함보다 더 일반적이고 다양한 원인으로 발생함. 소프트웨어의 체계적 오류 문제는 신속한 해결책이 없다. 시스템의 가정과 상호작용에 대해 주의 깊게 생각하기. 빈틈없는 테스트. 프로세스 격리. 재시작 허용. 시스템 동작 측정, 모니터링. 분석하기 등

인적 오류

대규모 인터넷 서비스에 대한 한 연구에 따르면 운영자의 설정 오류가 중단의 주요 원인임. 반면 하드웨어 결함은 중단 원인의 10~25%에 불과함.

어떻게 인적 오류를 줄일까? 최고의 시스템은 다양한 접근 방식을 결합한다.

  • 오류의 가능성을 최소화하는 방향으로 설계. 잘 설계된 추상화를 제공. 하지만 지나치게 제한적인 추상화면 이 인터페이스를 피해 작업한다. 그래서 균형을 맞추기 어려움.
  • 샌드박스를 제공하라. 실제 데이터를 사용해 안전하게 살펴보고 실험할 수 있도록 해야 한다.

신뢰성은 얼마나 중요할까

비즈니스 애플리케이션에서 버그는 생산성 저하의 원인이고 매출에 손실이 발생하고 명성에 타격을 준다는 면에서 많은 비용이 든다.

‘중요하지 않은’ 애플리케이션도 사용자에 대한 책임이 있다. 사진 앱에 아이들의 사진과 동영상을 모두 보관한 부모. 에러가 발생했을 때 부모의 심정은?

증명되지 않은 시장을 위해 시제품을 개발하는 비용이나 매우 작은 이익률의 서비스를 운영하는 비용을 줄이려 신뢰성을 희생해야 하는 상황이 있다. 하지만 이 경우에는 비용을 줄여야 하는 시점을 매우 잘 알고 있어야 함.

확장성

시스템이 현재 안정적으로 동작한다고 해서 미래에도 그럴 것이라는 보장은 없다. 성능 저하를 유발하는 흔한 이유 중 하나는 부하 증가다.

부하 기술하기

시스템의 현재 부하를 간결하게 기술헤야 한다. 그래야 부하 성장 질문(부하가 두 배로 되면 어떻게 될까?)을 논의 할 수 있다.

부하는 부하 매개변수(load parameter)로 기술할 수 있다. 가장 적합한 부하 매개변수 선택은 시스템 설계에 따라 달라진다. 웹 서버의 초당 요청 수, 데이터베이스 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자 수, 캐시 적중률 등이 될 수 있다.

트윗 예시

  • 트윗 작성

사용자는 팔로워에게 새로운 메시지를 게시할 수 있다(평균 초당 4.6k 요청, 피그일 때 초당 12k 요청 이상).

  • 홈 타임라인

사용자는 팔로우한 사람이 작성한 트윗을 볼 수 있다(초당 300k 요청).


홈 타임라인 기능을 설계할 때 팔로우하는 모든 사람의 트윗을 시간순으로 정렬해서 합치는 방식은 질의 부하를 버텨내기 어렵다.

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

그래서 Fanout on write 방식을 사용함. 트윗을 작성할 때마다 모든 팔로워의 홈 타임라인에 트윗을 추가함. 평균적으로 트윗이 약 75명의 팔로워에게 전달되므로 초당 4.6k 트윗은 홈 타임라인 캐시에 초당 345k건의 쓰기가 된다.

그러나 팔로워가 3천만 명이 넘는 경우 단일 트윗이 홈 타임라인에 3천만 건 이상의 쓰기 요청이 된다.

팔로워의 분포는 팬아웃 부하를 결정하기 때문에 확장성을 논의할 때 핵심 부하 매개변수가 된다.

트위터는 최종적으로 Fanout on write, Fanout on read를 조합하여 사용함. 유저가 홈 타임라인을 요청할 때 팔로우 목록을 조회 후 DB에서 트윗 가져옴.

대부분의 사용자의 트윗은 계속해서 사람들이 작성할 때 홈 타임라인에 펼쳐지지만 팔로워 수가 매우 많은 소수 사용자는 팬 아웃에서 제외된다. 유명인의 트윗은 별도로 가져와 읽는 시점에 사용자의 홈 타임라인에 합친다.

성능 기술하기

시스템 부하를 기술하면 부하가 증가할 때 어떤 일이 일어나는지 조사할 수 있다.

  • 부하 매개변수를 증가시키고 시스템 자원(CPU, 메모리, 네트워크 대역퐁 등)은 변경하지 않고 유지하면 시스템 성능은 어떻게 영향을 받을까?
  • 부하 매개변수를 증가시켰을 때 성능이 변하지 않고 유지되길 원한다면 자원을 얼마나 많이 늘려야 할까?

실제로 다양한 요청을 다루는 시스템에서 응답 시간은 많이 변한다. 응답 시간은 단일 숫자가 아니라 측정 가능한 값의 분포로 생각해야 한다.

평균 응답 시간을 살피는 일은 일반적이다. 하지만 이는 좋은 지표가 아님. 얼마나 많은 사용자가 실제로 지연을 경험했는지 알려주지 않기 때문이다.

백분위를 사용하는 편이 좋다. 응답 시간 목록을 가지고 가장 빠른 시간부터 제일 느린 시간까지 정렬하면 중간 지점이 중앙값이 된다. 중간 응답 시간이 200밀리초면 요청의 반은 200밀리초 미만으로 반환되고 나머지 반은 그보다 오래 걸린다는 뜻이다.

특이 값이 얼마나 좋지 않은지 알아보려면 상위 백분의를 살펴본다. p95, p99, p99.9가 일반적이다. p95는 95%의 요청이 이 응답 시간보다 빠르다는 뜻이다.

예를 들어, p95 응답 시간이 1.5초라면 100개 중 95개는 1.5초 미만이고 5개는 1.5초 이상 걸린다.


꼬리 지연 시간(tail latency): 상위 백분위 응답 시간은 서비스의 사용자 경험에 직접 영향을 주기 떄문에 중요하다.

예를 들어 아마존은 내부 서비스의 응답 시간 요구사항을 99.9분위로 기술한다. 1000개 중 1개만 영향이 있음에도 말이다.

보통 응답 시간이 가장 느린 요청을 경험한 고객들은 많은 구매를 해서 고객 중에서 계정에 가장 많은 데이터를 갖고 있어서다.(즉, 이 고객들은 가장 소중한 고객이다)

아마존은 응답 시간이 100밀리초 증가하면 판매량이 1% 줄어들고 1초가 느려지면 만족도 지표는 16% 줄어드는 현상을 관찰했다.

하지만 99.9분위 최적화 작업은 비용이 너무 들지만 아마존이 추구하는 목표에 충분히 이익을 가져다주지 못한다고 여겨진다. 최상위 백분위는 통제할 수 없는 임의 이벤트에 쉽게 영향을 받기 때문에 응답 시간을 줄이기가 매우 어려워 이점은 더욱 줄어든다.

부하 대응 접근 방식

용량 확장(scaling up): 수직 확장, 좀 더 강력한 장비로 이동 규모 확장(scaling out): 수평 확장, 다수의 낮은 사양 장비에 부하를 분산

다수의 장비에 부하를 분산하는 아키텍처를 비공유(shared-nothing) 아키텍처라 부른다.

다수의 장비에 상태 비저장 서비스를 배포하는 일은 상당히 간단하다. 하지만 단일 노드에 상태 유지 데이터 시스템을 분산 설치하는 일은 아주 많은 복잡도가 추가적으로 발생한다. 이런 이유로 확장 비용이나 데이터베이스를 분산으로 만들어야 하는 고가용성 요구가 있을 때까지 단일 노드에 데이터베이스를 유지하는 것(용량 확장)이 최근까지의 통념이다.

유지보수성

고통을 최소화하고 레거시 소프트웨어를 만들지 않게끔 설계하는 방법.

  • 운용성(operability)

운영팀이 시스템을 원활하게 운영할 수 있게 쉽게 만들어라.

  • 단순성(simplicity)

시스템에서 복잡도를 최대한 제거해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들어라.

  • 발전성(evolvabilty)

엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하라. 그래야 요구사항 변경 같은 예기치 않은 사용 사례를 적용하기가 쉽다. 이 속성은 유연성, 수정 가능성, 적응성으로 알려져 있다.

신뢰성, 확장성을 달성하기 위한 쉬운 해결책은 없다. 그보다 운용성, 단순성, 발전성을 염두에 두고 시스템을 생각하려 노력해야 한다.

운용성: 운영의 편리함 만들기

“좋은 운영은 종조 나쁜 소프트웨어의 제약을 피하는 대안이 될 수 있다. 하지만 좋은 소프트웨어라도 나쁘게 운영할 경우 작동을 신뢰할 수 없다.”

좋은 운영성이란 동일하게 반복되는 태스크를 쉽게 수행하게끔 만들어 운영팀이 고부가가치 활동에 노력을 집중한다는 의미다.

단순성: 복잡도 관리

복잡도는 같은 시스템에서 작업해야 하는 모든 사람의 진행을 느리게 하고 나아가 유지보수 비용이 증가한다. 복잡도는 다양한 증상으로 나타난다. 상태 공간의 급증, 모듈 간 강한 커플링, 복잡한 의존성, 일관성 없는 명명과 용어, 성능 문제 해결을 목표로 한 해킹, 임시방편으로 문제를 해결한 특수 사례 등.

추상화는 복잡도를 제거하기 위한 최상의 도구다.

발전성: 변화를 쉽게 만들기

시스템의 요구사항이 영원히 바뀌지 않을 가능성은 매우 적다.

데이터 시스템 변경을 쉽게 하고 변화된 요구사항에 시스템을 맞추는 방법은 시스템의 간단함과 추상화와 밀접한 관련이 있다. 간단하고 이해하기 쉬운 시스템은 대개 복잡한 시스템보다 수정하기 쉽다.