ember-cli 빌드 시 최소 인스턴스 크기에서 메모리 사용량으로 인한 OOM 실패 위험

업그레이드 과정에서 RAM+스왑 메모리에 가장 큰 부하가 걸리는 시점은 ‘ember’ 프로세스가 실행될 때입니다. 업데이트를 할 때마다 이전보다 부하가 더 커지는 것 같고, 권장 최소 사양 컴퓨터에서 실행할 수 없는 수준에 가까워지고 있습니다.

실제로 문제가 발생하기 전에 이 부분을 살펴보는 것이 좋을 것 같습니다. (비용 문제로 인해 권장 최소 사양을 높이는 것이 해결책이 되지는 않기를 바랍니다. 디스크 공간이 허락한다면 스왑 공간을 늘리는 것이 도움이 될 것입니다. 원칙적으로는 더 비싸지만 RAM이 더 큰 인스턴스로 일시적으로 이전하는 것도 가능합니다.)

저는 권장 최소 사양 내의 작은 인스턴스 두 대에서 규모가 modest한 포럼 두 개를 운영하고 있습니다. 두 경우 모두 RAM+스왑=3G입니다. 한쪽은 RAM 1G와 스왑 2G가 있는 Digital Ocean 인스턴스이고, 다른 쪽은 RAM 2G와 스왑 1G가 있는 Hetzner 인스턴스입니다.

DO 머신에서 ps auxc를 사용하여 ember 프로세스의 스냅샷 세 가지를 첨부합니다.

USER       PID %CPU %MEM      VSZ    RSS TTY   STAT START   TIME COMMAND
1000     10342 87.7 65.1 32930460 657936 ?     Rl   16:57   2:23 ember

USER       PID %CPU %MEM      VSZ    RSS TTY   STAT START   TIME COMMAND
1000     10342 84.9 60.7 43572204 612668 ?     Rl   16:57   2:57 ember

USER       PID %CPU %MEM      VSZ    RSS TTY   STAT START   TIME COMMAND
1000     10342 81.2 55.2 43405220 557128 ?     Rl   16:57   3:40 ember

물론 43GB의 프로세스 크기가 전부 가상 메모리에 존재하는 것은 아닙니다. 사용 가능한 메모리가 3G이기 때문입니다. RSS가 RAM 크기의 65%를 사용하는 것은 인상적이지만, 그 자체로는 문제가 되지 않습니다. 사용 가능한 메모리와 스왑의 양을 보면 머신이 Out of Memory (OOM) 상태에 근접해 있음을 알 수 있으며, 이는 가장 가능성이 높은 결과로 일부 프로세스가 강제 종료되고 업데이트가 깔끔하게 종료되지 않는 상황을 초래할 것입니다.

시점별 스냅샷으로 free 명령어의 출력을 보여드립니다:

# free
              total        used        free      shared  buff/cache   available
Mem:        1009140      863552       72768        6224       72820       34868
Swap:       2097144     1160628      936516

실패에 가장 가까운 상황을 포착하기 위해 vmstat 5를 사용했습니다.

# vmstat 5 5
procs -----------memory----------    ---swap-- -----io----  -system-- ------cpu-----
 r  b   swpd    free   buff  cache    si    so    bi    bo   in    cs us sy id wa st
 3  0 1392140  61200  11632  76432    41    32   117    93    0     1  2  1 97  0  0
 1  1 1467220  63416    324  67284  8786 20499 13178 20567 2539  8924 77 13  0 10  0
 0  2 1593340  57916   1096  53832 24262 46868 29986 46889 5377 18534 44 22  0 34  0
 4  0 1155632 120680   2772  86280 39111 35424 54768 37824 6987 25174 38 27  0 35  0
 3  0 1102988  74096   2852  85276 11261   246 12610   271 1879  6365 86  6  0  8  0

많은 컨텍스트 스위치(cs), 많은 디스크 활동(bi, bo) 및 많은 스왑 활동(si, so)을 볼 수 있습니다. 그러나 가장 중요한 것은 스왑 사용량이 1.6G까지 증가하고 사용 가능한 메모리가 60M까지 줄었으며 버퍼 사용량이 54M에 불과하다는 점입니다. 이는 사용 가능한 3G의 가상 메모리 중 약 2.6G가 사용 중임을 의미합니다. 이는 용량의 87%입니다. (5초 간격으로 샘플링하고 있기 때문에 실제로는 더 나쁠 수 있습니다.)

