팀 프로젝트 festabook에서 학습한 내용을 정리한 글입니다.
시스(CS 스터디)에서 발표한 내용을 정리한 글입니다.
💭 들어가며
본 글은 시스(CS 스터디)에서 발표한 내용을 정리한 자료로, 발표용 흐름에 맞추어 순차적으로 구성되어 있다. 개념에 대한 보다 정확한 이해가 필요하다면, 시스 유튜브에 업로드된 영상 자료를 함께 참고 바란다.
✅ 문제 상황
festabook의 개발자는 모두 백수이다. 우리는 서버 비용이 부담스러웠다. 따라서 의사결정 과정에서 scale down을 하고자 했는데, 단순히 사양을 낮추는 데서 끝내기보다는 동일하거나 더 낮은 사양에서도 성능 대비 가성비를 최대한 끌어올릴 수 있는 방법이 없을지 고민하게 되었다.
우리는 서버 전 계층 튜닝을 시도했고, HikariCP, JDBC, MySQL, Tomcat, GC까지 순차적으로 조정했다. 여러 튜닝 중 가장 큰 성능 향상을 가져온 것은 Tomcat의 기본 maxThreads 값을 200에서 15로 줄인 변경이었다. 이 튜닝만으로 TPS가 약 9.1% 향상되었고, p95 지연 시간은 2초에서 1초로, p99 지연 시간은 3초에서 1초로 감소했다.
다양한 이유가 있었고, 그 이유 중 하나인 Context Switching에 대해 알아보고자 한다.
✅ 사전 지식
CPU 관련해서 신빙성 있는 문서를 찾기가 쉽지 않았다. 아래에 첨부한 이미지들은 원본 출처가 명확하지 않은 자료들이며, 특정 벤더(Intel, AMD 등)가 공식적으로 제공한 아키텍처 다이어그램도 아니다. 따라서 해당 이미지들은 정확한 구현을 설명하기 위한 자료라기보다, 개념 이해를 돕기 위한 설명용이라고 이해하면 될 것 같다.
▶ 컴퓨터 구조

컴퓨터 구조는 크게 네 가지 구성 요소의 역할로 나누어 설명할 수 있다.
- CPU: 명령을 해석하고 데이터를 처리하는 중앙 처리 장치
- Memory: 프로그램과 데이터를 저장하는 장치
- I/O: 컴퓨터와 외부 장치 간 데이터 교환을 담당
- System Bus: CPU, 메모리, I/O를 연결해 데이터 전송을 수행하는 경로
▶ CPU 구성 요소

모든 세부 구조를 알면 좋겠지만, 여기서는 백엔드 개발자 관점에서 필요한 범위까지만 정리한다.
CPU 내부는 크게 연산, 제어, 저장, 연결이라는 네 가지 역할로 나눠볼 수 있다.
- 연산 장치: 실제 계산을 수행하는 하드웨어 블록이다. 정수 연산을 담당하는 ALU, 부동소수점 연산을 처리하는 FPU 등이 여기에 포함된다.
- 제어 장치: 명령어를 해석하고 실행 순서를 제어한다. 주소 변환을 제어하는 MMU가 여기에 해당된다.
- 레지스터 파일: CPU 내부에 존재하는 초고속 저장소로, 연산에 필요한 데이터와 중간 결과를 저장한다.
- 캐시 계층: 메인 메모리 접근 지연을 줄이기 위한 다단계 캐시 구조이다.
- L1 Cache: 가장 빠른 캐시로, 일반적으로 코어당 존재한다.
- L2 Cache: CPU 설계에 따라 코어당 존재할 수도 있고, 소수의 코어 단위로 공유될 수도 있다.
- L3 Cache: 여러 코어가 공유하는 마지막 단계 캐시, 요즘은 잘 설계되지 않는다고 한다. (정확하지 않음)
- 인터커넥트: 코어, 캐시, 메모리 컨트롤러, I/O 인터페이스 등을 연결하는 내부 데이터 경로이다.
▶ 멀티코어 CPU 구조

