5 minute read

성능 저하의 일반적인 원인 중 하나는 부하 증가다. 확장성은 부하 증가에 대처하는 시스템의 능력을 설명하는 용어다. 그러나 확장성을 일차원적으로 보면 안된다. 확장성은 “시스템이 특정 방식으로 확장하는 경우 대처할 수 있는 옵션은 무엇인가?”, “추가 부하를 처리하기 위해 컴퓨팅 리소스를 어떻게 추가할 수 있는가?” 등의 질문을 고려해야 한다.

부하 설명하기

시스템의 현재 부하를 간결하게 설명할 수 있어야 부하 증가에 대한 대처방안을 이야기할 수 있다. 부하는 부하 매개변수라고 하는 몇 가지 숫자로 설명할 수 있다.

  • 웹 서버에 대한 초당 요청 수
  • 데이터베이스의 읽기 대 쓰기 비율
  • 채팅방에서 동시에 활동 중인 사용자 수
  • 캐시의 적중률

2012년 11월에 발표된 데이터를 사용하여 트위터를 예로 들어보자. 트위터의 주요 작업은 두 가지다.

  1. 트윗 게시 사용자는 팔로워에게 새 메시지를 게시할 수 있다 (평균 초당 4.6천 건, 최대 초당 12천 건 이상 요청).

  2. 홈 타임라인 사용자는 자신이 팔로우하는 사람들이 올린 트윗을 볼 수 있다.(초당 30만 건 요청).

초당 12,000건의 쓰기(트윗 게시의 최고 속도)를 처리하는 것은 쉽다. 그러나 트위터의 확장 문제는 트윗의 양 때문이 아니라 팬 아웃(각 사용자가 많은 사람을 팔로우하고 각 사용자가 많은 사람을 팔로우하는 현상) 때문이다.

위 주요 두 가지 작업을 구현하는 방법은 크게 두 가지가 있다

  1. 트윗이 포스팅되면 새 트윗이 글로벌 트윗 컬렉션에 입력한다. 사용자가 홈 타임라인을 요청하면 팔로우하는 모든 사용자를 조회하고 각 사용자의 모든 트윗을 찾아서 병합(시간별로 정렬)한다.

  2. 각 사용자의 홈 타임라인에 대한 캐시를 각 수신자 사용자의 트윗 메일박스처럼 유지한다. 사용자가 트윗을 올리면 해당 사용자를 팔로우하는 모든 사용자를 조회하여 새 트윗을 각 사용자의 홈 타임라인 캐시에 삽입한다. 캐시에 결과가 미리 계산되어 있기 때문에 홈 타임라인 읽기 부하가 적다.

트위터의 첫 번째 버전에서는 1번 접근 방식을 사용했다. 이후 시스템이 홈 타임라인 쿼리의 부하로 인해 2번 방식으로 전환했다. 이 방식이 더 효과적인 이유는 게시된 트윗의 평균 비율이 홈 타임라인 읽기 비율보다 거의 두 배나 낮기 때문이다. 쓰기에 더 많은 작업을 수행하고 읽기에 더 적은 작업을 수행하는 것이 더 효율적이다.

2번 접근 방식의 단점은 트윗을 게시하는 데 많은 추가 작업이 필요하다는 것이다. 한 개의 트윗이 약 75명의 팔로워에게 된다. 초당 4.6천 개의 트윗이 홈 타임라인 캐시에 초당 345,000개의 쓰기가 된다.

하지만, 일부 사용자는 3천만 명이 넘는 팔로워를 보유하고 있다. 즉, 한 번의 트윗으로 홈 타임라인에 3천만 건 이상의 글이 작성될 수 있다는 뜻이다.

트위터의 목표는 5초 이내에 팔로워에게 트윗을 전달이기 때문에 어렵다.

사용자별 팔로워 분포가 팬 아웃 부하를 결정한다. 이것이 트위터 서비스의 가장 중요한 부하 매개변수다.

