Skip to content

juneyr.dev

[데이터중심어플리케이션설계] 1장. 신뢰-확장가능-유지보수 어플리케이션

6 min read

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

많은 어플리케이션이, 기존에 CPU 가 중요하던 계산 중심에서 데이터 중심으로 이동하고 있다. 이런 경우 대부분 중요한 문제는 데이터의 양, 복잡도, 데이터가 얼마나 빨리 변하는지이다.

일반적으로 이런 애플리케이션은 공통적인 요소로 구성을 하게 된다. 예를 들면

  • 데이터를 다시 찾을 수 있게 저장 (DB)
  • 읽기 속도 향상을 위해 값이 비싼 수행 결과를 기억 (캐시)
  • 키워드로 데이터 검색하거나 필터링 하게 제공 (검색색인)
  • 비동기 처리를 위해서 다른 프로세스로 메세지 전송(스트림 처리)
  • 주기적으로 대량의 누적된 데이터를 분석(배치 - 일괄처리)

와 같은 것들이다.

위와 같은 요소가 당연하게 여겨지긴 하겠지만.. 🤔 도구마다 사실 특징이 다르고, 우리는 적합한 도구를 찾아서 애플리케이션을 만들어야한다. 이 책에서는 다양한 도구의 공통점과 다른 점은 무엇이고 어떻게 구현해냈는지 알아본다.

일단, 이 장의 제목인 신뢰할 수 있고 확장가능하며 유지보수하기 쉬운 데이터시스템 을 구축하기 위한 기초적인 노력을 살펴보자.

데이터 시스템에 대한 생각

DB 랑 큐, 캐시는 서로 다른데 왜 이를 데이터 시스템이라는 포괄적 용어로 묶어야할까?

최근에 등장한 데이터 시스템 도구들을 생각해보자. 레디스는 메시지 큐를 데이터저장도구로 사용한다. 카프카는 DB 처럼 지속성을 보장하는 메시지 큐다. 이전에 DB와 큐, 캐시를 명확하게 나눴었지만, 그 분류가 흐려지고 있다.

또한, 현대로 올수록 애플리케이션 요구사항이 커졌다. 우리는 이제 DB 하나만으로 애플리케이션을 구성할 수가 없다. 작업을 작은 단위의 태스크로 나누고, 이 태스크를 가장 잘 수행하는 도구를 사용해야한다. 그리고 이 태스크를 코드로 연결한다.

이런 상황에서 개발자는 애플리케이션 개발자이면서, 데이터 시스템의 설계자이기도하다. 그럼 어떤 것이 좋은 데이터 시스템일까?

  • 신뢰성 시스템은 소프트웨어결함, 하드웨어결함, 인적 오류같은 일이 생겨도 지속적으로 올바르게 동작해야한다.
  • 확장성 시스템의 복잡도가 증가할때 이를 처리할 적절한 방법이 있어야한다.
  • 유지보수성 시간이 지남에 따라 다양한 사람들이 시스템 상에서 작업할 것이기때문에, 모든 사용자가 시스템 상에서 생산적으로 작업해야한다.

신뢰성

소프트웨어가 신뢰할 수 있다는 건 무얼 말할까? 일반적으로 우리는 이 점을 떠올린다.

  • 애플리케이션은 기대한 기능을 수행한다.
  • 시스템은 예상치 못한 사용에도 대처할 수 있다.
  • 성능은 예상된 부하와 데이터 양에서 충분한 사용례를 만족한다.
  • 시스템은 허가되지 않은 접근과 오남용을 방지한다.

무언가 잘못되더라도 지속적으로 올바르게 동작하는 것이 신뢰할 수 있는 소프트웨어다.

잘못될 수 있는 일을 결함 fault 이라고 부른다. 결함을 예측하고 대처할 수 있는 시스템을 탄력성을 지녔다고 이야기한다. 물론 모든 유형의 결함에 대처하는 시스템은 존재할 수 없으므로, 여기서 말하는 결함은 특정 유형을 이야기한다.

결함 fault (사양에서 벗어난 시스템의 요소)!= 장애 failure (시스템이 전체가 멈춘 경우)

하드웨어 결함

하드웨어 결함이 바로 결함의 유형 중 하나다. 하드디스크가 고장나고, 램에 결함이 생기고, 정전이 발생하고 ...

첫번째 대응책 첫번째 대응책으로 각 하드웨어 구성요소에 중복을 추가하는 방법이 있다.

  • 디스크는 RAID 구성으로
  • 서버는 이중전원 디바이스
  • hot-swap이 되는 CPU

