내가 네부캠을 하면서 진행했던 최종 프로젝트는 Code Clash
라는 아케이드 알고리즘 대전 플랫폼이다. 채점 서버 대부분과 api 서버 전반적인 부분을 맡아 개발했다. 채점 서버는 다음과 같은 구조로 이루어져 있다.
api 서버에서 채점 요청을 받으면, 데이터베이스에서 해당 문제와 관련된 모든 테스트케이스를 가져온다. 그리고 테스트케이스 하나에 대한 채점을 각각의 채점 서버에게 RR
방식으로 하나씩 넘겨준다. 채점 서버는 자식 프로세스를 생성하여 제출된 코드와 테스트케이스를 가지고 문제를 채점한다. api 서버는 모든 테스트케이스에 대한 채점이 완료될 때 까지 대기하고, 모든 채점이 완료되면 클라이언트에게 다시 응답해주는 방식이다.
그리고, 채점 컨테이너 하나당, CPU 1개와 1g mem을 할당받고 있다.
현재, 채점 서버는 코드의 실행 시간을 엄밀하게 측정하기 위해 한번에 하나의 테스트케이스만 채점하는 상황이다. 즉 한번에 하나의 자식 프로세스만 생성하여 그 프로세스로 하여금 코드를 실행하고 결과를 받아오는 식이다.
한번 이러한 상황에서 최악의 상황을 가정하여 채점 서버로 하여금 무한 루프를 도는 코드로 약 20개의 테스트케이스를 채점하게 하면 응답으로 받는 데에 얼마만큼의 시간이 걸릴까?
한번 테스트를 해보자. 부하 테스트로는 셋업이 간단하고 사용하기 간편한 artillery
를 사용했다.
다음의 세팅으로 테스트를 진행해보았다.
{
"config": {
"target": "https://codeclash.site",
"phases": [
{
"duration": 30,
"arrivalRate": 1
}
]
},
"scenarios": [
{
"flow": [
{
"post": {
"url": "/api/scores/grade?isExample=false",
"headers": {
"authorization": "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6Imd1c2Nrc2RuMTIzQG5hdmVyLmNvbSIsInN1YiI6MSwidHlwZSI6ImFjY2VzcyIsImlhdCI6MTcwMjk1NTY1NSwiZXhwIjoxNzAzMDQyMDU1fQ.dcj_8YiIUqS0J8uTexAYXVigBnCIL1lihD-npaDSsnM"
},
"json": {
"code": "function solution(n) { while(1) {} return n }",
"language": "javascript",
"problemId": "8"
}
}
}
]
}
]
}
30초 동안 매 초 1명의 사용자가 채점을 요청하게 했다. 해당 문제는 테스트케이스가 3개이므로 30초동안 90개의 테스트케이스를 채점하는 것이다. 그리고 제출 코드가 무한 루프를 돌게 했으므로 최악의 상황을 가정한 것이다. 해당 문제는 시간 제한이 1초이다.
다음과 같은 결과가 나왔다.
--------------------------------
Summary report @ 12:31:34(+0900)
--------------------------------
http.codes.200: ................................................................ 30
http.downloaded_bytes: ......................................................... 10110
http.request_rate: ............................................................. 1/sec
http.requests: ................................................................. 30
http.response_time:
min: ......................................................................... 1051
max: ......................................................................... 1425
mean: ........................................................................ 1267.1
median: ...................................................................... 1274.3
p95: ......................................................................... 1408.4
p99: ......................................................................... 1408.4
http.responses: ................................................................ 30
vusers.completed: .............................................................. 30
vusers.created: ................................................................ 30
vusers.created_by_name.0: ...................................................... 30
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1103.8
max: ......................................................................... 1440.7
mean: ........................................................................ 1283.7
median: ...................................................................... 1300.1
p95: ......................................................................... 1408.4
p99: ......................................................................... 1408.4
응답을 받는 데에 평균적으로 1200ms가 걸렸음을 볼 수 있다. 시간 제한이 1초이기 때문에 거의 대부분의 유저가 제출하자마자 응답을 받은 것으로 볼 수 있다. 그러면 이번에는 초당 2명의 사용자가 요청하는 것으로 바꿔보자.
"config": {
"target": "https://codeclash.site",
"phases": [
{
"duration": 10,
"arrivalRate": 2
}
]
이렇게 바꾸고 측정한 결과,
--------------------------------
Summary report @ 12:37:13(+0900)
--------------------------------
errors.ETIMEDOUT: .............................................................. 3
http.codes.200: ................................................................ 17
http.downloaded_bytes: ......................................................... 5729
http.request_rate: ............................................................. 2/sec
http.requests: ................................................................. 20
http.response_time:
min: ......................................................................... 1071
max: ......................................................................... 9326
mean: ........................................................................ 5444.2
median: ...................................................................... 5272.4
p95: ......................................................................... 9230.4
p99: ......................................................................... 9230.4
http.responses: ................................................................ 17
vusers.completed: .............................................................. 17
vusers.created: ................................................................ 20
vusers.created_by_name.0: ...................................................... 20
vusers.failed: ................................................................. 3
vusers.session_length:
min: ......................................................................... 1182.6
max: ......................................................................... 9341.4
mean: ........................................................................ 5467.3
median: ...................................................................... 5272.4
p95: ......................................................................... 9230.4
p99: ......................................................................... 9230.4
성능이 매우 떨어졌다. 채점 데이터가 쌓이는 순간 속도가 엄청나게 느려지는 것을 볼 수 있다. 평균 5400ms가 걸렸다.
아까 말했지만, 지금 서버는 한번에 하나의 테스트케이스만 채점하고 있다. 이것을 2개 이상으로 늘리면, 엄밀한 시간 측정은 힘들어지겠지만 성능을 개선할 수 있겠다고 생각했다. 따라서 한번에 채점할 수 있는 테스트케이스의 수를 하나 하나씩 늘리면서 성능 측정을 해보려고 한다. 일단 2개로 늘려보고 위와 같은 부하 테스트를 진행해보았다.
// before
app.use(queue({ activeLimit: 1, queuedLimit: -1 }));
// after
app.use(queue({ activeLimit: 2, queuedLimit: -1 }));
코드를 이렇게 수정했다. 이제 부하테스트를 해보자.
--------------------------------
Summary report @ 12:46:08(+0900)
--------------------------------
http.codes.200: ................................................................ 20
http.downloaded_bytes: ......................................................... 6740
http.request_rate: ............................................................. 2/sec
http.requests: ................................................................. 20
http.response_time:
min: ......................................................................... 1088
max: ......................................................................... 1505
mean: ........................................................................ 1295
median: ...................................................................... 1274.3
p95: ......................................................................... 1465.9
p99: ......................................................................... 1465.9
http.responses: ................................................................ 20
vusers.completed: .............................................................. 20
vusers.created: ................................................................ 20
vusers.created_by_name.0: ...................................................... 20
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1136.8
max: ......................................................................... 1520.6
mean: ........................................................................ 1316.5
median: ...................................................................... 1300.1
p95: ......................................................................... 1495.5
p99: ......................................................................... 1495.5
확연하게 줄은 것을 볼 수 있다!
평균값이 1200ms로 거의 4000ms가량 줄었다. max값은 1500ms로 8000ms가량 줄었다. 측정 시간의 엄밀함에 대한 희생으로 이정도의 개선을 보인 것이면 매우 좋은 tradeoff라고 생각한다. 3개, 4개 이렇게 쭉쭉 올려보자. 해놓고 내가 더 놀라는 것 같다 ㅋㅋㅋ
이번에는 최대 채점 가능한 프로세스의 개수를 3개로 해보자.
// before
app.use(queue({ activeLimit: 2, queuedLimit: -1 }));
// after
app.use(queue({ activeLimit: 3, queuedLimit: -1 }));
다시 부하 테스트를 진행해보자.
--------------------------------
Summary report @ 12:51:53(+0900)
--------------------------------
http.codes.200: ................................................................ 20
http.downloaded_bytes: ......................................................... 6740
http.request_rate: ............................................................. 2/sec
http.requests: ................................................................. 20
http.response_time:
min: ......................................................................... 1050
max: ......................................................................... 1198
mean: ........................................................................ 1104
median: ...................................................................... 1107.9
p95: ......................................................................... 1176.4
p99: ......................................................................... 1176.4
http.responses: ................................................................ 20
vusers.completed: .............................................................. 20
vusers.created: ................................................................ 20
vusers.created_by_name.0: ...................................................... 20
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1064.8
max: ......................................................................... 1223.5
mean: ........................................................................ 1126.2
median: ...................................................................... 1107.9
p95: ......................................................................... 1224.4
p99: ......................................................................... 1224.4
시간 제한과 일치하는, 거의 1초에 수렴하는 것을 볼 수 있다. 더 성능이 좋아졌다. 1초에 거의 수렴하니, 테스트 조건을 약간 바꿔서 해봐야 할 것 같다.
다음과 같이 변경했다.
"config": {
"target": "https://codeclash.site",
"phases": [
{
"duration": 10,
"arrivalRate": 4
}
]
초당 4명이 테스트케이스 3개에 대해서 요청하는 것으로 바꿔봤다. 즉 아까보다 2배의 부하가 들어오는 것이다.
다음과 같은 결과가 나왔다.
--------------------------------
Summary report @ 12:54:20(+0900)
--------------------------------
http.codes.200: ................................................................ 40
http.downloaded_bytes: ......................................................... 13480
http.request_rate: ............................................................. 4/sec
http.requests: ................................................................. 40
http.response_time:
min: ......................................................................... 1054
max: ......................................................................... 5367
mean: ........................................................................ 3049.3
median: ...................................................................... 3197.8
p95: ......................................................................... 4770.6
p99: ......................................................................... 4867
http.responses: ................................................................ 40
vusers.completed: .............................................................. 40
vusers.created: ................................................................ 40
vusers.created_by_name.0: ...................................................... 40
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1107.6
max: ......................................................................... 5381.7
mean: ........................................................................ 3067.4
median: ...................................................................... 3197.8
p95: ......................................................................... 4770.6
p99: ......................................................................... 4867
아까 테스트보다 2배의 부하를 걸어주니, 평균 시간이 3000ms로 증가했다. 다시 프로세스의 개수를 하나 더 늘려서 이것이 얼마나 감소하는지/증가하는지 살펴보도록 하자 무한정 늘린다고 성능이 계속 좋아지진 않을 것이다. 컨텍스트 스위칭으로 인한 오버헤드가 계속 커지기 때문이리라 생각하기 때문이다.
// before
app.use(queue({ activeLimit: 3, queuedLimit: -1 }));
// after
app.use(queue({ activeLimit: 4, queuedLimit: -1 }));
다음과 같이 4개로 바꿔주었고, 위에서 측정했던 것과 동일하게 부하 테스트를 해보았다.
--------------------------------
Summary report @ 12:59:58(+0900)
--------------------------------
http.codes.200: ................................................................ 40
http.downloaded_bytes: ......................................................... 13480
http.request_rate: ............................................................. 4/sec
http.requests: ................................................................. 40
http.response_time:
min: ......................................................................... 1128
max: ......................................................................... 2068
mean: ........................................................................ 1587.5
median: ...................................................................... 1587.9
p95: ......................................................................... 1978.7
p99: ......................................................................... 2018.7
http.responses: ................................................................ 40
vusers.completed: .............................................................. 40
vusers.created: ................................................................ 40
vusers.created_by_name.0: ...................................................... 40
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1197.8
max: ......................................................................... 2080.7
mean: ........................................................................ 1606.4
median: ...................................................................... 1587.9
p95: ......................................................................... 1978.7
p99: ......................................................................... 2018.7
위와 같은 결과가 나왔다. 평균 1500ms가 걸렸다. 다시 성능이 훨씬 개선된 것을 볼 수 있다.
이 과정을 계속 반복하여 어디까지 성능이 올라가나 살펴보자. 다시 4개에서 5개로 최대 채점 개수를 늘려주었다.
// before
app.use(queue({ activeLimit: 4, queuedLimit: -1 }));
// after
app.use(queue({ activeLimit: 5, queuedLimit: -1 }));
그리고 성능 측정을 다시 한번 해봤다.
--------------------------------
Summary report @ 13:05:08(+0900)
--------------------------------
http.codes.200: ................................................................ 40
http.downloaded_bytes: ......................................................... 13480
http.request_rate: ............................................................. 4/sec
http.requests: ................................................................. 40
http.response_time:
min: ......................................................................... 1094
max: ......................................................................... 1420
mean: ........................................................................ 1217.1
median: ...................................................................... 1224.4
p95: ......................................................................... 1326.4
p99: ......................................................................... 1353.1
http.responses: ................................................................ 40
vusers.completed: .............................................................. 40
vusers.created: ................................................................ 40
vusers.created_by_name.0: ...................................................... 40
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1110.7
max: ......................................................................... 1435.7
mean: ........................................................................ 1236.4
median: ...................................................................... 1249.1
p95: ......................................................................... 1353.1
p99: ......................................................................... 1353.1
다시 성능이 매우 좋아진 것을 볼 수 있다.. 어디까지 좋아지는거지???..
그리고 평균 시간이 시간 제한인 1초에 다시 수렴하기 때문에 부하 테스트 조건을 다시 변경해주었다.
"phases": [
{
"duration": 10,
"arrivalRate": 8
}
]
초당 8명이 한명당 3개의 테스트케이스를 채점해달라고 요청하는 상황으로 변경했다. 다시 아까의 2배이다. 처음과 비교해서는 4배이다. 결과는 어떻게 나올까?
--------------------------------
Summary report @ 13:07:45(+0900)
--------------------------------
http.codes.200: ................................................................ 80
http.downloaded_bytes: ......................................................... 26960
http.request_rate: ............................................................. 5/sec
http.requests: ................................................................. 80
http.response_time:
min: ......................................................................... 1055
max: ......................................................................... 8227
mean: ........................................................................ 4567
median: ...................................................................... 4583.6
p95: ......................................................................... 7865.6
p99: ......................................................................... 8024.5
http.responses: ................................................................ 80
vusers.completed: .............................................................. 80
vusers.created: ................................................................ 80
vusers.created_by_name.0: ...................................................... 80
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1112.1
max: ......................................................................... 8241.8
mean: ........................................................................ 4583.7
median: ...................................................................... 4676.2
p95: ......................................................................... 7865.6
p99: ......................................................................... 8024.5
어떤 사용자는 최대 8초 정도를 기다렸고, 평균값은 4000ms이다. 이런식으로는 평생해도 못구한다고 판단해서 스케일을 엄청 크게 한번에 늘리게 했다.
우선 최대 채점 가능한 갯수를 20개로 변경하였다. 그리고 초당 요청의 수를 16개로 이전보다 2배로 늘렸다. 최초에 비교하면 8배이다.
// before
app.use(queue({ activeLimit: 5, queuedLimit: -1 }));
// after
app.use(queue({ activeLimit: 20, queuedLimit: -1 }));
"phases": [
{
"duration": 10,
"arrivalRate": 16
}
]
결과는 다음과 같다.
--------------------------------
Summary report @ 13:13:52(+0900)
--------------------------------
errors.ETIMEDOUT: .............................................................. 113
http.codes.200: ................................................................ 47
http.downloaded_bytes: ......................................................... 15839
http.request_rate: ............................................................. 11/sec
http.requests: ................................................................. 160
http.response_time:
min: ......................................................................... 1596
max: ......................................................................... 9568
mean: ........................................................................ 4908.3
median: ...................................................................... 3678.4
p95: ......................................................................... 9047.6
p99: ......................................................................... 9230.4
http.responses: ................................................................ 47
vusers.completed: .............................................................. 47
vusers.created: ................................................................ 160
vusers.created_by_name.0: ...................................................... 160
vusers.failed: ................................................................. 113
vusers.session_length:
min: ......................................................................... 1631.5
max: ......................................................................... 9583.8
mean: ........................................................................ 4926.1
median: ...................................................................... 3678.4
p95: ......................................................................... 9230.4
p99: ......................................................................... 9230.4
타임아웃이 113개가 나왔고, 200은 47개가 나왔다. 여기서 채점 가능한 최대 크기를 다시 30개로 늘려본 다음 동일한 부하 테스트를 진행해보았다.
// before
app.use(queue({ activeLimit: 20, queuedLimit: -1 }));
// after
app.use(queue({ activeLimit: 30, queuedLimit: -1 }));
다음과 같은 놀라운 결과가 나왔다.
--------------------------------
Summary report @ 13:18:18(+0900)
--------------------------------
errors.ETIMEDOUT: .............................................................. 117
http.codes.200: ................................................................ 43
http.downloaded_bytes: ......................................................... 14491
http.request_rate: ............................................................. 11/sec
http.requests: ................................................................. 160
http.response_time:
min: ......................................................................... 1617
max: ......................................................................... 9780
mean: ........................................................................ 4811
median: ...................................................................... 5711.5
p95: ......................................................................... 8024.5
p99: ......................................................................... 9230.4
http.responses: ................................................................ 43
vusers.completed: .............................................................. 43
vusers.created: ................................................................ 160
vusers.created_by_name.0: ...................................................... 160
vusers.failed: ................................................................. 117
vusers.session_length:
min: ......................................................................... 1652.9
max: ......................................................................... 9795.7
mean: ........................................................................ 4830.8
median: ...................................................................... 5711.5
p95: ......................................................................... 8024.5
p99: ......................................................................... 9230.4
타임아웃이 117개가 나왔고, 200이 43개 나왔다. 오히려 아까 자식 프로세스의 갯수가 20개일 때보다 수치가 낮은 것을 볼 수 있다. 즉 여기서부터는 컨텍스트 스위칭으로 인한 오버헤드로 인해 성능이 더 악화됐다는 것이다.
따라서 나는 최적의 프로세스 개수가 대략 25개라 판단했고, 그 값으로 최대 채점 갯수를 설정했다. 굿굿!
실험 결과에 따라 나온 최적의 프로세스 개수를 25로 설정하고, 어느정도의 부하를 적절하게 감내할 수 있을 지 한번 테스트해봤다.
아까와 동일한 조건인 초당 16개의 채점 요청을 받는 시나리오로 테스트해봤다.
--------------------------------
Summary report @ 13:25:42(+0900)
--------------------------------
errors.ETIMEDOUT: .............................................................. 124
http.codes.200: ................................................................ 36
http.downloaded_bytes: ......................................................... 12132
http.request_rate: ............................................................. 11/sec
http.requests: ................................................................. 160
http.response_time:
min: ......................................................................... 1487
max: ......................................................................... 7802
mean: ........................................................................ 4111.9
median: ...................................................................... 4147.4
p95: ......................................................................... 7557.1
p99: ......................................................................... 7709.8
http.responses: ................................................................ 36
vusers.completed: .............................................................. 36
vusers.created: ................................................................ 160
vusers.created_by_name.0: ...................................................... 160
vusers.failed: ................................................................. 124
vusers.session_length:
min: ......................................................................... 1593.3
max: ......................................................................... 7815.5
mean: ........................................................................ 4131.6
median: ...................................................................... 4147.4
p95: ......................................................................... 7557.1
p99: ......................................................................... 7709.8
이런 수치가 나왔다. 타임아웃이 존재하면 안되므로, 초당 요청 건수를 조금씩 줄여서 모든 요청을 200으로 받게 해야겠다.
초당 요청을 14건으로 줄이고 테스트를 해봤다.
--------------------------------
Summary report @ 13:27:13(+0900)
--------------------------------
errors.ETIMEDOUT: .............................................................. 76
http.codes.200: ................................................................ 57
http.codes.500: ................................................................ 7
http.downloaded_bytes: ......................................................... 19573
http.request_rate: ............................................................. 9/sec
http.requests: ................................................................. 140
http.response_time:
min: ......................................................................... 1365
max: ......................................................................... 9239
mean: ........................................................................ 5244
median: ...................................................................... 5711.5
p95: ......................................................................... 9047.6
p99: ......................................................................... 9230.4
http.responses: ................................................................ 64
vusers.completed: .............................................................. 64
vusers.created: ................................................................ 140
vusers.created_by_name.0: ...................................................... 140
vusers.failed: ................................................................. 76
vusers.session_length:
min: ......................................................................... 1402.5
max: ......................................................................... 9252.7
mean: ........................................................................ 5260.8
median: ...................................................................... 5711.5
p95: ......................................................................... 9047.6
p99: ......................................................................... 9230.4
타임아웃이 조금 줄었다. 이번에는 12건으로 줄여보자.
--------------------------------
Summary report @ 13:28:27(+0900)
--------------------------------
errors.ETIMEDOUT: .............................................................. 36
http.codes.200: ................................................................ 84
http.downloaded_bytes: ......................................................... 28308
http.request_rate: ............................................................. 7/sec
http.requests: ................................................................. 120
http.response_time:
min: ......................................................................... 1203
max: ......................................................................... 9975
mean: ........................................................................ 5089.2
median: ...................................................................... 4676.2
p95: ......................................................................... 9607.1
p99: ......................................................................... 9999.2
http.responses: ................................................................ 84
vusers.completed: .............................................................. 84
vusers.created: ................................................................ 120
vusers.created_by_name.0: ...................................................... 120
vusers.failed: ................................................................. 36
vusers.session_length:
min: ......................................................................... 1239.5
max: ......................................................................... 9987.7
mean: ........................................................................ 5105.5
median: ...................................................................... 4676.2
p95: ......................................................................... 9607.1
p99: ......................................................................... 9999.2
다시 10건으로 줄여보자.
--------------------------------
Summary report @ 13:29:06(+0900)
--------------------------------
http.codes.200: ................................................................ 100
http.downloaded_bytes: ......................................................... 33700
http.request_rate: ............................................................. 15/sec
http.requests: ................................................................. 100
http.response_time:
min: ......................................................................... 1090
max: ......................................................................... 5348
mean: ........................................................................ 2726.3
median: ...................................................................... 2416.8
p95: ......................................................................... 4770.6
p99: ......................................................................... 5272.4
http.responses: ................................................................ 100
vusers.completed: .............................................................. 100
vusers.created: ................................................................ 100
vusers.created_by_name.0: ...................................................... 100
vusers.failed: ................................................................. 0
vusers.session_length:
min: ......................................................................... 1159.4
max: ......................................................................... 5363.3
mean: ........................................................................ 2743
median: ...................................................................... 2416.8
p95: ......................................................................... 4770.6
p99: ......................................................................... 5272.4
굿.
최적의 프로세스 개수는 25개이고, 10초동안 초당 10건의 요청이 들어오며 그 요청 하나당 3개의 테스트케이스 채점을 요청한다. 즉, 10 초당 300개, 다시말해 초당 30개의 테스트케이스까지 우리의 서버는 채점할 수 있는 것이다.
이 상황은 최악의 최악의 최악의 상황을 가정한 것이기 때문에, 대략 100명정도 까지의 유저는 커버할 수 있지 않을까 생각한다. 100명의 유저가 접속하면, 동시 채점 요청 횟수가 많아도 10번은 안될 것이라 생각했고, 그렇다면 100명까지는 원활하게 채점을 할 수 있을 것이라 생각했기 때문이다.
정말 대략적으로 생각해봤는데, 10초당 1000명의 사람들에 대한 채점 요청을 처리할 수 있지 않을까 생각한다.
초기에 초당 두 명의 사용자, 즉 초당 6개(이것도 타임아웃이 좀 있어서 6개 미만이 맞다) 테스트케이스를 채점할 수 있었던 반면, 지금은 초당 30개의 테스트케이스를 처리할 수 있게 엄청난 성능 향상을 본 상황이다. 대략 500%의 성능 향상을 이뤄냈다고 볼 수 있겠다.
끝!