Skip to content

Latest commit

 

History

History
249 lines (193 loc) · 20.3 KB

File metadata and controls

249 lines (193 loc) · 20.3 KB

5.1 swap 영역

swap 영역은 물리 메모리가 부족할 경우를 대비해서 만들어 놓은 영역이다. 메모리는 프로세스가 연산을 하기 위해 만들어 놓은 일종의 저장 공간과 같은 것인데, 이 공간이 모자라면 프로세스는 더 이상 연산을 위한 공간을 확보할 수 없기 때문에 전체 시스템이 응답 불가 상태에 빠질 수 있다. 이런 응답 불가 상태에 빠지지 않고 시스템이 안정적으로 운영될 수 있도록 비상용으로 확보해 놓은 메모리 공간이 swap 영역이다. 하지만 swap 영역은 물리 메모리가 아니라 디스크의 일부분을 메모리처럼 사용하기 위해 만들어 놓은 공간이기 때문에, 메모리가 부족할 때 사용한다고는 하지만 메모리에 비해 접근과 처리 속도가 현저하게 떨어진다. 그래서 swap 영역을 사용하게 되면 시스템의 성능 저하가 일어난다.

리눅스에서 사용 중인 swap 영역 정보는 free 명령을 통해서 확인할 수 있다.

ubuntu@ip-172-31-0-176:~$ free -k
               total        used        free      shared  buff/cache   available
Mem:          980300      704200       90408        1008      361972      276100
Swap:              0           0           0
ubuntu@ip-172-31-0-176:~$ 
  • total: 전체 swap 영역의 크기를 의미한다. -k 옵션은 KB 단위이기 때문에 약 10GB 정도의 영역을 확보해둔 것을 알 수 있다. (실제 내 서버는 swap 영역이 없다.)
  • used: 현재 사용중인 swap 영역의 크기를 의미한다. swap 영역을 사용했다는 것 자체가 시스템에 메모리와 관련해 문제가 있을 수 있다는 의미이다. 아주 적인 양이라도 swap 영역을 쓰기 시작했다면 반드시 살펴봐야 한다.
  • free: 현재 남아있는 swap 영역의 크기이다.

swap 영역을 사용한다는 것 자체가 시스템의 메모리가 부족할 수 있다는 의미이기 때문에 어떤 프로세스가 사용하고 있는지 확인해볼 필요가 있다. 서비스 용도가 아닌 관리 용도의 프로세스에 메모리 누수가 있어서 메모리를 계속해서 점유하려 하고, 그 과정에서 swap을 사용하고 있을 수도 있기 때문이다. 이런 경우라면 관리 용도의 프로세스를 죽여서 메모리 부족 현상으로 인한 성능 저하를 해결할 수 있다. swap의 사용 여부를 판단하는 것도 중요하지만 누가 swap을 사용하느냐도 메우 중요한 판단 기준이 된다.

모든 프로세스는 /proc/{pid}의 디렉터리에 자신과 관련된 정보들을 저장한다. 프로세스가 사용하는 메모리에 대한 정보도 이곳에 저장되는데 그중에서도 /proc/{pid}/smaps 파일이 바로 메모리 정보를 저장하고 있다.

ubuntu@ip-172-31-0-176:/proc/1397$ cat smaps | more
5c3ddd614000-5c3ddd615000 r--p 00000000 00:2f 266762                     /usr/local/bin/python3.11
Size:                  4 kB
KernelPageSize:        4 kB
MMUPageSize:           4 kB
Rss:                   0 kB
Pss:                   0 kB
Pss_Dirty:             0 kB
Shared_Clean:          0 kB
Shared_Dirty:          0 kB
Private_Clean:         0 kB
Private_Dirty:         0 kB
Referenced:            0 kB
Anonymous:             0 kB
KSM:                   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
SwapPss:               0 kB
Locked:                0 kB
THPeligible:           0
VmFlags: rd mr mw me sd