위와 같은 방식에서, 하나의 요소가 죽으면 복구/교체 되는 동안 중복된 구성 요소를 대신 사용할 수 있다.

최근까지 단일 장비의 전체 장애는 드물었어서, 대부분의 애플리케이션은 하드웨어 구성 요소의 중복으로 충분했다. (하나의 DB replica 등).

그러나 데이터의 양과 계산 요구가 늘어나면서 더 많은 장비를 사용하게 되었다. 따라서 하드웨어 결함율도 함께 증가했다. AWS와 같은 클라우드 플랫폼은 가상 인스턴스가 예고없이 증가하는 경우도 있다. 유연성과 탄력성이 단일 장비의 신뢰성보다 우선적으로 설계되었기 때문이다.

따라서 하드웨어 자체의 중복성을 추가하거나 소프트웨어 내결함성 기술을 사용하는 시스템으로 점점 옮겨가는 추세다.

소프트웨어 오류

또다른 종류의 결함으로 시스템 내 체계적오류가 있다.

  • 리눅스 커널 버그로, 2012년 6월 30일 윤초
  • CPU 시간 / 메모리 / 디스클처럼 공유자원을 과도하게 사용하는 일부 프로세스
  • 시스템의 속도가 느려져 반응이 없거나 잘못된 응답을 반환하는 서비스
  • 하나 요소의 결함이 다른 구성 요소의 결함의 원인이 되고 ... > 연쇄 장애.

이런 버그는 특정 상황에 의해 발생하기 전까지 오랫동안 나타나지 않는다. 이런 오류 문제는 신속한 해결책이 없다.

  • 시스템의 가정과 상호작용에 대해서 주의깊게 생각하기
  • 빈틈없는 테스트
  • 프로세스 격리
  • 모니터링
  • 분석하기

등이 도움을 줄 수 있을 뿐이다.

인적 오류

사람은 못미덥다. 어떻게 인적 오류를 최소화할 수 있을까?

  • 오류의 가능성을 최소화하는 방향으로 시스템을 설계하라. 잘 설계된 추상화, API, 관리 인터페이스를 사용하자. 옳은 일은 쉽게하고 잘못된 일은 막을 수 있다.
  • 실제 데이터를 볼 수 있지만 사용자에게는 영향이 없는 비-production 환경의 샌드박스를 제공하라.
  • 단위 테스트, 전체 시스템 통합 테스트, 수동 테스트까지 모든 수준에서 철저히 테스트하라.
  • 설정 변경은 빠르게 롤백 가능하게, 새로운 코드는 서서히 롤 아웃하게.
  • 성능 지표와 오류 rate 같은 모니터링 대책을 마련하라.
  • 조작 교육과 실습을 시행하라.

신뢰성은 얼마나 중요할까?

모든 애플리케이션은 안정적으로 작동해야한다. 발전소, 항공 교통 관제 애플리케이션 부터 비즈니스 애플리케이션까지. 모두 정상적으로 작동하지않을 경우 많은 비용을 들이게 된다.

확장성

성능 저하를 유발하는 흔한 이유 중 하나가 부하 증가다. 확장성은 증가한 부하에 대처하는 시스템 능력을 의미한다. 확장성은 0 or 1 이 아니다. 확장 가능하다 혹은 없다 라는 말은 맞지 않다. 확장성을 논하는 것은 시스템이 커지면 이를 대처하기 위해 어떻게 해야하는지. 계산 자원은 어떻게 투입할지에 대한 고려하는 것에 가깝다.

부하 기술하기

확장성을 논하기 위해서는 현재의 부하를 기술할 수 있어야한다. 그래야 '부하가 두배가 되면 어떡하지?' 라는 질문에 답할 수 있다. 부하는 부하 매개변수라고 부르는 몇개의 숫자로 나타낼 수 있다. 시스템 설계에 따라 적합한 매개변수가 달라진다.

  • 웹 서버의 초당 요청 수
  • DB의 read:write 비율
  • 대화방의 active user
  • ...

트위터의 예를 들어보자. 트위터의 주요 두가지 동작은 다음과 같다.

  • 트윗 작성
    • 팔로워에게 새로운 메시지를 게시. (평균 초당 4.6k 요청. 피크일때 초당 12k 이상)
  • 홈 타임라인 (read)
    • 팔로우한 사람이 작성한 트윗을 볼 수 있다.(평균 초당 300k요청)

