Skip to main content

Command Palette

Search for a command to run...

교통모니터링 프로젝트에서 겪은 FTP Passive Mode 포트 고갈 문제

ThreadPool 기반 동시 연결 수 제어로 FTP포트 풀 고갈 해결하기

Updated
교통모니터링 프로젝트에서 겪은 FTP Passive Mode 포트 고갈 문제
K

backend developer interested in technical problem

이탈리아 베로나의 교통 혼잡 구역 모니터링 프로젝트에서 교통데이터를 FTP로 전송하는 시스템을 운영하며 겪었던 Passive Mode 포트 범위 초과 문제와 해결 과정을 공유합니다.

아래는 모니터링 대상인 교통 혼잡지인 Porta Nuova입니다. 베로나 포르타 누오바(Verona Porta Nuova)는 이탈리아 베로나의 주요 기차역입니다. 베로나 중심부를 운행하는 두 역 중 하나입니다.(위키백과)

배경

프로젝트 개요

이탈리아 베로나에 교통 모니터링용 레이더를 설치하는 프로젝트를 진행했습니다. 여러대의 레이더가 한대의 PC에 데이터를 로깅하고, 이 PC가 파일시스템 이벤트를 감지하여 데이터를 NAS로 FTP를 통해 전송하는 시스템이었습니다.

당시에 보안상 다음과 같은 제약이 있었습니다.

  • Verona의 보안 정책상 특정 포트 범위만 허용

  • NAS 접근 가능 포트: 50203-50213 (11개 포트 1개는 ftp연결, 10개는 전송용)

  • 특정 포트범위만 포트포워딩이 되어있기 때문에 ftp passive모드를 통해 전송

  • 방화벽에서 해당 포트 외에는 모두 차단

FTP Passive Mode란?

FTP는 제어 연결(보통 21번포트)과 데이터 연결을 분리하여 사용하는데, 데이터 연결 방식에 따라 Active Mode와 Passive Mode로 나뉩니다.

Active Mode

  1. 클라이언트 → 서버 (포트 21): 제어 연결

  2. 클라이언트 → 서버: PORT 명령으로 데이터 수신 포트 전달

  3. 서버 → 클라이언트: 서버가 클라이언트로 데이터 연결 시도

Active mode는 클라이언트 방화벽이 서버의 인바운드 연결을 차단할 수 있습니다.

Passive Mode (프로젝트에서 사용한 모드)

  1. 클라이언트 → 서버 (포트 21): 제어 연결

  2. 클라이언트 → 서버: PASV 명령

  3. 서버 → 클라이언트: 227 Entering Passive Mode

  4. 클라이언트 → 서버 (포트 p1*256+p2): 데이터 연결

모든 연결이 클라이언트에서 시작되므로 방화벽 설정이 단순합니다.

프로젝트에서는 보안 정책상 Passive Mode를 사용했고, NAS에서 50203-50213 포트를 Passive Mode용으로 할당했습니다.

문제

증상찾기

프로젝트 초기에는 정상 동작했으나, 본격적인 데이터 수집이 시작되면서 전송 실패가 빈번하게 발생했습니다. 다음은 당시 로그입니다. 뒤에서 이야기하겠지만 일반적인 상황에서 나타나는 문제는 아닙니다. 부하 상황을 가정해 데이터를 적게는 수십GB정도 축적해두고 전송을 시작했을때만 나타나는 증상입니다.

ERROR - TimeoutError: Connect call failed ('94.84.172.10', 34272)
ERROR - TimeoutError: Connect call failed ('94.84.172.10', 46095)
ERROR - TimeoutError: Connect call failed ('94.84.172.10', 42009)

주요 증상:

  • TimeoutError: [Errno 10060] Connect call failed 에러 반복 발생

  • 재시도 로직이 계속 작동 (Retrying in 1 seconds...)

  • 전송 성공과 실패가 불규칙하게 발생

로그를 보니 다음과 같은 문제가 있었습니다.

  • 설정된 Passive 포트 범위: 50203-50213

  • 실제 연결 시도 포트: 34272, 46095, 42009 ... 의 무작위 포트 값 Passvie mode로 전송 시 지정한 포트 범위를 벗어난 포트로 연결 시도하는 문제로 에러가 나타났습니다.

원인 분석

설정 오류

설정이 잘 못 적용되었을 수가 있으니 NAS의 관리자 계정으로 들어가서 FTP 서버 설정을 확인했습니다 하지만 Passive Mode 포트 범위가 50203-50213으로 정확히 설정되어 있었습니다.

포트 풀 고갈 현상

