注意了!這樣用 systemd 可能會有風險

2022-09-14 06:02:42

在 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: root@pts/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: root@pts/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: root@pts/1    
           │   │ ├─5801 -bash
           │   │ ├─6012 sh parent_pro.sh
           │   │ ├─6013 sleep 10
           │   │ ├─6014 systemctl status
           │   │ └─6015 less
           │   └─session-1.scope
           │     ├─5125 sshd: root@pts/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: root@pts/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: root@pts/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: root@pts/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: root@pts/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 引數,可以確保子程序不會被清除。

參考資料

  1. unary operator expected解決方法
  2. Linux 系統註冊系統服務流程
  3. Systemd使用小結
  4. 使用systemctl管理服務