프로세스가 사용하고 있는 메모리 영역 중 해당 번지에 속한 메모리 영역이 swap 영역에 있는지 아닌지를 확인할 수 있다. 해당 프로세스의 논리적 메모리 5c3ddd614000-5c3ddd615000 사이에 있는 메모리는 크기가 4KB이며 swap 영역은 없다.

이렇게 /proc/{pid}/smaps 파일의 정보를 통해서 각 프로세스의 메모리 영역별로 사용하는 swap 영역을 확인할 수 있다. 하지만 프로세스의 메모리 영역별로 살펴봐야 하기 때문에 불편하다. 그래서 특정 프로세스가 사용하는 전체 swap 영역에 대한 정보가 필요할 경우에는 /proc/{pid}/status 파일을 통해서도 확인할 수 있다.

ubuntu@ip-172-31-0-176:/proc/1397$ cat status
Name:   python
Umask:  0022
State:  S (sleeping)
Tgid:   1397
Ngid:   0
Pid:    1397
PPid:   1320
TracerPid:      0
Uid:    1000    1000    1000    1000
Gid:    1000    1000    1000    1000
FDSize: 256
Groups: 1000 
NStgid: 1397    8
NSpid:  1397    8
NSpgid: 1320    1
NSsid:  1320    1
Kthread:        0
VmPeak:   470684 kB
VmSize:   470684 kB
VmLck:         0 kB
VmPin:         0 kB
VmHWM:    141868 kB
VmRSS:    114896 kB
RssAnon:          111716 kB
RssFile:            3180 kB
RssShmem:              0 kB
VmData:   144160 kB
VmStk:       132 kB
VmExe:         4 kB
VmLib:     31508 kB
VmPTE:       424 kB
**VmSwap:        0 kB**
HugetlbPages:          0 kB
CoreDumping:    0
THP_enabled:    1
untag_mask:     0xffffffffffffffff
Threads:        5
SigQ:   0/3739
SigPnd: 0000000000000000
ShdPnd: 0000000000000000
SigBlk: 0000000000000000
SigIgn: 0000000001001000
SigCgt: 0000000100004002
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 00000000a80425fb
CapAmb: 0000000000000000
NoNewPrivs:     0
Seccomp:        2
Seccomp_filters:        1
Speculation_Store_Bypass:       vulnerable
SpeculationIndirectBranch:      always enabled
Cpus_allowed:   0001
Cpus_allowed_list:      0
Mems_allowed:   00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000000,00000001
Mems_allowed_list:      0
voluntary_ctxt_switches:        30971412
nonvoluntary_ctxt_switches:     375374
x86_Thread_features:
x86_Thread_features_locked:

많은 출력 결과 중 VmSwap 항목이 해당 프로세스가 사용하는 swap 영역에 대한 정보를 의미한다.

이 외에 전체 프로세스별로 사용 중인 swap 영역의 크기를 확인하고 싶은 경우가 있는데, 이런 경우에 유용한 smem이라는 유틸리티가 있다. 이 유틸리티는 /proc/{pid}의 내용을 바탕으로 각 프로세스들의 메모리 사용 현황을 보여준다.

ubuntu@ip-172-31-0-176:~$ smem -t
  PID User     Command                         Swap      USS      PSS      RSS 
1973892 ubuntu   -bash                              0     2336     2439     4540 
 1396 ubuntu   /usr/local/bin/python -c fr        0     7584     7902     8864 
1975779 ubuntu   /usr/bin/python3 /usr/bin/s        0    14008    14392    17076 
 1320 ubuntu   /usr/local/bin/python /usr/        0    23820    24171    25208 
 1398 ubuntu   /usr/local/bin/python -c fr        0   111644   112683   114412 
 1397 ubuntu   /usr/local/bin/python -c fr        0   111916   112955   114684 
-------------------------------------------------------------------------------
    6 1                                           0   271308   274542   284784 

제일 마지막 줄이 전체 합계이다.

5.2 버디 시스템

바로 앞에서 swap 영역은 메모리가 부족할 때 사용한다고 했는데, 그럼 메모리가 부족한 상황이라는 것은 어떤 상황일까?

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

