검수요청.png검수요청.png

부하 (컴퓨터)

위키원
kskim5223 (토론 | 기여)님의 2020년 8월 12일 (수) 10:23 판 (원인 확인)
이동: 둘러보기, 검색

부하(load, 負荷)란 시스템에서 원하는 어떤 효과를 얻기 위해 취하는 행동에 필요한 동작이나 자원을 말한다. 예를 들어, 운영체계에서 프로세스들을 스케줄링함으로써 컴퓨터 자원의 이용 효율을 높일 수 있으나, 프로세스를 스케줄링하는 그 자체도 중앙처리장치(CPU)의 시간과 기억 장치가 소모된다. 만일 스케줄링을 잘못하여 스케줄링 자체의 부하가 너무 커진다면 아예 스케줄링을 하지 않는 것이 낫다. 이와 같이 부하는 적정한 한도 밑으로 유지되어야 하는 성질을 가지고 있다.

개요

부하는 컴퓨터계에서는 시스템이 수행해야 할 양의 척도로, 데이터(비트, 명령어, 작업, 프로세스)를 처리 / 실행 / 수행해 주는 주체인 CPUGPU에서 주로 취급하는 편인데 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]

프로세스

프로세스 상태

프로세스는 프로그램이 OS에 의해 실행되고 있을 때 그 실행 단위가 되는 개념이다. 프로세스는 커널 내부에서의 실행 단위를 나타내는 태스크와는 구별되기는 하지만 넓은 의미에서는 거의 같다. 프로세스란 '프로그램의 명령'과 '실행 시에 필요한 정보' 조합의 오브젝트를 말한다. 리눅스 커널은 프로세스마다 프로세스 디스크립터라는 관리용 테이블을 생성한다. 이 프로세스 디스크립터에 각종 실행 시 정보가 저장된다. 리눅스 커널은 이 프로세스 디스크립터 군을 우선도가 높은 순으로 재배열해서 실행 순으로 'Process=Task'가 실행되도록 조정한다. 이 조정 역할을 하는 것이 프로세스 스케줄러이다. 프로세스 스케줄러에 의해서 관리되는 프로세스는 디스크립터에 아래와 같은 상태 값을 갖게 된다.[3]

상태 값 의미
TASK_RUNNING 실행 가능 상태로 언제든지 CPU에 의해서 처리가 가능한 상태이다. 즉, CPU에 의해서 실행 중(RUNNING)일 수도 있고 가능한 상태(RUNNABLE)일 수도 있다.
TASK_INTERRUPTIBLE 인터럽트(Interrupt)에 의해서 언제든지 상태가 변할 수 있는 대기 상태이다. 일반적인 대기 상태를 의미한다. (ex. sleep, 터미널의 입력 대기)
TASK_UNINTERRUPTIBLE 인터럽트에 의해서 상태가 변경되지 않는 대기 상태이다. 즉, 현재 대기의 대상이 끝날 때까지 방해받지 않고 대기하는 상태이다. 보통 I/O 처리에 대해서 대기 중인 상태이다.
TASK_STOPPED 동작이 중단된 상태이다. 일시 정지된 상태로 재개 시그널을 받을 때까지 멈춰있게 된다. 터미널에서 전면(Foreground)에 실행 중인 프로그램에 대해서 Ctrl+Z를 입력하면 이러한 상태로 진입하게 된다.

보다 더 많은 상태 값이 존재하지만 이는 커널 소스의 include/linux/sched.h 파일에서 확인할 수 있다.[4]

평균 부하

평균 부하(Load Average)는 프로세스의 상태의 상태 중 R과 D 상태에 있는 프로세스 개수의 1분, 5분, 15분마다의 평균값이다. 즉, 얼마나 많은 프로세스가 실행 중 혹은 실행 대기 중인지를 의미하는 수치이다. 평균 부하가 높다면 많은 수의 프로세스가 실행 중이거나 I/O 등을 처리하기 위한 대기 상태에 있다는 것이며, 낮다면 적은 수의 프로세스가 실행 중이거나 대기 중이라는 의미이다. 프로세스의 수를 세는 것이기 때문에 시스템에 있는 CPU Core 수가 몇 개인지에 따라 각각의 값은 서로 가지고 있는 의미가 상대적이다.[5]

  • CPU core의 개수와 평균 부하의 관계
