Skip to main content

swap, 메모리 증설의 포인트

아래 내용은 DevOps와 SE를 위한 리눅스 커널 이야기 5장 swap, 메모리 증설의 포인트를 읽고 정리한 내용입니다.

메모리 확인 명령어

free -k 명령어 실행시에 아주 적은 양이라도 swap 영역을 쓰기 시작했다면 반드시 살펴봐야합니다.swap의 사용 여부를 판단하는 것도 중요하지만 누가 swap을 사용하느냐가 매우 중요한 판단 기준이 됩니다.

모든 프로세스는 /proc/<pid>의 디렉토리에 자신과 관련된 정보들을 저장하며 /proc/<pid>/smaps 파일이 바로 메모리 정보를 저장하고 있습니다.

# pid가 1번인 systemd init 프로세스 메모리 정보 확인
[ec2-user@ip-172-31-35-50 /]$ sudo cat /proc/1/smaps | more
# 55fffb800000부터 55fffb95b000까지 논리 메모리 주소 사용
55fffb800000-55fffb95b000 r-xp 00000000 ca:01 8499732 /usr/lib/systemd/
systemd
Size: 1388 kB # 1MB 크기의 사이즈
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 1280 kB
Pss: 1280 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 1280 kB
Private_Dirty: 0 kB
Referenced: 1280 kB
Anonymous: 0 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB # SWAP 사용하지 않음
SwapPss: 0 kB
Locked: 0 kB
THPeligible: 0
VmFlags: rd ex mr mw me dw sd
  • 각 프로세스의 메모리 영역 별로 사용하는 swap 영역: /proc/<pid>/smaps
  • 전체 swap 영역에 대한 정보: /proc/<pid>/status
  • 전체 프로세스별로 사용중인 swap 영역의 크기 확인: smem -t

smem은 리눅스에서 메모리 사용량을 보고하는 도구로 아마존 리눅스에는 기본적으로 smem 패키지가 포함되어 있지 않아 EPEL (Extra Packages for Enterprise Linux) 리포지터리를 추가 후에 설치하였습니다.


  • Size: 해당 메모리 매핑의 크기
  • KernelPageSize: 커널이 이 메모리 영역에 사용하는 페이지 크기
  • MMUPageSize: MMU(Memory Management Unit)가 이 메모리 영역에 사용하는 페이지 크기
  • RSS: Resident Set Size, 해당 매핑에 대한 물리 메모리의 양
  • PSS: Proportional Set Size, 이 매핑의 메모리 중 현재 프로세스만 사용하는 메모리와 다른 프로세스와 공유하는 메모리의 비율을 고려한 값
  • Shared_Clean & Shared_Dirty: 여러 프로세스에 의해 공유되는 페이지 중 clean 상태와 dirty 상태인 페이지의 크기
  • Private_Clean & Private_Dirty: 한 프로세스만이 사용하는 페이지 중 clean 상태와 dirty 상태인 페이지의 크기
  • Referenced: 이 메모리 영역이 최근에 참조된 크기
  • Anonymous: 이름이 없는 페이지의 크기
  • LazyFree: 지연된 해제를 기다리는 페이지의 크기
  • AnonHugePages: Transparent Huge Pages(THP)를 사용하는 익명 페이지의 크기
    • THP는 Linux 커널에서 지원하는 메모리 관리 기능 중 하나로 애플리케이션의 성능을 향상시키기 위해 대형 페이지를 자동으로 사용하게 합니다. 일반적으로 페이지 크기는 종종 4KB로 설정되지만 대형 페이지는 메모리를 효율적으로 관리하고 특히 대용량 메모리 작업을 수행할 때 TLB(Translation Lookaside Buffer) 캐시 효율을 향상시킬 수 있습니다.
    • TLB는 페이지 테이블의 캐시 메모리 역할을 수행하기 위해 페이지 테이블의 일부를 저장
    • 자동화: THP는 애플리케이션 또는 사용자의 개입 없이 대형 페이지를 자동으로 사용
    • 성능 향상: THP 사용으로 인해 특히 메모리 집약적인 작업에서 성능이 향상될 수 있음
    • 페이지 테이블 오버헤드 감소: 대형 페이지 사용으로 인해 페이지 테이블의 엔트리 수가 줄어들고, 이에 따라 메모리 오버헤드도 줄어듭니다.
  • ShmemPmdMapped: 공유 메모리가 PMD(페이지 중간 디렉토리)에 매핑된 크기
    • 페이지 테이블의 멀티레벨 구조는 주로 논리 주소(가상 주소)를 물리 주소로 변환하는 과정을 효율적으로 수행하기 위한 것으로 이 구조는 가상 메모리 주소 공간을 나타내는 데 필요한 메모리의 양을 최소화합니다.
    • 대부분의 현대 컴퓨터 아키텍처에서는 가상 메모리 주소를 실제 물리 메모리 주소로 변환하는 과정은 멀티레벨 페이지 테이블을 사용하여 수행하며 멀티레벨 구조는 주소 공간의 크기와 페이지 크기에 따라 여러 단계로 나뉨.
    1. PGD (Page Global Directory)
    2. P4D (Page 4th Directory) - 특정 커널 버전과 구성에서만 사용
    3. PMD (Page Middle Directory)
    4. PTE (Page Table Entry)
    • PMD는 페이지 테이블 멀티레벨 구조에서의 중간 단계를 의미하며 THP와 같은 대형 페이지를 지원하는데 중요한 역할을 합니다.
  • FilePmdMapped: 파일이 PMD에 매핑된 크기
  • Shared_Hugetlb & Private_Hugetlb: HugeTLB를 사용하는 페이지 중 공유 및 전용 페이지의 크기
  • Swap: 스왑 영역에서 사용되는 크기
  • SwapPss: PSS를 고려한 스왑 사용량
  • Locked: 해당 메모리 영역이 잠긴 크기, 잠긴 메모리는 스왑 아웃되지 않음
  • THPeligible: 이 매핑이 THP를 사용할 수 있는지의 여부를 나타냄
  • VmFlags: 메모리 영역의 다양한 특성을 나타내는 플래그들