[운영체제/OS] 메모리 관리 - 파티셔닝과 버디 시스템(Buddy system)

[컴퓨터 시스템] 동적 메모리 할당 - Buddy System의 개념

[Linux Kernel] Kernel 분석(v5.14.16) - Buddy (1)

ubuntu@ip-172-31-0-176:~$ cat /proc/buddyinfo
Node 0, zone      DMA     39      5      3      5      1      2      2      1      1      1      0 
Node 0, zone    DMA32   1125    654    844    439    211     64     12      5      1      1      6 

각각의 행은 2의 배수이며, 각각 연속 1개, 2개 4개의 영역을 의미한다. DMA 절의 메모리 가용량을 본다면 4KB x 39개 + 8KB x 5개 + 16KB x 3개 + …이다. 이를 토대로 계산해 보면 전체 가용영역을 계산할 수 있다.

실제 프로세스가 메모리를 할당 받으면 buddyinfo의 내용이 정말로 변하게 될까? 간단하게 테스트 하기 위해 malloc()을 이용한 프로그램을 사용해서 4MB의 영역에 대해 할당 요청을 해보자. 우선 테스트 전의 buddyinfo이다.

ubuntu@ip-172-31-0-13:~$ cat /proc/buddyinfo
Node 0, zone      DMA      0      0      0      0      0      1      1      1      0      1      3 
Node 0, zone    DMA32      1      0      0      1      2      0      1      0      2      1     93 

Normal 존에 4096KB의 버디가 10개 있다. (내 서버에는 없다.) 만약 프로세스에서 4MB 크기의 메모리 영역에 대한 할당을 요청하면 아마도 저 버디 리스트에서 할당될 것이다.

root@ip-172-31-0-13:/home/ubuntu# ./malloc &
[1] 3607
root@ip-172-31-0-13:/home/ubuntu# Allocated 1048576 MB
Allocated 2097152 MB
Allocated 3145728 MB
Allocated 4194304 MB
Allocated 5242880 MB
Allocated 6291456 MB
Allocated 7340032 MB
Allocated 8388608 MB
Allocated 9437184 MB
Allocated 10485760 MB
^C
ubuntu@ip-172-31-0-13:~$ cat /proc/buddyinfo
Node 0, zone      DMA     15     15      6      3      1      9      0      0      1      1      0 
Node 0, zone    DMA32      1    262    924    810    363     21     44     37     27     11      7 
ubuntu@ip-172-31-0-13:~$ cat /proc/buddyinfo
Node 0, zone      DMA     15     15      6      3      1      9      0      0      1      1      0 
Node 0, zone    DMA32      1    262    924    810    363     21     44     37     27     11      7 

(왜 오히려 늘어나지?)

Normal 존에 4096KB의 버디가 10개에서 9개로 줄어든 것을 확인할 수 있다. 이런식으로 커널은 메모리의 요청이 발생했을 때 버디 시스템에서 가장 적당한 버디 리스트를 찾아 프로세스에 넘겨준다.

5.3 메모리 재할당 과정

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

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

두번째는 swap을 사용하는 재할당이다. 위에서 언급한 캐시 용도의 메모리 외에 프로세스가 사용하는 메모리는 커널이 임의로 해제하고 재할당할 수 없고 그렇게 해서도 안된다. 프로세스가 언제 해당 메모리 영역을 참조하려 할지 알 수도 없을 뿐더러 해당 메모리 영역에 있는 내용이 어떤 내용인지도 알 수 없기 때문이다. 캐시 용도의 메모리를 해제할 만큼 해제하고도 더 이상 프로세스에 할당해줄 메모리가 없다면 어떻게 해야할까? 이때 swap을 사용하게 된다. 커널은 프로세스가 사용하는 메모리 중 Inactive 리스트에 있는 메모리를 골라서 swap 영역으로 이동시킨다. 그런 다음 해당 메모리 영역을 해제하고 다른 프로세스에 할당한다. 해당 메모리 영역이 물리 메모리에서는 해제되었지만 swap 영역으로 이동했기때문에 프로세스가 해당 메모리 영역을 참조하려고 하면 다시 swap 영역에서 불러들여야 한다. 이렇게 메모리를 swap 영역으로 쓰거나 읽는 작업이 디스크에서 일어나기 때문에 I/O를 일으키고 이 과정에서 시스템의 성능이 저하된다. 아무래도 디스크 작업은 메모리 작업보다 느릴 수밖에 없기 때문이다.

