마이의 개발 블로그

[OS] 네이티브 라이브러리를 쓸 때 alpine 이미지를 조심하세요: 멀티코어 성능 문제 해결 과정 본문

개발지식/OS

[OS] 네이티브 라이브러리를 쓸 때 alpine 이미지를 조심하세요: 멀티코어 성능 문제 해결 과정

개발자마이 2025. 9. 3. 15:30
반응형

배경

최근 맞춤법 검사기 라이브러리 WebSpeller의 사용을 주 기능으로 하는 SpringBoot 서버를 Docker 기반 서비스로 배포하는 과정에서, 서버 어플리케이션이 멀티코어 CPU를 제대로 활용하지 못하는 문제가 발생했습니다. 호스트 PC에서 직접 구동하는 경우 CPU 활용률과 처리 속도에 문제가 없었으나, 도커 이미지 빌드 후 컨테이너에서 동작시킬 경우 같은 CPU 활용률이더라도 처리속도가 떨어지는 현상이 관찰된 것입니다. 오늘은 문제를 해결하나가며 얻게된지식들을 정리해보려고 합니다.

문제 해결 과정

기존 검증 내용 체크

초기에는 클라우드 서버의 CPU 단일 코어 성능이 로컬 PC보다 낮아서 성능이 제한된 것이라고 단순히 생각했습니다. 그러나 다양한 각도에서 문제를 분석하며 아래와 같은 내용들을 확인하고, 문제의 범위를 좁혀나갔습니다.

 

1. WebSpeller 클래스는 어떤 특징을 가지고 있는가?

  • JNI(Java Native Interface)로 네이티브 모듈을 구동시키는 wrapper 클래스
  • 단건 요청에 대해 단일 쓰레드로만 처리 가능
  • 주요 비즈니스 로직이 CPU 집약적임
  • 인스턴스 초기화에 시간이 걸림

이런 특징들로 인해 '스프링 쓰레드(요청) 1개 -> WebSpeller 1개 -> 물리 쓰레드 1개 사용' 방식으로 로직을 구성했습니다. 그렇기에 병목 없는 멀티 코어의 활용은 성능면에서 가장 중요한 문제임을 확인할 수 있었습니다.

 

2. 이미 최적화가 완료된 기존의 잠재적인 병목 구간들

  • 인스턴스 생성/해제 오버헤드: 유저 요청 -> WebSpeller 인스턴스 생성 후 비즈니스 로직 수행 방식을 배제하고 풀링(pooling)을 적용하여 CPU 사양에 맞는 WebSpeller 풀을 사전에 생성하여 재사용하는 방식을 적용했습니다. 이로 인해 반복적인 인스턴스 생성과 해제에 드는 자원과 시간을 감소시켰습니다.
  • 쓰레드 활용 구조로 인한 병목: 각 WebSpeller 인스턴스가 1 물리 쓰레드를 사용하는 구조임을 테스트를 통해 확인했고, 환경 변수로 WebSpeller 풀 개수, CPU 쓰레드 수, 스프링 쓰레드 수를 조절하여 호스트 머신의 사양에 맞는 설정값을 찾아 병목을 최소화했습니다.
  • 파일 I/O 병목: 네이티브 모듈을 그대로 사용해야 했기 때문에 내부 I/O를 직접 검증할 수는 없었지만, 부하 테스트와 자원 사용량 모니터링을 통해 파일 I/O의 성능 저하 영향은 제한적임을 확인했습니다.
  • 네이티브 모듈 내 공유자원 경쟁 문제: 다중 요청 -> 다중 WebSpeller 인스턴스 사용 시, 동일한 네이티브 모듈을 사용하는데 이때 자원 경쟁으로 인한 병목이 있는지 확인했습니다. WebSpeller 인스턴스 개수에 비례하여 CPU 사용률과 처리속도가 올라가는 걸 확인함으로써 공유자원 경쟁으로 인한 병목은 없거나 거의 미미한 것으로 결론내릴 수 있었습니다.

이렇게 이미 체크된 사항을 배제하고 나니, 실행 환경을 의심하기 시작했습니다.

실행 환경 체크

다음의 순서로 실행 환경을 체크했습니다.

  • 클라우드 서버(Linux, Docker) vs 로컬 PC(Windows, IDE 구동) 비교 -> 클라우드 서버에서만 문제 발생
  • (로컬 PC에서) Docker 컨테이너 구동 vs IDE 구동 비교 -> Docker 컨테이너에서만 문제 발생
  • (기존) alpine 이미지 vs (변경) slim 이미지 비교 -> alpine 이미지에서만 문제 발생

기존에 멀티스테이지 빌드를 적용하며 용량최적화를 하고자 alpine 이미지를 적용한 게 문제의 원인이었습니다. 트러블슈팅은 완료되었지만 저는 보다 근본적인 이유를 찾고싶어 이에 대해 LLM에게 조언을 구했는데, 네이티브 라이브러리와 OS/라이브러리 ABI(Application Binary Interface, 컴파일된 코드와 OS/라이브러리 간의 호출 규약, 메모리 구조, 함수 시그니처 등 런타임 상호운용 규칙) 차이가 원인으로 지목되었습니다.

 
alpine Linux는 glibc 대신 musl C 라이브러리를 사용하므로, 일부 네이티브 라이브러리에서 멀티코어 활용이 제한될 수 있고, 이로 인해 동일한 애플리케이션이라도 기반 C 라이브러리 차이로 성능이 달라질 수 있다는 것이었습니다. 이는 저의 사전지식으로는 알기 어려운 문제이다보니 LLM의 발전에 새삼 놀라게 되었습니다.

문제의 해결: Docker 베이스 이미지 변경

실행 환경 체크를 통해 알게된 사실들을 종합하여 glibc를 포함한 표준 Linux 이미지로 base image를 변경했습니다. 변경 후 테스트 결과, 멀티코어 활용 능력이 정상적으로 회복되었고, 성능 문제도 해결되었습니다. 예를 들어, 4개 물리코어를 할당하여 동시 요청 4개를 처리할 때 CPU 사용률이 이전과 달리 4개의 코어 전체에서 균등하게 분산되며, 1개 단일 요청과 동일한 시간 내에 작업이 완료됨을 확인할 수 있었습니다.

Note

- alpine 기반 Docker 이미지에서 일부 네이티브 라이브러리의 멀티스레드 성능이 제한될 수 있습니다.
- 문제 해결을 위해 glibc를 포함한 표준 Linux 이미지를 사용하면 멀티코어 활용 문제를 해소할 수 있습니다.
- 성능 문제를 분석할 때는 다각도의 최적화(인스턴스 오버헤드, 쓰레드 활용 구조, 파일 I/O 병목, 공유자원 경쟁 문제 등)가 이루어졌는지 먼저 체크해볼 필요가 있습니다.

반응형
Comments