본문 바로가기
App 설계

DDIA - 신뢰성,확장성,유지보수성 - 확장성

by ocwokocw 2021. 10. 19.

- 이 글은 마틴 클레프만의 데이터 중심 애플리케이션 설계를 기반으로 작성되었습니다.

- 개요

어떤 시스템이 현재 안정적이라고 해서 미래에도 안정적이라는 보장은 없다. 이런 경우 보통 성능이 달라져서 문제가 되는데 성능이 달라지는 주요 Issue 는 부하가 증가되는것이다. 확장성이라는 것은 증가되는 부하에 대처하는 시스템의 능력이라고 할 수 있다.

 

프로젝트를 진행하다보면 설계자나 PM 들은 흔히 'A 시스템(모듈)은 확장 가능하다.', 'B 시스템(모듈)은 확장성이 없다.' 와 같은 말을 많이 한다. 하지만 이런 말들은 확장성을 논하는데에 아무런 도움이 되지 않는다. 확장성을 논할때에는 '시스템이 특정 방향으로 커지면 이에 대처하기 위한 선택은 무엇인가?' 와 같은 사고방식으로 접근해야 한다.


- 부하기술

시스템의 현재 부하를 간결하게 기술해야 부하성능질문(ex - 부하가 두 배로 되면 어떻게 될까?)을 논의할 수 있다. 부하는 부하매개변수로 나타내며 시스템 설계에 따라 달라진다.

  • 웹서버가 수신하는 초당 요청 수
  • DB의 Read/Write 비율
  • 대화방 동시 활성자
  • 캐시 적중율

이를 구체적으로 알아보기 위해 트위터 예제를 살펴보자. 나도 트위터를 하지 않아서 모르지만 트위터에는 2 가지 주요동작이 있다고 한다.

  • 트윗(tweet) 작성: 사용자는 팔로워에게 메시지를 게시할 수 있다.
  • 홈 타임라인(timeline): 사용자는 팔로우한 사람이 작성한 트윗을 볼 수 있다.

책에는 구체적인 수치까지 언급되어있지만 필자는 아직 그정도를 고려하면서 생각할 수준이 되지 않으므로 헷갈리지 않기 위해 최대한 구체적인 수치를 제거하였다.

 

피크일때의 트윗 작성 속도는 초당 12,000 건 쓰기가 발생한다고 한다. 이를 단순하게 처리하는것은 어렵지 않다. 문제는 트윗 양이 아닌 팬-아웃 때문이다.

 

1 명의 사용자는 여러 명을 팔로우 한다. 반면 다른 팔로워들도 해당 사용자를 팔로우 한다. 이를 구현하는 방식에는 2 가지 방식이 있다.

- 접근방식 1) RDB 로 요구사항을 구현

  1. 트윗을 작성하면 전역 트윗 컬렉션에 삽입한다.
  2. 사용자가 홈 타임라인을 요청하면 팔로우한 모든 사람들을 찾는다.
  3. 사람들이 작성한 트윗을 모두 찾아서 시간순으로 정렬한다.

SQL 에 익숙한 사람이라면 위와 같은 요구사항을 구현하는것은 그리 어려운일은 아니다. 

 

SELECT 
  tweets.*, users.*
FROM 
  tweets
  JOIN users
    ON tweets.sender_id = users.user_id
  JOIN follows
    ON follows.followee_id = users.user_id
WHERE
  follows.follower_id= current_user

 

- 접근방식 2) 각 개별사용자의 홈 타임라인 캐시를 유지하는 방식

  1. 사용자가 트윗 작성
  2. 해당 사용자를 팔로우 하는 모든 팔로워들 검색
  3. 각 팔로워들 홈 타임라인 캐시에 새로운 트윗 삽입

트위터는 초기에 접근방식 1) 을 이용하였으나 읽기 부하를 감당히 어려워 접근방식 2) 로 전환하였다. 일반적인 경우 트윗을 게시하는 요청보다 홈 타임라인을 읽는 요청량이 수백배 많았기 때문이다. 접근방식 2) 는 트윗 게시때에는 각 팔로워들 캐시를 갱신해야하기 때문에 쓰기 성능은 떨어지지만 캐시를 하기 때문에 읽기 성능은 향상된다.

 