로그를 보니 원인을 찾을 수 있었습니다. 네트워크 장애를 가정해 며칠간 쌓아놓은 데이터를 한번에 보내는 상황을 만들고자 데이터를 축적 후 전송시키기 위해 애플리케이션을 구동시켰을때 처리 대기열에 pc 한 대당 약 80만개의 파일이 쌓였습니다. 그리고 여러대의 pc에서 밀린 파일을 보내고자 하니 FTP연결 요청이 폭주하게 되고, 지정해놓은 포트범위를 모두 사용중이기에 포트가 고갈되는 현상이 발생했던 것입니다. 때문에 전체 요청중 약 15%는 실패하게 되었습니다. 아래 스크린샷으로 큐에 약 88만개의 파일이 대기중인것을 볼 수 있습니다. 포트가 고갈되면 서버는 에러를 응답할 줄 알았는데 무작위 포트를 할당해 줬던 것은 예상 밖이였습니다.

예상밖의 서버 동작

일반적으로 FTP 서버라면 포트가 부족할 때 연결 요청을 큐에 대기시키고, 포트가 해제될 때까지 기다립니다. 혹은 명시적으로 에러를 응답으로 보내죠. (421 Too many connections)

하지만 당시 사용한 FTP 서버는 설정 범위를 무시하고 임의의 포트를 할당했고, 클라이언트는 방화벽에 차단되어 요청이 실패했습니다(타임아웃)

의도된 로직인지는 모르겠지만 예상과 달리 동작해서 단번에 원인을 알수 없었던 것입니다.

해결 방법 검토

방법 1: 포트 범위 확장

포트범위를 확장하려면 베로나의 인프라를 담당하는 네트워크 업체에 연락을 해야했고 일처리가 상당히 느린 편이였습니다. 한가지 요청을하면 해결되기까지 적어도 2주는 걸리는, 한국인 입장에서는 속이 터지는 속도였습니다. 또한 근본적인 해결책은 될 수가 없는게 일시적 완화일 뿐, 여전히 포트 범위 밖으로 할당하는 문제는 발생할 수 있습니다.

(채택) 방법 2: 클라이언트 측 전송 제어

서버를 변경하지 않고, 클라이언트 측에서 전송속도를 제어하여 서버 부하 감소

서버 포트 풀 고갈 원인은 동시 연결 요청이 과다한 것이기 때문에, 클라이언트가 동시 연결 수를 제한해 서버에 폭주하는 FTP 요청을 줄여 포트 풀에 여유를 확보했습니다. 현실적으로 유일하게 실행 가능한 방법이었고, 오히려 클라이언트 측에서 제어함으로써 더 안정적인 시스템을 만들 수 있다고 판단했습니다. 이를위해 클라이언트쪽 코드를 수정했고 초기 설정값을 찾기위해 테스트를 진행했습니다.

구현 상세

FTP 포트 풀 고갈 문제는 동시 연결 수 제어로 해결했습니다. 추가로 클라이언트 PC의 안정적인 운영을 위해 CPU 기반 속도 조절도 적용했습니다.

우선 yaml에 설정할 값을 추가해줬습니다.

app:
  performance:
    max-concurrent-transfers: 3   # PC당 2-3개 (PC 4대, 실제 동시 최대 ~10개)
    max-cpu-percent: 80           # CPU 임계값
    cpu-throttle: 10              # 과부하 시 지연 (ms)

1. 동시 전송 수 제어로 포트 고갈 방지

여러 대의 PC가 동시에 FTP 서버로 파일을 전송하면 각 전송마다 하나의 Passive Mode 포트를 점유하게 됩니다. 서버는 11개의 포트만 사용할 수 있는데, 제어 없이 전송하면 PC당 수십 개의 연결을 시도하므로 포트가 즉시 고갈됩니다.

이를 해결하기 위해 각 PC가 동시에 사용할 수 있는 전송 워커 수를 2-3개로 제한했습니다. PC가 4대 있다면 최대 12개의 연결만 발생하고, 실제로는 모든 PC가 정확히 동시에 최대치를 사용하지 않기 때문에 평균 10개 수준의 연결이 유지됩니다. 이렇게 하면 11개의 포트 범위 내에서 안정적으로 운영할 수 있습니다.

ThreadPoolExecutor의 corePoolSizemaxPoolSize를 동일하게 설정하면 스레드 수가 동적으로 변하지 않는 고정 크기 풀이 생성됩니다. 이렇게 하면 정확히 설정한 개수만큼의 워커만 동작하므로, 각 PC가 점유하는 포트 수를 확실하게 제한할 수 있습니다.

