STUDY/Data Engineering

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

wonpick 2024. 1. 7. 11:16

데이터 중심 어플레케이션 설계 스터디를 진행하며 작성한 글 입니다. 


1. 데이터 중심 애플리케이션

  • 많은 애플리케이션은 계산 중심(compute-intensive) 보다 데이터 중심(data-intensive) 이다.
  • CPU성능은 애플리케이션을 제한하는 요소가 아니며 더 큰 문제는 데이터의 양, 복잡도, 변화 속도가 더 큰 문제이다

1) 데이터 시스템

데이터 시스템의 기초
관련자의 기술 숙련도, 기존 시스템의 의존성, 전달 시간 척도, 다양한 종류의 위험에 대한 조직의 내성, 규제 제약 등은 시스템 설계에 영향을 줄 수 있는 많은 요소이다.
(대부분의 소프트웨어 시스템이 중요하게 여기는 3가지 : 신뢰성, 확장성, 유지보수성)

1.내부적으로 문제가 있어도 데이터를 정확하고 완전하게 유지하려면 어떻게 해야 할까?
2.시스템의 일부 성능이 저하되더라도 클라이언트에 일관되게 좋은 성능을 어떻게 제공할 수 있을까?
3.부하 증가를 다루기 위해 어떻게 규모를 확장할까?
4.서비스를 위해 좋은 API는 어떤 모습일까?
  • 다양한 사용사례에 최적화된 데이터 도구(레디스, 카프카)가 있으나 분류 간 경계가 흐려지고 있다.
  • 점점 더 많은 애플리케이션이 과도한 요구사항을 가지고 있어 단일 도구로는 만족할 수 없다. 따라서 하나의 작업(Work)을 단일 도구에서 수행할 수 있는 Task로 나누고, 이 도구들은 애플리케이션 코드를 이용해 연결되어있다.
  • 서비스 제공을 위해 각 도구를 결합할 때 서비스 인터페이스나 애플리케이션 프로그래밍 인터페이스(API)는 보통 클라이언트가 모르게 구현 세부 사항을 숨긴다.
  • 개발자는 애플리케이션을 개발할 뿐 아니라, 데이터 시스템 설계자이기도 하다.

2. 신뢰성

: 대략 “무언가 잘못되더라도 지속적으로 올바르게 도착함”을 신뢰성의 의미로 이해할 수 있다.

  • 신뢰성이 왜 중요해? 신뢰성은 원자력 발전소나 항공 교통 관제 SW만을 위한게 아니다. 중요하지 않은 애플리케이션에서도 사용자에 대한 책임이 있다. 증명되지 않은 시장을 위해 시제품을 개발하는 비용이나 매우 작은 이익률의 서비스를 운영하는 비용을 줄이려 신뢰성을 희생 해야하는 상황이 있다. 하지만 이경우에 비용을 줄여야하는 시점을 매우 잘 알고 있어야한다.

1) 결함(falut)

  • 결함을 예측하고 대처하는 시스템: 내결함성(fault-tolerant) 또는 탄력성(tresilient)
  • 결함(사양에서 벗어난 시스템의 한 구성요소) != 장애(사용자에게 필요한 서비스를 제공하지 못하고, 시스템이 멈춘 경우)
  • 결함으로 인해 장애가 발생하지 않게 내결함성 구조를 설계해야 함.
    → 이 책에서는 신뢰할 수 없는 여러 부품으로 신뢰할 수 있는 시스템 구축하는 다양한 기법을 다룬다.
    • 카오스 몽키(넷플릭스): 고의적으로 결함을 일으켜 내결함성 훈련 (보안 같은 경우 해결책보다 예방이 중요, 해결책이 없는 경우도 있기 때문에)

