안동민 개발노트

본문 시작

시스템 모니터링과 디버깅

strace·proc·dmesg·journalctl·코어 덤프·perf로 시스템 콜과 자원 사용을 추적해 장애 원인을 좁힙니다.

프로그램이 이상하게 동작한다고 할 때, 로그만으로 원인을 찾지 못한다면 OS 수준의 디버깅 도구가 필요합니다.

프로세스가 어떤 시스템 콜을 호출하는지, 커널이 무슨 메시지를 출력하는지, 메모리와 CPU를 어떻게 사용하는지를 추적하면 대부분의 문제를 진단할 수 있습니다.


strace: 시스템 콜 추적

strace는 추적 대상이 커널에 요청하는 시스템 콜과 시그널을 보여줍니다. 권한과 필터의 영향을 받으며 새 자식·스레드까지 추적하려면 -f 같은 옵션을 사용합니다.

"프로그램이 내부에서 무엇을 하고 있는지"를 들여다보는 도구입니다.

strace 기본 사용
# 프로그램의 모든 시스템 콜 추적
strace ./myprogram

# 실행 중인 프로세스에 붙이기
strace -p 1234

# 파일 관련 시스템 콜만 추적
strace -e trace=file ./myprogram

# 네트워크 관련 시스템 콜만 추적
strace -e trace=network ./myprogram

# 시스템 콜별 커널 CPU 시간 통계 (기본값)
strace -c ./myprogram

# 타임스탬프와 함께 출력
strace -t ./myprogram

# 멀티스레드 프로그램 추적 (모든 스레드)
strace -f ./myprogram

프로그램이 특정 파일을 찾지 못해 실패한다면, strace -e trace=file로 어떤 경로를 탐색하는지 확인할 수 있습니다.

strace 출력 읽기

strace 출력 예시
# open("/etc/config.json", O_RDONLY) = -1 ENOENT (No such file or directory)
# → 파일이 존재하지 않음. 경로 확인 필요

# connect(3, {sa_family=AF_INET, sin_port=htons(3306), sin_addr=inet_addr("10.0.0.5")}, 16) = -1 ETIMEDOUT
# → 해당 주소·포트 연결 타임아웃. 네트워크·방화벽·서버 상태 등을 추가 확인

# write(1, "Hello\n", 6) = 6
# → stdout에 6바이트 성공적으로 기록

# mmap(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7f...
# → 익명 메모리 매핑 (malloc 구현에서도 사용할 수 있음)

strace -c는 기본적으로 시스템 콜의 커널 CPU 시간·호출 수·오류 수를 요약합니다. 다음은 출력 형식을 설명하기 위한 예시이며 이 교재에서 실행한 측정 결과가 아닙니다.

% time     seconds  usecs/call     calls    errors syscall
------ ----------- ----------- --------- --------- --------
 65.21    0.123456          12     10238           read
 20.15    0.038123           8      4762           write
  8.33    0.015789         157       100        10 open
  ...

위 65%는 집계한 시스템 콜 시간 중 read의 비중입니다. 애플리케이션 전체 실행 시간이나 I/O 대기 비율이 아니므로 이것만으로 I/O 병목을 확정하지 않습니다. 경과 시간 요약이 필요하면 지원하는 버전에서 -w를 함께 사용하고 추적 오버헤드도 고려합니다.


/proc 파일 시스템

/proc는 커널이 제공하는 가상 파일 시스템입니다.

실제 파일이 아니라 커널이 실시간으로 생성하는 정보입니다.

/proc 시스템 정보
# CPU 정보 (노출된 논리 CPU 수, 모델)
cat /proc/cpuinfo | grep "model name" | head -1
cat /proc/cpuinfo | grep processor | wc -l  # 노출된 논리 CPU 항목 수

# 메모리 정보
cat /proc/meminfo | head -10

# 시스템 부하 (1분, 5분, 15분 평균)
cat /proc/loadavg
# 0.75 0.62 0.58 2/456 12345
# → 실행 가능 태스크와 중단 불가 대기의 평균 수. CPU 사용률과 다름

# 커널 버전
cat /proc/version

# 부팅 후 경과 시간
cat /proc/uptime

프로세스별 /proc 정보

/proc/[pid] 활용
PID=1234

# 프로세스 상태 요약
cat /proc/$PID/status
# Name, State, Pid, PPid, Threads, VmSize, VmRSS 등

# 메모리 맵 (코드, 힙, 스택, 라이브러리 주소)
cat /proc/$PID/maps | head -20
# 7f1234560000-7f1234580000 r-xp ... /lib/libc.so.6
# → r-xp = 읽기/실행 가능, 쓰기 불가