일반적인 상황에서는 문제가 되지 않지만 팔로워들이 3 천만명 정도가 되는 유명한 사람인 경우 쓰기(게시) 부하가 큰 것이 심각한 문제가 된다. 1 트윗을 게시하면 3 천만 건의 쓰기가 발생하기 때문이다. 이런 경우 확장성에 있어서 핵심 부하매개변수는 사용자당 팔로워수의 분포가 된다.

 

이런 문제점들을 고려하여 최종적으로는 접근방식 2) 를 유지하되 팔로워수가 많은 소수의 사용자들에 대해서는 팬-아웃을 제외하였고, 사용자가 홈 타임라인 읽기 요청시 팔로워수가 많은 소수 사용자들의 게시글은 접근방식 1) 로 조회하여 병합한 후 시간순으로 정렬하는 하이브리드 방식을 채택하였다.


- 성능기술

부하를 기술하면 부하 증가시 어떤 일이 일어나는지 알 수 있는데, 부하 매개변수, 성능, 자원의 관점에서 2 가지 방법으로 살펴볼 수 있다. 

  • 부하 매개변수가 증가할 때 자원(CPU, 메모리 등)을 유지하면 시스템 성능에 어떤 영향을 미치는가?
  • 부하 매개변수가 증가할 때 성능을 유지하려면 자원을 얼마나 늘려야 하는가?

하둡(Hadoop) 같은 일괄 처리 시스템은 처리량(레코드 / 초)에 중점을 두며, 온라인 시스템의 경우 클라이언트의 응답시간에 중점을 둔다. 성능과 관련해서 지연 시간과 응답 시간의 개념을 같이 사용하는 경우가 있는데 이는 엄연히 다르다. 

  • 응답 시간: 요청을 처리하는 실제 시간 외에도 네트워크 지연, 큐 지연등을 포함한 시간
  • 지연 시간: 요청이 처리되기까지 대기하는 휴지 시간

클라이언트가 동일한 요청을 반복 해도 매번 응답 시간은 변한다. 따라서 응답 시간을 고려할 때는 단순값이 아니라 분포의 개념으로 봐야 한다. 때문에 성능 분석시에 '평균' 응답시간을 산출하여 이를 지표로 삼는 경우가 있는데 이는 좋은 접근이 아니다. 전형적인 응답시간을 알고 싶다면 '백분위' 개념을 적용해야 한다.

 

응답시간을 정렬했다고 했을 때 중간에 있는 중앙값이 200 밀리초라면 요청의 반은 200 밀리초 미만시간내에 나머지 반은 200 밀리초보다 오래걸린다는것을 알 수 있다. 이를 50분위(p.50)로 축약할 수 있다. 만약 특이 값(outlier)를 알아 보려면 상위 백분위를 사용해야 한다. 보통은 95분위, 99분위, 99.9분위를 사용한다.(p.95, p.99, p.999) 예를 들어 95 분위가 1.5 초 라면 100개의 요청중 95개는 1.5초 미만내에, 나머지 5개는 1.5초 보다 오래걸린다.

 

꼬리 지연 시간(tail latency) 상위 백분위 응답 시간은 서비스 사용자 경험에 직접적인 영향을 준다. 예를 들어 아마존은 99.9 분위를 목표로 하는데 응답시간이 느린 고객은 헤비 유저일 확률이 높으므로 이를 개선하여 고객을 행복하게 해주는것이 중요하다. 목표가 무조건 높다고 좋은것은 아니다. 99.99 분위의 경우 이익보다 비용이 훨씬 크고 너무 작은 변수에도 흔들리기 때문에 각 시스템의 상황에 따라서는 목표로 삼지 않아도 된다.

 

