✌️
Studylog
See More
Booklog
Booklog
  • 객체지향의 사실과 오해
    • 1장. 협력하는 객체들의 공동체
    • 2장. 이상한 나라의 객체
  • DevOps와 SE를 위한 리눅스 커널이야기
    • 1장. 시스템 구성 정보 확인하기
    • 2장. top을 통해 살펴보는 프로세스 정보들
    • 3장. Load Average와 시스템 부하
    • 4장. free 명령이 숨기고 있는 것들
Powered by GitBook
On this page
  • Load Average 정의
  • Load Average 계산 과정
  • 요약
  1. DevOps와 SE를 위한 리눅스 커널이야기

3장. Load Average와 시스템 부하

Previous2장. top을 통해 살펴보는 프로세스 정보들Next4장. free 명령이 숨기고 있는 것들

Last updated 19 days ago

흔히 Load가 높다 / 낮다 라는 표현하는 값의 의미가 무엇이고 시스템에 어떤 영향을 미치는지, 그리고 시스템의 부하를 어떻게 결정하면 좋을지 살펴본다.

Load Average 정의


man proc 이 정의하는 loadavg

  /proc/loadavg
          The first three fields in this file are load average figures giving the number of jobs in the run queue (state R) or waiting for disk I/O
          (state D) averaged over 1, 5, and 15 minutes.  They are the same as the load average numbers given by uptime(1) and other programs.   The
          fourth  field consists of two numbers separated by a slash (/).  The first of these is the number of currently runnable kernel scheduling
          entities (processes, threads).  The value after the slash is the number of kernel scheduling entities that currently exist on the system.
          The fifth field is the PID of the process that was most recently created on the system.

2장에서 확인한 프로세스의 상태 중 "R" 과 "D" 상태에 있는 프로세스 개수의 1분, 5분, 15분마다의 평균 값이라고 한다.

Load Average가 높다면 많은 수의 프로세스가 실행중이거나 I/O 등을 처리하기 위한 대기 상태에 있다는 것이다.

하지만, 프로세스의 수를 세는 것이기 때문에 시스템에 있는 CPU Core 수에 따라 각각의 값의 의미는 상대적이다.

Load Average 계산 과정


"fs/proc/loadavg.c d 파일의 loadavg_proc_show() 분석하기"

1

avnrun 이라는 unsigned long 타입의 3개짜리 배열을 선언한 뒤 get_aven_run(avnrun, FIXED_1/200, 0); 함수를 통해 avnrun 배열에 값을 입력한다.

2

avnrun 배열에 있는 값을 토대로 Load Average 값을 출력

이 함수를 통해 실제 계산되는 과정을 찾을 수 없다. 이 함수는 내부적으로 계산된 값을 보여주는 함수이다.

"get_aven_run() 분석하기"

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;
}

unsigned long 형태의 배열을 인자로 받아서 해당 배열에 값을 넣어주는 함수인데, 해당 함수 내부에 사용중인 avenrun 배열이 인자로 받은 loads 배열에 계산한 값을 넣어주고있다.

아쉽지만 이 함수도 Load Average를 계산 해주는 함수가 아니었다. 그래도 avenrun 배열이라는 힌트를 얻었으니, 이번엔 avenrun 배열을 찾아보자.

"avenrun 을 사용하고 있는 코드를 찾아라"

void calc_global_load(void)
{
	unsigned long sample_window;
	long active, delta;

	sample_window = READ_ONCE(calc_load_update);
	if (time_before(jiffies, sample_window + 10))
		return;

	/*
	 * Fold the 'old' NO_HZ-delta to include all NO_HZ CPUs.
	 */
	delta = calc_load_nohz_read();
	if (delta)
		atomic_long_add(delta, &calc_load_tasks);

	active = atomic_long_read(&calc_load_tasks);
	active = active > 0 ? active * FIXED_1 : 0;

	avenrun[0] = calc_load(avenrun[0], EXP_1, active);
	avenrun[1] = calc_load(avenrun[1], EXP_5, active);
	avenrun[2] = calc_load(avenrun[2], EXP_15, active);

	WRITE_ONCE(calc_load_update, sample_window + LOAD_FREQ);

	/*
	 * In case we went to NO_HZ for multiple LOAD_FREQ intervals
	 * catch up in bulk.
	 */
	calc_global_nohz();
}

