注意了!這樣用 systemd 可能會有風險
在 Linux 6 / CentOS 6 中,使用 service 來進行服務的起停,但是在 Linux 7 / CentOS 7 中,替換為使用 systemctl 命令來控制。將一些常用應用註冊成服務後,可以使用 systemctl 命令來方便的操作啟動、停止、重啟,但是作者最近發現如果配置不當,嚴重情況下可能影響正常業務執行,請大家務必關注。
$ service start loop.service The service command supports only basic LSB actions (start, stop, restart, try-restart, reload, force-reload, status). For other actions, please try to use systemctl.
問題描述
如果我們有一些負責自動化的應用,例如 puppet / mco、ansible,這裡簡稱為宿主服務。宿主服務註冊為系統服務並且隨系統開機自啟動。宿主服務支援接收服務端指令並拉起一些常駐程序,拉起的程序我們簡稱為子程序。當宿主服務被 kill 或意外終止時,會引起子程序一起被 kill。
為了復現這個問題,我寫了兩個指令碼。parent_pro.sh 作為宿主指令碼,註冊為系統 loop.service 並且隨系統啟動。該指令碼的作用是每隔10秒鐘檢查 config.txt 的配置,如果配置檔案中的數字變成 1 則拉起一個無限迴圈的子程序。具體的程式碼可以在我的 Github 上看到。
我的 service 定義資訊:
[Unit] Description=loop service After=network.target [Service] Type=forking StandardOutput=/root/namespace/parent_pro.log StandardError=/root/namespace/parent_pro.log WorkingDirectory=/root/namespace User=root Group=root ExecStart=/usr/bin/sh parent_pro.sh [Install] WantedBy=multi-user.target
下面執行啟動服務。
$ systemctl start loop.service $ ps -ef | grep sh root 4615 1 0 15:59 ? 00:00:00 /usr/bin/sh parent_pro.sh root 5153 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh root 5197 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh root 5248 4615 0 16:00 ? 00:00:00 sh /root/namespace/child_loop.sh
從上面的這段輸出可以看到,parent_pro.sh 已經啟動了三個子程序,我們再看下 parent_pro.sh 的詳細資訊。
$ systemctl status ● qking-101 State: running Jobs: 0 queued Failed: 0 units Since: Thu 2022-08-11 15:59:54 CST; 29s ago CGroup: / ├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22 ├─user.slice │ └─user-0.slice │ └─session-1.scope │ ├─5125 sshd: [email protected]/0 │ ├─5129 -bash │ ├─5224 systemctl status │ └─5225 less └─system.slice ├─rsyslog.service │ └─4618 /usr/sbin/rsyslogd -n ├─postfix.service │ ├─4944 /usr/libexec/postfix/master -w │ ├─4948 pickup -l -t unix -u │ └─4949 qmgr -l -t unix -u ├─loop.service │ ├─4615 /usr/bin/sh parent_pro.sh │ ├─5153 sh /root/namespace/child_loop.sh │ ├─5197 sh /root/namespace/child_loop.sh │ ├─5198 sleep 10 │ ├─5220 sleep 1 │ └─5222 sleep 1
可以看到我註冊的 loop.service 在 system.slice 這個 cgroup 目錄下。接下來複現問題場景。
$ kill 4615 $ ps -ef | grep sh root 1857 2 0 15:59 ? 00:00:00 [kdmflush] root 1873 2 0 15:59 ? 00:00:00 [kdmflush] root 3220 2 0 15:59 ? 00:00:00 [kdmflush] root 4613 1 0 15:59 ? 00:00:00 /usr/sbin/sshd -D root 5125 4613 0 16:00 ? 00:00:00 sshd: [email protected]/0 root 5129 5125 0 16:00 pts/0 00:00:00 -bash root 5330 5129 0 16:00 pts/0 00:00:00 grep --color=auto sh
殺掉父程序之後,子程序跟著消失了。但是如果手工執行 parent_pro.sh 不會出現這個問題。
$ nohup sh parent_pro.sh > parent_pro.log 2>&1 & $ systemctl status ● qking-101 State: running Jobs: 0 queued Failed: 0 units Since: Thu 2022-08-11 15:59:54 CST; 2h 21min ago CGroup: / ├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22 ├─user.slice │ └─user-0.slice │ ├─session-4.scope │ │ ├─5797 sshd: [email protected]/1 │ │ ├─5801 -bash │ │ ├─6012 sh parent_pro.sh │ │ ├─6013 sleep 10 │ │ ├─6014 systemctl status │ │ └─6015 less │ └─session-1.scope │ ├─5125 sshd: [email protected]/0 │ └─5129 -bash
修改配置檔案讓父程序拉起一些子程序。
$ echo 0 > config.txt $ ps -ef | grep sh root 6012 5801 0 18:21 pts/1 00:00:00 sh parent_pro.sh root 6018 6012 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6041 6012 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6084 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6147 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6232 6012 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh $ systemctl status ● qking-101 State: running Jobs: 0 queued Failed: 0 units Since: Thu 2022-08-11 15:59:54 CST; 2h 23min ago CGroup: / ├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22 ├─user.slice │ └─user-0.slice │ ├─session-4.scope │ │ ├─5797 sshd: [email protected]/1 │ │ ├─5801 -bash │ │ ├─6012 sh parent_pro.sh │ │ ├─6018 sh /root/namespace/child_loop.sh │ │ ├─6041 sh /root/namespace/child_loop.sh │ │ ├─6084 sh /root/namespace/child_loop.sh │ │ ├─6147 sh /root/namespace/child_loop.sh │ │ ├─6232 sh /root/namespace/child_loop.sh │ │ ├─6337 sh /root/namespace/child_loop.sh │ │ ├─6460 sh /root/namespace/child_loop.sh │ │ ├─6603 sh /root/namespace/child_loop.sh │ │ ├─6766 sh /root/namespace/child_loop.sh │ │ ├─6949 sh /root/namespace/child_loop.sh │ │ ├─6950 sleep 10 │ │ ├─7069 sleep 1 │ │ ├─7071 sleep 1 │ │ ├─7073 sleep 1 │ │ ├─7075 sleep 1 │ │ ├─7077 sleep 1 │ │ ├─7079 sleep 1 │ │ ├─7081 sleep 1 │ │ ├─7084 sleep 1 │ │ ├─7085 sleep 1 │ │ ├─7087 sleep 1 │ │ ├─7088 systemctl status │ │ └─7089 less │ └─session-1.scope │ ├─5125 sshd: [email protected]/0 │ └─5129 -bash └─system.slice ├─rsyslog.service
這時殺掉父程序不會對子程序產生影響。
$ kill 6012 $ ps -ef | grep sh root 6018 1 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6041 1 0 18:21 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6084 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6147 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6232 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6337 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6460 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6603 1 0 18:22 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6766 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 6949 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 7154 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 7377 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh root 7620 1 0 18:23 pts/1 00:00:00 sh /root/namespace/child_loop.sh [1]+ Terminated nohup sh parent_pro.sh > parent_pro.log 2>&1 $ systemctl status ● qking-101 State: running Jobs: 0 queued Failed: 0 units Since: Thu 2022-08-11 15:59:54 CST; 2h 24min ago CGroup: / ├─1 /usr/lib/systemd/systemd --switched-root --system --deserialize 22 ├─user.slice │ └─user-0.slice │ ├─session-4.scope │ │ ├─5797 sshd: [email protected]/1 │ │ ├─5801 -bash │ │ ├─6018 sh /root/namespace/child_loop.sh │ │ ├─6041 sh /root/namespace/child_loop.sh │ │ ├─6084 sh /root/namespace/child_loop.sh │ │ ├─6147 sh /root/namespace/child_loop.sh │ │ ├─6232 sh /root/namespace/child_loop.sh │ │ ├─6337 sh /root/namespace/child_loop.sh │ │ ├─6460 sh /root/namespace/child_loop.sh │ │ ├─6603 sh /root/namespace/child_loop.sh │ │ ├─6766 sh /root/namespace/child_loop.sh │ │ ├─6949 sh /root/namespace/child_loop.sh │ │ ├─7154 sh /root/namespace/child_loop.sh │ │ ├─7377 sh /root/namespace/child_loop.sh │ │ ├─7620 sh /root/namespace/child_loop.sh │ │ ├─9114 sleep 1 │ │ ├─9116 sleep 1 │ │ ├─9119 sleep 1 │ │ ├─9120 sleep 1 │ │ ├─9122 sleep 1 │ │ ├─9126 sleep 1 │ │ ├─9127 sleep 1 │ │ ├─9128 sleep 1 │ │ ├─9130 sleep 1 │ │ ├─9132 sleep 1 │ │ ├─9134 sleep 1 │ │ ├─9136 sleep 1 │ │ ├─9138 sleep 1 │ │ ├─9139 systemctl status │ │ └─9140 less │ └─session-1.scope │ ├─5125 sshd: [email protected]/0 │ └─5129 -bash └─system.slice
問題分析
仔細查閱了 systemd 相關文件,發現這個問題主要與 type 引數有關。
Type:定義 service 的型別,主要有以下型別:
- simple:預設型別,啟動的程式就是主體程式,這個程式要是退出那麼一切皆休。因為 simple 型別不存在主程序退出的情況也就不存在有返回狀態的情況,所以它一旦啟動就認為是成功的,除非沒起來。
- forking:標準 Unix Daemon 使用的啟動方式。啟動程式後會呼叫 fork() 函式,把必要的通訊頻道都設定好之後父程序退出,留下守護精靈的子程序。如果啟動的程序能夠自己建立pidfile,最好也指定下PIDFile=XXX。
- oneshot:一次性服務,這種服務型別就是啟動,完成,沒程序了。因為這類服務執行完就沒程序了,我們經常會需要 RemainAfterExit=yes。後面配置的意思是說,即使沒程序了,我們也要 Systemd 認為該服務是存在併成功了的。其他型別千萬不要去設定RemainAfterExit=yes,否則systemd會認為服務啟動成功了,重啟或再去啟動都會失敗。
- dbus:這個程式啟動時需要獲取一塊 DBus 空間,所以需要和 BusName= 一起用。只有它成功獲得了 DBus 空間,依賴它的程式才會被啟動。
- notify:這個程式在啟動完成後會通過 sd_notify 傳送一個通知訊息。所以還需要配合 NotifyAccess 來讓 Systemd 接收訊息,後者有三個級別:none,所有訊息都忽略掉; main,只接受我們程式的主程序發過去的訊息; all,我們程式的所有程序發過去的訊息都算。NotifyAccess 要是不寫的話預設是 main。
- idle:這個程式要等它裡面排程的全部其它東西都跑完才會跑它自己。比如你 ExecStart 的是個 shell 指令碼,裡面可能跑了一些別的東西,如果不這樣的話,那很可能別的東西的控制檯輸出裡會多一個“啟動成功”這樣的 Systemd 訊息。
最後,如果大家的服務會處理我這種場景,請務必記住這個引數要設定好了,另外還有一個 KillMode 引數也與 cgroup 有關,當我們執行 systemctl stop loop.service 的時候,整個 cgroup 會被刪除,通過設定 KillMode 引數,可以確保子程序不會被清除。