Preparing workspace
← 목록으로

EC2에서 분석 작업이 멈춘 이유: t3.micro보다 먼저 확인한 Docker 디스크 부족

CloudDocker

2026.08.13

들어가며

Vercel 웹 앱에서 ZIP 파일을 올리면 EC2의 분석 API와 worker가 받아 정적 분석을 수행하는 구조를 운영하고 있다.

작은 샘플은 정상적으로 끝났다.
그런데 27MB 정도의 ZIP부터는 화면이 계속 소스를 받는 중...에 머물렀다.

처음에는 가장 싼 사양의 t3.micro, Supabase Free 요금제, Docker healthcheck를 차례로 의심했다.
하지만 API와 worker는 모두 healthy였고, 실제 원인은 훨씬 단순했다.

8GiB 루트 EBS 사용량 100%
→ crawler SQLite가 파일을 열지 못함
→ 작업은 ingesting 10%에서 더 진행하지 못함

이 글은 문제를 어떻게 구분했는지, 운영 중인 Docker 데이터를 지우지 않고 공간을 확보한 과정, 그리고 EC2를 끄지 않고 EBS와 XFS 파일시스템을 확장한 과정을 정리한다.



1. 증상: 작은 파일은 되고, 27MB ZIP부터 멈췄다

웹 UI에는 아래 문구가 보였다.

소스를 받는 중...

분석 작업 테이블을 확인하니 최신 작업은 다음 상태였다.

항목
statusrunning
stageingesting
progress10
error code/messageNULL

이 상태는 업로드가 끝났다는 뜻이 아니었다.
분석 worker가 signed ZIP을 내려받고, 압축을 해제한 뒤, 검사할 작업 디렉터리를 만드는 초반 단계였다.

27MB라는 파일 크기만 보고 “서버 메모리가 부족한가?”라고 판단하기 쉽다.
하지만 압축 파일은 실제 분석 전에 임시 파일과 풀린 파일을 추가로 만든다.
특히 ZIP은 압축률에 따라 원본보다 훨씬 큰 공간을 사용할 수 있다.

먼저 worker가 살아 있는지 확인했다.

curl -fsS http://127.0.0.1:8081/v1/ready

응답은 정상이었다.

{
  "ok": true,
  "database": true,
  "workers": {
    "active": 1,
    "stale": 0,
    "required": true
  }
}

ready endpoint가 성공한다고 작업이 성공한다는 뜻은 아니다.
다만 worker 프로세스가 죽었거나, SQLite를 아예 열 수 없거나, 큐가 즉시 막힌 상태와는 구분할 수 있었다.



2. healthcheck가 정상인데도 작업이 실패한 이유

실제 로그에는 아래 오류가 있었다.

sqlite3.OperationalError: unable to open database file

처음에는 Docker volume 소유자와 컨테이너 user를 의심했다.

api_id="$(./scripts/production.sh ps -q api)"

docker inspect "$api_id" \
  --format '{{range .Mounts}}{{printf "%s %s -> %s\n" .Type .Name .Destination}}{{end}}'

docker exec "$api_id" sh -lc '
  id
  ls -ldn /data
  df -h /data
  df -i /data
  test -w /data && echo "DATA_WRITABLE=yes" || echo "DATA_WRITABLE=no"
'

컨테이너의 /data volume은 서비스 계정 UID/GID로 소유되어 있었고, 쓰기 권한도 있었다.

문제는 권한이 아니라 호스트 전체 디스크였다.

df -h /
df -i /
df -h /var/lib/docker
docker system df

결과는 다음과 같았다.

/dev/nvme0n1p1  8.0G  7.9G  51M  100% /

inode는 남아 있었지만 block storage가 사실상 바닥난 상태였다.
Docker named volume은 /var/lib/docker 아래에 있으므로, volume만 작아 보여도 루트 EBS가 차면 SQLite WAL 파일, 임시 파일, 로그를 더 쓸 수 없다.

Docker volume 쓰기 권한 정상
+ host root disk 100%
= SQLite의 "unable to open database file"

이 경우 t3.micro의 1GiB 메모리는 분명 여유가 없는 사양이지만, 이번 장애를 직접 일으킨 증거는 아니었다.
Supabase Free도 분석 worker가 EC2 로컬 SQLite에 쓰지 못한 원인이 아니었다.

원인과 개선 후보를 섞지 않는 것이 중요했다.

구분이번 장애의 직접 원인이후 검토할 개선
디스크8GiB EBS 100%루트 EBS 30GiB로 확장
메모리직접 증거 없음동시 분석 증가 시 t3.medium 검토
Supabase 요금제직접 원인 아님DB/Storage 사용량 별도 모니터링


3. 먼저 확보한 공간: image와 build cache만 정리

처음 디스크 점검에서 Docker가 차지한 공간은 다음과 같았다.

docker system df
Images       13 total, 1.612GB, reclaimable 1.425GB
Build Cache  52 entries, reclaimable 23.71MB

현재 실행 중인 컨테이너의 image와 volume은 건드리지 않고, 사용하지 않는 image와 build cache만 제거했다.

docker image prune -a -f
docker builder prune -a -f

df -h /
docker system df

정리 뒤 루트 EBS는 약 793MB까지 여유가 생겼다.

8.0G 중 7.2G 사용, 793MB 여유

서비스를 잠시 복구하기에는 도움이 됐지만, ZIP을 받고 풀고 분석하기에는 여전히 불안한 공간이었다.

이때 하지 않은 것

