Systemd 문제 해결: 서비스 종속성 및 순서 지정 지침 이해하기

Requires, Wants, After, Before 및 진단 명령을 올바르게 사용하여 systemd 종속성 및 순서 문제를 해결하세요.

Systemd 문제 해결: 서비스 종속성 및 순서 지정 지침 이해하기

대부분의 혼란스러운 systemd 종속성 버그는 요구 사항과 순서라는 두 가지 별개의 개념을 혼동하는 데서 발생합니다. Requires=Wants=는 "다른 유닛을 함께 가져와야 하는가?"에 대한 답을 제공합니다. After=Before=는 "이 유닛이 다른 유닛에 비해 언제 시작되어야 하는가?"에 대한 답을 제공합니다. 이 가이드에서 한 가지만 기억해야 한다면, After=postgresql.service가 PostgreSQL을 자동으로 시작하지 않는다는 점을 기억하세요. 이는 두 유닛이 모두 시작되는 경우, 해당 유닛이 PostgreSQL의 시작 작업이 실행될 때까지 기다려야 한다는 것만을 의미합니다.

이러한 구분은 "수동으로 시작하면 작동하지만 재부팅 후에는 실패"하는 많은 사고의 원인입니다. 웹 앱이 데이터베이스 소켓이 연결을 수락하기 전에 시작됩니다. 작업자가 /mnt/jobs가 마운트되기 전에 시작됩니다. 서비스가 network.target을 기다리지만 IP 주소가 아직 할당되지 않아 여전히 실패합니다. 이것들은 특이한 systemd 문제가 아닙니다. 명시적으로 만들어야 하는 일반적인 시작 가정입니다.

핵심 종속성 지침: RequiresWants

Systemd는 유닛 간의 직접적인 종속성을 정의하기 위해 두 가지 기본 지침을 사용합니다: RequiresWants. 이러한 지침은 유닛 파일(예: .service 파일)의 [Unit] 섹션 내에 배치됩니다.

Requires=

Requires= 지침은 강력한 종속성을 설정합니다. 유닛 A가 유닛 B를 Requires=하는 경우, systemd는 A가 시작될 때 B를 시작합니다. B를 시작할 수 없으면 A의 시작 작업도 실패합니다. A가 실행되는 동안 B가 명시적으로 중지되면, 필수 관계가 더 이상 유지되지 않으므로 A도 일반적으로 중지됩니다.

예시:

데이터베이스 서비스(mariadb.service)에 중요한 종속성을 가진 웹 애플리케이션 서비스(myapp.service)를 고려해 보세요. myapp.service 유닛 파일은 다음을 포함할 수 있습니다:

[Unit]
Description=My Web Application
Requires=mariadb.service

[Service]
ExecStart=/usr/bin/myapp

[Install]
WantedBy=multi-user.target

이 시나리오에서 mariadb.service가 시작에 실패하거나 수동으로 중지되면 systemd는 myapp.service도 중지합니다. myapp.service를 시작하려고 할 때 mariadb.service가 실행 중이 아니면 systemd는 먼저 mariadb.service를 시작하려고 시도합니다. mariadb.service가 실패하면 myapp.service는 시작되지 않습니다.

Wants=

Wants= 지침은 더 약한 선택적 종속성을 정의합니다. 유닛 A가 유닛 B를 Wants=하는 경우, systemd는 유닛 A를 시작할 때 유닛 B를 시작하려고 시도하지만, 유닛 B가 시작에 실패하거나 실행 중이 아니더라도 유닛 A는 여전히 활성화됩니다. 이는 다른 서비스의 혜택을 받지만 기능이 축소되거나 경고와 함께 독립적으로 작동할 수 있는 서비스에 유용합니다.

예시:

모니터링 에이전트(monitoring-agent.service)가 특정 로깅 서비스(app-logger.service) 없이도 실행될 수 있지만 이상적으로는 사용 가능한 것이 좋다고 가정해 보세요. monitoring-agent.service 유닛 파일은 다음과 같을 수 있습니다:

[Unit]
Description=Monitoring Agent
Wants=app-logger.service

[Service]
ExecStart=/usr/bin/monitoring-agent

[Install]
WantedBy=multi-user.target

여기서 systemd는 monitoring-agent.service가 활성화될 때 app-logger.service를 시작하려고 시도합니다. 그러나 app-logger.service가 시작에 실패하더라도 monitoring-agent.service는 계속해서 성공적으로 시작됩니다.

Requires=Wants=

  • Requires=: 강력한 종속성. 필수 유닛이 실패하면 종속 유닛이 실패하거나 중지됩니다.
  • Wants=: 약한 종속성. 종속 유닛이 원하는 유닛을 시작하려고 시도하지만 실패해도 계속 진행됩니다.

