Systemd 서비스 장애 해결: 단계별 가이드

상태 확인, 저널 로그, 유닛 파일 검토, 의존성 수정, 환경 디버깅을 통해 systemd 서비스 장애를 진단합니다.

Systemd 서비스 장애 해결: 단계별 가이드

Systemd 서비스 장애는 천천히 증거를 따라가면 디버깅하기 쉽습니다. 실패한 유닛은 일반적으로 세 가지 유용한 단서를 남깁니다: systemd가 기록한 상태, 실행하려고 시도한 명령, 그리고 systemd나 애플리케이션이 기록한 로그입니다. 이 순서대로 읽으면 문제가 유닛, 애플리케이션, 의존성, 호스트 중 어디에 있는지 알기 전에 유닛 파일을 수정하는 흔한 실수를 피할 수 있습니다.

아래 예제는 가상의 mywebapp.service를 사용하지만, 동일한 워크플로우는 데이터베이스 헬퍼, 큐 컨슈머, 백업 작업, 익스포터, 내부 데몬에도 적용됩니다.

첫 번째 방어선: systemctl status

서비스 시작에 실패했을 때 가장 먼저 실행해야 할 명령은 systemctl status <서비스_이름>입니다. 이 명령은 서비스의 현재 상태(활성 여부, 로드 여부, 그리고 중요한 최근 로그 스니펫)를 보여줍니다. 이를 통해 문제를 빠르게 식별할 수 있는 충분한 정보를 얻을 수 있습니다.

웹 애플리케이션 서비스 mywebapp.service가 시작되지 않는다고 가정해 보겠습니다:

systemctl status mywebapp.service

예제 출력 해석:

● mywebapp.service - My Web Application
     Loaded: loaded (/etc/systemd/system/mywebapp.service; enabled; vendor preset: disabled)
     Active: failed (Result: exit-code) since Mon 2023-10-26 10:30:05 UTC; 10s ago
    Process: 12345 ExecStart=/usr/local/bin/mywebapp-start.sh (code=exited, status=1/FAILURE)
   Main PID: 12345 (code=exited, status=1/FAILURE)
        CPU: 10ms

Oct 26 10:30:05 hostname systemd[1]: Started My Web Application.
Oct 26 10:30:05 hostname mywebapp-start.sh[12345]: Error: Port 8080 already in use
Oct 26 10:30:05 hostname systemd[1]: mywebapp.service: Main process exited, code=exited, status=1/FAILURE
Oct 26 10:30:05 hostname systemd[1]: mywebapp.service: Failed with result 'exit-code'.

이 출력에서 즉시 확인할 수 있는 것은:

  • 서비스 mywebapp.servicefailed 상태입니다.
  • Result: exit-code로 실패했으며, 이는 ExecStart 명령이 0이 아닌 종료 코드로 종료되었음을 의미합니다.
  • Process 줄은 mywebapp-start.sh 명령이 status=1/FAILURE로 실패했음을 보여줍니다.
  • 결정적으로, 로그 줄은 Error: Port 8080 already in use를 나타냅니다. 이는 문제의 명확한 지표입니다.

이 명령은 첫 번째 진단 도구로, 종종 원인을 직접 가리키거나 다음에 어디를 봐야 할지 좁혀줍니다.

journalctl로 깊이 파고들기

systemctl status가 빠른 요약을 제공하는 반면, journalctl은 상세 로깅을 위한 기본 명령어입니다. 시스템의 모든 부분(서비스 포함)에서 로그를 수집하는 systemd 저널을 쿼리합니다.

기본 로그 검토

특정 서비스의 모든 로그(과거 항목 포함)를 보려면:

journalctl -u mywebapp.service

이 명령은 mywebapp.service와 관련된 모든 로그 항목을 표시합니다. 서비스가 반복적으로 실패하면 각 실패 시도의 항목이 표시됩니다.

필터링 및 시간 기반 쿼리

특히 최근 실패 후 결과를 좁히려면 --since--priority와 같은 플래그를 사용할 수 있습니다:

  • 특정 시간 이후의 로그 표시:
    journalctl -u mywebapp.service --since "10 minutes ago"
    journalctl -u mywebapp.service --since "2023-10-26 10:00:00"
    
  • 오류 수준 메시지만 표시:
    journalctl -u mywebapp.service -p err
    
  • -xe와 결합하여 확장 설명 및 자세한 출력:
    journalctl -u mywebapp.service -xe --since "5 minutes ago"
    
    -x는 일부 systemd 메시지에 설명 텍스트를 추가할 수 있습니다. 이러한 설명을 힌트로 취급하고 유닛별 로그를 대체하는 것으로 간주하지 마십시오.

