# Chapter 1: 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션

<br>

## 💡 책에서 기억하고 싶은 내용

<br>

### 데이터 시스템에 대한 생각

* 일반적으로 데이터베이스, 큐, 캐시 등을 매우 다른 범주에 속하는 도구로 생각한다
  * 데이터베이스와 메시지 큐는 표면적으로 비슷하더라도 (둘 다 얼마 동안 데이터를 저장) `매우 다른 접근 패턴` 을 갖고 있어 `서로 다른 특성` 이 있기 때문에 구현 방식이 매우 다르다
* 데이터 저장과 처리를 위한 새로운 도구들
  * 메시지 큐로 사용하는 데이터스토어인 `Redis` 가 있고,
  * 데이터베이스처럼 지속성(durability) 을 보장하는 메시지 큐인 `Apache Kafka` 도 있다
* 점점 더 많은 애플리케이션이 단일 도구로는 더 이상 데이터 `처리` 와 `저장` 모두를 만족시킬 수 없는, 과도하고 광범위한 요구사항을 갖고 있다
  * 대신 작업(work)은 단일 도구에서 효율적으로 수행할 수 있는 태스크(task)로 나누고, 다양한 도구들은 애플리케이션 코드를 이용해 서로 연결한다
* 서비스 제공을 위해 각 도구를 결합할 때 `서비스 인터페이스` 나 `애플리케이션 프로그래밍 인터페이스 (API)` 는 보통 클라이언트가 모르게 `구현 세부 사항` 을 숨긴다
  * 좀 더 작은 범용 구성 요소들로 새롭고 특수한 목적의 데이터 시스템을 만든다
  * 복합 데이터 시스템(composite data system) 은 외부 클라이언트가 일관된 결과를 볼 수 있게끔 쓰기에서 `캐시` 를 무효화하거나 업데이트하는 등 특정 `보장 기능` 을 제공할 수 있다
* 대부분의 소프트웨어 시스템에서 중요하게 여기는 세 가지 관심사
  * `신뢰성 (Reliability)`
    * 하드웨어나 소프트웨어 결함, 인적 오류 (human error) 같은 `역경`에 직면하더라도 시스템은 `지속적으로 올바르게 동작` (원하는 성능 수준에서 정확한 기능을 수행) 해야 한다
  * `확장성 (Scalability)`
    * 시스템의 데이터 양, 트레픽 양, 복잡도가 `증가` 하면서 이를 처리할 수 있는 적절한 방법이 있어야 한다
  * `유지보수성 (Maintainability)`
    * 시간이 지남에 따라 여러 다양한 사람들이 시스템 상에서 작업 (현재 작업을 유지보수하고, 새로운 사용 사례를 시스템에 적용하는 엔지니어링과 운영) 할 것이기 때문에, 모든 사용자가 시스템 상에서 `생산적` 으로 작업할 수 있게 해야한다

### 신뢰성

* 소프트웨어의 일반적인 기대치
  * 애플리케이션은 사용자가 `기대한 기능` 을 수행한다
  * 시스템은 `사용자가 범한 실수` 나 `예상치 못한 소프트웨어 사용법` 을 `허용` 할 수 있다
  * 시스템 성능은 `예상된 부하` 와 `데이터 양` 에서 필수적인 사용 사례를 충분히 만족한다
  * 시스템은 `허가되지 않은 접근`과 `오남용` 을 `방지`한다
* `무언가 잘못되더라도 지속적으로 올바르게 동작함` 을 신뢰성의 의미로 이해할 수 있다
* 잘못될 수 있는 일을 `결함 (fault)` 라 부른다
  * 결함을 예측하고 대처할 수 있는 시스템을 `내결함성 (fault-tolerance)` 또는 `탄력성 (resilient)` 을 지녔다고 말한다
* `결함`은 `장애 (failure)` 와 동일하지 않다
  * `결함`은 사양에서 벗어난 시스템의 한 구성 요소로 정의되지만,
  * `장애`는 사용자에게 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우다