이렇게 두 가지 로직을 통해 메모리 재할당 작업이 진행된다. 이 중에서 우리가 알아보려는 과정은 두번째 과정이다. 첫 번째 재할당은 시스템 운영의 입장에서 자연스럽고 성능 저하에 크게 영향을 주지 않지만, swap을 사용하는 두 번째 재할당은 성능 저하를 일으키기 때문이다.

간단한 테스트를 통해서 실제로 메모리 재할당이 일어나는 과정을 알아보자. 먼저 dd 명령을 이용해서 1GB 크기의 파일을 7개 정도 생성해보고 그에 따른 메모리의 사용량 변화를 살펴보자.

하지만 커널이 항상 페이지 캐시만을 먼저 없애는 것은 아니다. 이번엔 조금 다른 방식으로 테스트해보자. 일정량 이상의 메모리를 사용하는 프로세스를 만든 후 다른 프로세스를 통해서 메모리 할당을 요청하는 방식이다.

여기까지 간단한 테스트를 통해서 캐시를 비우는 경우와 swap을 사용하는 경우를 확인해 보았다. 이를 통해서 커널은 기본적으로 유휴 메모리가 있을 경우 캐시로 활용하려 하고, 메모리 사용 요청이 증가하면 캐시로 활용하고 있는 메모리를 재할당해서 프로세스에 할당함을 확인할 수 있었다. 이런 동작은 커널의 기본적인 동작 원리인데, 커널에서는 몇가지 커널 파라미터를 이용해서 이런 동작 과정을 조금 더 사용자가 원하는 형태로 조절할 수 있도록 해준다. 바로 vm.swappinessvm.vfs_cache_pressure 두 가지 파라미터이다.

5.4 vm.swappinessvm.vfs_cache_pressure

먼저 vm_swappiness에 대해서 알아보자.

커널이 얼마나 공격적으로 메모리 영역을 swap 영역으로 옮기느냐를 결정하는 파라미터이며, 기본값은 60이라고 정의되어 있다. vm.swappiness 값은 커널 문서에도 정의되어 있는 것처럼 메모리가 부족한 상황에서 캐시를 비우느냐 아니면 특정 프로세스의 메모리 영역을 swap 영역으로 옮기느냐를 결정한다. 이 값이 커지면 캐시를 비우지 않고 swap 영역으로 옮기는 작업을 더 빨리 진행하고, 이 값이 작아지면 가능한 한 캐시를 비우는 작업을 진행한다.

코드 5-15를 보면 코드 5-13의 테스트와는 사뭇 다른 결과를 보여준다. 지난번에는 페이지 캐시가 거의 없어진 상황이 되어서야 swap을 사용하기 시작했지만, 이번에는 페이지 캐시의 용량이 꽤 남아 있는데도 swap을 사용하기 시작한다.

이렇게 vm.swappiness 값을 통해서 커널이 메모리를 재할당할 때 캐시 메모리를 재할당할 것인지 아니면 swap을 사용할 것인지의 비율을 조절할 수 있다. vm.swappiness 값이 작을수록 캐시 메모리를 재할당하고, 높을수록 swap 영역을 사용하게 된다.

그렇다면 왜 이런 인터페이스를 제공해주는 걸까? 무조건적인 페이지 캐시 해제가 항상 좋은 것만은 아니다. 페이지 캐시는 I/O 작업 시 디스크로의 접근을 줄여주기 때문에 전체적인 응답 속도 향상이 일어난다. 관점에 따라 다르겠지만 오히려 자주 사용하지 않는 프로세스의 메모리를 swap 영역으로 내리는게 더 좋을 수도 있다. 그렇기 때문에 커널은 vm.swappiness라는 파라미터를 통해서 사용자에게 선택권을 주고 있다.