로그 메시지 이해

Error, Failed, Warning과 같은 키워드나 문제가 무엇인지 나타내는 애플리케이션별 메시지를 찾으십시오. 타임스탬프에 주의하여 실패로 이어지는 이벤트 순서를 이해하십시오.

팁: 서비스의 ExecStart 스크립트가 표준 출력 또는 표준 오류로 출력하는 경우 해당 메시지는 일반적으로 journalctl에 의해 캡처됩니다. 스크립트가 설명적인 오류 메시지를 기록하는지 확인하십시오.

유닛 파일 검사: 서비스의 청사진

모든 systemd 서비스는 유닛 파일(예: mywebapp.service)로 정의됩니다. 이 파일의 잘못된 구성은 시작 실패의 일반적인 원인입니다. 서비스가 무엇을 하려고 하는지 이해해야 합니다.

유닛 파일 검색

서비스의 활성 유닛 파일을 보려면:

systemctl cat mywebapp.service

이 명령은 systemd가 사용 중인 정확한 유닛 파일(재정의 포함)을 보여줍니다.

확인해야 할 주요 지시문

실행 관련 문제는 [Service] 섹션에, 의존성은 [Unit]에 초점을 맞추십시오.

  • ExecStart: systemd가 서비스를 시작하기 위해 실행하는 명령입니다. 경로가 올바른지, 명령 자체가 실행 가능하며 수동으로(예: 지정된 User로) 호출할 때 성공적으로 실행되는지 확인하십시오.
    ExecStart=/usr/local/bin/mywebapp-start.sh
    
  • Type: 프로세스 시작 유형을 정의합니다. 일반적인 유형은 다음과 같습니다:
    • simple (기본값): ExecStart가 메인 프로세스입니다.
    • forking: ExecStart가 자식 프로세스를 포크하고 부모가 종료됩니다. systemd는 부모가 종료될 때까지 기다립니다.
    • oneshot: ExecStart가 실행되고 종료됩니다. systemd는 명령이 실행되는 동안 서비스를 활성 상태로 간주합니다.
    • notify: 서비스가 준비되면 systemd에 알림을 보냅니다.
    • 잘못된 Type은 systemd가 실제로 시작된 서비스를 실패한 것으로 간주하거나 그 반대로 이어질 수 있습니다.
  • User / Group: 서비스가 실행될 사용자와 그룹입니다. 권한 문제는 종종 서비스가 이 사용자 아래에서 권한이 없는 파일이나 리소스에 액세스하려고 시도하기 때문에 발생합니다.
    User=mywebappuser
    Group=mywebappgroup
    
  • WorkingDirectory: 서비스가 실행될 디렉터리입니다. ExecStart 또는 다른 명령의 상대 경로는 이에 의존합니다.
  • Restart: 서비스를 다시 시작해야 하는 시기를 정의합니다. on-failure 또는 always로 설정된 경우 실패하는 서비스가 계속 다시 시작되어 초기 실패를 포착하기 어렵게 만들 수 있습니다.
  • TimeoutStartSec / TimeoutStopSec: systemd가 서비스 시작 또는 중지를 기다리는 시간입니다. 서비스 초기화에 TimeoutStartSec보다 오래 걸리면 systemd가 이를 종료하고 실패를 보고합니다.

일반적인 유닛 파일 문제

  • 잘못된 경로: ExecStart 또는 다른 파일 경로의 오타.
  • 누락된 Environment 변수: 서비스는 종종 systemd의 깨끗한 환경에 없을 수 있는 특정 환경 변수(예: PATH)가 필요합니다(아래 참조).
  • 권한: 지정된 User에 스크립트 실행 권한이나 필요한 데이터 파일에 대한 읽기/쓰기 권한이 없습니다.
  • 구문 오류: 유닛 파일 자체의 단순한 오타.

ExecStart를 수동으로 테스트하려면:

서비스 사용자로 전환하여 명령을 직접 실행해 보십시오:

sudo -u mywebappuser /usr/local/bin/mywebapp-start.sh

이렇게 하면 journalctl에서 볼 수 있는 오류를 터미널에서 직접 재현할 수 있어 디버깅이 더 쉬워집니다.

의존성 관리: 서비스가 단독으로 시작할 수 없는 경우

서비스는 종종 시작하기 전에 다른 서비스나 시스템 구성 요소가 활성화되어 있어야 합니다. Systemd는 Wants, Requires, After, Before 지시문을 사용하여 이러한 의존성을 관리합니다.

의존성 식별

