haileyjpark

데이터 중심 애플리케이션 설계 (1장) 읽고 정리 본문

소프트웨어 공학

데이터 중심 애플리케이션 설계 (1장) 읽고 정리

개발하는 헤일리 2023. 5. 7. 21:28
반응형

서문

데이터 양, 데이터 복잡성, 데이터가 변하는 속도 등, 데이터가 주요 도전 과제인 애플리케이션을 데이터 중심적(data-intensive)이라고 말한다. 반대로 CPU 사이클이 병목인 경우 계산 중심적(compute-intensive)이라고 한다.

 

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

데이터 시스템

데이터베이스, 큐, 캐시 등을 데이터 시스템이라는 포괄적 용어로 묶는 이유

 

  • 최근에 만들어진 새로운 도구들은 다양한 use case에 최적화됐기 때문에 더이상 전통적인 분류에 딱 들어맞지 않고 분류가 흐려지고 있다. 예를 들어, 메시지 큐로 사용하는 datastore인 Redis가 있고, 데이터베이스처럼 지속성(durability)을 보장하는 메시지 큐인 아파치 카프카(Apache Kafka)도 있다.
  • 점점 더 많은 애플리케이션이 단일 도구로는 더이상 데이터 처리와 저장 모두를 만족시킬 수 없는 과도하고 광범위한 요구사항을 갖고 있다. 작업(work)은 단일 도구에서 효율적으로 수행할 수 있는 task로 나누고 다양한 도구들은 애플리케이션 코드를 이용해 서로 연결한다.

 

대부분의 소프트웨어 시스템에서 중요하게 여기는 세 가지 관심사

 

  • 신뢰성(Reliability)
    • 하드웨어나 소프트웨어 결함, 심지어 인적 오류(human error) 같은 역경에 직면하더라도 시스템은 지속적으로 올바르게 동작(원하는 성능 수준에서 정확한 기능을 수행해야 한다.
  • 확장성(Scalability)
    • 시스템의 데이터 양, 트래픽 양, 복잡도가 증가하면서 이를 처리할 수 있는 적절한 방법이 있어야 한다.
  • 유지보수성(Maintainability)
    • 시간이 지남에 따라 여러 다양한 사람들이 시스템 상에서 작업(현재 작업을 유지보수하고 새로운 사용 사례를 시스템에 적용하는 엔지니어링과 운영)할 것이기 때문에 모든 사용자가 시스템 상에서 생산적으로 작업할 수 있게 해야한다.

 

신뢰성

소프트웨어의 일반적인 기대치가 “올바르게 동작함"을 의미하는 경우, 대략 “무언가 잘못되더라도 지속적으로 올바르게 동작함"을 신뢰성의 의미로 이해할 수 있다.

 

결함을 예측하고 대처할 수 있는 시스템을 내결함성(fault-tolerant) 또는 탄력성(resilient)을 지녔다고 말함.

결함은 장애(failure)와 동일하지 않다. 일반적으로 결함은 사양에서 벗어난 시스템의 한 구성 요소로 정의되지만, 장애는 사용자에세 필요한 서비스를 제공하지 못하고 시스템 전체가 멈춘 경우다.

 

고의적으로 결함을 유도함으로써 내결함성 시스템을 지속적으로 훈련하고 테스트해서 결함이 자연적으로 발생했을 때 올바르게 처리할 수 있다는 자신감을 높인다. → 넷플릭스의 카오스 몽키가 이런 접근 방식의 한 예이다.

 

하드웨어 결함

하드웨어 결함 : 하드디스크가 고장 나고, 램에 결함이 생기고, 대규모 정전 사태가 발생하고, 누군가가 네트워크 케이블을 잘못 뽑는 것과 같은 결함을 말함

하드디스크의 평균 장애 시간(mean time to failure, MTTF)은 약 10~50년으로 보고됨. 따라서 10,000개의 디스크로 구성된 저장 클러스터는 평균적으로 하루에 한 개의 디스크가 죽는다고 예상해야 한다. (MTTF = 가동시간 / 장애 횟수)

 

데이터 양과 애플리케이션의 계산 요구가 늘어나면서 더 많은 애플리케이션이 많은 수의 장비를 사용하게 됐고 하드웨어 결함율도 증가했다. AWS같은 일부 플랫폼은 가상 장비 인스턴스가 별도의 경고 없이 사용할 수 없게 되는 상황이 일반적이다. (단일 장비 신뢰성보다 유연성과 탄력성을 우선적으로 처리하게끔 설계됐기 때문.)

 

따라서 소프트웨어 내결함성 기술을 사용하거나 하드웨어 중복성을 추가해 전체 장비의 손실을 견딜 수 있는 시스템으로 점점 옮겨가고 있다. 장비를 재부팅해야 하는 경우 (운영체제 보안 패치 적용 등) 단일 서버 시스템은 계획된 중단시간이 필요하지만 장비 장애를 견딜 수 있는 시스템은 전체 시스템의 중단시간 없이 한 번에 한 노드씩 패치가 가능하다.

 

소프트웨어 오류

시스템 내 체계적 오류(systematic errer)는 예상하기가 더 어렵고 노드 간 상관관계 때문에 상관관계 없는 하드웨어 결함보다 오히려 시스템 오류를 더욱 많이 유발하는 경향이 있다.

예시) CPU 시간, 메모리 등 공유 자원을 과도하게 사용하는 일부 프로세스, 한 구성 요소의 작은 결함이 다른 구성 요소의 결함을 야기하고 차례차례 더 많은 결함이 발생하는 연쇄 장애(cascading failure) 등

 

