aircloud 컨테이너에 ssh로 접속하여 영상 전처리 작업을 돌리던 중, SSH 연결이 반복적으로 끊기는 문제를 겪었다.
에러 메시지는 다음과 같다. WebSocket closed: 1006.
로그도 제대로 남지 않아 처음엔 막막했지만, 하나씩 단서를 좁혀가며 원인을 찾아낸 과정을 정리했다.
환경
- aircloud 컨테이너 (Ubuntu 24.04, RTX 4090, 16코어, 62GB RAM)
- SSH 접속 방식: CLI가 로컬에 터널을 열고(127.0.0.1:2222), 서버의 ssh-proxy와 WebSocket으로 연결한 뒤 그 위로 SSH를 태우는 구조
- VS Code Remote-SSH로 터널에 접속해서 개발
- 작업 내용: ffmpeg 다수 + Python 전처리 스크립트를 동시 실행하는 영상 전처리
일반적인 SSH와 달리 중간에 WebSocket 터널이 하나 더 끼어 있다는 점이 이 문제의 핵심 배경이다.
VS Code → localhost:2222 (SSH) → WebSocket 터널 → 서버 ssh-proxy → 컨테이너
증상
- 작업 중 SSH 세션이 예고 없이 끊긴다
- 터미널에는 WebSocket closed: 1006만 출력된다
- 재접속하면 즉시 정상 연결된다 (컨테이너와 서버 자체는 멀쩡함)
- VS Code는 reload하면 바로 복구된다
진단 과정
1. 1006의 의미
WebSocket 종료 코드 1006은 "정상 종료 절차(close frame) 없이 연결이 뚝 끊겼다"는 뜻이다. 클라이언트나 서버가 스스로 연결을 닫은 게 아니라, 중간의 무언가(프록시, 로드밸런서, 네트워크)가 일방적으로 연결을 잘랐다는 신호다.
2. idle timeout 가설
가장 흔한 원인은 프록시의 유휴 타임아웃이다.
nginx 같은 프록시는 proxy_read_timeout(기본 60초) 동안 데이터가 흐르지 않으면 WebSocket을 끊는다. 대응으로 ~/.ssh/config에 keepalive를 추가했다.
Host my-container
HostName localhost
Port 2222
User root
IdentityFile ~/.ssh/id_ed25519
ServerAliveInterval 30
ServerAliveCountMax 3
30초마다 keepalive 패킷을 보내면 "데이터 없음" 상태가 되지 않아 유휴 타임아웃을 방지할 수 있다. 하지만 활발히 작업 중에도 끊기는 상황이었기에, idle timeout만으로는 설명이 되지 않았다. (그래도 이 설정은 기본으로 넣어두는 것을 추천한다.)
3. CPU는 놀고 있는데 load는 폭주
htop을 열어보니 이상한 그림이 보였다.
- CPU 코어별 사용률: 각 50% 수준 - 여유 있음
- 메모리: 12.7G / 61.9G - 여유 충분 (OOM 아님)
- GPU: 55% - 정상
- Load average: 14 / 22 / 20 - 16코어 시스템에서 심각한 초과
CPU는 절반밖에 안 쓰는데 load average가 코어 수를 훌쩍 넘는다는 것은, 프로세스들이 CPU 연산이 아니라 무언가를 기다리며 줄 서 있다는 뜻이다. load average는 "실행 중 + 실행 대기 + I/O 대기(uninterruptible sleep)" 프로세스의 수이기 때문이다.
즉, 병목은 CPU가 아니라 I/O였다.
4. df -hT로 확인 - NFS
프로세스들이 무엇을 기다리는지 확인하기 위해 파일시스템 구성을 살펴봤다.
$ df -hT
Filesystem Type Size Used Avail Mounted on
overlay overlay 913G 286G 581G /
100.101.0.22:/vol-41648af0 nfs4 295G 60G 220G /workspace
작업 스크립트와 영상 데이터가 모두 있는 /workspace가 NFS, 즉 네트워크 스토리지였다. ffmpeg 여러 개가 동시에 읽고 쓰던 수 GB짜리 영상 파일들의 I/O가 전부 네트워크를 타고 있었던 것이다.
/workspace도 경로상으로는 / 아래의 폴더지만, 리눅스에서 경로 구조와 실제 저장 장치는 별개다. 리눅스는 모든 경로를 하나의 트리로 통일하되, 트리의 특정 폴더에 다른 장치를 마운트(접붙이기)할 수 있다. 겉보기엔 같은 복도에 나란히 있는 문이지만, 어떤 문은 같은 건물의 방으로, 어떤 문은 네트워크 건너 다른 건물로 이어진다. 어느 폴더가 어느 장치인지는 df -hT <경로>로 언제든 확인할 수 있다.
5. 원인 파악