sudo amazon-linux-extras install epel -y
sudo yum install smem -y

[ec2-user@ip-172-31-35-50 /]$ smem -t
PID User Command Swap USS PSS RSS
10733 ec2-user -bash 0 2252 2620 4156
10894 ec2-user python /usr/bin/smem -t 0 6964 7474 9260
-------------------------------------------------------------------------------
2 1 0 9216 10094 13416

여기서 USS와 PSS 그리고 RSS는 다음과 같습니다.

  • USS (Unique Set Size): USS는 프로세스가 독점적으로 사용하는 물리 메모리의 양을 나타내며 다른 프로세스와 공유되지 않는 메모리입니다. 이는 해당 프로세스가 종료되면 해제되는 메모리 양을 나타냅니다.
  • PSS (Proportional Set Size): PSS는 프로세스의 전체 메모리 사용량을 나타내되, 여러 프로세스 간에 공유되는 메모리를 고려하여 계산됩니다. 예를 들어, 4개의 프로세스가 4MB의 메모리를 공유하면, 해당 메모리는 각 프로세스의 PSS에 1MB로 계산됩니다.
  • RSS (Resident Set Size): 프로세스에 의해 사용되는 물리 메모리의 총량을 나타냅니다. 해당 프로세스가 현재 RAM에 로드된 총 메모리 양을 반영합니다. 하지만 RSS는 여러 프로세스간에 공유되는 메모리를 중복 계산하여 반영합니다.

버디 시스템

devops-se-ch-5-1

커널은 버디 시스템을 통해서 프로세스에 메모리를 할당합니다. 버디 시스템은 물리 메모리를 연속된 메모리 영역으로 관리합니다. 예를 들어 연속 1개의 페이지 크기별 버디, 연속 2개의 페이지 크기별 버디 등으로 관리합니다. 그래서 프로세스가 4KB의 메모리 영역을 요청하면 연속 1개짜리 페이지를 꺼내서 사용하도록 내어주고 만약 8KB의 메모리 영역을 요청하면 연속 1개짜리를 두 개 주는 것이 아니라 연속 2개짜리 영역 하나를 내어줍니다. 이런 방식으로 메모리의 단편화도 막을 수 있고 프로세스의 요청에 더 빠르게 응답할 수 있습니다.

위 이미지대로 버디 시스템이 메모리를 조각화하여 프로세스에 할당해준다면 아래 명령어로 현재 시스템의 메모리 조각화 상태를 확인할 수 있습니다.

버디 시스템의 현재 상황 파악하기
[ec2-user@ip-172-31-35-50 /]$ cat /proc/buddyinfo
Node 0, zone DMA 31 23 18 17 12 7 7 6 4 0 0
Node 0, zone DMA32 6918 5393 3210 3404 663 151 242 35 23 8 6