소프트웨어의 체계적 오류 문제는 신속한 해결책이 없다. 시스템의 가정과 상호작용에 대해 주의깊게 생각하기, 빈틈없는 테스트, 프로세스 격리, 모니터링 등의 액션이 문제 해결에 도움을 줄 수 있다.

시스템이 뭔가를 보장하길 기대한다면 수행 중에 이를 지속적으로 확인해 차이가 생기는 경우 경고를 발생시킬 수 있다.

 

인적 오류

사람이 미덥지 않음에도 시스템을 어떻게 신뢰성 있게 만들까?

  • 오류의 가능성을 최소화하는 방향으로 시스템을 설계하라.
  • 사람이 가장 많이 실수하는 부분에서 사람의 실수로 장애가 발생할 수 있는 부분을 분리하라. (비 프로덕션 샌드박스를 제공하라.)
  • 모든 수준에서 철저하게 테스트하라.
  • 장애 발생의 영향을 최소화하기 위해 인적 오류를 빠르고 쉽게 복구할 수 있게 하라. (설정 변경 내역을 빠르게 롤백하고 새로운 코드를 서서히 롤아웃하게 만들고 이전 계산이 잘못된 경우를 대비해 데이터 재계산 도구를 이용하는 등)
  • 성능 지표와 오류율 같은 상세하고 명확한 모니터링 대책을 마련하라. 문제 발생 시 지표(metrics)는 문제를 분석하는 데 매우 중요하다.
  • 조직 교육과 실습 시행

 

확장성

성능 저하를 유발하는 흔한 이유 중 하나는 부하 증가이다.

부하 기술하기

부하는 **부하 매개변수(load parameter)**라 부르는 몇 개의 숫자로 나타낼 수 있다.

웹 서버의 초당 요청 수, 데이터베이스의 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자(active user), 캐시 적중률 등이 부하 매개변수가 될 수 있다.

 

[트위터의 사례]

트위터의 첫번째 버전은 사용자가 자신의 홈 타임라인을 요청할 때마다 사용자가 팔로우하는 유저의 트윗을 찾아 시간 순으로 정렬하는 방식이었고, 시스템이 홈 타임라인의 부하를 버텨내기 힘들었다.

 

