본문 바로가기
IT & 데이터 사이언스/Data Engineering

[Docker] 컨테이너 자원 할당

by 바른 호랑이 2024. 8. 29.
728x90
반응형

안녕하세요. 바른호랑이입니다.

이번 게시글에서는 Docker 컨테이너 자원할당에 대해서 알아볼 예정입니다.

게시글은 '시작하세요! 도커/쿠버네티스 친절한 설명으로 쉽게 이해하는 컨테이너 관리'를 기반으로 작성하였으니 참고 바랍니다.

Docker는 기본적으로 물리적인 컴퓨팅 자원을 논리적으로 분할하여 사용하는 것이기에 자원의 총량을 고려하지 않고 적정량을 할당하지 않으면 호스트의 자원을 제한 없이 쓸 수 있습니다. 그렇기에 여러 개의 컨테이너를 사용하는 경우에 자원 할당을 제한하지 않으면 다른 컨테이너의 동작을 방해할 수 있기에 사전에 사용한 자원량을 정해놓고 생성 후 구동하는 것이 필요합니다. 

컨테이너에 자원이 얼마나 할당되어 있는지와 같은 현재 상태를 확인하기 위해서는 docker inspect 명령어를 사용할 수 있으며, 이를 보다 명확히 확인하기 위해 임시로 컨테이너를 생성한 후 docker inspect 명령어를 통해 확인해 보았습니다.

# create container
sudo docker run -d --name test \
ubuntu:24.04

# check container status
sudo docker inspect test

컨테이너의 메모리의 제한을 주기 위해서는 --memory 옵션을 사용할 수 있으며, 이때 해당 옵션에 입력할 수 있는 단위는 m(megabyte), g(gigabyte)로 최소 4MB까지 제한할 수 있습니다. 이를 실제로 확인해 보기 위해 아래의 명령어를 통해 컨테이너를 생성하고 컨테이너의 상태를 조회해 보았습니다.

# create container
sudo docker run -d \
--memory="1g" \
--name memory_1g \
nginx

# check container memory
sudo docker inspect memory_1g | grep \"Memory\"

컨테이너 내에서 동작하는 프로세스가 컨테이너의 할당된 메모리를 초과하면 컨테이너는 자동으로 종료되기에 애플리케이션에 따라 메모리를 적절하게 할당하는 것이 필요합니다. 아래의 코드를 통해 실제로 정지되는지 확인해 보았습니다.

# create container
sudo docker run -d --name memory_4m \
--memory="4m" \
mysql:5.7

추가적으로 컨테이너의 swap 메모리는 기본적으로 메모리의 2배로 설정되어 만들어지지만, --memory-swap 옵션을 통해 직접 지정해 줄 수도 있습니다. 

# swap memory: 실제 메모리를 초과하는 메모리가 필요할 경우 디스크 공간을 가상 메모리로 대체하여 사용하는 방법
# create container
sudo docker run -it --name swap_500m \
--memory=200m \
--memory-swap=500m \
ubuntu:24.04

 

컨테이너의 CPU 사용제한을 주기 위해서는 --cpu-shares 옵션을 사용할 수 있습니다. 다만 --cpu-shares 옵션은 사용할 cpu의 개수를 지정하는 것이 아니라 가중치를 설정해 해당 컨테이너가 CPU를 상대적으로 얼마나 사용할 수 있는지를 지정해 주는 옵션이라는 점을 유의해야 합니다. 만약 cpu의 자원제한을 걸지 않고 컨테이너를 생성하면 기본적으로 --cpu-shares는 1024로 설정되며, 이는 CPU 할당에서 1의 비중을 의미합니다. 다만 자원제한을 설정하더라도 CPU의 상황이 여유롭다면 각 컨테이너는 설정된 값과 상관없이 필요한 만큼 CPU를 사용할 수 있으며, CPU가 부족해지면 --cpu-shares의 값에 따라 CPU 자원이 배분되는 것을 유의해야합니다. 이를 보다 명확히 알아보기 위해 아래의 코드를 통해 컨테이너를 생성하고 강제로 부하를 준 후 cpu 사용량을 확인해 보았습니다.

# create container
# alicek106/stress 이미지는 강제로 과부하를 일으키기 위해 stress 패키지가 설치되어 있는 우분투 이미지임.
sudo docker run -d --name cpu_1024 \
--cpu-shares 1024 \
alicek106/stress \
stress --cpu 1

# check cpu usage
ps aux | grep stress

ps aux | grep stress 명령어

 

- a: 모든 사용자의 프로세스를 의미

- u: 프로세스 소유자의 이름, CPU 사용률, 메모리 사용률 등을 포함하여 출력

- x: 터미널에 종속되지 않은 프로세스도 포함하여 출력

 

는 현재 시스템에서 stress라는 단어가 포함된 프로세스에 대한 정보를 보여주는 명령어로 출력되는 정보는 왼쪽부터 차례대로 USER, PID, % CPU, % MEM, VSZ, RSS, TTY, STAT, START, TIME, COMMAND입니다.

 

