哈嘍大家好,我是鹹魚。 我們知道 CentOS 7 之後,Systemd 代替了原來的 SystemV 來管理服務,相比 SystemV ,Systemd 能夠很好地解決各個服務間的依賴關係,還能讓所有的服務同時啟動,而不是串列啟動。 通常情況下,yum 安裝的軟體會由系統的包管理器(如 RPM)安 ...
哈嘍大家好,我是鹹魚。
我們知道 CentOS 7 之後,Systemd 代替了原來的 SystemV 來管理服務,相比 SystemV ,Systemd 能夠很好地解決各個服務間的依賴關係,還能讓所有的服務同時啟動,而不是串列啟動。
通常情況下,yum 安裝的軟體會由系統的包管理器(如 RPM)安裝,並且會配置相應的 systemd 服務,因此由 systemd 來管理。然而,在一些情況下,特別是當採用源碼編譯安裝軟體或者軟體本身並沒有提供 systemd 管理的解決方案時,就需要手動編寫 systemd 服務文件(service 文件)來管理這些軟體。
那今天我們就來看看手動編寫 systemd 服務文件來管理軟體時發現的一些問題。
問題出現
我們的 zookeeper 是通過源碼編譯來安裝,為了方便管理,決定改成通過 systemd 來管理。
下麵是 zookeeper 的 service 文件
# zookeeper
[Unit]
Description=Zookeeper
After=network.target
[Service]
Type=forking
ExecStart=/opt/zookeeper/bin/zkServer.sh start
ExecStop=/opt/zookeeper/bin/zkServer.sh stop
PIDFile=/var/lib/zookeeper/zookeeper_server.pid
[Install]
WantedBy=multi-user.target
可以看到,配置文件分成幾個區塊,每個區塊包含若幹條鍵值對。
[Unit]
Unit
部分的 Description
欄位給出當前服務的簡單描述接下來的設置是啟動順序:
After
欄位:表示zookeeper.service
應該在哪些服務之後啟動。Before
欄位,表示zookeeper.service
應該在哪些服務之前啟動。
註意,After
和Before
欄位只涉及啟動順序,不涉及依賴關係。
[Service]
Service
部分定義如何啟動當前服務。
-
ExecStart
:啟動進程時執行的命令。 -
ExecStop
:停止服務時執行的命令。 -
Type
:定義啟動類型- simple(預設值):
ExecStart
欄位啟動的進程為主進程 - forking:
ExecStart
欄位將以fork()
方式啟動,此時父進程將會退出,子進程將成為主進程 - oneshot:類似於
simple
,但只執行一次,Systemd 會等它執行完,才啟動其他服務 - dbus:類似於
simple
,但會等待 D-Bus 信號後啟動 - notify:類似於
simple
,啟動結束後會發出通知信號,然後 Systemd 再啟動其他服務 - idle:類似於
simple
,但是要等到其他任務都執行完,才會啟動該服務。一種使用場合是為讓該服務的輸出,不與其他服務的輸出相混合
- simple(預設值):
[Install]
Install
部分定義如何安裝這個配置文件,即怎樣做到開機啟動
WantedBy
:表示該服務所在的 Target。
Target
的含義是服務組,表示一組服務。WantedBy=multi-user.target
指的是,kafka 和 zookeeper 所在的 Target 是 multi-user.target
。
這個設置非常重要,因為執行systemctl enable
命令時,zookeeper .service
的一個符號鏈接,就會放在/etc/systemd/system
目錄下麵的multi-user.target.wants
子目錄之中。
Systemd 有預設的啟動 Target。
[root@localhost ~]# systemctl get-default
multi-user.target
上面的結果表示,預設的啟動 Target 是 multi-user.target
。在這個組裡的所有服務,都將開機啟動。這就是為什麼 systemctl enable
命令能設置開機啟動的原因。
編寫好 service 文件之後,我們執行下麵的命令來啟動 zookeeper:
[root@localhost ~]# systemctl start zookeeper.service
接著看下 zookeeper 的運行狀態
[root@localhost ~]# systemctl status zookeeper.service
● zookeeper.service - Zookeeper
Loaded: loaded (/usr/lib/systemd/system/zookeeper.service; enabled; vendor preset: disabled)
Active: active (running) since 一 2024-04-01 09:10:23 CST; 2s ago
Process: 60955 ExecStop=/opt/zookeeper/bin/zkServer.sh stop (code=exited, status=0/SUCCESS)
Process: 61116 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61132 (java)
CGroup: /system.slice/zookeeper.service
└─61132 java -Dzookeeper.log.dir=/opt/zookeeper/bin/../logs -Dzookeeper.log.file=zookeeper--server-localhost.localdomain.log -Dzookeepe...
4月 01 09:10:22 localhost.localdomain systemd[1]: Starting Zookeeper...
4月 01 09:10:22 localhost.localdomain zkServer.sh[61116]: /usr/sbin/java
4月 01 09:10:22 localhost.localdomain zkServer.sh[61116]: ZooKeeper JMX enabled by default
4月 01 09:10:22 localhost.localdomain zkServer.sh[61116]: Using config: /opt/zookeeper/bin/../conf/zoo.cfg
4月 01 09:10:23 localhost.localdomain zkServer.sh[61116]: Starting zookeeper ... STARTED
4月 01 09:10:23 localhost.localdomain systemd[1]: Started Zookeeper.
active (running)
表示運行正常
當我們執行 systemctl stop zookeeper.service
命令停止 zookeeper 的時候,問題出現了
[root@localhost ~]# systemctl status zookeeper.service
● zookeeper.service - Zookeeper
Loaded: loaded (/usr/lib/systemd/system/zookeeper.service; enabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 一 2024-04-01 09:10:30 CST; 906ms ago
Process: 61183 ExecStop=/opt/zookeeper/bin/zkServer.sh stop (code=exited, status=0/SUCCESS)
Process: 61116 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61132 (code=exited, status=143)
4月 01 09:10:23 localhost.localdomain systemd[1]: Started Zookeeper.
4月 01 09:10:29 localhost.localdomain systemd[1]: Stopping Zookeeper...
4月 01 09:10:29 localhost.localdomain zkServer.sh[61183]: /usr/sbin/java
4月 01 09:10:29 localhost.localdomain zkServer.sh[61183]: ZooKeeper JMX enabled by default
4月 01 09:10:29 localhost.localdomain zkServer.sh[61183]: Using config: /opt/zookeeper/bin/../conf/zoo.cfg
4月 01 09:10:29 localhost.localdomain systemd[1]: zookeeper.service: main process exited, code=exited, status=143/n/a
4月 01 09:10:30 localhost.localdomain zkServer.sh[61183]: Stopping zookeeper ... STOPPED
4月 01 09:10:30 localhost.localdomain systemd[1]: Stopped Zookeeper.
4月 01 09:10:30 localhost.localdomain systemd[1]: Unit zookeeper.service entered failed state.
4月 01 09:10:30 localhost.localdomain systemd[1]: zookeeper.service failed.
可以看到,zookeeper 服務在停止後並不是 inactive
,而是 failed
狀態,最後兩行輸出里有 Unit zookeeper.service entered failed state./zookeeper.service failed
欄位
問題定位
我們接著看上面的輸出,可以看到在設置了 Type=forking
後,服務在啟動或關閉時執行對應的腳本會開啟一個進程,並且兩個進程都成功執行了(返回狀態碼為 0 )。
Process: 61183 ExecStop=/opt/zookeeper/bin/zkServer.sh stop (code=exited, status=0/SUCCESS)
Process: 61116 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61132 (code=exited, status=143)
但是主進程退出時返回的狀態碼卻是 143,而不是狀態碼 0。
接著看下 zookeeper 進程還在不在:
[root@localhost ~]# jps -l
61287 sun.tools.jps.Jps
[root@localhost ~]# ps -ef | grep zookeeper
root 61300 61250 0 09:49 pts/0 00:00:00 grep --color=auto zookeeper
奇怪,明明 zookeeper 進程已經成功退出了,但是 systemd 卻說它退出失敗
此時我註意到儘管在停止服務時,狀態碼為 0,但也只是表明執行 /opt/zookeeper/bin/zkServer.sh stop
命令本身成功完成,這個狀態碼並不代表腳本內部的執行邏輯一定是成功的。
我們看下 zkServer.sh
腳本中關於 stop 的邏輯
stop)
echo -n "Stopping zookeeper ... "
if [ ! -f "$ZOOPIDFILE" ]
then
echo "no zookeeper to stop (could not find file $ZOOPIDFILE)"
else
$KILL $(cat "$ZOOPIDFILE")
rm "$ZOOPIDFILE"
sleep 1
echo STOPPED
fi
exit 0
;;
沒有發現有什麼不妥,接著我們註釋掉 ExecStop
欄位,採用 systemd 預設的方式來停止服務。
預設情況下,systemd 將向進程發送
SIGTERM
信號(相當於kill
命令發送的終止信號),等待一段時間後,如果服務進程未正常退出,則發送SIGKILL
信號(相當於kill -9
命令發送的強制終止信號)強制終止服務進程。
然後重新啟停一下 zookeeper ,看下狀態:
[root@localhost ~]# systemctl status zookeeper.service
● zookeeper.service - Zookeeper
Loaded: loaded (/usr/lib/systemd/system/zookeeper.service; enabled; vendor preset: disabled)
Active: failed (Result: exit-code) since 一 2024-04-01 10:03:04 CST; 1s ago
Process: 61453 ExecStart=/opt/zookeeper/bin/zkServer.sh start (code=exited, status=0/SUCCESS)
Main PID: 61469 (code=exited, status=143)
4月 01 10:02:55 localhost.localdomain systemd[1]: Started Zookeeper.
4月 01 10:02:55 localhost.localdomain zkServer.sh[61453]: /usr/sbin/java
4月 01 10:02:55 localhost.localdomain zkServer.sh[61453]: ZooKeeper JMX enabled by default
4月 01 10:02:55 localhost.localdomain zkServer.sh[61453]: Using config: /opt/zookeeper/bin/../conf/zoo.cfg
4月 01 10:02:56 localhost.localdomain zkServer.sh[61453]: Starting zookeeper ... STARTED
4月 01 10:03:04 localhost.localdomain systemd[1]: Stopping Zookeeper...
4月 01 10:03:04 localhost.localdomain systemd[1]: zookeeper.service: main process exited, code=exited, status=143/n/a
4月 01 10:03:04 localhost.localdomain systemd[1]: Stopped Zookeeper.
4月 01 10:03:04 localhost.localdomain systemd[1]: Unit zookeeper.service entered failed state.
4月 01 10:03:04 localhost.localdomain systemd[1]: zookeeper.service failed.
[root@localhost ~]# jps -l
61524 sun.tools.jps.Jps
[root@localhost ~]# ps -ef | grep zookeeper
root 61537 61250 0 10:04 pts/0 00:00:00 grep --color=auto zookeeper
還是一樣的問題,zookeeper 已經成功退出但是卻顯示 failed 狀態,狀態碼是 143。
從上面我們得知:無論是通過 zkserver.sh
還是 systemd 預設方式來關閉服務,本質上都是向 zookeeper 進程發送 SIGTERM
信號(數值為 15 ),雖然 zookeeper 進程成功退出,但是 systemd 將此解釋為異常退出,因為預期的退出狀態碼為 0。
而根據 POSIX 規範:【因接收到信號而終止的命令的退出狀態應報告為大於 128】,所以被信號中斷的進程退出時會返回 128 加上信號數值作為退出狀態碼。
也就是說,當 zookeeper 進程收到 SIGTERM
信號時,會返回 128 + 15 也就是 143 作為退出狀態碼,這也就是為什麼進程在成功退出後 systemctl 顯示為 failed 狀態。
解決問題
既然知道了進程在退出時的狀態碼是 143 但是 systemd 不會解釋為成功,因為預期的退出狀態碼為 0,那麼我們只需要讓 systemd 把狀態碼 143 也解釋為成功就行。
所以在 zookeeper 的 service 文件中添加下麵的配置:
[Service]
...
SuccessExitStatus=143
...
表示當服務進程以狀態碼 143 正常退出時,systemd 將其視為成功退出而不是異常退出。