2) 결함의 종류

  • 하드웨어 결함
    • 1개의 하드 디스크의 평생 평균 장애시간(mean time to failure, MTTF) :
      10~50년 → 10000개의 디스크로 구성된 클러스터는 평균 하루에 1개 죽는다고 봐야함.
    • 대응 방안
      • 하드웨어 구성에 중복(redundancy) 을 추가
        디스크 *RAID 구성, 서버 이중 전원 디바이스 와 *hot-swap 이 가능한 CPU, 데이터 센터는 예비 전원용 디젤 발전기

      • 과거에는 다중 장비 중복은 고가용성(high availability)이 절대적인 소수의 애플리케이션에만 해당되었으나, 데이터의 양이 늘어나면서 단일 장비 신뢰성보다 유연성과, 탄력성을 우선적으로 처리하게끔 설계되는 경우가 많다.
        예시) AWS 같은 클라우드 플랫폼은 가상 장비 인스턴스의 단일 장비 신뢰성보다 유연성과 탄력성을 우선적으로 처리하게 설계됨.

  • 소프트웨어 오류
    • HW오류는 무작위적이고 독립적이다 (약한 상관관계 : 서버 랙의 온도 같은 공통 원인)
    • 하지만 SW 오류는 예상하기 어렵고, 노드 간 상관관계 때문에 HW보다 시스템 내 체계적 오류(systematic error)를 더욱 더 유발하는 경향이 있다.
      • 잘못된 입력이 있을 때 SW인스턴스가 죽는 버그
      • CPU 시간, 메모리, 디스크 공간, 네트워크 대역폭처럼 공유 자원을 과도하게 사용하는 프로세스
      • 시스템의 속도가 느려져 반응이 없거나, 잘못된 응답을 반환하는 서비스
      • 연쇄 장애(cascading failure)
  • 인적 오류
    • 운영자의 설정 오류가 서비스 중단의 주요 원인이다. (대규모 인터넷 서비스에 대한 연구에 따르면)
      • 잘 설계된 추상화 API, 인터페이스를 사용하라. 하지만 인터페이스가 지나치게 제한적이면 좋은 점은 잊은채 제한된 인터페이스를 피해 작업하게 된다.→ 균형 맞추기가 중요
      • 실수가 잦게 발생하는 곳은 실제 데이터로 안전하게 실험해볼 수 있는 sandbox(사용자에게 영향이 없는 비프로덕션)를 제공하라 (ex. stage환경)
      • 단위테스트 , 통합테스트, 수동테스트를 철저히 해라. 특히 정상적인 동작에서는 거의 발생하지 않는 코너케이스(corner case)에 유용하다.
      • 인적 오류를 빠르고 쉽게 복구할 수 있도록 하라,. 설정 변경 내역 빠르게 roll back하고 새로운 코드를 서서히 roll out 하게 만들어라. (일부 사용자에게만 영향이 미치게함)
      • 모니터링이 중요하다. 원격 측정(telemery, 로켓이 발사되면 원격 측정은 일어나는 일을 추적하고 장애 해석을 위해 필수적), 지표(metric)는 문제 분석에 매우 중요

3. 확장성

: 증가한 부하에 대처하는 시스템 능력을 설명하는데 사용하는 용어 

(시스템이 특정 방식으로 커지면 대처하기 위한 선택지는 무엇인가? 추가 부하를 다루기위해 계산 자원을 어떻게 투입할까?)

1) 부하 기술하기

시스템의 현재 부하를 간결하게 기술하는게 중요 (따라서 부하 성장 질문을 논의할 수 있기 때문)

  • 부하매개변수(load parameter): 웹서버의 초당 요청 수, 데이터베이스의 읽기 대 쓰기 비율, 대화방의 동시 활성 사용자, 캐시 적중률 등 → 평균이 중요할 수도 있고 소수의 극단적인 경우가 병목 현상을 유발할 수도 있음.

예시) 트위터의 fan-out(액션 전파) 문제

  • 트위터의 특징
    • 트위터는 조회 요청이 쓰기 요청 대비 25배 정도 많다.
    • 누군가를 팔로우 한것으로 사용자의 모든 액션이 전파(fan out)된다.

1. 구현방식 1 (읽을 때 > 쓸 때)
트윗 작성은 간단히 새로운 트윗을 트윗 전역 컬렉션에 삽입하고 사용자가 홈 타임라인을 요청하면 팔로우하는 모든 사람의 모든 트윗을 찾아 시간순으로 정렬해서 합친다.

 # 홈 타임라인 조회 SQL
 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