CPU Core가 하나인 경우와 2개인 경우 둘 다 평균 부하 값은 2의 근삿값이 나오겠지만 그 의미는 서로 차이가 있다. 첫 번째 경우는 하나의 Run Queue에 두 개의 프로세스가 있으며 이 경우 한 번에 하나만 실행되기 때문에 나머지 하나의 프로세스는 대기 상태에 있을 수밖에 없다. 이는 현재 시스템이 처리할 수 있는 프로세스보다 좀 더 많은 프로세스가 있다는 뜻이다. 하지만 두 번째 경우는 첫 번째와 똑같이 Run Queue에 두 개의 프로세스가 있지만, 서로 다른 CPU에 있기 때문에 A와 B는 동시에 실행될 수 있다. 현재 시스템에 처리 가능한 만큼의 프로세스가 있는 것이다. 이처럼 같은 평균 부하라고 해도 CPU core가 몇 개인지에 따라 전혀 다른 의미일 수 있다.[6]

계산 과정

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를 단순히 읽어서 보여주는 명령이다.[6]

/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 배열에 있는 값을 토대로 계산한 값을 넣어준다.[6]

grep으로 avenrun 배열 찾기
# grep -R avenrun ./*

...
./kernel/sched.c: avenrun[0] = calc_load(avenrun[0], EXP_1, active);
...

이 부분에서 제일 중요한 점은 calc_load() 함수와 관련된 부분이다. 해당 로직은 calc_global_load() 함수에 있다.[6]

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으로 찾다 보면 아래와 같은 함수를 볼 수 있다.[6]

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를 계산해서 넣어준다.[6]

비교

결과적으로 평균 부하는 상대적인 값이 아니고 계산하는 순간을 기준으로 존재하는 nr_running 상태의 프로세스 갯수와 nr_uninterruptible 상태의 프로세스 갯수를 합한 값을 바탕으로 계산되는 것이다. 평균 부하가 높다는 것은 단순히 CPU를 사용하려는 프로세스가 많다는 것을 의미하는 것이 아니고, I/O에 병목이 생겨서 I/O 작업을 대기하는 프로세스가 많을 수도 있다는 의미이다. 평균 부하 값만으로는 시스템에 어떤 상태의 부하가 일어나는지 확인하기 어렵다는 뜻이기도 하다. 부하를 일으키는 프로세스는 크게 두 가지 종류로 나눌 수 있다. nr_running으로 표현되는, CPU 자원을 많이 필요로 하는 CPU Bound 프로세스와 nr_uninterruptible로 표현되는, 많은 I/O 자원을 필요로 하는 I/O Bound 프로세스이다. 테스트 프로그램을 하나 만들어서 해당 프로세스들이 각각 어떻게 평균 부하로 표현되는지 살펴봐야 한다. 두 종류의 부하 프로세스는 같은 평균 부하를 보여준다고 해도 사실 일으키고 있는 부하는 전혀 다른 부하이다. 전자의 경우는 CPU가 너무 많이 사용해서 발생하는 부하이고, 후자의 경우는 I/O 리소스를 너무 많이 사용해서 발생하는 부하이다. 어떤 부하인지가 중요한 이유는, 부하의 종류에 따라서 해결 방법이 달라지기 때문이다. 평균 부하가 높다고 해서 단순히 CPU가 더 많은 장비를 사용하는 것으로 해결할 수 없다는 의미이다. 또한 비슷한 평균 부하라 하더라도 부하를 일으키는 원인이 무엇이냐에 따라 시스템의 반응 속도가 전혀 다를 수 있다.[5]

원인 확인

평균 부하 값은 시스템에 부하가 있다는 것을 알려주지만 구체적으로 어떤 부하인지는 알 수 없다. 어떤 부하가 일어나는지에 대한 정보는 vmstat을 통해서 확인할 수 있다.

procs -----------memory---------- ---swap-- -----io---- -system-- ------cpu-----
 r  b   swpd   free   buff  cache   si   so    bi    bo   in   cs us sy id wa st
 0  0  18432 292836     84 4678216    0    0     6   414    0    0  4  2 94  0  0
 2  0  18432 292420     84 4678652    0    0     0   140 6183 5941  2  1 97  0  0
 1  0  18432 292188     84 4678476    0    0     0   158 6345 6542  1  1 98  0  0
 0  0  18432 292332     84 4678648    0    0     0    52 6331 6393  1  1 98  0  0
 0  0  18432 301156     84 4671288    0    0   100   156 9068 8254  3  2 95  0  0
 1  0  18432 300888     84 4670744    0    0     0   160 8850 8926  2  1 96  0  1
 0  0  18432 300448     84 4671516    0    0     4   979 5865 4931  4  1 95  0  0

CPU 부하 프로세스는 r에 표시되고 I/O 부하 프로세스는 b컬럼에 표시되게 된다. r과 b가 각각 무엇을 의미하는지 man 페이지의 설명을 확인해볼 시 아래와 같다.

  Procs

             r: The number of processes waiting for run time.

             b: The number of processes in uninterruptible sleep.