서비스 수준 목표(SLO) 와 서비스 수준 협약(SLA)에서 백분위가 등장할 수 있다. 예를 들어 '응답 시간 중앙값이 200 밀리초 미만, 99 분위 1초 미만이며 정상 서비스 상태 제공시간을 99.9% 이상으로 한다.' 와 같이 성능을 구체적으로 설정할 수 있다.

 

큐 대기 지연(queueing delay)은 높은 백분위 응답시간의 상당부분을 차지하는데 서버는 병렬로 소수의 작업만 처리할 수 있다. 소수의 느린 요청이 서버를 점유하면 후속처리가 지체 되는데 이 현상을 선두 차단이라고 한다. 때문에 성능측정 시 응답시간과 독립적으로 요청을 보내야 하는데 client 가 다음 요청 전송전에 이전 요청 완료를 기다리면 대기시간이 줄어들어 측정값에 왜곡이 발생한다.

 

사용자가 병렬 요청을 한 후 해당 요청들의 응답을 모두 기다려야 하는 상황이라면 나머지 요청들이 모두 완료되었다고 해도 가장 느린 요청의 응답이 완료될때까지 기다려야 한다. 이런 현상을 꼬리 지연 증폭이라고 한다. 서비스 모니터링 대시보드에 응답시간 관련 지표를 표시하고 싶다면 단순한 값 표시보다는 단위 시간마다 구간 내 중앙값이나 다양한 백분위 값을 계산하여 지표로 산출할 필요가 있다.


- 부하 대응 접근 방식

업무를 수행하거나 공부를 하다 보면 Scaling up, Scaling out 이라는 용어를 들어보았을 것이다.

  • 용량 확장(Scailing up, 수직확장): Spec 이 높은 강력한 장비로 이동
  • 규모 확장(Scailing out, 수평확장): 다수의 낮은 사양의 장비로 분산

다수의 장비에 부하를 분산하는 방식을 비공유 아키텍처라고 한다. 상황에 따라서 용량이나 규모 둘 중 어느쪽을 확장해야 하는지가 다르지만 일반적으로는 다수의 낮은 사양의 가상장비 보다 적절한 사양의 장비 몇대가 더 효율을 내는것이 일반적이다.

 

가상 컨테이너를 사용하다보면 부하가 증가할 때 컴퓨팅 자원을 자동으로 추가해주는기능을 본적이 있을 것이다. 부하가 예측할 수 없을만큼 높은 경우라면 유용한 기능이지만 그렇다고해서 자동확장의 방식이 무조건 수동확장보다 좋은것은 아닌데 수동확장은 더 간단하고 예측이 가능한 장점이 있다.

 

다수의 장비에 상태를 저장하지 않는(stateless) 서비스 배포는 난이도가 높지 않다. 하지만 상태를 저장하는 장비를 분산해야하는 요구사항은 만족하기가 힘들다. 상태를 저장한다는 말이 낯설다면 흔히 알고있는 DB 서버의 경우로 생각해보면 이해가 좀 쉽다. DB 서버의 경우 WAS 처럼 여러대를 구성하기보다는 일반적으로 고 사양의 active-standby와 같은 소수 장비로 구성(scailing out 이 아닌 scailing up)하는 경우가 대부분이다.

 

일반적으로 하는 오해중 한 가지가 있는데 모든 시스템에 맞는 좋은 아키텍처가 있다거나 복잡하고 어렵지만 확장성이 좋다는 이유만으로 굳이 해당 Spec 이 필요하지 않은 시스템에 억지로 적용하는것이다. 대규모 동작 시스템 아키텍처의 경우 어플리케이션에 특화되어 있는것이 대부분이다. 아키텍처는 요구사항에 따라 결정되어야 하는데 영향을 미치는 요소에는 아래와 같은 것들이 있다.

  • Read/Write 양
  • Data 의 양
  • Data 의 복잡도
  • 응답시간 요구사항
  • 접근 패턴

예를 들어 1KB 데이터를 초당 100,000 건 처리하는 시스템과 2GB 데이터를 분당 3회 처리하는 시스템은 처리량은 같지만 그 성격이 달라야 한다.

댓글