如何優雅的使用 Systemd 管理服務

背景:我們在構建 Kubernetes 容器化平臺時,會在節點上部署各種 agent ,雖然容器化當道的今天很多程序可以直接採用 docker 方式進行運行,但我們在整個集羣內部仍然大量使用了 systemd 來管理基礎服務。不過在使用過程中發現可能出現相關依賴的服務組件異常後導致節點上服務不可用,其次還有可能出現個別服務 bug 而導致整個節點資源消耗殆盡的情況 (幸好發現的早,及時處理後沒有產生業務上的應用),因此,誕生了使用 systemd 在管理服務的過程中,能幫我們管理上下游依賴服務的狀態,且能相對限制部分資源的想法。

通常情況下,當用戶自己使用 systemd 進行管理服務時,需要關注 systemd service 中的如下幾個配置塊:

  • Unit: 主要用於配置整個服務的詳情信息以及服務依賴,用於快速識別該服務的相關情況以及依賴項目
  • Service: 主要用於配置整個服務生命週期的管理行爲
  • Install: 用於配置服務的安裝級別,當我們設置服務在 Linux 的那種級別下啓動或開機自啓動時會加載該部分 (典型的當 chkconfig nginx on 或者 systemctl enable|disable nginx.service 時會讀取該部分的配置)

Unit 塊

Unit 塊除了可以簡單描述整個服務的相關詳情外,還有最重要的一點可以用來管理該服務的啓動順序和啓動依賴 (必須也得由 systemd 管理)。

比如通常我們有一些服務,必須依賴一些基礎環境 (ntpd,rsyslog,network) 才能啓動成功,或者必須在某個服務後啓動,那我們就可以在這裏進行配置。

注意: 在 Unit 快中,每個指令後都可以指定一個以空格分隔的列表

服務描述

在 systemd 的 service 配置中,我們通常會使用如下幾個配置項來描述該服務的基本信息:

  • Description: 服務的簡單描述
  • Documentation: 指定服務的文檔,便於管理員快速追溯,一般可以使用 "http://", "https://", "file:", "info:", "man:" 五種 URI 類型

服務依賴管理

systemd 中會有幾種服務依賴管理的指令,可能在實際使用過程中會有一些疑惑,有時候功能可能重疊,因此需要根據具體的使用場景進行組合。

  • After: 用於指定該服務在那些服務之後啓動 ,停止時正好相反
  • Before: 用於指定該服務在那些服務之前啓動,停止時正好相反
  • Requires: 設置該服務必須依賴的其他服務,因此在該服務啓動之前,指定的服務列表必須全部在線,否則服務將啓動失敗或掉線。但如果未設置 After 和 Before 指令時,該服務和依賴的服務將會並行的同時啓動。注意:該指令依賴的服務不一定要在整個生命週期都保持一直在線,這取決於其他的檢查條件
  • Requisite: 和 Requires 類似,區別是在該服務啓動時,該指令指定的依賴資源必須全部處於啓動成功的狀態,否則該服務立馬失敗,並且不會啓動那些失敗的依賴服務。因此一般建議 Requisite 和 After 一起聯合使用會比較好。
  • WantsRequires 的弱化版,當該服務啓動時,儘可能的啓動該指令指定的服務,但不會影響該服務的啓動
  • BindsTo: 和 Requires 類似,但是依賴性更強,這裏列出來的任何服務停止運行或崩潰,該服務將立即被停止。考慮服務的啓動依賴,一般會和 After 一起使用
  • PartOf: 和 Requires 類似,但僅作用於服務的停止和重啓,表示該服務是所列服務的一部分,會隨指定服務的啓動而啓動。注意:該指令是單向依賴,服務的主動的重啓不會影響其他

示例

# 這裏指定了內部的Kubelet進程必須在docker 和kube-proxy 之後啓動,且需要強依賴,減少調度失敗和pod轉發失敗的問題
[Unit]
Description=kubelet - k8s node lifecycle manager.
Documentation=https://kubernetes.io/zh/docs/reference/command-line-tools-reference/kubelet/
After=docker.service kube-proxy.service
Requisite=docker.service kube-proxy.service