- ffmpeg 다수가 NFS에서 대용량 영상을 동시에 읽고 쓴다
- 이 트래픽이 컨테이너의 네트워크 회선을 포화시킨다
- SSH의 WebSocket 터널도 같은 네트워크 회선을 쓴다
- 포화된 회선에서 WebSocket의 ping/pong 응답이 밀린다
- 서버 쪽 프록시가 "응답 없음"으로 판단하고 연결을 자른다 → 1006
도로에 비유하면, NFS 트래픽은 도로를 가득 메운 화물 트럭이고 SSH의 ping/pong은 그 사이를 오가야 하는 오토바이다. 도로가 트럭으로 막히면 오토바이가 제시간에 도착하지 못하고, 검문소는 오토바이가 안 온다는 이유로 통행증을 취소해버린다.
CPU 50% + load 20이라는 모순처럼 보이던 지표도 이 시나리오로 설명된다. 프로세스들은 NFS 응답을 기다리며 줄 서 있었던 것이다.
해결 방법
1. 데이터를 로컬 디스크로 옮겨서 처리
다행히 루트(/)는 로컬 디스크였고 여유 공간도 충분했다. 작업 구조를 다음과 같이 바꾼다.
기존: NFS에서 직접 읽기 → 처리 → NFS에 쓰기 (모든 I/O가 네트워크 통과)
변경: NFS → 로컬로 배치 복사 → 로컬에서 처리 → 결과만 NFS로 복사
mkdir -p /root/work/corpus
cp -r /workspace/project/data/corpus/vlog /root/work/corpus/
# 스크립트의 입력 경로를 /root/work/... 로 변경 후 실행
처리 중 발생하는 무수한 랜덤 I/O가 전부 로컬 디스크에서 일어나고, 네트워크는 시작과 끝의 순차 복사 때만 쓴다. 네트워크 부담이 크게 줄 뿐 아니라, NFS의 레이턴시가 사라져 처리 속도 자체도 빨라지는 경우가 많다.
주의: 클라우드 컨테이너의 로컬 디스크는 컨테이너 재생성 시 사라질 수 있다. 원본과 최종 결과물은 반드시 영속 볼륨(NFS)에 두고, 로컬은 처리 중인 작업 공간으로만 쓸 것.
2. 동시 실행 개수 줄이기
스크립트 구조를 바꾸기 어렵다면 ffmpeg/워커의 동시 실행 개수를 절반으로 줄이는 것만으로도 효과가 있다.
NFS 요청이 시간축으로 분산되어 네트워크에 숨 쉴 틈이 생긴다. 총 처리량 손해도 생각보다 크지 않은데, 어차피 병목이 네트워크라 동시 실행을 늘려봤자 서로 기다리기만 했기 때문이다.
3. tmux 또는 screen은 필수 (피해 최소화)
어떤 대책을 쓰든, 장시간 작업은 반드시 tmux(또는 screen) 세션 안에서 실행한다.
tmux new -s preprocess
# 작업 실행 후 Ctrl+b, d 로 세션 분리
# 재접속 후 tmux attach -t preprocess 로 복귀
SSH가 끊겨도 프로세스는 계속 돌고, 재접속하면 화면 그대로 복귀할 수 있다. 끊김 자체를 못 막더라도 실질적인 피해가 사라진다.
4. keepalive 설정
앞서 언급한 ServerAliveInterval 30 설정은 이번 문제의 직접 해결책은 아니었지만, 유휴 타임아웃으로 인한 끊김을 막아주므로 기본으로 넣어두는 것이 좋다.
검증 방법
가설을 확정하려면 작게 실험하면 된다.
- 데이터 일부만 로컬로 복사해서 같은 강도로 작업 실행
- SSH 끊김이 사라지는지, load average가 코어 수 이하로 내려오는지 관찰
- iostat -x 2 5의 %iowait이나 top의 wa 값으로 I/O 대기 감소 확인
로컬 처리로도 여전히 끊긴다면 원인은 다른 곳(프록시/LB의 최대 연결 시간 설정, ssh-proxy 프로세스 불안정 등)에 있다는 뜻이므로, 인프라 담당자에게 close code 1006과 끊긴 시각 기록을 함께 전달해 서버 쪽 확인을 요청하면 된다.
배운 것들
- 에러는 가장 먼저 발생한 레이어에서 읽는다. paramiko 예외는 결과였고, 원인은 그 위에 찍힌 WebSocket 1006이었다.
- WebSocket 1006 = 중간자가 연결을 잘랐다. 클라이언트/서버 코드보다 프록시, LB, 네트워크를 먼저 의심할 것.
- CPU 사용률과 load average의 괴리는 I/O 병목의 신호다. CPU가 놀고 있는데 load가 높다면 프로세스들은 디스크나 네트워크를 기다리고 있다.
- 경로와 장치는 별개다. 같은 / 트리 안에 있어도 마운트에 따라 로컬 디스크일 수도, 네트워크 건너 스토리지일 수도 있다. df -hT가 그 지도를 보여준다.
- 네트워크 스토리지 위에서 I/O 헤비 작업을 돌리면, 그 대가는 스토리지 성능만이 아니라 같은 회선을 쓰는 모든 것(SSH 포함)이 치른다.