dev/Cloud & Infra

systemd 서비스 unit파일 작성에서 했던 실수

lugi 2019. 2. 2. 19:05

요즘 kafka, elasticsearch, vertica, ansible 등등등을 깔고 연동하고 하는 일을 많이하다보니 자연스럽게 이걸 시스템 서비스로 작성하는 것들도 하게 되었다. 그러면서 몇 가지 삽질을 한 것에 대해서 정리 해 보려고 한다.


아래는

RedHat 공홈에 나오는 service 파일 예제이다

일반적으로 /usr/lib/systemd/system/ 에 .service 라는 이름으로 작성하게 된다.


[Unit]
Description=Postfix Mail Transport Agent
After=syslog.target network.target
Conflicts=sendmail.service exim.service

[Service]
Type=forking
PIDFile=/var/spool/postfix/pid/master.pid
EnvironmentFile=-/etc/sysconfig/network
ExecStartPre=-/usr/libexec/postfix/aliasesdb
ExecStartPre=-/usr/libexec/postfix/chroot-update
ExecStart=/usr/sbin/postfix start
ExecReload=/usr/sbin/postfix reload
ExecStop=/usr/sbin/postfix stop

[Install]
WantedBy=multi-user.target

출처 : https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-managing_services_with_systemd-unit_files


서비스 파일 작성할 때 내가 했던 몇 가지 실수를 정리 해 본다.


1. 서비스 구동시 환경 변수를 사용한다면 .bash_profile 등은 먹지 않는다.

.bash_profile 같은 설정은 bash shell 에 접속했을 때 먹는 변수이다.

그러므로 OS 구동시 사용하는 systemd 서비스에 사용하기 위해서는 아무리 .bash_profile이나 기타 쉘/유저 기반의 환경 변수를 사용해봐야 소용이 없다.

EnvironmentFile 혹은 Environment 프로퍼티를 사용하여 환경 변수를 설정하자


Environment 프로퍼티의 작성 예시는


[Service]
# Client Env Vars
Environment=ETCD_CA_FILE=/path/to/CA.pem
Environment=ETCD_CERT_FILE=/path/to/server.crt
Environment=ETCD_KEY_FILE=/path/to/server.key
# Peer Env Vars
Environment=ETCD_PEER_CA_FILE=/path/to/CA.pem
Environment=ETCD_PEER_CERT_FILE=/path/to/peers.crt
Environment=ETCD_PEER_KEY_FILE=/path/to/peers.key

과 같고


EnvironmentFile의 작성 예시는 아래와 같이 작성한 후 해당 파일 경로를 값으로 준다


COREOS_DIGITALOCEAN_IPV4_ANCHOR_0=X.X.X.X
COREOS_DIGITALOCEAN_IPV4_PRIVATE_0=X.X.X.X
COREOS_DIGITALOCEAN_HOSTNAME=test.example.com
COREOS_DIGITALOCEAN_IPV4_PUBLIC_0=X.X.X.X
COREOS_DIGITALOCEAN_IPV6_PUBLIC_0=X:X:X:X:X:X:X:X