트위터는 현재, 트위터는 두 가지 접근 방식을 혼합하여 사용하고 있다. 대부분의 사용자의 트윗은 게시된 시점에 홈 타임라인으로 계속 팬아웃되지만, 팔로워 수가 매우 많은 소수의 사용자는 이 팬아웃에서 제외된다. 사용자가 팔로우할 수 있는 유명인의 트윗은 접근 방식 1에서와 같이 별도로 가져와서 읽을 때 해당 사용자의 홈 타임라인과 병합된다.

성능 설명하기

부하가 증가하면 어떤 일이 발생하는지 두 가지 질문으로 파악할 수 있다.

  • 부하 매개변수를 늘리고 시스템 리소스(CPU, 메모리, 네트워크 대역폭 등)를 변경하지 않은 상태에서 시스템 성능에 어떤 영향을 미치는가?

  • 부하 매개변수를 늘릴 때 성능을 변경하지 않으려면 리소스를 얼마나 늘려야 하는가?

일괄 처리 시스템(예를 들어, Hadoop)은 처리량(초당 처리할 수 있는 레코드 수 또는 특정 크기의 데이터의 작업에 소요되는 총 시간)에 관심을 갖습니다.

온라인 시스템은 클라이언트가 요청을 전송하고 응답을 받는 시간 사이에 걸리는 서비스 응답 시간이 더 중요하다

동일한 요청을 반복해서만 수행하더라도 시도할 때마다 조금씩 다른 응답 시간을 얻을 수 있다. 다양한 요청을 처리하는 시스템은 응답 시간이 매우 다양하다. 따라서, 응답 시간을 하나의 숫자가 아니라 측정할 수 있는 값의 분포로 생각해야 합니다.

대부분의 요청은 상당히 빠르지만, 간혹 훨씬 더 오래 걸리는 이상값이 있다. 느린 요청은 더 많은 데이터를 처리하기 때문에 본질적으로 더 많은 비용이 들 수도 있습니다. 추가 지연 시간이 발생하는 원인은 여러가지가 있다.

  • 백그라운드 프로세스로의 컨텍스트 전환
  • 네트워크 패킷 손실 및 TCP 재전송
  • 가비지 컬렉션 일시 중지
  • 디스크에서 강제로 읽어야 하는 페이지 오류
  • 서버 랙의 기계적 진동

서비스의 평균 응답 시간은 실제로 얼마나 많은 사용자가 해당 지연을 경험했는지 알려주지 않기 때문에 “유형별” 응답 시간을 알고자 하는 경우 좋은 측정 지표가 아니다. 평균 응답 시간은 백분위수를 사용하는 것이 좋다. 응답 시간 목록을 가장 빠른 것부터 가장 느린 것까지 정렬하면 중앙값이 중간 지점이 된다.

예를 들어, 응답 시간 중앙값이 200 밀리초라면 절반의 요청이 200 밀리초 이내에 반환되고 절반의 요청이 그보다 오래 걸린다는 뜻이다. 중앙값은 사용자가 일반적으로 얼마나 오래 기다려야 하는지 알고자 할 때 좋은 지표가 된다.

이상 값을 파악하려면 어떻게 하는가?

상위 백분위수를 살펴보면 된다. 95번째, 99번째, 99.9번째 백분위수가 일반적이다. (약칭: p95, p99, p999). 예를 들어, 95번째 백분위수 응답 시간이 1.5초인 경우라면 100개의 요청 중 95개가 1.5초 미만, 100개의 요청 중 5개가 1.5초 이상 걸린다는 의미다.

응답 시간의 높은 백분위수(tail latency라고도 함)는 사용자의 서비스 경험 측면에서 왜 중요할까?

예를 들어, Amazon은 내부 서비스에 대한 응답 시간 요구 사항을 1,000건의 요청 중 1건에만 영향을 미치지만 99.9번째 백분위수를 기준으로 한다. 요청 속도가 가장 느린 고객이 구매 횟수가 많아 계정에 가장 많은 데이터를 보유한 고객, 즉 가장 가치 있는 고객이기 때문이다.