Service 塊

毋庸置疑的是,我們通常使用 systemd 最初的想法是用來啓動和停止進程,因此,整個 systemd 最核心的部分也就在 Service 塊了,因此我們需要對該部分有很熟悉的理解。

基本指令

  • Type: 指定進程的啓動類型,必須設爲 simple, exec, forking, oneshot, dbus, notify, idle 之一。常用的幾種如下:
    • simple: 一般沒有其他指令時,爲默認值,表示 ExecStart 後的指令爲主進程,主進程啓動後服務即啓動成功。如果進程需要爲其他進程提供服務,需要通過 socket 來進行
    • exec: 同 simple 類似,但是表示該服務的主服務進程執行完成之後,才真正啓動成功。
    • forking: 標識在使用 ExecStart 後的指令啓動程序後,會進行 fork() 的系統調用。傳統 Unix 中守護進程的經典做法,如果使用此類型,建議設置 PIDFile= 來指定該服務的主進程。比如 nginx 進程的 daemon 方式,主進程執行完成後,至少有 1 個進程在運行,此時服務狀態會變成 active 狀態
    • oneshot: 同 simple 類似,但是表示只有在該服務的主服務進程退出之後,才真正執行成功。一般用於執行一次性任務 (配置 RemainAfterExit=yes 設置可停止的一次性服務)
  • PIDFile: 指定該服務的 PID 文件路徑。systemd 會在啓動後讀取主服務進程的 PID,並記錄在 MAINPID 變量中,在停止服務後,會主動進行刪除該文件
  • KillMode: 指定停止服務時,殺死進程的方式,必須設爲 control-group, process, mixed, none 之一。(process 表示僅殺死主進程;mixed 表示向主進程發 SIGTERM 信號,然後想服務內 cgroup 的其他進程發 SIGKILL 信號;none 表示僅執行 ExecStop 動作,而不殺死進程)

注意: 建議對長時間持續運行的服務儘可能使用 Type=simple;如果有 master/slave 進程的建議使用 Type=forking

環境變量初始化

通常我們的服務主進程可能會有比較多的參數,建議的做法是使用環境變量方式進行維護對應的參數,而在 systemd 中可支持 K/V 和配置文件方式來進行設置。

  • Environment: 指定 Key=Value 的環境配置
  • EnvironmentFile: 指定包含 key=value 的配置文件

注意: 通常情況下,我們會看到 EnvironmentFile=-filename 和 EnvironmentFile=filename 的寫法,前者表示可忽略,即當文件不存在或環境變量加載失敗時也不影響後續的處理邏輯。(在 etcd 的高版本里,大部分參數採用了環境變量加載的方式,可見未來通過環境變量來控制服務的啓動參數已經是趨勢)

進程生命週期管理

整個進程的生命週期是從準備啓動進程開始到整個進程結束的過程管理。通常情況下,可以包含爲如下幾個環節:

  • ExecStartPre: 啓動前, 該指令可用於在進程啓動前加載一些列的基礎環境以及初始化檢查機制
  • ExecStart: 啓動邏輯, 該指令用於啓動進程的核心邏輯
  • ExecStartPost: 啓動後, 該指令用於在啓動進程後進行回調的相關操作
  • ExecReload: 熱加載, 該指令用於對進程進行熱加載,通常情況用於 配置變更後的熱重啓 (/bin/kill -s HUP $MAINPID)
  • ExecStop: 停止, 該指令用於對進程執行指定的停止操作,通常情況下用於進行進程的優雅停止 (/bin/kill -s TERM $MAINPID)
  • ExecStopPost: 停止後, 該指令用於在進程停止後,進行一些列的資源釋放和等待操作,因爲通常情況當進程停止後,相關資源不能立即釋放,因爲服務不算真正唄停止,如果此時認爲進程已停止而強制做一些操作,可能會影響到相關服務處理邏輯
  • TimeoutStopSec: 停止超時時間, 指定服務停止後的超時時間,一般在通知服務是會使用 SIGTERM 信號,待超時時間內資源未釋放,將使用 SIGKILL 進行強制殺死

