Systemd 종속성 이해하기: 유닛 충돌 방지 및 해결
systemd 종속성, 순서, 대상 및 충돌이 어떻게 작동하는지 학습하여 서비스가 안정적으로 시작되고 오류를 더 쉽게 디버깅할 수 있습니다.
Systemd 종속성 이해하기: 유닛 충돌 방지 및 해결
Systemd 종속성은 절반만 이해하기 쉽습니다. 유닛 파일에 Requires=postgresql.service를 추가했는데도 애플리케이션이 너무 일찍 시작되어 모두가 systemd가 종속성을 무시했다고 생각합니다. systemd는 아무것도 무시하지 않았습니다. Requires=와 After=는 서로 다른 질문에 답합니다.
이 차이가 대부분의 systemd 종속성 문제의 핵심입니다. 한 지시문은 다른 유닛이 동일한 트랜잭션에 포함되는지 여부를 제어합니다. 다른 지시문은 순서를 제어합니다. 다른 지시문은 종료 동작을 연결하거나, 한 유닛의 수명을 다른 유닛에 바인딩하거나, 두 유닛을 상호 배타적으로 만듭니다. 이러한 개념을 분리하면 유닛 충돌이 훨씬 덜 신비로워집니다.
기본: Systemd 유닛 종속성 지시문
Systemd는 유닛 파일(일반적으로 /etc/systemd/system/ 또는 /lib/systemd/system/에 위치) 내에서 특정 지시문을 사용하여 한 유닛이 다른 유닛을 시작, 중지 또는 대기해야 하는 시기를 지정합니다. 이러한 지시문을 이해하는 것이 종속성을 올바르게 관리하는 첫 번째 단계입니다.
핵심 종속성 지시문
이러한 지시문은 유닛 간의 관계를 제어합니다. 그 자체로 항상 시작 순서를 제어하지는 않습니다.
Requires=:- 강력한 종속성을 설정합니다. 필요한 유닛이 시작에 실패하면 현재 유닛도 실패합니다.
PartOf=를 의미하지 않으며, 자동으로 "이 유닛 이후에 시작"을 의미하지 않습니다.
Wants=:- 약한 종속성입니다. 원하는 유닛이 실패하더라도 현재 유닛은 계속 시작을 시도합니다. 선택적 종속성에 사용됩니다.
BindsTo=:Requires=와 유사하지만 중지와 관련하여 더 강력합니다. 바인딩된 유닛이 중지되면(어떤 이유로든) 현재 유닛도 중지됩니다.
PartOf=:- 현재 유닛이 다른 유닛의 하위 부분임을 나타냅니다(예: 기본 서비스와 관련된 특정 소켓 활성화). 상위 유닛이 중지되면 하위 유닛도 중지됩니다.
Conflicts=:- 두 유닛이 동시에 활성화되어서는 안 된다고 말합니다. 하나를 시작하면 systemd가 동일한 트랜잭션의 일부로 다른 유닛을 중지합니다.
핵심 시작 동기화 지시문
이러한 지시문은 종속 유닛이 필요한 유닛에 비해 언제 시작되어야 하는지를 지시합니다.
After=:- 현재 유닛의 시작 작업이 나열된 유닛의 시작 작업 이후에 순서가 지정되도록 지정합니다. 그 자체로 해당 유닛을 가져오지 않습니다.
Before=:- 현재 유닛이 나열된 유닛 이전에 시작되어야 함을 지정합니다.
모범 사례: 일반적인 서비스 시작 순서의 경우
Wants=와After=를 결합하는 것이 가장 일반적이고 안전한 패턴입니다.Requires=는 종속성 실패가 반드시 종속 서비스를 실패하게 해야 하는 경우에만 사용해야 합니다.
예시: 서비스 파일에서 종속성 정의
PostgreSQL(postgresql.service)이 관리하는 데이터베이스와 통신해야 하는 사용자 정의 애플리케이션 서비스 myapp.service를 고려해 보겠습니다.
# /etc/systemd/system/myapp.service
[Unit]
Description=내 사용자 정의 애플리케이션
# PostgreSQL이 실행 중인지 확인한 후에 시작
Requires=postgresql.service
After=postgresql.service
[Service]
ExecStart=/usr/bin/myapp
[Install]
WantedBy=multi-user.target
해당 예시는 의도적으로 엄격합니다. PostgreSQL이 시작할 수 없으면 myapp.service도 실패해야 합니다. 데이터베이스 없이 저하된 모드로 실행할 수 있는 서비스의 경우 동일한 After=postgresql.service 순서와 함께 Wants=postgresql.service를 사용하십시오. 애플리케이션은 여전히 systemd에 먼저 PostgreSQL을 시작하도록 요청하지만 PostgreSQL을 사용할 수 없는 경우 계속 진행할 수 있습니다.
현실과 일치할 때 더 약한 관계를 선택하는 것은 부끄러운 일이 아닙니다. 메트릭 내보내기, 로그 전달자 또는 캐시 워머는 종속성이 있을 때 유용할 수 있지만 기본 시스템 부팅을 차단해서는 안 됩니다. 하드 종속성은 다른 유닛 없이 시작하는 것이 잘못되거나 위험한 경우에만 사용하는 것이 좋습니다.
네트워크 애플리케이션의 경우 더 주의하십시오. network.target은 종종 DHCP가 완료되었거나 DNS를 사용할 수 있음을 의미하는 것이 아니라 기본 네트워킹 스택이 있음을 의미합니다. 애플리케이션이 시작 시 구성된 네트워킹을 실제로 필요로 하는 경우 다음을 사용하십시오.
[Unit]
Wants=network-online.target
After=network-online.target
그런 다음 배포판에 systemd-networkd-wait-online.service 또는 NetworkManager의 wait-online 유닛과 같은 일치하는 wait-online 서비스가 활성화되어 있는지 확인하십시오. 이것이 없으면 network-online.target은 생각하는 대로 기다리지 않을 수 있습니다.
종속성 문제 진단
서비스가 시작에 실패하면 systemd는 일반적으로 로그에 충분한 정보를 제공하지만 종속성 체인이 근본 원인을 모호하게 할 수 있습니다. 문제 해결을 위한 필수 도구와 명령은 다음과 같습니다.
1. 유닛 상태 및 로그 확인
기본 시작점은 서비스 상태를 확인하고 실패한 시작 시도 직후에 관련 로그를 검토하는 것입니다.
# 종속성 실패를 자주 언급하는 전체 상태 확인
systemctl status myapp.service
# 유닛과 관련된 자세한 로그 보기
journalctl -u myapp.service --since "5 minutes ago"
2. 종속성 트리 분석
Systemd는 무엇이 무엇을 기다리고 있는지 정확히 볼 수 있는 강력한 시각화 도구를 제공합니다.
systemctl list-dependencies
이 명령은 지정된 유닛에 필요하거나 원하는 유닛을 전체 종속성 체인을 따라 표시합니다.
myapp.service가 시작하기 위해 필요한 것을 보려면:
# 순방향 종속성 (내 앞에 시작해야 하는 것)
systemctl list-dependencies --after myapp.service
# 역방향 종속성 (내게 의존하는 것)
systemctl list-dependencies --before myapp.service
트리 형식이 방해가 될 때 --plain을 사용하고 비활성 유닛도 확인해야 할 때 --all을 추가하십시오:
systemctl list-dependencies --plain --all myapp.service
systemd-analyze dot
더 큰 종속성 질문의 경우 그래프를 생성하고 시각적으로 검사하십시오:
systemd-analyze dot 'myapp.service' | dot -Tsvg > myapp-deps.svg
Graphviz가 설치되지 않은 서버에서도 텍스트 출력은 유닛 이름을 검색하고 systemd가 알고 있는 가장자리를 볼 수 있기 때문에 여전히 유용합니다. 부팅 타이밍 문제의 경우 systemd-analyze critical-chain myapp.service가 전체 그래프보다 읽기 쉬운 경우가 많습니다.
3. 충돌 및 순서 문제 감지
종속성 충돌은 서비스가 너무 일찍 시작되었거나 예기치 않게 중지되어 실패하는 경우가 많습니다.
순환 종속성: 이것은 가장 위험한 충돌로, 유닛 A가 B를 필요로 하고 유닛 B가 A를 필요로 하는 경우입니다. Systemd는 이를 해결하려고 시도하지만 종종 하나 또는 두 유닛이 무기한 failed 또는 activating 상태로 남게 됩니다.
전체 시스템에 걸친 잠재적 문제를 찾으려면 순서와 관련된 특정 실패 메시지에 대해 로그를 검색할 수 있습니다:
journalctl -b | grep -E "failed|refused to start|dependency was not satisfied"
또한 런타임 동작이 문제라고 가정하기 전에 사용자 정의 유닛에 대해 systemd-analyze verify를 실행하십시오:
systemd-analyze verify /etc/systemd/system/myapp.service
순서 지정 주기, 알 수 없는 지시문 및 잘못된 유닛 참조를 조기에 플래그 지정할 수 있습니다. 설계가 올바르다는 것을 증명하지는 않지만 복잡한 종속성 문제인 것처럼 오타를 쫓는 것을 방지할 수 있습니다.
일반적인 종속성 문제 해결
일단 식별되면 종속성 문제는 관련 유닛 파일의 지시문을 조정하여 해결할 수 있습니다.
시나리오 1: 서비스가 필수 구성 요소가 준비되기 전에 시작됨
증상: 애플리케이션 로그에 데이터베이스 연결 오류가 표시되지만 systemctl status에서 postgresql.service가 active로 나타납니다.
진단: 서비스에 After=postgresql.service가 없거나 PostgreSQL이 애플리케이션에 필요한 특정 데이터베이스, 소켓, 자격 증명 또는 스키마가 준비되기 전에 활성화될 수 있습니다. Systemd는 유닛 순서를 지정할 수 있지만 모든 애플리케이션 수준 준비 조건을 자동으로 이해할 수는 없습니다.
수정: 간단한 유닛 관계부터 시작하십시오:
[Unit]
Requires=postgresql.service
After=postgresql.service
애플리케이션이 여전히 데이터베이스와 경쟁하는 경우 올바른 계층에서 준비 문제를 해결하십시오. 일부 서비스는 Type=notify를 지원하고 초기화 후에만 준비되었다고 보고합니다. 일부 애플리케이션은 종속성이 부팅 중뿐만 아니라 언제든지 다시 시작될 수 있으므로 재시도 로직이 필요합니다. 좁은 로컬 검사의 경우 ExecStartPre= 명령이 합리적일 수 있습니다:
[Service]
ExecStartPre=/usr/bin/pg_isready -q -h 127.0.0.1 -p 5432
ExecStart=/usr/local/bin/myapp
이런 종류의 사전 검사는 드물게 사용하십시오. sleep과 루프가 있는 셸 스크립트로 성장하면 애플리케이션에 적절한 재시도 동작이 필요할 가능성이 높습니다.
sleep 30을 종속성 수정으로 사용하지 마십시오. 조용한 개발 머신에서는 경쟁을 숨길 수 있지만 느린 스토리지, 바쁜 VM 또는 DNS를 기다리는 호스트에서는 다시 실패할 수 있습니다. 실제 순서 지정 지시문, 준비 알림, 소켓 활성화 또는 애플리케이션 재시도 루프는 충분한 시간이 지났다는 희망 대신 서비스가 준비된 이유를 제공합니다.
시나리오 2: 시작/중지 순서 충돌
증상: 시스템을 중지하면 중요한 프로세스가 중단되거나 갑자기 실패합니다.
진단: 이것은 종종 BindsTo=의 오용 또는 형제 서비스의 Before= 및 After= 지시문 간의 복잡한 상호 작용을 나타냅니다.
수정: 형제 서비스(예: 동일한 대상에 의해 시작된 서비스)를 검토하십시오. 서비스 A가 서비스 B가 실행되는 동안 실행되어야 하는 경우 BindsTo= 또는 Requires=를 사용하십시오. 서비스 A가 서비스 B가 정리를 시작하기 전에 작업을 완료해야 하는 경우 After= 순서가 올바른지 확인하십시오.
종료 순서는 시작 순서의 역순임을 기억하십시오. app.service에 After=database.service가 있는 경우 종료 시 systemd는 database.service 전에 app.service를 중지합니다. 이것은 일반적으로 원하는 동작입니다. 애플리케이션은 데이터베이스가 사라지기 전에 작업 수락을 중지합니다. 많은 종료 버그는 별도의 종료 전용 설정이 아닌 시작 순서 누락으로 인해 발생합니다.
시나리오 3: 불필요한 종속성 제거
증상: 불필요한 서비스가 시작 체인에 포함되어 시스템 부팅이 느립니다.
진단: 선택적 연결만 필요한 경우 Requires=를 사용했을 수 있습니다.
수정: Requires=를 Wants=로 변경하십시오. 서비스가 기능을 위해 종속성이 절대적으로 필요하지 않은 경우 Wants=를 사용하면 종속성이 실패하거나 마스킹된 경우에도 시스템이 계속 진행할 수 있습니다.
# 이전 (너무 엄격함)
Requires=optional_logging.service
# 이후 (더 나음)
Wants=optional_logging.service
After=optional_logging.service
이것은 모니터링 에이전트, 메트릭 내보내기, 사이드카 헬퍼 및 선택적 로컬 캐시에 특히 유용합니다. 기본 서비스가 이들 없이 유용한 작업을 수행할 수 있는 경우 Requires=는 부분 중단을 전체 중단으로 만듭니다. 진정으로 필요한 것, 즉 시작할 수 없는 애플리케이션의 로컬 데이터베이스, 애플리케이션 데이터를 보유하는 마운트 지점 또는 유일하게 지원되는 활성화 경로인 소켓 유닛에 대해 엄격한 종속성을 사용하십시오.
시나리오 4: 마운트 또는 장치가 준비되지 않음
증상: 서비스가 부팅 시 No such file or directory 오류로 실패하지만 로그인한 후에는 경로가 존재합니다. 이것은 /mnt/data, /srv/app, 이동식 디스크, 암호화된 볼륨 또는 네트워크 파일 시스템에서 읽는 서비스에서 자주 발생합니다.
진단: 서비스가 마운트 유닛이 활성화되기 전에 시작되고 있거나 마운트가 선택 사항이고 서비스를 중지하지 않고 실패했습니다.
수정: 마운트 유닛 이름을 찾으십시오:
systemd-escape -p --suffix=mount /mnt/data
/mnt/data의 경우 일반적으로 mnt-data.mount를 생성합니다. 그런 다음 서비스를 그 이후로 순서를 지정하고 서비스가 데이터 없이 실행될 수 없는 경우 필요로 하십시오:
[Unit]
Requires=mnt-data.mount
After=mnt-data.mount
마운트가 /etc/fstab에서 오는 경우 nofail, x-systemd.automount 및 _netdev와 같은 옵션이 부팅 동작에 영향을 미칩니다. 해당 마운트 실패가 서비스를 차단하려는 경우가 아니라면 선택적 마운트에 하드 Requires=를 추가하지 마십시오.
시나리오 5: 대상이 너무 많이 포함함
증상: 서비스를 활성화하면 관련 없는 유닛이 시작되는 것처럼 보이거나 서비스를 비활성화해도 부팅 중에 나타나는 것을 막지 못합니다.
진단: 서비스가 [Install] WantedBy=...를 통해 대상, 다른 서비스의 Wants=, 또는 소켓, 타이머, 경로 또는 마운트 유닛에 의해 포함될 수 있습니다. enable은 부팅 시 활성화를 위한 심볼릭 링크를 생성합니다. 유닛이 시작될 수 있는 유일한 방법은 아닙니다.
수정: 유닛 및 역방향 종속성을 모두 검사하십시오:
systemctl cat myapp.service
systemctl list-dependencies --reverse myapp.service
systemctl list-timers --all | grep myapp
systemctl list-sockets --all | grep myapp
소켓 유닛이 서비스를 활성화하는 경우 myapp.service만 비활성화하는 것만으로는 충분하지 않을 수 있습니다. 원하는 동작에 따라 myapp.socket도 비활성화하거나 마스킹해야 할 수 있습니다.
변경 사항 적용 및 다시 로드
유닛 파일을 수정할 때마다 변경 사항을 테스트하기 전에 systemd에 구성을 다시 로드하도록 지시해야 합니다.
# 1. systemd 관리자 구성 다시 로드
sudo systemctl daemon-reload
# 2. 영향을 받는 서비스 다시 시작
sudo systemctl restart myapp.service
# 3. 상태 확인
systemctl status myapp.service
작은 정신적 체크리스트
종속성 문제가 복잡하게 느껴지면 몇 가지 간단한 확인 사항으로 줄이십시오.
먼저, 다른 유닛을 전혀 시작해야 하는지 물어보십시오. 그렇다면 Wants= 또는 Requires=를 사용하십시오. 현재 서비스가 그것 없이 실행될 수 있다면 Wants=를 선호하십시오. 해당 유닛의 실패가 이 서비스를 실패하게 해야 한다면 Requires=를 사용하십시오.
둘째, 시작 순서가 중요한지 물어보십시오. 그렇다면 After= 또는 Before=를 추가하십시오. 종속성 지시문이 자체적으로 순서를 처리할 것이라고 기대하지 마십시오.
셋째, 시작 후 수명 결합이 중요한지 물어보십시오. 한 유닛이 중지되면 다른 유닛도 중지되어야 하는 경우 BindsTo= 또는 PartOf=를 살펴보십시오. 무심코 사용하지 마십시오. 일상적인 다시 시작이 예상보다 더 멀리 계단식으로 전파될 수 있습니다.
마지막으로 systemd가 실제로 로드한 것을 검사하십시오:
systemctl cat myapp.service
systemctl show myapp.service -p Wants -p Requires -p After -p Before -p Conflicts
해당 출력은 편집했다고 생각하는 파일보다 더 유용한 경우가 많습니다. 드롭인, 공급업체 유닛, 생성된 유닛, 소켓, 타이머 및 대상이 모두 최종 동작을 변경할 수 있습니다.