그 다음으로 살펴볼 파라미터는 vm.vfs_cache_pressure이다.

커널이 메모리를 재할당할 때 디렉터리나 inode에 대한 캐시를 재할당하려는 경항을 조절한다고 설명하고 있다. vm.swappiness 값에 의해 캐시를 재할당할지 swap 영역을 사용할지가 결정된다면, vm.vfs_cache_pressure 값은 캐시를 재할당한다고 결정했을 때 PageCache를 더 많이 재할당할지 아니면 디렉터리나 inode 캐시를 더 많이 재할당할지를 결정한다. (이 파라미터는 디렉토리와 inode 오브젝트에 대한 캐시로 사용된 메모리를 반환(reclaim)하는 경향의 정도를 지정하는 항목이다. 기본 값은 100.)

vm.vfs_cache_pressure 파라미터의 기본값은 100이며, 이 값보다 크냐 작으냐에 따라 얼마나 많은 양을 재할당할 것인지를 결정한다. 100 이상이 되면 미사용 중이 아닌 캐시들도 반환하려고 하기 때문에 성능 저하가 발생할 수 있다.

linux_메모리_효율을_위한_vfs_cache_pressure [AllThatLinux!]

5.5 메모리 증설의 포인트

결국 swap 영역의 사용이 문제가 되는 이유는 불필요한 I/O를 일으켜서 시스템의 성능 저하를 일으키기도 하지만 그보다는 메모리가 현재의 워크로드를 수용하기에 부족하다는 것을 알 수 있기 때문이다. 캐시 영역 등을 비워도 요구하는 메모리의 양을 확보할 수 없기 때문에 다른 프로세스에서 사용하는 영역을 swap 영역으로 빼고 그 영역을 사용하는 상황이 된다.

그렇다면 시스템이 swap을 사용한다면 어떻게 대처해야 할까? 대답은 메모리를 증설해야 할 수도 있고, 그렇지 않을 수도 있다. 만약 애플리케이션이 메모리 해제를 하지 않아서 메모리 누수가 생긴 것이라면, 메모리를 증설한다고 해도 발현되는 시간만 조금 더 늘어날 뿐 결국 swap 영역을 사용하기 때문이다. 그렇다면 메모리의 누수가 있는건지, 아니면 정말로 더 많은 메모리가 필요한 것인지 어떻게 알 수 있을까?

두가지 경우로 나눠 생각해보자. 첫 번째는 메모리의 사용량이 선형적으로 증가하는 경우이다. 메모리의 사용량을 그래프로 그려서 확인해보면 그림과 같다.

위 서버는 시간이 지남에 따라 메모리의 사용량이 계속적으로 증가하고 있다. 이런 경우에는 보통 메모리 누수를 의심해볼 수 있다. 애플리케이션이 요청을 처리하기 위해 메모리를 할당 받고 요청이 끝나면 해당 메모리를 해제해야 하는데, 제대로 해제되지 않으면 사용하는 메모리가 계속해서 늘어난다. pmap 등의 명령을 통해서 해당 프로세스가 사용하는 힙 메모리 영역이 어떻게 변화하는지를 살펴보면 도움이 된다.

두번째는 순간적으로 메모리의 사용량이 폭증하는 경우다. 이 경우를 그래프로 그려보면 그림과 같다.

평상시에는 사용하는 메모리 양이 일정 수준을 유지하고 있다가 순간적으로 요청이 증가하면 메모리의 사용량이 폭증해서 swap을 사용하게 된다. 순간적으로 요청이 폭증하면 응답이 느려질 수 있기 때문에, 안정적인 서비스를 위해서 사용한 메모리의 최대치를 계산해서 메모리를 증설하면 도움이 된다.