# 열린 파일 디스크립터 목록
ls -la /proc/$PID/fd/
# 0 → stdin, 1 → stdout, 2 → stderr
# 3 → socket:[12345] (네트워크 연결)
# 4 → /var/log/app.log (로그 파일)

# 실행 명령어
cat /proc/$PID/cmdline | tr '\0' ' '

# 환경 변수
cat /proc/$PID/environ | tr '\0' '\n'

# I/O 통계
cat /proc/$PID/io
# read_bytes, write_bytes → 실제 디스크 I/O

/proc/[pid]/maps는 프로세스의 메모리 레이아웃을 보여줍니다.

코드 세그먼트, 힙, 스택, 공유 라이브러리의 가상 주소 범위와 권한을 확인할 수 있습니다.

proc_monitor.py
import os

def process_info(pid):
    """특정 프로세스의 상태를 /proc에서 읽기"""
    proc_dir = f"/proc/{pid}"

    if not os.path.exists(proc_dir):
        print(f"PID {pid} 프로세스가 존재하지 않습니다.")
        return

    # 상태 정보 파싱
    with open(f"{proc_dir}/status") as f:
        status = {}
        for line in f:
            parts = line.strip().split(":\t")
            if len(parts) == 2:
                status[parts[0]] = parts[1].strip()

    print(f"이름: {status.get('Name', 'N/A')}")
    print(f"상태: {status.get('State', 'N/A')}")
    print(f"스레드 수: {status.get('Threads', 'N/A')}")
    print(f"가상 메모리: {status.get('VmSize', 'N/A')}")
    print(f"물리 메모리: {status.get('VmRSS', 'N/A')}")

    # fd 수 확인
    fd_dir = f"{proc_dir}/fd"
    try:
        fd_count = len(os.listdir(fd_dir))
        print(f"열린 fd 수: {fd_count}")
    except PermissionError:
        print("fd 확인 권한 없음 (소유자·접근 정책 확인)")

process_info(1)

이 코드는 /proc의 여러 파일을 순서대로 읽으므로 하나의 원자적 스냅샷이 아닙니다. 확인 도중 프로세스가 종료되거나 PID가 재사용될 수 있으며, 예제는 이러한 경합과 모든 읽기 오류를 처리하지 않습니다.


dmesg와 시스템 로그

dmesg는 커널이 출력하는 메시지를 보여줍니다.

하드웨어 오류, 드라이버 문제, OOM Killer 동작 등 커널 수준의 이벤트가 기록됩니다.

커널 로그
# 커널 메시지 (에러와 경고만)
dmesg --level=err,warn | tail -20

# OOM Killer가 동작했는지 확인
dmesg | grep -i "oom"
# → "Out of memory: Kill process 1234 (java) score 950 or sacrifice child"
# → 종료 대상 선정에는 점수뿐 아니라 OOM 범위와 제외 조건 등이 적용됨

# 디스크 에러 확인
dmesg | grep -iE "error|fail|I/O"

# 실시간 커널 메시지 모니터링
dmesg -w

# 사람이 읽기 쉬운 타임스탬프
dmesg -T | tail -20

journalctl (systemd 로그)

journalctl 활용
# 최근 부팅 이후 로그
journalctl -b

# 특정 서비스의 로그
journalctl -u nginx.service --since "2 hours ago"

# 실시간 로그 추적
journalctl -f

# 에러 이상 심각도만
journalctl -p err

# 특정 시간 범위
journalctl --since "2024-01-15 10:00" --until "2024-01-15 11:00"

# 디스크 사용량 확인 및 정리
journalctl --disk-usage
journalctl --vacuum-size=500M  # 보관된 저널을 정리. 활성 파일은 제외

전통적 로그 파일

log_files.sh
# 시스템 로그
/var/log/syslog          # Ubuntu/Debian
/var/log/messages        # CentOS/RHEL

# 인증 로그 (SSH 로그인 시도 등)
/var/log/auth.log        # Ubuntu
/var/log/secure          # CentOS

# 커널 로그
/var/log/kern.log

# 실패한 로그인 시도 확인 (보안)
grep "Failed password" /var/log/auth.log | tail -20

# 특정 IP의 SSH 로그인 시도 횟수
grep "Failed password" /var/log/auth.log | awk '{print $11}' | sort | uniq -c | sort -rn | head -10

같은 시간대의 애플리케이션 로그, 서비스 재시작, 커널 이벤트를 대조합니다. 위 awk의 11번째 필드는 특정 SSH 로그 형식만 전제하므로 invalid user 등 메시지 형태가 달라지면 원본 줄에서 IP 위치를 다시 확인해야 합니다.


코어 덤프 분석

프로세스가 세그먼테이션 폴트로 비정상 종료되면 코어 덤프(Core Dump)가 생성될 수 있습니다.

코어 덤프는 프로세스가 죽는 순간의 메모리 스냅샷입니다.