Requires=Wants=를 암시한다는 점에 유의하는 것이 중요합니다. 유닛이 다른 유닛을 필요로 하는 경우, 암시적으로 원하기도 합니다.

순서 지정 지침: AfterBefore

RequiresWants무엇이 실행되어야 하는지를 정의하는 반면, AfterBefore는 유닛이 서로에 대해 언제 시작되어야 하는지를 정의합니다. 이러한 지침은 시스템 부팅 프로세스 중 또는 유닛이 요청 시 활성화될 때 작업 순서를 제어합니다. 이들은 종종 종속성 지침과 함께 사용됩니다.

After=

After= 지침은 현재 유닛의 시작 작업이 나열된 유닛의 시작 작업 이후에 순서가 지정되어야 함을 지정합니다. 그 자체로는 해당 유닛을 트랜잭션으로 가져오지 않습니다. 또한 종속성이 애플리케이션에 대해 논리적으로 준비되었음을 증명하지 않습니다. 단지 systemd의 유닛 활성화 상태에 대한 관점만을 사용합니다.

예시:

네트워크 종속 서비스(custom-network-app.service)는 네트워크가 완전히 구성된 후에만 시작되어야 합니다. 이는 일반적으로 네트워크 대상(network.target) 이후에 시작되도록 보장하여 처리됩니다.

[Unit]
Description=Custom Network Application
Requires=network.target
After=network.target

[Service]
ExecStart=/usr/bin/custom-network-app

[Install]
WantedBy=multi-user.target

이 구성에서 systemd는 custom-network-app.servicenetwork.target이 동일한 트랜잭션의 일부인 경우 custom-network-app.servicenetwork.target 이후에 순서 지정합니다. 주소, DNS 또는 다른 호스트로의 경로가 필요한 서비스의 경우 network-online.target이 의도에 더 가깝지만, 배포판의 wait-online 서비스가 활성화되고 올바르게 구성된 경우에만 해당됩니다.

Before=

Before= 지침은 현재 유닛이 Before=에 나열된 유닛 이전에 시작되어야 함을 지정합니다. 이는 종료 중에 다른 유닛 이후에 중지되거나 특정 서비스에 환경을 제공하기 위해 이전에 시작되어야 하는 서비스에 유용합니다.

예시:

메일 서버(postfix.service)가 이메일을 보낼 수 있는 사용자 대상 서비스보다 먼저 실행되어야 하는 시나리오를 상상해 보세요. Before=를 사용하여 postfix.service가 일찍 시작되도록 할 수 있습니다.

[Unit]
Description=Postfix Mail Transfer Agent
# ... Conflicts=와 같은 다른 지침
Before=user-session.target

[Service]
ExecStart=/usr/lib/postfix/master

[Install]
WantedBy=multi-user.target

이 설정은 user-session.target의 일부인 모든 항목이 시작을 시작하기 전에 postfix.service를 시작하려고 시도합니다. 마찬가지로 종료 중에는 postfix.service에 해당하는 After=user-session.target이 있는 경우 마지막으로 중지되는 서비스 중 하나가 됩니다.

After=Before=

  • After=: 나열된 유닛이 현재 유닛이 시작되기 전에 활성 상태임을 보장합니다.
  • Before=: 현재 유닛이 나열된 유닛 전에 시작됨을 보장합니다.

After=Before=는 상호 보완적입니다. 유닛 A가 Before=B라고 말하면 순서는 A 먼저, 그 다음 B입니다. 유닛 B가 After=A라고 말하면 결과는 동일합니다. 일반적으로 하나의 유닛 파일에서 관계를 표현하기만 하면 됩니다. 자신의 서비스를 편집할 때는 서비스가 무엇 이후에 와야 하는지 말하는 것이 일반적으로 더 명확합니다. 그렇게 하면 추론이 로컬로 유지되기 때문입니다.

강력한 구성을 위한 지침 결합

실제 시나리오에서는 이러한 지침을 결합하여 복잡한 종속성 그래프를 만드는 경우가 많습니다. multi-user.target은 시스템이 다중 사용자 작업에 준비되었음을 나타내는 일반적인 대상입니다. 많은 서비스가 WantedBy=multi-user.targetAfter=multi-user.target(또는 더 정확하게는 multi-user.target이 종속하는 After=basic.targetAfter=getty.target 등)으로 구성됩니다.

일반적인 패턴:

데이터베이스가 필요하고 네트워크가 구성된 후에 시작되어야 하는 서비스는 다음과 같을 수 있습니다:

[Unit]
Description=My Application Service
Requires=mariadb.service
Wants=other-optional-service.service
After=network.target mariadb.service

[Service]
ExecStart=/usr/local/bin/my_app

[Install]
WantedBy=multi-user.target

패턴 설명:

  1. Requires=mariadb.service: my_app.service가 작동하려면 mariadb.service가 실행 중이어야 함을 보장합니다. mariadb.service가 실패하면 my_app.service가 중지됩니다.
  2. Wants=other-optional-service.service: other-optional-service.service를 시작하려고 시도하지만 실패해도 my_app.service는 계속 진행됩니다.
  3. After=network.target mariadb.service: my_app.service를 네트워크 대상 및 MariaDB 시작 작업 이후에 순서 지정합니다. 앱이 다른 호스트의 데이터베이스에 TCP로 연결해야 하는 경우 network.target이 "네트워크를 사용할 수 있음"을 의미한다고 가정하는 대신 적절한 network-online 설정을 사용하세요.
  4. WantedBy=multi-user.target: 활성화되면(systemctl enable my_app.service), 이 지침은 시스템이 multi-user.target 상태에 도달할 때 my_app.service가 시작되도록 심볼릭 링크를 추가합니다.

고급 고려 사항 및 모범 사례

  • WantedByRequiredBy: WantsRequires와 유사하게, WantedBy는 약한 순서이고 RequiredBy는 강력한 순서입니다. 대부분의 서비스는 WantedBy=multi-user.target을 사용합니다.
  • Conflicts=: 이 지침은 현재 유닛과 동시에 실행되어서는 안 되는 유닛을 지정합니다. 현재 유닛이 시작되면 충돌하는 유닛이 중지되고 그 반대의 경우도 마찬가지입니다.
  • 전이적 종속성: 종속성은 전이적입니다. A가 B를 필요로 하고 B가 C를 필요로 하면 A는 간접적으로 C를 필요로 합니다. Systemd는 이러한 체인을 자동으로 처리합니다.
  • Condition*= 지침: ConditionPathExists=, ConditionFileNotEmpty=, ConditionVirtualization= 등을 사용하여 시스템 상태에 따라 유닛 활성화를 조건부로 만들어 견고성을 더욱 향상시킵니다.
  • systemctl list-dependencies <unit> 사용: 이 명령은 유닛의 종속성 트리(직접 및 간접 종속성 포함)를 시각화하는 데 매우 유용합니다.
  • systemctl status <unit> 사용: 구성 변경 후 항상 서비스 상태를 확인하세요. 종속성 문제를 포함한 실패 이유를 표시하는 경우가 많습니다.
  • 순환 종속성 피하기: systemd가 해결하려고 시도하지만 직접적인 순환 종속성(A Requires B, B Requires A)은 시작 루프나 실패로 이어질 수 있습니다. 이를 피하기 위해 종속성을 신중하게 설계하세요.

실용적인 디버깅 과정

종속성 버그가 나타나면 systemd가 실제로 구축한 트랜잭션을 살펴보는 것부터 시작하세요:

systemctl status my_app.service
journalctl -b -u my_app.service --no-pager
systemctl list-dependencies my_app.service
systemctl show my_app.service -p Wants -p Requires -p After -p Before -p BindsTo -p PartOf

systemctl status는 systemd가 유닛을 시작하지 못했는지, 나중에 종료했는지, 또는 애플리케이션이 비정상임에도 활성 상태로 간주하는지 알려줍니다. journalctl -b는 현재 부팅 내에서 유지되며, 종속성 문제는 종종 부팅 전용이기 때문에 중요합니다. systemctl show는 직설적이지만 유용합니다: 드롭인, 공급업체 파일 및 생성된 종속성이 적용된 후 최종 병합된 유닛 속성을 보여줍니다.

종속성이 어디서 왔는지 확실하지 않은 경우 전체 유닛을 검사하세요:

systemctl cat my_app.service

이것은 패키지된 유닛과 /etc/systemd/system/my_app.service.d/ 아래의 모든 재정의 파일을 보여줍니다. 기본 유닛은 올바르지만 이전 마이그레이션의 After=mysql.service가 포함된 오래된 재정의가 여전히 있는 프로덕션 서비스를 본 적이 있습니다. 서비스는 더 이상 존재하지 않는 유닛을 기다리고 있었고 로그는 애플리케이션이 손상된 것처럼 보이게 했습니다.

부팅 타이밍 문제의 경우 다음을 사용하세요:

systemd-analyze critical-chain my_app.service
systemd-analyze blame