트윗 작성의 초당 12k 쓰기는 상당히 쉽다. 그러나 어려운 점은.. 팔로워들 모두의 타임라인에 이를 꽂아줘야한다(fan-out) 는 점이다. 크게 두가지 구현 방법이 있다.

  1. 트윗 작성은 새로운 트윗을 전역 컬렉션에 삽입한다. 홈 타임라인 read를 요청하면 팔로우하는 모든 사람을 찾고, 그 사람들의 모든 트윗을 찾아 시간순 정렬해서 merge.
  2. 각 사용자용 홈 타임라인 캐시를 유지한다. 사용자가 트윗을 작성하면 해당 사용자의 팔로워를 찾아서 캐시에 새로운 트윗을 삽입한다. (홈 타임라인 캐시의 읽기 는 미리 계산해서 비용이 저렴)

트위터는 처음엔 1로 접근했다. 그러나 시스템이 홈 타임라인 질의 부하를 견뎌내기 힘들어 2로 전환했다. (평균적으로 트윗 게시 < 홈 타임라인 요청량 이므로 2가 더 잘 동작한다.) 그러나 2의 단점은 트윗 작성 자체가 많은 부가작업을 필요로 한다는 점이다. 평균 75명의 팔로워에게 트윗이 전달되므로 초당 4.6k 트윗은 캐시에 345k 쓰는 작업이 될 수도 있다. 적시에 트윗을 전송하는게 중요한 과제가 된다.

성능 기술하기

위에서 부하를 기술하는 방법을 알았다. 이제 성능을 어떻게 측정할 건지도 정해야한다.

  • 부하를 늘리고 자원은 그대로하면 성능은 어떻게 변할까?
  • 부하를 증가했을 때 성능이 유지되게 하려면 자원은 얼마나 늘려야할까?

위 두 질문에 대답을 해야하니까.

성능은 어떤 지표를 쓸까?

  • 하둡과 같은 일괄 처리 시스템은 처리량 (초당 처리할 수 있는 레코드 수) 를 성능 지표로 쓴다.
  • 온라인 시스템에서는 서비스 응답시간을 지표로 쓴다.
    • 유저의 요청때마다 응답시간은 다르므로, 응답시간은 단일 상수가 아니라 분포에 가깝다.
    • 이 분포에서 평균값은 대개 outlier 까지 반영한 오류일 확률이 높으므로,
    • 중앙값인 median 을 사용하는 것이 맞다.
    • 사용자가 보통 얼마나 오랫동안 기다려야하는 지 알 수 있다면 중앙값이 좋은 지표다.
    • 값이 얼마나 좋지 않은지 알아보려면 상위 백분위를 알아보는 것도 좋다.
    • 상위 95퍼센트가 1.5초라면, 100개 요청 중 95개는 1.5초 미만이고, 5개는 1.5초 이상이다.
    • 99.9 분위는 아마존에서도 사용하는 지표다. 1000명 중에 가장 느린 1명은 중요한 고객일 수 있기때문.
    • 이렇게 백분위는 서비스 수준 목표 / 서비스 수준 협약서에 많이 사용한다.

성능을 테스트할 때는 클라이언트 관점에서 테스트해야한다.

  • 요청은 이전 요청의 응답이 오기 전에 다시 보내자.
  • 내부적으로 테스트하지말고 클라이언트 관점에서 테스트한다. 빠진 요소가 있다면 해당 요소의 지연을 못잡을 수 있다.

부하 대응 접근 방식

사람들은 확장성과 관련해서

  • 용량 확장 / 수직확장 (scaling up)
    • 장비를 고사양으로 이동
  • 규모 확장 / 수평확장 (scaling out)
    • 다수의 사양 낮은 장비 을 구분지어 이야기하곤한다.

적절한 사양의 장비 몇대가 다량의 사양 낮은 장비보다 간단하고 저렴하다. 그러므로 항상 이를 실용적으로 조합해서 사용해야한다.

일부 시스템은 탄력적이다. (AWS 를 생각해보자.) 즉, 부하증가를 감지하면 컴퓨팅 자원을 자동으로 추가할 수 있다. 반면 일부 시스템은 수동으로 확장해야한다. 탄력적인 시스템은 부하를 예측 못할 만큼 높을때 유용하지만, 수동 확장이 더 간단하고 운영상의 예상치 못한 일이 적다

다수 장비에 stateless 애플리케이션을 배포하는 건 비교적 쉽다. 하지만 상태저장이 있다면 분산 설치하는 일이 복잡도가 높아진다. 이런 이유로 최근까지도 고가용성 요구가 있기전까지는 DB를 하나의 노드에 유지하는 것이 통념이다.