다음 명령은 이번 목적에 맞지 않았다.

# 실행하지 않았다.
docker compose down -v
docker system prune --volumes

-v--volumes는 안 쓰는 volume만 지우는 것처럼 보여도, 판단을 잘못하면 crawler의 SQLite 데이터나 애플리케이션 데이터를 잃을 수 있다.
공간이 급해도 먼저 docker system df로 reclaimable 대상을 확인하고, volume은 별도 백업과 확인 뒤에 다루는 편이 안전하다.



4. EBS는 EC2와 Docker를 끄지 않고 확장할 수 있다

8GiB 루트 볼륨은 Docker image, build cache, 로그, SQLite, 분석용 임시 파일을 함께 담기에는 너무 작았다.
그래서 gp3 root EBS를 30GiB로 늘렸다.

이번에는 EC2를 중지하거나 Docker를 내리지 않았다.
EBS volume 확장은 온라인으로 요청할 수 있고, 확장 뒤에는 서버에서 partition과 파일시스템을 따로 늘려야 한다.

AWS 콘솔에서는 다음 순서로 진행했다.

EC2
→ Elastic Block Store
→ Volumes
→ 인스턴스에 attached 된 root volume 선택
→ Actions
→ Modify volume
→ Size를 30GiB로 변경

루트 볼륨을 확실하게 고르려면 인스턴스 화면의 Storage 탭에서 Volume ID를 확인하는 것이 가장 안전하다.
여러 개의 8GiB volume이 있다면, 크기만 보고 고르면 안 된다.

AWS 콘솔에서 volume 상태가 optimizing 또는 completed로 바뀐 뒤, 서버에서 디스크 크기를 확인했다.

lsblk
df -T /

확장 직후에는 disk만 30GiB이고 root partition과 파일시스템은 여전히 8GiB였다.

nvme0n1       30G  disk
└─nvme0n1p1    8G  part /

Amazon Linux 2023의 root filesystem이 XFS인 것을 확인한 뒤 다음 명령을 실행했다.

sudo growpart /dev/nvme0n1 1
sudo xfs_growfs -d /
df -h /

결과는 다음과 같았다.

/dev/nvme0n1p1   30G  7.4G   23G  25% /

growpart는 partition 1을 실제 disk 끝까지 넓히고, xfs_growfs는 그 넓어진 partition을 XFS filesystem에 반영한다.
둘 중 하나만 해서는 df -h /의 여유 공간이 늘지 않는다.

만약 df -T /의 type이 ext4라면 마지막 단계는 달라진다.

sudo resize2fs /dev/nvme0n1p1

filesystem 종류를 확인하지 않고 xfs_growfsresize2fs를 실행하면 안 된다.
또 확장 작업에는 mkfs가 필요하지 않다. 기존 filesystem을 포맷하는 명령이므로 운영 root volume에 실행하면 안 된다.



5. 복구 뒤 확인할 순서

EBS 확장만으로 API와 worker를 재시작할 필요는 없었다.
그래도 이전에 멈춘 작업이 자동으로 다시 진행되는지는 별도 문제다. 새 작업을 한 번 요청하고, 상태를 계층별로 확인했다.

# crawler API / worker 준비 상태
curl -fsS http://127.0.0.1:8081/v1/ready

# host storage 여유
df -h /

# 실행 중인 서비스
./scripts/production.sh ps

# 최근 crawler 로그
./scripts/production.sh logs --tail 200 api
./scripts/production.sh logs --tail 200 worker

작업 테이블에서는 다음을 본다.

submitting
→ running / ingesting
→ running / analyzing
→ done / 100

작업이 다시 ingesting에서 멈춘다면 그때는 새 로그와 disk 사용량을 같이 확인한다.

df -h /
docker system df
docker stats --no-stream

디스크가 충분한데도 실패한다면 다음 후보를 분리해서 본다.

증상다음 확인 지점
API가 5xx 반환API log, signed URL, Hub 인증 키
worker가 없음/v1/ready, Compose service, queue
압축 해제 중 실패ZIP 크기·파일 수·확장 비율 제한, worker log
분석 중 OOMdocker stats, EC2 memory, dmesg/OOM log
상태 callback 실패Hub API URL, Bearer key, crawler delivery queue


마치며

이번 장애에서 가장 위험했던 부분은 Docker healthcheck가 정상이어서 서버가 멀쩡하다고 생각하기 쉬웠다는 점이다.

실제로는 worker가 떠 있고 HTTP readiness도 성공했지만, root EBS가 100%라 분석 과정에서 필요한 SQLite와 임시 파일을 만들 수 없었다.

운영 서버에서는 최소한 아래 세 가지를 함께 봐야 한다.

1. 서비스 상태: healthcheck, ready endpoint, queue worker
2. Docker 상태: image/cache/volume별 reclaimable 공간
3. 호스트 상태: df -h, inode, memory, OOM log

디스크가 거의 찬 뒤에 한 번씩 image를 지우는 방식은 임시 대응일 뿐이다.
분석 서버처럼 파일을 내려받고 압축을 푸는 워크로드라면, 처음부터 image·로그·임시 파일을 감안한 EBS 용량을 잡고 사용량 알림도 준비해야 한다.

이번에는 30GiB로 확장해 약 23GiB의 여유를 확보했다.
다음에는 70~80% 수준에서 먼저 알림을 받고, 100%가 되기 전에 원인을 확인하는 쪽으로 운영 기준을 바꾸려고 한다.

Ranohub

0%