calc_load_tasks 값을 atomic_long_read() 라는 매크로를 통해서 읽어온 뒤 active 변수에 할당한다.

이 active 값을 기준으로 avenrun[] 배열에 있는 값들을 calc_load() 함수를 이용하여 계산한다.

이 코드에서 얻을 수 있는 정보는 active 변수와 calc_load() 함수이며, 이 값을 알기 위해서 선행 되었던 calc_load_tasks 가 어떤 값을 가지게 되는지 분석해야한다.

"calc_load_tasks 분석하기"

long calc_load_fold_active(struct rq *this_rq, long adjust)
{
	long nr_active, delta = 0;

	nr_active = this_rq->nr_running - adjust; // 1번
	nr_active += (int)this_rq->nr_uninterruptible; // 2번

	if (nr_active != this_rq->calc_load_active) {
		delta = nr_active - this_rq->calc_load_active;
		this_rq->calc_load_active = nr_active;
	}

	return delta;
}
1

nr_active 변수에 Run Queue 를 기준으로 nr_running 상태의 프로세스 개수를 adjust 값을 뺀 뒤 입력한다. (R 상태 프로세스)

2

nr_active 변수에 nr_uninterruptible 상태의 프로세스 개수를 더한다. (D 상태 프로세스)

3

nr_active 값이 기존에 계산된 값과 다르다면 그 차이 값을 calc_load_active 에 입력한다.

/*
 * Called from sched_tick() to periodically update this CPU's
 * active count.
 */
void calc_global_load_tick(struct rq *this_rq)
{
	long delta;

	if (time_before(jiffies, this_rq->calc_load_update))
		return;

	delta  = calc_load_fold_active(this_rq, 0);
	if (delta)
		atomic_long_add(delta, &calc_load_tasks); // calc_load_task 입력

	this_rq->calc_load_update += LOAD_FREQ;
}

kernel/sched/core.c 의 void sched_tick(void) 함수가 Tick 주기마다 깨어나서 calc_global_load_trick() 함수를 실행 하면, 현재 Run Queue 에 있는 R 상태의 프로세스 개수와 D 상태의 프로세스 개수를 세어 calc_load_tasks 변수에 넣어준다.

Load Average 계산 과정 정리 하기


책에서 가이드하는 커널 버전과 달라 사용되는 함수 명칭이나 위치가 변경 되었는데, 사진에서 달라진 함수 명칭을 최신 소스 코드에서 최대한 찾아 반영하여 내용을 작성함

  • calc_load_account_active() -> calc_global_load_tick() 로 변경 되었으며 내부적으로 calc_load_fold_active() 함수를 호출함

결국 프로세스의 개수를 세어 Load Average 를 계산하는 과정을 거치고 있다.

CPU Bound vs I/O Bound


Load Average는 단순히 CPU를 사용하려는 프로세스(R 상태)가 많다는 것을 의미하는 것이 아니고 I/O 병목이 생겨서 I/O 작업을 대기하는 프로세스(D 상태)가 많을 수도 있다는 의미다.

즉, Load Average 값만 가지고 시스템이 현재 어떤 부하를 감당하고 있는지 모른다.

test = 0
while True:
    test += 1
while True:
    f = open("./io_test.txt", "w")
    f.write("TEST")
    f.close()

두 스크립트를 실행 하고 uptime 명령어를 통해 확인해보면 Load Average가 올라가는 것을 확인할 수 있다.

Load Average가 비슷한 수준으로 증가하지만, 사실 일으키고 있는 부하는 전혀 다른 부하이며 어떤 부하인지가 중요한 이유는 부하의 종류에 따라 해결 방법이 달라지기 때문이다.

즉, Load Average가 높다고 해서 단순히 CPU 사용량이 많다라고 판단할 수 없다.

"그렇다면 어떻게 부하의 원인을 확인할 수 있을까?"

vmstat

Load Average 값은 시스템 부하가 "있다" 라는 것을 알려주긴 하지만 구체적으로 어떤 부하인지 알 수 없었다.