코어 덤프 설정
# 코어 덤프 활성화
ulimit -c unlimited

# 코어 덤프 저장 경로 설정
echo "/var/crash/core.%e.%p.%t" > /proc/sys/kernel/core_pattern
# %e = 실행 파일 이름
# %p = PID
# %t = 타임스탬프
GDB 코어 덤프 분석
# 코어 덤프 분석 (gdb 사용)
gdb ./myprogram /var/crash/core.myprogram.1234.1705312800

# gdb 프롬프트에서:
(gdb) bt           # 콜 스택 (backtrace) 확인
#0  0x00007f... in strlen () from /lib/libc.so.6
#1  0x00401234 in process_request (req=0x0) at server.c:142
#2  0x004015ab in handle_client (fd=5) at server.c:89
#3  0x00401800 in main () at server.c:42

(gdb) frame 1      # 문제 프레임으로 이동
(gdb) list         # 소스 코드 확인
(gdb) print req    # 변수 값 확인 → 0x0 (NULL!)
(gdb) info locals  # 모든 로컬 변수 출력

위 예시의 req=0x0은 NULL 사용을 의심할 단서입니다. 실제로 실패한 명령, strlen에 전달한 값, 소스와 디버그 정보까지 대조해 원인을 확인합니다. 코어 덤프는 크기 제한·저장 경로 권한·core_pattern 처리 방식에 따라 생성되지 않을 수도 있습니다.

운영 환경에서는 코어 덤프 크기를 제한하거나 별도의 디렉토리에 저장하도록 설정합니다.

코어 덤프에 민감한 데이터(비밀번호, 토큰)가 포함될 수 있으므로 접근 권한 관리에도 주의합니다.


perf: 성능 프로파일링

perf는 리눅스 커널의 성능 분석 도구입니다.

선택한 하드웨어·소프트웨어 이벤트를 세거나 샘플링해 비용이 집중되는 구간을 찾습니다.

perf 사용
# CPU 프로파일링 (30초간 샘플링)
perf record -g -p 1234 -- sleep 30

# 결과 확인
perf report

# 시스템 전체 통계 (5초간)
perf stat -a sleep 5
# → cache-misses, context-switches, page-faults 등

# 특정 이벤트 카운트
perf stat -e cache-misses,cache-references ./myprogram
# perf report 출력 예시
  45.23%  myapp  myapp       [.] hash_lookup
  22.11%  myapp  myapp       [.] parse_json
  15.67%  myapp  libc.so.6   [.] malloc
   8.44%  myapp  myapp       [.] validate_input

예시의 hash_lookup은 조사할 후보입니다. 어떤 이벤트를 기록했고 자체 비용인지 하위 호출을 포함한 비용인지 확인한 뒤 최적화를 검토합니다.

flame_graph.sh
# 플레임 그래프 생성 (시각화)
perf record -g -p 1234 -- sleep 30
perf script | stackcollapse-perf.pl | flamegraph.pl > flamegraph.svg

위 변환에는 별도로 설치한 FlameGraph의 스크립트가 필요합니다. 플레임 그래프는 수집한 스택을 합쳐 보여주며, 가로축은 시간 순서가 아닙니다.

성능 출력에서 비율의 분모 읽기

같은 퍼센트라도 도구와 기록 방식에 따라 뜻이 다릅니다.

성능 출력에서 비율의 분모 읽기
출력집계하는 대상여기서 바로 알 수 없는 것
strace -c추적한 시스템 콜의 커널 CPU 시간 비중전체 실행 시간, 대기 시간, I/O 병목 확정
perf report선택한 이벤트의 샘플 비용 분포모든 이벤트에 공통인 CPU·벽시계 시간 비율
CPU 플레임 그래프샘플에 나타난 스택의 누적 폭실행 순서나 각 호출의 개별 소요 시간
strace -c
집계하는 대상: 추적한 시스템 콜의 커널 CPU 시간 비중
여기서 바로 알 수 없는 것: 전체 실행 시간, 대기 시간, I/O 병목 확정
perf report
집계하는 대상: 선택한 이벤트의 샘플 비용 분포
여기서 바로 알 수 없는 것: 모든 이벤트에 공통인 CPU·벽시계 시간 비율
CPU 플레임 그래프
집계하는 대상: 샘플에 나타난 스택의 누적 폭
여기서 바로 알 수 없는 것: 실행 순서나 각 호출의 개별 소요 시간

프로세스·기간·이벤트를 맞춰 비교합니다. /proc, 로그와 샘플은 서로 보완하는 증거이며 한 지표만으로 병목을 확정하지 않습니다.

다음 절에서는 커널 파라미터 튜닝과 OS 관련 기술 면접 주제를 정리하겠습니다.