본 게시글은 천강민님의 게시글[1]과 김민환 님의 게시글[2]의 리뷰입니다.
여러번 정독하였음에도 기본 CS 지식의 부재로 해당 내용을 바로 이해하지 못하였고
부족한 CS 지식을 보완하면서 해당 내용을 조금이나마 이해할 수 있었습니다.
Overview
UNIX(Linux)는 제한 그룹*을 통해 프로세스가 사용할 리소스를 제한할 수 있습니다.
각 제한 그룹은 하나 이상의 하위 시스템과 연결되어 있으며, 하위 시스템은 제한 그룹이 가진 인터페이스 파일을 기반으로 각 리소스를 제한하는데 사용됩니다.
예를 들어,
cpu.weight는 두 프로세스 간의 CPU 점유율의 비율을 제어할 수 있고
cpu.height는 개별 프로세스의 최대 CPU 점유율을 제어할 수 있습니다.
cgroups이란?
원문 게시글[1][2]에서는 cpu 소개 전에 cgroups를 먼저 소개하고 있습니다.
이는 대다수 UNIX OS가 cgroups[3]를 사용하며 Docker, K8s도 cgroups 개념[4][5]을 사용하기 때문에 필수 CS 지식으로서 언급하신 것 같습니다.
원본 게시글[1]의 소개 서문은 아래와 같이 시작되어 있습니다.
cgroups(이하 제어 그룹) v2는 프로세스를 계층적으로 구성하고 각 계층 별로 시스템 리소스를 제어할 수 있도록 지원하는 기능입니다.
RedHat 게시글[3]을 요약하면 다음과 같은 특징을 가지고 있습니다.
제어 그룹은 Linux kernel 기능으로서 프로세스 그룹 간 리소스 및 조합을 할당합니다.
리소스는 cpu, mem, network bandwidth 등이며 아래 경로에서 확인 가능합니다.
cd /sys/fs/cgroup
ls -al
여기서 프로세스는 하나의 프로그램으로 이해해도 좋습니다.
Linux는 그 종류와 버전 별로 시스템 가동을 위해 필요한 system process[6]를 가지고 있으며 루트 시스템 프로세스인 systemd(혹은 init)[7]을 사용하여 부팅 시에 호출하게 됩니다. 또한 모든 프로세스는 단일 트리 구조로서 부모가 자식에게 일련의 설정값(PATH[D1], Open FileDescriptor) 등을 전달하는 구조를 가집니다.
제어 그룹의 각 계층은 하나 이상의 하위 시스템[D2] 등에 연결되어 있습니다.
하위 시스템은 cpu, mem 등과 연결되어 있는 resource controller를 의미합니다.
엄연히 따지면 제어 그룹과 process의 트리 구조는 다르다.
제어 그룹도 트리 구조이며 부모가 자식에게 설정값을 전달하지만
상호 독립된 개별적인 트리 구조라는 점에서 process와는 차이점이 존재합니다.
제어 그룹 하위에 경로를 만들면 하위 시스템이 적절한 인터페이스를 생성합니다. [8]
[ec2-user@... cgroup]# sudo mkdir sample
[ec2-user@... sample]$ ls | sed 's/^\([^\.]*\)\..*/\1/' | sort | uniq | tr '\n' ' ' && echo
cgroup cpu io memory pids
일반 사용자(e.g. ec2-user)는 /sys/fs/cgroup에 쓰기 권한이 없습니다.
따라서 루트 사용자(e.g. root)를 쓰거나 루트 권한을 참조하여 생성해야 합니다.[ec2-user@ip-172-31-10-203 cgroup]$ mkdir sample mkdir: cannot create directory ‘sample’: Permission denied
각 제어 그룹에서 적용된 하위 시스템은 /sys/fs/cgroup/cgroup.controllers에서 확인 가능합니다.
[ec2-user@... cgroup]$ cat /sys/fs/cgroup/cgroup.controllers
cpuset cpu io memory hugetlb pids misc
각 제어 그룹의 하위에서 사용 가능한 하위 시스템은 /sys/fs/cgroup/cgroup.subtree_control에 정의되어 있습니다.
[ec2-user@... cgroup]$ cat /sys/fs/cgroup/cgroup.subtree_control
cpu io memory pids
연속으로 실행해보면 앞서 생성한 /sys/fs/cgroup/sample/cgroup.controllers와 /sys/fs/cgroup/cgroup.subtree_control가 같은 값을 가진 것을 알 수 있습니다.
원본 게시글[2]에서는 이러한 폴더구조가 기본적으로 논리 제어 그룹이라고 말합니다.
이를 실질적인 격리 제어 그룹으로 사용하려면 <제어그룹>/cgroup.procs에 현재 터미널의 프로세스 기록이 필요하다고 말합니다.[2]
아래 코드를 통해 격리 제어 그룹에 현재 프로세스를 추가할 수 있습니다.
[ec2-user@... sample] cat /sys/fs/cgroup/sample/cgroup.procs
# 아무것도 나오지 않음
[ec2-user@... sample] echo $$
# 206266 가 출력이 됨
[ec2-user@... sample] echo $$ | sudo tee /sys/fs/cgroup/sample/cgroup.procs
# 206266 가 출력이 됨
[ec2-user@... sample] cat
/sys/fs/cgroup/sample/cgroup.procs
# 206266, xxxxxx가 출력이 됨
# xxxxxx는 cat을 실행할 때마다 달라짐
위 테스트를 통해 격리 제어 그룹과 관련된 2가지 특이사항을 발견했습니다.
cat /sys/fs/cgroup/cgroup.procs를 입력할때마다 랜덤 PID가 출력됩니다.
해당 명령어가 제(206266)가 실행한 cat process의 pid라고 생각합니다.
cat process 시작 → cgroup.procs에 기록 → cgroup.procs에서 값 읽기 → cat process 종료 → cgroup.procs에서 제거echo $$ | sudo tee … 를 입력한 터미널을 닫으면 제(206266)가 사라집니다.
개별 명령 터미널마다 별도의 프로세스 이므로 당연히 사라집니다.
terminal process 시작 → cgroup.procs에 기록 → terminal process 종료 → cgroup.procs에서 제거
즉 생성된 터미널이 가진 PID를 기반으로
터미널에서 입력한 명령어들은 하위 프로세스가 되며
루트 PID와 그 자식 PID들이 모두 cgroup.procs의 대상이 됨을 알 수 있었습니다.
원본 게시글[1]에서 유용한 스크립트를 공유해주셨습니다.
✅ 잠깐! 특정 프로세스가 어느 제어그룹에 속하는지 찾고 싶다면?
find /sys/fs/cgroup -name cgroup.procs -exec grep -H 9 {} \;
CPU Controller
CPU Controller의 주요 설정값인 cpu.weight, cpu.max에 따라서
CPU Controller의 핵심 지표인 cpu.stat 파일의 지표들의 변경을 추적하고자 합니다.
Cgroups option | K8s option | 정의 |
---|---|---|
cpu.weight | spec.containers[].resources.requests.cpu | CPU 사용 가중치 |
cpu.max | spec.containers[].resources.limits.cpu | CPU 사용 상한선 |
cpu.weight 및 cpu.max는 기본값으로 0, 10000을 가지며 비활성화 상태입니다.
비활성화 상태일 때에는 두 설정값의 영향력을 정확히 판단하기 어렵습니다.
cpu.weight는 두 프로세스 간의 CPU 점유율의 비율을 제어할 수 있고
cpu.height는 개별 프로세스의 최대 CPU 점유율을 제어할 수 있습니다.
컨테이너 및 오케스트레이션 환경에서 API 서버가 서빙되는 컨테이너는
공격자가 접근가능한 공격 표면으로 악용(Exploit) 될 수 있습니다. [11][12] 컨테이너 내부에서부터 대량의 스트레스를 가하는 등의 이상 작용을 할 수 있습니다. 따라소 보안 관점에서도 cpu.height | spec.containers[].resources.limits.cpu 설정하는 것이 옳습니다.
Pre-requisites
이를 위해서 Amazon Linux 3에 stress-ng를 설치하였습니다.
yum update && yum install stress-ng
추가적으로 3개의 터미널을 열고 개별적으로 cgroups를 생성했습니다.
sudo mkdir /sys/fs/cgroup/B
sudo mkdir /sys/fs/cgroup/C
cpu.stat 소개
usage_usec 0 # CPU 총 사용 시간(마이크로초 단위)
user_usec 0 # 사용자 모드에서 실행된 시간
system_usec 0 # 커널 모드에서 실행된 시간
### 아래 값은 cpu.max에 MAX(QUOTA)가 설정된 경우 증가
nr_periods 0 # 스케줄러에 의해 CPU가 할당된 횟수
nr_throttled 0 # 사용 가능한 CPU 시간을 초과 또는 남은 시간이 부족하여 쓰로틀링에 걸린 횟수
throttled_usec 0 # 쓰로틀링 걸린 시간
### 아래 값은 cpu.max.burst가 설정된 경우 증가
nr_bursts 0 # CPU 버스트 모드로 전환된 횟수
burst_usec 0 # 버스트 모드에서 사용한 시간
cpu.weight 테스트
EC2 (t3.medium : pCPU 1, vCPU 2, SMT enabled), Amazon Linux 2023
가설
vCPU가 2이기 때문에 스트레스 도구의 인자가 2보다 커져야지 cpu.weight 비중에 따라서 CPU Scheduling[9]이 될 것으로 추정합니다.
계획
두 개의 제한 그룹과 각 그룹에 속한 두 개의 터미널에 각각 cpu.weight를 100, 200 할당했습니다. 이 환경에서 stress-ng --cpu <수치>에서 수치를 1 ~ 5까지 증분하며, top 명령어로 매트릭 변경을 모니터링했습니다.
B Terminal Only : stress --cpu 1, 2, … , 5
C Terminal Only : stress --cpu 1, 2, … , 5
B,C Treminal Both : stress --cpu 1, 2, … , 5
결론
서버의 vCPU가 2 이기 때문에 <수치> 의 총합이 2가 넘어가면 T CPU가 일정 구간에서 유지되며, cpu.weight 비중에 맞춰서 CPU Scheduling이 일어나는 것을 확인했습니다. 상세 지표에는 다루지 않지만 <수치>의 증가에 따라서 %Cpu(s)의 8가지 지표 중에서 st(stolne time)이 급증하며 CPU Scheulding 상의 비효율성이 증가됨을 알았습니다.
T CPU% : 프로세스 별 CPU 사용량 (평균) * cnt
환경
아래의 구문을 이용해서 cpu.weight 설정을 하였습니다.
Terminal B
sudo echo $$ | sudo tee /sys/fs/cgroup/B/cgroup.procs sudo echo 100 | sudo tee /sys/fs/cgroup/B/cpu.weight
Terminal C
sudo echo $$ | sudo tee /sys/fs/cgroup/C/cgroup.procs sudo echo 200 | sudo tee /sys/fs/cgroup/C/cpu.weight
이후 아래와 같이 대시보드 구성했습니다.
# 좌상단 3개
watch -n 1 'cat /sys/fs/cgroup/B/cgroup.procs'
watch -n 1 'cat /sys/fs/cgroup/B/cpu.weight'
watch -n 1 'cat /sys/fs/cgroup/B/cpu.stat'
# 중앙 1개
top
# 좌하단
cd ~
stress-ng .
지표 측정
EC2 (t3.medium : pCPU 1, vCPU 2, SMT enabled), Amazon Linux 2023
%Cpu(s) : 시스템 전체 부하량
CPU% : 프로세스 CPU 사용량 (평균)
T CPU% : 프로세스 CPU 사용량 (평균) * cnt
cnt : stress-ng --cpu <cnt>의 수량
thr : nr_throttle, throttled_usec 값 (0,0)
Terminal | B | C | |||||||
---|---|---|---|---|---|---|---|---|---|
cpu.weight | 100 | 200 | |||||||
Cov | %Cpu(s) | cnt | CPU% | T CPU% | thr | cnt | CPU% | T CPU% | thr |
100-1 | 40 | 1 | 75 | 80 | 0,0 | - | - | - | - |
100-2 | 99 | 2 | 85 | 170 | 0,0 | - | - | - | - |
100-3 | 85 | 3 | 55 | 165 | 0,0 | - | - | - | - |
100-4 | 85 | 4 | 42 | 172 | 0,0 | - | - | - | - |
100-5 | 90 | 5 | 38 | 190 | 0,0 | - | - | - | - |
200-1 | 40 | - | - | - | - | 1 | 75 | 75 | 0 |
200-2 | 85 | - | - | - | - | 2 | 85 | 170 | 0 |
200-3 | 85 | - | - | - | - | 3 | 55 | 165 | 0 |
200-4 | 85 | - | - | - | - | 4 | 42 | 168 | 0 |
200-5 | 85 | - | - | - | - | 5 | 33 | 165 | 0 |
300-1 | 88 | 1 | 85 | 85 | 0 | 1 | 85 | 85 | 0 |
300-2 | 87 | 2 | 30 | 60 | 2 | 60 | 120 | ||
300-3 | 88 | 3 | 19 | 57 | 3 | 38 | 114 | ||
300-4 | 89 | 4 | 15 | 60 | 4 | 31 | 124 | ||
300-5 | 88 | 5 | 11 | 55 | 5 | 22 | 110 |
cpu.max 테스트
EC2 (t3.medium : pCPU 1, vCPU 2, SMT enabled), Amazon Linux 2023
가설
vCPU가 2이기 때문에 스트레스 도구의 인자가 2보다 커져야지 cpu.max로 인한 유의미한 테스트가 가능할 것으로 추정합니다.
2보다 커지면 스트레스 도구의 인자가 커질수록 cpu.stat의 nr_throttle, throttled_usec 수치가 급증할 것으로 보입니다.
계획
두 개의 제한 그룹과 각 그룹에 속한 두 개의 터미널 아래 처럼 cpu.max 할당했습니다.
이 환경에서 stress-ng -cpu <수치>를 1~2로 증분하며, top 매트릭 변경을 모니터링했습니다.
(10000 100000)
(50000 100000)
본 테스트는 B, C Terimanl 동시 테스트만 진행하였습니다.
B, C Terminal Only : stress --cpu 1, 2
결론
적절한 cpu.max 설정값을 가지면 <숫자>의 크기와 무관하게 지표가 증가합니다.
nr_periods, nr_throttled, throttled_usec 지표가 전체적으로 상승합니다.
nr_periods 651
nr_throttled 640
throttled_usec 76219551
서버의 vCPU 수와 무관하게 <수치> 의 총합이 N 배로 커지더라도
T CPU(%)는 고정값으로 유지되며 CPU(%)가 T CPU(%) / N으로 조절됩니다.
환경
아래의 구문을 이용해서 cpu.max 설정을 하였습니다.
Terminal B
sudo echo $$ | sudo tee /sys/fs/cgroup/B/cgroup.procs sudo echo "10000 100000" | sudo tee /sys/fs/cgroup/B/cpu.max
Terminal C
sudo echo $$ | sudo tee /sys/fs/cgroup/C/cgroup.procs sudo echo "50000 100000" | sudo tee /sys/fs/cgroup/C/cpu.max
이후 아래와 같이 대시보드 구성했습니다.
# 좌상단 3개
watch -n 1 'cat /sys/fs/cgroup/B/cgroup.procs'
watch -n 1 'cat /sys/fs/cgroup/B/cpu.weight'
watch -n 1 'cat /sys/fs/cgroup/B/cpu.stat'
# 중앙 1개
top
# 좌하단
cd ~
stress-ng .
지표 측정
EC2 (t3.medium : pCPU 1, vCPU 2, SMT enabled), Amazon Linux 2023
%Cpu(s) : 시스템 전체 부하량
CPU% : 프로세스 CPU 사용량 (평균)
T CPU% : 프로세스 CPU 사용량 (평균) * cnt
cnt : stress-ng --cpu <cnt>의 수량
thr : nr_throttle, throttled_usec 값 (0,0)
Terminal | B | C | |||||||
---|---|---|---|---|---|---|---|---|---|
cpu.weight | 100 | 200 | |||||||
Cov | %Cpu(s) | cnt | CPU% | T CPU% | thr | cnt | CPU% | T CPU% | thr |
300-1 | 32 | 1 | 10 | 10 | 0 | 1 | 50 | 10 | 0 |
300-2 | 32 | 2 | 5 | 10 | 0 | 2 | 25 | 50 | 0 |
Ref. (14)
Tech. Ref. (12)
Description. Ref. (2)
Tech. Ref. (12)
[7] systemd vs. init: Decoding the Linux boot process, FOSS Linux
[12] Dirty COW Vulnerability : Impact on Containers, Blog of Aquasec
Description Ref. (2)
[D1] The parent process is able to alter the environment before passing it to a child process.
[D2] You should be aware that subsystems are also called resource controllers, or simply controllers, in the libcgroup man pages and other documentation.