그래서 접근 방식을 전환해 사용자가 트윗을 작성할 때마다 사용자를 팔로우하고 있는 유저들의 홈 타임라인 캐시에 트윗을 삽입하도록 했다. 이 방식은 사용자를 팔로우하고 있는 유저들이 홈 타임라인을 요청할 때는 읽기 요청이 결과를 미리 계산했기 때문에 비용이 저렴하다.

 

평균적으로 트윗 게시 요청량이 홈 타임라인 읽기 요청량에 비해 수백 배 적기 때문에 두번째 방식이 훨씬 더 잘 동작한다. 이 경우에는 쓰기 시점에 더 많은 일을 하고, 읽기 시점에 적은 일을 하는 것이 바람직하다.

 

하지만 이 방식은 트윗 작성시마다 팔로워들의 홈타임라인 캐시에 많은 쓰기 요청이 생긴다는 단점이 있다. 그래서 트위터는 하이브리드 방식으로 변경하여, 팔로워가 많은 유명인이 트윗을 게시했을 경우는 팬 아웃에서 제외되고, 첫번째 방식 처럼 읽는 시점에 사용자의 홈 타임라인에 합친다. 이 혼합형 접근 방식은 좋은 성능으로 지속적인 전송이 가능하다.

 

성능 기술하기

처리량(throughput) : 초당 처리할 수 있는 레코드 수나 일정 크기의 데이터 집합으로 작업을 수행할 때 걸리는 전체 시간

하둡같은 일괄처리 시스템은 보통 처리량에 관심을 가진다. 온라인 시스템에서 더 중요한 사항은 서비스 응답시간(response time), 즉 클라이언트가 요청을 보내고 응답을 받는 사이의 시간이다.

 

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

응답 시간: 클라이언트 관점에서 본 시간으로, 요청을 처리하는 실제 시간(서비스 시간) 외에도 네트워크 지연과 큐 지연도 포함.

지연 시간: 요청이 처리되길 기다리는 시간으로, 서비스를 기다리며 휴지(latent) 상태인 시간.

 

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

모든 요청에 동일한 시간이 걸려야 한다고 생각하는 상황에서 다양한 값을 얻게 된다.

백그라운드 프로세스의 context switch, 네트워크 패킷 손실과 TCP 재전송, garbage collection pause, 디스크에서 읽기를 강제하는 page fault, 서버 랙의 기계적인 진동이나 다른 여러 원인으로 추가 지연이 생길 수 있다.

 

산술 평균(arithmetic mean)은 얼마나 많은 사용자가 실제로 지연을 경험했는지 알려주지 않기 떄문에, 평균보다는 백분위(percentile)를 사용하는 편이 더 좋다. 응답 시간 목록을 가지고 가장 빠른 시간부터 제일 느린 시간까지 정렬하면 중간 지점이 중앙값(median)이 된다.

사용자가 여러 개의 요청을 보내면 최소한 하나의 요청이 중앙값보다 느릴 확률이 50%보다 훨씬 높다.

 

특이 값(outlier)이 얼마나 좋지 않은지 알아보려면 상위 백분위를 살펴보는 것도 좋다. 이때 사용하는 백분위는 95분위, 99분위, 99.9분위가 일반적이다. 예를 들어, 95분위 응답 시간이 1.5초라면 100개의 요청 중 95개는 1.5초 미만이고, 100개의 요청 중 5개는 1.5초보다 더 걸린다. (상위 백분위 응답 시간은 꼬리 지연 시간(tail latency)로 알려져 있음)

 

큐 대기 지연(queueing delay)은 높은 백분위에서 응답 시간의 상당 부분을 차지한다. 서버는 병렬로 소수의 작업만 처리할 수 있기 때문에 소수의 느린 요청 처리만으로도 후속 요청 처리가 지체된다. 이 현상을 선두 차단(head-of-line blocking)이라 한다. 서버에서 후속 요청이 빠르게 처리되더라도 이전 요청이 완료되길 기다리는 시간 때문에 클라이언트는 전체적으로 응답 시간이 느리다고 생각할 것이다. → 클라이언트 쪽 응답 시간 측정이 중요하다.

 