* 결함으로 인해 장애가 발생하지 않게끔 `내결함성 구조` 를 설계하는 것이 가장 좋다
  * 결함을 예측하고 대체할 수 있는 시스템
* `내결함성 시스템` 에서 경고 없이 개별 프로세스를 무작위로 죽이는 것과 같이 `고의적으로 결함을 일으켜` 결함률을 증가시키는 방법
  * 많은 중대한 버그는 미흡한 오류 처리에 기인한다
  * `고의적`으로 결함을 유도함으로써 내결함성 시스템을 지속적으로 훈련하고 테스트해서, 결함이 자연적으로 발생했을 때 올바르게 처리할 수 있다는 자신감을 높인다
  * ex) Netflix의 Chaos Monkey

### 하드웨어 결함

* 시스템 장애율을 줄이기 위한 첫 번째 대응으로 각 하드웨어 구성 요소에 `중복(redundancy)` 을 추가하는 방법이 일반적이다
  * 디스크는 `RAID` (Redundant Array of Independent Disk) 구성으로 설치할 수 있고,
  * 서버는 이중 전원 디바이스와 `hot-swap` 가능한 CPU를,
    * 데이터센터는 건전지와 예비 전원용 디젤 발전기를 갖출 수 있다

### 소프트웨어 오류

* 시스템 내 체계적 오류 (`systematic error`) 는 예상하기 더 어렵고, 노드 간 상관관계 때문에 상관관계 없는 하드웨어 결함보다 오히려 시스템 오류를 더욱 많이 유발하는 경향이 있다
  * ex)
    * 잘못된 특정 입력이 있을 때 모든 애플리케이션 서버 인스턴스가 죽는 소프트웨어 버그
    * CPU 시간, 메모리, 디스크 공간, 네트워크 대역폭처럼 `공유 자원` 을 과도하게 사용하는 일부 `프로세스`
    * 시스템의 속도가 느려저 반응이 없거나, 잘못된 응답을 반환하는 서비스
    * 한 구성 요소의 작은 결함이 다른 구성 요소의 결함을 야기하고 차례차례 더 많은 결함이 발생하는 `연쇄 장애 (cascading failure)`
* 이 같은 소프트웨어 결함을 유발하는 버그는 `특정 상황` 에 의해 발생하기 전까지 오랫동안 나타나지 않는다
* 소프트웨어의 `체계적 오류 문제` 는 신속한 해결책이 없지만 아래와 같은 일들이 문제 해결에 도움을 줄 수 있다
  * 시스템의 가정(presume) 과 상호 작용에 대해 주의 깊게 생각하기
  * 빈틈없는 테스트
  * 프로세스 격리
  * 죽은 프로세스의 재시작 허용
  * 프로덕션 환경에서 시스템 동작의 측정
  * 모니터링
  * 분석하기
* 시스템이 무언가 보장하길 기대한다면, 수행 중에 이를 지속적으로 확인해 차이가 생기면 경고를 발생시킬 수 있다

### 인적 오류

신뢰성 있는 시스템을 만들기위한 접근 방식

* `오류의 가능성`을 `최소화` 하는 방향으로 시스템을 설계
  * 잘 설계된 추상화, API, 관리 인터페이스를 사용하여 `옳은 일` 은 쉽게하고, `잘못된 일` 은 막을 수 있다
* 사람의 실수로 장애가 발생할 수 있는 부분을 분리
  * 실제 데이터를 사용해 안전하게 살펴보고 실험할 수 있지만, 실제 사용자에게는 영향이 없는 `비 프로덕션 샌드박스 (sandbox)` 를 활용
* 단위 테스트 → 전체 시스템 통합 테스트, 수동 테스트까지 `모든 수준`에서 `철저하게 테스트`
* 인적 오류를 빠르고 쉽게 `복구` 할 수 있게 하기
  1. 설정 변경 내역에 대한 빠른 `롤백`
  2. 새로운 코드를 서서히 `롤아웃` 하기 (예상치 못한 버그가 일부 사용자에게만 영향이 미치게 함)
  3. 데이터 재계산 도구 제공
