포스트

주니어 백엔드 개발자가 반드시 알아야 할 실무 지식 - 9장 최소한 알고 있어야 할 서버 지식

애플리케이션 코드만 안다고 서비스가 운영되지는 않는다.
백엔드 개발자는 최소한 서버가 어디서 돌고 어떤 자원을 쓰는지 정도는 이해해야 한다.


알아야 할 기본 요소

  • 프로세스와 스레드
  • 메모리 구조
  • 파일 디스크립터
  • 디스크와 로그
  • 컨테이너/VM
  • OS 레벨 제한

각 항목을 한 줄씩 풀면 다음과 같다.

  • 프로세스는 실행 중인 프로그램이고, 자기만의 메모리 공간을 가진다. 스레드는 한 프로세스 안에서 메모리를 공유하며 실행되는 흐름이다. Java 애플리케이션 하나는 보통 JVM 프로세스 하나와 그 안의 많은 스레드로 이루어진다(Process and Thread).
  • 메모리 구조는 JVM이 관리하는 힙과, 그 바깥에서 쓰는 메모리(스레드 스택, 메타스페이스, 다이렉트 버퍼 등)를 구분하는 데서 시작한다. OS가 보는 프로세스 메모리(RSS)는 힙 크기보다 크다(다이렉트 메모리와 Cleaner).
  • 파일 디스크립터(FD)는 프로세스가 연 파일을 가리키는 번호다. 일반 파일뿐 아니라 소켓도 FD로 다룬다. socket(2)은 성공하면 새 소켓의 FD를 돌려준다(socket(2)).
  • 컨테이너는 같은 커널 위에서 프로세스를 격리하고 자원을 제한하는 방식이고, VM은 하드웨어를 가상화해 별도의 커널을 띄우는 방식이다. 컨테이너의 자원 제한은 리눅스 cgroup이 맡는다.

왜 필요한가

운영 문제는 종종 코드 바깥에서 시작한다.

  • OOM Kill
  • FD 고갈
  • 디스크 꽉 참
  • CPU throttling
  • swap 문제

이런 상황은 코드만 봐서는 설명이 안 된다.

각 증상이 무엇인지 알아야 로그와 지표에서 알아볼 수 있다.

  • OOM Kill: 메모리가 한도에 닿았는데 회수할 수 없으면 커널의 OOM killer가 프로세스를 강제로 종료한다. cgroup v2 문서는 memory.max에 닿은 뒤 회수로도 충분히 확보하지 못하면 그 cgroup 안에서 OOM killer가 호출된다고 설명한다(Linux kernel, Control Group v2). 이때 Java 쪽에는 OutOfMemoryError 같은 흔적이 남지 않고 프로세스가 사라지기만 한다. 힙이 넉넉해 보여도 힙 바깥 메모리까지 더한 RSS가 컨테이너 한도를 넘으면 일어날 수 있다.
  • FD 고갈: 프로세스가 열 수 있는 FD 수에는 상한(RLIMIT_NOFILE)이 있고, 넘기면 open(), pipe(), dup() 같은 호출이 EMFILE 오류로 실패한다(getrlimit(2)). 이 오류의 메시지가 로그에서 자주 보는 “Too many open files”다(errno(3)). 소켓도 FD이므로, 커넥션을 닫지 않는 코드나 갑자기 늘어난 동시 연결이 원인이 된다.
  • 디스크 꽉 참: 로그 파일이 계속 자라 디스크를 채우면 로그뿐 아니라 임시 파일을 쓰는 다른 기능까지 실패한다.
  • CPU throttling: 컨테이너에 CPU 한도를 걸면, 정해진 주기 안에 할당량을 다 쓴 프로세스는 다음 주기까지 실행되지 못한다. 평균 CPU 사용률은 낮은데 응답 시간이 튀는 현상이 여기서 나온다.
  • swap 문제: 물리 메모리가 부족하면 OS가 메모리 페이지를 디스크로 내보낸다. 내보낸 페이지에 다시 접근할 때 디스크 I/O가 끼어들어, 프로세스 전체가 눈에 띄게 느려진다.

이런 멈춤과 지연을 프로세스 입장에서 정리한 글은 프로세스는 왜 멈추는가다.


실무에서 특히 중요한 것

1. 로그

