새로운 내용을 공부할 때
새로운 내용의 공부를 시작할 때 용어의 정의를 이해하지 못하거나 정확하게 알지 못한다면 그 용어가 포함된 문장을 이해하지 못합니다.
작은 단어 하나가 내용을 이해하지 못하게 하기 때문에 용어를 정확하게 이해하는 것이 중요합니다.
TIL) 서버 자원 최적화와 리액티브 스트림으로 가는 길
비동기 프로그램을 가기 전에 제한된 자원과 Blocking I/O 환경에서 어떠한 문제가 발생할 수 있는지 확인해보겠습니다.
현재 웹 요청이 많아지게 되었고 503이나 클라이언트 타임아웃이 많아지게 되어 해결책을 톰캣 스레드풀, 커넥션 스레드풀의 설정 값을 늘리는 방법이 생각날 수 있습니다.
스레드풀과 DB 커넥션 풀의 크기를 늘리는 것은 위험하다.
애플리케이션의 스레드 풀과 데이터베이스 커넥션 풀 크기를 무분별하게 높이는 것은 다양한 성능 문제를 유발할 수 있습니다.
특히 CPU, 메모리, DB 등 주요 리소스의 효율성이 떨어지게 되며, 최악의 경우 시스템 다운이나 장애로 이어질 수 있습니다.
1. 스레드 증가와 메모리 사용량
각 스레드는 독립적인 실행 흐름을 가집니다.
스레드는 메모리에서 생성되어 약 1MB 정도라고 생각할 수 있으나 부가적으로 발생되는 메모리 소비가 생깁니다.
스레드 수 증가 = 전체 메모리 사용량 증가와 직결됩니다.
- 스택 영역(Stack) 영역: 각 스레드는 고유한 Stack 영역을 할당 받게 됩니다. 메서드 호출 정보, 지역 변수 등이 저장됩니다. 기본적인 1MB를 받습니다.
- 힙(Heap) 영역: 스레드가 생성하는 객체는 Heap에 저장됩니다. 스레드 수가 많아지면 결과적으로 Heap 사용량도 증가합니다.
- 네이티브 메모리(Native Memory): 버퍼, JNI 호출등 외부 자원 사용시 메모리가 증가합니다.
- 스레드 로컬 저장소(TLS): 스레드별 고유하게 관리하는 상태 저장공간으로 스레드 수 증가에 비례합니다.
2. 스레드 증가로 인한 CPU 비효율
-
컨텍스트 스위칭 오버헤드:
스레드 수가 증가하면 CPU는 더 많은 스레드 간 전환을 해야합니다.
이 과정에서 현재 실행 중인 스레드의 상태(레지스터, PC)를 TCB(스레드 작업 정보 블럭)에 저장하고, 다른 스레드 상태를 복원하는 작업이 발생됩니다.
이 과정 자체가 CPU 입장에서 오버헤드 입니다.
-
캐시 미스 발생(Cache Miss) 및 캐시 오염:
스레드 전환시 CPU 캐시(Line)가 다른 스레드의 메모리로 덮어쓰여, 이전 스레드의 데이터가 무효화됩니다.
해당 스레드가 실행될 때 메모리에서 데이터를 재로딩해야 하므로 캐시미스 발생률이 증가하게 됩니다.
캐시는 데이터를 그래도 유지하며, 새로운 스레드의 요청에 따라 필요한 데이터를 가져오고 정책에 따라 캐시 데이터를 교체하게 됩니다.
스레드가 많아질 수 록 이 정책에 대한 효율이 떨어지게 됩니다.
-
스케줄러 부하 증가:
CPU 스케줄러는
Ready Queue
에 다음 실행할 스레드를 선택합니다.리눅스는 CFS 스케줄러 기준으로 Red-Black-Tree 기반으로 O(logN) 비용이지만,
스레드 수가 많아질수록 스케줄링 관리 오버헤드가 누적되며 전체 시스템 효율이 비선형적으로 저하됩니다.
3. JVM GC 빈도 및 STW 시간 증가
-
힙 메모리 증가 → GC 부하 증가:
스레드 수 증가 → Heap 객체 생성 증가 → GC 빈도 증가
Heap이 일정 임계점에 도달하면 JVM은 Stop-The-World(STW)를 발생시켜 GC를 수행합니다.
-
Minor GC, Promotion 증가:
Young 영역이 더 자주 차게 되고, 살아남은 객체들이 Old 영역으로 빠르게 이동합니다.
-
Full GC 발생과 긴 STW:
Old 영역이 가득 차면 Full GC(또는 Major GC)가 발생합니다.
Full GC는 Young GC에 비해 훨씬 긴 STW를 유발하며, 모든 GC Root에서 시작해 Heap 전체를 탐색합니다.
최근 G1GC, ZGC, Shenandoah GC 등에서는 Full GC 시 STW 시간을 줄이려는 시도가 있지만 Old 영역의 압박이 심할 경우 여전히 시스템 정지 시간은 길어질 수 있습니다
4. Blocking I/O와 메모리 폭발 위험
-
Blocking I/O의 특징:
Tomcat(NIO Connector 포함)의 워커 스레드는 요청-응답을 처리하는 동안 Blocking 방식으로 동작합니다.
I/O 작업(예: DB 조회, 파일 읽기)이 발생하면 해당 스레드는 Block 상태로 전환되고 CPU를 사용하지 않습니다.
-
동시 요청 증가 → 메모리 부담:
예시: ThreadPool 200개, DB Connection Pool 100개
- 80개 Thread가 동시에 DB I/O 요청 → Block 상태
- 나머지 120개 Thread가 처리
- 80개 Thread가 동시에 깨고 대량 데이터를 Heap에 로딩 → 순간 Heap 사용량 급증 → OOM 발생 가능
5. DB 커넥션 풀 증가와 DB 서버 부하
-
DB 커넥션 수 증가 → 세션 관리 오버헤드:
DB Connection Pool을 무턱대고 늘리면, DB Server는 그만큼 더 많은 동시 세션을 관리해야 합니다.
각 세션은 별도의 메모리, 커서, Buffer Pool, Temp Table, Sort Buffer 등을 사용합니다.
-
쿼리 복잡도에 따른 DB 리소스 소비 폭발:
특히 GROUP BY, JOIN, 윈도우 함수 등 복잡한 쿼리는
→ CPU, 메모리, 디스크 I/O 모두 폭증
→ DB 세션 자체도 Block 상태로 대기 가능
→ 세션 수가 늘어나면 DB 메모리 과다 소모 → OS 스왑 발생 → 심할 경우 DB 자체 OOM으로 장애 발생 가능
해결방안
AI의 해결방안과 인사이트를 얻어보겠습니다.
아래 내용은 모두 알지 못하므로 조금씩 학습합니다.
영역 | 안전한 접근 방법 |
---|---|
스레드 풀 | CPU 코어 수 × 2~4 배 내외 유지 (I/O 바운드에 따라 조정), 불필요한 Blocking 최소화 |
커넥션 풀 | DB Server의 처리 한계 내에서 최소한으로 제한, 필요한 경우 Queueing 적용 |
메모리 | 대량 데이터 처리 시 반드시 Paging, Cursor, Streaming 적용 |
시스템 안정화 | 비동기/논블로킹 I/O 적용 (Netty, WebFlux 등), Backpressure 필수 적용 |
모니터링 | GC 로그, OS 메모리, DB 세션 수 모니터링 필수, 부하 테스트로 사전 |
기법
목표 | 주요 기법 |
---|---|
불필요한 데이터 자체 차단 | Paging, Cursor, Projection 최소화 |
한 번에 메모리 안 올리고 나눠서 | Streaming, Queueing |
자주 쓰는 건 캐시에서 처리 | Caching |
네트워크 전송량 줄이기 | Compression |
비동기/이벤트로 분산 처리 | Async, Queueing, Kafka |
읽기와 쓰기 분리/복제 사용 | Read Replica, CQRS |
사용자/시스템 단위 속도 제한 | Rate Limiting, Throttling |
댓글남기기