systemctl list-dependencies <서비스_이름>을 사용하여 서비스가 명시적으로 필요로 하거나 실행을 원하는 항목을 확인하십시오.

systemctl list-dependencies mywebapp.service

[Unit] 섹션의 일반적인 지시문:

  • After=: 이 서비스가 나열된 유닛 후에 시작해야 함을 지정합니다. 나열된 유닛이 실패하면 이 서비스는 여전히 시작을 시도합니다(Requires=도 사용하지 않는 한).
  • Requires=: 이 서비스가 나열된 유닛을 필요로 함을 지정합니다. 필요한 유닛 중 하나라도 시작에 실패하면 이 서비스는 시작되지 않습니다.
  • Wants=: Requires=의 약한 형태입니다. 원하는 유닛이 실패해도 이 서비스는 여전히 시작을 시도합니다.

예제:

[Unit]
Description=My Web Application
After=network.target mysql.service
Requires=mysql.service

여기서 mywebapp.servicenetwork.targetmysql.service 다음에 정렬되며 mysql.service가 성공적으로 시작되어야 합니다. mysql.service가 실패하면 mywebapp.service는 시작되지 않습니다.

의존성 충돌 해결

의존성 문제로 인해 서비스가 실패하면 journalctl은 일반적으로 충족할 수 없는 의존성을 나타냅니다. 예를 들어, Dependency failed for My Web Application이라고 표시된 후 mysql.service의 실패에 대한 세부 정보가 표시될 수 있습니다.

해결 단계:

  1. 종속 서비스 확인: systemctl status <종속_서비스>(예: systemctl status mysql.service) 및 journalctl -u <종속_서비스>를 실행하여 해당 서비스의 실패를 먼저 해결하십시오.
  2. After=Requires= 지시문 확인: 원하는 시작 순서와 엄격성을 올바르게 반영하는지 확인하십시오. 때로는 서비스가 다른 유닛의 시작 작업이 완료될 때까지 기다리는 것뿐만 아니라 특정 포트가 열릴 때까지 기다려야 할 수도 있습니다. 좁은 검사의 경우 ExecStartPre=가 도움이 될 수 있습니다. 네트워크 데몬의 경우 소켓 활성화 또는 애플리케이션 수준 재시도 로직이 더 안정적인 경우가 많습니다.

환경 변수 및 경로: 숨겨진 함정

Systemd 서비스는 매우 깨끗하고 최소한의 환경에서 실행됩니다. 이로 인해 사용자 셸에서 완벽하게 작동하는 명령이 systemd에 의해 실행될 때 중요한 환경 변수(예: PATH)가 누락되어 실패하는 경우가 자주 발생합니다.

Systemd의 깨끗한 환경

systemd가 서비스를 시작할 때 systemctl start를 실행한 사용자의 전체 환경을 상속하지 않습니다. 예를 들어 PATH 변수는 종종 축소되어 python 또는 node와 같은 명령이 /usr/bin 또는 /bin과 같은 표준 위치에 없으면 찾을 수 없습니다.

증상: ExecStart=/usr/local/bin/myscript.shpython: command not found, node: command not found, 누락된 라이브러리 오류, 또는 필수 설정이 비어 있다는 애플리케이션 메시지와 함께 실패합니다.

해결 방법: 서비스 환경을 명시적으로 만드십시오.

[Service]
WorkingDirectory=/opt/mywebapp
Environment="APP_ENV=production"
Environment="PATH=/opt/mywebapp/venv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin"
ExecStart=/opt/mywebapp/venv/bin/gunicorn app:app

많은 변수의 경우 환경 파일을 사용하십시오:

[Service]
EnvironmentFile=/etc/mywebapp/mywebapp.env
ExecStart=/opt/mywebapp/bin/server

해당 파일을 간단하게 유지하십시오. EnvironmentFile=은 Bash 스크립트가 아닙니다. export KEY=value, 명령 대체, 셸 조건문이 아닌 KEY=value 줄을 사용하십시오. 또한 파일에 비밀이 포함된 경우 제한적인 권한을 설정하십시오:

sudo chown root:mywebapp /etc/mywebapp/mywebapp.env
sudo chmod 0640 /etc/mywebapp/mywebapp.env

권한: 서비스 사용자로 실패 재현

수동 테스트는 종종 root 또는 로그인 사용자로 수행되는 반면 유닛은 전용 서비스 계정으로 실행되기 때문에 권한 문제가 일반적입니다.

구성된 사용자를 확인하십시오:

systemctl show mywebapp.service -p User -p Group

그런 다음 해당 사용자로 동일한 명령을 실행하십시오:

sudo -u mywebappuser /usr/local/bin/mywebapp-start.sh

앱에 작업 디렉터리가 필요한 경우 포함하십시오:

sudo -u mywebappuser bash -lc 'cd /opt/mywebapp && /usr/local/bin/mywebapp-start.sh'

실행 파일 이상을 살펴보십시오. 서비스 사용자는 /etc/mywebapp/config.yml에 대한 읽기 액세스, /var/lib/mywebapp에 대한 쓰기 액세스, 모든 상위 디렉터리에 대한 실행 액세스, 또는 /run/mywebapp 아래에 Unix 소켓을 생성할 수 있는 권한이 필요할 수 있습니다. 빠른 확인으로 많은 추측을 줄일 수 있습니다:

sudo -u mywebappuser test -r /etc/mywebapp/config.yml
sudo -u mywebappuser test -w /var/lib/mywebapp
namei -l /var/lib/mywebapp/uploads

서비스가 80 또는 443과 같은 낮은 포트에 바인딩할 때만 실패하는 경우 즉시 root로 실행하지 마십시오. 서비스에 따라 역방향 프록시, 소켓 활성화 또는 대상 기능이 더 안전할 수 있습니다.

시작 제한 및 재시작 루프

반복적으로 충돌하는 서비스는 start request repeated too quickly와 같은 메시지와 함께 중지될 수 있습니다. 이는 systemd의 속도 제한이 적용되었음을 의미합니다. 원래 실패는 더 일찍 발생했으므로 속도 제한 메시지에만 집중하지 마십시오.

사용:

journalctl -u mywebapp.service --since "30 minutes ago"
systemctl show mywebapp.service -p NRestarts -p Restart -p StartLimitBurst -p StartLimitIntervalUSec

근본 원인을 수정한 후 실패 상태를 지우십시오:

sudo systemctl reset-failed mywebapp.service
sudo systemctl start mywebapp.service

Restart=always에 주의하십시오. 복원력 있는 데몬에 유용하지만 디버깅 중에는 저널을 넘치게 하고 첫 번째 명확한 오류를 숨길 수 있습니다. 유닛을 일시적으로 중지하고 로그를 검토한 다음 한 가지를 변경한 후 수동으로 시작할 수 있습니다.

다시 로드하기 전에 유닛 검증

유닛 파일을 편집한 후 서비스를 다시 시작하기 전에 파일을 검증하고 systemd를 다시 로드하십시오:

sudo systemd-analyze verify /etc/systemd/system/mywebapp.service
sudo systemctl daemon-reload
sudo systemctl restart mywebapp.service

서비스에 드롭인 재정의가 있는 경우 병합된 버전을 검사하십시오:

systemctl cat mywebapp.service
systemctl show mywebapp.service -p FragmentPath -p DropInPaths -p ExecStart

이렇게 하면 어색한 경우를 잡을 수 있습니다: /usr/lib/systemd/system 아래의 파일을 편집했지만 /etc/systemd/system/mywebapp.service.d/override.conf 아래의 드롭인이 여전히 ExecStart를 변경하는 경우; 또는 systemd가 로드한 파일이 아닌 복사된 유닛 파일을 수정한 경우.

실용적인 작업 순서

프로덕션 서비스가 중단된 경우 짧고 반복 가능한 루프를 사용하십시오:

  1. systemctl status mywebapp.service --no-pager를 실행합니다.
  2. journalctl -u mywebapp.service --since "15 minutes ago"를 읽습니다.
  3. systemctl cat mywebapp.service를 검사합니다.
  4. 명령, 사용자, 작업 디렉터리, 환경 및 의존성을 확인합니다.
  5. 서비스 사용자로 명령을 재현합니다.
  6. 한 가지를 변경합니다.
  7. 유닛이 변경된 경우 systemctl daemon-reload를 실행합니다.
  8. 다시 시작하고 저널을 다시 확인합니다.

이 순서는 조사를 현실에 기반하게 유지합니다. 저널에 Permission denied가 표시되면 권한을 수정하십시오. No such file or directory가 표시되면 systemd의 관점에서 경로를 확인하십시오. Dependency failed가 표시되면 먼저 의존성을 디버깅하십시오. 프로세스가 0/SUCCESS 상태로 종료되었지만 서비스가 실패했다고 표시되면 Type=과 애플리케이션이 데몬화되는지 또는 즉시 종료되는지 확인하십시오.

목표는 모든 systemd 지시문을 암기하는 것이 아닙니다. 실패 메시지를 생성한 계층과 계속 일치시키는 것입니다.