작은 비율의 백엔드 호출만 느려도 최종 사용자 요청이 여러 번 백엔드를 호출하면 느린 호출이 발생할 가능성이 증가하고, 그래서 최종 사용자 요청 중 많은 비율의 응답 시간이 결국 느려지는데, 이 효과를 꼬리 지연 증폭(tail latency amplification)이라 한다.

CPU와 메모리 비용을 최소로 하면서 좋은 백분위 근사치를 계산할 수 있는 알고리즘 : 포워드 디케이(forward decay), T 다이제스트 (t-digest), Hdr히스토그램(HdrHistogram) 등

 

부하 대응 접근 방식

용량 확장(scaling up) : 수직 확장(vertical scaling), 좀 더 강력한 장비로 이동

규모 확장(scaling out) : 수평 확장(horizontal scaling), 다수의 낮은 사양 장비에 부하를 분산

바공유 (shared-nothing) 아키텍쳐 : 다수의 장비에 부하를 분산하는 아키텍쳐

 

다수의 장비에 상태 비저장 (stateless) 서비스를 배포하는 일은 간단하지만, 단일 노드에 상태 유지(stateful) 데이터 시스템을 분산 설치하는 일은 아주 많은 복잡도가 추가로 발생한다.

 

[아키텍처를 결정하는 요소]

  • 읽기의 양
  • 쓰기의 양
  • 저장할 데이터의 양
  • 데이터의 복잡도
  • 응답 시간 요구 사항
  • 접근 패턴 등

 

유지보수성

주의를 기울여야 할 소프트웨어 시스템 설계 원칙

  • 운용성(operability) : 운영팀이 시스템을 원활하게 운영할 수 있게 쉽게 만들기
  • 단순성(simplicity) : 시스템에서 복잡도를 최대한 제거해 새로운 엔지니어가 시스템을 이해하기 쉽게 만들기
  • 발전성(evolvability) : 엔지니어가 이후에 시스템을 쉽게 변경할 수 있게 하기. 요구사항 변경 같은 예기치 않은 사용 사례를 적용하기 쉬워야 한다. 이 속성은 유연성(extensibility), 수정 가능성(modifiability), 적응성(plasticity)로 알려져 있다.

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

 

단순성: 복잡도 관리

복잡도는 다양한 증상으로 나타난다.

상태 공간의 급증, 모듈 간 강한 커플링, 복잡한 의존성, 일관성 없는 네이밍과 용어, 성능 문제 해결을 목표로 한 해킹, 임시방편으로 문제를 해결한 특수 사례 등이 이런 증상이다.

 

복잡도를 줄이면 소프트웨어 유지보수성이 크게 향상된다. 단순성이 구축하려는 시스템의 핵심 목표여야 한다.

 

우발적 복잡도(accidental complexity) : 소프트웨어가 풀어야 할 (사용자에게 보이는) 문제에 내재하고 있지 않고 구현에서만 발생하는 것

우발적 복잡도를 제거하기 위한 최상의 도구는 추상화다. 좋은 추상화는 깔끔하고 직관적인 외관 아래로 많은 세부 구현을 숨길 수 있고 재사용에 용이하다.

 

 

메모

  • 아마존은 응답시간이 100밀리초 증가하면 판매량이 1% 줄어들고 1초가 느려지면 고객의 만족도 지표는 16% 줄어드는 현상을 관찰했다. → 우리 회사 데이터 분석팀에서 하시는 업무들도 궁금해졌다.
  • 서비스 수준 협약서 같은 문서로 서비스 제공 시간을 명시하고 협약서를 지키지 못하면 고객이 환불을 요구할 수 있게 한다는 내용이 있었는데, 처음 알게 된 내용이라 신기했다.
  • 범용적이고 모든 상황에 맞는 확장 아키텍쳐는 없다. 각 애플리케이션에 특화되어있음.
  • 좋은 추상화에 대해 다시 한번 개념 서치해보고 읽어보기