Published on

CPU 100% 문제 해결 시도 경험

Authors
  • avatar
    Name
    김민석
    Twitter

Introduction

상황

image.png

사용자가 PDF 파일을 업로드하면, Tesseract OCR을 통해 텍스트를 추출하는 기능을 구현했다.

그러나 AWS 프리티어(t2.micro) 환경에서 애플리케이션이 실행 중, PDF 파일을 처리할 때 CPU 사용률이 100%로 고정되면서 서버가 응답하지 않는 문제가 지속적으로 발생했다.

문제가 발생한 상황에서 안정적인 서비스를 제공하기 위해 다양한 방법을 시도했다. 이번 경험을 통해 성능 병목을 찾고, 해결하기 위한 작업을 진행하며 배운 기록들을 작성하겠다.

image.png

문제

image.png

- CPU 100% 사용

Tesseract OCR이미지 처리텍스트 추출을 위해 연산 집약적인 작업을 수행한다.

이 과정에서 PDF 파일의 모든 페이지를 개별적으로 처리해야 하며, 특히 PDFRenderer를 사용해 고해상도(기본적으로 300 DPI) 이미지를 렌더링하는 작업은 CPU 사용량을 급격히 증가시킨다.

결과적으로 CPU 리소스가 100%까지 도달하여 다른 작업을 수행할 여유가 사라지고 서버가 응답하지 않는 문제가 발생했다.

- 메모리 누수

VisualVM으로 애플리케이션의 메모리 사용량을 분석한 결과, byte[] 객체가 지속적으로 메모리에 쌓이는 현상이 보였다. PDF 파일에서 이미지를 추출하거나 OCR 작업을 수행하는 과정에서 발생한 메모리 누수이다.

메모리 누수로 인해 GC(Garbage Collection)가 적시에 수행되지 못하면서 사용 가능한 메모리가 점차 감소했고, 결과적으로 메모리 부족으로 인해 서비스가 비정상적으로 종료되었다.

- 스레드 과부하

Thread Dump를 분석한 결과, 많은 스레드가 대기 상태로 남아 있거나 비효율적으로 작업을 수행하고 있음을 확인했다. 이는 병렬 작업의 스케줄링과 작업 분배가 효율적으로 이루어지지 않았다는 것이다.

- AWS 인스턴스의 성능 한계

AWS 프리티어(t2.micro)1 vCPU1GB 메모리로 제한된 리소스를 제공한다는 것이다.

Tesseract OCRPDFRenderer가 요구하는 높은 연산량을 감당하기에는 턱없이 부족했다. 특히 CPU 바운드 작업인 OCR메모리 바운드 작업인 PDF 처리 작업을 동시에 수행할 때, 리소스 병목현상이 발생하여 서버가 비정상적으로 동작했다.

원인 + 시도

Tesseract OCR은 고연산 작업으로 인해 높은 CPU 사용량을 유발했다..

특히, PDF 파일이 다수의 페이지를 포함하고 있을 경우 페이지당 이미지 처리가 CPU를 과부하 상태로 만드는 것이다. PDFRenderer의 고해상도 이미지 처리(DPI 설정 문제)CPU와 메모리 사용량 증가에 영향을 준것이다.

스레드풀이 없거나, 병렬 처리가 적절히 설계되지 않았다. AWS 프리티어(t2.micro)의 리소스 한계로 인해 연산 작업이 원활히 실행되지 못했다.

PDFRenderer DPI 설정 조정

PDFRenderer에서 기본적으로 300 DPI로 설정된 해상도를 150 DPI로 낮춰, 메모리 사용량과 이미지 처리 속도를 개선 했다.

BufferedImage image = renderer.renderImageWithDPI(page, 150, ImageType.GRAY);

스레드풀 도입

Tesseract OCR의 병렬 처리를 구현하기 위해 ExecutorService를 사용해 스레드풀을 도입했다. 이를 통해 CPU 작업을 분산하고, 하나의 스레드에서 처리되는 문제를 해결하고자 했다.

ExecutorService executorService = Executors.newFixedThreadPool(4);