r은 실행되기를 기다리거나 현재 실행되고 있는 프로세스의 갯수를, b는 I/O를 위해 대기열에 있는 프로세스의 갯수를 말한다. 즉 각각이 nr_running, nr_uninterruptible을 의미한다고 볼 수 있다. vmstat으로 확인하면 CPU가 일으키는 평균 부하인지, 아니면 I/O가 일으키는 평균 부하인지 확인해볼 수 있다. 사실 I/O가 일으키는 평균 부하 값이 1이나 2 정도로 낮은 편에 속한다고 해도(CPU가 1개 혹은 2개인 경우에는 높은 수준의 값이지만) 시스템에 문제를 일으킬 수 있는 소지가 있다. 지속적으로 I/O를 일으키는 프로세스가 시스템에 존재한다는 것을 의미하며, 의도하지 않은 불필요한 프로세스일 가능성이 있다.[5]

평균 부하가 시스템에 끼치는 영향

같은 수준의 평균 부하라면 시스템에 끼치는 영향도 같은까? 부하를 일으키는 원인이 무엇이냐에 따라서 같을 수도 다를 수도 있다. 같은 수치의 평균 부하라고 해도 그 원인에 따라 영향이 다를 수 있다는 뜻이다. CPU 기반의 부하를 일으키는 10개의 프로세스를 생성하고, GET 요청에 대한 응답시간을 측정한다. top 명령을 통해 프로세스 상태를 살펴보면, 이미 돌고 있는 10개의 스크립트들이 CPU를 차지하고 있고 요청을 처리하기 위한 nginx와 java 프로세스들이 중간중간에 끼어들어서 실행되고 있는 것을 볼 수 있다. nginx와 java를 방해하는 프로세스가 없는 상태보다는 응답 속도가 느려질 수 밖에 없는 상황이다. I/O 기반의 부하를 일으키는 10개의 프로세스를 생성하고, GET 요청에 대한 응답시간을 측정한다. 위의 경우와 같이 10개의 프로세스를 듸우기 때문에 uptime을 통해서 보는 평균 부하 값은 비슷하다. 하지만 앞선 케이스에 비해 응답소도가 빠르다. 왜 이런 결과가 발생하는걸까? I/O 부하를 일으키는 스크립트들은 I/O 대기 상태이기 때문에 프로세스 상태가 D이다. 하지만 CPU 기반의 부하일때와는 다르게 nginx와 java의 CPU Usage가 더 많다. 이는 CPU에 대한 경합이 전자의 경우보다 덜하기 때문에 더 빠른 응답 속도를 보여줄 수 있다는 의미이다. 즉, 돌리고 있는 프로세스가 어떤 시스템의 자원을 많이 쓰느냐에 따라서 부하가 시스템에 미치는 영향이 다르다는 것이다. 이를 통하여 서로 다른 형태의 부하는 시스템의 성능에 다른 영향을 끼친다는 것을 알 수 있다. 하지만, 커널은 완벽하지 않기 때문에 버그가 있을 수 있으며, 커널 버전이 달라지면 잘 알고 있는 모니터링용 지표가 제대로 수집되지 않을 가능성도 있다. 그렇기 대문에 하나의 지표로만 모니터링하거나 시스템의 상태를 확인하지 말고 다양한 툴들과 지표를 조합해서 운영해야 한다.[5]

각주

  1. 부하 나무위키 - https://namu.wiki/w/%EB%B6%80%ED%95%98
  2. injae Kim, 〈서버에 걸리는 부하, 추측하지 말고 계측하자〉, 《데브로그》, 2020-07-09
  3. 가그린민트, 〈부하란 무엇인가?〉, 《티스토리》, 2019-04-21
  4. Lunatine, 〈Load Average에 대하여〉, 《루나틴 박스》, 2016-02-19
  5. 5.0 5.1 5.2 5.3 12bme, 〈(엔지니어링) Load Average와 시스템 부하〉, 《티스토리》, 2019-12-25
  6. 6.0 6.1 6.2 6.3 6.4 6.5 임지후, 〈3.Load Average와 시스템 부하〉, 《깃북스》, 2016-02-19

참고자료

같이 보기


  검수요청.png검수요청.png 이 부하 (컴퓨터) 문서는 하드웨어에 관한 글로서 검토가 필요합니다. 위키 문서는 누구든지 자유롭게 편집할 수 있습니다. [편집]을 눌러 문서 내용을 검토·수정해 주세요.