9장 예측
064 엔터프라이즈 아키텍트 리차드
리차드(실명)는 텍사스 어빙에 본사를 둔 글로벌 편의점 체인의 엔터프라이즈 아키텍트였다.
리차드의 회사는 새로 출시한 오라클 애플리케이션의 여러 기능에서 성능 문제를 겪고 있었다.
특히 그중에서도 [재료 규격 검색]이라는 기능이 전체 구현의 핵심이었다.
이 회사는 지구상에서 가장 특별한 컴퓨터를 도입한 상태였음에도 테스트 주방의 셰프가 새 레시피에 사용할 수 있는 치즈 종류를 확인하는 데 거의 2분이나 걸렸다.
구문별로 집계한 추적 프로파일은 믿기 어려운 것이었다.
회의실에 있던 모든 사람들이 “얼마나 걸리나” 질문(약 2분)뿐만 아니라 “왜” 질문에 대한 답을 알 수 있을 정도였다.
| Statement |
Duration seconds |
Duration % |
| SELECT executions, end_of_fetch_count, elapsed_t… |
97.676 |
88.6% |
| select gsmFormulaOutputTypeMML.Name from M… |
4.862 |
4.4% |
| select gsmMaterialTypeMML.Name from MaterialS… |
4.593 |
4.2% |
| SELECT Thumbnail, SpecNumber, SpecName, Shor… |
2.089 |
1.9% |
| 59 other statements |
0.979 |
0.9% |
| Total (63) |
110.200 |
100.0% |
총 110초의 실행 시간 중 거의 100초가량이 하나의 구문에 의해 소비
되고 있었다.
그건 애플리케이션이 실행하는 구문이 아니었다!
이 구문은 다른 3개의 구문을 더 빠르게 실행시키도록 돕기 위해 데이터베이스가 추가한 기능이었던 것이다.
우리는 그 기능을 바로 비활성화했다.
“만일 ~한다면” 질문의 대답은 “약 97.676초”라고 예상했다면 맞는 추측이 된다.
데이터베이스 기능을 비활성화한 후의 새로운 프로파일은 다음과 같았다.
| Statement |
Duration seconds |
Duration % |
| select gsmMaterialTypeMML.Name from MaterialS… |
4.577 |
39.3% |
| select gsmFormulaOutputTypeMML.Name from M… |
4.520 |
38.8% |
| SELECT Thumbnail, SpecNumber, SpecName, Shor… |
2.175 |
18.7% |
| select default$ from col$ where rowid=:1 |
0.233 |
2.0% |
| 27 other statements |
0.141 |
1.2% |
| Total (31) |
11.647 |
100.0% |
이 시점에서 당연히 다음 질문은 “그 밖에?”였다.
문제를 다 해결한 것일까?
새 프로파일을 서브루틴으로 그룹화해보니 그 답을 알 수 있었다.
| Subroutine |
Duration seconds |
Duration % |
Count |
Mean |
| CPU: PARSE dbcalls |
7.817 |
67.1% |
881 |
0.008 |
| SQL*Net message from client |
1.689 |
14.5% |
1,772 |
0.000 |
| unaccounted-for between calls |
1.078 |
9.3% |
7,060 |
0.000 |
| CPU: FETCH dbcalls |
0.508 |
4.4% |
2,490 |
0.000 |
| 10 other subroutines |
0.555 |
4.8% |
13,544 |
0.000 |
| Total (14) |
11.647 |
100.0% |
25,747 |
0.000 |
능력 있는 오라클 분석 가라면 호출 해석 PARSE 작업이 프로파일의 대부분을 차지해서는 안 된다는 사실을 잘 알 것이다.
그래서 간단한 질문을 다시 한번 반복해봤다.
리차드는 우리가 애플리케이션을 어디까지 더 빠르게 만들 수 있는지 궁금하다며 애플리케이션의 수정이 필요하다는 점에 동의했다.
코드 재작성의 효과는 확실했다.
| Subroutine |
Duration seconds |
Duration % |
Count |
Mean |
| SQL*Net message from client |
0.472 |
53.8% |
10 0.047 |
184 |
| CPU: FETCH dbcalls |
0.247 |
28.1% |
542 |
0.000 |
| CPU: EXEC dbcalls |
0.140 |
16.0% |
545 |
0.000 |
| unaccounted-for between calls |
0.008 |
0.9% |
46 |
0.000 |
| 8 other subroutines |
0.011 |
1.3% |
2,243 |
0.000 |
| Total (12) |
0.877 |
100.0% |
3,386 |
0.000 |
우리가 한 일이라고는 그저, 적절한 지점을 ‘관찰’ 하고 불필요한 호출을 제거한 것뿐이었다.
먼저 필요하지 않은 구문의 실행을 제거한 후, 애플리케이션을 재작성해서 오라클의 성능 관련 안티패턴을 제거한 것이다.
이 시점에서 “그 밖에?”라는 질문의 답은 그다음 문제를 해결하는 것이었다.
065 예측이 필요한 이유
예측하는 능력은 실제로 연습을 통해 기르고 향상시킬 수 있는 실질적인 스킬이다.
예측을 잘 이용하면, 엉뚱한 생각으로 돈과 시간을 낭비하는 일을 막을 수 있다.
예측 능력이 좋은 사람은 상대적으로 낮은 위험을 감수하면서도 더 대담한 결과를 도출할 수 있다.
그런 사람들은 시장에 큰 경쟁우위를 가져다준다.
066 프로파일을 통한 예측
예측 능력을 높이는 가장 빠른 방법은 피드백을 활용하는 것이다.
- 최선을 다해 뭔가를 예측한다.
- 예측한 가설을 실행하고 측정한 다음, 예측한 내용과 측정한 내용을 비교해본다. 예측에서 어떤 부분이 맞았고 어떤 부분은 틀렸는지를 확인한 후, 이를 토대로 다음 가설을 예측해본다. 다시 1번 단계로 돌아간다.
예측 능력을 높이는 가장 빠른 방법은 피드백을 바탕 으로 꾸준히 연습하는 것이다.
067 할지 말지에 대한 예측
초당 트랜잭션 Transactions per second (TPS) 보고서가 느려져서 프로파일을 확보해보니 다음과 같은 결과가 나타났다고 가정해보자.
| Event |
Duration seconds |
Duration % |
Count |
Mean |
| disk read |
1 |
0.1% |
10 |
0.100 |
| non-disk stuff |
999 |
99.9% |
4,990 |
0.200 |
| Total |
1,000 |
100.0% |
5,000 |
0.200 |
프로파일이 정확하다면 빠른 스토리지를 구매해도 TPS 보고서의 성능은 0.1% 이상 개선되지 않는다고 100% 확신할 수 있을 것이다.
프로파일이 있다면 이 정도로 쉽게 할 수 있는 예측도 있다.
068 선형 동작