2. 구현 방식 2(읽을 때 < 쓸 때) 수신 사용자용 트윗 우편함처럼 개별 사용자의 홈 타임라인 캐시를 유지하고 사용자가 트윗을 작성하면 해당 사용자를 팔로우하는 사람을 모두 찾아 각자의 캐시에 새로운 트윗을 삽입한다. 그러면 타임라인 읽기 요청은 결과를 미리 계산해서 비용이 저렴하다.

 

  • 주요 기능
    • 트윗 작성(Write Operation): 평균 초당 4,600개의 요청, 피크일땐 12,000개 이상
    • 홈 타임라인(Read operation): 팔로우한 사람이 작성한 트윗 보기(초당 300,000 요청)
  • fan out 문제 해결 방식
    • hybird 방식 (사용자 당 팔로워의 분포)핵심 부하 매개변수) 대부분의 사용자들의 트윗은 계속해서 사람들이 작성할 때 홈 타임라인에 펼쳐지지만 팔로워수가 매우 많은 인플루언서들은 팬 아웃에서 제외 되는 방식
      • 일반 유저(A)의 트윗 포스트 : 포스트 희생시키고 캐시 서버 관리하는 방식 A가 포스팅 → tweet테이블에 A의 트윗기록 추가 → A 팔로우하는 모든 유저 확인 → 해당 유저의 피드캐시 테이블에 트윗내용 캐싱
      • 인플루언서(B)의 트윗 포스트 : 접근 방식 1처럼 읽는 시점에 사용자의 홈 타임라인에 합쳐지는 방식 B가 포스팅 → tweet테이블에 B의 트윗기록 추가 → B 팔로우하는 C 유저가 피드 요청 → C가 팔로우하는 인플루언서 확인 → 인플루언서들의 트윗 합쳐서 반환

2) 성능 기술하기

시스템 부하 기술의 장점 : 부하 증가 시 어떤 일이 일어나는지 조사 가능

아래 질문 2가지는 모두 성능 수치가 필요함

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

시스템 성능에 대해서 간단히 살펴보자

  • 하둡(일괄 처리 시스템):
    처리량(throughput): 초당 처리할 수 있는 레코드 수나 일정 크기의 데이터 집합으로 작업 수행 시 걸리는 전체 시간
  • 온라인 시스템:
    응답 시간(response time): 클라이언트 관점에서 본 시간으로, 요청을 처리하는 실제 시간 외에도 네트워크 및 큐 지연도 포함
    지연시간(latency): 요청이 처리되길 기다리는 시간, 서비스를 기다리며 휴지(latent) 상태인 시간을 말함
    • 응답시간을 측정 가능한 값의 분포로 생각해야 함. 전형적인 응답시간을 알고싶다면백분위를 사용해라, 산술 평균은 의미가 없기 때문에 사용하지 않음
      • 응답시간 측정에서 백분위 활용
        예시)
        백분위는 서비스 수준 목표(service level objective,SLO)와 서비스 수준 협약서(service level agreemet, SLA)에 자주 사용하고 기대 서능과 서비스 가용성을 정의하는 계약서에 자주 등장함. 이런 협약서에 “응답 시간 중앙값이 200밀리초 미만이고 99분위가 1초 미만인 경우를(응답 시간이 길면 서비스가 종료될 수 있다.) 정상 서비스 상태로 간주하며 서비스 제공 시간은 99.9% 이상이어야한다”라는 지표는 서비스 클라이언트의 기대치를 설정해 이 협약을 지키지 못하면 고객이 환불을 요구할 수 있게 된다.
      • 클라이언트 쪽 응답 처리 시간이 중요한 이유?
        높은 백분위에서 보통 큐 대기 지연(queueing delay)이 응답 시간의 상당 부분을 차지함. 서버는 병렬로 수수 작업 처리만 가능하므로 소수의 느린 요청이 후속 처리를 지체 시키게 할 수 있다.(선두 차단 현상, head of line blocking) 따라서 클라이언트는 전체적으로 응답 시간이 느리다고 생각할 것이고 이런 문제 때문에 응답시간 측정이 매우 중요하다.
    • 특이 값(outlier) 발생 이유
      • 백그라운드 프로세스의 컨텍스트 스위치(context switch), 네트워크 패킷 소닛과 TCP 재전송, 가비지 컬렉션 휴지(garbage collection pause), 디스크 읽기 강제하는 페이지 폴트(page fault), 서버 렉의 기계적인 진동 등의 원인 때문
    • 사용자가 보통 얼마나 오랫동안 기다려야하는 지 알고 싶다면 중앙값이 좋은 지표이다.
      • 사용자 요청의 절반은 중앙값 응답 시간 미만, 나머지 반은 중앙값보다 오래걸린다는 의미 중앙값은 50분위, 50p로 기술 → 최소한 하나의 요청이 중앙값보다 느릴 확률이 50%보다 훨씬 높다.
    • 특이 값이 얼마나 좋은지 안좋은지 알기 위해 95분위, 99분위, 99.9분위 확인 (p95,p99,p999)
      • 요청의 p95가 1.5초라면 100개의 요청 중 95개는 1.5초 미만이고, 100개의 요청 중 5개는 1.5초보다 더 걸린다.)
    • 꼬리 지연시간(tail latency): 상위 백분위 응답 시간
      • 아마존의 경우 내부 서비스 응답 요구시간을 99.9분위로 기술 ( 99.9분위는 요청 1000개중 1개만 영향이 있음에도 )
        • 이유? 보통 응답시간이 가장 느린 요청을 가진 고객은 구매를 많이해서 계정에 가장 많은 데이터를 갖고 있기 때문
        • 반면 99.99 분위를 최적화 할 때는 비용이 많이 들어 이익을 가져다 주지 못함
  • 확장성 테스트 시 (인위적으로 부하 생성)
    • 응답 시간과 독립적으로 요청을 지속적으로 보내야함. (클라이언트가 다음 요청을 보내기전에 이전 요청이 완료되길 기다리면 테스트에서 인위적으로 대기 시간을 실제보다 더 짧게 만들어 평가 왜곡함.)