- USER: 프로세스를 실행한 사용자 이름

- PID: 프로세스 ID

- % CPU: 프로세스가 사용하는 CPU사용률

- % MEM: 프로세스가 사용하는 물리적 메모리의 비율

- VSZ: 가상 메모리 사용량(KB 단위)

- RSS: 실제 메모리 사용량(KB 단위)

- TTY: 프로세스가 연결된 터미널로 '?'는 터미널에 연결되지 않은 경우를 나타냄

- STAT: 프로세스의 상태

(R: Running / 실행 중, S: Sleeping / 대기 중, Ss: Session Leader(대기 중이면서 세션리더), S+: Foreground Sleeping / 포그라운드에서 대기 중)

- START: 프로세스가 시작된 시간

- TIME: 프로세스가 CPU에서 사용한 총 시간

- COMMAND: 실행된 명령어 및 인자

 

위의 내용대로 결과물의 두 번째 행을 해석해 보면 아래와 같습니다.

 

root라는 사용자가 11150의 PID를 가지는 프로세스를 실행시켰고, 사용 중인 CPU 사용률은 98.9%이며 메모리 사용률은 0.0 임. 가상메모리는 7488KB, 실제 메모리는 128KB를 사용 중이며, 터미널에 연결되어 있지 않은 상태이고, 현재 실행 중임. 프로세스가 시작된 시간은 14:29이고 프로세스가 CPU에서 사용한 총시간은 6:58이며 실행한 명령어는 stress --cpu 1 임.

 

위의 예시에서는 cpu-shares의 값을 1024로 설정하였지만 호스트에 다른 컨테이너가 없기에 CPU를 100% 사용하고 있습니다. 이 상태에서 만약 --cpu-shares가 512인 컨테이너가 같이 실행되면 어떤 결과가 나올지 확인해 보기 위해 아래의 코드로 새로운 컨테이너를 생성 후 ps aux | grep stress 명령어로 상태를 확인해 보았습니다.

# create container
sudo docker run -d --name cpu_512 \
--cpu-shares 512 \
alicek106/stress \
stress --cpu 1

# check cpu usage
ps aux | grep stress

어느 정도 시간이 흐른 뒤, CPU 사용률을 확인해 보았더니 약 2:1 비율로 CPU를 나누어 사용 중인 것을 확인할 수 있었습니다. 즉, 1024:512(2:1) 비율대로 시스템의 CPU를 사용하는 것을 알 수 있었습니다. 해당 내용을 확인한 후에는 위에서 생성한 컨테이너들은 일부러 CPU에 부하를 주기 위해 명령어를 실행시켜 놓았으므로 삭제해 주었습니다.

호스트에 CPU가 여러 개 있을 경우에는 --cpuset-cpus 옵션을 사용하여 컨테이너가 특정 CPU만 사용하도록 설정할 수도 있으며, CPU 집중적인 작업이 필요할 경우 여러 개의 CPU를 사용하도록 할당할 수도 있습니다. 해당 옵션을 적용했을 경우 어떠한 방식으로 구동되는지 확인해 보기 위해 CPU별 사용량을 확인할 수 있는 도구인 htop을 설치하고 컨테이너를 생성해 보았습니다.

# install htop
sudo apt install htop

# check cpu usage
htop

# create container
sudo docker run -d --name cputset_2 \
--cpuset-cpus=2 \
alicek106/stress \
stress --cpu 1

# check cpu usage
htop

컨테이너의 CFS(Completely Fair Scheduler, Linux에서 사용하는 CPU 스케줄링 알고리즘으로 프로세스가 CPU를 기다리는 데 소요한 시간을 계산하여 우선순위를 동적으로 할당하는 방식. 오래 대기한 프로세스에 더 높은 우선순위가 부여될 확률이 높음.) 주기는 기본적으로 100ms로 설정되며, 해당 내용을 변경하여 적용하기 위해서는 --cpu-period와 --cpu-quota 옵션을 사용할 수 있습니다. --cpu-period의 값은 기본적으로 100000이며, 이는 100ms를 의미하고, --cpu-quota는 --cpu-period에 설정된 시간 중 CPU 스케줄링에 얼마나 할당할 것인지를 설정합니다. 예를 들어 --cpu-period값을 100000으로 주고, --cpu-quota를 25000으로 주었다면 100000 중 25000만큼을 할당해 준 것이므로 일반적인 컨테이너보다 CPU 성능이 1/4로 감소합니다. 즉, 컨테이너는 [--cpu-quota] / [--cpu-period]만큼 CPU 시간을 할당받는다고 할 수 있습니다. 이를 보다 명확하게 알아보기 위해 아래의 코드를 통해 컨테이너 2개를 생성하고 ps aux | grep stress 명령어로 확인해 보았습니다.

