반응형

[성능 개선 이야기] 99%가 3초 이내였습니다. 나머지 1%는 1분이었습니다

안녕하세요, 가야태자 @talkit 입니다.

지난 글에서 목표를 400 TPS가 아니라 40 TPS로 잡은 이야기를 했습니다.

오늘은 그 다음 이야기입니다. 테스트 결과를 어떻게 읽었는가에 대한 이야기죠.

그리고 이건 제가 하마터면 통과 도장을 찍을 뻔한 이야기이기도 합니다.


40 TPS까지 올렸습니다

앞 글에서 말씀드린 대로 부하를 단계적으로 올렸습니다.

1 TPS   → 전 구간 3초 이내
10 TPS  → 전 구간 3초 이내
20 TPS  → 전 구간 3초 이내
40 TPS  → ?

20까지는 아주 깨끗했습니다. 그래서 40도 무난하겠거니 했습니다.

40 TPS 결과가 나왔습니다.

전체 요청의 99%가 3초 이내

숫자만 보면 훌륭합니다. 목표는 40 TPS였고, 응답 시간 기준도 만족했습니다.

여기서 끝냈으면 통과였습니다.


그런데 메뉴 하나가 1분이 넘었습니다

결과를 메뉴별로 펼쳐 보다가 발견했습니다.

특정 메뉴 하나가 1분 이상 걸리고 있었습니다.

3초 기준인데 1분입니다. 20배가 아니라 20배 이상이죠. 그런데도 전체 지표는 99%를 만족하고 있었습니다.

당연합니다. 그 메뉴는 전체 요청 중 아주 작은 비중이었으니까요. 1%도 안 되는 요청이 아무리 심하게 느려도, 99%라는 숫자는 흔들리지 않습니다.


만약 요약만 봤다면

이 부분이 이 글에서 제일 하고 싶은 이야기입니다.

성능 테스트 결과를 볼 때 보통 이런 숫자들을 봅니다.

  • 평균 응답 시간
  • 90% / 95% / 99% 백분위
  • TPS
  • 에러율

이 숫자들은 전부 뭉쳐진 값입니다. 그리고 뭉쳐진 값은 소수의 심각한 문제를 지웁니다.

만약 이 프로젝트의 성능 기준이 "전체 요청의 99%가 3초 이내" 였다면 어떻게 됐을까요?

계약상으로는 합격입니다. 보고서에 통과라고 쓰고, 도장 찍고, 오픈합니다.

그리고 오픈한 다음에 그 메뉴를 쓰는 담당자가 이렇게 말하겠죠.

"이 화면은 눌러 놓고 커피 마시고 와야 해요."

그분 입장에서 시스템은 망가진 상태입니다. 그런데 성능 테스트는 통과했습니다. 둘 다 사실입니다.


어떻게 찾았나

특별한 방법은 아닙니다. 메뉴별로 쪼개서 봤을 뿐입니다.

JMeter의 Aggregate Report는 원래 샘플러별로 행이 나옵니다. 즉 메뉴별 수치가 이미 다 나와 있습니다. 문제는 사람들이 맨 아래 전체 요약(Total) 행만 보는 것이죠.

그리고 하나 더, 최댓값(Max)을 꼭 보셔야 합니다.

평균과 백분위만 보면 이런 문제는 안 보입니다. 그런데 Max 열을 보면 60,000ms 같은 숫자가 덩그러니 찍혀 있습니다. 눈에 확 띕니다.

정리하면 이렇습니다.

  • 전체 요약이 아니라 거래(메뉴)별로 볼 것
  • 평균이 아니라 최댓값도 볼 것

이 두 가지만 지켜도 웬만한 건 걸러집니다.


원인은 INSERT였습니다

해당 메뉴를 들여다보니 이런 구조였습니다.

100건이 넘는 데이터를 저장하는데, INSERT 문을 한 건씩 100번 실행하고 있었습니다.

for (건수만큼) {
    INSERT ... 1건
}

이러면 데이터베이스와 100번 왕복합니다. 요청 보내고, 응답 받고, 또 보내고, 또 받고요.

한 번의 왕복이 아무리 짧아도 100번이면 쌓입니다. 게다가 사용자가 여러 명이 동시에 이 화면을 쓰면, 그 왕복들이 전부 겹칩니다.