# DMA 영역
31*4 + 23*8 + 18*16 + 17*32 + 12*64 + 7*128 + 7*256 + 6*512 + 4*1024
-> 124KB + 184KB + 288KB + 544KB + 768KB + 896KB + 1792KB + 3072KB + 4096KB = 8764KB
# 8764KB, 약 8MB

# DMA32 영역
6918*4 + 5393*8 + 3210*16 + 3404*32 + 663*64 + 151*128 + 242*256 + 35*512 + 23*1024 + 8*2048 + 6*4096
-> 27672KB + 43144KB + 51360KB + 108928KB + 42432KB + 19328KB + 61952KB + 17920KB + 23552KB + 16384KB + 24576KB = 400448KB
# 400448KB, 약 400MB

그럼 위에서 DMA 영역과 DMA32 영역을 합친 총 용량과 free 용량을 비교해보겠습니다. free 명령어 결과 값이 더 크게 나오긴 하네요... 본문에서는 크게 차이가 나지 않았었는데 직접 해보니 살짝 차이가 나는 것 같습니다.

# 409212KB < 447508KB
[ec2-user@ip-172-31-35-50 /]$ free -k
total used free shared buff/cache available
Mem: 987700 75944 447508 436 464248 771028
Swap: 0 0 0

추가로 본문에서는 malloc()을 활용하여 4MB 영역에 대해 할당 요청을 했을 때 Normal존(4096KB)에 버디가 1 감소한 것을 볼 수 있었습니다. 이런식으로 커널은 메모리의 요청이 발생했을 때 버디 시스템에서 가장 적당한 버디리스트를 찾아 프로세스에 넘겨줍니다.

메모리 재할당

커널에서의 메모리 재할당은 주로 두 가지 로직으로 처리된다고 합니다.

하나는 캐시 메모리 영역을 해제하고 가용 메모리 영역으로 돌려 프로세스에 할당하는 방법프로세스의 메모리를 swap으로 이동시키는 방법 두 가지가 있습니다. 전자의 경우 성능 저하가 발생하지 않지만 후자의 경우 스와핑이 발생하여 I/O 처리로 인한 성능 저하가 발생합니다.

커널은 메모리가 아무 데도 쓰이지 않고 가용 상태로 남아있는 것을 좋아하지 않으며 프로세스가 사용하고 있지 않는 가용한 메모리는 주로 커널에서 캐시 용도로 사용합니다. Page Cache, Buffer Cache, inode cache, dentry cache 등이 그 예이며 이렇게 사용하고 있지 않는 메모리를 캐시 용도로 사용하면 시스템의 성능이 전반적으로 향상되지만 정작 프로세스가 메모리를 필요로 할 때 사용할 메모리가 부족해질 수 있습니다. 이럴 때 메모리 재할당이 일어나는데 첫 번째 방법으로 커널은 캐시 용도로 사용하던 메모리를 사용 해제하고 가용 메모리 영역으로 돌린 후 프로세스가 사용할 수 있도록 재할당합니다.

devops-se-ch-4-1

free 명령이 숨기고 있는 것들에서도 메모리를 사용할수록 가용 영역과 Cache 영역이 줄고 사용 영역이 늘어나며 점차 더 이상 반환할 메모리도 없고 가용할 메모리가 없어지는 순간 시스템은 swap 영역을 사용한다고 설명합니다. 이때 커널은 프로세스가 사용하는 메모리 중 Inactive 리스트에 있는 메모리를 골라서 swap 영역으로 이동시킵니다. 그런 다음 해당 메모리 영역을 해제하고 다른 프로세스에 할당합니다. 다시 풀어 말하자면 참조된지 가장 오래된 메모리 영역을 골라 swap 영역으로 이동시키고 빈 메모리 영역을 다른 프로세스에 할당하는 것입니다.

이때 해당 메모리 영역이 물리 메모리에서는 해제되었지만 swap 영역으로 이동했기 때문에 프로세스가 해당 메모리 영역을 참조하려고 하면 다시 swap 영역에서 불러들여야 합니다. 이렇게 메모리를 swap 영역으로 쓰거나 읽는 작업이 디스크에서 일어나기 때문에 I/O를 일으키고 이 과정에서 시스템의 성능이 저하됩니다.

  • 캐시 메모리 영역을 비우는 방법 ➡️ 성능 저하 X
  • 프로세스의 메모리를 swap으로 이동시키는 방법 ➡️ 성능 저하 O