포트 범위 초과 문제가 해결되었고, 전송 성공율이 96% 를 기록해 안정적인 운영이 가능해졌습니다.


2. CPU 기반 동적 속도 조절로 클라이언트 PC 과부하 방지

과부하 방지 로직을 넣은김에 클라이언트쪽도 과부하를 방지하기위한 로직을 추가했습니다.

동시 전송 수를 제한해도 여러 워커가 동시에 파일을 읽고 네트워크로 전송하면 클라이언트 PC의 CPU에 과부하가 발생할 수 있습니다. 실제로 클라이언트 PC의 CPU가 85-95%까지 치솟으면서 PC 전체가 느려지고 다른 작업에 영향을 주었던 일이 있습니다(전송하는 PC의 스펙이 좋지 못한 문제도 있습니다.)

해결책으로 각 워커가 파일을 전송하기 전에 클라이언트 PC의 CPU 사용률을 체크하도록 했습니다. CPU가 80%를 초과하면 자동으로 10ms 대기한 후 전송을 시작합니다. 이렇게 하면 클라이언트 PC의 CPU 사용률이 높을 때 워커가 자동으로 속도를 늦춰서 PC가 다른 작업을 처리할 여유를 확보할 수 있습니다.

@Component
public class CpuMonitor {

    @Value("${app.performance.max-cpu-percent:80}")
    private double maxCpuPercent;

    @Value("${app.performance.cpu-throttle:10}")
    private long cpuThrottleMs;

    private final OperatingSystemMXBean osBean =
        ManagementFactory.getPlatformMXBean(OperatingSystemMXBean.class);

    public void throttleIfNeeded() throws InterruptedException {
        double cpuLoad = osBean.getCpuLoad() * 100;

        if (cpuLoad > maxCpuPercent) {
            Thread.sleep(cpuThrottleMs);  // 10ms 대기
        }
    }
}

워커 스레드는 다음과 같이 매 작업 전에 CPU 부하를 체크합니다:

private void transferWorker(int workerId) {
    while (running) {
        // 1. CPU 부하 조절
        cpuMonitor.throttleIfNeeded();

        // 2. 작업 가져오기
        TransferTask task = transferQueue.poll(500, TimeUnit.MILLISECONDS);

        // 3. 파일 전송
        if (task != null) {
            transferFile(task);
        }
    }
}

이 방식으로 클라이언트 PC의 CPU 사용률이 피크기준 85-95%에서 70%대로 감소했습니다.


마치며

  • FTP Passive Mode는 문서상으로는 단순해 보이지만 서버마다 구현 방식이 다르니 실제 구현체의 동작과 제약도 고려해야합니다.

  • FTP의 제약사항(포트범위만큼만 동시 전송이 가능)으로 인해 2차개발에서 MinIO기반 object storage로 대체하는 계기가 됐습니다.

  • 또한 문제를 해결하는데 로깅과 메트릭 수집이 중요하다는걸 깨닫고 프로젝트 2차 개발에서는 Prometheus와 Grafana를 도입해 모니터링을 하게되는 계기가 됐습니다.

참고 자료

14 views

More from this blog

rfc 959 읽어보면서 ftp이해하기

들어가며 프로젝트에서 FTP를 통해 파일을 전송하는 클라이언트를 개발했을 때 이야기입니다. 프로토콜에 대한 이해 없이 개발을 진행하다 보니 불필요하게 시간을 소모한 경험이 있어서 RFC 원문을 읽어보게 되었습니다. 읽게 된 계기 당시 FTP 프로토콜에 대한 이해가 부족한 상태로 개발을 시작했습니다. '제어를 위한 포트와 데이터 전송을 위한 포트가 분리되어 있다' 정도만 알고 있었고 Active 모드와 Passive 모드의 차이를 제대로 이해...

Dec 5, 20256
rfc 959 읽어보면서 ftp이해하기

mapstruct 로 보일러플레이트 코드 줄이기

배경 사내에서 교통 모니터링용 레이더 디바이스를 관리하는 API를 개발했을때 이야기입니다. 반복되는 코드에서 느꼈던 피로를 개선하고자 mapstruct를 적용해봤습니다. 실황 우선 일부 필드만 추출한 Device에 대해 간단히 이야기해야할 것 같습니다. 교통 모니터링용 레이더 디바이스로 신호등이나 가로등에 설치하며 설치된 좌표를 기록하는 위도, 경도와 레이더가 바라보는 방향을 의미하는 heading_angle필드 등이 있습니다. public...

Nov 13, 20259
mapstruct 로 보일러플레이트 코드 줄이기
T

takoyakisoba

38 posts