로그는 남기기만 하면 끝이 아니라:

  • 어디에 저장되는지
  • 얼마나 쌓이는지
  • rotation이 되는지

를 알아야 한다.

rotation은 로그 파일이 일정 크기나 기간을 넘으면 새 파일로 바꾸고 오래된 파일을 압축하거나 지우는 작업이다. 리눅스에서 흔히 쓰는 logrotate는 주기(daily)나 크기(size) 기준으로 파일을 돌리고, rotate N으로 남길 개수를 정하며, compress로 이전 파일을 압축한다(logrotate(8)).

돌리는 방식에 따라 주의할 점이 다르다. create는 기존 파일을 옮기고 같은 이름으로 새 파일을 만든다. 이때 애플리케이션이 옛 파일을 계속 열고 있으면 로그가 옮겨진 파일에 계속 쓰이므로, postrotate에서 애플리케이션에 파일을 다시 열라고 신호를 보내야 한다. copytruncate는 파일을 복사한 뒤 원본을 0바이트로 자르는 방식이라 애플리케이션 쪽 조치가 필요 없지만, man 페이지는 복사와 자르기 사이의 짧은 틈에 일부 로그가 유실될 수 있다고 경고한다.

로그 파일을 손으로 지웠는데 디스크 사용량이 줄지 않는 경우도 같은 원리다. 이름이 지워져도 그 파일을 연 프로세스가 남아 있으면, 마지막 FD가 닫힐 때까지 파일은 존재한다(unlink(2)). lsof로 보면 이런 파일 경로 뒤에 (deleted)가 붙는다.

2. 자원 제한

  • container memory limit
  • CPU quota
  • open files
  • ulimit

같은 제한은 서비스 동작에 직접적인 영향을 준다.

제한마다 넘었을 때의 반응이 다르다.

제한넘었을 때확인 위치
컨테이너 메모리 (memory.max)회수 실패 시 OOM killcgroup의 memory.events, 커널 로그
CPU 할당량 (cpu.max)다음 주기까지 실행 중단(throttling)cgroup의 cpu.stat
열린 파일 수 (RLIMIT_NOFILE)새 FD 생성이 EMFILE로 실패ulimit -n, /proc/<pid>/limits

CPU 할당량은 “주기마다 쓸 수 있는 시간”으로 정해진다. cpu.max의 형식은 $MAX $PERIOD이고 기본값은 max 100000, 즉 제한 없음과 100ms 주기다. CFS bandwidth control 문서는 할당량을 다 쓴 스레드는 할당량이 다시 채워지는 다음 주기까지 실행되지 못한다고 설명하고, 이 현상을 nr_throttled(스로틀된 횟수)와 throttled_time(스로틀된 총 시간) 통계로 보여 준다(CFS Bandwidth Control). cgroup v2에서는 cpu.stat의 nr_throttled와 throttled_usec가 같은 정보를 담는다. 응답 지연이 주기적으로 튄다면 이 두 값부터 확인할 만하다. 스케줄러와 함께 본 자세한 내용은 컨텍스트 스위칭과 스케줄링에 있다.

ulimit은 셸에서 이런 프로세스 단위 제한을 보고 바꾸는 명령이다. 제한에는 실제로 적용되는 soft limit과 그 상한인 hard limit이 있고, 일반 프로세스는 soft limit을 hard limit까지만 올릴 수 있다(getrlimit(2)). 셸에서 바꾼 값은 서비스 매니저나 컨테이너 런타임이 띄우는 프로세스에는 적용되지 않을 수 있으므로, 실제로 돌고 있는 프로세스의 /proc/<pid>/limits를 확인하는 편이 정확하다.


이 장에서 가져갈 점

서버 지식은 인프라 엔지니어 수준으로 깊을 필요는 없지만,
문제가 났을 때 애플리케이션 바깥 계층을 전혀 못 읽는 상태는 위험하다.

최소한의 기준은 “증상을 보고 어느 제한에 닿았는지 짐작하고, 어디서 확인하는지 아는 것”이다. 예외 없이 사라진 프로세스는 OOM kill을, “Too many open files”는 FD 상한을, 평균은 낮은데 튀는 지연은 CPU throttling을 먼저 의심하고, 위 표의 위치에서 확인하면 된다.

참고한 자료외부 출처 7

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다