분산 시스템을 위한 도구가 좋아지면서 이 통념이 조금씩 바뀌었다. 나중에는 분산시스템이 기본 아키텍처가 될 수도!

시스템의 아키텍처는 해당 시스템을 사용하는 애플리케이션에 특화되어있다. 예를 들어 1kB 요청을 초당 1B 처리하는 시스템과 2GB 요청을 분당 3건 처리하는 시스템은 같은 데이터 처리량이라고 해도 매우 다르다. 이 경우 아키텍처의 주요 동작이 무엇이고 비주요 동작은 무엇인지 아는 것이 필요하다. 이 가정이 잘못되면 확장에 대한 엔지니어링 노력이 잘못되기 마련이다.

유지보수성

유지보수는 필연적이다. 그러나 많은 사람들은 소위 레거시 시스템 유지보수를 좋아하지않는다. 희망적인 점은, 유지보수 중 고통을 최소화하고 레거시를 만들지 않도록 설계할 수 있다는 점이다. 그러기 위해 지켜야할 원칙은 세가지다.

  • 운용성 운영팀이 시스템을 원활하게 운영할 수 있도록 쉽게 만들어라.
  • 단순성 시스템 복잡도를 줄여서 새로운 엔지니어가 시스템을 이해하기 쉽게 만들어라.
  • 발전성 엔지니어가 이후에 쉽게 시스템을 변경할 수 있게 만들어라.

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

시스템이 원활하게 작동하려면 운영팀이 필수다. 운영팀은 대개 다음과 같은 일을 한다.

  • 시스템 상태를 모니터링하고 서비스를 복원
  • 시스템 장애, 성능 저하들의 원인을 추적
  • 보안패치
  • 문제를 예측해 미리 해결
  • 배포, 설정 관리등을 위한 모범 사례와 도구 마련
  • ...

좋은 운영성이란 동일 반복 태스크를 쉽게 수행하게끔 만든다는 점이다.

  • 좋은 모니터링으로 내부 시스템에 대한 가시성 제공
  • 표준 도구를 이용해 자동화와 통합
  • 좋은 문서
  • 개별 장비 의존성을 회피.

단순성: 복잡도 관리

복잡도 수렁에 바진 소프트웨어를 진흙덩어리로 묘사하곤한다. 복잡성은 다양한 증상으로 나타난다.

  • 모듈 간 강한 결합
  • 상태 공간의 급증
  • 일관성 없는 네이밍과 용어
  • 성능 문제 해결이나 임시 방편인 특수 사례

복잡도 때문에 시스템 유지보수가 어려우면, 예산과 일정이 초과된다. 때문에 복잡도는 반드시 줄이고 단순화해야한다. 그러나 이 말이 기능을 줄인다는 의미는 아니다. 오히려 우발적 복잡도 (소프트웨어가 풀 문제에 있는 것이 아니고, 구현에서 발생하는 복잡도)를 잡는다는 의미에 가깝다.

우발적 복잡도를 잡는 가장 최상의 도구는 추상화다. 좋은 추상화는 깔끔하고, 직관적이며 많은 세부 구현을 숨길 수 있다. 예를 들어 고수준 PL 은 기계 언어, CPU 레지스터 ... 등을 숨긴 추상화다. 우리는 고수준 언어로 프로그래밍해도 기계어를 사용한다. 단지 직접하지 않을 뿐이다.

좋은 추상화를 찾기는 어렵다. 이제 책 전반에 걸쳐 좋은 추상화를 살펴보자.

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

시스템의 요구사항은 끊임없이 변하기 마련이다. 조직 프로세스 측면에서 agile은 변화에 적응하기 위한 프레임 워크를 제공한다. 그러나 agile에 대한 설명은 대부분 작은 규모에 초점을 맞추고 있다. 이 책에서는 대규모 수준에서 민첩성을 높이는 방법을 찾는다.

이 발전성이라는 개념은 시스템의 간단함과 추상화와 깊은 관련이 있기도하다.

정리

안타깝게도 애플리케이션을 신뢰할 수 있고 확장 가능하며 유지보수하기 쉬운 애플리케이션으로 만들어주는 왕도는 없다. 그러나 여러 애플리케이션에서 재현되는 패턴과 기술이 있다. 이제 시스템 패턴을 함께 살펴보자.

참고

O'REILLY, 데이터 중심 애플리케이션 설계, 마틴 클레프만