# create container 1
sudo docker run -d --name quota_1_4 \
--cpu-period=100000 \
--cpu-quota=25000 \
alicek106/stress \
stress --cpu 1

# create container 2
sudo docker run -d --name quota_1_1 \
--cpu-period=100000 \
--cpu-quota=50000 \
alicek106/stress \
stress --cpu 1

# check cpu usage
ps aux | grep stress

생성 후 CPU 할당량을 확인해 보니 첫 번째 컨테이너 25%, 두 번째 컨테이너가 50%를 사용하는 것을 확인할 수 있었습니다. 이외에도 --cpus 옵션을 통해 --cpu-period, --cpu-quota 옵션처럼 CPU 할당량을 줄 수도 있습니다. --cpus 옵션은 직접 CPU의 개수를 지정한다는 점이 차별점이며, 이를 확인해 보기 위해 직접 컨테이너를 생성 후 결과를 확인해 보았습니다.

# create container 1
sudo docker run -d --name cpus_container \
--cpus=0.5 \
alicek106/stress \
stress --cpu 1

# create container 2
sudo docker run -d --name quota_1_1 \
--cpu-period=100000 \
--cpu-quota=50000 \
alicek106/stress \
stress --cpu 1

# check cpu usage
ps aux | grep stress

CPU의 할당량을 조정하는 명령어로 --cpu-share, --cpus, --cpu-period, --cpu-quota, --cpuset-cpu와 같은 다양한 명령어들이 있지만 병렬처리를 위해 CPU를 많이 소모하는 워크로드를 수행해야 할 경우에는 --cpuset-cpu 옵션을 사용하는 것이 좋습니다. 이는 해당 명령어가 특정 컨테이너가 특정 CPU에서만 동작하는 CPU 친화성(Affinity)을 보장할 수 있고, CPU 캐시 미스 도는 콘텍스트 스위칭과 같이 성능을 하락시키는 요인을 최소화할 가능성이 높아지기 때문입니다.

 

기본적으로 컨테이너를 생성할 경우 아무런 옵션을 설정하지 않으면 컨테이너 내부에서 파일을 읽고 쓰는 대역폭 제한은 설정되지 않으며, 특정 컨테이너가 블록 입출력을 과도하게 사용하지 않게 설정하려면 --device-write-bps, --device-read-bps, --device-write-iops, --device-read-iops 등의 옵션을 지정하여 Block I/O를 제한할 수 있습니다. 단, Direct I/O의 경우에만 블록 입출력이 제한되며, Buffered I/O는 제한되지 않는다는 점을 유의해야 합니다.

--device-write-bps, --device-read-bps는 각기 쓰고 읽는 작업의 초당 제한을 설정하는 옵션으로 kb, mb, gb 단위로 제한할 수 있습니다. 만약 아래와 같이 컨테이너를 생성한다면 초당 쓰기 작업의 최대치가 1MB로 제한됩니다.

# check device name
df -h /var/lib/docker
lsblk

# create container
sudo docker run -it \
--device-write-bps /dev/sda:1mb \
ubuntu:24.04

# check block I/O result
dd if=/dev/zero of=test.out bs=1M count=10 oflag=direct

해당 옵션을 사용할 경우 주의해야 점은 Block I/O를 제한하는 옵션은 [디바이스 이름]:[값] 형태로 설정해야 한다는 점입니다. 만약 /dev/xvda라는 디바이스를 사용 중이라면 디바이스 이름에 /dev/xvda라는 값을 /dev/sda라는 디바이스를 사용중이라면 /dev/sda를 입력해주어야 합니다.

--device-write-iops, --device-read-iops 옵션은 --device-write-bps, --device-read-bps 옵션처럼 Block I/O 제한을 설정하는 것은 동일하지만 절대적인 값이 아닌 상대적인 수치에 따라 제한을 적용합니다. 만약 --device-write-iops /dev/sda:5라는 옵션을 준 컨테이너와 --device-write-iops /dev/sda:10이라는 옵션을 준 컨테이너가 있을 때 쓰기 작업을 수행하면 수행 시간이 2배가량 차이 난다고 할 수 있습니다.

 

도커 엔진의 경우 보편적으로 컨테이너 내부의 저장 공간을 제한하는 기능을 제공하지 않지만 도커의 스토리지 드라이버나 파일 시스템 등이 특정 조건을 만족하는 경우 제한적인 사용이 가능합니다. 이는 스토리지 드라이버에 대해 학습할 때 보다 세부적으로 알아볼 예정입니다. 다만 모든 스토리지 드라이버에서 컨테이너 저장 공간을 제한할 수 있는 것을 아니라는 점을 유의해야 하며, 만약 컨테이너 애플리케이션이 해당 스토리지 드라이버에 적합하지 않을 경우 해당 기능을 사용하지 않는 것이 좋을 수도 있습니다. 그렇기에 컨테이너의 저장공간 제한은 상황에 맞게 적절한 고려를 한 후 이루어져야 합니다.

728x90
반응형

댓글