探索Go守護進程的實現(xiàn)方法
在后端開發(fā)的世界里,守護進程(daemon)這個概念與Unix系統(tǒng)一樣古老。守護進程是在后臺運行的長期服務程序,不與任何終端關(guān)聯(lián)。盡管現(xiàn)代進程管理工具如systemd[1]和supervisor[2]等讓應用轉(zhuǎn)化為守護進程變得十分簡單,我們甚至可以使用以下命令來在后臺運行程序:
nohup ./your_go_program &
但在某些情況下,程序的原生轉(zhuǎn)化為守護進程的能力仍然是有必要的。比如分布式文件系統(tǒng)juicefs cli的mount子命令,它就支持以-d選項啟動,并以守護進程方式運行:
$juicefs mount -h
NAME:
juicefs mount - Mount a volume
USAGE:
juicefs mount [command options] META-URL MOUNTPOINT
... ...
OPTIONS:
-d, --background run in background (default: false)
... ...
... ...
這種自我守護化的能力會讓很多Go程序受益,在這一篇文章中,我們就來探索一下Go應用轉(zhuǎn)化為守護進程的實現(xiàn)方法。
1. 標準的守護進程轉(zhuǎn)化方法
[W.Richard Stevens]( "W.Richard Stevens")的經(jīng)典著作《UNIX環(huán)境高級編程[3]》中對將程序轉(zhuǎn)化為一個守護進程的 (daemonize) 步驟進行了詳細的說明,主要步驟如下:
- 創(chuàng)建子進程并終止父進程
通過fork()系統(tǒng)調(diào)用創(chuàng)建子進程,父進程立即終止,保證子進程不是控制終端的會話組首領(lǐng)。
- 創(chuàng)建新的會話
子進程調(diào)用setsid()來創(chuàng)建一個新會話,成為會話組首領(lǐng),從而擺脫控制終端和進程組。
- 更改工作目錄
使用chdir("/") 將當前工作目錄更改為根目錄,避免守護進程持有任何工作目錄的引用,防止對文件系統(tǒng)卸載的阻止。
- 重設文件權(quán)限掩碼
通過umask(0) 清除文件權(quán)限掩碼,使得守護進程可以自由設置文件權(quán)限。
- 關(guān)閉文件描述符
關(guān)閉繼承自父進程的已經(jīng)open的文件描述符(通常是標準輸入、標準輸出和標準錯誤)。
- 重定向標準輸入/輸出/錯誤
重新打開標準輸入、輸出和錯誤,重定向到/dev/null,以避免守護進程無意輸出內(nèi)容到不應有的地方。
注:fork()系統(tǒng)調(diào)用是一個較為難理解的調(diào)用,它用于在UNIX/Linux系統(tǒng)中創(chuàng)建一個新的進程。新創(chuàng)建的進程被稱為子進程,它是由調(diào)用fork()的進程(即父進程)復制出來的。子進程與父進程擁有相同的代碼段、數(shù)據(jù)段、堆和棧,但它們是各自獨立的進程,有不同的進程ID (PID)。在父進程中,fork()返回子進程的PID(正整數(shù)),在子進程中,fork()返回0,如果fork()調(diào)用失敗(例如系統(tǒng)資源不足),則返回-1,并設置errno以指示錯誤原因。
下面是一個符合UNIX標準的守護進程轉(zhuǎn)化函數(shù)的C語言實現(xiàn),參考了《UNIX環(huán)境高級編程》中的經(jīng)典步驟:
// daemonize/c/daemon.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <syslog.h>
#include <signal.h>
void daemonize()
{
pid_t pid;
// 1. Fork off the parent process
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
// If we got a good PID, then we can exit the parent process.
if (pid > 0) {
exit(EXIT_SUCCESS);
}
// 2. Create a new session to become session leader to lose controlling TTY
if (setsid() < 0) {
exit(EXIT_FAILURE);
}
// 3. Fork again to ensure the process won't allocate controlling TTY in future
pid = fork();
if (pid < 0) {
exit(EXIT_FAILURE);
}
if (pid > 0) {
exit(EXIT_SUCCESS);
}
// 4. Change the current working directory to root.
if (chdir("/") < 0) {
exit(EXIT_FAILURE);
}
// 5. Set the file mode creation mask to 0.
umask(0);
// 6. Close all open file descriptors.
for (int x = sysconf(_SC_OPEN_MAX); x>=0; x--) {
close(x);
}
// 7. Reopen stdin, stdout, stderr to /dev/null
open("/dev/null", O_RDWR); // stdin
dup(0); // stdout
dup(0); // stderr
// Optional: Log the daemon starting
openlog("daemonized_process", LOG_PID, LOG_DAEMON);
syslog(LOG_NOTICE, "Daemon started.");
closelog();
}
int main() {
daemonize();
// Daemon process main loop
while (1) {
// Perform some background task...
sleep(30); // Sleep for 30 seconds.
}
return EXIT_SUCCESS;
}
注:這里省略了書中設置系統(tǒng)信號handler的步驟。
這里的daemonize函數(shù)完成了標準的守護化轉(zhuǎn)化過程,并確保了程序在后臺無依賴地穩(wěn)定運行。我們編譯運行該程序后,程序進入后臺運行,通過ps命令可以查看到類似下面內(nèi)容:
$ ./c-daemon-app
$ ps -ef|grep c-daemon-app
root 28517 1 0 14:11 ? 00:00:00 ./c-daemon-app
我們看到c-daemon-app的父進程是ppid為1的進程,即linux的init進程。我們看到上面c代碼中轉(zhuǎn)化為守護進程的函數(shù)daemonize進行了兩次fork,至于為何要做兩次fork,在我的《理解Zombie和Daemon Process[4]》一文中有說明,這里就不贅述了。
那么Go是否可以參考上述步驟實現(xiàn)Go程序的守護進程轉(zhuǎn)化呢?我們接著往下看。
2. Go語言實現(xiàn)守護進程的挑戰(zhàn)
關(guān)于Go如何實現(xiàn)守護進程的轉(zhuǎn)換,在Go尚未發(fā)布1.0之前的2009年就有issue提到,在runtime: support for daemonize[5]中,Go社區(qū)與Go語言的早起元老們討論了在Go中實現(xiàn)原生守護進程的復雜性,主要挑戰(zhàn)源于Go的運行時及其線程管理方式。當一個進程執(zhí)行fork操作時,只有主線程被復制到子進程中,如果fork前Go程序有多個線程(及多個goroutine)在執(zhí)行(可能是由于go runtime調(diào)度goroutine和gc產(chǎn)生的線程),那么fork后,這些非執(zhí)行fork線程的線程(以及goroutine)將不會被復制到新的子進程中,這可能會導致后續(xù)子進程中線程運行的不確定性(基于一些fork前線程留下的數(shù)據(jù)狀態(tài))。
理想情況下是Go runtime提供類似的daemonize函數(shù),然后在多線程啟動之前實現(xiàn)守護進程的轉(zhuǎn)化,不過Go團隊至今也沒有提供該機制,而是建議大家使用如systemd的第三方工具來實現(xiàn)Go程序的守護進程轉(zhuǎn)化。
既然Go官方不提供方案,Go社區(qū)就會另辟蹊徑,接下來,我們看看目前Go社區(qū)的守護進程解決方案。
3. Go社區(qū)的守護進程解決方案
盡管面臨挑戰(zhàn),Go社區(qū)還是開發(fā)了一些庫來支持Go守護進程的實現(xiàn),其中一個star比較多的解決方案是github.com/sevlyar/go-daemon。
go-daemon庫的作者巧妙地解決了Go語言中無法直接使用fork系統(tǒng)調(diào)用的問題。go-daemon采用了一個簡單而有效的技巧來模擬fork的行為:該庫定義了一個特殊的環(huán)境變量作為標記。程序運行時,首先檢查這個環(huán)境變量是否存在。如果環(huán)境變量不存在,執(zhí)行父進程相關(guān)操作,然后使用os.StartProcess(本質(zhì)是fork-and-exec)啟動帶有特定環(huán)境變量標記的程序副本。如果環(huán)境變量存在,執(zhí)行子進程相關(guān)操作,繼續(xù)執(zhí)行主程序邏輯,下面是該庫作者提供的原理圖:
圖片
這種方法有效地模擬了fork的行為,同時避免了Go運行時中與線程和goroutine相關(guān)的問題。下面是使用go-daemon包實現(xiàn)Go守護進程的示例:
// daemonize/go-daemon/main.go
package main
import (
"log"
"time"
"github.com/sevlyar/go-daemon"
)
func main() {
cntxt := &daemon.Context{
PidFileName: "example.pid",
PidFilePerm: 0644,
LogFileName: "example.log",
LogFilePerm: 0640,
WorkDir: "./",
Umask: 027,
}
d, err := cntxt.Reborn()
if err != nil {
log.Fatal("無法運行:", err)
}
if d != nil {
return
}
defer cntxt.Release()
log.Print("守護進程已啟動")
// 守護進程邏輯
for {
// ... 執(zhí)行任務 ...
time.Sleep(time.Second * 30)
}
}
運行該程序后,通過ps可以查看到對應的守護進程:
$make
go build -o go-daemon-app
$./go-daemon-app
$ps -ef|grep go-daemon-app
501 4025 1 0 9:20下午 ?? 0:00.01 ./go-daemon-app
此外,該程序會在當前目錄下生成example.pid(用于實現(xiàn)file lock),用于防止意外重復執(zhí)行同一個go-daemon-app:
$./go-daemon-app
2024/09/26 21:21:28 無法運行:daemon: Resource temporarily unavailable
雖然原生守護進程化提供了精細的控制且無需安裝和配置外部依賴,但進程管理工具提供了額外的功能,如開機自啟[6]、異常退出后的自動重啟和日志記錄等,并且Go團隊推薦使用進程管理工具來實現(xiàn)Go守護進程。進程管理工具的缺點在于需要額外的配置(比如systemd)或安裝設置(比如supervisor)。
4. 小結(jié)
在Go中實現(xiàn)守護進程化,雖然因為語言運行時的特性而具有挑戰(zhàn)性,但通過社區(qū)開發(fā)的庫和謹慎的實現(xiàn)是可以實現(xiàn)的。隨著Go語言的不斷發(fā)展,我們可能會看到更多對進程管理功能的原生支持。同時,開發(fā)者可以根據(jù)具體需求,在原生守護進程化、進程管理工具或混合方法之間做出選擇。
本文涉及的源碼可以在這里[7]下載。
參考資料
[1] systemd: https://tonybai.com/2016/12/27/when-docker-meets-systemd
[2] supervisor: http://supervisord.org
[3] UNIX環(huán)境高級編程: https://book.douban.com/subject/25900403/
[4] 理解Zombie和Daemon Process: https://tonybai.com/2005/09/21/understand-zombie-and-daemon-process/
[5] runtime: support for daemonize: https://github.com/golang/go/issues/227
[6] 開機自啟: https://tonybai.com/2022/09/12/how-to-install-a-go-app-as-a-system-service-like-gitlab-runner
[7] 這里: https://github.com/bigwhite/experiments/tree/master/daemonize
[8] Gopher部落知識星球: https://public.zsxq.com/groups/51284458844544
[9] 鏈接地址: https://m.do.co/c/bff6eed92687