이 방법에서 우리는 성능을 개선할 수 있는 방법이 다음과 같은 단 2가
지뿐임을 쉽게 확인할 수 있다.
- 실행 횟수(C 열의 값)를 줄이거나
- 실행당 평균 실행 시간(D 열의 값)을 줄이기
B 열의 각 값은 같은 행의 C 열과 D 열의 곱이므로, 입력값을 변경하면 어떤 일이 일어날지 쉽게 예상할 수 있다.
- C2 열의 값에 0.2를 곱하면 B2 열의 값도 원래 값의 0.2배가 된다.
- D3 열의 값을 원래 값보다 0.09배 줄이면 B3 열의 값도 원래 값의
0.09배로 줄어든다.
- C4 열의 값에 0.50를 곱하고 D4 열의 값에 0.33을 곱하면 B4 열의 값은 원래 값의 0.50 × 0.33배가 된다.
왜곡, 상호의존성, 대기열 때문에 여러분이 예측에 사용하려는 숫자가 생각만큼 명확하지 않을 수도 있다.
069 왜곡
| Subroutine |
Duration seconds |
Duration % |
Count |
Mean |
| single-block read |
60,499 |
76.8% |
10,013,394 |
0.006 042 |
| other |
18,290 |
23.2% |
919,906 |
0.019 882 |
| Total |
78,789 |
100.0% |
10,933,300 |
0.007 206 |
여기서 몇 가지 간단한 예측을 해볼 수 있다.
- ‘단일 블록 읽기’ 호출의 평균 실행 시간을 0.006초에서 0.004초로
줄인다면 시간을 얼마나 절약할 수 있을까?
10,013,394호출 × (0.006 - 0.004)초/호출 ≈ 약 20,000초 정도를 절약할 수 있다.
- 10,013,394회의 ‘단일 블록 읽기’ 호출을 제거하면 시간을 얼마나 절약할 수 있을까?
60,499초, 즉 약 17시간을 절약할 수 있다.
- ‘단일 블록 읽기’ 호출의 절반을 제거한다면 시간을 얼마나 절약할수 있을까?
60,499초의 절반을 절약할 수 있을 것이다. 맞을까? 사실은… 그렇지 않을 것이다. 그럴 수도 있겠지만 가능성은 낮다.
왜곡은 데이터의 비균일성이다.
어떤 호출은 0.006초보다 훨씬 적게 걸리기도 하고 어떤 호출은 그보다 훨씬 오래 걸리기도 한다.
이 호출의 절반을 제거 했을 때 절약할 수 있는 시간은 어떤 호출이 제거되느냐에 따라 달라진다.
다음은 ‘단일 블록 읽기’ 호출을 실행 시간으로 그룹화한 프로파일이
다.
이는 11개의 버킷 bucket 으로 나뉜다.
|
Range (seconds) {min ≤ duration < max} |
Duration Seconds |
Duration % |
Calls |
| 1. |
0.000 000 0.000 001 |
|
|
|
| 2. |
0.000 001 0.000 010 |
0 |
0.0% |
15 |
| 3. |
0.000 010 0.000 100 |
204 |
0.3% |
9,345,811 |
| 4. |
0.000 100 0.001 000 |
22 |
0.0% |
108,181 |
| 5. |
0.001 000 0.010 000 |
595 |
1.0% |
103,462 |
| 6. |
0.010 000 0.100 000 |
11,194 |
18.5% |
315,149 |
| 7. |
0.100 000 1.000 000 |
26,915 |
44.5% |
133,382 |
| 8. |
1.000 000 10.000 000 |
21,353 |
35.3% |
7,375 |
| 9. |
10.000 000 100.000 000 |
217 |
0.4% |
19 |
| 10. |
100.000 000 1,000.000 000 |
|
|
|
| 11. |
1,000.000 000 +∞ |
|
|
|
|
Total (11) |
60,499 |
100% |
10,013,394 |
3번 버킷의 900만개 호출을 제거하더라도 프로그램의 실행 시간은 겨우 204초 단축된다는 사실을 알 수 있다.
반면 7번 버킷의 133,382개 호출을 제거하면 26,915초(7.5시간)을 단축시킬 수 있다.
8번 버킷의 7,375개 호출을 제거하면 21,353초(5.9시간)를 더 단축시킬 수 있다.
최상의 경우는 6~9번 버킷을 제거한 경우로 이 경우에는 채 5%도 되지 않는 호출을 제거함으로써 98.7%의 차이를 만들어 낼 수 있다.
목록상의 왜곡에 대해 알기 전까지는 얼마나 많은 값이 평균값과 유사한지조차 우리는 알 수 없다.
그렇기 때문에 추적을 통해 세부 정보를 얻는 것이 좋다.
070 이벤트 간의 상호의존성
데이터베이스 버퍼 캐시를 적게 활용할수록 캐시 일관성 작업도 더 적게 수행할 수 있다.
즉 이벤트 실행을 제거하면 기대했던 것보다 더 많은 부분이 개선된다.
하지만 상호의존성이 불리하게 작용하는 경우도 있다.
내가 두통약을덜 사면 진통제는 더 많이 사야 하는 것과 유사하다.
이벤트 간의 상호의존성을 이해하면 예측의 정확도를 높일 수 있다.
시간을 들여서라도 여러분의 예측을 평가하고 실수로부터 배우려 한다면 여러분의 시스템에서 발생하는 이벤트 간 상호의존성에 대해 빠르게 학습할 수 있을 것이다.
071 비선형 동작
| Subroutine |
Duration |
Count |
Mean |
| queued for CPU |
6.819 |
20,265 |
0.000 336 |
| other (CPU, disk, etc.) |
4.782 |
50,295 |
0.000 095 |
| Total |
11.601 |
70,560 |
0.000 164 |
이 데모의 핵심은 소스 코드를 조금만 수정해서 (CPU 대기를 유발하는) 데이터베이스 호출을 2만 번에서 5천 번으로 줄일 수 있음을 보여주는 것이었다.
다음 표처럼 호출 수를 원래 값에서 25% 정도로 줄이면 실행 시간 역시 25% 정도로 줄어든다고 생각하는 편이 합리적일 것이다.
| Subroutine |
Duration |
Count |
Mean |
| queued for CPU (predicted) |
1.705(0.25 × 6.819) |
5,066(0.25 × 20,265) |
0.000 336 |
하지만 실제로는 호출 수를 25%로 줄였을 때 실행 시간은 원래 값에서 11% 수준으로 줄어든다.
| Subroutine |
Duration |
Count |
Mean |
| queued for CPU (actual) |
0.769 |
5,003 |
0.000 154 |
예측이 빗나간 이유는, 호출 수를 줄이면 호출당 평균 실행 시간 역시 0.000 336초에서 0.000 154초로 줄어든다는 사실을 고려하지 않았기 때문이다.
소스 코드를 변경하기 전에는 높은 호출 수와 높은 동시성이 결합되어 호출 응답 시간에 대기에 따른 지연이 추가됐었다.
하지만 각 실행의 호출 수를 줄이자 트래픽 강도가 낮아져서 호출당 응답 시간이 낮아진 것이다.
트래픽 강도 ρ = 0.9이고 평균 대기 지연 Q = 4.26 S(4.26서비스 시간)인 M/M/2 시스템을 생각해보자.
트래픽 강도를 3분의 1로 줄인다면 어떤 일이 벌어질까?
직감적으로 트래픽 강도가 3분의 1로 줄어든다면 대기 지연 역시 3분의 1로 줄어들 것이라고 예상할 것이다.
즉 예측을 수식으로 표현해보면 Q = 4.26 S × (1 - 1/3) = 2.84 S가 된다.
하지만 실제로는 ρ = 0.6일 때의 대기 지연은 이보다 훨씬 나아서 Q = 0.56 S가 된다.