* 상세하고 명확한 `모니터링 대책` 마련
  * 문제가 발생했을 때 `지표 (metrics)` 는 문제를 분석하는 데 매우 중요하다
* 조작 교육과 실습을 실행

### 확장성

* 성능 저하를 유발하는 흔한 이유 중 하나는 `부하 증가` 다
* 확장성을 논한다는 것은 아래와 같은 질문을 고려하는 것이다
  * “시스템이 특정 방식으로 커지면 이에 대처하기 위한 선택은 무엇인가?”
  * “추가 부하를 다루기 위해 계산 자원을 어떻게 투입할까?

### 부하 기술하기

* 시스템의 현재 부하를 간결하게 기술해야 한다
  * 그래야 부하 성장 질문 (부하가 두 배로 되면 어떻게 될까?)을 논의할 수 있다
* 부하는 `부하 매개변수 (load parameter)` 라 부르는 몇 개의 숫자로 나타낼 수 있다
  * ex)
    * 웹 서버의 초당 요청 수 (RPS)
    * DB 읽기 vs 쓰기 비율
    * 동시 활성 사용자 (active user)
    * 캐시 적중률 (cache hit)
* 평균적인 경우가 중요할 수도 있고, 소수의 극단적인 경우가 병목 현상의 원인일 수 있다

### 성능 기술하기

* 시스템 부하를 기술하면, 부하가 증가할 때 어떤 일이 일어나는지 조사할 수 있다
  * `부하 매개변수` 를 증가시키고, 시스템 `자원`(CPU, 메모리, 네트워크 대역폭 등)은 변경하지 않고 `유지`하면 시스템 성능은 어떻게 영향을 받을까?
  * `부하 매개변수` 를 증가시켰을 때, `성능`이 변하지 않고 `유지`되길 원한다면 자원은 얼마나 많이 늘려야할까?
* 하둡 같은 일괄 처리 시스템은 보통 `처리량 (throughput)` 에 관심을 가진다
* 온라인 시스템에서는 `서비스 응답 시간 (response time)` , 즉 client가 요청을 보내고 응답을 받는 사이의 시간이 중요하다
* 응답 시간은 단일 숫자가 아니라 측정 가능한 값의 `분포` 로 생각해야 한다
* 일반적으로 평균보다는 `백분위 (percentile)` 를 사용하는 편이 좋다
  * 응답 시간 목록을 가지고 가장 빠른 시간부터 제일 느린 시간까지 정렬하면, 중간 지점이 `중앙값 (median)` 이 된다
    * 사용자가 보통 얼마나 오랫동안 기다려야 하는지 알고 싶다면 `중앙값` 이 좋은 지표다
    * 중앙값은 50분위로서, `p50` 으로 축약할 수 있다
    * 중앙값은 `단일 요청`을 참고한다!
  * `특이 값`이 얼마나 좋지 않는지 알아보려면, 상위 백분위를 살펴보는 것이 좋다
    * 일반적으로 p95, p99, p999를 사용한다
    * 요청의 95%, 99%, 99.9% 가 특정 기준치보다 더 빠르면 해당 특정 기준치가 각 백분위의 응답 시간 기준치가 된다
    * `꼬리 지연 시간 (tail latency)` 으로 알려진 상위 백분위 응답 시간은 서비스의 사용자 경험에 직접 영향을 주기 때문에 중요하다
    * `큐 대기 지연 (queueing delay)` 은 높은 백분위에서 응답 시간의 상당 부분을 차지한다
      * 서버는 병렬로 소수의 작업만 처리할 수 있기 때문에, 소수의 느린 요청 처리만으로 `후속 요청 처리` 가 지체 된다
        * 이 현상을 `선두 차단 (head-of-line blocking)` 이라 한다
      * 서버에서 후속 요청이 빠르게 처리되더라도, 이전 요청이 완료되길 기다리는 시간 때문에 클라이언트는 전체적으로 응답 시간이 느리다고 생각할 것이다
        * 이런 문제 때문에 client side 응답 측정이 중요하다!

### 지연 시간(latency) 과 응답 시간(response time)

