8.0 운영체제 — 애플리케이션 아래의 계약
운영체제 지식은 커널 개발자만의 전공이 아니다. 애플리케이션의 지연, 메모리 증가, I/O 정체는 결국 커널이 CPU·메모리·장치를 배분한 결과로 나타난다. 이 인트로는 운영체제 기능을 미리 외우는 대신, 익숙한 시스템 지표를 원인과 연결하기 위해 어떤 질문을 던져야 하는지 세우고 챕터 8의 학습 경로를 안내한다.
학습 목표
- 애플리케이션 개발자가 성능 진단과 아키텍처 판단을 위해 운영체제의 동작 모델을 알아야 하는 이유를 설명한다.
- 스레드 실행, 메모리 할당, 파일 쓰기, 비동기 I/O 같은 익숙한 표현 아래에 어떤 커널의 결정이 숨어 있는지 식별한다.
- CPU·메모리·I/O 문제를 만났을 때 챕터 8의 각 문서에서 답해야 할 질문을 세운다.
출발점: 원인은 대시보드에 적혀 있지 않다
한 API 서버의 평소 p99 응답 시간은 80ms였다. 배포 후 어느 순간부터 p99가 2초를 넘기기 시작했다. 에러율은 높지 않고, 프로세스도 죽지 않는다. 대시보드에는 다음 값들이 함께 보인다.
| 관찰 | 팀에서 바로 나온 해석 |
|---|---|
| CPU 사용률 65% | "CPU가 남으니 스레드를 더 늘리자" |
| load average 20 | "CPU가 이미 포화된 것 아닌가?" |
| RSS의 지속적인 증가 | "메모리 누수 같다" |
| 간헐적인 iowait 상승 | "디스크가 느린 것 같다" |
| 이벤트 루프 지연 증가 | "블로킹 코드를 전부 async로 바꾸자" |
문제는 이 해석들이 서로 충돌한다는 데 있다. CPU가 남았다는 말과 포화됐다는 말이 동시에 나오고, 메모리 누수와 디스크 병목이 같은 현상의 후보가 된다. 이때 지표 하나를 골라 처방부터 적용하면 원인이 잠시 가려지거나 병목이 다른 곳으로 옮겨갈 뿐이다.
지표는 원인이 아니다. 커널이 자원을 배분하고 작업을 기다리게 한 결과를 특정 방식으로 센 값이다. 따라서 다음 질문이 먼저 와야 한다.
- CPU가 100%가 아닌데도 요청은 왜 실행 차례를 기다리는가?
- load average는 CPU 사용률과 같은 것을 세는가?
- RSS가 늘었다는 사실만으로 메모리 누수를 말할 수 있는가?
write()가 성공했는데 데이터는 왜 사라질 수 있는가?- 비동기 I/O는 무엇을 더 빠르게 하고, 무엇은 빠르게 하지 못하는가?
이 질문에 답하려면 모니터링 도구의 사용법보다 그 숫자를 만들어 낸 운영체제의 동작 모델이 필요하다.
운영체제는 자원 관리자이자 추상화 제공자다
애플리케이션은 CPU 코어, 물리 메모리 칩, 저장 장치를 직접 다루지 않는다. 운영체제가 그 사이에서 두 가지 역할을 맡는다.
첫째, 운영체제는 **자원 관리자(resource manager)**다. 실행 가능한 작업이 코어보다 많으면 누구를 먼저 얼마나 실행할지 정한다. 프로세스들이 요구하는 메모리가 물리 메모리보다 많아지면 어떤 페이지를 남기고 회수할지 정한다. 여러 프로그램의 파일과 네트워크 요청을 장치에 전달하고, 기다리는 작업을 재우거나 깨운다. 성능 문제는 대부분 이 배분 과정의 대기열과 포화 지점에서 시작한다.
둘째, 운영체제는 **추상화 제공자(abstraction provider)**다. 프로그램마다 CPU를 혼자 쓰는 듯한 실행 흐름을 주고, 연속된 메모리를 가진 듯한 가상 주소 공간을 주며, 서로 다른 저장 장치를 파일과 파일 디스크립터라는 공통 인터페이스로 보여 준다. 개발자는 이 추상화 덕분에 하드웨어 모델마다 애플리케이션을 다시 작성하지 않는다.
추상화는 현실을 없애지 않고 평소에 감춘다. 여유 자원이 충분할 때는 감춰진 비용이 거의 드러나지 않는다. 그러나 부하가 늘거나 장애가 발생해 경계 조건에 닿으면 CPU 실행 대기, 페이지 폴트, write-back 같은 아래 계층의 사건이 지연과 처리량에 직접 나타난다. 운영체제를 안다는 것은 모든 커널 기능을 외운다는 뜻이 아니라, 추상화가 흔들릴 때 아래에서 어떤 일이 일어나는지 설명할 모델을 갖는다는 뜻이다.
익숙한 코드 아래에는 계약이 있다
애플리케이션의 표현과 운영체제가 실제로 제공하는 보장은 종종 다르다. 이 간극을 계약으로 보면 무엇을 배워야 하는지가 선명해진다.
| 애플리케이션에서 보이는 표현 | 아래에서 확인해야 할 계약과 비용 |
|---|---|
| "스레드가 실행 중이다" | 지금 코어에서 실행 중인지, 실행 가능하지만 run queue에서 기다리는지, I/O나 락 때문에 잠들었는지 구분해야 한다. |
| "메모리 1GiB를 할당했다" | 가상 주소를 약속받은 것인지, 물리 페이지가 실제로 지급되어 RSS에 잡힌 것인지 구분해야 한다. |
| "파일에 쓰기가 끝났다" | 데이터가 페이지 캐시에 복사된 것인지, 저장 장치까지 내려가 전원 장애에도 남는지 구분해야 한다. |
| "비동기로 바꿨다" | 단일 작업이 빨라진 것인지, 적은 스레드로 더 많은 대기를 표현하게 된 것인지 구분해야 한다. |
이 구분을 모르더라도 코드는 정상적으로 실행될 수 있다. 차이는 문제가 생긴 뒤에 드러난다. 스레드를 더 늘렸는데 처리량은 그대로이고 p99만 악화되거나, 큰 메모리를 할당했는데 한참 뒤 첫 접근에서 지연이 생기거나, 성공 응답을 보낸 데이터가 전원 장애 후 사라질 수 있다. 추상화의 이름만 아는 개발자는 현상을 예외로 취급하지만, 계약을 아는 개발자는 예상 가능한 비용으로 해석한다.
운영체제 지식이 필요한 세 가지 순간
1. 성능 저하와 장애를 진단할 때
CPU 사용률, load average, RSS, page fault, iowait, context switch는 서로 독립된 경고등이 아니다. 태스크가 실행되고, 메모리 페이지를 받고, I/O 완료를 기다리는 하나의 경로를 다른 위치에서 관찰한 값들이다. 예를 들어 load average가 높다는 사실만으로 CPU 증설을 결정할 수 없다. 실행 가능한 태스크가 코어를 기다리는지, 디스크 요청을 끝내지 못한 태스크가 쌓였는지에 따라 처방이 달라진다.
운영체제 모델은 "어느 지표가 높다"를 "어떤 자원을 기다리는 작업이 어디에 쌓였다"로 번역하게 한다. 이 번역이 되어야 다음에 확인할 지표와 실험이 정해진다.
2. 동시성과 자원 용량을 설계할 때
스레드 풀 크기, 프로세스 수, 이벤트 루프, 메모리 제한은 프레임워크 설정처럼 보이지만 실제로는 커널 자원을 어떻게 나눠 쓸지 정하는 선택이다. CPU 바운드 작업에 코어 수보다 훨씬 많은 스레드를 주면 계산 능력이 늘어나는 대신 실행 대기와 교대 비용이 커진다. 반대로 대부분이 I/O를 기다리는 작업에는 코어보다 많은 실행 흐름이 필요할 수 있다.
중요한 질문은 "어떤 기술이 더 빠른가"가 아니다. 이 워크로드는 무엇을 기다리며, 동시성이 늘 때 어느 비용이 연결 수나 작업 수에 비례해 증가하는가이다. 운영체제는 이 질문에 답하기 위한 비용 모델을 제공한다.
3. 데이터의 완료와 안전성을 판단할 때
함수의 반환은 개발자가 기대하는 비즈니스 완료와 같지 않을 수 있다. write() 성공은 보통 데이터가 커널의 메모리에 들어갔다는 뜻이지, 전원이 끊겨도 남는다는 뜻은 아니다. 반대로 모든 쓰기에 가장 강한 내구성을 요구하면 저장 장치 왕복을 매번 기다리느라 처리량을 크게 잃을 수 있다.
로그, 메시지, 트랜잭션을 설계할 때는 "성공"이 어느 계층의 완료를 뜻하는지 정해야 한다. 운영체제의 I/O 계약을 알아야 유실 허용량과 지연·처리량 사이의 트레이드오프를 의도적으로 선택할 수 있다.
이 챕터에서 세울 세 가지 모델
앞의 장애 사례를 해결하는 데 필요한 모델은 세 문서로 나뉜다.
8.1 프로세스와 스케줄링
누가 언제 CPU를 사용하며, 실행 흐름을 바꾸는 비용은 무엇인가?
프로세스와 스레드를 커널의 실행 단위로 통일해 보고, 시스템 콜과 컨텍스트 스위치를 구분한다. 스케줄러, run queue, load average, 자발적·비자발적 스위치의 관계를 세우면 "CPU가 남는데 왜 느린가"와 "스레드를 더 늘려야 하는가"를 근거로 판단할 수 있다.
8.2 가상 메모리
주소와 실제 메모리는 언제 연결되며, 메모리 지표는 무엇을 세는가?
가상 주소, 물리 페이지, 페이지 캐시를 구분하고 demand paging과 copy-on-write를 살펴본다. VSZ·RSS·PSS, minor·major fault, swap·OOM의 관계를 세우면 "메모리가 늘었다"를 누수, 정상적인 캐시, 실제 메모리 압력으로 나눠 조사할 수 있다.
8.3 파일 I/O와 I/O 모델
데이터와 I/O 요청은 어디를 거치며, 완료는 무엇을 의미하는가?
페이지 캐시, write-back, fsync가 만드는 데이터 경로와 블로킹·readiness·completion으로 이어지는 제어 경로를 다룬다. 이를 통해 write()의 성공과 내구성을 구분하고, thread-per-connection과 이벤트 루프를 동시 연결 수와 작업 성격에 따라 선택할 수 있다.
세 모델은 독립된 지식 목록이 아니다. I/O로 잠든 태스크는 스케줄러의 자발적 스위치를 만들고, 파일을 읽은 데이터는 페이지 캐시에 올라와 메모리 지표에 잡힌다. 한 자원의 문제처럼 보이는 현상이 다른 지표에 나타나는 이유가 이 연결에 있다.
이 챕터를 읽는 방법
각 문서에서 명령과 용어를 외우기보다 다음 원칙을 반복해서 적용한다.
- 숫자가 무엇을 세는지 묻는다. 같은 "사용량"이라도 CPU 사용 시간, 실행 대기 수, 약속된 주소, 지급된 물리 페이지는 서로 다른 상태다.
- API 반환과 실제 완료를 구분한다. 호출이 성공했다면 어느 계층까지 일이 끝났고, 무엇이 아직 큐나 캐시에 남았는지 확인한다.
- 평균보다 포화점과 꼬리를 본다. 처리량 평균이 유지되어도 대기열이 생기면 p99 지연은 먼저 무너진다.
- 빠르다는 주장을 측정으로 바꾼다. 스레드 수, working set, 동시 연결 수를 변화시키고 처리량·지연·스위치·페이지 폴트를 함께 관찰한다.
- 도구 출력보다 먼저 가설을 쓴다. 어떤 커널 사건이 어떤 지표 변화를 만들 것인지 예측한 뒤 측정해야 숫자를 원인처럼 오해하지 않는다.
챕터 8의 실습은 이 원칙을 컨텍스트 스위치 비용과 I/O 모델별 처리량 측정에 적용한다. 최종 목표는 명령어를 많이 아는 것이 아니라, 낯선 장애에서도 "어느 자원의 어떤 대기열을 확인해야 하는가"를 스스로 정하는 것이다.
정리
- 운영체제는 CPU·메모리·장치를 배분하는 자원 관리자이자, 프로세스·가상 메모리·파일이라는 추상화를 제공하는 계층이다.
- 시스템 지표는 원인이 아니라 커널 동작의 결과다. 지표가 무엇을 세는지 알아야 올바른 다음 질문과 처방을 선택할 수 있다.
- 스레드 실행, 메모리 할당, 파일 쓰기, 비동기 I/O에는 애플리케이션의 표현과 실제 완료 사이의 계약과 비용이 있다.
- 챕터 8은 실행과 스케줄링, 가상 메모리, 파일·I/O라는 세 모델을 연결해 성능 진단과 아키텍처 선택의 근거를 만든다.
확인 문제
1. API 서버의 p99가 급증했고 CPU 사용률은 60%, load average는 코어 수의 두 배다. "CPU가 40% 남았다"는 말만으로 스레드를 늘리면 안 되는 이유를 설명하고, 어떤 질문부터 해야 하는지 제시하라.
정답과 해설
CPU 사용률은 관찰 구간에 CPU가 일한 시간의 비율이고, load average는 같은 대상을 세는 지표가 아니다. 실행 가능한 태스크가 코어를 기다리는지, I/O 완료를 기다리는 태스크가 쌓였는지 먼저 구분해야 한다. 전자라면 스레드 증설이 실행 대기와 컨텍스트 스위치를 늘리고, 후자라면 CPU가 아니라 I/O 경로를 조사해야 한다. 구체적인 지표 해석은 8.1에서 다룬다.
2. 팀원이 "write()가 성공했고 async 함수도 resolve됐으므로 데이터는 디스크에 안전하게 저장됐다"고 주장한다. 이 문장에서 분리해야 할 두 질문은 무엇인가?
정답과 해설
첫째, 함수가 반환한 시점에 데이터가 페이지 캐시, 장치 캐시, 비휘발성 매체 중 어디까지 도달했는지 물어야 한다. 둘째, async가 표현한 것이 작업의 내구성 완료인지, 호출 스레드를 점유하지 않고 대기를 표현했다는 뜻인지 구분해야 한다. 비동기 실행 모델과 데이터 내구성은 별개의 계약이며, 8.3에서 각각의 경로를 다룬다.
참고 자료
- Remzi Arpaci-Dusseau, Andrea Arpaci-Dusseau, Operating Systems: Three Easy Pieces — 가상화, 동시성, 영속성이라는 세 축으로 운영체제의 문제를 설명하는 공개 교재. 챕터 8 전체의 개념 지도로 활용한다.
- Brendan Gregg, Systems Performance 2nd ed. (2020) — 운영체제 자원과 애플리케이션 성능을 연결하는 모델, 관찰 방법론, 포화 중심 진단을 참고한다.
- Linux man-pages, proc(5) — Linux가 프로세스와 시스템 상태를 어떤 인터페이스로 노출하는지 확인하는 1차 자료다.