그래서 40에서만 터진 겁니다

여기가 앞 글과 이어지는 지점입니다.

부하가 낮을 때는 이 구조도 그럭저럭 넘어갑니다. 왕복 100번이 좀 느려도 견딜 만하죠.

그런데 동시 사용자가 늘어나면 이야기가 달라집니다. 커넥션을 잡고 있는 시간이 길어지고, 그러면 다음 사람이 기다리고, 기다리는 사람이 쌓이면서 한 번에 무너집니다.

성능 문제는 부하에 비례해서 나빠지지 않습니다. 어느 지점까지는 멀쩡하다가 임계를 넘으면 급격히 나빠집니다. 그래서 목표 부하까지 반드시 올려 봐야 합니다.

앞 글에서 목표를 400이 아니라 40으로 제대로 잡은 게 여기서 값을 했습니다. 400으로 갔다면 모든 게 다 느려져서 이 메뉴 하나를 골라낼 수 없었을 겁니다.


조치는 간단했습니다

한 번에 넣도록 바꿨습니다.

INSERT 100번  →  일괄 INSERT 1번

JDBC라면 addBatch()로 모아서 executeBatch()로 한 번에 보내는 방식이고, 쿼리 차원에서 여러 행을 한 번에 넣는 방법도 있습니다.

왕복 100번이 1번이 됐습니다. 그리고 해당 메뉴 응답 시간이 3초 이내로 들어왔습니다.

원인을 찾는 데는 시간이 걸렸는데, 고치는 데는 오래 걸리지 않았습니다. 성능 개선은 대체로 이렇습니다. 찾는 게 일이고 고치는 건 금방입니다.

결국 왕복 횟수 이야기입니다

예전에 다른 프로젝트에서 대량 조회가 느려서 Oracle의 SDU와 JDBC FetchSize를 조정한 적이 있습니다. 그것도 결국 데이터베이스와 몇 번 왕복하느냐의 문제였습니다.

  • 읽을 때 — 한 번에 몇 행씩 가져올 것인가 (FetchSize)
  • 쓸 때 — 한 번에 몇 행씩 보낼 것인가 (배치)

대용량 처리에서 느리다면, 먼저 왕복 횟수부터 세어 보시길 권합니다. 쿼리 튜닝보다 먼저 볼 곳인 경우가 많습니다.


그래서 남은 생각

집계 지표는 소수를 지웁니다

평균도, 99% 백분위도 마찬가지입니다. 비중이 작으면 아무리 심해도 숫자에 안 나타납니다.

그런데 사용자는 평균으로 시스템을 쓰지 않습니다. 자기가 쓰는 그 화면 하나로 시스템을 판단합니다.

그래서 "99% 이내"라는 기준은 위험합니다

성능 목표를 잡을 때 "전체 요청의 99%가 N초 이내"로 쓰는 경우가 많습니다. 편하고, 흔한 방식입니다.

그런데 이 기준은 오늘 같은 상황을 통과시킵니다. 기준을 만족했는데 시스템은 못 쓰는 상태가 되는 거죠.

이렇게 잡으시길 권합니다.

  • 전체가 아니라 거래별로 기준을 둘 것
  • 백분위와 함께 최댓값 기준도 둘 것 (예: 어떤 거래도 N초를 넘지 않을 것)

숫자 하나로 요약하고 싶은 마음은 이해합니다. 그런데 요약하는 순간 뭔가는 지워집니다. 무엇이 지워지는지 알고 요약해야 합니다.


마무리

성능 테스트에서 제일 위험한 순간은 결과가 나쁠 때가 아닙니다. 결과가 좋아 보일 때입니다.

나쁘면 원인을 찾게 되지만, 좋으면 거기서 멈추니까요.

99%라는 숫자를 보고 끝냈다면, 그 메뉴는 오픈하고 나서야 발견됐을 겁니다. 그때는 고치는 비용도, 설명하는 비용도 훨씬 컸겠죠 ^^;;

요약된 숫자를 볼 때는 한 번쯤 펼쳐 보시길 권합니다.

즐거운 하루 되십시오.

반응형

+ Recent posts