ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 리눅스(Linux) 단일 호스트 부하 디버깅 (feat. 서버/인프라를 지탱하는 기술)
    SE General 2021. 5. 26. 23:19
    반응형

    이번 글에서는 보면서 '다시 한 번 곱씹어 내것으로 만들어야겠다'고 느낀 [1]의 '리눅스 단일 호스트 부하의 진상규명'을 내용요약을 중심으로 기술하였습니다. 부하 디버깅 관련 개념 및 방법실제로 리눅스의 어떤 명령어를 통해 디버깅이 가능한지를 다루며 아래와 같은 부분으로 이뤄져 있습니다:

     

    • 부하 디버깅의 관점과 프로세스
    • 부하란 무엇이며 어떻게 이루어져 있는가
    • 부하 디버깅 관련 커맨드: ps, sar, vmstat

    부하 디버깅의 관점과 프로세스

    부하를 이해한다는 것은 OS의 상태를 이해한다는 것입니다. 그렇기에 OS가 어떻게 동작하는지, 리눅스 커널의 소스를 살펴보며 부하, 성능과 연관된 요소들이 어떻게 측정되는지 살펴보며 부하를 정확히 이해할 수 있습니다.

     

    먼저 간단하게 표면적인 계측치를 먼저 살펴보면 아래와 같이 top 커맨드의 출력이 표시됩니다. CPU 사용률, 프로세스 아이디, 메모리 사용률, 프로세스 상태 등 다양한 값들이 존재합니다. 하지만 수많은 수치를 보며 원인을 진단하기는 어렵습니다.

     

    Processes: 357 total, 2 running, 355 sleeping, 1791 threads                                                                        07:42:31
    Load Avg: 1.50, 1.59, 1.25  CPU usage: 2.10% user, 1.77% sys, 96.11% idle  SharedLibs: 182M resident, 63M data, 40M linkedit.
    MemRegions: 58567 total, 3051M resident, 200M private, 9135M shared. PhysMem: 15G used (1873M wired), 536M unused.
    VM: 2041G vsize, 1370M framework vsize, 0(0) swapins, 0(0) swapouts. Networks: packets: 495901/669M in, 215805/21M out.
    Disks: 334702/6735M read, 77116/2335M written.
    
    PID   COMMAND      %CPU  TIME     #TH    #WQ  #PORT MEM    PURG   CMPR PGRP PPID STATE    BOOSTS          %CPU_ME %CPU_OTHRS UID  FAULTS
    3278  mdworker_sha 0.0   00:00.14 5      2    59    9528K  0B     0B   3278 1    sleeping *0[1]           0.00000 0.00000    501  10356
    3269  jamf         0.0   00:00.03 2      1    27    2048K  0B     0B   3269 1    sleeping *0[1]           0.00000 0.00000    0    4310
    3262  mdworker_sha 0.0   00:00.14 4      1    58    8624K  0B     0B   3262 1    sleeping *0[1]           0.00000 0.00000    501  8987

     

    그렇기에 좀 더 구체적인 프로세스를 확립하여 발생 시 어떤 부분을 봐야하는지 시나리오를 만들 수 있습니다.

     

    병목 규명작업의 기본적인 흐름

    부하는 대부분 처리의 모든 부분에서 고르게 발생하기 보다는, 처리의 특정 부분이 오래 걸리며 병목을 발생시키는 형태로 일어나게 됩니다. 그렇기에 아래와 같이 크게 2가지 순서로 병목을 진단할 수 있습니다:

     

    • 평균 부하[2]의 확인
    • CPU, I/O 중 병목 원인 조사

     

    평균 부하의 확인

    먼저 부하의 규명을 위해 top이나 uptime 등의 커맨드를 통해 평균 부하를 확인합니다. 평균 부하는 시스템 전체의 현황을 보여주나, 이것만으로 병목의 원인을 판단하기는 쉽지 않습니다. 그렇기에 큰 그림에서의 원인점을 평균 부하를 통해 찾고, 2번째 'CPU, I/O 중 병목 원인 조사'를 통해 세부적인 요소를 탐색해 나갈 수 있습니다.

     

    평균 부하는 낮은데 시스템 성능이 예상보다 느리다면, 소프트웨어 설정이나 오류, 네트워크 등의 원인을 확인해볼 필요가 있습니다. 

     

    CPU, I/O 중 병목 원인 조사

    평균 부하가 높다면 이어서 원인이 CPU에 있는지 아니면 I/O에 있는지 살펴봅니다. 그러한 부분은 sar, vmstat과 같은 커맨드를 통해 시간이 경과함에 따라서 CPU 사용률의 변화나 I/O 대기율의 추이를 확인하고 2가지 원인에 따라 다른 단계로 이어나갈 수 있습니다:

     

    CPU가 병목인 경우

     

    CPU가 병목인 경우에 다음과 같은 순서로 조사해나갑니다:

     

    1. 사용자 프로그램의 처리가 병목인지, 시스템 프로그램이 원인인지 확인 (top, sar)

    2. 동시에 ps로 프로세스의 상태나 CPU 사용시간 등을 확인하여 원인 프로세스를 탐색함

    3. 프로세스를 찾은 후에,  strace나 oprofile을 통해 원인점의 범위를 좁혀나감

     

    일반적으로 CPU가 병목이 되는 원인은 아래 2가지 상황에 속합니다:

     

    • CPU 자연 단일 병목: 처리량이 늘어 CPU에 부하가 걸리고, 디스크나 메모리 용량 등 CPU 외적인 부분에서는 병목이 발생되고 있지 않은 상태. 증설이나 튜닝, 최적화를 통해 해결 가능.
    • 버그: 프로그램의 오류 등으로 CPU에 필요이상의 부하로 병목이 발생. 원인을 찾아 제거 및 교정함.

     

    I/O가 병목인 경우

     

    I/O가 병목인 경우에는 대부분 아래 2가지 상황에 속하며 그러한 상황에 따라 여러 대응책을 고민해볼 수 있습니다:

     

    • 프로그램의 입출력이 많아 높은 부하 발생: 캐시에 필요한 메모리가 부족한 경우 등이 속함. 서버가 저장하고 있는 데이터의 용량과 증설 가능한 메모리량을 비교해서 1) 프로그램 개선, 2) 캐시 확대가 가능한 경우 메모리 증설로 확대, 3) 1, 2가 불가능할 경우, 데이터 분산이나 캐시서버 도입 검토

     

    • 스왑이 발생하여 디스크 액세스가 발생: ps로 원인 프로세스의 규명, 프로그램 버그인지 확인 및 수정, 메모리 증설 또는 데이터 분산을 검토

     

     

    부하란 무엇이며 어떻게 이루어져 있는가

    부하는 앞서 살펴본 것과 같이 크게 1) CPU 부하, 2) I/O 부하로 나누어 집니다. 일반적으로 App서버는 DB로부터 얻은 데이터를 가공해 클라이언트로 전달하는 처리를 수행하기에 I/O는 드물어 CPU 바운드한 서버의 특징을 가집니다. 반대로 DB는 디스크에 존재하는 데이터를 검색하고 처리하는 부분을 담당하기에 CPU 부하보다는 I/O 바운드한 서버입니다. 

     

    현대의 서버 내부에서 task는 멀티태스킹 운영체제에서 실행되게 됩니다. 여러 (커널 내부의 실행단위인) task를 실행하지만 그러한 task들은 유한한 CPU, 디스크와 같은 하드웨어 리소스를 공유하며 사용하게 됩니다. 

    Multi-Tasking - Image from Author inspired by [1]

    OS는 위와 같이 짧은 간격으로 여러 task를 전환(switch)해가면서 처리하게 됩니다. 실행할 task가 적다면 대기가 없이 바로 전환할 수 있지만, task 수가 증가하면서 A를 수행하는 동안 B와 같은 task는 CPU 자원이 사용가능하게 될 때까지 대기하게 됩니다. 이러한 '처리를 수행하려해도 대기한다'라는 대기상태는 프로그램의 실행 지연으로 나타나게 됩니다. 

     

    Processes: 385 total, 2 running, 383 sleeping, 1829 threads                                                                                                                                                                                  22:11:27
    Load Avg: 2.08, 1.80, 1.67

     

    위와 같이 top 커맨드 실행 시, 2번째 줄에 보이는 평균 부하는 단위시간 당 대기된 task의 수를 보여줍니다. 그렇기에 많은 지연이 발생하였다는 것은 그만큼 부하가 높은 상황이라고 할 수 있습니다. 현재 부하의 정도를 파악할 수는 있으나 CPU 병목인지 I/O 병목인지, 어떤 프로세스가 원인이되는지 등을 파악하기 위해서는 추가적인 조사가 필요하게 됩니다.

     

    부하는 결국 여러 task들이 하드웨어 리소스를 위해 경쟁하면서 발생하는 대기시간입니다. 그 부분을 이해하기 위해서는 task는 언제 대기상태가 되는지를 관장하는 운영체제에 대한 이해가 필요합니다.

     

    Task의 대기를 제어하는 것은 리눅스 커널 내에서도 '프로세스 스케쥴러'라는 프로그램이 담당합니다. 프로세스 스케쥴러는 멀티태스킹 제어에 있어서 task들 간의 우선순위를 정하고 그러한 순위에 따라 하드웨어 리소스를 할당합니다. 그렇기에 프로세스 스케쥴러와 프로세스 상태를 자세히 살펴보며, 위의 top 커맨드에서 살펴본 평균 부하가 어떻게 계산되는지 파악함으로써 최종적으로 부하를 더 깊이 이해할 수 있습니다. 

     

    프로세스 스케쥴링과 프로세스, 그리고 부하

    프로세스는 프로그램이 운영체제에 의해 실행되고 있을 때 실행단위가 되는 개념입니다. 그러한 프로세스는 '프로그램의 명령'과 '실행에 필요한 정보'가 포함된 객체인데요. 

     

    리눅스 커널은 프로세스마다 '프로세스 디스크립터'라는 관리용 테이블을 생성하여, 각종 실행 시의 정보를 보관하게 합니다. 리눅스 커널의 프로세스 스케쥴러는 이 '프로세스 디스크립터'들을 우선순위로 나열하여 실행되도록 스케쥴링합니다. 동시에 스케쥴러는 프로세스를 프로세스 디스크립터의 상태값(CPU 할당 대기, I/O 완료 대기 등)에 따라 나누어 관리하고 필요에 따라 상태를 변경하여 실행순서를 제어합니다. 

     

    상태 설명
    TASK_RUNNING 실행가능 상태. CPU 리소스가 확보되면 언제든지 실행이 가능한 상태
    TASK_INTERRUPTIBLE 중단 가능한 대기상태. 주로 복귀시간이 예측 불가능한 장시간의 대기상태. sleep이나 사용자로부터 입력 대기 등
    TASK_UNINTERRUPTIBLE 중단 불가능한 대기상태. 주로 단시간에 복귀할 경우의 대기상태. 디스크 입출력 대기 
    TASK_STOPPED 중지 시그널을 받아서 실행 중단된 상태. 재개될 때까지 스케쥴링되지 않음
    TASK_ZOMBIE 좀비 상태. 자식 프로세스가 exit해서 부모 프로세스로 반환될 때까지의 상태

     

    위와 같은 프로세스의 상태는 아래와 같은 라이프사이클을 가집니다:

     

    Process Lifecycle - Image from Author inspired by [1]

    프로세스는 생성되며 TASK_RUNNING 상태를 가지며, 그러한 상태에서 실제로 CPU의 리소스를 할당받아 실행되기도 리소스를 할당받지 않고 받게되면 바로 실행될 수 있는 TASK_RUNNING-WAIT으로 존재하기도 합니다. 

     

    사용자의 입력, 디스크 I/O 등의 CPU 이외의 처리가 필요한 경우에는 TASK_INTERRUPTIBLE, TASK_UNINTERRUPTIBLE의 상태로그러한 처리를 진행하게 됩니다. 

     

    앞에서 살펴본 평균 부하 계산 시에는 TASK_RUNNING-WAIT과 TASK_UNINTERRUPTIBLE이 포함됩니다. 그렇기에 구체적으로 말하면 1) CPU를 사용하고자 하여도 대기하는 프로세스, 2) 계속해서 처리하고자 하여도 디스크 입출력의 종료를 대기하는 프로세스평균 부하로 나타납니다. 

     

    하드웨어는 일정 주기로 CPU에 '타이머 인터럽트'라는 중단 신호를 보냅니다 [3]. 이때마다 CPU는 실행 중인 프로세스가  CPU를 얼마나 사용했는지 계산하는 등의 시간과 관련된 처리를 수행합니다. 이 타이머 인터럽트마다 실행 가능 상태인 task과 I/O 대기 task의 개수를 세고 단위시간으로 나누어 평균 부하를 계산합니다.

     

     

    Linux v5.13-rc3의 loadavg.c 코드에서는 아래와 같은 calc_global_load를 실행하여 평균 부하를 업데이트하는 것을 확인할 수 있습니다.

     

    /*
     * calc_load - update the avenrun load estimates 10 ticks after the
     * CPUs have updated calc_load_tasks.
     *
     * Called from the global timer code.
     */
    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();
    }

     

    active의 계산은 calc_load_fold_active에서 확인할 수 있습니다:

     

    long calc_load_fold_active(struct rq *this_rq, long adjust)
    {
    	long nr_active, delta = 0;
    
    	nr_active = this_rq->nr_running - adjust;
    	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;
    	}
    
    	return delta;
    }

    위의 코드에서 nr_running과 nr_uninterruptible이 더해져 delta가 계산되는 것을 볼 수 있습니다.

     

     

    CPU 사용률과 I/O 대기율

    이전 섹션에서 부하란 무엇인지 프로세스 레벨까지 살펴보며 자세히 알아보았습니다. 디버깅 시 그러한 평균 부하가 존재한다는 사실을 인지하고 나서는 원인이 CPU인지 I/O인지 조사를 진행하여야 합니다.

     

    sar(System Activity Reporter)는 시스템 상황 레포트를 확인하기 위한 커맨드로 시간 경과에 따른 부하를 잘 보여줍니다:

     

    Image from [6]

     

    또는 그러한 sar 데이터를 ksar [5]와 같은 도구를 통해 아래와 같이 그래프로 살펴볼 수 있습니다:

    ksar - Image from Author

     

    위에서 sar을 통해서 user 또는 system 프로세스의 CPU 사용률, I/O 대기율 등을 각 코어별로 또는 합쳐서 탐색할 수 있습니다. 그렇기에 부하의 원인이 CPU 병목인지 I/O 병목인지 파악할 수 있게 됩니다. 

     

    CPU 사용률의 계산

    평균 부하와 같이 CPU 사용률의 계산을 커널 코드를 확인하며 구체적으로 살펴보겠습니다. CPU 사용률은 역시 타이머 인터럽트 시에 계산되게 됩니다. 평균 부하가 전역적인 수치인 반면 CPU 사용률은 각 코어별로 기록하게 됩니다.

     

    커널은 프로세스 전환을 위해 각 프로세스가 생성된 후부터 얼마나 CPU 시간을 이용했는지 프로세스별로 기록합니다. 이것이 바로 '프로세스 어카운팅'에 해당됩니다. 이러한 기록을 기반으로 스케쥴러는 우선순위를 조정하게 되는데요. 개별 코어에서 수행된 여러 프로세스들의 이 프로세스 어카운팅 값을 더하면 각 코어의 CPU 사용률을 산출할 수 있습니다. 

     

     kernel_stat.h에 정의된 cpu_usage_stat을 살펴보면:

    enum cpu_usage_stat {
    	CPUTIME_USER,
    	CPUTIME_NICE,
    	CPUTIME_SYSTEM,
    	CPUTIME_SOFTIRQ,
    	CPUTIME_IRQ,
    	CPUTIME_IDLE,
    	CPUTIME_IOWAIT,
    	CPUTIME_STEAL,
    	CPUTIME_GUEST,
    	CPUTIME_GUEST_NICE,
    	NR_STATS,
    };
    
    struct kernel_cpustat {
    	u64 cpustat[NR_STATS];
    };
    
    struct kernel_stat {
    	unsigned long irqs_sum;
    	unsigned int softirqs[NR_SOFTIRQS];
    };

     

    타이머 인터럽트마다의 사용률 계산은 update_process_times의 실행을 따라가다보면 나오는 irqtime_account_process_tick에서 조건에 따라 사용자 또는 시스템 등의 사용률로 계산되는 것을 확인할 수 있습니다:

    static void irqtime_account_process_tick(struct task_struct *p, int user_tick,
    					 int ticks)
    {
    	u64 other, cputime = TICK_NSEC * ticks;
    
    	/*
    	 * When returning from idle, many ticks can get accounted at
    	 * once, including some ticks of steal, irq, and softirq time.
    	 * Subtract those ticks from the amount of time accounted to
    	 * idle, or potentially user or system time. Due to rounding,
    	 * other time can exceed ticks occasionally.
    	 */
    	other = account_other_time(ULONG_MAX);
    	if (other >= cputime)
    		return;
    
    	cputime -= other;
    
    	if (this_cpu_ksoftirqd() == p) {
    		/*
    		 * ksoftirqd time do not get accounted in cpu_softirq_time.
    		 * So, we have to handle it separately here.
    		 * Also, p->stime needs to be updated for ksoftirqd.
    		 */
    		account_system_index_time(p, cputime, CPUTIME_SOFTIRQ);
    	} else if (user_tick) {
    		account_user_time(p, cputime);
    	} else if (p == this_rq()->idle) {
    		account_idle_time(cputime);
    	} else if (p->flags & PF_VCPU) { /* System time or guest time */
    		account_guest_time(p, cputime);
    	} else {
    		account_system_index_time(p, cputime, CPUTIME_SYSTEM);
    	}
    }

     

    account_user_time의 경우 프로세스 디스크립터인 p의 utime 및 cpustat을 변경해줍니다. 

    void account_user_time(struct task_struct *p, u64 cputime)
    {
    	int index;
    
    	/* Add user time to process. */
    	p->utime += cputime;
    	account_group_user_time(p, cputime);
    
    	index = (task_nice(p) > 0) ? CPUTIME_NICE : CPUTIME_USER;
    
    	/* Add user time to cpustat. */
    	task_group_account_field(p, index, cputime);
    
    	/* Account for user time used */
    	acct_account_cputime(p);
    }

     

    부하 디버깅 관련 커맨드: ps, sar, vmstat

    이번 장에서는 부하 디버깅 관련 커맨드인 ps, sar, vmstat을 간략하게 알아보겠습니다.

     

    먼저 ps 커맨드는 프로세스가 지닌 정보를 출력하며 주로 프로세스 디스크립터에 있는 정보에 접근해 보기 좋게 뿌려줍니다. 구체적으로 CPU 사용률, 물리 메모리 사용률, VSZ 및 RSS 크기, 프로세스 상태(STAT), CPU 사용시간(TIME) 등을 제공합니다.

     

    sar 커맨드는 앞서 살펴본 것과 같이 운영체제의 각종 지표를 참조해 전달합니다. 백데이터는 sadc라는 백그라운드 실행 프로그램이 모으고 저장하게 됩니다. 시간 경과에 따른 변화량 등을 잘 보여주기에 과거의 데이터를 확인하는 등에 매우 유용합니다.

     

    마지막으로 vmstat(Virtual Memory Statistics) 커맨드는 가상 메모리 관련 정보를 참조할 수 있는 도구입니다. 특히 bi(블록 디바이스에서 수신한 블록), bo(블록 디바이스로 송신한 블록)은 I/O와 연관된 수치를 보여주기에 위에서 I/O 병목으로 판단되었을 때 더욱 구체적인 사실을 파악할 수 있게 해줍니다. 

     

    Reference

    [1] 서버/인프라를 지탱하는 기술

    [2] https://en.wikipedia.org/wiki/Load_(computing) 

    [3] https://en.wikibooks.org/wiki/Operating_System_Design/Processes/Interrupt

    [4] https://stackoverflow.com/a/42417247/8854614

    [5] https://github.com/vlsi/ksar

    [6] https://www.linuxtechi.com/generate-cpu-memory-io-report-sar-command/

     

     

    반응형
Kaden Sungbin Cho