- Processor: 물리적인 CPU 패키지 단위
- Core: 각종 연산을 하는 CPU의 핵심 요소
- L1 Cache: 코어 전용
- L2 Cache: 같은 프로세서 내 코어들이 공유 (실제 구현은 벤더별 상이)
- System Memory(RAM): 캐시에 없을 때 접근
▶ 컴퓨터 메모리 계층

- Register
- CPU가 직접 연산에 사용하는 값 가장 빠르고 가장 작음
- L1 / L2 Cache (SRAM)
- 최근 사용 데이터 보관, 캐시 히트 여부가 성능을 좌우
- Main Memory (DRAM)
- 애플리케이션 실행 데이터, 캐시 미스 시 접근
- Local Storage (Disk)
- 파일 및 DB 데이터, 메모리보다 수천 배 느림
- Remote Storage
- 네트워크 너머 자원, 가장 느림
▶ 가상 메모리 ↔ 물리 메모리

- 프로세스는 가상 주소(Virtual Address)만 사용
- CPU/MMU가 페이지 테이블과 TLB를 통해 물리 주소(Physical Address)로 변환
- MMU(Memory Management Unit): 가상 주소를 물리 주소로 변환하고, 메모리 접근 권한을 검사하는 장치
- TLB(Translation Lookaside Buffer): 가상 페이지 번호(VPN) → 물리 프레임 번호(PFN) 변환 결과를 캐시하는 하드웨어 캐시
- 실제 데이터는 물리 메모리(RAM)에 존재
이러한 구조를 사용하는 이유는 프로세스 간 메모리 보호를 보장하고, 물리 메모리 크기를 초과하는 메모리 사용(Swap)을 가능하게 하며, 실제 물리 주소를 알 필요 없는 추상화된 메모리 모델을 제공하기 위해서다.
▶ Context Switching
CPU 코어가 한 프로세스(또는 스레드)의 실행을 멈추고, 다른 프로세스(또는 스레드)를 실행하기 위해 현재 상태를 바꿔 끼우는 작업이다.
CPU 코어는 한 순간에 하나의 실행 흐름만 실제로 수행한다. 여러 작업을 동시에 처리하는 것처럼 보이기 위해 OS는 실행 대상을 빠르게 바꾼다.
✅ 파고들기
▶ Context Switching 비용
CPU에서의 컨텍스트 스위칭은 단순히 실행할 프로세스의 차례를 바꾸는 작업이 아니다. 스레드 전환 시 CPU 내부에서는 여러 하드웨어 상태가 교체되며, 이 과정에서 상당한 비용이 발생한다. 대표적인 비용은 다음 세 가지다.
1️⃣ 상태 저장/복원
컨텍스트 스위칭 시 CPU는 현재 실행 중인 프로세스의 상태를 저장하고, 다음 프로세스의 상태를 복원한다.
- 일반 레지스터
- PC(Program Counter): 다음에 실행할 명령어의 주소
- SP(Stack Pointer): 현재 스택의 최상단 위치
- FPU(Floating Point Unit) / SIMD(Single Instruction Multiple Data) 레지스터
- 커널 내부 스케줄링 상태: 프로세스/스레드 상태, 우선순위 등
→ 이 과정 동안 CPU는 실제 애플리케이션 로직을 수행하지 못한다.
2️⃣ L1, L2 캐시 손실
프로세스가 교체되면 CPU 코어의 캐시는 완전히 다른 주소 공간의 작업 집합으로 채워진다.
- 이전 프로세스가 사용하던 캐시 라인이 대규모로 덮어쓰기(eviction)
- 이전 프로세스의 캐시 지역성(cache locality) 사실상 전부 상실
결과적으로 캐시 히트율이 떨어지고, 더 느린 계층(L2 또는 메모리)에 대한 접근이 증가한다.
3️⃣ TLB
가상 메모리 환경에서는 모든 메모리 접근 시 가상 주소 → 물리 주소 변환이 필요하다. 이 변환 비용을 줄이기 위해 CPU는 TLB를 사용한다.
- CPU가 가상 주소를 생성한다.
- MMU가 TLB를 먼저 조회한다.
- Hit: 즉시 물리 주소를 생성해 메모리에 접근한다.
- Miss: 페이지 테이블을 조회해 PFN을 얻고, 해당 결과를 TLB에 적재한 뒤 다시 접근한다.
TLB 안의 엔트리는 현재 프로세스의 주소 공간에만 유효하다.
- 프로세스 1: VPN 10 → PFN 100
- 프로세스 2: VPN 10 → PFN 170
같은 VPN이 서로 다른 PFN을 가질 수 있으므로, 프로세스가 바뀌면 기존 TLB 엔트리는 그대로 사용할 수 없다.
▶ 가장 치명적인 비용은
캐시 손실이다.
앞서 언급한 L1, L2 캐시와 TLB는 모두 메모리 계층 캐시이며, USENIX 논문에 따르면 이 두 요소가 전체 비용의 대부분을 차지한다.
🔽 Direct Cost (직접 비용)
- 데이터 접근 없이 스위칭 자체에 드는 비용
- 컨텍스트 스위치마다 반드시 발생
- 1️⃣이 여기 해당됨
- 구성 요소
- CPU 레지스터 저장/복원
- 커널 스케줄러 실행
- TLB 무효화 및 재적재 비용
- 파이프라인 flush
→ 실험 결과: 약 3.8μs
🔽 Indirect Cost (간접 비용)
- 캐시/메모리 상태가 깨져서 생기는 추가 비용
- 항상 발생하지 않음 (워크로드, 데이터 크기, 접근 패턴에 따라 달라짐)
- 2️⃣, 3️⃣이 여기 해당됨
- 원인
- 데이터 캐시(L1/L2) eviction
- 캐시 및 TLB warm-up 지연
→ 해당 비용의 실험 결과를 도출하는 것이 해당 논문의 목적
▶ Indirect Cost 실험 결과
실험 방법
- 두 프로세스는 번갈아 가며 실행되도록 강제로 컨텍스트 스위칭을 유발한다.
- 각 프로세스는 실행될 때마다 array를 읽기만 한다.
- 배열 크기와 접근 간격(stride)을 바꿔가며 접근한다.
🔽 결과 1: array size ≤ ~200KB
- 전체 데이터가 L2 캐시(512 KB)에 수용
- 컨텍스트 스위칭 비용: 4~8 μs
→ 필요한 데이터가 어차피 L2 안에 남아있어서 다시 메모리까지 내려갈 일이 적다. 이 경우 컨텍스트 스위칭은 사실상 거의 비용이 없다.
🔽 결과 2: array size = 256~512 KB
- 두 프로세스의 데이터 합이 L2 캐시(512 KB) 초과
- 컨텍스트 스위칭 비용: 38.6 μs → 203.2 μs
→ 매 스위칭마다 캐시가 무효화되어, 메모리에서 재로딩이 발생했다. 그 결과 비용이 6배 이상 급증했다.
🔽 결과 3: stride 실험
stride는 메모리 접근에서 이전 접근 주소와 다음 접근 주소 사이의 간격을 의미한다.
- stride 8B (연속 접근)
- 평균 비용: 116.5 μs
- stride 128B (캐시 라인마다 점프)
- 평균 비용: 825.3 μs
- 최대 비용: ~1500 μs (1.5 ms)
→ 데이터 크기가 같아도, 메모리 접근 패턴 하나만으로 몇 배의 차이가 발생한다.
✅ 확장하기
▶ 프로세스와 스레드 컨텍스트 스위칭의 차이
🔽 프로세스 컨텍스트 스위칭
- 서로 다른 프로세스 간 전환
- 각 프로세스는 독립적인 주소 공간을 가짐
- 전환 시 페이지 테이블이 변경됨
→ 기존 주소 변환 정보를 사용할 수 없어 TLB flush 또는 ASID 전환이 필요
🔽 스레드 컨텍스트 스위칭
- 같은 프로세스 내부의 스레드 간 전환
- 주소 공간 공유
- 페이지 테이블이 동일
→ 주소 변환 정보가 유지되므로 일반적으로 TLB flush가 필요하지 않음
📍 참고 자료
'CS > OS' 카테고리의 다른 글
| [OS] TPS는 왜 수렴하지 않았을까 (0) | 2026.02.03 |
|---|---|
| [OS] 동기/비동기, 블로킹/논블로킹 (0) | 2025.04.19 |