dd 명령을 사용한 메모리 변화 관측

# create 1GB files x 7
dd if=/dev/zero of=./file_1 bs=1024 count=1000000
dd if=/dev/zero of=./file_2 bs=1024 count=1000000
...
dd if=/dev/zero of=./file_7 bs=1024 count=1000000

# the other bash terminal
vmstat 1

또한 본문에서 dd if=/dev/zero of=./file_1 bs=1024 count=1000000로 1GB 크기의 파일을 7개 생성할 때 free 명령어 결과 free 영역이 줄어드는 것을 관찰하였고 이때 프로세스의 메모리 할당 요청에서 커널이 페이지 캐시를 비워서 확보하는 것을 볼 수 있었지만 더 이상 cache 영역으로도 줄일 수 없을 때에는 swap 영역을 사용하는 것을 볼 수 있었습니다.

기본적으로 커널은 유휴 메모리가 있을 경우 캐시로 활용하려 하지만 메모리 사용 요청이 증가하면 캐시로 활용하고 있는 메모리를 재할당해서 프로세스에 할당합니다.

vm.swappiness와 vm.vfs_cache_pressure

vm.swappiness

  • 기본값 60
  • 이 값이 커지면 캐시를 비우지 않고 swap 영역으로 옮기는 작업을 더 빨리 진행 (swap 영역 사용)
  • 이 값이 작아지면 가능한 한 캐시를 비우는 작업을 진행 (캐시 메모리 재할당)
  • 무조건적인 페이지 캐시 해제가 항상 좋은 것만은 아니며 오히려 자주 사용하지 않는 프로세스의 메모리를 swap 영역으로 내리는 게 더 좋을 수도 있다고 합니다.
  • sysctl -w vm.swappiness=100일 때, cache 영역이 남아있음에도 swap 영역을 적극 사용하는 것을 테스트 결과 확인

vm.vfs_cache_pressure

  • 캐시를 재할당한다고 결정했을 때 PageCache를 더 많이 재할당할지 아니면 디렉터리(dentry)나 inode 캐시를 더 많이 재할당할지를 결정
  • shrink_dcache_memory() 함수의 소스 코드에서 dentry_stat.nr_unused의 값을 100으로 나눈 후에 커널 파라미터로 설정한 sysctl_vfs_cache_pressure 값을 곱하는 것을 확인 가능

메모리 증설의 포인트

- 메모리 사용량이 선형적으로 증가하는 경우

  • 애플리케이션이 요청을 처리하기 위해 메모리를 할당 받고 요청이 끝나면 해당 메모리를 해제해야 하는데, 제대로 해제하지 못할 경우 사용하는 메모리가 계속해서 증가
  • pmap등의 명령을 사용해 해당 프로세스가 사용하는 힙 메모리 영역의 변화를 모니터링
  • gdb 도구로 힙 메모리의 영역에 메모리 덤프를 생성하여 실제 어떤 데이터들이 메모리에 있는지 확인하고 어떤 로직에서 문제가 발생했는지 예측

- 메모리 사용량이 폭증하는 경우

  • 순간적으로 요청이 폭증하면 응답이 느려질 수 있기 때문에, 안정적인 서비스를 위해서 사용한 메모리의 최대치를 계산해서 메모리를 증설하거나 swap을 사용하여 방어

메모리 덤프

1MB 영역 할당 및 쓰기 작업 진행
#include <stdio.h>
#include <stdlib.h>
#include <sys/time.h>
#include <unistd.h>

#define MEGABYTE 1024*1024

int main() {
struct timeval tv;
char *current_data;

while(1) {
gettimeofday(&tv, NULL);
current_data = (char *) malloc(MEGABYTE);
sprintf(current_data, "%d", tv.tv_usec);
printf("current_data = %s\n", current_data);
sleep(1);
}

exit(0);
}
메모리 증가 확인
gcc malloc.c -o malloc
./malloc

