성균관대 동아리 코드당 (https://codedang.com) 에서 사용하고 있는 OJ Sandbox (Qindao OJ) 와 관련된 글을 적었습니다.
Qindao OJ 오픈소스 깃허브 : https://github.com/QingdaoU/Judger
Sandbox에서 ‘메모리’와 관련해서 이상한 결과를 반환하는 것을 확인했습니다.
특히, Memory를 과도하게 사용한 상황에서 Memory Limit Exceeded
가 아닌, Runtime Error
를 반환하는 문제입니다.
Memory를 매우 많이 먹는 코드 채점을 진행할 때, 이상하게 Runtime Error
가 발생합니다. (스테이지 서버를 사용하였습니다.)
메모리를 많이 먹는 작업을 진행하면, Memory Limit Exceeded
가 발생해야 하는 것이 정상적입니다.
Runtime Error
가 발생하는 경우 (주석은 무시합시다)
Memory Limit Exceeded
가 발생하는 경우 하지만 또, 너무 과하게 메모리 사용량을 잡는 것이 아니라, 어느 수준까지만 메모리를 사용하게 되면Memory Limit Exceeded
가 발생합니다.
Memory Limit Exceeded
가 발생한 아래의 코드는 260000000개의 int 메모리를 사용하였습니다. 그러면, 총 967MB정도 사용했다고 볼 수 있습니다.
Runtime Error
가 발생한 위의 코드는 270000000개의 int 메모리를 사용하였습니다. 그러면, 총 1GB 넘게 메모리를 사용하였습니다. 1GB가 넘으면 Runtime Error를 발생시킵니다.
추가적으로, Stage 서버의 배포 상황은 Docker-compose로 배포가 되어있으며,, 메모리는 32GB, CPU 8-Core 까지 사용 가능합니다.
하지만, 운영환경에서는 조금 다릅니다.
분명 512MB로 Memory Limit이 정의되어있는 문제에서 다음과 같은 문제가 발생합니다.
512MB 안쪽으로 들어왔지만 통과하는 코드
512MB 안쪽으로 들어왔지만 Runtime Error
가 발생하는 코드
절대 이문제에서 Memory Limit Exceeded
를 볼 수 없습니다. 이러한 오류가 발생하기 이전에, Runtime Error
가 발생합니다. 운영환경의 채점 서버는 ECS Task에서 메모리를 512MB로 hard limit을 걸어놓았습니다. 이와 관련하여 깊게 살펴보아야 합니다.
문제의 원인을 찾기 위해 Local 환경에서 이를 확인해보았습니다.
Local에서 테스트하는 환경은 다음과 같습니다.
dev container 및 Message Queue (RabbitMQ) 컨테이너 실행
dev container 내부에서 Iris 서버, Nest client 서버 실행 (submission 제출 및 처리 로직 확인 위해)
Message Queue 콘솔에서 직접 Message 날려서 submission 상황 가정
이 후, docker stats로 container들의 메모리 사용량 확인
메시지큐로 전달한 형식은 다음과 같습니다.
(로컬환경에서 확인할 수 있는 문제와 답으로 해당 문제에 맞는 메시지를 전송하는 형식입니다.)
{
"problemId": 1,
"code": "#include <stdio.h>\n\nint main() {\n int a, b;\n scanf(\"%d%d\", &a, &b);\n int* arr;\narr = (int *)malloc(sizeof(int) * 1000000000);\nfor(int i = 0; i < 1000000000; i++){\narr[i] = 5;\n}\nprintf(\"%d\\n\", a + b);\n return 0;\n}\n",
"language": "C",
"timeLimit": 1000,
"memoryLimit": 2111224192
}
code 부분을 다듬어 드리면, 다음과 같습니다.
#include <stdio.h>
#include <stdlib.h>
int main() {
int a, b;
scanf("%d%d", &a, &b);
int* arr;
arr = (int *)malloc(sizeof(int) * 1000000000);
for(int i = 0; i < 1000000000; i++){
arr[i] = 5;
}
printf("%d\n", a + b);
return 0;
}
memoryLimit
memory allocation 할당크기
이 두가지의 변인을 가져갑니다.
memoryLimit = 2111224192 (2GB), memoryAllocation = int형 * 500000000 (2GB)
RuntimeError가 발생합니다. 모든 테스트 케이스가 모두 memoryLimit 안쪽으로 들어왔음에도 이러한 현상이 발생했습니다. 생각해보니 memory가 integer형이라 2GB가 최대이겠네요. (64bit이면 더넘긴하겠습니다)
memoryLimit = 2111224192 (2GB), memoryAllocation = int형 * 400000000 (1.6GB)
마찬가지로 Runtime Error입니다.
memoryLimit = 2111224192 (2GB), memoryAllocation = int형 * 300000000 (1.2GB)
여기서부터 Runtime Error가 발생하지 않습니다.
memoryLimit = 2111224192 (2GB), memoryAllocation = int형 * 350000000 (1.4GB)
여기도 멀쩡하구요.
그 이후에 보니까, 375000000 부터 안되더라구요. 근데 보면, signal이 9가 찍힙니다!
직접 오픈소스 까보니까, signal 9는, process kill 하는 sigkill
의 의미이고, timeout이 지나면 sigkill을 하는 것을 알 수 있었습니다.
그래서 귀납적인 방법으로 언제 signal이 9인지 보니까, real_time이 2000(2초)가 넘어가면 signal이 9가 찍히고 Runtime Error
가 발생합니다!
그래서 370000000 개의 integer상황으로 테스트해보면, 가끔 real_time이 2초가 넘어서, signal이 9(process kill) 가 발생해, sandbox에서는 Runtime Error
를 반환하고, 저희 또한 Runtime Error
결과를 반환하여 Messge Queue의 Result에 삽입합니다. → 이 상황이 Runtime Error
는 아니라고 생각합니다. Runtime에 발생한 문제가 아니라, 내부적으로 ‘시간’이 많이 걸린 작업이어서 반환하는 것이기 때문입니다. 그래서 추가적으로 1779 branch에서 Real_Time_Exceeded 오류로 반환하게 바꾼 코드를 커밋하였습니다. 아래는 커밋 내역입니다. https://github.com/skkuding/codedang/commit/445b19364b8648842522dd807429ad53b65e6e88
별개로, sandbox는 최대 memory를 2기가만 사용할 수 있는 것 같습니다! 만일, memoryLimit이 그 이상이 되면 sandbox 내부적으로 자체 에러가 발생해요! (sandbox error)
재민이에게 어떻게 설정해서 so파일 만들었는지 확인해봐야할 거 같아요.
현재 sandbox에서 처리할 수 있는 메모리는 최대 1GB로 설정되어있다. → 이상으로 넘어가면 runtime error. 그래서 stage 서버에서 1GB가 넘는 메모리를 사용할 시, runtime error발생 (아예 sandbox 구동이 불가능해져서, 메모리 사용량도 3MB로 줄고 채점이 진행되지 않음)
운영 서버에서는 유효한 범위의 memory 사용량이 반환되지만, runtime error가 발생. 코드 내부에서 memory allocation할때, 480MB이상은 가져오지 못한다. 아무리 malloc의 사이즈를 키워도, 480MB이상을 넘기지 않는다. 그 이유는 운영 환경에서 사용할 수 있는 memory가 480mb 이상이 되지 않으니까! (memory hard limit)
그래서 운영환경에서 memory hardlimit을 800정도 올리면 문제에서 512MB로 제한된 문제에서 Rumtime Error
를 반환하지 않게 할 수 있다. (다 유효하게 처리할 수 있을 것이다)