대기열(Queue) 지연은 응답 시간의 많은 부분을 차지한다. 서버는 소수의 작업만 병렬로 처리할 수 있기 때문에, 소수의 느린 요청만 처리해도 후속 요청의 처리가 지연되는데, 이를 헤드 오브 라인 블로킹이라고도 한다. 후속 요청이 서버에서 빠르게 처리되더라도 클라이언트는 이전 요청이 완료될 때까지 기다리는 시간으로 인해 전체 응답 시간이 느려진다. 따라서, 클라이언트 측에서 응답 시간을 측정하는 것이 중요하다.

부하에 대처하기

부하를 설명하는 매개변수를 알고나면 확장성에 대해 이야기할 수 있다. 부하 매개변수가 어느 정도 증가하더라도 좋은 성능을 유지하려면 어떻게 해야 할까? 한 수준의 부하에 적합한 아키텍처는 그 10배의 부하를 감당할 수 없다. 따라서 빠르게 성장하는 서비스에서 작업하는 경우, 부하가 몇 배로 증가할 때마다 또는 그보다 더 자주 아키텍처를 다시 고려해야한다.

사람들은 종종 스케일업(수직 확장, 더 강력한 머신으로 이동)과 스케일아웃(수평 확장, 여러 대의 작은 머신에 부하를 분산)을 이분법적으로 말한다. 실제로 좋은 아키텍처는 실용적인 접근 방식이 혼합되어 있다. 예를 들어, 상당히 강력한 여러 대의 머신을 사용하는 것이 다수의 소규모 가상 머신을 사용하는 것보다 더 간단하고 저렴하다.

일부 시스템은 탄력적이어서 부하 증가를 감지하면 자동으로 컴퓨팅 리소스를 추가할 수 있는 반면, 다른 시스템은 수동으로 확장(사람이 용량을 분석하고 시스템에 더 많은 머신을 추가하기로 결정)한다. 탄력적 시스템은 부하를 예측할 수 없는 경우에 유용할 수 있지만, 수동으로 확장하는 시스템이 더 간단하고 운영상의 돌발 상황이 적을 수 있다.

여러 머신에 걸쳐 상태 저장 서비스를 배포하는 것은 매우 간단하지만, 상태 저장 데이터 시스템을 단일 노드에서 분산 설정으로 전환하는 것은 많은 복잡성을 불러일으킨다. 확장 비용이나 고가용성 요구 사항으로 인해 데이터베이스를 분산해야 할 때까지는 데이터베이스를 단일 노드에 유지(확장)하는 것이 최근까지 일반적인 상식이었다.

분산 시스템을 위한 도구와 추상화가 개선됨에 따라, 향후에는 대량의 데이터나 트래픽을 처리하지 않는 사례에서도 분산형 데이터 시스템이 기본값이 될 것으로 예상한다.

대규모로 운영되는 시스템의 아키텍처는 애플리케이션에 따라 매우 특수하며, 보편적인 확장 가능한 아키텍처와 같은 것은 존재하지 않는다.

대규모 시스템에서 발생하는 문제는 읽기 볼륨, 쓰기 볼륨, 저장할 데이터의 양, 데이터의 복잡성, 응답 시간 요구 사항, 액세스 패턴 등이 혼합된 것일 수 있다. 예를 들어, 초당 10만건의 요청을 처리하도록 설계된 시스템과 각각 1kB 크기의 요청을 분당 3건씩 처리하도록 설계된 시스템은 데이터 처리량이 동일하더라도 2GB 크기의 요청을 분당 3건씩 처리하도록 설계된 시스템과는 매우 다르다.

특정 애플리케이션에 맞게 제대로 확장되는 아키텍처는 어떤 작업이 자주 발생하고 어떤 작업이 드물게 발생하는지에 대한 가정, 즉 부하 매개변수를 중심으로 구축된다. 초기 단계의 제품에서는 미래에 발생할 부하에 맞춰 확장하는 것보다, 제품 기능을 빠르게 반복할 수 있는 것이 더 중요하다. 특정 애플리케이션에 특화되어 있더라도 확장 가능한 아키텍처는 익숙한 패턴으로 구축 할 수 있다.