부하 (컴퓨터)
부하(load, 負荷)란 시스템에서 원하는 어떤 효과를 얻기 위해 취하는 행동에 필요한 동작이나 자원을 말한다. 예를 들어, 운영체계에서 프로세스들을 스케줄링함으로써 컴퓨터 자원의 이용 효율을 높일 수 있으나, 프로세스를 스케줄링하는 그 자체도 중앙처리장치(CPU)의 시간과 기억 장치가 소모된다. 만일 스케줄링을 잘못하여 스케줄링 자체의 부하가 너무 커진다면 아예 스케줄링을 하지 않는 것이 낫다. 이와 같이 부하는 적정한 한도 밑으로 유지되어야 하는 성질을 가지고 있다.
개요
부하는 컴퓨터계에서는 시스템이 수행해야 할 양의 척도로, 데이터(비트, 명령어, 작업, 프로세스)를 처리 / 실행 / 수행해 주는 주체인 CPU와 GPU에서 주로 취급하는 편인데 Windows의 작업 관리자에서 확인할 수 있는 이용률 또는 사용률(영문 버전에서는 utilization 또는 usage라고 표시되어 있다)과 혼동해서 같은 의미처럼 취급하는 사람들이 많지만, 이용률과 부하는 엄연히 서로 다른 개념이다. 여러 프로세스들을 관리하는 서버 및 네트워크 분야에서의 부하 개념은 실행 중인 프로세스의 양과 대기 중인 프로세스의 양을 모두 합친 전체 프로세스의 양을 가리키고, 전체 프로세스의 양에 대한 실행 중인 프로세스의 양을 비율로 환산한 것을 이용률이라고 부른다. 쉽게 말해서 주어진 일의 양을 부하, 한 번에 처리할 수 있는 일의 양을 이용률에 해당되며, 만약 주어진 일의 양이 한 번에 처리할 수 있는 일의 양보다 더 적다면 이때 CPU의 이용률은 100%보다 낮은 값이 된다. 다만, 값이 수시로 바뀌는 순간 부하로는 그래프와 함께 모니터링하지 않는 한 전체적인 부하를 제대로 파악하기 어려우므로, 일반적으로 그냥 부하라고 하면 '일정 단위 시간당 평균 부하'로 통용된다.[1]
특징
단일 서버에 걸리는 부하의 원인은 크게 CPU 부하와 I/O 부하, 이 2가지로 분류된다.
- CPU 부하
- CPU 부하가 높은 경우는 서버에서 실행되고 있는 프로그램 자체의 연산량이 많은 경우나 프로그램에 오류 등이 발생한 경우이다. 이러한 경우에는 프로그램에서 발생하는 오류를 제거하거나 알고리즘의 시간, 공간 복잡도를 개선하여 대응할 수 있다.
- I/O 부하
- I/O 부하가 높은 경우는 서버에서 실행되고 있는 프로그램의 입출력이 많거나, 데이터베이스나 하드디스크 등의 저장 장치로의 접근이 많아 스왑이 발생하는 경우가 대부분이다. 이러한 경우 특정한 프로세스가 극단적으로 메모리를 소비하고 있는지 확인한 후, 프로그램 자체에 오류가 있다면 프로그램을 개선하거나 탑재된 메모리의 용량 자체가 부족한 경우 램을 추가하여 메모리를 증설하는 방법으로 대응할 수 있다.
저장 장치나 하드디스크로의 입출력이 빈번하게 발생하는 경우 또한 메모리를 증설하거나, 메모리 증설로 대응할 수 없는 경우는 데이터 자체를 분산(샤딩이나 파티셔닝)하거나 캐시 서버등을 도입하는 방안을 고려해볼 수 있다. 이러한 단일 서버에서 발생하는 부하를 측정하기 위해서는 리눅스 운영체제의 커널을 이용하면 된다. 리눅스나 윈도우 등의 운영체제에서는, 동시에 여러 프로세스들을 처리하기 위해 멀티태스킹 방식을 사용한다. CPU나 디스크 등의 유한한 하드웨어에서 여러 프로세스들을 동시에 처리하기 위해, 매우 짧은 시간 간격으로 여러 프로세스들을 돌아가면서 처리하는 방식이다. 이러한 멀티태스킹 방식에서, 처리해야 할 프로세스가 점점 많아지게 될 시 CPU를 사용하고 싶어 대기하고 있는 프로세스들이 점점 쌓이게 된다. 이렇게 CPU를 사용하려고 기다리고 있는 프로세스를 운영체제에서는 평균 부하(Load average)라고 정의한다. CPU를 사용하려고 기다리고 있는 프로세스가 많을수록, CPU에 주어진 일이 많다는 의미이고 결국 시스템에 걸리는 부하가 크다는 뜻이다. 리눅스에선 top 명령어에서 이러한 평균 부하를 확인할 수 있다. top 명령어로 1분, 5분, 15분 동안 몇 개의 태스크가 CPU를 사용하기 위해 기다리고 있는 대기 상태에 있었는지를 알아볼 수 있다. 따라서 평균 부하가 높은 상황은 지연되는 태스크가 많다는 것을 의미한다. 하지만, 평균 부하는 단순히 지연되는 태스크의 수를 의미하며, CPU 부하가 높은지 I/O 부하가 높은지까지는 상세하게 알 수 없다.[2]
프로세스
프로세스는 흔히 작업(Task)으로도 불리운다. 어떠한 명령을 수행하기 위한 코드와 거기에 필요한 데이터 값의 덩어리를 객체로 표현한 것이 프로세스이다. 프로세스가 시스템에서 처리되기 위해서는 CPU(Processor) 연산을 필요로 한다. 하지만, CPU는 물리적으로 한정적인 자원이기 때문에 이를 특정 프로세스가 독점하지 못하도록 OS(Linux)는 프로세스 스케줄러(Scheduler)를 통해서 조절하게 된다. 멀티(Multi)프로세서는 CPU 자원이 1개 이상일 뿐이지 근본적으로 한정적 자원이라는 점에서는 변함이 없다. 프로세스 스케줄러는 프로세스에 대한 디스크립터(기술자 - Descriptor)를 두고 관리를 하는데 프로세스 스케줄러에 의해서 관리되는 프로세스는 디스크립터에 아래와 같은 상태 값을 갖게 된다.
상태 값 의미 TASK_RUNNING 실행 가능 상태로 언제든지 CPU에 의해서 처리가 가능한 상태이다. 즉, CPU에 의해서 실행 중(RUNNING)일 수도 있고 가능한 상태(RUNNABLE)일 수도 있다. TASK_INTERRUPTIBLE 인터럽트(Interrupt)에 의해서 언제든지 상태가 변할 수 있는 대기 상태이다. 일반적인 대기 상태를 의미한다. (ex. sleep, 터미널의 입력 대기) TASK_UNINTERRUPTIBLE 인터럽트에 의해서 상태가 변경되지 않는 대기 상태이다. 즉, 현재 대기의 대상이 끝날 때까지 방해 받지 않고 대기하는 상태이다. 보통 I/O 처리에 대해서 대기 중인 상태이다. TASK_STOPPED 동작이 중단된 상태이다. 일시 정지된 상태로 재개 시그널을 받을 때까지 멈춰있게 된다. 터미널에서 전면(Foreground)에 실행 중인 프로그램에 대해서 Ctrl+Z를 입력하면 이러한 상태로 진입하게 된다.
평균 부하
평균 부하(Load Average)는 프로세스의 상태의 상태 중 R과 D 상태에 있는 프로세스 갯수의 1분, 5분, 15분마다의 평균값이다. 즉, 얼마나 많은 프로세스가 실행 중 혹은 실행 대기중이냐를 의미하는 수치이다. 평균 부하가 높다면 많은 수의 프로세스가 실행 중이거나 I/O 등을 처리하기 위한 대기 상태에 있다는 것이며, 낮다면 적은 수의 프로세스가 실행 중이거나 대기 중이라는 의미이다. 프로세스의 수를 세는것이기 때문에 시스템에 있는 CPU Core 수가 몇개냐에 따라 각각의 값은 의미가 상대적이다.[4]
계산 방법
- uptime 명령
# uptime 07:35:37 up 20 days, 12:45, 1 user, load average : 0.00, 0.00, 0.00 # strace -s 65535 -f -t -o uptime_dump uptime 07:36:26 up 20days, 12:59, 1 user, load average : 0.00, 0.00, 0.00
생성된 덤프 파일을 편집기로 열어서 확인할 시 execve()를 통해서 bash가 uptime 명령을 실행시키고, 관련된 라이브러리 파일들을 읽는 과정을 확인할 수 있다. 그 중 파일 하단부에는 아래와 같은 내용이 존재한다.
8011 07:36:26 open("/proc/loadavg", O_RDONLY) = 4 8011 07:36:26 lseek(4, 0, SEEK_SET) = 0 8011 07:36:26 read(4, "0.00 0.00 0.00 2/128 8011\n", 2047) = 26 8011 07:36:26 fstat(1, {st_mode=S_IFCHR|0620, st_rdev = makedev(136, 0), ...}) = 0
uptime 명령은 /proc/loadavg 파일을 열어서 그 파일의 내용을 읽고 화면에 출력해주는 명령이다. 즉, 직접 평균 부하 값을 계산하는 게 아니고 커널이 미리 준비해둔 /proc/loadavg를 단순히 읽어서 보여주는 명령이다.[5]
- /proc/loadavg 내용
# cat /proc/loadavg 0.00 0.00 0.00 1/127 8027
uptime에서 계산되었던 값들이 기록되어 있다. 이 값을 어떻게 만들어지는지 알아보기 위하여 우선 커널 코드를 살펴봐야 한다. 커널의 동작 원리를 분석하기 위해 어디서 시작할지 모를 때에는 strace로 시스템 콜을 분석해서 시작점을 찾는 방법도 유용하다. /proc 파일 시스템과 관련된 커널 소스는 fs/proc/에 위치해있다. 그 중 loadavg 파일과 관련된 파일은 fs/proc/loadavg.c 파일이다. 이 파일을 보면 loadavg_proc_show() 함수를 볼 수 있다.
static int loadavg_proc_show(struct seq_file *m, void *v) { unsigned long avnrun[3]; get_avenrun(avnrun, FIXED_1/200, 0); seq_printf(m, "%lu.%02lu %lu.%02lu %lu.%02lu %ld/%d %d\n", LOAD_INT(avnrun[0]), LOAD_FRAC(avnrun[0]), LOAD_INT(avnrun[1]), LOAD_FRAC(avnrun[1]), LOAD_INT(avnrun[2]), LOAD_FRAC(avnrun[2]), nr_running(), nr_treads, task_active_pid_ns(current) -> last_pid); return 0; }
이 함수를 통해서도 실제 계산되는 과정을 찾을 수는 없다. 이 함수 역시 내부적으로 계산된 값을 보여주는 함수이다. get_avenrun()함수를 통해 배열에 값을 넣는다는 사실이 도출된 것으로 이번에는 해당 함수를 찾아본다. 커널 함수를 찾는 방법에는 여러 가지가 있겠지만, 가장 쉽고 단순한 방법 중 하나인 grep을 사용하도록 한다.
#grep -R get_avenrun ./* .... ./kernel/sched.c:void get_avenrun)(unsigned long *loads, unsigned long offset, int shift) ...
kernel/sched.c파일에 get_avenrun함수가 정의되어 있다. 해당 함수의 내용을 아래와 같다.
void get_avenrun(unsigned long *loads, unsigned long offset, int shift){ loads[0] = (avenrun[0] + offset) << shift; loads[1] = (avenrun[1] + offset) << shift; loads[2] = (avenrun[2] + offset) << shift; }
이 함수는 unsinged long 형태의 배열을 인자로 받아서 해당 배열에 값을 넣어주는 함수인데, 중요한 배열이 하나 더 있다. 바로 avenrun배열로, 인자로 받은 loads 배열에 avenrun 배열에 있는 값을 토대로 계산한 값을 넣어준다.[5]
- grep으로 avenrun 배열 찾기
# grep -R avenrun ./* ... ./kernel/sched.c: avenrun[0] = calc_load(avenrun[0], EXP_1, active); ...
여기서 제일 중요한 부분은 calc_load()함수와 관련된 부분이다. 해당 로직은 calc_global_load() 함수에 있다.[5]
- clac_global_load()함수
void calc_global_load(void) { unsigned long upd = calc_load_update + 10; long active; if(time_before(jiffies, upd)); return; //calc_load_task값을 atomic_long_read()라는 매크로를 통해 읽어온후 active값에 넣는다. active = atomic_long_read(&calc_load_tasks); active = active > 0 ? active * FIXED_1 : 0; //active 값을 바탕으로 avenrun[]배열에 있는 값들을 calc_load()함수를 이용해서 계산하다. avenrun[0] = calc_load(avenrun[0], EXP_1, active); avenrun[1] = calc_load(avenrun[1], EXP_2, active); avenrun[2] = calc_load(avenrun[2], EXP_15, active); calc_load_update += LOAD_FREQ; }
active변수와 calc_load()함수를 보면 먼저 active 변수의 값을 알기 위해서는 calc_load_tasks가 어떤 값을 가지게 되는지 살펴봐야한다. grep으로 찾다보면 다음과 같은 함수를 볼 수 있다.[5]
- active 변수
static void calc_load_account_active(struct rq *this_rq) { long nr_active, delta; //nr_active 변수에 Run Queue를 기준으로 nr_running 상태의 프로세스 개수를 입력한다. 이 프로세스들이 바로 R 상태의 프로세스다. nr_active = this_rq->nr_running; //nr_ative 변수에 Run Queue를 기준으로 nr_uninterruptible 상태의 프로세스 개수를 더해준다. 이 프로세스들이 바로 D상태의 프로세스다. nr_active += (long) this_rq->nr_uninterruptible; if(nr_active != this_rq->calc_load_active){ delta = nr_active - this_rq->calc_load_active; this_rq->calc_load_active = nr_active; //nr_active값이 기존에 계산된 값과 다르다면 그 차이 값을 구한 후 calc_load_tasks변수에 입력한다. atomic_long_add(delta, &calc_load_tasks); } }
이렇게 cpu_load_account_active()함수가 매번 Tick 주기마다 깨어나서 현재 CPU의 Run Queue에 있는 nr_running프로세스의 개수와 nr_uninterruptible 프로세스의 개수를 세어서 calc_load_tasks 변수에 넣어준다. 그후 5초 간격으로 calc_global_load()함수가 calc_load_tasks 변수값을 바탕으로 1/5/15분 마다의 평균 Load Average를 계산해서 넣어준다.[5]
각주
- ↑ 부하 나무위키 - https://namu.wiki/w/%EB%B6%80%ED%95%98
- ↑ injae Kim, 〈서버에 걸리는 부하, 추측하지 말고 계측하자〉, 《데브로그》, 2020-07-09
- ↑ Lunatine, 〈Load Average에 대하여〉, 《루나틴 박스》, 2016-02-19
- ↑ 12bme, 〈(엔지니어링) Load Average와 시스템 부하〉, 《티스토리》, 2019-12-25
- ↑ 5.0 5.1 5.2 5.3 5.4 임지후, 〈3.Load Average와 시스템 부하〉, 《깃북스》, 2016-02-19
참고자료
- 부하 네이버 지식백과 - https://terms.naver.com/entry.nhn?docId=850456&cid=42346&categoryId=42346
- 부하 나무위키 - https://namu.wiki/w/%EB%B6%80%ED%95%98
- Lunatine, 〈Load Average에 대하여〉, 《루나틴 박스》, 2016-02-19
- 임지후, 〈3.Load Average와 시스템 부하〉, 《깃북스》, 2016-02-19
- 12bme, 〈(엔지니어링) Load Average와 시스템 부하〉, 《티스토리》, 2019-12-25
- injae Kim, 〈서버에 걸리는 부하, 추측하지 말고 계측하자〉, 《데브로그》, 2020-07-09
같이 보기