8월에 업데이트했을 당시에도 상황이 우려스러웠습니다(약 2G 사용으로, 오늘처럼 임계치에 매우 근접하지는 않았지만):

# vmstat 5 5
procs -----------memory----------    ---swap-- -----io----  -system-- ------cpu-----
 r  b    swpd   free   buff  cache    si    so    bi    bo   in    cs us sy id wa st
 3  0  700404  62740   1956  48748    35    29   108    92    3     8  2  1 96  0  1
 1  0  741000  65996   1880  44360  3708 11190  3982 11191  643  1437 92  4  0  3  1
 1  0  834836  70452   1480  53856   528 18969  4274 18974  532  1575 93  6  0  1  0
 4  1 1010144  82192   4644  44400 30065 38803 35455 39946 4432 19267 28 26  0 39  7
 1  0  644116 307764   1644  55348 24406 21154 27724 21945 2551  8672 52 22  0 21  6

Hi @Ed_S - 이 테스트들을 진행할 때 어떤 버전의 Discourse를 사용하셨나요? 우리는 ember-cli와 그 애드온을 정기적으로 업데이트하고 있으므로, 동일한 환경에서 보고 있는지 확인하고 싶습니다.

또한, VM에 CPU 코어가 몇 개 있나요? 1개인가요? (콘솔에서 lscpu를 실행하여 확인할 수 있습니다)

모두 동일한 데이터로 작업할 수 있도록 다음을 실행해 보실 수 있을까요?

/var/discourse/launcher enter app
cd /var/www/discourse/app/assets/javascripts/discourse
apt-get update && apt-get install time
NODE_OPTIONS='--max-old-space-size=2048' /usr/bin/time -v yarn ember build -prod

제 테스트 드롭렛(1 CPU, 1GB RAM, 2GB 스왑)에서는 다음과 같은 결과가 나타났습니다:

Command being timed: "yarn ember build -prod"
	User time (seconds): 369.74
	System time (seconds): 22.62
	Percent of CPU this job got: 81%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 8:02.73
	Average shared text size (kbytes): 0
	Average unshared data size (kbytes): 0
	Average stack size (kbytes): 0
	Average total size (kbytes): 0
	Maximum resident set size (kbytes): 774912
	Average resident set size (kbytes): 0
	Major (requiring I/O) page faults: 253770
	Minor (reclaiming a frame) page faults: 1158920
	Voluntary context switches: 519269
	Involuntary context switches: 383328
	Swaps: 0
	File system inputs: 7521784
	File system outputs: 316304
	Socket messages sent: 0
	Socket messages received: 0
	Signals delivered: 0
	Page size (bytes): 4096
	Exit status: 0

우리는 꽤 표준적인 ember 툴링을 사용하고 있으므로, 메모리 사용량을 줄이기 위해 설정 측면에서 우리가 할 수 있는 일이 많지 않을 것 같습니다. 장기적인 목표는 Embroider 사용을 도입하는 것이며, 이를 통해 더 많은 옵션을 얻을 수 있을 것입니다.

@david님 감사합니다. ember가 그 자체로 독립적인 존재라는 점을 잘 알고 있습니다.

방금 해당 명령어들을 실행했습니다.

# /var/discourse/launcher enter app
x86_64 아키텍처 감지됨.

경고: Discourse 베이스 이미지를 다운로드하기 시작합니다.
네트워크 속도에 따라 몇 분에서 1시간까지 걸릴 수 있습니다

조금만 기다려 주세요

2.0.20220720-0049: discourse/base에서 끌어오기 중
Digest: sha256:7ff397003c78b64c9131726756014710e2e67568fbc88daad846d2b368a02364
Status: discourse/base:2.0.20220720-0049의 더 새로운 이미지 다운로드 완료
docker.io/discourse/base:2.0.20220720-0049

이것은 프로덕션 설치 환경이며, 어제 기준으로 최신 상태였습니다. 현재 보고된 내용은 다음과 같습니다:

설치된 버전: 2.9.0.beta12 (8f5936871c)

CPU 1개 인스턴스이며, 님의 환경과 마찬가지로 RAM 1GB와 스왑 2GB를 사용하고 있습니다.

time 명령어의 결과는 다음과 같습니다.