실전 백분위 확인

상위 백분위는 백엔드 서버에서 특히 중요함.
(병렬로 호출해도 최종 사용자 요청은 여전히 병렬 호출 중 가장 느린 호출이 완료되길 기다려야하기 때문
→ 꼬리 지연 증폭(tail latency amplification))
응답시간 백분위 지표 확인
- 단순구현: 시간 내 모든 요청의 응답 시간 목록을 유지하고 1분마다 목록 정렬
- CPU 메모리 비용 최소할 수 있는 백분위 근사치 계산 알고리즘 : 포워드 디케이,T다이제스트, Hdr히스토그램
💡 이전까지는 성능 측정을 위한 부하 지표를 기술하는 매개변수에 대해 설명했고, 이후는 실질적인 확장성 논의

3) 부하 대응 접근 방식

  • 아래 두가지 경우에서 실용적인 조합이 필요
    용량확장(scaling up) = 수직확장(vertical scaling) → 좀 더 강력한 장비로 이동
    규모확장(scaling out) = 수평확장(horizontal scling) → 다수의 낮은 사양장비에 부하를 분산
    (다수의 장비에 분하 분산 → 비공유 아키텍쳐, shared nothing)
    • 현실적으로 좋은 아키텍쳐의 조합 추천 방식 : 적절한 사양의 장비 몇대 > 값 싼 다수의 가상 장비
  • 탄력적(elastic) : 부하 증가를 감지하면 자동으로 자원 추가
    • 하지만 오히려 수동 확장이 더 간단하고 예상치 못한 운영이슈가 적다. (209쪽 파티션 재균형화 참고)
  • 다수의 장비에 상태 비저장(stateless) 서비스 배포 방법은 간단하다. 하지만 단일 노드에 상태 유지(stateful) 데이터 시스템을 분산설치하는 일은 복잡도가 크다.
    → 최근까지 통념 : 고가용성 요구(확장 비용이나 분산시스템 제작이 필요한 경우)가 있을 때 단일 노드에 데이터베이스를 유지(용량 확장) 했음.
    → 일부 애플리케이션 : 분산 시스템을 위한 도구와 추상화가 좋아지면서 용량 확장이 아닌 분산으로 데이터 시스템을 유지함.
    → 향후 : 분산 데이터 시스템 자체가 기본 아키텍처가 될 수 있음.
  • 대규모로 동작하는 시스템의 아키텍처는 해당 시스템을 사용하는 애플리케이션에 특화되어있다.
    • 범용적이고 모든 상황에 맞는 (one size fits all) 확장 아키텍처는 없다.
    • 아키텍처를 결정하는 요소 : 읽기양, 쓰기양, 저장할 데이터 양, 데이터 복잡도, 응답시간 요구사항, 접근 패턴 등
    • 예를 들어, 각 크기 1KB 초당 100,000건 요청/2GB 분당 3건 요청 은 서로 같은 데이터 처리량이지만 설계한 시스템 자체는 매우 다르다.
    • 특정 애플리케이션에 적합한 확장성을 갖춘 아키텍처
      • 주요 동작이 무엇이고 아닌 동작이 무엇인지에 대한 가정(부하 매개변수)
      • 부하 매개변수를 기준으로 아키텍처를 구축함. 잘못되면 역효과를 낳기 때문. → 스타트업 초기단계에는 미래를 가정한 부하에 대비해 확장하기 보다는 빠르게 반복해서 제품 기능을 개선하는 것이 좀 더 중요하다.
      • 하지만! 확장성을 갖춘 아키텍처가 특정 애플리케이션에 특화되었더라도, 아키텍처는 보통 범용적인 구성 요소로 구축됨.

