[카테고리:] 정보

  • 서버에서 502 오류 없이 PHP-FPM 프로세스가 멈추는 원인

    서버에서 502 오류 없이 PHP-FPM 프로세스가 멈추는 원인

    웹사이트가 평소보다 느려지거나 특정 페이지에서 응답이 끝나지 않는데도 502 Bad Gateway 오류는 발생하지 않는 상황이 있습니다. Nginx는 실행 중이고 PHP-FPM 서비스도 active 상태로 표시되지만, 실제로는 PHP 요청이 오랫동안 처리되지 않거나 일부 워커 프로세스가 특정 작업에서 멈춘 것처럼 보이는 경우입니다.

    이러한 현상은 PHP-FPM 자체가 완전히 종료된 경우와 다릅니다. PHP-FPM 프로세스 멈춤 문제는 데이터베이스 잠금 대기, 외부 API 응답 지연, 파일 I/O 정체, 워커 프로세스 부족, PHP 확장 모듈의 비정상 동작 등으로 발생할 수 있습니다.

    특히 서버 상태를 확인했을 때 프로세스가 살아 있다는 이유만으로 정상이라고 판단하면 장애 원인을 놓치기 쉽습니다. 프로세스의 존재 여부와 실제 요청 처리 가능 여부는 서로 다르기 때문입니다.

    이번 글에서는 502 오류가 나타나지 않는 이유부터 PHP-FPM 워커 상태, slowlog, 프로세스 풀 설정, 데이터베이스 대기 및 운영체제 로그까지 순서대로 확인합니다.

    PHP-FPM 프로세스 멈춤 핵심 명령어 한눈에 보기

    다음은 PHP 8.3을 사용하는 Ubuntu 서버의 예시입니다. 실제 서버에서는 PHP 버전과 서비스 이름, 설정 경로를 변경해야 합니다.

    점검 목적명령어
    PHP-FPM 서비스 상태systemctl status php8.3-fpm
    프로세스 목록ps -eo pid,ppid,stat,etime,%cpu,%mem,wchan:24,cmd | grep '[p]hp-fpm'
    서비스 로그journalctl -u php8.3-fpm -n 100
    프로세스 풀 설정grep -E '^(pm\.|request_|slowlog)' /etc/php/8.3/fpm/pool.d/www.conf
    PHP-FPM 설정 검사sudo php-fpm8.3 -tt
    메모리 상태free -h
    커널 OOM 로그journalctl -k -b | grep -Ei 'oom|out of memory|killed process'
    Nginx 오류 로그sudo tail -n 100 /var/log/nginx/error.log

    가장 먼저 확인할 것은 PHP-FPM 서비스가 실행 중인지가 아니라 워커 프로세스가 실제로 요청을 처리하고 있는지 여부입니다.

    502 오류 없이 PHP-FPM 프로세스가 멈추는 원인과 slowlog 워커 프로세스 점검 방법
    502 오류 없이 PHP-FPM 응답이 멈출 때 워커 상태, slowlog, 프로세스 풀과 데이터베이스 대기를 확인하는 방법

    1. PHP-FPM이 멈췄는데 502 오류가 발생하지 않는 이유

    PHP-FPM은 PHP 요청을 처리하는 FastCGI 프로세스 관리자입니다. Nginx나 Apache가 전달한 PHP 요청을 워커 프로세스가 실행한 뒤 처리 결과를 웹서버에 반환하는 구조입니다.

    그런데 PHP-FPM 워커가 특정 작업에서 오랫동안 대기하면 웹서버와의 연결이 즉시 끊어지지 않을 수 있습니다. 이 경우 요청은 완료되지 않았지만 아직 502 오류가 발생할 조건에도 도달하지 않은 상태일 수 있습니다.

    예를 들어 다음과 같은 상황이 발생할 수 있습니다.

    • PHP 요청이 DB 잠금 해제를 기다리는 경우
    • 외부 API 연결이나 응답을 장시간 기다리는 경우
    • 디스크 또는 네트워크 파일시스템의 I/O가 지연되는 경우
    • 모든 PHP-FPM 워커가 사용 중이어서 새로운 요청이 대기하는 경우
    • 웹서버의 FastCGI 타임아웃에 도달하기 전까지 요청이 유지되는 경우

    이후 타임아웃이 발생하면 502가 아닌 504 Gateway Timeout으로 표시될 수도 있습니다. 또한 브라우저나 프록시가 먼저 연결을 종료하면 사용자가 HTTP 오류 페이지를 보지 못할 수도 있습니다.

    따라서 502 오류가 없다는 사실만으로 PHP-FPM이 정상이라고 판단해서는 안 됩니다.

    2. PHP-FPM 프로세스가 실제로 실행 중인지 확인하기

    먼저 서비스 상태를 확인합니다.

    systemctl status php8.3-fpm

    서비스가 실행 중이라면 다음과 같이 표시될 수 있습니다.

    Active: active (running)

    하지만 이 상태는 PHP-FPM의 마스터 프로세스가 실행 중이라는 정보일 뿐, 모든 워커가 정상적으로 요청을 처리한다는 뜻은 아닙니다.

    프로세스 목록과 상태를 확인합니다.

    ps -eo pid,ppid,stat,etime,%cpu,%mem,wchan:24,cmd | grep '[p]hp-fpm'

    확인할 주요 항목은 다음과 같습니다.

    • STAT: 프로세스의 실행 또는 대기 상태
    • ETIME: 프로세스가 시작된 이후 경과 시간
    • %CPU: CPU 사용률
    • %MEM: 메모리 사용 비율
    • WCHAN: 커널에서 대기 중인 위치에 대한 단서

    특히 D 상태가 지속된다면 중단하기 어려운 커널 대기 상태에 있는지 확인해야 합니다. 디스크 또는 네트워크 파일시스템의 I/O 정체와 관련될 수 있습니다.

    다만 S 상태라고 해서 반드시 문제가 있는 것은 아닙니다. 정상적인 유휴 프로세스도 대기 상태로 표시될 수 있으므로 요청 처리 상황과 함께 해석해야 합니다.

    3. pm.max_children 부족으로 요청이 대기하는 경우

    PHP-FPM은 프로세스 풀 설정에 따라 동시에 실행할 수 있는 워커 수가 제한됩니다.

    대표적인 설정이 pm.max_children입니다.

    현재 설정을 확인합니다.

    sudo grep -E '^(pm|pm\.max_children|pm\.max_requests|pm\.start_servers|pm\.min_spare_servers|pm\.max_spare_servers)' \
      /etc/php/8.3/fpm/pool.d/www.conf

    설정 예시는 다음과 같습니다.

    pm = dynamic
    pm.max_children = 20
    pm.start_servers = 4
    pm.min_spare_servers = 2
    pm.max_spare_servers = 6

    이 환경에서 20개의 워커가 모두 장시간 실행되는 요청을 처리하고 있다면 새로운 요청이 대기할 수 있습니다.

    PHP-FPM 로그에 다음과 같은 메시지가 나타날 수도 있습니다.

    server reached pm.max_children setting (20),
    consider raising it

    그러나 pm.max_children을 무조건 높이는 것은 올바른 해결 방법이 아닙니다.

    워커 수를 늘리면 메모리 사용량도 증가하므로 현재 워커의 실제 메모리 사용량과 서버의 가용 메모리를 확인해야 합니다.

    또한 워커가 DB 잠금이나 외부 API 대기 때문에 점유되어 있다면 워커 수를 늘려도 근본적인 문제가 남을 수 있습니다.

    4. PHP-FPM slowlog로 오래 실행되는 PHP 코드 찾기

    PHP-FPM 프로세스가 멈춘 것처럼 보이는 원인을 분석할 때 가장 유용한 기능 중 하나가 slowlog입니다.

    slowlog는 설정한 시간을 초과해 실행되는 PHP 요청의 실행 위치를 기록하는 기능입니다.

    PHP-FPM 풀 설정 파일에 다음과 같은 항목을 지정할 수 있습니다.

    request_slowlog_timeout = 5s
    slowlog = /var/log/php-fpm/www-slow.log

    위 경로는 예시이며, PHP-FPM 워커와 서비스가 해당 로그 경로에 기록할 수 있도록 디렉터리와 권한이 준비되어 있어야 합니다.

    설정을 적용하기 전에 문법을 검사합니다.

    sudo php-fpm8.3 -tt

    문제가 없다면 운영 환경의 변경 절차에 따라 PHP-FPM 서비스를 재로드할 수 있습니다.

    sudo systemctl reload php8.3-fpm

    이후 느린 요청이 발생하면 slowlog에 PHP 함수 호출 위치와 실행 흐름이 기록될 수 있습니다.

    예를 들어 DB 쿼리 호출이나 HTTP 요청 함수에서 오랫동안 대기하는 상황을 찾는 데 도움이 됩니다.

    단, slowlog는 모든 종류의 커널 대기나 네이티브 확장 모듈 내부 문제를 완벽하게 보여주는 도구는 아닙니다.

    5. DB 잠금과 느린 쿼리로 PHP 요청이 멈추는 경우

    PHP 애플리케이션이 MySQL이나 MariaDB를 사용한다면 데이터베이스 쿼리 대기를 확인해야 합니다.

    특히 트랜잭션이 잠금을 오래 유지하거나 느린 쿼리가 반복되면 PHP 워커가 DB 응답을 기다리면서 요청 처리 시간이 길어질 수 있습니다.

    MySQL에서는 다음 명령어로 현재 실행 중인 세션을 확인할 수 있습니다.

    SHOW FULL PROCESSLIST;

    MySQL 8.0 환경에서는 InnoDB 잠금 대기를 다음 뷰로 확인할 수도 있습니다.

    SELECT *
    FROM sys.innodb_lock_waits;

    다만 해당 뷰의 사용 가능 여부는 MySQL 버전과 sys 스키마 구성에 따라 달라집니다.

    확인해야 할 항목은 다음과 같습니다.

    • 장시간 실행 중인 쿼리가 있는지
    • 잠금을 기다리는 트랜잭션이 있는지
    • 인덱스 부족으로 쿼리가 느려지는지
    • DB 연결 수가 한계에 도달했는지
    • DB 서버의 CPU·디스크 사용량이 과도한지

    잠금 대기가 확인되더라도 원인을 파악하지 않고 DB 세션을 강제로 종료하면 트랜잭션 롤백이나 서비스 오류가 발생할 수 있습니다.

    6. 외부 API와 네트워크 연결 지연 확인하기

    PHP 코드에서 외부 API를 호출하는 경우 상대 서버의 응답 지연이 PHP-FPM 워커 점유로 이어질 수 있습니다.

    예를 들어 결제 API, 인증 서버, 외부 데이터 조회, SMTP 연결 등이 영향을 줄 수 있습니다.

    PHP cURL을 사용하는 애플리케이션에서는 연결 제한 시간과 전체 요청 제한 시간을 명시적으로 설정하는 것이 좋습니다.

    $ch = curl_init('https://api.example.com/data');
    
    curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 3);
    curl_setopt($ch, CURLOPT_TIMEOUT, 10);
    
    $response = curl_exec($ch);
    
    if ($response === false) {
        error_log(curl_error($ch));
    }
    
    curl_close($ch);

    위 코드는 진단용 예시이며 실제 서비스에서는 오류 처리, 재시도 정책, 응답 코드 검증을 추가해야 합니다.

    외부 연결을 점검할 때는 PHP-FPM 워커가 특정 원격 주소에 연결되어 있는지도 확인할 수 있습니다.

    sudo ss -tnp

    다만 네트워크 연결이 존재한다는 사실만으로 해당 연결이 장애 원인이라고 단정할 수는 없습니다. 애플리케이션 로그와 요청 처리 시간을 함께 분석해야 합니다.

    7. 메모리 부족과 OOM으로 PHP-FPM 워커가 종료되는 경우

    PHP-FPM이 정상적으로 실행되는 것처럼 보여도 일부 워커가 메모리 부족으로 종료되고 새 워커가 생성되는 상황이 반복될 수 있습니다.

    현재 메모리 상태를 확인합니다.

    free -h

    커널 로그에서 OOM 관련 메시지를 검색합니다.

    sudo journalctl -k -b | grep -Ei 'oom|out of memory|killed process'

    OOM 로그에서 PHP-FPM 워커가 종료된 기록이 있다면 프로세스별 메모리 사용량과 메모리 제한을 확인해야 합니다.

    특히 컨테이너나 cgroup으로 관리되는 서버에서는 호스트 메모리가 충분해 보여도 서비스에 적용된 메모리 제한으로 인해 워커가 종료될 수 있습니다.

    또한 PHP의 memory_limit는 개별 PHP 스크립트의 메모리 사용 제한과 관련된 설정이며, PHP-FPM 전체 프로세스 풀의 메모리 사용량을 직접 제한하는 값은 아닙니다.

    8. PHP-FPM 상태 페이지로 워커 포화 확인하기

    PHP-FPM의 pm.status_path 기능을 사용하면 프로세스 풀의 상태를 더 구체적으로 확인할 수 있습니다.

    풀 설정에 다음과 같은 항목을 지정할 수 있습니다.

    pm.status_path = /fpm-status

    상태 페이지에서는 다음 정보를 확인할 수 있습니다.

    • active processes: 현재 요청을 처리 중인 워커 수
    • idle processes: 유휴 워커 수
    • listen queue: 처리 대기 중인 연결 수
    • max children reached: 워커 수 한계에 도달한 횟수
    • slow requests: slowlog 기준 시간을 초과한 요청 수

    단, pm.status_path를 설정하는 것만으로 외부 HTTP 요청이 자동으로 연결되는 것은 아닙니다. Nginx 또는 Apache에서 해당 경로를 PHP-FPM으로 전달하도록 별도의 웹서버 설정이 필요합니다.

    상태 페이지에는 내부 운영 정보가 포함될 수 있으므로 외부 인터넷에 공개하지 않고 로컬 또는 관리자 전용으로 제한해야 합니다.

    또한 기본 풀의 모든 워커가 점유된 상황에서는 같은 풀을 통해 제공하는 상태 페이지도 응답하지 않을 수 있습니다. 필요하다면 PHP-FPM의 별도 상태 리스너 구성을 검토할 수 있습니다.

    9. Nginx 로그에서 502 없이 발생하는 응답 지연 확인하기

    PHP-FPM 문제가 발생하더라도 Nginx 오류 로그에 항상 502 메시지가 기록되는 것은 아닙니다.

    먼저 오류 로그를 확인합니다.

    sudo tail -n 100 /var/log/nginx/error.log

    다음과 같은 메시지가 나타날 수 있습니다.

    upstream timed out (110: Connection timed out)
    while reading response header from upstream

    이 메시지는 Nginx가 업스트림 응답 헤더를 기다리다가 설정된 제한 시간에 도달했다는 의미입니다.

    이런 상황에서는 일반적으로 504 응답이 발생할 수 있지만, 실제 클라이언트가 받은 결과는 연결 종료 시점과 프록시 구성에 따라 달라질 수 있습니다.

    또한 Nginx 액세스 로그에 $request_time과 $upstream_response_time이 기록되도록 구성하면 요청 처리 시간을 비교할 수 있습니다.

    예를 들어 다음과 같은 로그 형식을 사용할 수 있습니다.

    log_format upstream_timing
      '$remote_addr "$request" '
      'status=$status '
      'request_time=$request_time '
      'upstream_time=$upstream_response_time';

    위 설정은 Nginx의 http 컨텍스트에서 정의하는 예시입니다. 실제 기록에는 액세스 로그에서 해당 형식을 사용하도록 추가 설정해야 합니다.

    로그 설정을 변경하기 전에는 반드시 기존 설정과 운영 환경을 확인해야 합니다.

    10. PHP 확장 모듈과 파일 I/O 정체 확인하기

    일부 PHP 확장 모듈이나 네이티브 라이브러리의 문제도 PHP-FPM 워커가 응답하지 않는 원인이 될 수 있습니다.

    예를 들어 이미지 처리, 압축, 데이터베이스 드라이버, 외부 라이브러리 호출 과정에서 비정상적인 대기나 오류가 발생할 수 있습니다.

    로드된 PHP 확장 모듈은 다음 명령어로 확인할 수 있습니다.

    php -m

    다만 CLI PHP와 PHP-FPM은 서로 다른 설정 파일을 사용할 수 있으므로 php -m 결과만으로 FPM의 실제 확장 구성을 확정해서는 안 됩니다.

    PHP-FPM 설정 파일과 추가 설정 로딩 상태도 함께 확인해야 합니다.

    sudo php-fpm8.3 -i | grep -E 'Loaded Configuration File|Scan this dir'

    프로세스가 파일 I/O에서 대기하는지 확인할 때는 프로세스 상태와 커널 로그를 함께 분석해야 합니다.

    특히 NFS 또는 네트워크 스토리지를 사용하는 경우에는 파일 접근 지연이 PHP 요청 지연으로 이어질 수 있습니다.

    이때 서비스 재시작만 반복하면 일시적으로 증상이 사라지더라도 스토리지나 외부 시스템의 문제가 해결되지 않을 수 있습니다.

    실전 점검 순서

    502 오류 없이 PHP-FPM 프로세스가 멈춘 것처럼 보인다면 다음 순서로 확인합니다.

    # 1. PHP-FPM 서비스 상태
    systemctl status php8.3-fpm
    
    # 2. 워커 프로세스 상태
    ps -eo pid,ppid,stat,etime,%cpu,%mem,wchan:24,cmd | grep '[p]hp-fpm'
    
    # 3. PHP-FPM 서비스 로그
    sudo journalctl -u php8.3-fpm -n 100
    
    # 4. 프로세스 풀 설정
    sudo grep -E '^(pm\.|request_|slowlog)' \
      /etc/php/8.3/fpm/pool.d/www.conf
    
    # 5. PHP-FPM 설정 문법 검사
    sudo php-fpm8.3 -tt
    
    # 6. 메모리 상태
    free -h
    
    # 7. OOM 로그
    sudo journalctl -k -b | grep -Ei 'oom|out of memory|killed process'
    
    # 8. Nginx 오류 로그
    sudo tail -n 100 /var/log/nginx/error.log
    
    # 9. 네트워크 연결
    sudo ss -tnp
    
    # 10. PHP-FPM 확장 모듈 정보
    sudo php-fpm8.3 -i | grep -E 'Loaded Configuration File|Scan this dir'

    위 명령어는 PHP 8.3과 Ubuntu 계열을 기준으로 작성한 예시입니다. 실제 서버에서는 PHP 버전과 배포판에 맞게 변경해야 합니다.

    문제가 발생한 시점의 로그와 프로세스 상태를 먼저 확보한 후 설정 변경이나 서비스 재시작을 진행하는 것이 좋습니다.

    정리

    PHP-FPM 프로세스 멈춤 현상은 502 오류가 발생하지 않아도 나타날 수 있습니다.

    PHP-FPM 서비스가 실행 중이더라도 워커가 데이터베이스 잠금, 외부 API 응답, 파일 I/O 또는 장시간 실행되는 PHP 코드에서 대기한다면 실제 요청 처리가 지연될 수 있습니다.

    우선 프로세스 상태와 PHP-FPM 로그를 확인하고, pm.max_children과 slowlog를 통해 워커 포화와 장시간 실행 요청을 분석해야 합니다.

    메모리 부족과 OOM 로그, DB 잠금, Nginx 응답 시간도 함께 확인하면 문제가 발생하는 구간을 더 정확하게 좁힐 수 있습니다.

    핵심 점검 순서는 PHP-FPM 서비스 상태 확인 → 워커 프로세스 분석 → slowlog 확인 → 프로세스 풀 설정 점검 → DB·API·파일 I/O 대기 분석 → 메모리 및 웹서버 로그 확인입니다.

  • 리눅스 서버에서 Mount Failed 오류가 발생할 때 확인해야 할 설정

    리눅스 서버에서 Mount Failed 오류가 발생할 때 확인해야 할 설정

    리눅스 서버에 새로운 디스크를 연결하거나 /etc/fstab 설정을 변경한 뒤 Mount Failed 오류가 발생하면 저장소를 사용하는 서비스까지 중단될 수 있습니다. 특히 서버를 재부팅한 이후 기존 디스크가 연결되지 않거나 NFS 공유 디렉터리에 접근하지 못하는 경우에는 설정 오류와 실제 저장장치 문제를 구분해야 합니다.

    마운트 실패는 하나의 고정된 오류 메시지가 아닙니다. 실제로는 wrong fs type, special device does not exist, mount point does not exist, access denied by server처럼 원인에 따라 서로 다른 메시지가 표시됩니다.

    이때 무작정 마운트 명령어를 반복하거나 파일시스템 복구를 시도하기보다 장치 인식 상태, UUID, fstab 설정, 파일시스템 유형, 마운트 옵션 및 시스템 로그를 순서대로 확인하는 것이 중요합니다.

    Mount Failed 핵심 명령어 한눈에 보기

    점검 항목명령어
    디스크와 파일시스템lsblk -f
    UUID 확인sudo blkid
    현재 마운트 상태findmnt
    fstab 설정cat /etc/fstab
    fstab 검증sudo findmnt --verify --verbose
    장치 서명 확인sudo file -s /dev/sdb1
    커널 로그sudo journalctl -k -b
    실패한 systemd 유닛systemctl --failed
    NFS 마운트 목록findmnt -t nfs,nfs4

    주의: 마운트 오류가 발생했다고 해서 mkfs, fsck -y, xfs_repair -L 같은 명령어를 바로 실행해서는 안 됩니다. 저장된 데이터와 파일시스템 상태를 먼저 확인해야 합니다.

    리눅스 Mount Failed 오류 원인과 fstab UUID 파일시스템 및 NFS 설정 점검
    리눅스 Mount Failed 오류 발생 시 UUID, fstab, 파일시스템 유형 및 네트워크 마운트 설정을 확인하는 방법

    1. Mount Failed 오류가 발생하는 주요 원인

    리눅스에서 마운트는 디스크 파티션이나 네트워크 파일시스템을 특정 디렉터리에 연결하는 작업입니다. 장치 정보나 마운트 설정이 실제 환경과 일치하지 않으면 마운트가 실패할 수 있습니다.

    • UUID 불일치: fstab에 등록된 UUID와 실제 파일시스템 UUID가 다른 경우
    • 장치 경로 변경: 디스크 연결 순서가 바뀌어 /dev/sdb1 등의 이름이 변경된 경우
    • 파일시스템 유형 오류: ext4, XFS, Btrfs 등의 유형을 잘못 지정한 경우
    • 마운트 지점 문제: 대상 디렉터리가 없거나 올바르지 않은 경우
    • 마운트 옵션 충돌: 파일시스템에서 지원하지 않는 옵션을 지정한 경우
    • 파일시스템 손상: 슈퍼블록 또는 메타데이터에 문제가 발생한 경우
    • NFS 연결 문제: 네트워크, 공유 경로, Export 권한이 잘못된 경우
    • 부팅 순서 문제: 장치나 네트워크가 준비되기 전에 마운트가 시작된 경우

    특히 동일한 오류 메시지가 여러 원인에서 나타날 수 있으므로 마운트 명령어의 출력과 커널 로그를 함께 확인해야 합니다.

    2. lsblk와 blkid로 실제 디스크 상태 확인하기

    가장 먼저 운영체제가 저장장치를 정상적으로 인식하는지 확인합니다.

    lsblk -f

    출력 예시는 다음과 같습니다.

    NAME   FSTYPE UUID                                  MOUNTPOINTS
    sda
    └─sda1 ext4   11111111-2222-3333-4444-555555555555 /
    sdb
    └─sdb1 ext4   aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee

    여기서 FSTYPE은 파일시스템 유형, UUID는 파일시스템 식별자, MOUNTPOINTS는 현재 연결된 디렉터리입니다.

    추가로 다음 명령어를 실행합니다.

    sudo blkid /dev/sdb1

    예시 결과:

    /dev/sdb1: UUID="aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee" TYPE="ext4"

    실제 UUID가 fstab에 기록된 값과 다르다면 해당 설정을 수정해야 합니다.

    또한 장치 자체가 lsblk에 표시되지 않는다면 파일시스템 설정에 앞서 디스크 연결, 가상머신 볼륨 연결 상태 또는 스토리지 장치 인식 문제를 확인해야 합니다.

    3. /etc/fstab 설정 확인하기

    /etc/fstab은 서버에서 사용할 파일시스템의 자동 마운트 정보를 관리하는 설정 파일입니다.

    cat /etc/fstab

    다음은 일반적인 ext4 마운트 설정 예시입니다.

    UUID=aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee /mnt/data ext4 defaults 0 2
    설정 항목의미
    UUID연결할 파일시스템 식별자
    /mnt/data마운트 대상 경로
    ext4파일시스템 유형
    defaults기본 마운트 옵션
    0dump 백업 필드
    2부팅 시 파일시스템 검사 관련 필드

    fstab 설정을 검증하려면 다음 명령어를 사용합니다.

    sudo findmnt --verify --verbose

    이 명령어는 설정 형식과 장치 참조를 점검하는 데 도움이 되지만, 실제 마운트 성공을 보장하지는 않습니다.

    설정을 변경하기 전에는 백업 파일을 만들어 두는 것이 좋습니다.

    sudo cp -a /etc/fstab /etc/fstab.bak

    특히 루트 파일시스템이나 중요한 데이터 디스크의 fstab 설정을 잘못 변경하면 재부팅 시 복구 모드로 진입하거나 서비스가 시작되지 않을 수 있습니다.

    4. 마운트 지점과 파일시스템 유형 확인하기

    마운트 대상 디렉터리가 존재하지 않으면 다음과 같은 오류가 발생할 수 있습니다.

    mount: /mnt/data: mount point does not exist.

    먼저 디렉터리를 확인합니다.

    ls -ld /mnt/data

    새로운 마운트 지점이 필요한 상황이라면 경로를 확인한 뒤 생성할 수 있습니다.

    sudo mkdir -p /mnt/data

    다만 기존 파일이 존재하는 디렉터리에 다른 파일시스템을 마운트하면 원래 디렉터리의 내용이 마운트된 동안 가려질 수 있습니다.

    파일시스템 유형은 다음 명령어로 확인합니다.

    lsblk -f /dev/sdb1
    sudo file -s /dev/sdb1

    실제 파일시스템이 XFS인데 fstab에 ext4로 지정했다면 유형 불일치로 마운트가 실패할 수 있습니다.

    장치에 파일시스템이 없거나 암호화·LVM 계층이 존재하는 경우에는 올바른 논리 장치가 무엇인지 먼저 확인해야 합니다.

    5. wrong fs type과 bad superblock 오류 확인하기

    마운트 실패 시 다음과 같은 메시지가 나타날 수 있습니다.

    mount: /mnt/data: wrong fs type, bad option,
    bad superblock on /dev/sdb1,
    missing codepage or helper program, or other error.

    이 메시지는 파일시스템 유형 오류, 지원하지 않는 옵션, 필요한 도구 누락 또는 파일시스템 손상 등 여러 원인에서 발생할 수 있습니다.

    먼저 커널 로그를 확인합니다.

    sudo journalctl -k -b | grep -Ei 'ext4|xfs|btrfs|superblock|I/O error|mount'

    예를 들어 다음과 같은 메시지가 나타날 수 있습니다.

    EXT4-fs (sdb1): VFS: Can't find ext4 filesystem

    이 경우 실제 파일시스템이 ext4인지, 올바른 장치를 선택했는지, 메타데이터에 문제가 있는지 확인해야 합니다.

    반면 다음과 같은 오류가 나타난다면 스토리지 장치 자체의 문제를 의심할 수 있습니다.

    I/O error, dev sdb, sector 2048

    I/O 오류가 반복된다면 쓰기 작업을 최소화하고 저장장치 상태와 백업을 우선 확인해야 합니다.

    6. NFS Mount Failed 오류 확인하기

    NFS 공유 디렉터리에서 마운트 오류가 발생한다면 로컬 디스크와 다른 항목을 점검해야 합니다.

    먼저 현재 NFS 마운트 상태를 확인합니다.

    findmnt -t nfs,nfs4

    NFS 서버 연결 상태도 확인합니다.

    ping -c 4 192.0.2.10
    nc -vz -w 3 192.0.2.10 2049

    위 IP 주소는 예시입니다. 또한 ping이 실패했다고 해서 반드시 NFS 연결이 불가능한 것은 아닙니다. ICMP 차단 여부와 NFS 서비스 상태를 구분해야 합니다.

    NFSv4 공유 경로를 연결하는 예시는 다음과 같습니다.

    sudo mount -t nfs4 192.0.2.10:/exports/data /mnt/shared

    NFS 서버에서 공유 설정을 확인할 수 있다면 다음 명령어를 사용합니다.

    sudo exportfs -v

    access denied by server while mounting 오류가 발생한다면 클라이언트 IP 주소가 허용되어 있는지, Export 경로와 NFS 버전이 맞는지 확인해야 합니다.

    7. systemd 부팅 과정에서 Mount Failed가 발생하는 경우

    수동 마운트는 성공하지만 서버 재부팅 후에만 실패한다면 systemd 마운트 유닛과 장치 준비 순서를 확인해야 합니다.

    systemctl --failed

    마운트 경로에 대응하는 systemd 유닛 이름을 확인합니다.

    systemd-escape -p --suffix=mount /mnt/data

    출력 예시:

    mnt-data.mount

    유닛 상태와 부팅 로그를 확인합니다.

    systemctl status mnt-data.mount
    journalctl -b -u mnt-data.mount

    네트워크 파일시스템에서는 _netdev 옵션을 검토할 수 있습니다.

    192.0.2.10:/exports/data /mnt/shared nfs4 defaults,_netdev 0 0

    또한 x-systemd.automount를 사용하면 접근 시점에 마운트를 시도하도록 구성할 수 있습니다.

    다만 nofail 옵션은 마운트 실패가 부팅에 미치는 영향을 줄일 수 있을 뿐, 저장소 연결 문제를 해결하지는 않습니다.

    데이터베이스나 중요한 애플리케이션이 해당 저장소에 의존한다면 마운트가 완료되기 전에 서비스가 실행되지 않도록 서비스 의존성도 함께 검토해야 합니다.

    8. 파일시스템 복구 전 확인해야 할 사항

    Mount Failed 오류가 발생했다고 해서 파일시스템이 반드시 손상된 것은 아닙니다.

    파일시스템 복구 도구는 종류에 따라 다르며, 마운트된 상태에서 잘못 실행하면 데이터 손상을 일으킬 수 있습니다.

    먼저 파일시스템 유형을 확인합니다.

    lsblk -f

    ext4는 일반적으로 e2fsck, XFS는 xfs_repair 같은 전용 도구를 사용합니다.

    그러나 복구 명령어를 실행하기 전에는 대상 장치가 정확한지, 마운트가 해제되어 있는지, 백업이나 스냅샷을 확보할 수 있는지 확인해야 합니다.

    특히 mkfs는 기존 파일시스템을 복구하는 명령어가 아니라 새로운 파일시스템을 생성하는 명령어이므로 기존 데이터가 있는 장치에 실행해서는 안 됩니다.

    실전 점검 순서

    리눅스 서버에서 Mount Failed 오류가 발생하면 다음 순서로 확인하는 것이 좋습니다.

    # 1. 디스크 및 파일시스템 확인
    lsblk -f
    
    # 2. UUID 확인
    sudo blkid
    
    # 3. 현재 마운트 상태 확인
    findmnt
    
    # 4. fstab 설정 확인
    cat /etc/fstab
    
    # 5. fstab 검증
    sudo findmnt --verify --verbose
    
    # 6. 마운트 대상 디렉터리 확인
    ls -ld /mnt/data
    
    # 7. 실제 파일시스템 서명 확인
    sudo file -s /dev/sdb1
    
    # 8. 커널 오류 확인
    sudo journalctl -k -b | grep -Ei 'mount|ext4|xfs|superblock|I/O error'
    
    # 9. 실패한 systemd 유닛 확인
    systemctl --failed
    
    # 10. NFS 마운트 확인
    findmnt -t nfs,nfs4

    /dev/sdb1과 /mnt/data는 실제 서버의 장치와 마운트 경로로 변경해야 합니다.

    원인을 확인하기 전에는 디스크를 포맷하거나 파일시스템 복구 명령어를 무작정 실행하지 않아야 합니다.

    정리

    리눅스 서버에서 Mount Failed 오류가 발생하면 먼저 디스크가 정상적으로 인식되는지 확인하고, 실제 UUID와 /etc/fstab 설정이 일치하는지 점검해야 합니다.

    장치와 UUID가 정상이라면 파일시스템 유형, 마운트 옵션, 대상 디렉터리 및 커널 로그를 확인해야 합니다.

    NFS 환경에서는 서버 연결과 Export 설정을, 부팅 시에만 실패하는 환경에서는 systemd 유닛과 장치·네트워크 준비 순서를 추가로 살펴봐야 합니다.

    핵심 점검 순서는 장치 인식 확인 → UUID 및 fstab 검증 → 파일시스템·마운트 옵션 점검 → 커널 로그 분석 → NFS 또는 systemd 확인 → 안전한 복구입니다.

  • Apache 서버에서 .htaccess 설정이 적용되지 않는 원인과 해결 방법

    Apache 서버에서 .htaccess 설정이 적용되지 않는 원인과 해결 방법

    Apache 웹서버에서 .htaccess 파일을 수정했는데도 리디렉션이 작동하지 않거나, URL 재작성 규칙이 적용되지 않고, 접근 제한 설정이 무시되는 현상이 발생할 수 있습니다. 파일을 여러 번 수정해도 결과가 달라지지 않는다면 규칙 자체보다 Apache가 해당 파일을 읽고 있는지 먼저 확인해야 합니다.

    특히 워드프레스 고유주소를 변경한 뒤 404 오류가 발생하거나, HTTP를 HTTPS로 전환하는 규칙이 작동하지 않는 경우에는 .htaccess 설정과 Apache 서버 설정을 함께 점검해야 합니다.

    .htaccess 설정 미적용 문제는 AllowOverride None, mod_rewrite 비활성화, 잘못된 VirtualHost, 파일 위치 및 권한 문제, RewriteRule 작성 오류 등으로 발생할 수 있습니다.

    이번 글에서는 Apache 실행 환경 확인 → AllowOverride 설정 점검 → mod_rewrite 활성화 여부 확인 → VirtualHost와 파일 경로 분석 → RewriteRule 및 오류 로그 확인 순서로 원인을 살펴봅니다.

    Apache .htaccess 핵심 명령어 한눈에 보기

    Apache 설정을 변경하기 전에 현재 실행 중인 서버와 설정 파일의 위치를 확인해야 합니다. 다음은 Apache 2.4 환경에서 사용할 수 있는 대표적인 점검 명령어입니다.

    점검 목적 명령어
    Apache 버전 확인 apache2 -v 또는 httpd -v
    Apache 설정 파일 확인 apache2ctl -V 또는 apachectl -V
    활성 VirtualHost 확인 apache2ctl -S 또는 httpd -S
    활성 모듈 확인 apache2ctl -M 또는 httpd -M
    mod_rewrite 확인 apache2ctl -M | grep rewrite
    설정 문법 검사 sudo apache2ctl configtest
    파일 위치 및 권한 ls -la /var/www/html/.htaccess
    Apache 오류 로그 sudo journalctl -u apache2 -n 100

    배포판에 따라 Apache 명령어와 서비스 이름이 다를 수 있습니다. Ubuntu·Debian 계열에서는 주로 apache2, RHEL·Rocky Linux·AlmaLinux 계열에서는 httpd를 사용합니다.

    Apache .htaccess 설정 미적용 원인과 AllowOverride mod_rewrite VirtualHost 점검 방법
    Apache .htaccess 설정이 적용되지 않을 때 AllowOverride, mod_rewrite, VirtualHost 및 오류 로그를 확인하는 방법

    1. Apache가 .htaccess 파일을 읽지 않는 이유

    .htaccess는 Apache에서 디렉터리별 설정을 적용하기 위해 사용하는 파일입니다. 리디렉션, URL 재작성, 접근 제어 등 서버에서 허용한 설정을 디렉터리 단위로 지정할 수 있습니다.

    하지만 .htaccess 파일을 생성했다고 해서 Apache가 반드시 해당 파일을 읽는 것은 아닙니다.

    Apache의 AllowOverride 설정에 따라 디렉터리별 설정 파일을 읽을지 여부와 허용할 지시어 범위가 결정됩니다.

    예를 들어 다음과 같은 설정이 적용되어 있다면 .htaccess 파일은 처리되지 않습니다.

    <Directory "/var/www/html">
        AllowOverride None
        Require all granted
    </Directory>

    이 상태에서는 .htaccess에 RewriteRule이나 Redirect 규칙을 작성하더라도 해당 파일을 통한 설정이 적용되지 않습니다.

    따라서 규칙을 수정하기 전에 현재 사이트의 실제 DocumentRoot와 그 경로에 적용되는 Directory 설정을 확인해야 합니다.

    2. AllowOverride 설정 확인 및 수정하기

    Apache 2.4에서 AllowOverride는 .htaccess 사용 여부를 결정하는 핵심 설정입니다.

    Ubuntu·Debian 계열에서는 다음 경로를 우선 확인할 수 있습니다.

    /etc/apache2/apache2.conf
    /etc/apache2/sites-available/

    RHEL 계열에서는 다음 경로를 확인할 수 있습니다.

    /etc/httpd/conf/httpd.conf
    /etc/httpd/conf.d/

    관련 설정을 검색하려면 다음 명령어를 사용할 수 있습니다.

    sudo grep -RniE 'AllowOverride|AllowOverrideList|AccessFileName' \
      /etc/apache2/

    RHEL 계열에서는 검색 경로를 /etc/httpd/로 변경하면 됩니다.

    RewriteRule과 같은 FileInfo 계열 지시어를 .htaccess에서 사용해야 한다면 다음과 같이 필요한 범위만 허용할 수 있습니다.

    <Directory "/var/www/html">
        AllowOverride FileInfo
        Require all granted
    </Directory>

    이 설정은 FileInfo 범주에 해당하는 .htaccess 지시어를 허용하는 예시입니다.

    반면 다음 설정은 여러 종류의 디렉터리별 지시어를 허용합니다.

    AllowOverride All

    워드프레스 운영 환경에서 AllowOverride All을 사용하는 사례가 있지만, 모든 서버에서 반드시 All을 사용해야 하는 것은 아닙니다.

    보안과 관리 측면에서는 실제 필요한 지시어 범위만 허용하는 것이 좋습니다. 또한 AllowOverride는 서버 설정의 <Directory> 컨텍스트에 지정해야 하며, .htaccess 파일 내부에 작성하는 설정이 아닙니다.

    3. mod_rewrite 모듈 활성화 여부 확인하기

    .htaccess 파일은 읽히지만 RewriteRule이 작동하지 않는다면 mod_rewrite 모듈을 확인해야 합니다.

    Ubuntu·Debian 계열에서는 다음 명령어로 확인합니다.

    apache2ctl -M | grep rewrite

    정상적으로 로드되어 있다면 다음과 같은 결과가 나타날 수 있습니다.

    rewrite_module (shared)

    모듈이 활성화되어 있지 않다면 Ubuntu·Debian 계열에서 다음 명령어를 사용할 수 있습니다.

    sudo a2enmod rewrite

    RHEL 계열에서는 다음과 같이 확인할 수 있습니다.

    httpd -M | grep rewrite

    RHEL 계열은 Apache 패키지와 설정 구성에 따라 모듈 로드 방식이 다를 수 있으므로 현재 모듈 설정을 확인해야 합니다.

    mod_rewrite가 활성화되어 있어도 AllowOverride에서 해당 지시어를 허용하지 않으면 .htaccess의 RewriteRule이 적용되지 않을 수 있습니다.

    따라서 두 설정을 함께 점검해야 합니다.

    4. .htaccess 파일 위치와 DocumentRoot 확인하기

    Apache가 정상적으로 설정되어 있어도 .htaccess 파일이 실제 웹사이트 경로와 다른 위치에 있다면 원하는 규칙이 적용되지 않을 수 있습니다.

    먼저 현재 VirtualHost 구성을 확인합니다.

    apache2ctl -S

    RHEL 계열에서는 다음 명령어를 사용할 수 있습니다.

    httpd -S

    예를 들어 사이트 설정이 다음과 같다고 가정하겠습니다.

    <VirtualHost *:80>
        ServerName example.com
        DocumentRoot /var/www/example/public
    </VirtualHost>

    이 경우 웹사이트 루트에 적용할 .htaccess 파일은 일반적으로 다음 경로에 위치해야 합니다.

    /var/www/example/public/.htaccess

    하지만 파일을 /var/www/html/.htaccess에 작성했다면 해당 사이트에는 적용되지 않을 수 있습니다.

    실제 파일 위치를 확인합니다.

    ls -la /var/www/example/public/.htaccess

    또한 Apache는 요청한 파일 경로와 관련된 디렉터리 계층의 설정을 처리할 수 있으므로, 상위 디렉터리에 있는 설정 파일과 Directory 설정도 함께 확인해야 합니다.

    5. .htaccess 파일 권한과 소유권 확인하기

    파일 권한이 잘못되어 Apache 프로세스가 .htaccess를 읽지 못하면 설정 적용에 문제가 발생할 수 있습니다.

    다음 명령어로 권한을 확인합니다.

    ls -l /var/www/example/public/.htaccess

    일반적인 권한 예시는 다음과 같습니다.

    -rw-r--r-- 1 root root 512 Oct 8 10:00 .htaccess

    이 예시에서 파일 권한은 644입니다. 다만 실제 접근 가능 여부는 파일뿐 아니라 상위 디렉터리의 실행 권한, ACL 및 SELinux 설정에도 영향을 받을 수 있습니다.

    상위 디렉터리 권한을 확인하려면 다음 명령어를 사용할 수 있습니다.

    namei -l /var/www/example/public/.htaccess

    SELinux가 활성화된 환경이라면 보안 컨텍스트도 확인할 수 있습니다.

    ls -Z /var/www/example/public/.htaccess

    권한 문제를 해결한다는 이유로 chmod 777을 적용하는 것은 권장되지 않습니다.

    실제 Apache 실행 사용자와 디렉터리 접근 경로를 확인한 후 필요한 최소 권한만 부여해야 합니다.

    6. RewriteRule과 RewriteBase 설정 오류 확인하기

    .htaccess 파일이 정상적으로 읽히더라도 RewriteRule 작성 방식이 잘못됐다면 원하는 URL로 연결되지 않을 수 있습니다.

    다음은 존재하지 않는 파일과 디렉터리에 대한 요청을 index.php로 전달하는 일반적인 예시입니다.

    <IfModule mod_rewrite.c>
    RewriteEngine On
    
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule ^ index.php [L]
    </IfModule>

    위 예시는 사이트 구조에 따라 수정해야 합니다.

    RewriteEngine On은 URL 재작성 기능을 활성화하며, RewriteCond는 규칙 적용 조건을 지정합니다.

    !-f는 요청 경로가 실제 일반 파일이 아닌 경우, !-d는 실제 디렉터리가 아닌 경우를 의미합니다.

    특히 .htaccess의 RewriteRule 패턴은 일반적인 서버 설정의 URL 패턴과 해석 방식이 다릅니다. 디렉터리별 컨텍스트에서는 해당 디렉터리의 URL 접두사가 제거된 경로를 기준으로 규칙이 평가됩니다.

    따라서 .htaccess에서 RewriteRule 패턴의 맨 앞에 /를 붙이면 예상과 다르게 동작할 수 있습니다.

    서브디렉터리에 설치된 애플리케이션에서는 RewriteBase가 필요한지 검토해야 하지만, 모든 RewriteRule에 반드시 필요한 설정은 아닙니다.

    7. 워드프레스 고유주소 변경 후 404 오류가 발생하는 경우

    워드프레스에서 고유주소 구조를 변경한 뒤 메인 페이지는 열리지만 개별 글이나 페이지에서 404 오류가 발생하는 경우가 있습니다.

    Apache 환경이라면 다음 항목을 확인해야 합니다.

    • 사이트의 DocumentRoot가 올바른지
    • 워드프레스 루트에 .htaccess 파일이 있는지
    • AllowOverride에서 RewriteRule 사용을 허용하는지
    • mod_rewrite가 활성화되어 있는지
    • 다른 VirtualHost나 프록시가 요청을 처리하고 있지 않은지

    워드프레스가 웹 루트에 설치된 일반적인 환경에서는 다음과 같은 재작성 규칙이 사용될 수 있습니다.

    # BEGIN WordPress
    <IfModule mod_rewrite.c>
    RewriteEngine On
    RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}]
    RewriteBase /
    RewriteRule ^index\.php$ - [L]
    RewriteCond %{REQUEST_FILENAME} !-f
    RewriteCond %{REQUEST_FILENAME} !-d
    RewriteRule . /index.php [L]
    </IfModule>
    # END WordPress

    위 코드는 일반적인 루트 설치 환경의 예시이며, 워드프레스 설치 경로와 구성에 따라 달라질 수 있습니다.

    워드프레스 관리자 화면의 설정 → 고유주소에서 현재 구조를 확인하고 필요한 경우 변경사항을 저장해 재작성 규칙을 갱신할 수 있습니다.

    다만 Apache가 .htaccess를 읽지 않도록 설정되어 있다면 워드프레스에서 고유주소를 다시 저장해도 문제가 해결되지 않을 수 있습니다.

    8. Apache 오류 로그로 설정 문제 확인하기

    .htaccess가 적용되지 않는 원인을 확인하기 어려울 때는 Apache 오류 로그를 분석해야 합니다.

    Ubuntu·Debian 계열에서는 일반적으로 다음 경로를 확인합니다.

    sudo tail -n 100 /var/log/apache2/error.log

    RHEL 계열에서는 다음 경로를 확인할 수 있습니다.

    sudo tail -n 100 /var/log/httpd/error_log

    다만 VirtualHost별로 별도의 ErrorLog가 설정되어 있다면 해당 로그 파일을 확인해야 합니다.

    예를 들어 허용되지 않는 지시어가 .htaccess에 작성된 경우 다음과 비슷한 오류가 발생할 수 있습니다.

    RewriteEngine not allowed here

    이 메시지는 해당 디렉터리 컨텍스트에서 RewriteEngine 지시어를 사용할 수 없다는 의미입니다.

    반면 다음과 같은 메시지가 발생할 수도 있습니다.

    Invalid command 'RewriteEngine'

    이 경우에는 mod_rewrite 모듈이 로드되어 있는지 확인해야 합니다.

    실제 오류 문구는 Apache 버전과 설정에 따라 다를 수 있습니다.

    또한 .htaccess가 아예 무시되는 상황에서는 파일 내부의 잘못된 지시어가 로그에 나타나지 않을 수도 있습니다.

    9. Apache 설정 변경 후 적용되지 않는 경우

    .htaccess 파일은 일반적으로 요청 처리 과정에서 읽히므로 파일 내용만 수정한 경우 Apache를 반드시 재시작할 필요는 없습니다.

    하지만 AllowOverride, VirtualHost, 모듈 로드 설정 등 Apache의 서버 설정을 변경했다면 설정 검증 후 재로드가 필요할 수 있습니다.

    Ubuntu·Debian 계열에서는 다음 명령어로 문법을 검사합니다.

    sudo apache2ctl configtest

    RHEL 계열에서는 다음 명령어를 사용할 수 있습니다.

    sudo httpd -t

    설정이 정상이라면 다음과 같은 메시지가 나타날 수 있습니다.

    Syntax OK

    변경 내용을 적용하려면 운영 환경에 맞게 재로드합니다.

    # Ubuntu / Debian
    sudo systemctl reload apache2
    
    # RHEL / Rocky Linux / AlmaLinux
    sudo systemctl reload httpd

    재로드 후에도 설정이 적용되지 않는다면 실제 요청을 처리하는 웹서버가 Apache인지 확인해야 합니다.

    Nginx가 앞단에서 요청을 처리하거나 정적 파일을 직접 제공하는 환경에서는 Apache의 .htaccess 규칙이 해당 요청에 적용되지 않을 수 있습니다.

    특히 Nginx 자체는 .htaccess 파일을 해석하지 않으므로 Nginx만 사용하는 환경에서는 동등한 설정을 Nginx 서버 설정에 작성해야 합니다.

    실전 점검 순서

    Apache 서버에서 .htaccess 설정이 적용되지 않는다면 다음 순서로 점검할 수 있습니다.

    # 1. Apache 버전 확인
    apache2 -v
    
    # 2. VirtualHost 확인
    apache2ctl -S
    
    # 3. mod_rewrite 확인
    apache2ctl -M | grep rewrite
    
    # 4. AllowOverride 설정 검색
    sudo grep -RniE 'AllowOverride|AllowOverrideList|AccessFileName' /etc/apache2/
    
    # 5. .htaccess 파일 확인
    ls -la /var/www/html/.htaccess
    
    # 6. 상위 디렉터리 권한 확인
    namei -l /var/www/html/.htaccess
    
    # 7. Apache 설정 문법 검사
    sudo apache2ctl configtest
    
    # 8. Apache 오류 로그 확인
    sudo tail -n 100 /var/log/apache2/error.log
    
    # 9. 서버 설정을 변경했다면 재로드
    sudo systemctl reload apache2

    위 명령어는 Ubuntu·Debian 계열의 예시입니다. RHEL 계열에서는 httpd 명령어와 /etc/httpd/ 설정 경로를 사용해야 합니다.

    또한 /var/www/html은 예시 경로이므로 실제 VirtualHost의 DocumentRoot에 맞게 변경해야 합니다.

    운영 서버에서는 설정 파일을 수정하기 전에 원본을 백업하고, 변경 후 반드시 문법 검사를 진행해야 합니다.

    정리

    Apache 서버에서 .htaccess 설정 미적용 문제가 발생하는 원인은 AllowOverride None, mod_rewrite 비활성화, 잘못된 DocumentRoot, 파일 권한 문제, RewriteRule 오류 등 다양합니다.

    먼저 Apache가 실제로 .htaccess 파일을 읽도록 설정되어 있는지 확인하고, 해당 디렉터리에서 필요한 지시어가 허용되는지 점검해야 합니다.

    URL 재작성 규칙이 작동하지 않는다면 mod_rewrite 활성화 여부와 RewriteRule 작성 방식을 함께 확인하는 것이 중요합니다.

    특히 워드프레스 고유주소 변경 후 404 오류가 발생한다면 고유주소 설정만 반복해서 저장하기보다 Apache의 Directory 설정과 VirtualHost 구성을 먼저 점검해야 합니다.

    핵심 점검 순서는 Apache 실행 환경 확인 → AllowOverride 점검 → mod_rewrite 확인 → .htaccess 위치 및 권한 검사 → RewriteRule 분석 → 오류 로그 확인 → 설정 재로드입니다.

  • Docker 컨테이너가 Restarting 상태에서 벗어나지 못하는 이유

    Docker 컨테이너가 Restarting 상태에서 벗어나지 못하는 이유

    Docker 컨테이너를 실행했는데 정상적으로 서비스가 시작되지 않고 Restarting (1), Restarting (137) 같은 상태가 반복되는 경우가 있습니다. 컨테이너를 재시작해도 잠시 실행되는 듯하다가 종료되고, 다시 시작되는 현상이 계속된다면 단순한 Docker 실행 오류보다 컨테이너 내부 프로세스의 종료 원인을 먼저 확인해야 합니다.

    특히 웹서버, 데이터베이스, API 서버를 Docker로 운영하는 환경에서는 컨테이너 재시작이 반복되면서 웹사이트 접속 장애, API 응답 실패, 데이터베이스 연결 오류가 함께 발생할 수 있습니다.

    Docker 컨테이너가 Restarting 상태에서 벗어나지 못하는 이유는 애플리케이션 실행 실패, 잘못된 환경변수, 파일 권한 문제, 메모리 부족, 시작 명령어 오류, 외부 서비스 연결 실패 등 다양합니다. 여기에 Docker의 재시작 정책이 적용되면 종료된 컨테이너가 계속 다시 시작될 수 있습니다.

    이번 글에서는 컨테이너 상태 확인 → 로그 분석 → 종료 코드 점검 → 메모리 및 설정 확인 → 재시작 정책 분석 → 안전한 복구 순서로 문제의 원인을 찾는 방법을 설명합니다.

    Docker Restarting 핵심 명령어 한눈에 보기

    다음 명령어에서는 문제가 발생한 컨테이너 이름을 my-container로 가정합니다. 실제 환경에서는 컨테이너 이름 또는 ID로 변경해야 합니다.

    점검 목적 명령어
    전체 컨테이너 상태 docker ps -a
    최근 로그 확인 docker logs --tail 100 my-container
    종료 코드 확인 docker inspect -f '{{.State.ExitCode}}' my-container
    메모리 강제 종료 여부 docker inspect -f '{{.State.OOMKilled}}' my-container
    재시작 횟수 docker inspect -f '{{.RestartCount}}' my-container
    재시작 정책 docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' my-container
    컨테이너 상세 정보 docker inspect my-container
    Docker 이벤트 확인 docker events --since 10m
    Compose 설정 검사 docker compose config

    가장 중요한 것은 Restarting 상태 자체가 아니라 컨테이너 내부의 주 프로세스가 왜 종료되는지 확인하는 것입니다. 재시작 정책만 변경해도 반복 재시작은 멈출 수 있지만, 애플리케이션 오류까지 해결되는 것은 아닙니다.

    Docker 컨테이너 Restarting 상태 반복 원인과 docker logs inspect 종료 코드 확인 방법
    Docker Restarting 상태가 반복될 때 컨테이너 로그, 종료 코드, 메모리 및 재시작 정책을 점검하는 방법

    1. Docker 컨테이너가 Restarting 상태를 반복하는 이유

    Docker 컨테이너는 일반적으로 컨테이너 내부의 주 프로세스가 실행되는 동안 유지됩니다.

    하지만 주 프로세스가 오류로 종료되거나 정상 종료되더라도 설정된 재시작 정책의 조건에 해당하면 Docker가 컨테이너를 다시 시작할 수 있습니다.

    예를 들어 다음 명령어를 실행했다고 가정하겠습니다.

    docker ps -a

    출력 예시는 다음과 같습니다.

    CONTAINER ID   IMAGE        STATUS
    a1b2c3d4e5f6   myapp:latest Restarting (1) 5 seconds ago

    여기서 Restarting (1)의 숫자 1은 일반적으로 마지막 종료 코드가 1이었다는 의미입니다.

    Docker가 오류를 자동으로 수정했다는 의미가 아니라 종료된 컨테이너를 다시 시작하려고 시도하는 상태입니다.

    주요 발생 원인은 다음과 같습니다.

    • 애플리케이션 오류: 프로그램이 시작 직후 예외를 발생시키고 종료되는 경우
    • 환경변수 누락: 데이터베이스 주소, 비밀번호, API 키 등의 설정이 잘못된 경우
    • 시작 명령어 오류: ENTRYPOINT 또는 CMD에서 지정한 실행 파일이나 스크립트가 잘못된 경우
    • 파일 권한 문제: 애플리케이션이 필요한 파일이나 디렉터리에 접근하지 못하는 경우
    • 메모리 부족: 프로세스가 OOM으로 강제 종료되는 경우
    • 외부 서비스 연결 실패: 데이터베이스 등 필수 서비스에 연결하지 못해 애플리케이션이 종료되는 경우
    • 재시작 정책: 종료된 컨테이너를 계속 다시 시작하도록 설정된 경우

    2. docker logs로 컨테이너 종료 원인 확인하기

    Docker Restarting 오류가 발생했을 때 가장 먼저 확인해야 할 명령어는 docker logs입니다.

    docker logs --tail 100 my-container

    최근 로그를 확인하면 애플리케이션이 종료되기 직전에 어떤 오류가 발생했는지 파악할 수 있습니다.

    예를 들어 다음과 같은 메시지가 나타날 수 있습니다.

    Error: Missing required environment variable: DB_HOST
    Application startup failed
    Process exited with code 1

    위 로그는 데이터베이스 접속에 필요한 환경변수가 없어 애플리케이션이 시작하지 못한 사례입니다.

    또 다른 예시는 다음과 같습니다.

    Permission denied: /app/start.sh

    이 경우에는 시작 스크립트의 실행 권한, 파일 소유권 또는 컨테이너 실행 사용자를 확인해야 합니다.

    최근 로그를 실시간으로 확인하려면 다음 명령어를 사용할 수 있습니다.

    docker logs -f --tail 50 my-container

    다만 컨테이너가 매우 짧은 간격으로 종료된다면 로그가 빠르게 반복될 수 있습니다. 필요한 경우 시간 범위를 지정해 확인하는 것이 좋습니다.

    docker logs --since 10m my-container

    로그가 비어 있다면 프로그램이 표준 출력이나 표준 오류로 메시지를 남기지 않았거나, 실행 자체가 시작되지 않았을 가능성도 확인해야 합니다.

    3. Exit Code로 컨테이너 종료 이유 분석하기

    컨테이너가 종료된 이유를 분석할 때 종료 코드(Exit Code)는 중요한 단서가 됩니다.

    다음 명령어로 마지막 종료 코드를 확인할 수 있습니다.

    docker inspect \
      -f '{{.State.ExitCode}}' \
      my-container

    대표적인 종료 코드의 의미는 다음과 같습니다.

    종료 코드 일반적인 의미 확인할 항목
    0 정상 종료 프로그램이 의도보다 일찍 종료되는지 확인
    1 일반적인 실행 오류 애플리케이션 로그 및 설정 확인
    126 명령어 실행 불가 실행 권한 및 파일 형식 확인
    127 명령어를 찾을 수 없음 실행 파일 경로 및 이미지 구성 확인
    137 SIGKILL에 의한 종료일 수 있음 OOM, 강제 종료 및 시스템 로그 확인
    143 SIGTERM에 의한 종료일 수 있음 서비스 종료 및 재시작 이력 확인

    종료 코드만으로 정확한 원인을 확정할 수는 없습니다. 예를 들어 Exit Code 137은 OOM으로 발생할 수 있지만 사용자가 강제 종료했거나 외부에서 SIGKILL을 보낸 경우에도 나타날 수 있습니다.

    따라서 종료 코드와 함께 로그, OOMKilled 상태, Docker 이벤트를 확인해야 합니다.

    4. OOMKilled와 메모리 부족 확인하기

    Docker 컨테이너가 실행 직후 종료되거나 부하가 증가할 때마다 재시작된다면 메모리 부족 문제를 확인해야 합니다.

    다음 명령어로 Docker가 기록한 OOM 종료 여부를 확인합니다.

    docker inspect \
      -f '{{.State.OOMKilled}}' \
      my-container

    결과가 true라면 컨테이너가 메모리 부족으로 종료됐다는 중요한 단서가 됩니다.

    현재 설정된 메모리 제한도 확인할 수 있습니다.

    docker inspect \
      -f '{{.HostConfig.Memory}}' \
      my-container

    출력되는 값은 바이트 단위이며, 0은 Docker의 해당 메모리 제한이 설정되지 않았다는 의미입니다.

    다만 컨테이너에 별도 제한이 없더라도 호스트 시스템이나 상위 cgroup의 메모리 제약으로 프로세스가 종료될 수 있습니다.

    실행 중인 컨테이너의 메모리 사용량은 다음 명령어로 확인할 수 있습니다.

    docker stats --no-stream

    호스트 시스템의 메모리 상태도 함께 점검합니다.

    free -h

    OOM 관련 커널 로그는 다음 명령어로 확인할 수 있습니다.

    sudo journalctl -k -b | grep -Ei 'out of memory|oom|killed process'

    OOM이 확인됐다면 메모리 제한을 무조건 늘리기보다 애플리케이션의 메모리 사용량과 누수 가능성을 먼저 분석해야 합니다.

    5. ENTRYPOINT와 CMD 설정 오류 확인하기

    Docker 이미지에서 실행할 명령어가 잘못 지정되어 있으면 컨테이너가 시작 직후 종료될 수 있습니다.

    Dockerfile에는 일반적으로 다음과 같이 실행 명령어를 지정합니다.

    FROM python:3.12-slim
    
    WORKDIR /app
    COPY . /app
    
    CMD ["python", "app.py"]

    여기서 app.py 파일이 없거나 실행 과정에서 오류가 발생한다면 주 프로세스가 종료될 수 있습니다.

    현재 컨테이너에 설정된 시작 명령어를 확인합니다.

    docker inspect \
      -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' \
      my-container

    시작 스크립트를 사용하는 경우에는 파일 실행 권한과 Shebang 설정도 확인해야 합니다.

    #!/bin/sh
    exec python /app/app.py

    위 예시에서 exec는 셸 프로세스를 애플리케이션 프로세스로 교체하는 데 사용됩니다.

    컨테이너 시작 명령어를 수정해야 한다면 실행 중인 컨테이너 내부만 임시로 변경하기보다 Dockerfile이나 Compose 설정을 수정하고 이미지를 다시 빌드하는 것이 일반적입니다.

    6. 환경변수와 데이터베이스 연결 오류 확인하기

    애플리케이션이 시작할 때 데이터베이스 연결을 필수로 요구한다면 DB 서버가 준비되지 않았거나 연결 설정이 잘못됐을 때 컨테이너가 종료될 수 있습니다.

    예를 들어 다음과 같은 오류가 발생할 수 있습니다.

    Connection refused
    Database connection failed
    Application startup aborted

    이 경우 확인할 사항은 다음과 같습니다.

    • 데이터베이스 컨테이너가 정상 실행 중인지
    • 데이터베이스 호스트 이름과 포트가 정확한지
    • 두 컨테이너가 올바른 Docker 네트워크에 연결되어 있는지
    • 데이터베이스가 연결을 받을 준비가 완료됐는지
    • 애플리케이션에 연결 재시도 기능이 있는지

    컨테이너 환경변수는 다음 명령어로 확인할 수 있습니다.

    docker inspect \
      -f '{{json .Config.Env}}' \
      my-container

    주의할 점은 이 명령어가 비밀번호나 API 키 같은 민감한 값을 출력할 수 있다는 것입니다. 결과를 외부에 공유할 때는 반드시 비밀 정보를 제거해야 합니다.

    데이터베이스 연결이 실패한다고 해서 반드시 Docker 네트워크 자체에 문제가 있는 것은 아닙니다. DB 서비스가 아직 초기화 중이거나 애플리케이션의 시작 순서가 맞지 않는 경우도 있습니다.

    7. Docker Restart Policy가 무한 재시작에 미치는 영향

    Docker는 컨테이너가 종료됐을 때 자동으로 다시 시작하도록 설정할 수 있습니다.

    현재 재시작 정책을 확인합니다.

    docker inspect \
      -f '{{.HostConfig.RestartPolicy.Name}}' \
      my-container

    대표적인 재시작 정책은 다음과 같습니다.

    정책 동작
    no 자동 재시작하지 않음
    always 종료된 컨테이너를 자동 재시작하도록 관리
    unless-stopped 명시적으로 중지된 경우를 제외하고 자동 재시작
    on-failure 0이 아닌 종료 코드로 종료되면 재시작

    예를 들어 restart: always가 설정되어 있고 애플리케이션이 시작할 때마다 오류로 종료된다면 컨테이너가 반복적으로 Restarting 상태에 들어갈 수 있습니다.

    재시작 횟수는 다음 명령어로 확인할 수 있습니다.

    docker inspect \
      -f '{{.RestartCount}}' \
      my-container

    재시작 횟수가 계속 증가한다면 주 프로세스가 반복 종료되고 있는지 확인해야 합니다.

    장애 분석을 위해 자동 재시작을 일시적으로 중단해야 한다면 서비스 영향과 복구 절차를 확인한 후 다음 명령어를 사용할 수 있습니다.

    docker update --restart=no my-container

    이 명령어는 컨테이너의 재시작 정책을 변경합니다. 그러나 애플리케이션의 오류 자체를 해결하지는 않습니다.

    또한 Docker Compose로 관리하는 서비스라면 Compose 파일의 restart 설정도 함께 확인해야 합니다.

    8. Docker Compose에서 Restarting 오류 확인하기

    Docker Compose 환경에서는 여러 컨테이너가 서로 의존하는 경우가 많습니다.

    서비스 상태를 확인합니다.

    docker compose ps -a

    특정 서비스의 로그를 확인합니다.

    docker compose logs --tail 100 app

    Compose 설정 파일의 문법과 최종 적용 구성을 확인합니다.

    docker compose config

    단, Compose 설정 출력에는 환경변수 값 등 민감한 정보가 포함될 수 있으므로 공유 시 주의해야 합니다.

    다음은 데이터베이스와 애플리케이션을 함께 사용하는 Compose 설정의 예시입니다.

    services:
      db:
        image: postgres:16
        environment:
          POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
        healthcheck:
          test: ["CMD-SHELL", "pg_isready -U postgres"]
          interval: 10s
          timeout: 5s
          retries: 5
    
      app:
        image: myapp:latest
        depends_on:
          db:
            condition: service_healthy
        restart: on-failure

    위 설정은 DB 서비스의 헬스체크가 통과한 뒤 애플리케이션 시작을 진행하도록 구성한 예시입니다. 실제 환경에서는 데이터베이스 사용자와 헬스체크 명령어를 맞게 조정해야 합니다.

    다만 depends_on과 헬스체크를 설정하더라도 서비스가 실행된 이후의 장애까지 자동으로 해결하는 것은 아닙니다. 애플리케이션에도 적절한 연결 재시도 처리가 필요합니다.

    9. Healthcheck 실패와 Restarting 상태 구분하기

    Docker에서 unhealthy 상태가 나타났다고 해서 반드시 컨테이너가 자동으로 재시작되는 것은 아닙니다.

    일반적인 Docker Engine 환경에서는 헬스체크 실패만으로 재시작 정책이 실행되지는 않습니다.

    컨테이너가 Restarting 상태라면 주 프로세스 종료와 재시작 정책을 우선 확인해야 합니다.

    헬스체크 상태는 다음 명령어로 확인할 수 있습니다.

    docker inspect \
      -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}no-healthcheck{{end}}' \
      my-container

    헬스체크 로그가 필요한 경우에는 다음 명령어를 사용할 수 있습니다.

    docker inspect \
      -f '{{json .State.Health}}' \
      my-container

    단, 별도의 오케스트레이션 도구나 감시 시스템이 헬스체크 결과를 기반으로 컨테이너를 재생성하도록 구성되어 있다면 해당 시스템의 정책도 확인해야 합니다.

    10. Docker 컨테이너 Restarting 상태 해결 순서

    반복 재시작 문제는 무작정 컨테이너를 삭제하거나 이미지를 다시 설치하기보다 원인을 순서대로 확인하는 것이 중요합니다.

    먼저 컨테이너 상태와 로그를 확인합니다.

    docker ps -a
    docker logs --tail 100 my-container

    다음으로 종료 코드와 메모리 종료 여부를 확인합니다.

    docker inspect \
      -f 'ExitCode={{.State.ExitCode}} OOMKilled={{.State.OOMKilled}}' \
      my-container

    재시작 횟수와 정책을 확인합니다.

    docker inspect \
      -f 'RestartCount={{.RestartCount}} RestartPolicy={{.HostConfig.RestartPolicy.Name}}' \
      my-container

    이후 로그에서 확인된 오류에 따라 환경변수, 시작 명령어, 파일 권한, 메모리 제한, 외부 서비스 연결을 점검합니다.

    원인을 수정했다면 컨테이너를 다시 실행하고 상태와 로그가 안정적으로 유지되는지 확인합니다.

    docker ps -a
    docker logs --tail 50 my-container

    Docker Compose 환경이라면 설정 변경 후 서비스 재생성이 필요한지 확인해야 합니다. 이미지나 환경변수 설정을 변경했다면 기존 컨테이너를 단순히 재시작하는 것만으로는 변경 사항이 반영되지 않을 수 있습니다.

    실전 점검 명령어 모음

    다음 명령어를 순서대로 실행하면 Docker Restarting 오류의 주요 원인을 확인할 수 있습니다.

    # 1. 컨테이너 상태 확인
    docker ps -a
    
    # 2. 최근 오류 로그 확인
    docker logs --tail 100 my-container
    
    # 3. 종료 코드 확인
    docker inspect -f '{{.State.ExitCode}}' my-container
    
    # 4. OOM 종료 여부 확인
    docker inspect -f '{{.State.OOMKilled}}' my-container
    
    # 5. 재시작 횟수 확인
    docker inspect -f '{{.RestartCount}}' my-container
    
    # 6. 재시작 정책 확인
    docker inspect -f '{{.HostConfig.RestartPolicy.Name}}' my-container
    
    # 7. 시작 명령어 확인
    docker inspect -f 'Entrypoint={{json .Config.Entrypoint}} Cmd={{json .Config.Cmd}}' my-container
    
    # 8. 최근 Docker 이벤트 확인
    docker events --since 10m --until 0s
    
    # 9. 호스트 메모리 확인
    free -h
    
    # 10. OOM 관련 로그 확인
    sudo journalctl -k -b | grep -Ei 'out of memory|oom|killed process'

    위 명령어에서 my-container는 실제 컨테이너 이름이나 ID로 변경해야 합니다.

    Docker 이벤트와 로그에는 서비스 구성에 따라 민감한 정보가 포함될 수 있으므로 외부에 공유하기 전 확인하는 것이 좋습니다.

    정리

    Docker 컨테이너가 Restarting 상태에서 벗어나지 못하는 이유는 대부분 주 프로세스가 종료되고 재시작 정책에 따라 다시 실행되는 과정이 반복되기 때문입니다.

    애플리케이션 실행 오류, 환경변수 누락, 시작 명령어 문제, 파일 권한 오류, 메모리 부족, 데이터베이스 연결 실패 등이 원인이 될 수 있습니다.

    먼저 docker logs로 오류 메시지를 확인하고, docker inspect로 종료 코드와 OOMKilled 상태, 재시작 정책을 분석해야 합니다.

    특히 Exit Code 137이 나타난다면 OOM 여부를 추가로 확인해야 하며, Exit Code 1이 반복된다면 애플리케이션 로그와 설정을 우선 살펴보는 것이 좋습니다.

    핵심 점검 순서는 컨테이너 상태 확인 → 로그 분석 → 종료 코드 점검 → 메모리 및 실행 설정 확인 → 재시작 정책 분석 → 원인 수정 후 정상 실행 확인입니다.

  • 서버에서 TLS Handshake Failed 오류가 발생하는 원인과 확인 방법

    서버에서 TLS Handshake Failed 오류가 발생하는 원인과 확인 방법

    웹사이트에 HTTPS로 접속하거나 서버에서 외부 API를 호출하는 과정에서 TLS Handshake Failed 오류가 발생하는 경우가 있습니다. 웹브라우저에서는 보안 연결을 설정할 수 없다는 메시지가 나타나고, 서버 로그에는 handshake failure, protocol version, certificate verify failed 같은 오류가 기록되기도 합니다.

    이런 문제가 발생하면 SSL 인증서가 만료됐다고 생각하기 쉽지만, 실제로는 TLS 버전 불일치, 암호화 알고리즘 충돌, SNI 설정 오류, 인증서 체인 문제, 프록시 설정 또는 네트워크 연결 문제 등 여러 원인이 영향을 줄 수 있습니다.

    특히 브라우저에서는 정상적으로 접속되는데 특정 프로그램이나 서버에서만 TLS 연결이 실패한다면, 인증서 자체보다 클라이언트의 TLS 지원 범위와 인증서 검증 환경을 함께 살펴봐야 합니다.

    이번 글에서는 오류 메시지 확인 → 인증서와 TLS 버전 검사 → 암호화 알고리즘 및 SNI 확인 → 서버 로그 분석 → 프록시와 네트워크 점검 순서로 원인을 좁히는 방법을 설명합니다.

    TLS Handshake Failed 핵심 명령어 한눈에 보기

    TLS 오류를 확인할 때는 서버의 인증서뿐 아니라 클라이언트와 서버가 실제로 어떤 TLS 조건으로 연결을 시도하는지 살펴봐야 합니다. 다음 예시는 example.com의 HTTPS 443번 포트를 기준으로 작성했습니다.

    점검 목적명령어
    HTTPS 연결 상세 확인 curl -Iv https://example.com/
    TLS 연결 및 인증서 확인 openssl s_client -connect example.com:443 -servername example.com
    TLS 1.2 연결 확인 openssl s_client -connect example.com:443 -servername example.com -tls1_2
    TLS 1.3 연결 확인 openssl s_client -connect example.com:443 -servername example.com -tls1_3
    인증서 날짜 확인 openssl x509 -in server.crt -noout -dates
    Nginx 설정 검사 sudo nginx -t
    Nginx 오류 로그 sudo tail -n 100 /var/log/nginx/error.log
    443번 포트 확인 sudo ss -lntp 'sport = :443'

    가장 먼저 확인할 것은 오류가 TLS 협상 중 발생했는지, 인증서 검증 단계에서 발생했는지 구분하는 것입니다. 두 문제 모두 보안 연결 실패로 나타날 수 있지만 해결 방법은 다릅니다.

    서버 TLS Handshake Failed 오류와 OpenSSL curl 명령어를 이용한 SSL TLS 연결 점검
    TLS Handshake Failed 오류 발생 시 인증서, TLS 버전, SNI 및 서버 연결 상태를 점검하는 방법

    1. TLS Handshake Failed 오류가 발생하는 이유

    TLS(Transport Layer Security)는 클라이언트와 서버가 통신 내용을 암호화하고, 서버 신원을 검증할 수 있도록 지원하는 보안 프로토콜입니다.

    HTTPS 연결을 시작하면 클라이언트와 서버는 지원하는 TLS 버전과 암호화 알고리즘을 협상하고, 필요한 인증서 검증과 키 교환 절차를 수행합니다.

    이 과정이 정상적으로 완료되지 않으면 TLS Handshake Failed 또는 관련 오류가 발생할 수 있습니다.

    대표적인 원인은 다음과 같습니다.

    • TLS 버전 불일치: 클라이언트와 서버가 공통으로 지원하는 TLS 버전이 없는 경우
    • 암호화 알고리즘 불일치: 공통으로 사용할 수 있는 Cipher Suite 또는 관련 암호화 조건이 없는 경우
    • 인증서 검증 실패: 인증서 만료, 신뢰할 수 없는 발급기관, 인증서 체인 누락 등이 발생한 경우
    • SNI 설정 문제: 요청한 도메인에 맞는 인증서나 TLS 가상 호스트가 선택되지 않는 경우
    • 서버 설정 오류: Nginx, Apache, 로드밸런서의 TLS 설정이 잘못된 경우
    • 클라이언트 환경 문제: 오래된 OpenSSL, Java 또는 시스템 신뢰 저장소를 사용하는 경우
    • 네트워크 및 프록시 문제: 연결 중단, TLS 검사 장비, 잘못된 프록시 라우팅 등이 발생하는 경우
    • 상호 TLS 인증 문제: 서버가 요구하는 클라이언트 인증서가 없거나 유효하지 않은 경우

    다만 모든 인증서 검증 오류가 TLS 프로토콜의 핸드셰이크 실패로 동일하게 기록되는 것은 아닙니다. 클라이언트 프로그램에 따라 TLS 연결 실패와 인증서 검증 실패를 서로 다른 메시지로 표시할 수 있습니다.

    2. curl로 HTTPS 연결 오류 확인하기

    가장 먼저 curl을 이용해 HTTPS 연결 과정을 확인합니다.

    curl -Iv https://example.com/

    -I는 HTTP 헤더 요청, -v는 연결 과정을 자세히 출력하는 옵션입니다.

    예를 들어 TLS 버전이 맞지 않는 환경에서는 다음과 비슷한 오류가 발생할 수 있습니다.

    curl: (35) OpenSSL/SSL_connect:
    SSL_ERROR_SYSCALL

    다만 위 메시지만으로 TLS 버전 불일치를 확정할 수는 없습니다. 연결이 비정상적으로 종료되거나 네트워크 장비가 연결을 차단한 경우에도 유사한 메시지가 나타날 수 있습니다.

    인증서 검증 실패라면 다음과 같은 오류가 나타날 수 있습니다.

    curl: (60) SSL certificate problem:
    unable to get local issuer certificate

    이 경우에는 서버의 인증서 체인과 클라이언트의 신뢰할 수 있는 CA 인증서 목록을 확인해야 합니다.

    원인을 확인하기 위해 curl -k로 인증서 검증을 비활성화하는 방법은 일시적인 진단 용도로만 제한해야 합니다. 운영 프로그램에서 인증서 검증을 끄는 것은 중간자 공격 위험을 높일 수 있습니다.

    3. OpenSSL로 TLS 버전과 인증서 확인하기

    OpenSSL의 s_client 명령어를 사용하면 서버가 제공하는 인증서와 협상된 TLS 버전, 암호화 알고리즘 등을 확인할 수 있습니다.

    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -showcerts

    여기서 -servername 옵션은 SNI(Server Name Indication)를 지정합니다. 여러 도메인을 하나의 IP 주소에서 서비스하는 환경에서는 올바른 인증서를 선택하는 데 중요합니다.

    연결에 성공하면 다음과 같은 정보가 표시될 수 있습니다.

    New, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384
    Server public key is 2048 bit
    Verify return code: 0 (ok)

    이는 예시 출력이며 실제 TLS 버전과 암호화 알고리즘은 서버 및 클라이언트 설정에 따라 달라집니다.

    다만 Verify return code: 0 (ok)만으로 접속 도메인과 인증서 이름이 일치한다고 단정해서는 안 됩니다. 도메인 이름까지 검증하려면 다음 명령어를 사용할 수 있습니다.

    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -verify_hostname example.com \
      -verify_return_error

    이 명령어는 인증서 검증 오류를 확인하는 데 도움이 됩니다. 단, OpenSSL 버전과 신뢰 저장소 설정에 따라 결과가 달라질 수 있습니다.

    4. TLS 1.2와 TLS 1.3 버전 불일치 확인하기

    클라이언트와 서버가 공통으로 지원하는 TLS 버전이 없다면 핸드셰이크가 실패할 수 있습니다.

    TLS 1.2 연결을 확인합니다.

    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -tls1_2

    TLS 1.3 연결도 별도로 확인합니다.

    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -tls1_3

    예를 들어 TLS 1.3 연결은 성공하지만 TLS 1.2 연결이 실패한다면 서버가 TLS 1.2를 지원하지 않거나 해당 버전에서 사용할 수 있는 암호화 조건이 맞지 않을 가능성이 있습니다.

    반대로 특정 클라이언트만 실패한다면 그 클라이언트의 OpenSSL 또는 TLS 라이브러리 버전도 확인해야 합니다.

    openssl version -a
    curl -V

    현재 운영 환경에서는 일반적으로 TLS 1.2와 TLS 1.3을 중심으로 호환성과 보안 요구사항을 검토하는 것이 좋습니다.

    오래된 클라이언트를 지원하기 위해 TLS 1.0이나 TLS 1.1을 무조건 활성화하는 것은 보안상 권장되지 않습니다.

    5. SSL 인증서 만료와 인증서 체인 문제 확인하기

    TLS 연결 문제 중에는 인증서 자체의 유효성이나 인증서 체인 구성에 문제가 있는 경우도 있습니다.

    로컬 인증서 파일의 유효기간은 다음 명령어로 확인할 수 있습니다.

    openssl x509 \
      -in server.crt \
      -noout -dates

    출력 예시는 다음과 같습니다.

    notBefore=Oct  1 00:00:00 2026 GMT
    notAfter=Dec 30 23:59:59 2026 GMT

    notBefore는 인증서 유효기간 시작, notAfter는 만료 시각입니다.

    다만 서버에 저장된 인증서 파일과 실제로 외부 클라이언트에 제공되는 인증서가 다를 수 있습니다.

    따라서 외부에서 다음 명령어로 실제 서버가 제공하는 인증서도 확인해야 합니다.

    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -showcerts

    인증서 체인이 누락된 경우에는 브라우저에서는 접속되더라도 일부 API 클라이언트나 서버 환경에서 인증서 검증에 실패할 수 있습니다.

    Nginx에서는 일반적으로 서버 인증서와 필요한 중간 인증서를 포함하는 전체 인증서 체인 파일을 ssl_certificate에 지정합니다.

    인증서를 교체할 때는 도메인 이름, 유효기간, 중간 인증서 포함 여부를 함께 확인해야 합니다.

    6. SNI 설정 오류와 도메인별 인증서 문제

    하나의 서버 IP 주소에서 여러 HTTPS 도메인을 운영한다면 SNI 설정이 중요합니다.

    SNI는 클라이언트가 TLS 연결을 시작하면서 접속하려는 서버 이름을 전달하는 기능입니다.

    예를 들어 같은 IP 주소에서 example.com과 api.example.com을 함께 운영한다고 가정하겠습니다.

    서버 이름을 지정하지 않거나 잘못된 이름을 전달하면 기본 가상 호스트의 인증서가 선택될 수 있습니다.

    다음 명령어로 정확한 도메인을 지정해 확인합니다.

    openssl s_client \
      -connect example.com:443 \
      -servername example.com

    Nginx에서는 도메인별 서버 설정을 확인해야 합니다.

    server {
        listen 443 ssl;
        server_name example.com;
    
        ssl_certificate /etc/nginx/ssl/fullchain.pem;
        ssl_certificate_key /etc/nginx/ssl/privkey.pem;
    }

    위 코드는 설정 구조를 보여주는 예시입니다. 실제 인증서 경로와 도메인은 서버 환경에 맞게 변경해야 합니다.

    또한 SNI가 올바르더라도 인증서의 SAN 항목에 요청한 도메인이 포함되어 있지 않으면 클라이언트의 인증서 이름 검증이 실패할 수 있습니다.

    7. Nginx 로그로 TLS 핸드셰이크 실패 원인 확인하기

    Nginx를 사용하는 서버라면 오류 로그에서 TLS 연결 실패 메시지를 확인할 수 있습니다.

    sudo tail -n 100 /var/log/nginx/error.log

    다음과 같은 로그가 기록될 수 있습니다.

    SSL_do_handshake() failed
    (SSL: error:0A000102:SSL routines::unsupported protocol)

    이 메시지는 지원하지 않는 TLS 프로토콜과 관련된 문제를 나타낼 수 있습니다.

    또 다른 예시는 다음과 같습니다.

    SSL_do_handshake() failed
    (SSL: error:0A0000C1:SSL routines::no shared cipher)

    이 경우에는 클라이언트와 서버 사이에 공통으로 사용할 수 있는 암호화 조건이 없는지 확인해야 합니다.

    다만 오류 코드와 로그 문구는 OpenSSL 및 Nginx 버전에 따라 다를 수 있습니다.

    Nginx 설정을 변경하기 전에는 다음 명령어로 현재 설정 문법을 검사해야 합니다.

    sudo nginx -t

    설정이 정상이고 변경 내용을 적용해야 한다면 운영 환경의 변경 절차에 따라 재로드할 수 있습니다.

    sudo systemctl reload nginx

    TLS 오류가 발생했다는 이유만으로 암호화 알고리즘을 광범위하게 허용하거나 보안 수준을 낮추는 것은 피해야 합니다.

    8. 프록시와 로드밸런서에서 발생하는 TLS 오류

    웹사이트 앞에 CDN, 리버스 프록시, 로드밸런서가 있다면 TLS 연결이 여러 구간에서 이루어질 수 있습니다.

    예를 들어 다음과 같은 구조를 생각할 수 있습니다.

    Client
       |
       | HTTPS
       v
    Load Balancer
       |
       | HTTPS
       v
    Nginx
       |
       | HTTP 또는 HTTPS
       v
    Application

    이 경우 클라이언트와 로드밸런서 사이의 TLS 연결은 성공하지만 로드밸런서와 Nginx 사이에서 핸드셰이크가 실패할 수 있습니다.

    따라서 외부 HTTPS 연결이 정상이라고 해서 내부 TLS 연결까지 정상이라고 판단해서는 안 됩니다.

    확인해야 할 항목은 다음과 같습니다.

    • 로드밸런서와 백엔드 서버의 TLS 버전 호환성
    • 백엔드 인증서의 도메인 이름과 신뢰 체인
    • 프록시가 전달하는 SNI 값
    • HTTPS 백엔드 포트와 프로토콜 설정
    • 상호 TLS 인증서 요구 여부
    • CDN과 원본 서버의 인증서 설정

    특히 HTTPS 백엔드에 HTTP 방식으로 연결하거나 반대로 HTTP 포트에 TLS 연결을 시도하면 프로토콜 불일치 오류가 발생할 수 있습니다.

    9. TLS Handshake Failed 오류가 특정 클라이언트에서만 발생하는 경우

    서버 자체는 정상인데 특정 프로그램이나 운영체제에서만 TLS 연결에 실패한다면 클라이언트 환경을 확인해야 합니다.

    대표적으로 오래된 Java 런타임이나 OpenSSL 라이브러리가 서버에서 허용하는 TLS 조건을 지원하지 않을 수 있습니다.

    또한 시스템의 CA 인증서 저장소가 오래됐다면 정상적인 서버 인증서도 신뢰하지 못할 수 있습니다.

    다음 명령어로 기본적인 클라이언트 환경을 확인할 수 있습니다.

    openssl version -a
    curl -V
    date -u

    시스템 시간이 크게 잘못 설정된 경우에도 인증서의 유효기간 검증이 실패할 수 있으므로 시간 동기화 상태를 확인해야 합니다.

    클라이언트 문제를 해결할 때는 인증서 검증을 비활성화하기보다 신뢰 저장소와 TLS 라이브러리를 정상적으로 업데이트하는 것이 좋습니다.

    실전 점검 순서

    TLS Handshake Failed 오류가 발생했다면 다음 순서로 점검할 수 있습니다.

    # 1. HTTPS 연결 상태 확인
    curl -Iv https://example.com/
    
    # 2. TLS 연결 및 인증서 확인
    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -verify_hostname example.com \
      -verify_return_error
    
    # 3. TLS 1.2 연결 확인
    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -tls1_2
    
    # 4. TLS 1.3 연결 확인
    openssl s_client \
      -connect example.com:443 \
      -servername example.com \
      -tls1_3
    
    # 5. 클라이언트 TLS 환경 확인
    openssl version -a
    curl -V
    
    # 6. 서버의 443번 포트 확인
    sudo ss -lntp 'sport = :443'
    
    # 7. Nginx 설정 검사
    sudo nginx -t
    
    # 8. Nginx 오류 로그 확인
    sudo tail -n 100 /var/log/nginx/error.log

    위 명령어에서 example.com은 실제 점검하려는 도메인으로 변경해야 합니다.

    Nginx를 사용하지 않는 서버라면 Apache, HAProxy, Envoy 또는 해당 애플리케이션의 TLS 로그와 설정을 확인해야 합니다.

    핸드셰이크 실패 메시지와 인증서 검증 실패 메시지를 구분하고, 클라이언트와 서버 양쪽의 설정을 비교하는 것이 중요합니다.

    정리

    서버에서 TLS Handshake Failed 오류가 발생하는 원인은 인증서 만료뿐 아니라 TLS 버전 불일치, 암호화 알고리즘 충돌, SNI 설정 오류, 인증서 체인 누락 및 프록시 설정 문제 등 다양합니다.

    먼저 curl -Iv로 연결 실패 메시지를 확인하고, openssl s_client로 TLS 버전과 인증서 검증 상태를 분석해야 합니다.

    서버를 직접 관리하는 환경이라면 Nginx 또는 사용하는 웹서버의 TLS 설정과 오류 로그를 함께 확인하는 것이 좋습니다.

    특히 특정 클라이언트에서만 실패한다면 서버 설정을 무작정 변경하기보다 클라이언트의 TLS 라이브러리, CA 인증서 저장소, SNI 지원 여부를 먼저 점검해야 합니다.

    핵심 점검 순서는 오류 메시지 확인 → TLS 버전 검사 → 인증서 및 SNI 확인 → 서버 로그 분석 → 프록시·네트워크 점검 → 원인별 설정 수정입니다.

  • 리눅스 서버에서 Stale File Handle 오류가 발생하는 이유와 해결 방법

    리눅스 서버에서 Stale File Handle 오류가 발생하는 이유와 해결 방법

    리눅스 서버에서 평소 정상적으로 사용하던 공유 디렉터리에 접근하려는데 갑자기 Stale file handle 오류가 발생하는 경우가 있습니다. 파일을 삭제하거나 수정하지 않았는데도 ls, stat, df 명령어가 실패하거나 애플리케이션에서 파일을 읽지 못하는 현상이 나타날 수 있습니다.

    특히 NFS(Network File System)를 사용하는 서버에서는 공유 디렉터리를 변경하거나 스토리지 볼륨을 복원한 뒤 이러한 오류가 발생하기도 합니다. 파일이 존재하는 것처럼 보이는데 접근할 수 없어 파일 권한이나 네트워크 연결 문제로 오해하기 쉽습니다.

    하지만 Stale File Handle 오류는 클라이언트가 보유한 파일 식별 정보가 서버에서 더 이상 유효하지 않을 때 발생하는 경우가 많습니다. 단순히 네트워크를 다시 연결하거나 서버를 재시작한다고 해서 항상 해결되는 문제는 아닙니다.

    이번 글에서는 오류 메시지 확인 → NFS 마운트 상태 점검 → 파일 핸들 무효화 원인 분석 → 점유 프로세스 확인 → 안전한 재마운트 및 재발 방지 순서로 해결 방법을 알아봅니다.

    Stale File Handle 핵심 명령어 한눈에 보기

    오류가 발생했다면 먼저 해당 경로가 어떤 파일시스템에 연결되어 있는지 확인해야 합니다. 다음 명령어에서는 /mnt/shared를 NFS 마운트 경로로 가정합니다.

    점검 목적 명령어
    NFS 마운트 확인 findmnt -t nfs,nfs4
    마운트 정보 확인 findmnt --mountpoint /mnt/shared
    파일 접근 상태 확인 stat /mnt/shared
    NFS 클라이언트 통계 nfsstat -m
    커널 로그 확인 sudo journalctl -k -b
    점유 프로세스 확인 sudo fuser -vm /mnt/shared
    하위 마운트 확인 findmnt -R /mnt/shared
    마운트 해제 sudo umount /mnt/shared
    마운트 재연결 sudo mount /mnt/shared

    Stale File Handle 오류가 발생했다고 해서 즉시 마운트를 해제하면 안 됩니다. 먼저 해당 경로를 사용 중인 프로세스와 서비스 영향을 확인해야 합니다.

    리눅스 Stale File Handle 오류 발생 원인과 NFS 마운트 상태 확인 및 복구 방법
    리눅스 Stale File Handle 오류 발생 시 NFS 마운트 상태와 파일 핸들을 점검하는 방법

    1. Stale File Handle 오류가 발생하는 원인

    리눅스에서 Stale file handle은 일반적으로 ESTALE 오류 코드와 관련이 있습니다. 파일시스템에서 기존 파일 핸들이 더 이상 유효하지 않다고 판단할 때 반환될 수 있습니다.

    NFS 클라이언트는 서버에 저장된 파일이나 디렉터리에 접근할 때 파일 핸들이라는 식별 정보를 사용합니다. 이 정보는 단순한 파일 이름이 아니라 서버가 특정 파일시스템 객체를 식별하기 위해 사용하는 값입니다.

    예를 들어 서버에서 기존 디렉터리를 삭제하고 동일한 이름의 디렉터리를 다시 생성했다고 가정하겠습니다.

    # NFS 서버에서 실행하는 예시
    rm -r /exports/data/old-directory
    mkdir /exports/data/old-directory

    위 명령어는 원리를 설명하기 위한 예시이며, 실제 운영 서버에서 그대로 실행해서는 안 됩니다.

    새로 생성된 디렉터리는 이름이 같아도 기존 디렉터리와 다른 파일시스템 객체입니다. 따라서 클라이언트가 이전 디렉터리의 파일 핸들을 계속 사용한다면 Stale File Handle 오류가 발생할 수 있습니다.

    대표적인 발생 원인은 다음과 같습니다.

    • 공유 디렉터리 삭제 및 재생성: 기존 파일시스템 객체가 다른 객체로 교체된 경우
    • NFS Export 구성 변경: 공유 경로나 내보내기 구성이 달라진 경우
    • 스토리지 볼륨 교체: 공유 파일시스템이 다른 볼륨으로 변경된 경우
    • 스냅샷 복원: 파일 식별 정보가 이전 상태와 달라진 경우
    • 서버 재구성: 재시작 과정에서 파일시스템이나 공유 구성이 변경된 경우
    • 오래된 애플리케이션 참조: 프로세스가 유효하지 않은 파일 핸들을 계속 사용하는 경우

    단, NFS 서버를 단순히 재시작했다는 이유만으로 반드시 Stale File Handle 오류가 발생하는 것은 아닙니다. 기존 파일 핸들이 유지된다면 정상적으로 접근할 수 있습니다.

    2. 실제 오류 메시지와 발생 범위 확인하기

    NFS 공유 디렉터리에 접근할 때 다음과 같은 오류가 나타날 수 있습니다.

    $ ls -la /mnt/shared
    
    ls: cannot access '/mnt/shared':
    Stale file handle

    특정 파일에서만 오류가 발생하는 경우도 있습니다.

    $ stat /mnt/shared/report.csv
    
    stat: cannot statx '/mnt/shared/report.csv':
    Stale file handle

    이 오류는 파일이 없다는 의미의 No such file or directory와 구분해야 합니다.

    파일 이름이 동일하게 존재하더라도 클라이언트가 참조하는 파일 핸들이 더 이상 유효하지 않을 수 있습니다.

    오류가 발생했을 때는 다음 사항을 먼저 확인하는 것이 좋습니다.

    • 특정 파일에서만 오류가 발생하는지
    • 공유 디렉터리 전체에 접근할 수 없는지
    • 다른 NFS 클라이언트에서도 동일한 오류가 발생하는지
    • 오류 발생 직전에 서버의 공유 경로나 스토리지가 변경됐는지

    특정 파일에서만 발생한다면 해당 파일의 교체나 삭제 이력을, 공유 디렉터리 전체에서 발생한다면 마운트와 Export 구성을 우선 확인해야 합니다.

    3. findmnt로 NFS 마운트 상태 확인하기

    먼저 해당 서버에서 사용 중인 NFS 마운트 목록을 확인합니다.

    findmnt -t nfs,nfs4

    다음은 출력 예시입니다.

    TARGET       SOURCE                    FSTYPE OPTIONS
    /mnt/shared  192.0.2.10:/exports/data  nfs4   rw,relatime,vers=4.2

    주요 항목은 다음과 같습니다.

    • TARGET: 클라이언트에서 사용하는 마운트 경로
    • SOURCE: NFS 서버 주소와 공유 디렉터리
    • FSTYPE: 파일시스템 유형
    • OPTIONS: 마운트 옵션과 NFS 프로토콜 버전

    특정 마운트 지점을 확인하려면 다음 명령어를 사용합니다.

    findmnt --mountpoint /mnt/shared

    오류가 마운트 지점의 하위 경로에서 발생했다면 해당 경로에 적용되는 파일시스템도 확인할 수 있습니다.

    findmnt -T /mnt/shared/report.csv

    단, 파일 핸들이 이미 무효화된 상태에서는 경로를 직접 조회하는 명령어가 실패할 수도 있습니다.

    또한 findmnt에서 마운트가 정상적으로 표시되더라도 NFS 서버의 파일 핸들이 유효하다는 의미는 아닙니다. 마운트 정보와 실제 파일 접근 가능 여부를 함께 확인해야 합니다.

    4. NFS 서버 연결과 커널 로그 점검하기

    NFS 서버의 응답이 불안정하다면 파일 접근 오류나 장시간 대기 현상이 함께 나타날 수 있습니다.

    먼저 서버 연결 상태를 확인합니다.

    ping -c 4 192.0.2.10

    다만 ping이 성공해도 NFS 서비스가 정상이라는 의미는 아닙니다. 반대로 ICMP가 차단된 환경에서는 ping이 실패하더라도 NFS 통신은 정상일 수 있습니다.

    NFSv4에서 일반적으로 사용하는 TCP 2049번 포트의 연결 가능 여부를 확인할 수 있습니다.

    nc -vz -w 3 192.0.2.10 2049

    커널 로그에서 NFS 관련 메시지를 검색합니다.

    sudo journalctl -k -b | grep -Ei 'nfs|stale|server not responding'

    예를 들어 다음과 같은 메시지가 나타날 수 있습니다.

    nfs: server 192.0.2.10 not responding, still trying

    이 메시지는 NFS 서버 응답 문제를 나타낼 수 있습니다. 하지만 네트워크 타임아웃과 Stale File Handle은 서로 다른 원인으로 발생할 수 있으므로 구분해야 합니다.

    따라서 서버가 응답하는지만 확인할 것이 아니라 공유 디렉터리의 변경 이력과 실제 파일 접근 결과를 함께 점검해야 합니다.

    5. NFS 서버의 Export 설정 확인하기

    클라이언트에서 파일 핸들 오류가 반복된다면 NFS 서버의 공유 설정이 변경됐는지 확인해야 합니다.

    NFS 서버 관리 권한이 있는 경우 다음 명령어로 현재 Export 설정을 확인할 수 있습니다.

    sudo exportfs -v

    설정 파일도 확인할 수 있습니다.

    sudo cat /etc/exports

    예를 들어 다음과 같은 설정이 있을 수 있습니다.

    /exports/data 192.0.2.0/24(rw,sync,root_squash)

    여기서 확인할 사항은 다음과 같습니다.

    • 공유 경로가 이전과 동일한지
    • 해당 경로에 실제 파일시스템이 정상적으로 마운트되어 있는지
    • 스토리지 볼륨이나 파일시스템이 교체되지 않았는지
    • Export 설정 변경 이후 문제가 발생했는지
    • 여러 NFS 서버가 동일한 공유 경로를 제공하는 구조인지

    특히 서버에서 동일한 경로 이름을 유지하더라도 실제 파일시스템이 다른 볼륨으로 교체됐다면 기존 파일 핸들이 무효화될 수 있습니다.

    운영 중인 NFS 서버에서 Export 설정을 무작정 변경하거나 서비스를 재시작하면 다른 클라이언트에도 영향을 줄 수 있으므로 변경 이력을 먼저 확인하는 것이 중요합니다.

    6. fuser로 마운트를 사용하는 프로세스 확인하기

    마운트 해제나 재연결을 진행하기 전에 해당 디렉터리를 사용 중인 프로세스를 확인해야 합니다.

    sudo fuser -vm /mnt/shared

    다음은 출력 예시입니다.

                         USER        PID ACCESS COMMAND
    /mnt/shared:         root       2451 ..c.. bash
                         app        3187 f.... python3

    주요 항목은 다음과 같습니다.

    • USER: 프로세스를 실행한 사용자
    • PID: 프로세스 식별 번호
    • ACCESS: 파일이나 디렉터리 사용 상태
    • COMMAND: 실행 중인 프로그램

    예를 들어 bash가 해당 디렉터리를 현재 작업 경로로 사용하거나 Python 프로그램이 공유 파일을 열고 있을 수 있습니다.

    프로세스의 상세 정보를 확인하려면 다음 명령어를 사용합니다.

    ps -fp 3187

    서비스를 종료해야 한다면 해당 프로세스가 어떤 애플리케이션에 속하는지 확인하고 서비스 관리자를 통해 정상적으로 중지하는 것이 좋습니다.

    운영 서버에서는 fuser -km처럼 관련 프로세스를 일괄 종료하는 명령어를 원인 확인 없이 실행해서는 안 됩니다.

    7. NFS 마운트 해제 후 재연결하기

    NFS 서버의 공유 경로와 파일시스템이 정상이며 관련 서비스를 안전하게 중지할 수 있다면 마운트를 해제한 뒤 다시 연결하는 방법을 검토할 수 있습니다.

    먼저 현재 작업 디렉터리가 마운트 내부에 있다면 다른 경로로 이동합니다.

    cd /

    그다음 마운트 해제를 시도합니다.

    sudo umount /mnt/shared

    마운트 해제가 성공했다면 /etc/fstab에 등록된 경우 다음 명령어로 다시 연결합니다.

    sudo mount /mnt/shared

    재연결 후 상태를 확인합니다.

    findmnt --mountpoint /mnt/shared
    ls -la /mnt/shared

    만약 /etc/fstab에 등록되지 않았다면 실제 NFS 서버 주소와 공유 경로, 필요한 마운트 옵션을 확인한 뒤 명시적으로 마운트해야 합니다.

    sudo mount -t nfs4 192.0.2.10:/exports/data /mnt/shared

    위 명령어는 예시이며 실제 서버의 Export 경로와 NFS 버전에 맞게 변경해야 합니다.

    재마운트는 클라이언트가 새로운 파일 핸들을 얻는 데 도움이 될 수 있지만 서버 측 파일시스템이나 Export 구성이 잘못된 경우에는 근본 원인을 해결해야 합니다.

    8. Device or Resource Busy로 마운트 해제가 실패하는 경우

    NFS 마운트를 해제하려는데 다음과 같은 오류가 나타날 수 있습니다.

    umount: /mnt/shared: target is busy.

    이는 마운트 지점을 사용하는 프로세스가 있거나 하위 마운트가 존재할 때 발생할 수 있습니다.

    먼저 점유 프로세스를 확인합니다.

    sudo fuser -vm /mnt/shared

    하위 마운트도 확인합니다.

    findmnt -R /mnt/shared

    점유 프로세스를 확인했다면 정상적인 서비스 종료가 가능한지 먼저 판단해야 합니다.

    직접 실행한 일반 프로세스이며 종료해도 안전하다면 다음과 같이 정상 종료 신호를 보낼 수 있습니다.

    kill -15 3187

    여기서 PID는 실제 확인된 값으로 변경해야 합니다.

    반면 NFS I/O를 기다리는 프로세스가 커널의 인터럽트 불가능한 대기 상태에 있다면 종료 신호를 보내도 즉시 종료되지 않을 수 있습니다.

    따라서 강제 종료를 반복하기보다 NFS 서버 응답 상태와 파일시스템 상태를 함께 확인해야 합니다.

    9. umount -l과 umount -f 사용 시 주의사항

    일반적인 마운트 해제가 실패할 때 다음 명령어를 사용하는 방법이 소개되기도 합니다.

    sudo umount -l /mnt/shared

    -l은 Lazy Unmount 옵션입니다. 마운트 지점을 현재 마운트 네임스페이스에서 분리하고 기존 참조가 해제된 이후 정리가 완료되는 방식입니다.

    따라서 명령어가 성공하더라도 기존 프로세스가 사용하던 파일 핸들이 즉시 해제되거나 NFS 서버 연결 문제가 완전히 해결됐다는 의미는 아닙니다.

    강제 마운트 해제 옵션은 다음과 같습니다.

    sudo umount -f /mnt/shared

    하지만 -f는 파일시스템과 커널 동작에 따라 효과가 다를 수 있으며 모든 상황에서 성공하는 것은 아닙니다.

    특히 운영 서버에서는 미완료 파일 작업이나 애플리케이션의 데이터 접근 상태를 확인하지 않고 강제 마운트 해제를 실행하면 장애가 확대될 수 있습니다.

    가능하면 정상적인 서비스 종료와 일반 마운트 해제를 먼저 시도하고 Lazy 또는 Force 옵션은 마지막 복구 수단으로 검토해야 합니다.

    10. Stale File Handle 오류 재발 방지 방법

    재마운트 이후에도 오류가 반복된다면 NFS 서버와 애플리케이션의 파일 접근 구조를 점검해야 합니다.

    • 공유 디렉터리 변경 관리: 기존 디렉터리를 삭제하고 같은 이름으로 재생성하는 작업 최소화
    • 스토리지 교체 절차 수립: 볼륨 교체나 스냅샷 복원 전 클라이언트 영향 확인
    • NFS Export 설정 관리: 공유 경로와 파일시스템 변경 이력 기록
    • 애플리케이션 복구 처리: ESTALE 오류 발생 시 파일을 다시 열거나 연결을 재설정하도록 설계
    • 서버 로그 모니터링: NFS 오류와 스토리지 변경 기록 확인
    • 배포 방식 점검: 운영 중인 공유 디렉터리를 무분별하게 삭제하거나 교체하지 않도록 관리

    특히 NFS 서버에서 디렉터리를 삭제한 뒤 같은 이름으로 다시 만드는 배포 작업이 반복된다면 클라이언트가 이전 파일 핸들을 계속 참조하는 문제가 발생할 수 있습니다.

    이러한 환경에서는 파일과 디렉터리의 교체 방식, 애플리케이션의 재시도 처리, 공유 스토리지 운영 절차를 함께 개선해야 합니다.

    실전 점검 순서

    리눅스 서버에서 Stale File Handle 오류가 발생했다면 다음 순서로 원인을 확인할 수 있습니다.

    # 1. NFS 마운트 목록 확인
    findmnt -t nfs,nfs4
    
    # 2. 오류 경로의 마운트 확인
    findmnt --mountpoint /mnt/shared
    
    # 3. 파일 접근 오류 확인
    stat /mnt/shared
    
    # 4. NFS 마운트 통계 확인
    nfsstat -m
    
    # 5. 커널 로그 확인
    sudo journalctl -k -b | grep -Ei 'nfs|stale|server not responding'
    
    # 6. 점유 프로세스 확인
    sudo fuser -vm /mnt/shared
    
    # 7. 하위 마운트 확인
    findmnt -R /mnt/shared
    
    # 8. 서버 설정과 서비스 영향 확인 후 마운트 해제
    sudo umount /mnt/shared
    
    # 9. fstab에 등록된 경우 재마운트
    sudo mount /mnt/shared
    
    # 10. 복구 상태 확인
    findmnt --mountpoint /mnt/shared
    ls -la /mnt/shared

    위 명령어는 /mnt/shared가 실제 NFS 마운트 지점이며 NFS 서버의 공유 경로가 정상이라는 가정하에 작성한 예시입니다.

    운영 서버에서는 마운트를 사용하는 프로세스와 서비스 중단 가능성을 확인한 후 마운트 해제 및 재연결을 진행해야 합니다.

    정리

    리눅스 서버에서 Stale File Handle 오류가 발생하는 가장 대표적인 이유는 클라이언트가 보유한 파일 핸들이 NFS 서버에서 더 이상 유효하지 않기 때문입니다.

    공유 디렉터리 삭제 및 재생성, 스토리지 볼륨 교체, 스냅샷 복원, Export 설정 변경 등이 원인이 될 수 있습니다.

    먼저 findmnt로 마운트 정보를 확인하고 journalctl로 관련 로그를 분석한 뒤 fuser로 해당 경로를 사용하는 프로세스를 확인해야 합니다.

    서버의 공유 설정이 정상이고 서비스 중단이 가능한 상황이라면 안전하게 마운트를 해제하고 재연결할 수 있습니다. 다만 서버 측 파일시스템이나 Export 구성에 문제가 있다면 해당 원인을 먼저 해결해야 합니다.

    핵심 점검 순서는 오류 경로 확인 → NFS 마운트 상태 분석 → 서버 변경 이력 점검 → 점유 프로세스 확인 → 안전한 재마운트 → 재발 방지입니다.

  • MySQL에서 Lock Wait Timeout Exceeded 오류가 발생하는 원인과 해결 방법

    MySQL에서 Lock Wait Timeout Exceeded 오류가 발생하는 원인과 해결 방법

    MySQL에서 UPDATE나 DELETE 쿼리를 실행했는데 응답이 오랫동안 없거나, 일정 시간이 지난 뒤 Lock wait timeout exceeded; try restarting transaction 오류가 발생하는 경우가 있습니다. 특히 주문 정보 수정, 재고 차감, 회원 데이터 업데이트처럼 여러 사용자가 같은 데이터를 변경하는 환경에서 발생하기 쉽습니다.

    이 오류가 발생하면 데이터베이스 성능이 부족하거나 SQL 문법에 문제가 있다고 생각하기 쉽습니다. 하지만 대부분은 다른 트랜잭션이 보유한 잠금이 해제되기를 기다리다가 설정된 대기 시간을 초과했기 때문입니다.

    예를 들어 첫 번째 트랜잭션이 특정 주문 데이터를 수정한 뒤 COMMIT을 실행하지 않았다면, 다른 트랜잭션이 같은 데이터를 변경하려 할 때 잠금 대기 상태에 들어갈 수 있습니다. 이 상태가 오래 지속되면 MySQL은 오류 코드 1205를 반환합니다.

    이때 무조건 MySQL 서비스를 재시작하거나 innodb_lock_wait_timeout 값을 크게 늘리면 문제가 일시적으로 사라져도 실제 잠금 충돌은 계속될 수 있습니다.

    이번 글에서는 잠금 대기 확인 → 차단 트랜잭션 식별 → 원인 SQL 분석 → 안전한 트랜잭션 정리 → 재발 방지 순서로 Lock Wait Timeout Exceeded 오류를 해결하는 방법을 살펴봅니다.

    Lock Wait Timeout Exceeded 핵심 SQL 한눈에 보기

    MySQL에서 잠금 대기 오류가 발생했다면 먼저 현재 트랜잭션과 잠금 대기 관계를 확인해야 합니다. 아래 명령어는 주로 MySQL 8.0 환경을 기준으로 작성했습니다.

    점검 목적 SQL 명령어
    잠금 대기 시간 확인 SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
    현재 실행 세션 확인 SHOW FULL PROCESSLIST;
    InnoDB 트랜잭션 조회 SELECT * FROM information_schema.INNODB_TRX;
    잠금 대기 관계 조회 SELECT * FROM performance_schema.data_lock_waits;
    잠금 정보 조회 SELECT * FROM performance_schema.data_locks;
    InnoDB 상태 확인 SHOW ENGINE INNODB STATUS\G

    가장 중요한 것은 잠금을 기다리는 세션과 실제로 잠금을 보유한 세션을 구분하는 것입니다. 오류가 발생한 쿼리만 확인하면 다른 트랜잭션이 잠금을 장시간 유지하고 있다는 근본 원인을 놓칠 수 있습니다.

    MySQL Lock Wait Timeout Exceeded 오류와 InnoDB 트랜잭션 잠금 대기 확인 방법
    MySQL에서 Lock Wait Timeout Exceeded 오류가 발생했을 때 잠금을 보유한 트랜잭션과 대기 중인 세션을 확인하는 예시입니다.

    1. Lock Wait Timeout Exceeded 오류가 발생하는 이유

    MySQL InnoDB 스토리지 엔진은 여러 트랜잭션이 동시에 데이터를 변경할 때 데이터 일관성을 유지하기 위해 잠금 기능을 사용합니다.

    이 과정에서 다른 트랜잭션이 보유한 잠금과 충돌하면 해당 잠금이 해제될 때까지 기다리게 됩니다.

    대기 시간이 설정된 한도를 초과하면 다음 오류가 발생합니다.

    ERROR 1205 (HY000):
    Lock wait timeout exceeded; try restarting transaction

    대표적인 원인은 다음과 같습니다.

    • COMMIT 누락: 데이터 수정 후 트랜잭션을 종료하지 않은 경우
    • 장시간 트랜잭션: 하나의 트랜잭션에서 너무 많은 작업을 수행하는 경우
    • 동일 행 동시 수정: 여러 세션이 같은 레코드를 UPDATE하는 경우
    • 인덱스 부족: 비효율적인 검색으로 더 많은 레코드와 범위에 잠금이 발생하는 경우
    • 잠금 범위 확대: 격리 수준과 쿼리 조건에 따라 갭 락 또는 넥스트 키 락이 발생하는 경우
    • 애플리케이션 오류: 예외 발생 후 트랜잭션이 정리되지 않는 경우
    • DDL과의 충돌: ALTER TABLE 등에서 메타데이터 잠금을 기다리는 경우

    단, innodb_lock_wait_timeout은 일반적인 InnoDB 행 잠금 대기에 적용되며, 메타데이터 잠금 대기에는 별도의 lock_wait_timeout 설정이 사용됩니다.

    2. 두 트랜잭션이 충돌하는 실제 상황

    예를 들어 주문 테이블에서 같은 주문번호를 두 세션이 동시에 수정한다고 가정하겠습니다.

    먼저 첫 번째 세션에서 트랜잭션을 시작합니다.

    -- 세션 A
    START TRANSACTION;
    
    UPDATE orders
    SET status = 'processing'
    WHERE id = 1001;

    여기서 COMMIT을 실행하지 않고 트랜잭션을 유지합니다.

    이 상태에서 두 번째 세션이 동일한 행을 수정합니다.

    -- 세션 B
    START TRANSACTION;
    
    UPDATE orders
    SET status = 'completed'
    WHERE id = 1001;

    세션 B는 세션 A가 보유한 잠금이 해제되기를 기다립니다. 대기 시간이 초과되면 다음과 같은 오류가 발생할 수 있습니다.

    ERROR 1205 (HY000):
    Lock wait timeout exceeded; try restarting transaction

    세션 A에서 COMMIT 또는 ROLLBACK을 실행해 잠금을 해제하면 다른 세션이 해당 행에 접근할 수 있게 됩니다.

    중요한 점은 잠금 대기 시간이 초과됐다고 해서 기본 설정에서 트랜잭션 전체가 자동으로 롤백되는 것은 아니라는 사실입니다.

    기본적으로 InnoDB는 시간 초과가 발생한 명령문만 롤백합니다. 따라서 세션 B에서 이전에 실행한 변경 작업이 있다면 트랜잭션이 계속 열린 상태로 남을 수 있습니다.

    실패한 작업을 처음부터 다시 시도해야 한다면 애플리케이션에서 트랜잭션 전체를 ROLLBACK하고 새 트랜잭션으로 재시도하는 것이 일반적으로 안전합니다.

    3. SHOW FULL PROCESSLIST로 실행 중인 세션 확인하기

    잠금 대기가 발생했다면 먼저 MySQL에서 어떤 세션이 실행 중인지 확인합니다.

    SHOW FULL PROCESSLIST;

    출력에는 세션 ID, 사용자, 접속 호스트, 실행 상태와 SQL 정보 등이 표시됩니다.

    주요 항목은 다음과 같습니다.

    • Id: MySQL 연결 세션 ID
    • User: 연결 사용자
    • Host: 클라이언트 접속 정보
    • Command: Query 또는 Sleep 등 현재 명령 상태
    • Time: 현재 상태가 지속된 시간
    • State: 서버 내부 처리 상태
    • Info: 현재 실행 중인 SQL 문

    특히 Sleep 상태인 세션이라도 트랜잭션을 종료하지 않았다면 잠금을 유지할 수 있습니다.

    따라서 Sleep 세션을 모두 정상 상태로 판단하거나, 반대로 오래된 Sleep 세션을 무조건 종료하는 것은 적절하지 않습니다.

    4. INNODB_TRX로 장시간 열린 트랜잭션 찾기

    현재 실행 중인 InnoDB 트랜잭션은 다음 SQL로 조회할 수 있습니다.

    SELECT
        trx_id,
        trx_state,
        trx_started,
        trx_mysql_thread_id,
        trx_query
    FROM information_schema.INNODB_TRX
    ORDER BY trx_started;

    여기서 trx_started는 트랜잭션 시작 시각, trx_mysql_thread_id는 연결 세션 ID를 나타냅니다.

    trx_state가 LOCK WAIT라면 해당 트랜잭션이 잠금을 기다리는 상태일 수 있습니다.

    다만 오래된 트랜잭션이라고 해서 반드시 현재 발생한 잠금 충돌의 원인인 것은 아닙니다. 실제 차단 관계는 다음 단계에서 확인해야 합니다.

    5. Performance Schema로 잠금 대기 관계 확인하기

    MySQL 8.0에서는 performance_schema.data_lock_waits와 data_locks를 사용해 잠금 대기 관계를 확인할 수 있습니다.

    다음 SQL은 잠금을 기다리는 트랜잭션과 잠금을 차단하는 트랜잭션을 함께 조회하는 예시입니다.

    SELECT
        w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx_id,
        w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx_id,
        r.OBJECT_SCHEMA,
        r.OBJECT_NAME,
        r.INDEX_NAME,
        r.LOCK_TYPE AS waiting_lock_type,
        r.LOCK_MODE AS waiting_lock_mode,
        b.LOCK_MODE AS blocking_lock_mode
    FROM performance_schema.data_lock_waits AS w
    JOIN performance_schema.data_locks AS r
      ON w.REQUESTING_ENGINE_LOCK_ID = r.ENGINE_LOCK_ID
    JOIN performance_schema.data_locks AS b
      ON w.BLOCKING_ENGINE_LOCK_ID = b.ENGINE_LOCK_ID;

    이 결과를 통해 어느 테이블과 인덱스에서 잠금 충돌이 발생하고 있는지 확인할 수 있습니다.

    다만 이 SQL은 트랜잭션 ID와 잠금 정보를 보여주므로 실제 연결 세션을 종료하려면 세션 ID까지 확인해야 합니다.

    MySQL의 sys.innodb_lock_waits 뷰를 사용할 수 있는 환경이라면 더 간단하게 대기 세션과 차단 세션을 조회할 수 있습니다.

    SELECT
        waiting_pid,
        blocking_pid,
        locked_table,
        waiting_query,
        blocking_query
    FROM sys.innodb_lock_waits;

    여기서 blocking_pid는 차단 세션의 연결 ID입니다. 단, 해당 뷰가 설치되어 있고 필요한 조회 권한이 있어야 합니다.

    차단 트랜잭션이 현재 아무 SQL도 실행하지 않는 상태라면 blocking_query가 NULL일 수 있습니다. 이는 해당 트랜잭션이 잠금을 보유하지 않는다는 의미가 아닙니다.

    6. 차단 세션을 안전하게 종료하는 방법

    잠금 충돌의 원인이 되는 세션을 확인했다면 먼저 애플리케이션에서 해당 트랜잭션을 정상적으로 종료할 수 있는지 확인해야 합니다.

    정상적인 트랜잭션 종료 방법은 다음 두 가지입니다.

    COMMIT;
    ROLLBACK;

    단, 위 명령어는 현재 연결된 세션의 트랜잭션에 적용됩니다. 다른 세션에서 실행한다고 해서 차단 세션의 트랜잭션이 종료되는 것은 아닙니다.

    차단 세션이 응답하지 않고 운영상 종료가 필요하다고 판단된다면 관리 권한을 가진 계정에서 해당 연결을 종료할 수 있습니다.

    KILL CONNECTION 123;

    여기서 123은 실제 MySQL 연결 ID입니다. 트랜잭션 ID나 운영체제 PID를 입력하는 것이 아닙니다.

    연결이 종료되면 미완료 트랜잭션의 롤백이 진행될 수 있습니다. 대량의 변경 작업을 수행한 트랜잭션은 롤백에 시간이 걸릴 수 있습니다.

    또한 KILL QUERY는 현재 실행 중인 SQL을 중단하는 명령이며, 연결이나 트랜잭션 전체를 종료하는 명령과는 다릅니다.

    따라서 차단 세션을 종료하기 전에 실제 서비스와 연결된 작업인지, 데이터 변경 중인지, 종료 후 롤백 영향이 어느 정도인지 반드시 확인해야 합니다.

    7. innodb_lock_wait_timeout 값을 늘리면 해결될까?

    MySQL에서는 innodb_lock_wait_timeout으로 InnoDB 행 잠금 대기 시간을 설정할 수 있습니다.

    현재 값을 확인합니다.

    SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';

    일반적인 기본값은 50초입니다. 다만 서버 버전과 운영 설정에 따라 실제 적용값은 다를 수 있습니다.

    특정 세션에서 임시로 값을 변경하려면 다음과 같이 실행할 수 있습니다.

    SET SESSION innodb_lock_wait_timeout = 30;

    이 설정은 현재 세션에 적용됩니다.

    하지만 잠금 대기 시간을 늘리는 것은 잠금 충돌 자체를 제거하는 해결 방법이 아닙니다.

    잠금을 보유한 트랜잭션이 오래 유지되는 상황이라면 대기 시간을 늘려도 응답 지연과 연결 누적이 심해질 수 있습니다.

    따라서 먼저 잠금 충돌 원인을 해결하고, 서비스의 정상적인 트랜잭션 처리 시간에 맞춰 적절한 대기 시간을 설정해야 합니다.

    8. 인덱스와 트랜잭션 구조 개선하기

    잠금 대기 오류가 반복된다면 SQL 실행 계획과 트랜잭션 범위를 점검해야 합니다.

    예를 들어 특정 주문번호를 수정하는 쿼리가 있다면 해당 조건에 적절한 인덱스가 있는지 확인합니다.

    EXPLAIN
    UPDATE orders
    SET status = 'completed'
    WHERE id = 1001;

    적절한 인덱스가 없다면 많은 레코드를 검사하면서 불필요하게 넓은 범위에 잠금이 발생할 수 있습니다.

    특히 InnoDB에서는 인덱스 검색 조건과 트랜잭션 격리 수준에 따라 레코드 락, 갭 락, 넥스트 키 락의 범위가 달라질 수 있습니다.

    다음 사항을 함께 점검하는 것이 좋습니다.

    • UPDATE와 DELETE 조건에 적절한 인덱스가 있는지 확인
    • 트랜잭션 안에서 외부 API 호출이나 장시간 작업을 수행하지 않도록 개선
    • 필요한 데이터만 수정하고 가능한 한 빠르게 COMMIT 실행
    • 여러 테이블을 수정할 때 일관된 잠금 획득 순서 유지
    • 대량 변경 작업을 적절한 크기의 배치로 분할
    • 잠금 대기 오류 발생 시 ROLLBACK 후 제한된 횟수로 재시도

    특히 자동 재시도 로직에서는 동일한 작업이 중복 처리되지 않도록 트랜잭션 경계와 멱등성을 고려해야 합니다.

    9. Lock Wait Timeout과 Deadlock의 차이

    잠금 대기 오류와 함께 자주 언급되는 문제가 Deadlock입니다. 하지만 두 오류는 발생 원리가 다릅니다.

    구분 Lock Wait Timeout Deadlock
    대표 오류 코드 1205 1213
    발생 원인 잠금 대기 시간 초과 트랜잭션 간 순환 대기
    대표 메시지 Lock wait timeout exceeded Deadlock found when trying to get lock
    기본 롤백 동작 시간 초과된 명령문 롤백 선택된 희생 트랜잭션 롤백
    우선 점검 장시간 트랜잭션과 차단 세션 잠금 획득 순서와 충돌 구조

    Deadlock은 트랜잭션들이 서로 상대방의 잠금 해제를 기다리는 순환 대기 상태입니다.

    반면 Lock Wait Timeout은 반드시 순환 대기가 발생한 것은 아니며, 다른 트랜잭션이 잠금을 너무 오래 유지해도 발생할 수 있습니다.

    따라서 두 오류를 구분해 로그와 트랜잭션 구조를 분석해야 합니다.

    실전 점검 순서

    MySQL에서 Lock Wait Timeout Exceeded 오류가 반복된다면 다음 순서로 확인할 수 있습니다.

    -- 1. 잠금 대기 시간 확인
    SHOW VARIABLES LIKE 'innodb_lock_wait_timeout';
    
    -- 2. 현재 세션 확인
    SHOW FULL PROCESSLIST;
    
    -- 3. 실행 중인 트랜잭션 확인
    SELECT
        trx_id,
        trx_state,
        trx_started,
        trx_mysql_thread_id,
        trx_query
    FROM information_schema.INNODB_TRX
    ORDER BY trx_started;
    
    -- 4. 잠금 대기 관계 확인
    SELECT
        waiting_pid,
        blocking_pid,
        locked_table,
        waiting_query,
        blocking_query
    FROM sys.innodb_lock_waits;
    
    -- 5. InnoDB 상태 확인
    SHOW ENGINE INNODB STATUS\G
    
    -- 6. 실제 차단 세션과 영향 확인 후
    -- 필요한 경우에만 연결 종료
    -- KILL CONNECTION 123;

    위 명령어는 MySQL 8.0에서 사용할 수 있는 대표적인 점검 예시입니다. 버전과 권한, Performance Schema 설정에 따라 조회 가능한 정보가 달라질 수 있습니다.

    특히 차단 세션을 발견했다고 해서 즉시 종료하지 말고 트랜잭션이 수행 중인 작업과 서비스 영향을 먼저 확인해야 합니다.

    정리

    MySQL에서 Lock Wait Timeout Exceeded 오류가 발생하는 이유는 대부분 다른 트랜잭션이 보유한 잠금이 해제되지 않아 설정된 대기 시간을 초과했기 때문입니다.

    먼저 SHOW FULL PROCESSLIST와 INNODB_TRX로 세션과 트랜잭션 상태를 확인하고, sys.innodb_lock_waits 또는 Performance Schema로 실제 차단 관계를 분석해야 합니다.

    잠금 대기 시간을 늘리거나 MySQL을 재시작하기보다 장시간 열린 트랜잭션, COMMIT 누락, 비효율적인 인덱스, 과도한 트랜잭션 범위를 개선하는 것이 근본적인 해결 방법입니다.

    핵심 점검 순서는 잠금 대기 확인 → 차단 세션 식별 → 원인 SQL 분석 → 안전한 트랜잭션 정리 → 인덱스와 애플리케이션 개선입니다.

  • Nginx에서 413 Request Entity Too Large 오류가 발생하는 이유

    Nginx에서 413 Request Entity Too Large 오류가 발생하는 이유

    Nginx 웹서버에서 이미지나 동영상, 압축파일을 업로드하려는데 갑자기 413 Request Entity Too Large 오류가 발생하는 경우가 있습니다. 작은 파일은 정상적으로 업로드되지만 특정 크기 이상의 파일만 실패하거나, API 요청을 보낼 때 413 상태 코드가 반환되는 현상도 나타날 수 있습니다.

    이런 오류가 발생하면 서버 저장 공간이 부족하거나 파일 권한에 문제가 있다고 생각하기 쉽습니다. 하지만 413 오류는 대부분 클라이언트가 전송한 HTTP 요청 본문(Request Body)의 크기가 서버 또는 중간 프록시에서 허용한 제한을 초과했을 때 발생합니다.

    특히 Nginx의 client_max_body_size 설정이 실제 업로드 크기보다 작거나, Nginx 앞단의 CDN·로드밸런서, 뒷단의 PHP·애플리케이션 서버가 별도의 용량 제한을 적용하고 있다면 설정을 한 곳만 변경해도 문제가 계속될 수 있습니다.

    이번 글에서는 413 오류 발생 위치 확인 → Nginx 업로드 제한 점검 → client_max_body_size 변경 → PHP 및 프록시 제한 확인 → 설정 검증 순서로 안전하게 해결하는 방법을 살펴봅니다.

    413 Request Entity Too Large 핵심 명령어 한눈에 보기

    413 오류가 발생했다면 아래 명령어로 Nginx 설정과 오류 로그를 먼저 확인할 수 있습니다.

    점검 목적 명령어
    Nginx 설정 문법 검사 sudo nginx -t
    현재 적용 설정 출력 sudo nginx -T
    업로드 제한 설정 검색 sudo nginx -T 2>&1 | grep -n client_max_body_size
    Nginx 오류 로그 확인 sudo tail -n 100 /var/log/nginx/error.log
    설정 다시 불러오기 sudo systemctl reload nginx
    PHP 업로드 제한 확인 php -i | grep -E 'upload_max_filesize|post_max_size'

    PHP CLI에서 조회한 설정과 실제 PHP-FPM 웹 요청에 적용되는 설정은 다를 수 있습니다. 따라서 PHP를 사용하는 환경에서는 웹 요청을 처리하는 PHP-FPM의 설정도 별도로 확인해야 합니다.

    Nginx 413 Request Entity Too Large 오류와 client_max_body_size 설정 확인 화면
    Nginx에서 413 오류가 발생했을 때 요청 본문 크기 제한과 client_max_body_size 설정을 확인하는 예시입니다.

    1. Nginx 413 오류가 발생하는 원인

    HTTP 413은 요청 본문의 크기가 서버에서 처리하도록 허용한 크기를 초과했다는 의미입니다. 현재 HTTP 표준에서는 413 Content Too Large라는 명칭을 사용하지만, Nginx 오류 페이지나 기존 문서에서는 413 Request Entity Too Large라는 표현도 널리 사용됩니다.

    대표적인 발생 원인은 다음과 같습니다.

    • Nginx 업로드 제한: client_max_body_size보다 큰 요청을 전송한 경우
    • PHP 제한: upload_max_filesize 또는 post_max_size가 부족한 경우
    • 애플리케이션 제한: Node.js, Django 등에서 요청 본문 크기를 제한한 경우
    • 프록시 제한: CDN, API Gateway 또는 로드밸런서에서 요청을 차단한 경우
    • 요청 크기 증가: 파일 외에 multipart 데이터나 JSON 본문이 추가된 경우
    • 설정 적용 오류: 다른 server 또는 location 블록의 설정이 적용되는 경우

    중요한 점은 413 오류가 반드시 Nginx 자체에서 생성된 것은 아니라는 사실입니다. 중간 프록시나 애플리케이션 서버가 413을 반환할 수도 있으므로 어느 계층에서 제한이 발생했는지 구분해야 합니다.

    2. 오류 로그에서 요청 크기 초과 여부 확인하기

    Nginx가 요청 본문을 차단했다면 오류 로그에 관련 메시지가 기록될 수 있습니다.

    sudo tail -n 100 /var/log/nginx/error.log

    대표적인 메시지는 다음과 같습니다.

    client intended to send too large body: 12582912 bytes

    위 예시는 클라이언트가 약 12MiB 크기의 요청 본문을 전송하려 했다는 의미입니다. 설정된 허용 크기보다 요청 본문이 크면 Nginx가 413 응답을 반환할 수 있습니다.

    다만 로그 파일의 위치는 배포판과 Nginx 설정에 따라 다르며, 로그 레벨이나 요청 처리 경로에 따라 같은 메시지가 남지 않을 수도 있습니다.

    따라서 Nginx 로그에 관련 기록이 없다면 앞단 프록시와 뒷단 애플리케이션 로그도 확인해야 합니다.

    3. client_max_body_size 설정 확인하기

    Nginx에서 HTTP 요청 본문의 최대 허용 크기를 제어하는 대표적인 지시어는 client_max_body_size입니다.

    Nginx의 기본값은 일반적으로 1m이며, 설정 파일에서 명시적으로 변경할 수 있습니다.

    sudo nginx -T 2>&1 | grep -n client_max_body_size

    이 명령어는 Nginx가 읽는 설정 파일들을 출력한 뒤 해당 지시어가 포함된 줄을 검색합니다.

    다만 검색 결과만으로 어떤 server 또는 location 블록에 설정이 적용되는지 확정할 수는 없습니다. 실제 요청을 처리하는 가상 호스트와 경로의 설정 구조를 확인해야 합니다.

    client_max_body_size는 http, server, location 컨텍스트에 지정할 수 있습니다. 하위 컨텍스트에서 별도로 설정하면 해당 범위의 값이 적용됩니다.

    4. Nginx 업로드 용량 제한 변경하기

    예를 들어 최대 50MiB의 요청 본문을 허용하려면 해당 사이트의 server 블록에 다음 설정을 추가할 수 있습니다.

    server {
        listen 80;
        server_name example.com;
    
        client_max_body_size 50m;
    
        # 기존 사이트 설정
    }

    특정 업로드 경로에만 제한을 적용하려면 해당 location 블록에 설정할 수도 있습니다.

    location /upload/ {
        client_max_body_size 50m;
    
        proxy_pass http://127.0.0.1:3000;
    }

    위 코드는 프록시를 사용하는 환경의 예시입니다. 실제 사이트에 이미 location 블록이 있다면 새 블록을 무작정 추가하지 말고 기존 요청 처리 설정에 맞춰 수정해야 합니다.

    또한 50m은 요청 본문 전체에 적용되는 제한입니다. multipart/form-data 업로드에서는 파일 내용 외에도 폼 필드와 구분자 등의 데이터가 포함되므로 파일 크기와 요청 본문 크기가 정확히 일치하지 않을 수 있습니다.

    따라서 최대 50MiB 파일을 허용하려는 목적이라면 실제 요청 본문 크기를 고려해 적절한 여유를 두는 것이 좋습니다.

    5. 설정을 수정했는데 413 오류가 계속 발생하는 경우

    client_max_body_size를 늘렸는데도 오류가 계속된다면 설정이 실제로 적용됐는지 먼저 확인해야 합니다.

    sudo nginx -t

    정상적인 경우 다음과 비슷한 결과가 표시됩니다.

    nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
    nginx: configuration file /etc/nginx/nginx.conf test is successful

    설정 문법이 정상이라면 변경 사항을 다시 불러옵니다.

    sudo systemctl reload nginx

    이후 실제 적용된 설정을 확인합니다.

    sudo nginx -T

    다음 항목도 함께 점검해야 합니다.

    • 설정을 수정한 파일이 실제 Nginx에서 로드되는지
    • 요청 도메인이 올바른 server 블록과 일치하는지
    • 요청 경로에 다른 location 설정이 적용되는지
    • Docker 컨테이너 내부의 Nginx를 별도로 사용하고 있는지
    • CDN 또는 리버스 프록시가 앞단에서 요청을 제한하는지

    특히 여러 개의 Nginx 인스턴스나 프록시를 운영하는 환경에서는 한 서버의 설정만 변경해도 실제 요청이 다른 서버를 거칠 수 있습니다.

    6. PHP 업로드 제한도 함께 확인하기

    워드프레스처럼 PHP를 사용하는 웹사이트라면 Nginx 제한 외에도 PHP 업로드 설정을 확인해야 합니다.

    PHP 설정에서 주로 확인하는 항목은 다음과 같습니다.

    upload_max_filesize = 50M
    post_max_size = 60M

    upload_max_filesize는 개별 업로드 파일의 크기 제한이며, post_max_size는 POST 요청 데이터 전체의 크기 제한입니다.

    따라서 일반적으로 post_max_size는 upload_max_filesize보다 충분히 크게 설정해야 합니다. 여러 파일을 한 번에 업로드한다면 전체 요청 크기도 고려해야 합니다.

    또한 PHP의 memory_limit와 애플리케이션의 파일 처리 방식에 따라 별도의 메모리 문제가 발생할 수 있지만, 메모리 제한이 곧바로 Nginx의 413 오류를 의미하는 것은 아닙니다.

    PHP-FPM 설정을 수정했다면 해당 서비스의 설정을 검증하고 실제 PHP-FPM 서비스에 변경 사항을 반영해야 합니다. 서비스 이름은 설치된 PHP 버전에 따라 다릅니다.

    7. Node.js와 API 서버에서 발생하는 413 오류

    Nginx가 Node.js 애플리케이션으로 요청을 전달하는 구조에서는 Nginx 제한을 늘려도 애플리케이션에서 별도의 요청 크기 제한이 적용될 수 있습니다.

    예를 들어 Express의 JSON 요청 본문 파서는 다음과 같이 크기 제한을 설정할 수 있습니다.

    app.use(express.json({ limit: '10mb' }));

    이 경우 Nginx에서 50MiB를 허용하더라도 JSON 본문이 애플리케이션의 10MB 제한을 초과하면 요청이 거부될 수 있습니다.

    다만 JSON 본문 제한과 multipart 파일 업로드 제한은 서로 다른 미들웨어에서 처리될 수 있으므로 구분해야 합니다.

    실제 413 응답이 Nginx에서 생성된 것인지 애플리케이션에서 생성된 것인지 로그와 응답 내용을 비교하는 것이 중요합니다.

    8. client_max_body_size를 무제한으로 설정해도 될까?

    Nginx에서는 다음과 같이 설정할 수 있습니다.

    client_max_body_size 0;

    이 값은 Nginx의 요청 본문 크기 검사를 비활성화합니다. 하지만 무제한 업로드를 허용하면 서버 자원 사용량과 악의적인 대용량 요청에 대한 위험이 커질 수 있습니다.

    따라서 운영 환경에서는 가능한 한 서비스에 필요한 업로드 크기를 기준으로 적절한 상한을 설정하는 것이 좋습니다.

    또한 업로드 제한을 높일 때는 디스크 용량, 임시 파일 저장 경로, 업로드 처리 시간, 애플리케이션 검증 및 접근 제어도 함께 고려해야 합니다.

    실전 점검 순서

    Nginx에서 413 Request Entity Too Large 오류가 발생했다면 다음 순서로 점검할 수 있습니다.

    # 1. Nginx 오류 로그 확인
    sudo tail -n 100 /var/log/nginx/error.log
    
    # 2. 현재 업로드 제한 설정 검색
    sudo nginx -T 2>&1 | grep -n client_max_body_size
    
    # 3. 실제 가상 호스트와 location 설정 확인
    sudo nginx -T
    
    # 4. 설정 수정 후 문법 검사
    sudo nginx -t
    
    # 5. 정상일 때 설정 다시 불러오기
    sudo systemctl reload nginx
    
    # 6. PHP 사용 환경의 제한 확인
    php -i | grep -E 'upload_max_filesize|post_max_size'
    
    # 7. 오류 로그 재확인
    sudo tail -n 100 /var/log/nginx/error.log

    PHP CLI의 설정 조회는 참고용이며, PHP-FPM을 사용하는 경우에는 실제 웹 요청에 적용되는 설정을 별도로 확인해야 합니다.

    설정 변경 후에는 기존에 실패했던 파일과 허용 크기보다 큰 테스트 파일을 각각 업로드해 제한이 의도한 대로 동작하는지 확인하는 것이 좋습니다.

    정리

    Nginx에서 413 Request Entity Too Large 오류가 발생하는 가장 대표적인 이유는 요청 본문의 크기가 서버에서 허용한 제한을 초과했기 때문입니다.

    Nginx가 요청을 차단했다면 client_max_body_size 설정을 확인하고, PHP 또는 애플리케이션이 요청을 처리하는 환경에서는 각 계층의 업로드 제한도 함께 점검해야 합니다.

    특히 Nginx 설정을 변경했는데도 413 오류가 계속된다면 다른 server·location 설정, CDN·로드밸런서, 애플리케이션 자체 제한을 확인해야 합니다.

    핵심은 오류 발생 위치 확인 → 요청 크기 제한 점검 → 필요한 범위만 설정 변경 → nginx -t 검증 → 실제 업로드 테스트 순서를 지키는 것입니다.

  • SSH 접속 시 Host Key Verification Failed 오류가 발생하는 이유

    SSH 접속 시 Host Key Verification Failed 오류가 발생하는 이유

    SSH로 정상 접속하던 리눅스 서버에 다시 연결하려는데 갑자기 WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED!라는 경고와 함께 Host Key Verification Failed 오류가 발생한 경험이 있으신가요? 서버를 재설치하거나 클라우드 인스턴스를 교체한 뒤, 또는 동일한 IP 주소에 다른 서버를 연결한 이후에 이런 문제가 발생할 수 있습니다.

    서버는 정상적으로 실행 중이고 네트워크 연결에도 문제가 없어 보이는데 SSH 접속만 거부되면 비밀번호나 SSH 포트 설정부터 확인하기 쉽습니다. 하지만 이 오류는 대부분 사용자 계정 인증 이전에 수행되는 서버 신원 확인 과정에서 문제가 발견됐다는 의미입니다.

    SSH 클라이언트는 이전에 접속했던 서버의 공개 호스트 키를 known_hosts 파일에 저장하고, 다음 접속에서 서버가 제시하는 키와 비교합니다. 저장된 키와 현재 서버의 키가 다르면 서버가 실제로 교체된 것인지, 설정이 변경된 것인지, 또는 다른 서버로 연결되고 있는지 확인해야 합니다.

    이때 인터넷에서 찾은 명령어로 known_hosts를 무조건 삭제하거나 호스트 키 검증 기능을 끄는 것은 안전한 해결 방법이 아닙니다. 이번 글에서는 오류 메시지 확인 → 저장된 호스트 키 조회 → 서버 신원 검증 → 문제가 있는 항목 수정 → SSH 재접속 순서로 해결 방법을 살펴봅니다.

    SSH Host Key Verification Failed 핵심 명령어 한눈에 보기

    SSH 호스트 키 오류가 발생했다면 아래 명령어로 저장된 키와 연결 상태를 확인할 수 있습니다. 예제의 192.0.2.10은 설명용 IP 주소이므로 실제 서버 주소로 변경해야 합니다.

    점검 목적 명령어
    SSH 상세 오류 확인 ssh -vvv user@192.0.2.10
    저장된 호스트 키 검색 ssh-keygen -F 192.0.2.10
    기존 호스트 키 항목 제거 ssh-keygen -R 192.0.2.10
    비표준 포트 키 항목 제거 ssh-keygen -R '[192.0.2.10]:2222'
    서버의 ED25519 키 지문 확인 sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

    주의할 점은 ssh-keygen -R 명령어를 실행하기 전에 서버의 호스트 키가 변경된 이유와 새 키의 지문을 신뢰할 수 있는 경로로 확인해야 한다는 것입니다. 저장된 키를 삭제하는 작업 자체가 새 서버의 신원을 보증하지는 않습니다.

    SSH Host Key Verification Failed 오류와 REMOTE HOST IDENTIFICATION HAS CHANGED 경고 화면
    SSH 접속 시 저장된 서버 호스트 키와 현재 서버의 키가 일치하지 않을 때 표시되는 경고 메시지 예시입니다.

    1. Host Key Verification Failed 오류가 발생하는 원인

    SSH는 암호화된 연결을 만드는 과정에서 접속 대상 서버의 신원을 확인합니다. 이때 사용하는 것이 서버의 SSH Host Key입니다. 사용자의 로그인에 사용하는 SSH 개인 키와는 역할이 다릅니다.

    클라이언트는 이전에 신뢰한 서버의 공개 호스트 키를 일반적으로 ~/.ssh/known_hosts에 저장합니다. 이후 동일한 서버 주소로 연결할 때 현재 서버가 제시한 키와 비교합니다.

    대표적인 오류 원인은 다음과 같습니다.

    • 서버 재설치: 운영체제 재설치 과정에서 SSH 호스트 키가 새로 생성된 경우
    • 클라우드 인스턴스 교체: 동일한 IP에 새로운 가상 서버가 연결된 경우
    • SSH 호스트 키 교체: 보안 정책이나 관리 작업으로 서버 키가 변경된 경우
    • DNS 또는 IP 변경: 기존 서버가 아닌 다른 서버로 연결되는 경우
    • known_hosts 문제: 오래된 키, 잘못된 항목 또는 신뢰할 수 없는 키가 저장된 경우
    • 보안 위협: 중간자 공격이나 연결 경로 변조 가능성이 있는 경우

    특히 중요한 것은 호스트 키 불일치가 반드시 공격을 의미하지는 않지만, 공격 가능성을 배제할 수 있는 오류도 아니라는 점입니다.

    따라서 최근 서버 교체 이력이 없는데 갑자기 키가 달라졌다면 기존 항목을 삭제하기 전에 서버 관리자나 클라우드 콘솔 등을 통해 원인을 확인해야 합니다.

    2. SSH 오류 메시지에서 확인해야 할 부분

    호스트 키가 변경된 서버에 접속하면 다음과 비슷한 경고가 나타날 수 있습니다.

    $ ssh user@192.0.2.10
    
    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    @ WARNING: REMOTE HOST IDENTIFICATION HAS CHANGED! @
    @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
    
    IT IS POSSIBLE THAT SOMEONE IS DOING SOMETHING NASTY!
    The fingerprint for the ED25519 key sent by the remote host is
    SHA256:EXAMPLE_FINGERPRINT_NOT_A_REAL_KEY
    
    Offending ED25519 key in /home/user/.ssh/known_hosts:12
    Host key verification failed.

    위 내용은 설명을 위해 일부를 간략화한 출력 예시입니다. 실제 메시지의 문구와 순서는 OpenSSH 버전 및 설정에 따라 달라질 수 있습니다.

    여기서 가장 중요한 정보는 다음 세 가지입니다.

    • ED25519: 서버가 제시한 호스트 키의 알고리즘
    • SHA256 지문: 현재 연결 대상 서버가 제시한 키의 식별값
    • known_hosts:12: 충돌이 감지된 저장 키의 파일 위치와 줄 번호

    이 메시지가 나타났다면 SSH 클라이언트가 기존에 저장된 서버 신원과 현재 연결 대상의 신원이 일치하지 않는다고 판단한 것입니다.

    이 단계에서 바로 known_hosts 파일 전체를 삭제하기보다 문제가 발생한 호스트와 해당 키를 확인하는 것이 좋습니다.

    3. known_hosts 파일에 저장된 서버 키 확인하기

    OpenSSH는 사용자가 신뢰한 서버 호스트 키를 일반적으로 ~/.ssh/known_hosts 파일에 저장합니다.

    특정 IP 주소에 저장된 항목을 검색하려면 다음 명령어를 사용합니다.

    ssh-keygen -F 192.0.2.10

    이 명령어는 해시 처리된 호스트 이름이 저장된 경우에도 해당 호스트의 항목을 검색하는 데 사용할 수 있습니다.

    SSH 접속에 2222번과 같은 비표준 포트를 사용했다면 호스트와 포트를 함께 지정해야 할 수 있습니다.

    ssh-keygen -F '[192.0.2.10]:2222'

    기존 항목을 직접 확인하려면 다음 명령어를 사용할 수도 있습니다.

    cat ~/.ssh/known_hosts

    다만 파일에는 여러 서버의 키가 저장되어 있을 수 있고, 호스트 이름이 해시 처리되어 있다면 내용을 직접 읽어도 어떤 서버인지 구분하기 어렵습니다.

    따라서 특정 서버의 키를 확인할 때는 ssh-keygen -F 명령어를 우선 사용하는 것이 편리합니다.

    4. 서버의 새로운 호스트 키 지문을 안전하게 확인하기

    기존 키를 수정하기 전에 가장 중요한 작업은 현재 접속하려는 서버가 실제로 신뢰할 수 있는 서버인지 확인하는 것입니다.

    서버에 클라우드 제공업체의 웹 콘솔, 가상 머신 콘솔 또는 별도의 관리 경로로 접근할 수 있다면 해당 서버에서 직접 호스트 공개 키의 지문을 확인할 수 있습니다.

    sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub

    출력되는 SHA256 지문을 SSH 클라이언트가 보여준 지문과 비교합니다. 두 지문이 일치하고 서버 변경 사유도 확인됐다면 새로운 호스트 키를 신뢰할 근거가 됩니다.

    단, 서버가 RSA나 ECDSA 등 다른 알고리즘의 호스트 키를 제시했다면 실제 제시된 키와 동일한 알고리즘의 공개 키 지문을 비교해야 합니다.

    SSH 연결 자체가 의심되는 상황에서 해당 SSH 연결을 통해서만 새 키를 확인하는 것은 독립적인 신원 검증이 아닙니다. 가능하면 클라우드 콘솔이나 신뢰할 수 있는 서버 관리자 등 별도의 경로를 사용해야 합니다.

    5. ssh-keygen -R로 잘못된 호스트 키 항목 수정하기

    서버가 정상적으로 재설치되었거나 호스트 키가 정당하게 변경된 사실을 확인했다면 기존에 저장된 키 항목을 제거할 수 있습니다.

    먼저 필요하다면 known_hosts 파일을 백업합니다.

    cp ~/.ssh/known_hosts ~/.ssh/known_hosts.bak

    그다음 해당 서버의 기존 항목만 제거합니다.

    ssh-keygen -R 192.0.2.10

    비표준 포트를 사용하는 경우에는 다음과 같이 지정합니다.

    ssh-keygen -R '[192.0.2.10]:2222'

    이 명령어는 지정한 호스트에 해당하는 항목을 known_hosts 파일에서 제거하는 데 사용됩니다.

    도메인 이름과 IP 주소를 각각 사용해 접속한 적이 있다면 두 이름에 대한 저장 항목을 별도로 확인해야 할 수 있습니다.

    ssh-keygen -F server.example.com
    ssh-keygen -F 192.0.2.10

    중요한 것은 파일 전체를 삭제하지 않고 문제가 확인된 호스트의 항목만 정리하는 것입니다.

    6. SSH 재접속 시 새 호스트 키 등록하기

    기존 키 항목을 정리한 뒤에는 SSH 연결을 다시 시도합니다.

    ssh user@192.0.2.10

    처음 접속하는 서버로 인식되면 다음과 같은 확인 메시지가 나타날 수 있습니다.

    The authenticity of host '192.0.2.10' can't be established.
    ED25519 key fingerprint is SHA256:EXAMPLE_FINGERPRINT_NOT_A_REAL_KEY
    
    Are you sure you want to continue connecting (yes/no/[fingerprint])?

    여기에서 바로 yes를 입력하는 것이 아니라 앞서 신뢰할 수 있는 경로에서 확인한 서버의 호스트 키 지문과 화면에 표시된 지문이 일치하는지 비교해야 합니다.

    지문이 일치하고 접속 대상이 올바르다는 사실을 확인했다면 새 키를 등록하고 접속을 계속할 수 있습니다.

    반대로 지문이 일치하지 않는다면 연결을 중단하고 DNS, IP 주소, 서버 교체 여부, 네트워크 경로를 다시 확인해야 합니다.

    7. StrictHostKeyChecking=no를 사용해도 될까?

    SSH 호스트 키 오류를 검색하면 다음과 같은 명령어를 해결 방법으로 소개하는 경우가 있습니다.

    ssh -o StrictHostKeyChecking=no user@192.0.2.10

    하지만 StrictHostKeyChecking=no는 일부 호스트 키 확인 동작을 완화하는 옵션이며, 기존 호스트 키가 변경된 상황에서도 연결을 무조건 안전하게 만들어주지는 않습니다. OpenSSH 버전과 설정에 따라 연결 제한이나 경고가 남을 수 있습니다.

    또한 다음과 같이 호스트 키 저장을 사실상 무력화하는 방식도 운영 서버에서는 피하는 것이 좋습니다.

    ssh -o UserKnownHostsFile=/dev/null \
        -o StrictHostKeyChecking=no \
        user@192.0.2.10

    이런 설정은 서버 신원 확인을 약화시켜 잘못된 서버에 연결하거나 중간자 공격을 감지하지 못할 위험을 높입니다.

    호스트 키 오류를 해결하기 위해 보안 검증 기능을 끄는 것보다 실제 서버의 키를 확인하고 known_hosts 항목을 올바르게 갱신하는 것이 권장되는 방법입니다.

    8. known_hosts를 수정했는데 오류가 계속 발생하는 경우

    기존 항목을 제거했는데도 같은 오류가 반복된다면 SSH 클라이언트가 다른 설정 파일이나 호스트 이름을 사용하고 있을 가능성을 확인해야 합니다.

    먼저 SSH 상세 로그를 확인합니다.

    ssh -vvv user@192.0.2.10

    현재 적용되는 SSH 설정을 확인하려면 다음 명령어도 사용할 수 있습니다.

    ssh -G 192.0.2.10

    특히 userknownhostsfile, globalknownhostsfile, hostname, port, hostkeyalias 등의 설정을 확인합니다.

    환경에 따라 사용자별 known_hosts 외에 시스템 전역의 호스트 키 파일이 사용될 수도 있습니다.

    또한 DNS가 변경됐거나 로드밸런서 뒤의 여러 서버가 서로 다른 호스트 키를 제시한다면 연결할 때마다 키가 달라지는 현상이 발생할 수 있습니다. 이 경우 단순히 키를 반복 삭제하기보다 서버 구성과 접속 경로를 확인해야 합니다.

    실전 해결 순서

    SSH 접속 시 Host Key Verification Failed 오류가 발생했다면 다음 순서로 확인하는 것이 좋습니다.

    # 1. 상세 오류 메시지 확인
    ssh -vvv user@192.0.2.10
    
    # 2. 기존에 저장된 서버 키 검색
    ssh-keygen -F 192.0.2.10
    
    # 3. 서버 콘솔에서 실제 키 지문 확인
    sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
    
    # 4. 서버 신원 검증 후 기존 항목 백업
    cp ~/.ssh/known_hosts ~/.ssh/known_hosts.bak
    
    # 5. 확인된 기존 호스트 키 항목 제거
    ssh-keygen -R 192.0.2.10
    
    # 6. SSH 재접속 및 지문 비교
    ssh user@192.0.2.10

    3단계는 SSH 클라이언트가 아니라 신뢰할 수 있는 별도의 경로로 접근한 실제 서버에서 실행하는 명령어입니다. 서버 지문을 검증하지 못했다면 5~6단계로 바로 진행하지 않는 것이 안전합니다.

    정리

    SSH 접속 시 Host Key Verification Failed 오류가 발생했다면 비밀번호나 SSH 포트 문제를 먼저 의심하기보다 서버의 호스트 키 검증 과정에서 무엇이 달라졌는지 확인해야 합니다.

    서버 재설치나 인스턴스 교체처럼 정당한 변경이 있었다면 새 호스트 키의 지문을 검증한 뒤 ssh-keygen -R로 기존 항목을 정리하고 다시 연결하면 됩니다.

    반대로 서버 변경 이력이 없는데 갑자기 호스트 키가 달라졌다면 known_hosts 파일을 바로 삭제하거나 StrictHostKeyChecking을 끄지 말고 서버 신원과 연결 경로부터 확인하는 것이 중요합니다.

    핵심은 오류 메시지 확인 → 기존 키 조회 → 새 키 지문 검증 → known_hosts 수정 → 안전한 재접속 순서를 지키는 것입니다.

  • 리눅스 서버에서 Cannot Allocate Memory 오류가 발생하는 원인

    리눅스 서버에서 Cannot Allocate Memory 오류가 발생하는 원인

    리눅스 서버에서 프로그램을 실행하거나 새로운 프로세스를 생성하려는데 갑자기 Cannot allocate memory 오류가 발생하는 경우가 있습니다. SSH 접속은 정상적으로 되지만 명령어 실행이 실패하거나, Python·Java·MySQL 같은 애플리케이션이 시작되지 않는 현상도 나타날 수 있습니다.

    이런 오류가 발생하면 서버의 RAM이 모두 사용됐다고 생각하기 쉽습니다. 하지만 실제 물리 메모리가 부족한 경우뿐 아니라 Swap 부족, 프로세스의 가상 메모리 제한, 컨테이너 메모리 제한, 커널의 메모리 할당 정책 때문에도 문제가 발생할 수 있습니다.

    특히 free -h에서 사용 가능한 메모리가 남아 있는데도 fork: Cannot allocate memory가 발생한다면 단순히 RAM 사용량만 확인해서는 원인을 찾기 어렵습니다.

    이번 글에서는 메모리 상태 확인 → 프로세스별 사용량 분석 → OOM Killer 로그 점검 → Swap 및 시스템 제한 확인 → 원인별 해결 순서로 서버를 안전하게 점검하는 방법을 살펴봅니다.

    Cannot Allocate Memory 핵심 명령어 한눈에 보기

    오류가 발생했다면 먼저 아래 명령어로 시스템 메모리와 프로세스 상태를 확인합니다. 메모리가 심하게 부족한 서버에서는 새 명령어 실행 자체가 실패할 수 있으므로 가능하면 기존 SSH 세션이나 서버 콘솔을 유지한 상태에서 점검해야 합니다.

    점검 목적명령어
    RAM·Swap 사용량free -h
    메모리 상태 변화vmstat 1 5
    메모리 사용 상위 프로세스ps aux --sort=-rss | head -n 15
    OOM 관련 커널 로그sudo journalctl -k -b | grep -Ei 'out of memory|oom|killed process'
    메모리 커밋 상태grep -E 'CommitLimit|Committed_AS' /proc/meminfo
    프로세스 자원 제한ulimit -a
    Swap 장치 확인swapon --show

    가장 먼저 확인할 값은 단순한 free 메모리보다 실제로 새 작업에 사용할 수 있는 메모리를 추정하는 available 값입니다. 다만 available이 충분해 보여도 메모리 커밋 제한이나 cgroup 제한 등으로 할당에 실패할 수 있습니다.

    리눅스 Cannot Allocate Memory 오류와 free vmstat OOM 메모리 점검 방법
    Cannot Allocate Memory 오류 발생 시 RAM, Swap, 메모리 사용 프로세스와 OOM 로그를 확인하는 예시입니다.

    1. Cannot Allocate Memory 오류가 발생하는 이유

    리눅스에서 Cannot allocate memory는 일반적으로 ENOMEM 오류와 관련이 있습니다. 프로그램이 메모리 할당이나 프로세스 생성에 필요한 자원을 확보하지 못했을 때 나타날 수 있습니다.

    예를 들어 새로운 프로세스를 실행하려 할 때 다음과 같은 메시지가 표시될 수 있습니다.

    bash: fork: Cannot allocate memory

    또는 Python 프로그램에서 메모리 할당이 실패하면 다음과 같은 오류가 발생할 수 있습니다.

    MemoryError

    두 오류 모두 메모리 자원 문제와 관련될 수 있지만 발생 위치와 직접적인 원인은 다를 수 있습니다.

    대표적인 원인은 다음과 같습니다.

    • 물리 메모리 부족: 실행 중인 프로세스가 사용 가능한 RAM을 대부분 소비한 경우
    • Swap 부족: 메모리 압박 상황에서 추가적인 여유 공간이 부족한 경우
    • 메모리 커밋 제한: 커널의 overcommit 정책에 따라 새로운 메모리 예약이 거부된 경우
    • 프로세스 자원 제한: 주소 공간 크기 등 사용자 또는 프로세스별 제한에 도달한 경우
    • 컨테이너 제한: Docker나 Kubernetes의 메모리 제한에 도달한 경우
    • 커널 메모리 할당 문제: 특정 크기나 유형의 메모리 할당이 실패한 경우
    • 애플리케이션 메모리 증가: 메모리 누수나 과도한 캐시 사용으로 사용량이 계속 늘어난 경우

    따라서 메모리 부족 메시지가 나타났다고 해서 무조건 서버 RAM을 증설하거나 Swap부터 추가하는 것은 적절하지 않을 수 있습니다.

    2. free -h로 RAM과 Swap 사용량 확인하기

    가장 기본적인 점검은 free 명령어입니다.

    free -h

    다음은 설명을 위한 출력 예시입니다.

                   total   used   free  shared  buff/cache  available
    Mem:            7.7Gi  6.2Gi  210Mi   120Mi      1.3Gi      890Mi
    Swap:           2.0Gi  1.7Gi  350Mi

    여기서 중요한 항목은 다음과 같습니다.

    • total: 전체 메모리 크기
    • used: 사용 중인 메모리의 추정치
    • free: 현재 사용되지 않는 메모리
    • buff/cache: 버퍼와 파일 캐시 등으로 사용되는 메모리
    • available: 새로운 작업에 사용할 수 있는 메모리의 추정치

    리눅스는 남는 메모리를 파일 캐시 등에 활용하므로 free 값이 작다는 사실만으로 메모리가 부족하다고 판단하면 안 됩니다.

    또한 Swap 사용량이 높다는 이유만으로 현재 메모리 부족 상태라고 단정할 수도 없습니다. Swap에 오래전에 이동된 페이지가 남아 있을 수 있기 때문입니다.

    실제 메모리 압박 여부는 available 값과 함께 Swap 입출력, 프로세스 사용량, OOM 로그를 비교해야 합니다.

    3. vmstat로 메모리 부족과 Swap 활동 확인하기

    메모리 부족이 일시적인지 지속적인지 확인하려면 vmstat 명령어를 사용할 수 있습니다.

    vmstat 1 5

    이 명령어는 1초 간격으로 시스템 상태를 5회 출력합니다.

    메모리 관련 항목에서는 swpd, free, si, so 값을 확인합니다.

    • swpd: 사용 중인 가상 메모리의 Swap 양
    • si: Swap에서 메모리로 읽어 들이는 양
    • so: 메모리에서 Swap으로 내보내는 양
    • free: 사용하지 않는 메모리 양

    si와 so 값이 반복적으로 높게 나타나고 서버 응답 속도도 느려진다면 메모리 압박으로 인해 Swap 입출력이 활발하게 발생하는 상황일 수 있습니다.

    단, vmstat의 첫 번째 출력 행은 일반적으로 부팅 이후의 평균 통계를 포함하므로 실시간 상태를 분석할 때는 이후 출력 행도 함께 확인해야 합니다.

    4. 메모리를 많이 사용하는 프로세스 찾기

    메모리 사용량이 비정상적으로 높다면 어떤 프로세스가 메모리를 소비하고 있는지 확인해야 합니다.

    ps aux --sort=-rss | head -n 15

    위 명령어는 RSS 기준으로 메모리 사용량이 큰 프로세스를 확인하는 데 사용할 수 있습니다.

    주요 항목은 다음과 같습니다.

    • PID: 프로세스 식별 번호
    • %MEM: 물리 메모리 대비 사용 비율
    • VSZ: 프로세스의 가상 주소 공간 크기
    • RSS: 실제 메모리에 상주하는 메모리 크기
    • COMMAND: 실행 중인 프로그램

    특히 특정 프로세스의 RSS가 계속 증가한다면 애플리케이션의 메모리 누수나 캐시 정책을 점검할 필요가 있습니다.

    다만 RSS에는 공유 메모리도 포함될 수 있으므로 여러 프로세스의 RSS를 단순 합산하면 실제 물리 메모리 사용량과 차이가 날 수 있습니다.

    원인이 확인되지 않은 상태에서 메모리 사용량이 높다는 이유만으로 프로세스를 강제 종료하면 데이터 손실이나 서비스 장애가 발생할 수 있습니다.

    5. OOM Killer 로그 확인하기

    리눅스 커널은 메모리 부족 상황에서 시스템을 보호하기 위해 OOM Killer를 통해 프로세스를 종료할 수 있습니다.

    최근 부팅 이후의 커널 로그에서 관련 메시지를 검색합니다.

    sudo journalctl -k -b | grep -Ei 'out of memory|oom|killed process'

    OOM이 발생한 경우 다음과 비슷한 메시지가 기록될 수 있습니다.

    Out of memory: Killed process 2481 (java)
    total-vm:4194304kB, anon-rss:2097152kB

    위 예시는 커널이 메모리 부족 상황에서 Java 프로세스를 종료한 경우를 나타냅니다.

    다만 Cannot Allocate Memory 오류가 발생했다고 해서 반드시 OOM Killer가 실행되는 것은 아닙니다. 메모리 할당이 거부되더라도 프로세스 종료 없이 오류만 반환될 수 있습니다.

    또한 OOM 로그가 없다고 해서 메모리 부족 문제가 없었다고 단정할 수 없습니다. 로그 보존 상태와 커널 설정, 컨테이너 환경에 따라 확인 가능한 정보가 달라질 수 있습니다.

    6. RAM이 남아 있는데 Cannot Allocate Memory가 발생하는 이유

    free -h에서 available 메모리가 충분해 보여도 새로운 메모리 할당이 실패할 수 있습니다.

    대표적으로 커널의 메모리 커밋 정책을 확인해야 하는 경우가 있습니다.

    cat /proc/sys/vm/overcommit_memory

    이 값은 커널이 메모리 할당 요청을 어떻게 판단할지 결정하는 설정입니다.

    • 0: 커널의 휴리스틱에 따라 메모리 커밋을 판단
    • 1: 메모리 커밋을 폭넓게 허용
    • 2: 설정된 커밋 한도에 따라 엄격하게 제한

    현재 커밋 상태는 다음 명령어로 확인합니다.

    grep -E 'CommitLimit|Committed_AS' /proc/meminfo

    Committed_AS가 CommitLimit에 가까운 상황에서 엄격한 overcommit 정책이 적용되고 있다면 새로운 메모리 예약이 거부될 수 있습니다.

    다만 문제를 해결하기 위해 vm.overcommit_memory=1로 무조건 변경하는 것은 권장하지 않습니다. 실제 메모리 부족 위험이 사라지는 것이 아니라 메모리 할당 실패 시점과 시스템 동작이 달라질 수 있기 때문입니다.

    7. ulimit과 프로세스별 메모리 제한 확인하기

    특정 사용자나 프로그램에서만 오류가 발생한다면 프로세스별 자원 제한을 확인할 필요가 있습니다.

    ulimit -a

    특히 다음 항목을 확인합니다.

    • virtual memory: 프로세스의 가상 주소 공간 크기 제한
    • data seg size: 데이터 세그먼트 크기 제한
    • max user processes: 사용자에게 적용되는 프로세스 수 제한

    프로세스 수 제한에 도달하면 일반적으로 Resource temporarily unavailable와 같은 다른 오류가 발생할 수 있으므로 메모리 할당 실패와 구분해야 합니다.

    또한 systemd로 실행하는 서비스에는 셸의 ulimit과 별도로 자원 제한이 적용될 수 있습니다.

    systemctl show myservice.service -p MemoryMax -p LimitAS -p LimitDATA

    위 명령어의 myservice.service는 실제 서비스 이름으로 변경해야 합니다.

    특정 서비스에서만 문제가 발생한다면 시스템 전체 메모리와 서비스별 제한을 함께 확인하는 것이 중요합니다.

    8. Docker 컨테이너에서 메모리 부족이 발생하는 경우

    Docker 컨테이너는 호스트 서버에 RAM이 충분히 남아 있어도 컨테이너에 설정된 메모리 제한에 도달할 수 있습니다.

    실시간 컨테이너 메모리 사용량을 확인합니다.

    docker stats --no-stream

    특정 컨테이너의 메모리 제한과 OOM 종료 여부를 확인하려면 다음 명령어를 사용할 수 있습니다.

    docker inspect mycontainer \
      --format 'Memory={{.HostConfig.Memory}} OOMKilled={{.State.OOMKilled}}'

    Memory 값은 바이트 단위이며, 0이면 Docker의 해당 메모리 제한이 설정되지 않았다는 의미입니다.

    OOMKilled가 true라면 컨테이너가 OOM 상황으로 종료됐음을 나타낼 수 있습니다. 다만 현재 상태와 종료 이력에 따라 추가 로그 확인이 필요할 수 있습니다.

    컨테이너의 메모리 제한을 높이기 전에 애플리케이션이 정상적으로 메모리를 사용하는지, 메모리 누수가 있는지 먼저 확인해야 합니다.

    9. Swap 추가가 필요한 상황과 주의할 점

    서버에 Swap이 없거나 메모리 압박이 반복된다면 Swap 구성을 검토할 수 있습니다.

    swapon --show
    free -h

    Swap은 메모리 부족 상황에서 일부 페이지를 디스크로 이동할 수 있도록 지원하지만 RAM을 대체하는 고속 메모리는 아닙니다.

    특히 디스크가 느리거나 메모리 부족이 지속되는 서버에서는 Swap 사용량 증가로 응답 지연이 심해질 수 있습니다.

    또한 Swap을 추가한다고 해서 프로세스의 주소 공간 제한, 컨테이너 메모리 제한, 특정 메모리 할당 실패가 모두 해결되는 것은 아닙니다.

    따라서 실제 메모리 압박이 확인됐을 때 워크로드와 저장장치 성능을 고려해 Swap 크기를 결정해야 합니다.

    실전 점검 순서

    리눅스 서버에서 Cannot Allocate Memory 오류가 반복된다면 다음 순서로 원인을 좁힐 수 있습니다.

    # 1. 전체 RAM과 Swap 확인
    free -h
    
    # 2. 메모리와 Swap 활동 확인
    vmstat 1 5
    
    # 3. 메모리 사용량이 큰 프로세스 확인
    ps aux --sort=-rss | head -n 15
    
    # 4. OOM Killer 로그 검색
    sudo journalctl -k -b | grep -Ei 'out of memory|oom|killed process'
    
    # 5. Swap 장치 확인
    swapon --show
    
    # 6. 메모리 커밋 정책 확인
    cat /proc/sys/vm/overcommit_memory
    
    # 7. 커밋 사용량 확인
    grep -E 'CommitLimit|Committed_AS' /proc/meminfo
    
    # 8. 현재 셸의 자원 제한 확인
    ulimit -a

    Docker나 systemd 서비스에서만 문제가 발생한다면 해당 컨테이너 또는 서비스에 적용된 메모리 제한도 추가로 확인합니다.

    메모리 사용량이 높은 프로세스를 발견했다면 서비스의 정상적인 사용 패턴인지, 특정 요청 이후 급격히 증가하는지, 재시작 이후에도 반복되는지 비교해야 합니다.

    운영 서버에서는 원인을 확인하기 전에 캐시를 강제로 비우거나 프로세스를 무작정 종료하는 방법을 우선 사용하지 않는 것이 좋습니다.

    정리

    리눅스 서버에서 Cannot Allocate Memory 오류가 발생하는 이유는 단순한 RAM 부족만이 아닙니다. Swap 부족, 메모리 커밋 정책, 프로세스별 자원 제한, Docker 메모리 제한 등 여러 요인이 영향을 줄 수 있습니다.

    먼저 free -h로 전체 메모리를 확인하고, vmstat로 메모리 압박을 점검한 뒤, 프로세스 사용량과 OOM 로그를 함께 분석하는 것이 좋습니다.

    특히 사용 가능한 RAM이 남아 있는데도 오류가 발생한다면 메모리 커밋 제한과 서비스·컨테이너별 메모리 제한을 확인해야 합니다.

    핵심 점검 순서는 RAM·Swap 확인 → 메모리 사용 프로세스 분석 → OOM 로그 점검 → 시스템 제한 확인 → 원인에 맞는 설정 변경입니다.