(출처 : https://coreos.com/os/docs/latest/using-environment-variables-in-systemd-units.html)


2. [Install] 섹션 작성시 target의 차이를 구분하자

리눅스에 대해 자세히 공부한 것은 아니라서, 위의 파일을 작성할 때도 일단 다른 서비스의 파일을 복붙해서 고치는 식으로 썼다. 그러다보니 [Install] 섹션 작성시 WantedBy를 network.target 으로 한 파일을 자연스럽게 가져다 썼는데, 그러다보니 몇몇 서비스에서 붙어야 하는 다른 네트워크에 붙지 못하는 문제가 생겼다.

일단 [Install] 섹션에 대해서 이해를 하고 넘어가자면 systemctl enable [service name] 으로 VM 구동시 서비스가 자동으로 구동되도록 할 때 이용하는 섹션이며, WantedBy 는 이 서비스가 어떤 전제조건 하에서 실행되는 지를 결정하는 프로퍼티이다.


해당 부분에 설정되는 타겟은 https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-Managing_Services_with_systemd-Targets 과 영향이 있는데, network.target 은 network가 active 되었을 때를 의미하며, multi-user.target은 runlevel3 환경이 셋업될 때를 의미한다. 이 때 network.target 은 network가 active 인지를 판단하지 실제로 이것이 up되었는지에 대해서는 관여하지 않는다, 그러므로 서비스가 구동되는 시점에 network가 active 이지만 up이 덜 되었다면, 서비스가 다른 네트워크와 통신하는 것이 실패할 수 있다. multi-user.target 은 실제로 runlevel 3  환경이 모두 구동되었을 때이므로 이런 문제를 겪지 않았다.


https://unix.stackexchange.com/questions/126009/cause-a-script-to-execute-after-networking-has-started 에 따르면 실제로 network가 online 상태인지를 판단하는 network-online.target 도 있다고 한다.


3. 서비스를 구동할 때는 Type 을 잘 고려하자

위의 예제에 나온 것은 Type=forking 이다. unit 파일에서 Type은 이 서비스로 인한 서비스가 어떤 형태로 구동될 것인가를 결정하는데 이 종류는 아래와 같다


TypeConfigures the unit process startup type that affects the functionality of ExecStart and related options. One of:
  • simple – The default value. The process started with ExecStart is the main process of the service.
  • forking – The process started with ExecStart spawns a child process that becomes the main process of the service. The parent process exits when the startup is complete.
  • oneshot – This type is similar to simple, but the process exits before starting consequent units.
  • dbus – This type is similar to simple, but consequent units are started only after the main process gains a D-Bus name.
  • notify – This type is similar to simple, but consequent units are started only after a notification message is sent via the sd_notify() function.
  • idle – similar to simple, the actual execution of the service binary is delayed until all jobs are finished, which avoids mixing the status output with shell output of services.

(출처 : https://access.redhat.com/documentation/en-us/red_hat_enterprise_linux/7/html/system_administrators_guide/sect-managing_services_with_systemd-unit_files)


많이 쓰이는 방식은 forking, simple, oneshot 이라고 한다.

forking 은 ExecStart에 명시된 부모프로세스에서 fork()를 호출하여 자식 프로세스를 생성하여 구동하는 방식이며, 자식 프로세스 생성이 완료되면 부모 프로세스는 종료된다. 이것은 부모 프로세스가 생성한 자식 프로세스가 서비스 데몬등으로 지속적으로 구동될 때 적합하다.

simple 은 ExecStart 가 즉시 수행되면서 서비스가 구동되었다고 간주한다. 이 때, 해당 프로세스의 문제로 인해 정상적인 구동이 되지 않았다고 하더라도, systemctl 의 status 는 success 로 표현된다. 실행 이후 별도로 관리가 필요하지 않을 때 적합하다.

oneshot은 simple과 유사하지만, 메인 프로세스가 종료된 이후에도 서비스 매니저는 이를 활성화 상태로 간주하도록 옵션을 줄 수 있다.


내가 겪은 문제는, 서비스 구동에 필요한 것들을 .sh 파일로 작성하여 ExecStart에 넣고, ExecStop에 shutdown 커맨드를 .sh로 넣어 구동을 했는데 java 애플리케이션 구동 등등등이 포함되어 있다보니, 구동 시간이 좀 걸렸다. 그러다보니 forking 으로 .sh 를 ExecStart 를 수행 해 놓으니 애플리케이션들이 초기화 되는 중에 timed out 이 발생하면서, ExecStop 이 돌았고, 서비스들이 뜨는 중에 그걸 죽여버리는 문제가 있었다. 물론 timeout을 늘리는 방법 등으로 해결 할 수도 있을 것 같았지만, 실행 후 굳이 이를 데몬으로 후속 관리가 필요하거나 하지 않아서 Type을 simple로 바꿔 해결했다.


4. nfs 마운트는 RequiresMountsFor로 되더라

클러스터로 묶어놓은 플랫폼들이 몇 개 있는데, 해당 플랫폼들의 config를 nfs로 한 곳에서 관리하도록 해 놓았다. 그래서 해당 플랫폼들을 구동시키는데는 nfs 마운트가 필요했는데, 해당 마운트들은 /etc/fstab 에서 관리되며 그냥 서비스를 구동할 경우에는 nfs 가 mount 되기 전에 서비스가 구동되어 설정 파일을 찾지 못하는 문제가 있었다. 처음엔 After 에 target을 넣어 종속성을 줘 보려고 이런 저런 시도를 많이 했는데 그냥 Unit 섹션에 RequiresMountsFor=경로명을 주고 해결했다.


이외에도 요즘 리눅스를 만지면서 자잘자잘한 문제들을 이것저것 겪고 있는데... 그 때 그 때 정리를 잘 해두어야겠다. 리눅스 공부도 좀 하고