# VSZ 프로세스가 사용하는 가상 메모리 증가 확인
# 36096
[ec2-user@ip-172-31-35-50 ~]$ ps aux | grep -i malloc | grep -iv grep
ec2-user 5399 0.0 0.0 36096 704 pts/1 S+ 11:59 0:00 ./malloc
# 38152
[ec2-user@ip-172-31-35-50 ~]$ ps aux | grep -i malloc | grep -iv grep
ec2-user 5399 0.0 0.0 38152 704 pts/1 S+ 11:59 0:00 ./malloc
# 39180
[ec2-user@ip-172-31-35-50 ~]$ ps aux | grep -i malloc | grep -iv grep
ec2-user 5399 0.0 0.0 39180 704 pts/1 S+ 11:59 0:00 ./malloc
# 40208
[ec2-user@ip-172-31-35-50 ~]$ ps aux | grep -i malloc | grep -iv grep
ec2-user 5399 0.0 0.0 40208 704 pts/1 S+ 11:59 0:00 ./malloc
메모리 누수 영역 논리 주소 확인
[ec2-user@ip-172-31-35-50 ~]$ cat /proc/5399/smaps
...(생략)
7fe66c165000-7fe6741e5000 rw-p 00000000 00:00 0 # 논리 메모리 주소 확인
Size: 131584 kB # 계속 늘어난 사이즈, 최대 사이즈
KernelPageSize: 4 kB
MMUPageSize: 4 kB
Rss: 512 kB
Pss: 512 kB
Shared_Clean: 0 kB
Shared_Dirty: 0 kB
Private_Clean: 0 kB
Private_Dirty: 512 kB
Referenced: 512 kB
Anonymous: 512 kB
LazyFree: 0 kB
AnonHugePages: 0 kB
ShmemPmdMapped: 0 kB
FilePmdMapped: 0 kB
Shared_Hugetlb: 0 kB
Private_Hugetlb: 0 kB
Swap: 0 kB
SwapPss: 0 kB
Locked: 0 kB
THPeligible: 0
VmFlags: rd wr mr mw me ac sd
메모리 덤프
[ec2-user@ip-172-31-35-50 ~]$ sudo gdb -p 5399
GNU gdb (GDB) Red Hat Enterprise Linux 8.0.1-36.amzn2.0.1
Copyright (C) 2017 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law. Type "show copying"
and "show warranty" for details.
This GDB was configured as "x86_64-redhat-linux-gnu".
Type "show configuration" for configuration details.
For bug reporting instructions, please see:
<http://www.gnu.org/software/gdb/bugs/>.
Find the GDB manual and other documentation resources online at:
<http://www.gnu.org/software/gdb/documentation/>.
For help, type "help".
Type "apropos word" to search for commands related to "word".
Attaching to process 5399
Reading symbols from /home/ec2-user/malloc...(no debugging symbols found)...done.
Reading symbols from /lib64/libc.so.6...(no debugging symbols found)...done.
Reading symbols from /lib64/ld-linux-x86-64.so.2...(no debugging symbols found)...done.
0x00007fe6742a4c01 in nanosleep () from /lib64/libc.so.6
Missing separate debuginfos, use: debuginfo-install glibc-2.26-63.amzn2.0.1.x86_64
(gdb) dump memory /root/memory_dump 0x7fe66c165000 0x7fe6741e5000
메모리 덤프 파일 내용 확인
[root@ip-172-31-35-50 ~]# strings ./memory_dump
738906
738735
738554
738378
738207
738034
737895
737733
737567
737396
737239
737121
736978
736831
736657
736479
736352
736218
736058
735902
735738
735573
735398
735238
735104
734957
734771
734609
734451
734288
734141
733994
733837
733666
733508
733350
733170
733006
732857
732695
732525
732365
732239
732088
731928
731752
731576
731430
731284
731122
730916
730752
730568
730425
730281
730136
729952
729809
729637
729459
729281
729126
729000
728705
728533
728360
728164
727990
727828
727652
727498
727326
727139
726961
726785
726603
726430
726255
726082
725909
725757
725604
725445
725294
725129
724983
724819
724667
724510
724345
724188
723995
723821
723648
723504
723363
723217
723076
722910
722694
722530
722407
722281
722140
722010
721850
721667
721516
721330
721158
720993
720847
720689
720528
720348
720181
720025
719847
719674
719428
719154
718909
718652
718406
718131
717864
717618
717375

이처럼 애플리케이션이 구동되는 서버에서 메모리 누수가 의심될 때 메모리 덤프를 사용해 실제 메모리의 내용을 살펴서 어떤 로직에서 사용한 메모리 영역이 해제되지 않았는지 확인하여 해당 로직을 수정하여 메모리 누수를 막을 수 있다고 합니다.