重啓策略

  • Restart: 當服務進程 正常退出、異常退出、被殺死、超時的時候, 是否重新啓動該服務。
  • RestartSec: 多久後重啓
  • StartLimitBurst: 啓動的最大次數限制,超過後停止繼續重啓
  • StartLimitInterval: 啓動時間的最大間隔

所謂 "服務進程" 是指 ExecStartPre=, ExecStartPost=, ExecStop=, ExecStopPost=, ExecReload= 中設置的進程。 當進程是由於 systemd 的正常操作 (例如 systemctl stop|restart) 而被停止時, 該服務不會被重新啓動。 所謂 "超時" 可以是看門狗的 "keep-alive ping" 超時, 也可以是 systemctl start|reload|stop 操作超時.

可選值如下:

  • no: 默認值,不會重啓
  • on-success: 服務進程正常退出 (退出碼爲 "0", 或者進程收到 SIGHUP, SIGINT, SIGTERM, SIGPIPE 信號之一, 並且 退出碼符合 SuccessExitStatus= 的設置) 時進行重啓
  • on-failure: 僅在服務進程異常退出 (退出碼不爲 "0", 或者 進程被強制殺死 (包括 "core dump" 以及收到 SIGHUP, SIGINT, SIGTERM, SIGPIPE 之外的其他信號), 或者進程由於 看門狗超時 或者 systemd 的操作超時 而被殺死) 時重啓
  • always: 無條件重啓

資源限制

通常情況下,我們會對目標服務或進程進行一定資源的限制,以防止服務 bug 導致的整個系統資源耗盡。

在 systemd 中,可以使用如下指令進行簡單的資源限制:

  • LimitCPU: 限制 CPU 使用時長 (秒),等同 ulimit -t
  • LimitNOFILE: 限制進程使用的文件描述符數量,等同 ulimit -n
  • LimitNPROC: 限制進程的數量,等同於 ulimit -u

注意: 一般爲防止服務導致的系統過載,會對進程設置一些資源限制

計算資源調度

  • CPUSchedulingPolicy: 設置 CPU 的調度策略可設爲 other, batch, idle, fifo, rr 之一
  • CPUSchedulingPriority: 設置 CPU 調度的優先級,取決於調度策略
  • CPUAffinity: 設置 CPU 的親和性,可以指定 cpu 的編號,比如 0,1

示例

[Service]
EnvironmentFile=/etc/calico/calico.env
ExecStartPre=-/usr/bin/docker pull ${CALICO_IMAGE}
ExecStart=/usr/bin/docker run --net=host --privileged ${CALICO_IMAGE}
ExecStop=-/usr/bin/docker stop calico-node
ExecStopPost=-/usr/bin/docker rm -f -v calico-node
Restart=on-failure
RestartSec=5
StartLimitBurst=3
StartLimitInterval=60s
CPUSchedulingPolicy=fifo
CPUAffinity=0,2
LimitNOFILE=1000
LimitNPROC=3000
LimitCORE=infinity

Install 塊

在 systemd 的服務管理中,install 塊主要包含了單元的啓用信息。只有 systemctl 的 enable/disable 指令會調用該部分內容。

  • Alias: 指定服務的別名信息
  • WantedBy/RequiredBy: 設置啓用服務的依賴服務

示例

[Install]
Alias=syslog.service
WantedBy=multi-user.target

完整示例

systemd service 完整配置

$ cat  /usr/lib/systemd/system/docker.service
[Unit]
Description=Docker CE Binary Release.
Documentation=https://docs.docker.com
Documentation=https://download.docker.com/linux/static/stable/x86_64/
After=network-online.target firewalld.service
Wants=network-online.target

[Service]
Type=notify
Environment="WELCOME=BGBiao Docker Base Environment."
ExecStartPre=/bin/echo ${WELCOME}
ExecStart=/usr/local/sbin/dockerd  $DOCKER_NETWORK_OPTIONS --live-restore --data-root /opt/data/docker
ExecStartPost=/usr/sbin/iptables -P FORWARD ACCEPT
ExecReload=/bin/kill -s HUP ${MAINPID}
ExecStop=/bin/kill -s TERM ${MAINPID}
ExecStopPost=/usr/bin/sleep 3