이 정보를 보기 위해선 vmstat 을 활용할 수 있다.

man vmstat

  • FIELD DESCRIPTION FOR VM MODE r: The number of runnable processes (running or waiting for run time). b: The number of processes blocked waiting for I/O to complete.

vmstat 의 두 출력 값 사이의 차이점은 바로 첫 번째 컬럼인 r 열과 두 번째 열인 b 열의 값이다.

  • r: 실행되기를 기다리거나 현재 실행되고 있는 프로세스의 개수

  • b: I/O를 위해 대기열에 있는 프로세스의 개수

즉 각각이 위 Load Average 계산 과정에서 살펴보았던 커널 코드의 nr_running, nr_uninterruptible 변수인 셈이다.

두 스크립트를 돌려보면 비슷한 수준의 Load Average가 나오지만, vmstat 으로 확인해 보면 CPU가 일으키는 Load Average인지, 아니면 I/O가 일으키는 Load Average인지 확인해볼 수 있다.

Load Average가 시스템에 미치는 영향

"같은 수준의 Load Average 라면 시스템에 끼치는 영향도 같을까?"

위에서 CPU Bound, I/O Bound는 비슷한 수준의 Load Average 를 증가시킨다고 했었다. 그럼 이렇게 시스템에 입혀지는 부하는 같은 영향을 주고 있는지 궁금할 수 있다.

  • 하지만 언제나 그렇듯 "같을수도, 아닐수도 있다" 가 정답이다.

테스트 해보기

책에서는 nginx 와 java를 통해서 간단한 GET 요청을 처리할 수 있도록 세팅한 뒤 파이썬 스크립트를 이용하여 10개의 프로세스를 생성한다.

그리고 클라이언트의 역할을 하는 서버에서 siege 툴을 이용하여 응답 시간을 측정한다.

I/O 를 일으키는 파이썬 스크립트들은 D 상태에 빠져있는 것을 볼 수 있다.

하지만, CPU 기반의 부하일 때와는 다르게 파이썬 스크립트보다 nginx 와 java 의 CPU Usage 가 더 많다.

  • CPU에 대한 경합이 전자의 경우보다 덜하기 때문에 더 빠른 응답 속도를 보여줄 수 있다.

즉, 수행하고 있는 프로세스가 어떤 시스템 자원을 많이 쓰느냐에 따라 부하가 시스템에 미치는 영향이 다르다.

하지만, CPU 기반의 부하가 I/O 기반의 부하보다 응답 속도가 더 빠르다는 것은 꼭 아니란 것을 명심하자.

요약


  1. Load Average는 실행 중 혹은 실행 대기 중이거나 I/O 작업 등을 위해 대기 큐에 있는 프로세스들의 수를 기반으로 만들어진 값이다.

  2. Load Average 자체의 절대적인 높음과 낮음은 없으며 현재 시스템에 장착되어 있는 CPU 코어를 기반으로 한 상대적인 값으로 해석해야한다.

  3. 커널에도 버그가 있으며 Load Average 값을 절대 신뢰해서는 안된다.

  4. vmstat 툴 역시 시스템의 부하를 측정하는 데 사용될 수 있으며 r 과 b 열이 어떤 부하인지 정보를 제공한다. 특히 b 열의 경우 I/O 작업 등의 이유로 대기 상태에 있는 프로세스의 수이며 전체적인 시스템의 성능을 떨어뜨릴 수 있는 프로세스들이다. b 열의 수가 높다면 I/O 처리 과정에 문제가 있지는 않은지 살펴봐야 한다.

  5. Amazon Linux2 기준 /sys/kernel/debug/sched/debug 는 vmstat 툴을 통해 확인할 수 있는 것보다 더 자세한 정보를 제공해주며 특히 nr_running 과 runnable tasks 항목에서는 각 CPU에 할당된 프로세스 수와 프로세스의 PID 등의 정보를 확인할 수 있다.

참고 하기 좋은 자료


지금 진행하는 과정부터 실제 Tovalds 의 linux 소스코드를 분석한다. []

링크
github
github
github
https://brewagebear.github.io/linux-kernel-internal-1/
run i/o bound script, cpu bound script