4. 유지보수성

SW비용은 대부분 초기 개발이 아니라 지속해서 이어지는 유지보수에 들어간다.

  • 버그 수정, 시스템 운영 유지, 장애 조사, 새로운 플랫폼 적응, 기술 채무 상환(technical debt), 새로운 기능 추가

많은 사람들은 레거시 시스템을 유지보수하는 것을 좋아하지 않는다. 따라서 유지보수 고통을 최소하기 위해 소프트 웨어 시스템 설계 원칙을 지키는게 중요하다.

  • 유지보수성 원칙
    • 운용성 : 운영팀이 원활하게 운영할 수 있도록 쉽게 만들어라
    • 단순성 : 복잡도를 최대한 제거해 새로운 팀원도 잘 이해할 수 있게 만들어라
    • 발전성 : 이후 변경에 열려있어야 한다. 요구사항 변경같은 새로운 사용사례를 쉽게 적용할 수 있어야한다.
      • 유연성, 수정가능성, 적응성

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

  • 좋은 운영은 나쁜(불완전한) SW 제약을 피하는 대안이 된다. 좋은 SW일지라도 운영이 안되면 작동 신뢰가 불가하다.
  • 좋은 운영성이란?
    • 런타임 동작과 시스템 내부에 대한 가시성 제공
    • 개별 장비 의존성을 회피, 장비를 내리더라도 시스템 전체에 영향을 주지 않아야 함
    • 이해하기 쉬운 운영모델을 제공: X를 하면 Y가 발생
    • 어드민 등으로 기본값을 재정의할 수 있는 권한 부여
    • 자기 회복이 가능할 뿐아니라 관리자가 상태를 수동으로 제어할 수 있어야 함
  • 동일하게 반복되는 태스크를 쉽게 수행하게끔 만들어 고부가가치 활동에 노력 집중

2) 단순성 : 복잡도 관리

  • 커다란 진흙 덩어리(big ball of mud) : 복잡도 수렁에 빠진 SW 프로젝트
    • 상태 공간의 급증, 모듈 간 강한 커플링(tight couplig), 복잡한 의존성, 일관성 없는 명명(naming), 성능 문제를 목표로 한 해킹, 임시 방편으로 해결한 특수 사례(special-casing) 등
  • 우발적 복잡도(accidental complexity) 줄이기 (시스템을 단순하게 만드는 일이 반드시 기능을 줄인다는 의미는 아니다.우발적 복잡도를 줄인다는 의미)
    • 추상화: 깔끔하고 직관적인 외관 아래로 세부 구현을 숨기는 것
      • 예시) 고수준 프로그래밍 언어는 기계어(프로그래밍 언어 추상화 덕에 기계어 생각 안해도 됨), CPU 레지스터, 시스템 호출 등을 숨긴 추상화, SQL은 디스크에 기록하고 메모리에 저장한 복잡한 데이터 구조와 다른 클라이언트의 동시 요청과 고장 후 불일치를 숨긴 추상화. → 좋은 추상화를 찾기는 어려움

3) 발전성 (민첩성) : 변화를 쉽게 만들기

  • 시스템 요구사항은 계속 바뀐다. 비즈니스 우선순위 바뀌고, 사용자가 새로운 기능 요청, 시스템의 성장으로 인한 아키텍처 변화 등등으로 인해
  • 애자일(agile) 작업 패턴은 변화에 적응하기 위한 프로세스이다.
    • 테스트 주도 개발(TDD,test driven development), 리팩토링(refactoring) 등
    • 보통 애자일 기법에 대한 설명은 매우 작고, 로컬 규모에 초점이 맞춰저 있으나, 이 책에서는 대규모 데이터 시스템 수준에서 민첩성(=발전성)을 높이는 방법을 찾음
      • 예시) 이전 트위터 fan-out 의 경우 접근 방식 1에서 접근 방식 2로 “리팩토링”하는 방법을 찾았던 것.

참고

  1. 트위터 시스템 디자인 실험기 : 현구막 기술 블로그