* `응답 시간`
  * client 관점에서 본 시간으로, 요청을 처리하는 실제 시간 (서비스 시간) 외에도 네트워크 지연과 큐 지연도 포함한다
* `지연 시간`
  * 요청이 처리되길 기다리는 시간
  * 서비스를 기다리며 휴지(latent) 상태인 시간

### 부하 대응 접근 방식

* 다수의 장비에 부하를 분산하는 아키텍처를 `비공유 (shared-nothing)` 아키텍처라 부른다
* `탄력적인 시스템`은 부하를 예측할 수 없을 만큼 높은 경우 유용하지만, `수동으로 확장하는 시스템`이 더 간단하고 운영상 예상치 못할 일이 적다
* 다수의 장비에 `stateless` 서비스를 배포하는 일은 상당히 간단하지만, 단일 노드에 `stateful` 데이터 시스템을 `분산 설치` 하는 일은 아주 많은 복잡도가 추가적으로 발생한다
  * 확장 비용이나 데이터베이스를 분산으로 만들어야 하는 고가용성 요구가 있을 때까지, 단일 노드에 데이터베이스를 유지하는 것 (scale-up) 이 최근까지의 통념이다
* 아키텍처를 결정하는 요소는 읽기의 양, 쓰기의 양, 저장할 데이터의 양, 데이터의 복잡도, 응답 시간 요구사항, 접근 패턴 등이 있다
* 특정 애플리케이션에 적합한 확장성을 갖춘 아키텍처는 주요 동작이 무엇이고 잘 하지 않는 동작이 무엇인지에 대한 `가정` 을 바탕으로 구축한다
  * 이 가정은 곧 `부하 매개변수` 가 된다
  * 이 가정이 잘못되면, 확장에 대한 엔지니어링 노력은 헛수고가 되고 최악의 경우 역효과를 낳는다
  * 검증되지 않은 제품의 경우 미래를 가정한 `부하에 대비해 확장`하기 보다는, 빠르게 반복해서 `제품 기능을 개선`하는 작업이 좀 더 중요하다

### 유지보수성

유지보수 중 고통을 최소화하고 레거시 소프트웨어를 만들지 않기 위한 시스템 설계 원칙은 다음 세 가지다

1. `운용성 (operability)`
   * 운영팀이 시스템을 원활하게 운영할 수 있게, 쉽게 만들어라
2. `단순성 (simplicity)`
   * 시스템의 `복잡도`를 최대한 `제거`해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들어라
     * 복잡도를 제거하기 위한 최상의 도구는 `추상화` 다
3. `발전성 (evolvability)`
   * 엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하라
     * 그래야 요구사항 변경 같은 예기치 않은 사용 사례를 적용하기가 쉽다
   * 이 속성은 `유연성 (extensibility)`, `수정 가능성 (modifiability)`, `적응성 (plasticity)`으로 알려져 있다
   * `애자일 (agile)` 작업 패턴은 변화에 적응하기 위한 프레임워크를 제공한다

### 정리

* `신뢰성` 은 결함이 발생해도 시스템이 올바르게 동작하게 만든다는 의미다
  * 결함은 하드웨어와 소프트웨어 버그, 그리고 사람에게 있을 수 있다
  * 내결함성 기술은 최종 사용자에게 특정 유형의 결함을 숨길 수 있게 해준다
* `확장성` 은 부하가 증가해도 좋은 성능을 유지하기 위한 전략을 의미한다
  * 확장 가능한 시스템에서는 부하가 높은 상태에서 신뢰성을 유지하기 위해 `처리 용량` 을 추가할 수 있다
* `유지보수성` 에는 많은 측면이 있지만, 유지보수성의 본질은 시스템에서 작업하는 엔지니어와 운영팀의 삶을 개선하는데 있다
  * 좋은 `추상화` 는 `복잡도` 를 줄이고, 쉽게 시스템을 변경할 수 있게 하며, 새로운 사용 사례에 적용하는 데 도움이 된다
  * 좋은 `운용성` 이란 시스템의 건강 상태를 잘 관찰할 수 있고, 시스템을 효율적으로 관리하는 방법을 보유한다는 의미다
