Systemd 문제 해결: 서비스 종속성 및 순서 지정 지시자 이해

이 문서는 systemd 서비스 종속성 문제 해결을 위한 포괄적인 가이드를 제공합니다. 서비스 시작 순서를 관리하고, 경쟁 상태(race conditions)를 방지하며, 핵심 서비스가 안정적으로 시작되도록 보장하기 위해 `Requires`, `Wants`, `After`, `Before` 지시자를 효과적으로 사용하는 방법을 알아보십시오. 견고한 Linux 서비스 구성을 구축하려는 시스템 관리자 및 개발자에게 필수적인 자료입니다.

38 조회수

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

Linux를 위한 최신 시스템 및 서비스 관리자인 Systemd는 시스템 서비스를 관리하는 강력하고 유연한 방법을 제공합니다. Systemd를 구성할 때 흔히 발생하는 문제는 서비스가 올바른 순서로 시작하고 종속성이 제대로 충족되는지 확인하는 것입니다. 잘못 구성된 종속성은 경합 조건(race conditions)으로 이어질 수 있으며, 이는 서비스가 전제 조건이 준비되기 전에 시작을 시도하여 실패하거나 예기치 않은 동작을 초래할 수 있습니다. 이 글에서는 서비스 종속성 및 순서를 제어하는 Systemd 유닛 파일의 핵심 지시문인 Requires, Wants, After, Before에 대해 자세히 다룹니다. 이러한 지시문을 이해하고 올바르게 구현하는 것은 강력하고 신뢰할 수 있는 시스템 구성을 구축하는 데 필수적입니다.

서비스 종속성을 제대로 관리하는 것은 단순히 시작 실패를 방지하는 것 이상의 의미를 가집니다. 이는 예측 가능하고 안정적인 운영 환경을 조성하는 것입니다. 서비스들이 서로 의존할 때, Systemd는 서비스의 시작 및 종료를 어떻게 조율해야 하는지에 대한 명시적인 지시를 필요로 합니다. 이러한 지시를 제공하지 않으면 추적하기 어려운 미묘한 버그가 발생할 수 있으며, 이는 특정 부하 조건이나 시스템 재부팅 시에만 나타나는 경우가 많습니다. 종속성 및 순서 지정 지시문을 숙달함으로써 서비스 라이프사이클을 세밀하게 제어하고 중요 애플리케이션 및 시스템 구성 요소가 의도한 대로 작동하도록 보장할 수 있습니다.

핵심 종속성 지시문: RequiresWants

Systemd는 유닛 간의 직접적인 종속성을 정의하기 위해 두 가지 주요 지시문인 RequiresWants를 사용합니다. 이 지시문들은 유닛 파일(예: .service 파일)의 [Unit] 섹션 안에 위치합니다.

Requires=

Requires= 지시문은 강력한 종속성을 설정합니다. 유닛 A가 유닛 B를 Requires= 한다면, 유닛 A가 성공적으로 활성화된 것으로 간주되려면 유닛 B가 반드시 활성 상태여야 합니다. 만약 유닛 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도 중지할 것입니다. mariadb.service가 실행되고 있지 않은 상태에서 myapp.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= vs. Wants=

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

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

순서 지정 지시문: AfterBefore

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

After=

After= 지시문은 현재 유닛이 After=에 나열된 유닛들이 성공적으로 활성화된 후에만 시작되어야 함을 지정합니다. 이는 종속 서비스가 자체 시작 시퀀스를 시작하기 전에 전제 조건 서비스가 작동 중인지 확인합니다.

예시:

네트워크에 의존하는 서비스(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.service를 시작하려고 시도하기 전에 network.target이 활성 상태인지 확인할 것입니다. 만약 network.target이 아직 준비되지 않았다면, custom-network-app.service는 지연될 것입니다.

Before=

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

예시:

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

[Unit]
Description=Postfix Mail Transfer Agent
# ... other directives like 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이 있다면, postfix.service는 가장 나중에 중지되는 서비스 중 하나가 될 것입니다.

After= vs. Before=

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

After=Before=는 상호 보완적이라는 것을 이해하는 것이 중요합니다. 만약 유닛 A가 유닛 B 다음에 시작하고 유닛 B도 유닛 A 다음에 시작하기를 원한다면, 일반적으로 유닛 A에 After=BBefore=B를 모두 사용할 것입니다. 이는 엄격한 순서: A가 시작한 다음 B가 시작하는 것을 만듭니다. 그 반대의 경우, B가 시작한 다음 A가 시작하는 것도 마찬가지입니다. 순서를 지정할 때는 일반적으로 유닛이 시작되기 전에 무엇이 일어나야 하는지보다, 유닛이 시작된 후에 무엇이 일어나야 하는지를 지정하는 것이 더 직관적입니다. 예를 들어, 서비스가 네트워크 다음에 시작되도록 하려면 서비스에 After=network.target을 추가합니다. 서비스가 종료 타겟 이전에 시작되도록 하려면 Before=shutdown.target을 사용합니다.

견고한 구성을 위한 지시문 결합

실제 시나리오에서는 복잡한 종속성 그래프를 만들기 위해 이러한 지시문들을 자주 결합하게 됩니다. 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.servicenetwork.targetmariadb.service가 성공적으로 활성화된 후에만 시작되도록 보장합니다. 이는 데이터베이스에 접근 가능하고 네트워크가 준비되었는지 확인하는 데 중요합니다.
  4. WantedBy=multi-user.target: 활성화될 때(systemctl enable my_app.service), 이 지시문은 my_app.service가 시스템이 multi-user.target 상태에 도달했을 때 시작되도록 심볼릭 링크를 추가합니다.

고급 고려 사항 및 모범 사례

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

결론

Systemd의 종속성 및 순서 지정 지시문을 숙달하는 것은 Linux 서비스를 관리하는 모든 사람에게 필수적입니다. Requires, Wants, After, Before를 올바르게 사용하여 안정적으로 시작하고 경합 조건과 같은 일반적인 함정을 피하는 탄력적인 시스템을 구축할 수 있습니다. 강력한 종속성과 약한 종속성 간의 미묘한 차이, 그리고 "무엇을"과 "언제" 사이의 차이를 이해하면 서비스 라이프사이클을 정밀하게 제어할 수 있어 더욱 안정적이고 예측 가능한 시스템 동작을 유도할 수 있습니다. systemctl statussystemctl list-dependencies를 사용하여 구성을 철저히 테스트하여 서비스가 의도한 대로 조율되는지 항상 확인하십시오.