for (int page = 0; page < document.getNumberOfPages(); page++) {
    int finalPage = page;
    executorService.submit(() -> {
        try {
            BufferedImage image = renderer.renderImageWithDPI(finalPage, 150, ImageType.GRAY);
            synchronized (extractedText) {
                extractedText.append(tesseract.doOCR(image));
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    });
}
executorService.shutdown();
executorService.awaitTermination(10, TimeUnit.MINUTES);

Spring Cache를 활용한 OCR 결과 캐싱

같은 이미지에 대해 OCR 처리를 반복적으로 수행하지 않도록 Cache를 활용하여 결과를 캐싱했다.

이를 통해 중복 연산을 줄이고 성능을 개선하였다.

@Cacheable("ocrCache")
public String getCachedPageText(BufferedImage image) throws TesseractException {
    return tesseract.doOCR(image);
}

도커 환경 설정 최적화

Docker Compose를 활용한 메모리 제한 및 환경 변수 설정

도커 컨테이너의 메모리와 CPU 사용량제한하고, 환경 변수를 설정하여 리소스 사용을 관리했다.

services:
  application:
    image: springboot-app
    mem_limit: 1024m
    cpus: "1.0"
    build:
      context: .
      dockerfile: Dockerfile
    ports:
      - "8080:8080"
    environment:
      - JAVA_OPTS=-XX:+UseSerialGC -Xms512m -Xmx768m
      - SPRING_PROFILES_ACTIVE=prod
      - TESSDATA_PREFIX=/usr/share/tesseract-ocr/4.00/tessdata

VisualVM을 통한 모니터링 및 문제 분석

VisualVM을 사용해 애플리케이션의 메모리와 CPU 사용량을 실시간으로 모니터링하며 성능 병목을 분석했다.

  • 메모리 누수 확인.
image.png

byte[] 객체가 지속적으로 메모리를 차지하며 GC가 정상적으로 작동하지 않는 것을 발견했다.

  • CPU 과부하 확인
image.png

CPU 사용량이 지속적으로 100%를 유지하며 OCR 작업이 주요 원인임을 확인했다.

Thread Dump를 통한 문제 진단

Thread Dump를 통해 스레드가 대기 상태에 머무르거나 병렬 처리가 올바르게 작동하지 않는 점을 분석했다.

image.png

비효율적으로 대기 중인 스레드의 상태를 확인하며 스레드풀 설정 문제를 확인했다.

http-nio-8080-exec-1~10

AWS 인스턴스 업그레이드

AWS 프리티어(t2.micro) 인스턴스를 t3.large로 업그레이드하여 CPU와 메모리 리소스를 확장했다.

image.png

업그레이드 이후 CPU 사용률이 감소했으나 OCR 작업으로 인한 높은 리소스 요구는 여전히 존재했다..

image.png

결말.

인스턴스를 t2.micro에서 t3.large로 업그레이드한 후, CPU 사용량 문제는 일부 완화되었다.

t3.large는 2개의 vCPU와 8GB의 메모리를 제공하므로, 이전보다 더 많은 작업을 동시에 처리할 수 있게 되었다. 특히 Tesseract OCR의 연산량을 다루는 데 있어서 CPU 리소스의 여유가 생겼고, 애플리케이션이 중단 없이 동작할 수 있었다.

하지만, OCR 작업 자체가 고도로 연산 집약적이기 때문에 성능 문제를 완전히 해결하지는 못했다..

코드 최적화 노력.. 그리고 결과

  • 스레드풀 도입 및 병렬 처리: 기존에는 모든 페이지를 순차적으로 처리했으나, 스레드풀을 이용해 각 페이지를 병렬로 처리하도록 수정했다. 이를 통해 CPU 사용률 분산 효과와 함께 OCR 처리 속도가 일부 개선되었다.
    • 그러나, Tesseract OCR싱글 스레드로 동작하기 때문에 병렬 처리가 완벽히 활용되지 못했다.
  • Docker 환경: Docker Compose를 활용하여 메모리와 CPU 제한을 설정하고, 각 컨테이너가 할당된 자원을 초과하지 않도록 했다. 특히, mem_limitcpus 설정을 통해 Docker 컨테이너가 전체 인스턴스의 리소스를 독점하지 않도록 조정했다.
    • 하지만, OCR 작업은 여전히 Docker 컨테이너 내부에서도 높은 리소스를 소비하는 문제를 남겼다..
  • DPI 개선: PDFRenderer를 통해 생성하는 이미지의 DPI를 기존 300에서 150으로 낮춰 처리 속도를 높이고 메모리 소비를 줄였다.
    • 그러나 OCR의 정확도가 일부 낮아지는 문제가 발생했기 때문에, 이미지 해상도와 성능 간의 적절한 균형을 찾는 데 시간이 소요되었다..

VisualVM 분석 및 발견.

VisualVM으로 애플리케이션의 CPU, 메모리, 스레드 상태를 모니터링한 결과, 애플리케이션 성능의 주요 병목은 OCR 처리 과정에서 발생하는 메모리 누수와 스레드 비효율성으로 나타났다.

  • 메모리 누수: VisualVM 힙 덤프에서 byte[] 객체가 메모리에 지속적으로 남아있는 현상을 발견했습니다. 이는 PDF에서 고해상도 이미지를 추출한 후 메모리를 해제하지 않아 발생한 문제였다. 이를 개선하기 위해 이미지 처리 후 BufferedImage 객체를 명시적으로 null로 설정하고 GC를 트리거했다.
  • 스레드 과부하: Thread Dump를 분석한 결과, 많은 스레드가 WAITING 상태에서 대기하며 비효율적으로 동작하는 현상이 발견되었다. 병렬 작업의 적절한 스케줄링이 필요했으나, OCR 작업의 한계로 인해 완전한 해결은 어려웠다.

회고

이번 경험을 통해 단순히 하드웨어 업그레이드만으로는 리소스 집약적인 서비스의 문제를 완전히 해결할 수 없다는 점을 배웠다. 리소스 관리의 중요성을 실감하며, 문제를 체계적으로 분석하고 해결 방안을 마련하는 과정에서 많은 것을 배웠다. 특히 VisualVMThread Dump 분석은 문제의 원인을 명확히 파악하는 데 큰 도움을 주었다.

공부거리

  • Tomcat 내부 구조: Catalina, NIO, http-nio-8080-exec-Poller와 Acceptor의 역할.
  • HTTP 스레드풀: http-nio-8080-exec-1~10의 동작 원리와 방안.
  • 보안: 공개키와 세션키의 역할 및 작동 원리.