부하가 큰 시스템에 (아주 약간이라도) 부하를 더하면 응답 시간은 급증하게 된다.
하지만 반대로 무거운 작업 부하를 (아주 조금이라도!) 줄일 수만 있다면 응답 시간이 엄청나게 개선되는 효과로 이어진다.
곡선의 일반적인 형태를 이해하는 것은 중요하다.
낭비되는 작업 부하를 조금만 신경써서 제거한다면 응답 시간이 기대 이상으로 좋아 지는 이유를 이해할 수 있을 것이다.
고부하 상황에서 낭비되는 부하를 제거하면 여러분이 기대하는 것보다 더 나은 개선 효과를 얻을 수 있다.
9장 예측
064 엔터프라이즈 아키텍트 리차드
리차드(실명)는 텍사스 어빙에 본사를 둔 글로벌 편의점 체인의 엔터프라이즈 아키텍트였다.
리차드의 회사는 새로 출시한 오라클 애플리케이션의 여러 기능에서 성능 문제를 겪고 있었다.
특히 그중에서도 [재료 규격 검색]이라는 기능이 전체 구현의 핵심이었다.
이 회사는 지구상에서 가장 특별한 컴퓨터를 도입한 상태였음에도 테스트 주방의 셰프가 새 레시피에 사용할 수 있는 치즈 종류를 확인하는 데 거의 2분이나 걸렸다.
구문별로 집계한 추적 프로파일은 믿기 어려운 것이었다.
회의실에 있던 모든 사람들이 “얼마나 걸리나” 질문(약 2분)뿐만 아니라 “왜” 질문에 대한 답을 알 수 있을 정도였다.
총 110초의 실행 시간 중 거의 100초가량이 하나의 구문에 의해 소비
되고 있었다.
그건 애플리케이션이 실행하는 구문이 아니었다!
이 구문은 다른 3개의 구문을 더 빠르게 실행시키도록 돕기 위해 데이터베이스가 추가한 기능이었던 것이다.
우리는 그 기능을 바로 비활성화했다.
“만일 ~한다면” 질문의 대답은 “약 97.676초”라고 예상했다면 맞는 추측이 된다.
데이터베이스 기능을 비활성화한 후의 새로운 프로파일은 다음과 같았다.
이 시점에서 당연히 다음 질문은 “그 밖에?”였다.
문제를 다 해결한 것일까?
새 프로파일을 서브루틴으로 그룹화해보니 그 답을 알 수 있었다.
능력 있는 오라클 분석 가라면 호출 해석 PARSE 작업이 프로파일의 대부분을 차지해서는 안 된다는 사실을 잘 알 것이다.
그래서 간단한 질문을 다시 한번 반복해봤다.
얼마나 걸리나?
11.6초
왜?
881개의 DB 호출 해석 때문에
만일 ...한다면 구문 해석 작업 대부분을 제거한다면?
거의 8초의 해석 작업과 수많은 네트워크 라운드 트립 그리고 상당한 호출 간 불분명한 루프 시간을 없앨 수 있을 것이다. 그렇게 하면 재료 검색은 1~2초 정도면 끝날 것이다.
리차드는 우리가 애플리케이션을 어디까지 더 빠르게 만들 수 있는지 궁금하다며 애플리케이션의 수정이 필요하다는 점에 동의했다.
코드 재작성의 효과는 확실했다.
우리가 한 일이라고는 그저, 적절한 지점을 ‘관찰’ 하고 불필요한 호출을 제거한 것뿐이었다.
먼저 필요하지 않은 구문의 실행을 제거한 후, 애플리케이션을 재작성해서 오라클의 성능 관련 안티패턴을 제거한 것이다.
이 시점에서 “그 밖에?”라는 질문의 답은 그다음 문제를 해결하는 것이었다.
065 예측이 필요한 이유
예측하는 능력은 실제로 연습을 통해 기르고 향상시킬 수 있는 실질적인 스킬이다.
예측 능력이 좋은 사람은 상대적으로 낮은 위험을 감수하면서도 더 대담한 결과를 도출할 수 있다.
그런 사람들은 시장에 큰 경쟁우위를 가져다준다.
066 프로파일을 통한 예측
예측 능력을 높이는 가장 빠른 방법은 피드백을 활용하는 것이다.
067 할지 말지에 대한 예측
초당 트랜잭션 Transactions per second (TPS) 보고서가 느려져서 프로파일을 확보해보니 다음과 같은 결과가 나타났다고 가정해보자.
프로파일이 정확하다면 빠른 스토리지를 구매해도 TPS 보고서의 성능은 0.1% 이상 개선되지 않는다고 100% 확신할 수 있을 것이다.
프로파일이 있다면 이 정도로 쉽게 할 수 있는 예측도 있다.
068 선형 동작
이 방법에서 우리는 성능을 개선할 수 있는 방법이 다음과 같은 단 2가
지뿐임을 쉽게 확인할 수 있다.
B 열의 각 값은 같은 행의 C 열과 D 열의 곱이므로, 입력값을 변경하면 어떤 일이 일어날지 쉽게 예상할 수 있다.
0.09배로 줄어든다.
왜곡, 상호의존성, 대기열 때문에 여러분이 예측에 사용하려는 숫자가 생각만큼 명확하지 않을 수도 있다.
069 왜곡
여기서 몇 가지 간단한 예측을 해볼 수 있다.
줄인다면 시간을 얼마나 절약할 수 있을까?
10,013,394호출 × (0.006 - 0.004)초/호출 ≈ 약 20,000초 정도를 절약할 수 있다.
60,499초, 즉 약 17시간을 절약할 수 있다.
60,499초의 절반을 절약할 수 있을 것이다. 맞을까? 사실은… 그렇지 않을 것이다. 그럴 수도 있겠지만 가능성은 낮다.
왜곡은 데이터의 비균일성이다.
어떤 호출은 0.006초보다 훨씬 적게 걸리기도 하고 어떤 호출은 그보다 훨씬 오래 걸리기도 한다.
이 호출의 절반을 제거 했을 때 절약할 수 있는 시간은 어떤 호출이 제거되느냐에 따라 달라진다.
다음은 ‘단일 블록 읽기’ 호출을 실행 시간으로 그룹화한 프로파일이
다.
이는 11개의 버킷 bucket 으로 나뉜다.
3번 버킷의 900만개 호출을 제거하더라도 프로그램의 실행 시간은 겨우 204초 단축된다는 사실을 알 수 있다.
반면 7번 버킷의 133,382개 호출을 제거하면 26,915초(7.5시간)을 단축시킬 수 있다.
8번 버킷의 7,375개 호출을 제거하면 21,353초(5.9시간)를 더 단축시킬 수 있다.
최상의 경우는 6~9번 버킷을 제거한 경우로 이 경우에는 채 5%도 되지 않는 호출을 제거함으로써 98.7%의 차이를 만들어 낼 수 있다.
목록상의 왜곡에 대해 알기 전까지는 얼마나 많은 값이 평균값과 유사한지조차 우리는 알 수 없다.
그렇기 때문에 추적을 통해 세부 정보를 얻는 것이 좋다.
070 이벤트 간의 상호의존성
데이터베이스 버퍼 캐시를 적게 활용할수록 캐시 일관성 작업도 더 적게 수행할 수 있다.
즉 이벤트 실행을 제거하면 기대했던 것보다 더 많은 부분이 개선된다.
하지만 상호의존성이 불리하게 작용하는 경우도 있다.
내가 두통약을덜 사면 진통제는 더 많이 사야 하는 것과 유사하다.
이벤트 간의 상호의존성을 이해하면 예측의 정확도를 높일 수 있다.
시간을 들여서라도 여러분의 예측을 평가하고 실수로부터 배우려 한다면 여러분의 시스템에서 발생하는 이벤트 간 상호의존성에 대해 빠르게 학습할 수 있을 것이다.
071 비선형 동작
이 데모의 핵심은 소스 코드를 조금만 수정해서 (CPU 대기를 유발하는) 데이터베이스 호출을 2만 번에서 5천 번으로 줄일 수 있음을 보여주는 것이었다.
다음 표처럼 호출 수를 원래 값에서 25% 정도로 줄이면 실행 시간 역시 25% 정도로 줄어든다고 생각하는 편이 합리적일 것이다.
하지만 실제로는 호출 수를 25%로 줄였을 때 실행 시간은 원래 값에서 11% 수준으로 줄어든다.
예측이 빗나간 이유는, 호출 수를 줄이면 호출당 평균 실행 시간 역시 0.000 336초에서 0.000 154초로 줄어든다는 사실을 고려하지 않았기 때문이다.
소스 코드를 변경하기 전에는 높은 호출 수와 높은 동시성이 결합되어 호출 응답 시간에 대기에 따른 지연이 추가됐었다.
하지만 각 실행의 호출 수를 줄이자 트래픽 강도가 낮아져서 호출당 응답 시간이 낮아진 것이다.
트래픽 강도 ρ = 0.9이고 평균 대기 지연 Q = 4.26 S(4.26서비스 시간)인 M/M/2 시스템을 생각해보자.
트래픽 강도를 3분의 1로 줄인다면 어떤 일이 벌어질까?
직감적으로 트래픽 강도가 3분의 1로 줄어든다면 대기 지연 역시 3분의 1로 줄어들 것이라고 예상할 것이다.
즉 예측을 수식으로 표현해보면 Q = 4.26 S × (1 - 1/3) = 2.84 S가 된다.
하지만 실제로는 ρ = 0.6일 때의 대기 지연은 이보다 훨씬 나아서 Q = 0.56 S가 된다.
부하가 큰 시스템에 (아주 약간이라도) 부하를 더하면 응답 시간은 급증하게 된다.
하지만 반대로 무거운 작업 부하를 (아주 조금이라도!) 줄일 수만 있다면 응답 시간이 엄청나게 개선되는 효과로 이어진다.
곡선의 일반적인 형태를 이해하는 것은 중요하다.
낭비되는 작업 부하를 조금만 신경써서 제거한다면 응답 시간이 기대 이상으로 좋아 지는 이유를 이해할 수 있을 것이다.