Done in 303.21s.
	Command being timed: "yarn ember build -prod"
	User time (seconds): 222.71
	System time (seconds): 17.17
	Percent of CPU this job got: 78%
	Elapsed (wall clock) time (h:mm:ss or m:ss): 5:04.15
	Average shared text size (kbytes): 0
	Average unshared data size (kbytes): 0
	Average stack size (kbytes): 0
	Average total size (kbytes): 0
	Maximum resident set size (kbytes): 702292
	Average resident set size (kbytes): 0
	Major (requiring I/O) page faults: 348190
	Minor (reclaiming a frame) page faults: 1152689
	Voluntary context switches: 617736
	Involuntary context switches: 774189
	Swaps: 0
	File system inputs: 5001936
	File system outputs: 318280
	Socket messages sent: 0
	Socket messages received: 0
	Signals delivered: 0
	Page size (bytes): 4096
	Exit status: 0

직전까지 호스트를 업데이트하고 재부팅했기 때문에 컨테이너 내부의 모든 것이 새로 재시작되었을 것입니다.

다른 창에서 실행 중인 vmstat이 보고한 메모리 사용량 중 가장 심각한 시점의 데이터는 다음과 같습니다:

# vmstat 1
procs  -----------memory----------    ---swap--  -----io----   -system-- ------cpu-----
 r  b    swpd   free   buff  cache    si     so    bi     bo    in    cs us sy id wa st
 2  0  704000 136044  24136 158144  1517   3503  8256   4377   886  3564 43  8 43  6  0
...
 5  0 1451436  71604   1248  50196 55016 110236 73204 121060 13152 45971 29 60  0 10  1

Node의 허용 가능한 힙을 500M에서 2G로 명시적으로 늘린 것 같습니다. 이것이 한 발짝 더 나아간 것일 수 있으며, 1.5G가 더 적절할 수도 있습니다:

Ember가 머신에서 실행되는 유일한 것이 아니라는 점과, RAM+스왑의 전역 한도에 도달해 있다는 점을 유의할 가치가 있습니다. 따라서 머신의 히스토리와 모든 다른 실행 중인 프로세스의 요구 사항이 영향을 미칩니다. 어제보다 더 낮은 고수준(peak)에 도달하는 데 도움이 되었을 수도 있으므로, 제 재부팅이 여기서 도움이 되었을 수 있습니다.

위 풀 리퀘스트는 다음에서 참조되었습니다.
Failed to upgrade discourse instance to Feb 15 2022
여기서도 누군가가 재부팅으로 해결된 메모리 부족 문제를 겪었다는 것을 언급하고 있습니다.

time 명령이 피크 메모리 사용량을 보고하지 않는다는 점은 유감입니다. 최소 3G의 RAM과 스왑이 없는 머신에서라면 RSS 계수가 Ember의 피크 사용량을 알려줄 수 있을 것입니다. 또는 다른 전술을 사용할 수도 있습니다 - 여러 가지가 여기에 설명되어 있고, 여기에도 몇 가지 아이디어가 있습니다.

어색한 점은, 여기서는 실제로 메모리 사용량에 관심이 있는 반면, 많은 경우 사람들은 RAM 사용량에 관심이 있다는 것입니다. 이는 다른 질문입니다.

해당 플래그를 추가한 이유는 Node 자체의 OOM 킬러가 빌드를 종료시켰기 때문입니다. 500M은 충분하지 않았죠. 1.5G로 조정해 보는 것에 동의합니다. 제 드롭렛(droplet)에서 직접 테스트해 봤는데 정상적으로 작동하는 것 같습니다. 사실 1.0G만으로도 충분한 것 같습니다.

다양한 max_heap 크기로 메모리 사용량을 추적해 봤습니다:

(while(true); do (free -m -t | grep Total | awk '{print $3}') && sleep 0.5; done) | tee 1000mb.csv

빌드 중 메모리 사용량은 다음과 같습니다:

빌드 시간에는 큰 차이가 없었지만, 1GB와 1.5GB 제한이 전체 사용량이 현저히 적음을 명확히 보여줍니다. 예상대로, time 출력 결과에서 node 제한이 낮을수록 "Major page faults"가 훨씬 적게 나타났습니다.

1.5GB와 1GB 사이의 차이가 왜 이렇게 작은지 궁금하네요… :face_with_monocle:

어쨌든, 제한을 줄이는 것이 좋은 아이디어라고 동의합니다. 사양이 높은 머신에서 빌드 성능에 영향을 주지 않도록 하기 위해, 제한이 너무 낮다는 것을 확실히 아는 경우에만 제한을 오버라이드해야 한다고 생각합니다. 그렇지 않으면 Node의 기본값을 그대로 사용하도록 두면 됩니다.

PR을 올렸습니다. 조만간 병합되도록 하겠습니다. 이 문제를 제기해 주셔서 감사합니다 @Ed_S!