created at 2023-12-21
Table of contents
서론
저는 여러 페이즈를 진행하며 성능 개선을 진행하였고, 각 페이즈 별 성능수치들을 먼저 보여드리겠습니다.
- Phase 1 : HPA 를 통한 파드 오토 스케일링 아웃 적용
- Phase 2 : Readiness 타임아웃 증가
- Phase 3 : 내장 tomcat max-threads 크기 증가
- Phase 4 : 내장 tomcat 최적화
표를 봐도 얼마나 개선되었는지 한 눈에 잘 안보이죠?ㅎㅎ 그래서 그래프로 표현해봤습니다!
그럼 이제 각각의 성능 개선 페이즈를 하나씩 살펴보겠습니다!
서버 로드 테스트 환경
- Thread(virtual user) : 296
- URL : GET /rooms
- Duration : 10min
- Test-Server
- CPU : 8 virtual core
- Memory : 32GB
- OS : MacOS Big Sur
- Target-Server EC2 인스턴스
- Instance Type: t3.medium, 3대 워커노드 운용
- CPU : 2 virtual core, total 6 virtual core
- Memory : 4GB, total 12GB
- OS : Amazon Linux 2 x86_64 HVM gp2
로드 테스트
Original. 단일 채팅서버 파드로 핸들링 시 결과
Metric | Before |
---|---|
Total Tests | 40,228 |
Error Rate | 51.11% (20,560) |
TPS 평균 (Average) | 109.27 |
TPS p50 | 69.00 |
TPS p95 | 4.00 |
TPS p99 | 2.84 |
TPS p99.9 | 1.63 |
MTTFB 평균 (Average) | 1605.44 ms |
MTTFB p50 | 1636.55 ms |
MTTFB p95 | 24013.28 ms |
MTTFB p99 | 27690.40 ms |
MTTFB p99.9 | 28157.50 ms |
MTTFB 차이 평균 (Average Difference) | 2838.38 ms |
MTTFB 평균적인 변동률 (Average Variability) | 75.00% |
- 테스트 지표 확인
만약 HPA 파드 오토 스케일 아웃 없이 단일 파드로만 핸들링할 때 어떤 결과가 나올지 궁금하지 않나요? 그래서 실험해보았고 위의 결과가 나왔습니다. 실험 중 오류가 너무 많이 발생되서 자동으로 중지될 정도로 많은 오류를 반환합니다. 에러률이 50% 이상을 기록해버렸습니다. 전체 트랜잭션의 95% 는 4 TPS 내로 처리되고 99.9% 는 1.63 TPS 내로 처리됩니다. 하위 트랜잭션처리량이 매우 낮죠.
MTTFB 또한 높은 수치를 보여줍니다. p99 는 27초, p99.9 는 28초로 처리되고 있습니다… 이는 서버가 매우 느리게 응답하고 있다는 것을 의미합니다. 또한 평균 MTTFB 와 매우 큰 차이를 보인다는 뜻은 서버의 응답시간이 매우 불안정하다는 것을 보여주죠!
해당 로드 테스트는 10분동안 실행하도록 지시했지만 03:10 에 높은 에러율로 인해 종료되었습니다.
Phase 1. HPA 를 통한 파드 오토 스케일링 적용 시 결과
그러면 CPU 자원을 좀 더 많이 써볼까요? Kubernetes HPA(HorizontalPodAutoscaler) 를 통해 파드를 cpu 사용량에 따라 자동으로 스케일 아웃/인을 수행하도록 설정했습니다. 그리고 이를 통해 다시 로드 테스트를 진행해보았습니다.
Metric | Before | After | Change |
---|---|---|---|
Total Tests | 40,228 | 142,805 | 254.91% 🟢 |
Error Rate | 51.11%(20,560) | 6.57%(9,389) | -87.16% 🟢 |
TPS 평균 (Average) | 109.27 | 228.45 | 109.07% 🟢 |
TPS p50 | 69.00 | 238.25 | 245.29% 🟢 |
TPS p95 | 4.00 | 80.38 | 1909.50% 🟢 |
TPS p99 | 2.84 | 6.82 | 139.44% 🟢 |
TPS p99.9 | 1.63 | 1.87 | 14.72% 🟢 |
MTTFB 평균 (Average) | 1605.44 ms | 1265.08 ms | -21.20% 🟢 |
MTTFB p50 | 1636.55 ms | 934.00 ms | -42.96% 🟢 |
MTTFB p95 | 24013.28 ms | 4105.42 ms | -82.89% 🟢 |
MTTFB p99 | 27690.40 ms | 19557.53 ms | -29.33% 🟢 |
MTTFB p99.9 | 28157.50 ms | 21906.02 ms | -22.19% 🟢 |
MTTFB 차이 평균 (Average Difference) | 2838.38 ms | 1020.87 ms | -64.12% 🟢 |
MTTFB 평균적인 변동률 (Average Variability) | 75.00% | 86.83% | 15.77% 🔴 |
- 테스트 지표 확인 및 개선점
중간중간 TPS 가 감소하는 모습을 확 위의 그래프에서 확인할 수 있습니다. TPS 감소 이유는 아래의 그래프에서 원인을 파악할 수 있습니다.
MTTFB 가 중간중간 5초가 넘습니다. 그리고 이런 피크를 보일 때 마다 오류또한 증가합니다. 마찬가지로 TPS 또한 같이 감소하는 것을 확인할 수 있었습니다. 저는 채팅서버의 로드가 중간중간 증가해서 헬스체크의 타임아웃이 발생했고, 서비스가 로드밸런싱을 다시 수행하면서 TPS 가 감소했다고 생각했어요. 한번 확인해보겠습니다.
채팅서버의 Deployment 의 readinessProbe 헬스체크 timeoutSeconds는 1초로 설정되어 있었습니다.
deploy 로그를 확인했더니 역시 health 타임아웃 오류가 발생했었습니다. 그렇다면 타임아웃을 60초로 설정하면 견딜 수 있겠죠?
Phase 2. Readiness 타임아웃 1초 -> 60초
Metric | Before | After | Change |
---|---|---|---|
Total Tests | 142,805 | 138,487 | -3.02% 🔴 |
Error Rate | 6.57%(9,389) | 0.02%(25) | No Error 🟢 |
TPS 평균 (Average) | 228.45 | 237.09 | 3.78% 🟢 |
TPS p50 | 238.25 | 243.50 | 2.21% 🟢 |
TPS p95 | 80.38 | 126.53 | 57.38% 🟢 |
TPS p99 | 6.82 | 64.56 | 846.16% 🟢 |
TPS p99.9 | 1.87 | 47.60 | 2430.48% 🟢 |
MTTFB 평균 (Average) | 1265.08 ms | 1224.22 ms | -3.23% 🟢 |
MTTFB p50 | 934.00 ms | 1150.59 ms | 23.19% 🔴 |
MTTFB p95 | 4105.42 ms | 2383.01 ms | -41.98% 🟢 |
MTTFB p99 | 19557.53 ms | 3636.65 ms | -81.39% 🟢 |
MTTFB p99.9 | 21906.02 ms | 4132.45 ms | -81.15% 🟢 |
MTTFB 차이 평균 (Average Difference) | 1020.87 ms | 437.17 ms | -57.18% 🟢 |
MTTFB 평균적인 변동률 (Average Variability) | 86.83% | 35.20% | -59.46% 🟢 |
- 테스트 지표 확인 및 개선점
Error 는 0.0% 로 에러없이 정상적인 요청처리를 확인할 수 있었습니다. 또한 MTTFB와 TPS 의 p95, p99, p99.9 모두 큰 폭으로 상승한 것을 확인할 수 있었습니다! 원래는 서버가 어느정도 부하가 발생해서 K8S health check restAPI 를 1초내에 응답하지 못하면 서버가 꺼짐으로 인해 에러가 대폭 발생했습니다. 그 제한시간을 60초로 늘림에 따라 에러률이 거의 0% 로 줄었죠. Error 가 사라짐으로 인해 길고 긴 요청 대기시간 또한 사라졌습니다. 대기시간이 사라진 것은 MTTFB p99.9 의 감소량을 보연 알 수 있죠(21906.02 ms 에서 4132.45 ms 로 81.15% 감소)!
그래도 여전히 군데군데 MTTFB 피크가 4000ms 로 올라가면서 서버가 살작 불안정합니다. 그리고 이에 맞추어 TPS 또한 떨어지고, 오류도 중간에 조금씩 발생하는 것을 확인할 수 있습니다.
Phase 3. 내장 tomcat max-threads 크기 10개 -> 100개
그렇다면 이번엔 Spring boot 내장 톰켓 서버의 max-threads-size 를 늘려봅시다. 이유는 채팅서버의 cpu 사용량이 생각보다 작기 때문입니다. 많은 스레드를 돌리거나 요청을 많이 받을 수 있어야지 cpu 사용률도 올라가니 이 부분에 문제가 있다고 판단하고 스레드 풀 사이즈를 늘리는 방향을 설정하였습니다.
Metric | Before | After | Change |
---|---|---|---|
Total Tests | 138,487 | 134,988 | -2.52% 🔴 |
Error Rate | 0.02%(25) | 0.03%(35) | No Error 🟢 |
TPS 평균 (Average) | 237.09 | 234.29 | -1.18% 🔴 |
TPS p50 | 243.50 | 244.50 | 0.41% 🟢 |
TPS p95 | 126.53 | 110.55 | -12.63% 🔴 |
TPS p99 | 64.56 | 59.92 | -7.00% 🔴 |
TPS p99.9 | 47.60 | 53.07 | 11.51% 🟢 |
MTTFB 평균 (Average) | 1224.22 ms | 1249.87 ms | 2.09% 🔴 |
MTTFB p50 | 1150.59 ms | 1217.48 ms | 5.80% 🔴 |
MTTFB p95 | 2383.01 ms | 2280.26 ms | -4.47% 🟢 |
MTTFB p99 | 3636.65 ms | 3473.83 ms | -4.47% 🟢 |
MTTFB p99.9 | 4132.45 ms | 3887.59 ms | -5.93% 🟢 |
MTTFB 차이 평균 (Average Difference) | 437.17 ms | 350.82 ms | -19.76% 🟢 |
MTTFB 평균적인 변동률 (Average Variability) | 35.20% | 24.47% | -30.49% 🟢 |
그래프로 볼 땐 몰랐지만, 수치를 확인해보니 확실히 TPS 와 MTTFB 의 최상위 지표(p99.9)가 높게 나오는 것을 확인할 수 있었습니다. 또한 MTTFB 의 변동폭(MTTFB 차이 평균)이 작은 것 또한 관찰되죠. 이 말은 트래픽을 균일한 시간 내 처리하게 되니까 서버의 안전성이 향상되었다고 볼 수 있겠죠?
반면, 아직 생각보다 cpu 리소스를 제대로 활용하지 못하는 모습을 볼 수 있습니다.
빨간선은 request 이며 노란선은 limit 입니다. y-grid 는 0.5 cpu 를 의미합니다.
Phase 4. 내장 tomcat 최적화
Metric | Before | After | Change |
---|---|---|---|
Total Tests | 134,988 | 181,050 | 34.02% 🟢 |
Error Rate | 0.03%(35) | 0.00%(0) | No Error 🟢 |
TPS 평균 (Average) | 234.29 | 312.16 | 33.26% 🟢 |
TPS p50 | 244.50 | 319.00 | 30.53% 🟢 |
TPS p95 | 110.55 | 217.45 | 96.82% 🟢 |
TPS p99 | 59.92 | 132.28 | 120.51% 🟢 |
TPS p99.9 | 53.07 | 96.52 | 81.91% 🟢 |
MTTFB 평균 (Average) | 1249.87 ms | 950.89 ms | -23.89% 🟢 |
MTTFB p50 | 1217.48 ms | 919.20 ms | -24.55% 🟢 |
MTTFB p95 | 2280.26 ms | 1322.11 ms | -42.04% 🟢 |
MTTFB p99 | 3473.83 ms | 1833.22 ms | -47.26% 🟢 |
MTTFB p99.9 | 3887.59 ms | 2099.12 ms | -46.05% 🟢 |
MTTFB 차이 평균 (Average Difference) | 350.82 ms | 112.52 ms | -67.89% 🟢 |
MTTFB 평균적인 변동률 (Average Variability) | 24.47% | 10.67% | -56.39% 🟢 |
PR https://github.com/ghkdqhrbals/spring-chatting-server/pull/334 에서 적용된 톰켓 최적화 설정을 아래에 풀어볼게요.
- 대기 큐 크기(accept-count) 100 -> 100
- 유지
- 동시 연결 개수(max-connections) 100 -> 8192
- 현재 vuser 300 의 동시 연결 요청을 전송하기때문에 이를 고려하여 default 개수까지 증량
- 최대 스레드 풀 크기(max-threads) 100 -> 150
- 채팅서버 vcpu limit 1500m 이기때문에 각 코어당 * 100 하여 처리 1.5 * 100 = 150
- 현재 대부분의 API 요청에서 i/o 작업을 하기 때문에 왠만하면 스레드를 늘리는 것이 좋습니다. 만약 cpu 소모량이 많다고 한다면 줄이는 것이 좋다고 합니다. 괜한 context switch 를 줄이는 것이 효율적이기 때문입니다.
- 연결 타임아웃(connection-timeout) 10s -> 60s
- 현재 readinessProbe 타임아웃 설정이 60s 로 설정되어 있습니다. 이에 맞추어 오류를 줄이기 위해 60s 로 설정하는 것이 올바르다고 생각해서 변경하게 되었습니다.
- 항상 유지되는 최소 스레드 크기(min-spare-threads) 30 -> 30
- 유지
정말 신기하지 않나요? 모오든 지표에서 아주 큰 폭으로 상승하는 것이 확인되었습니다. 우리는 드디어 동시 요청 스레드 300 을 우리는 에러없이 안정적으로 견딜 수 있는 서버를 만들어낸거에요!
그럼 지표들을 살펴보겠습니다.
TPS 의 전체 지표들이 최소 30% 상승하였습니다. 심지어 TPS p99 는 120% 가 개선되면서 대부분의 요청들이 꾸준히 안정적으로 처리되고 있죠. MTTFB 도 볼까요? MTTFB 평균은 23%가 개선되었으며 90th, 95th, 99th, 99.9th 레이턴시 퍼센트가 모두 40% 이상 개선되었습니다. 즉, 마찬가지로 대부분의 요청이 매우 안정적으로 처리되고 있음을 보여주죠. MTTFB 그래프를 보시게 되면 눈으로 봐도 상당히 안정적으로 처리되고 있음을 확인할 수 있습니다(초기 Initial 과정 빼곤).
저는 이 결과를 통해 병목이 가장 많이 걸리는 곳이 내장 톰켓이였다는 것을 확인할 수 있었습니다.
그럼 이제 현재까지의 모든 테스트들을 요약하여 시작과 끝의 성능지표를 비교해보겠습니다.
성능개선 시작과 끝 비교!
Metric | Before | After | Change |
---|---|---|---|
Total Tests | 40,228 | 181,050 | 349.29% 🟢 |
Error Rate | 51.11%(20,560) | 0.00%(0) | No Error 🟢 |
TPS 평균 (Average) | 109.27 | 312.16 | 185.94% 🟢 |
TPS p50 | 69.00 | 319.00 | 362.32% 🟢 |
TPS p95 | 4.00 | 217.45 | 5362.50% 🟢 |
TPS p99 | 2.84 | 132.28 | 4556.34% 🟢 |
TPS p99.9 | 1.63 | 96.52 | 5852.76% 🟢 |
MTTFB 평균 (Average) | 1605.44 ms | 950.89 ms | -40.68% 🟢 |
MTTFB p50 | 1636.55 ms | 919.20 ms | -43.90% 🟢 |
MTTFB p95 | 24013.28 ms | 1322.11 ms | -94.47% 🟢 |
MTTFB p99 | 27690.40 ms | 1833.22 ms | -93.40% 🟢 |
MTTFB p99.9 | 28157.50 ms | 2099.12 ms | -92.52% 🟢 |
MTTFB 차이 평균 (Average Difference) | 2838.38 ms | 112.52 ms | -96.04% 🟢 |
MTTFB 평균적인 변동률 (Average Variability) | 75.00% | 10.67% | -85.77% 🟢 |
와우… 정말 놀라운 결과입니다. TPS 는 3배, MTTFB 평균은 40% 가량 개선되었습니다. 그리고 TPS, MTTFB 의 p50, p95, p99, p99.9 모두 매우 큰 폭으로 개선되었습니다!
TPS p99.9 는 5852.76% 개선되었습니다ㅋㅋ;; 무슨일이죠
이는 서버의 안정성이 매우 향상되었음을 의미합니다. 그리고 에러율은 0.00000% 로 에러가 전혀 발생하지 않았습니다.
결론
Appendix - MTTFB 평균과 변동량/변동률, p50/p95/p99/p99.9 측정 코드
import sys
import pandas as pd
import numpy as np
import locale
original_locale = locale.getlocale()
locale.setlocale(locale.LC_ALL, '')
def calculate_error_rate(data):
total_tests = data['Tests'].sum()
total_errors = data['Errors'].sum()
error_rate = (total_errors / (total_tests+total_errors)) * 100
return error_rate
def format_with_commas(number):
return locale.format_string("%d", number, grouping=True)
def calculate_weighted_average_tps(data):
tps = data['TPS']
average_tps = tps.mean()
return average_tps
def calculate_percentiles(data, column, percentiles, ascending=True):
values = data[column]
if not ascending:
values = -values # 내림차순 정렬을 위해 값에 마이너스(-)를 곱합니다.
sorted_values = np.sort(values)
return {f'p{p}': np.percentile(sorted_values, p) for p in percentiles}
def calculate_average_mttfb_difference(data):
mttfb = data['Mean_time_to_first_byte']
mttfb_differences = mttfb.diff().abs()[1:] # 첫 번째 값은 NaN 이므로 제외
average_difference = mttfb_differences.mean()
return average_difference
def calculate_total_weighted_mttfb_average(data):
mttfb = data['Mean_time_to_first_byte']
tests = data['Tests']
total_weighted_mttfb = (mttfb * tests).sum()
total_tests = tests.sum()
weighted_average = total_weighted_mttfb / total_tests
return weighted_average
def calculate_mttfb_variation(data):
mttfb = data['Mean_time_to_first_byte']
tests = data['Tests']
mttfb_changes = mttfb.diff()[1:]
mttfb_abs_percent_changes = (mttfb_changes / mttfb[:-1]).abs() * 100
average_tests_weights = (tests[:-1] + tests[1:]) / 2
weighted_avg_abs_changes = mttfb_abs_percent_changes * average_tests_weights[1:]
average_weighted_avg_abs_change = weighted_avg_abs_changes.sum() / average_tests_weights[1:].sum()
return average_weighted_avg_abs_change
if __name__ == "__main__":
if len(sys.argv) != 2:
print("Usage: python script.py <path_to_csv_file>")
sys.exit(1)
csv_file_path = sys.argv[1]
data = pd.read_csv(csv_file_path)
total_tests = data['Tests'].sum()
total_errors = data['Errors'].sum()
error_rate = calculate_error_rate(data)
print(f"Total Tests: {format_with_commas(total_tests+total_errors)}")
print(f"Error Rate: {error_rate:.2f}% ({format_with_commas(total_errors)})")
average_tps = calculate_weighted_average_tps(data)
tps_percentiles = calculate_percentiles(data, 'TPS', [50, 95, 99, 99.9], ascending=False)
print(f"TPS 평균: {average_tps:.2f}")
print(f"TPS p50: {-tps_percentiles['p50']:.2f}")
print(f"TPS p95: {(-tps_percentiles['p95']):.2f}")
print(f"TPS p99: {(-tps_percentiles['p99']):.2f}")
print(f"TPS p99.9: {(-tps_percentiles['p99.9']):.2f}")
weighted_average = calculate_total_weighted_mttfb_average(data)
mttfb_percentiles = calculate_percentiles(data, 'Mean_time_to_first_byte', [50, 95, 99, 99.9])
print(f"MTTFB 평균: {(weighted_average):.2f} ms")
print(f"MTTFB p50: {(mttfb_percentiles['p50']):.2f} ms")
print(f"MTTFB p95: {(mttfb_percentiles['p95']):.2f} ms")
print(f"MTTFB p99: {(mttfb_percentiles['p99']):.2f} ms")
print(f"MTTFB p99.9: {(mttfb_percentiles['p99.9']):.2f} ms")
diff_average = calculate_average_mttfb_difference(data)
print(f"MTTFB 차이 평균 : {(diff_average):.2f} ms")
variation = calculate_mttfb_variation(data)
print(f"MTTFB 평균적인 변동률: {(variation):.2f}%")
locale.setlocale(locale.LC_ALL, original_locale)