업데이트:

2 분 소요

개요

오늘은 운영 환경에서 이미지 기반 처리 작업이 실제로 어느 정도 시간이 걸리는지 확인했다.

처음에는 API 서버나 게이트웨이, 네트워크 구간에서 지연이 생기는 것일 수도 있다고 생각했다.
하지만 직접 운영 경로와 작업 서버를 나누어 측정해보니, 병목은 외부 호출 구간이 아니라 작업 서버 내부의 모델 추론 구조에 가까웠다.

운영 환경에서 먼저 확인한 것

먼저 단순 헬스체크를 확인했을 때는 응답이 매우 빨랐다.
수 ms에서 100ms대 수준이라서, 서버 자체가 느리거나 네트워크가 막혀 있는 상황은 아니었다.

문제는 실제 파일 처리 요청이었다.

직접 작업 서버에 요청했을 때 첫 요청은 80초대가 걸렸고, 다시 요청했을 때도 40초 전후가 걸렸다.
운영 API를 통한 사용자 흐름에서도 업로드 요청은 30초대에서 PROCESSING 상태로 반환되었고, 실제 완료까지는 약 80초 정도가 걸렸다.

이 결과를 보고 동기 응답으로 완료 결과를 바로 돌려주는 구조는 어렵다고 판단했다.
현재 구조에서는 요청을 받은 뒤 작업 상태를 반환하고, 클라이언트가 결과를 다시 조회하는 비동기 흐름이 더 자연스럽다.

처음 의심했던 병목들

처음에는 여러 가능성을 열어두고 봤다.

API 서버의 대기 정책
게이트웨이 라우팅 지연
파일 업로드 구간
작업 서버의 CPU 한계
이미지 크기와 전처리 비용
모델 초기화 비용
결과 파싱 로직

그중 실제로 가장 강하게 의심된 것은 모델 초기화 비용이었다.

작업 중 CPU 사용률을 확인했을 때 처리 중에는 거의 한 코어를 꽉 쓰고 있었다.
단순히 요청이 밀린다기보다는, 한 번의 추론 작업 자체가 무겁고 그 앞뒤로 초기화 비용까지 붙어 있는 형태에 가까웠다.

요청마다 새 프로세스가 생성되고 있었다

코드를 확인해보니 작업 서버는 요청을 받을 때마다 별도의 Python 프로세스를 실행하고 있었다.

흐름은 대략 이랬다.

worker server
  -> request
  -> temporary image file
  -> new python subprocess
  -> import model package
  -> create model object
  -> run inference
  -> return json through stdout
  -> subprocess exit

즉, 서버 프로세스가 살아 있어도 모델 객체가 계속 유지되는 구조가 아니었다.
요청이 들어올 때마다 새로운 프로세스가 뜨고, 그 안에서 모델 패키지를 import하고, 모델 객체를 다시 만들고, 추론을 끝내면 프로세스가 종료되는 방식이었다.

처음에는 이 구조가 단순하고 격리 측면에서는 안전하다고 볼 수도 있다고 생각했다.
특히 네이티브 라이브러리를 사용하는 모델 추론에서는 프로세스를 분리해두면 장애가 전체 서버로 번지는 것을 줄일 수 있다.

하지만 운영 기준으로 보면 이 비용이 너무 컸다.
매 요청마다 모델을 새로 준비하는 구조라면, 실제 추론 시간 외에도 초기화 비용이 계속 반복된다.

개선 방향

이번에는 기본 실행 방식을 in-process 구조로 바꾸었다.

서버가 시작될 때 모델 객체를 한 번 생성하고, 이후 요청에서는 같은 모델 객체를 재사용하도록 했다.
다만 모델 객체를 여러 요청이 동시에 건드리는 상황은 위험할 수 있어서 lock을 두어 한 번에 하나의 추론만 실행되도록 했다.

또 기존 subprocess 방식도 완전히 제거하지 않았다.
운영 중 네이티브 라이브러리 문제나 비정상 종료가 발생할 수 있기 때문에, 환경 변수로 다시 subprocess 방식으로 되돌릴 수 있게 남겨두었다.

정리하면 개선 방향은 이렇다.

기본값은 in-process 실행
모델은 서버 시작 시 한 번 로드
요청마다 모델 객체 재사용
동시 접근은 lock으로 제한
문제가 생기면 subprocess 방식으로 롤백 가능

테스트와 배포 확인

로컬 테스트는 모두 통과했다.

하지만 여기서 끝내면 안 된다는 것도 다시 확인했다.
코드를 push했다고 해서 운영 환경이 바로 바뀐 것은 아니었다.

운영 서버를 다시 확인해보니 여전히 이전 이미지가 실행 중이었다.
컨테이너도 오래 떠 있었고, 새 커밋 기준의 이미지 태그도 레지스트리에 올라와 있지 않았다.

결국 이번 작업에서 한 번 더 분명해진 점은 이것이다.

git push 완료와 운영 배포 완료는 전혀 다른 상태다.

코드는 수정되었지만, 실제 운영 반영 여부는 컨테이너 이미지와 실행 중인 프로세스를 기준으로 다시 확인해야 한다.

정리

오늘 작업은 단순히 성능이 느리다는 느낌을 확인한 것이 아니라, 운영 환경에서 병목을 좁혀간 과정이었다.

처음에는 API나 네트워크 문제일 수 있다고 생각했지만, 측정 결과 작업 서버 내부의 모델 실행 구조가 핵심이었다.
특히 요청마다 새 프로세스를 띄우고 모델을 다시 준비하는 구조가 큰 비용을 만들고 있었다.

이번 개선으로 모델을 재사용하는 구조를 만들었고, 동시에 기존 실행 방식으로 되돌릴 수 있는 경로도 남겨두었다.

다음에 이어서 확인해야 할 것은 CI/CD 쪽이다.
새 코드가 실제 운영 이미지로 빌드되고 배포되는지까지 확인해야 이번 성능 개선이 운영 환경에 반영되었다고 말할 수 있다.

댓글남기기