LimitNOFILE=100
LimitNPROC=3000
LimitCORE=infinity
TimeoutStartSec=0
Delegate=yes
KillMode=process
Restart=always
StartLimitBurst=3
StartLimitInterval=60s

[Install]
WantedBy=multi-user.target

service 生命週期過程

# 加載配置並重啓docker
$ systemctl daemon-reload
$ systemctl restart docker

# 查看整個啓動過程信息
$ cat /var/log/messages

Oct 31 14:56:39 host-192-168-0-171 systemd: Reloading.
Oct 31 14:56:43 host-192-168-0-171 systemd: Starting Docker CE Binary Release....
Oct 31 14:56:43 host-192-168-0-171 echo: BGBiao Docker Base Environment.
Oct 31 14:56:43 host-192-168-0-171 dockerd: time="2020-10-31T14:56:43.390806162+08:00" level=info msg="Starting up"
......
Oct 31 14:56:43 host-192-168-0-171 systemd: Started Docker CE Binary Release..


# 停止docker,查看整個過程

$ echo >  /var/log/messages
$ systemctl stop docker
$ cat /var/log/messages

Oct 31 15:01:11 host-192-168-0-171 systemd: Stopping Docker CE Binary Release....
Oct 31 15:01:11 host-192-168-0-171 dockerd: time="2020-10-31T15:01:11.618357841+08:00" level=info msg="Processing signal 'terminated'"
Oct 31 15:01:11 host-192-168-0-171 dockerd: time="2020-10-31T15:01:11.618919303+08:00" level=info msg="Daemon shutdown complete"
Oct 31 15:01:11 host-192-168-0-171 dockerd: time="2020-10-31T15:01:11.618948850+08:00" level=info msg="stopping healthcheck following graceful shutdown" module=libcontainerd
Oct 31 15:01:11 host-192-168-0-171 dockerd: time="2020-10-31T15:01:11.618971913+08:00" level=info msg="stopping event stream following graceful shutdown" error="context canceled" module=libcontainerd namespace=plugins.moby
Oct 31 15:01:11 host-192-168-0-171 dockerd: time="2020-10-31T15:01:11.618952081+08:00" level=info msg="stopping event stream following graceful shutdown" error="context canceled" module=libcontainerd namespace=moby
Oct 31 15:01:11 host-192-168-0-171 dockerd: time="2020-10-31T15:01:11.618954836+08:00" level=info msg="Processing signal 'terminated'"
Oct 31 15:01:15 host-192-168-0-171 systemd: Stopped Docker CE Binary Release..


# 執行
$ systemctl enable docker
Created symlink from /etc/systemd/system/multi-user.target.wants/docker.service to /usr/lib/systemd/system/docker.service.

# 會在多用戶下創建服務軟鏈,可保證系統在啓動時默認啓動服務
$ ls -ld /etc/systemd/system/multi-user.target.wants/docker.service
lrwxrwxrwx 1 root root 38 Oct 31 15:05 /etc/systemd/system/multi-user.target.wants/docker.service -> /usr/lib/systemd/system/docker.service


# 查看資源限制
## 系統的資源限制
$ ulimit -a | egrep  '(files|processes)'
open files                      (-n) 65535
max user processes              (-u) 63457

$ systemctl status docker | grep -i pid
 Main PID: 21926 (dockerd)

## dockerd 進程的資源限制(生產環境需嚴格設置參數)
$ cat /proc/21926/limits | egrep '(files|processes)'
Max processes             3000                 3000                 processes
Max open files            100                  100                  files

由上述的整個過程可以看到,我們在整個 dockerd 的生命週期中,systemd 如何去管理進程的生命週期的。

Tips

需要注意的是,對於資源限制,Linux 中的 /etc/security/limits.conf 作用域變小了 (只對登錄用戶的資源進行限制),可能對 systemd 的 service 的資源限制不生效,因此對於 systemd servie 的資源限制,可以對全局文件進行修改:

$ cat /etc/systemd/system.conf
DefaultLimitNOFILE=100000
DefaultLimitNPROC=65535

官網文檔

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章