critical-chain은 타임스탬프를 응시하는 것보다 낫습니다. 서비스로 가는 경로를 지연시킨 유닛을 보여주기 때문입니다. blame은 "나쁜" 서비스의 순위로 취급하면 오해의 소지가 있지만 하나의 종속성이 예상보다 훨씬 오래 걸릴 때 유용합니다.

프로덕션에서 유지되는 패턴

로컬 데이터베이스 종속성의 경우 이것은 합리적인 시작점입니다:

[Unit]
Description=API service
Requires=postgresql.service
After=postgresql.service

[Service]
ExecStart=/usr/local/bin/api
User=api
Restart=on-failure

[Install]
WantedBy=multi-user.target

이는 PostgreSQL이 API와 함께 시작되어야 하고 API 시작이 PostgreSQL 이후에 순서 지정되어야 함을 의미합니다. 모든 마이그레이션이 완료되었거나 데이터베이스가 앱에서 사용하는 정확한 자격 증명을 수락한다는 것을 보장하지는 않습니다. 그것이 중요하다면 애플리케이션 수준의 준비 상태 확인을 추가하세요. Systemd는 프로세스를 순서화할 수 있지만 서비스가 이를 확인하도록 가르치지 않는 한 스키마 상태를 이해할 수 없습니다.

마운트된 경로의 경우 마운트 유닛을 수동으로 지정하는 것보다 RequiresMountsFor=를 선호하세요:

[Unit]
RequiresMountsFor=/srv/uploads

[Service]
ExecStart=/usr/local/bin/upload-worker
User=uploads

Systemd는 경로에 필요한 마운트 유닛을 파생합니다. 이는 /srv/uploadssrv-uploads.mount에 매핑된다는 것을 기억하는 것보다 유지 관리가 더 쉽습니다.

선택적 도우미의 경우 Wants=를 사용하세요:

[Unit]
Wants=metrics-agent.service
After=metrics-agent.service

메트릭 에이전트가 실패하면 기본 서비스는 계속 시작될 수 있습니다. 이것은 일반적으로 로깅 사이드카, 선택적 내보내기 도구 및 로컬 알림 도우미에 대해 원하는 것입니다. 두 서비스가 관련되어 있다고 해서 Requires=를 사용하지 마세요. 종속 서비스가 다른 유닛 없이 유용한 작업을 실제로 수행할 수 없는 경우에만 사용하세요.

함께 중지되어야 하는 긴밀하게 결합된 서비스의 경우 BindsTo=PartOf=를 살펴보세요. BindsTo=Requires=보다 강력하며 특정 장치 유닛에 연결된 서비스와 같이 바인딩된 유닛이 사라지면 서비스가 사라져야 할 때 유용합니다. PartOf=는 그룹에 유용한 경우가 많습니다: 상위 유닛을 다시 시작하거나 중지하면 하위 유닛에 전파될 수 있습니다. 이것들은 첫 번째 선택 지침은 아니지만 Requires=로는 깔끔하게 표현할 수 없는 문제를 해결합니다.

일반적인 함정

WantedBy=multi-user.target으로 활성화된 일반적인 장기 실행 서비스에 After=multi-user.target을 추가하지 마세요. 이는 종종 이상한 순서를 만들고 작성자가 의도한 바를 거의 말하지 않습니다. 대부분의 서비스는 multi-user.target에 의해 풀인됩니다. 이미 도달한 후에 시작할 필요가 없습니다.

network.target이 "인터넷에 연결 가능"을 의미한다고 가정하지 마세요. 이는 네트워크 관리를 위한 동기화 지점이지 연결성 테스트가 아닙니다. 애플리케이션이 원격 API와 통신하는 경우 어쨌든 애플리케이션 내에 재시도 로직을 추가하세요. 부팅 시간 네트워크 순서는 노이즈를 줄이지만 DNS 실패, 라우팅 변경 또는 원격 종속성 중단으로부터 보호할 수 없습니다.

더 나은 옵션이 없는 한 ExecStartPre=/bin/sleep 30에서 긴 절전 모드를 숨기지 마세요. 절전 모드는 종속성이 빠르게 준비될 때 부팅 속도를 늦추고 종속성이 예상보다 오래 걸릴 때 여전히 실패합니다. 실제 소켓, 파일 또는 API를 확인하는 작은 준비 루프가 일반적으로 더 명확합니다.

종속성 지침을 변경한 후 systemctl daemon-reload를 실행하고 서비스를 다시 시작한 다음 동일한 부팅에서 저널을 확인하세요. 가장 빠른 수정은 일반적으로 잘못된 정확한 순서 가정을 증명하는 것입니다.