성능 모니터링은 성공적인 애플리케이션 운영의 핵심 요소입니다. 어떤 대상의 성능을 파악하는 가장 기본적인 방법 중 하나는 해당 작업이 발생할 때마다 소요 시간을 측정하고, 이를 바탕으로 통계를 도출하는 것입니다.
평균(Mean)
값들의 집합에서 평균(mean 또는 average)은 대상이 얼마나 좋거나 나쁘게 동작하는지 살펴보는 좋은 출발점입니다. 평균은 고려 대상인 모든 값을 더한 뒤, 발생 횟수로 나누어 계산합니다.
Ruby에서는 다음과 같이 평균 응답 시간을 계산할 수 있습니다:
def mean(array)
(array.sum.to_f / array.length).round(2)
end
durations = [1,2,3,4,5,6,7,8,9,0]
mean(durations) #=> 4.5
참고: 예제에서는 나눗셈의 정확도를 높이기 위해 총 소요 시간 값을 Float로 변환합니다. 그렇지 않으면 Ruby는 가장 가까운 정수로 내림하여 4를 반환하게 됩니다.
중앙값(Median)
또 하나 유용한 통계 지표는 중앙값(median)입니다. 이름이 비슷하게 들리지만, 값 집합의 평균과 중앙값에는 차이가 있습니다.
중앙값은 데이터 집합을 상위 절반과 하위 절반으로 나누는 값입니다.
홀수 개의 값을 가진 데이터셋에서는 값을 먼저 정렬한 후 가운데 숫자를 선택하면 중앙값을 구할 수 있습니다. 짝수 개의 값을 가진 집합에서는 정렬 후 가운데 두 숫자의 평균이 중앙값이 됩니다.
def median(array)
sorted_array = array.sort
length = sorted_array.length
if length.odd? # 홀수 배열일 때 가운데 숫자
sorted_array[length / 2]
else # 가운데 두 숫자의 평균
first_value = sorted_array[length / 2]
second_value = sorted_array[length / 2 - 1]
(first_value + second_value) / 2.to_f
end
end
# 짝수 개 배열
durations = [1,2,3,4,5,6,7,8,9,0]
median(durations) #=> 4.5
# 홀수 개 배열
durations = [1,1,2,3,4,5,6,7,8,9,0]
median(durations) #=> 4
이 통계 지표는 데이터에 큰 왜곡(skew)이나 긴 꼬리(long tail)가 있는지 확인하는 좋은 방법입니다.
durations = [1,2,3,4,5,2000]
median(durations) #=> 3.5
mean(durations) #=> 335.83
위 소요 시간 데이터의 평균은 2000ms라는 단 하나의 이상치(outlier) 때문에 335.83이 됩니다. 반면 중앙값은 3.5에 불과하며, 이는 데이터에 왜곡이 있음을 나타냅니다.
데이터셋의 평균과 중앙값을 함께 계산하면 큰 이상치나 긴 꼬리가 있는지 파악할 수 있습니다.
평균의 문제점
평균과 중앙값은 성능을 나타내는 좋은 지표이지만, 전체 상황을 말해주지는 못합니다. 웹페이지를 열 번 요청하면 평균은 매우 낮을 수 있지만, 그중 하나 이상의 요청이 완료되는 데 매우 오랜 시간이 걸릴 수도 있습니다.
아래 이미지는 AppSignal에서 특정 액션에 대한 99번째(파란색)와 90번째(초록색) 백분위수 및 평균(빨간색)을 보여줍니다. 99번째와 90번째 백분위수가 평균과 상당히 멀리 떨어져 있고 몇몇 스파이크가 보입니다. 이는 평균적인 고객은 좋은 경험을 하지만, 가끔씩 페이지 렌더링을 위해 거의 두 배나 오래 기다려야 하는 사용자가 있다는 의미입니다. 이상적으로는 모든 사용자에게 더 일관된 경험을 제공하기 위해 이러한 값들을 서로 최대한 가깝게 만드는 것이 좋습니다.
예를 들어, 10명의 고객이 100밀리초에서 1초 사이의 소요 시간으로 페이지를 요청하는 다음 소요 시간 집합을 생각해 봅시다.
[100,100,100,100,100,100,100,100,100,1_000]
이 경우 평균은 190ms에 불과하지만, 한 명의 사용자는 1초라는 매우 나쁜 응답 시간을 경험했습니다. 평균만 추적하면 웹사이트의 성능이 훌륭하다고 착각하기 쉽지만, 실제로는 가끔씩 사용자가 끔찍한 경험을 하고 있는 것입니다.
위 예제는 단 10개의 요청에 대한 것이지만, 하루에 천 건의 요청이 발생한다면 그중 백 명의 사용자가 나쁜 경험을 했다는 뜻입니다.
백분위수(Percentiles)
값들의 분포를 더 잘 파악하기 위해 백분위수를 사용합니다. 백분위수는 중앙값과 유사한 개념으로, 데이터셋에서 절반은 그 수보다 아래에 있고 절반은 위에 있는 지점을 나타내는 숫자입니다. 마찬가지로 20번째 백분위수는 데이터셋에 있는 숫자들 중 20%가 그 수보다 아래에 있음을 의미합니다.
다음의 (정렬된) 집합이 주어졌다고 가정해 봅시다:
[100,100,200,200,300,300,400,400,500,5_000]
20번째 백분위수를 알고 싶다면 다음과 같이 계산할 수 있습니다: 집합에는 10개의 값이 있고, 배열이 0부터 시작하므로 원하는 값은 위치 1(20.0 / 100 * 10 - 1)에 있습니다. 이 배열은 짝수 개의 항목을 포함하고 있으므로, 인덱스(2)와 인덱스 + 1(3) 사이의 평균을 계산해야 합니다. 따라서 20번째 백분위수는 150이 됩니다.
매우 단순한 Ruby 구현은 다음과 같습니다:
def percentile(array, wanted_percentile)
sorted_array = array.sort
index = (wanted_percentile.to_f / 100) * sorted_array.length - 1
# 인덱스가 정수가 아닌지 확인
if index != index.to_i
sorted_array.at(index.ceil)
elsif sorted_array.length.even?
first_value = sorted_array.at(index)
second_value = sorted_array.at(index + 1)
(first_value + second_value) / 2
else
sorted_array.at(index)
end
end
# 홀수 개의 숫자를 가진 배열
durations = [100,200,200,300,300,400,400,500,5_000]
percentile(durations, 20) #=> 100
percentile(durations, 90) #=> 500
percentile(durations, 95) #=> 5000, 인덱스가 소수이므로 올림하여 계산
# 짝수 개의 숫자를 가진 배열
durations = [100,100,200,200,300,300,400,400,500,5_000]
percentile(durations, 20) #=> 150, 인덱스 1 & 2의 평균 `(100 + 200) / 2`
percentile(durations, 90) #=> 2750, 인덱스 8 & 9의 평균 `(500 + 5000) / 2`
percentile(durations, 95) #=> 500, 인덱스가 소수이므로 올림하여 계산
이 percentile 함수는 앞서 살펴본 median 계산과 매우 유사해 보이며, 실제로 중앙값은 50번째 백분위수와 같습니다.
durations = [1,2,3]
percentile(durations, 50) == median(durations) #=> true
AppSignal은 위의 통계들을 활용하여 애플리케이션의 성능 메트릭을 생성합니다. 단순히 평균에만 의존하지 않고 90번째와 95번째 백분위수를 함께 계산하여 이상치를 드러냄으로써, 요청 분포를 더 정확하게 파악할 수 있도록 돕습니다. 자세한 내용은 성능 투어 페이지에서 확인하세요.
특이 사항
백분위수와 평균이 계산되는 방식 때문에, 때때로 90번째 백분위수가 평균보다 낮게 나오는 경우가 있습니다. 예를 들어 다음 데이터셋을 보겠습니다:
durations = [1,1,1,1,1,1,1,1,1,1,2000]
percentile(durations, 90) #=> 1
mean(durations) #=> 182.73
이 경우 평균은 182.73이 되고, 90번째 백분위수는 겨우 1입니다.
메트릭 수집 시스템이 90번째 백분위수와 평균만 보여준다 해도, 90번째 백분위수가 평균보다 낮아진다면 데이터셋 어딘가에 큰 이상치가 존재한다는 사실을 추론할 수 있습니다.
👋 이 글이 마음에 드셨다면, Ruby(Rails 포함) 성능에 관한 글을 더 많이 준비해 두었습니다. Ruby 성능 모니터링 체크리스트도 함께 확인해 보세요.
이 글의 마무리까지 거의 다 왔습니다
오늘은 여기까지입니다! 다음 글에서는 Quantiles를 활용해 모든 고객의 요청에 대한 백분위수를 효율적으로 저장하고 계산하는 방법을 다룰 예정입니다. 통계와 APM, 에러 추적 또는 성능 모니터링에 대해 궁금한 점이나 의견이 있으시면 Twitter @AppSignal 또는 이메일로 연락해 주세요.