一、概述
1.1 背景介紹
systemctl start xxx敲了無數遍,但真要從零寫一個 Service 文件丟到生產環境跑,很多人就開始心虛了。網上抄一段配置,Type=simple還是forking搞不清楚,Restart=always往上一貼就覺得萬事大吉,結果進程掛了不重啟、OOM 了沒人管、日志把磁盤寫爆了才發現 journald 根本沒配輪轉。
Systemd 從 2010 年誕生到現在,已經不只是一個 init 系統了。它是 Linux 世界的 PID 1,是服務管理器、日志系統、定時任務調度器、設備管理器、網絡配置工具的集合體。2026 年的主流發行版(Ubuntu 24.04/RHEL 9/Debian 12)全部默認使用 systemd 256+,cgroup v2 也已經是標配。不管你是跑 Java 微服務、Go 二進制、Python 腳本還是 Nginx,最終都要落到一個.service文件上。
這篇文章的目標很明確:從 systemd 的架構講起,把 Unit 文件的每個 Section、每個關鍵指令都講透,最后給出一個經過生產驗證的完整 Service 配置模板。看完之后,你應該能獨立編寫一個帶資源限制、安全加固、健康檢查、日志管理的生產級 Service 文件。
1.2 技術特點
體系化:從架構到配置到實戰,一條線串起來,不是零散的參數羅列
生產導向:每個配置項都說明"為什么要這么配",而不只是"可以這么配"
安全優先:覆蓋 ProtectSystem、PrivateTmp、NoNewPrivileges 等安全加固指令
現代技術棧:基于 systemd 256+、cgroup v2、journald 的 2026 年最佳實踐
1.3 適用場景
場景一:需要將自研服務部署到 Linux 服務器,編寫規范的 Service 文件
場景二:現有 Service 配置過于簡陋,需要補充資源限制和安全加固
場景三:想用 systemd timer 替代 cron,實現更可靠的定時任務管理
場景四:需要理解 systemd 的依賴管理機制,解決服務啟動順序問題
1.4 環境要求
| 組件 | 版本要求 | 說明 |
|---|---|---|
| 操作系統 | Ubuntu 24.04 LTS / RHEL 9.x | 內核 6.8+,cgroup v2 默認啟用 |
| systemd | 256+ | 支持本文涉及的所有特性 |
| journald | 隨 systemd 版本 | 日志管理組件 |
| cgroup | v2 | 資源限制依賴 cgroup v2 |
二、Systemd 架構和核心概念
2.1 Systemd 不只是 init
很多人對 systemd 的認知停留在"啟動服務的工具",這個理解太窄了。Systemd 是一整套系統管理框架,PID 1 進程(/usr/lib/systemd/systemd)是它的核心,但遠不是全部。
+------------------+
| PID 1 (systemd) |
+--------+---------+
|
+----------+-----------+-----------+----------+
| | | | |
+---------+ +--------+ +--------+ +--------+ +--------+
| journald| | logind | | udevd | | networkd| | resolved|
+---------+ +--------+ +--------+ +--------+ +--------+
日志管理 會話管理 設備管理 網絡管理 DNS解析
+----------+----------+----------+
| systemd-tmpfiles | systemd-sysctl | systemd-modules-load |
+----------+----------+----------+
臨時文件管理 內核參數 內核模塊加載
PID 1 負責的核心工作:解析 Unit 文件、管理依賴關系、啟動/停止/監控服務進程、處理 cgroup 資源分配。其他組件各司其職,通過 D-Bus 與 PID 1 通信。
2.2 三個核心概念:Unit、Target、Slice
2.2.1 Unit(單元)
Unit 是 systemd 管理的基本對象。一個 Unit 對應一個配置文件,文件后綴決定了 Unit 的類型:
| 類型 | 后綴 | 用途 | 典型示例 |
|---|---|---|---|
| Service | .service | 管理守護進程 | nginx.service |
| Socket | .socket | 套接字激活 | sshd.socket |
| Timer | .timer | 定時任務 | logrotate.timer |
| Mount | .mount | 掛載點 | home.mount |
| Target | .target | 邏輯分組 | multi-user.target |
| Slice | .slice | 資源分組 | user.slice |
| Path | .path | 文件監控 | cups.path |
| Device | .device | 設備管理 | 由 udev 自動生成 |
日常打交道最多的就是前三個:Service、Socket、Timer。
2.2.2 Target(目標)
Target 是一組 Unit 的邏輯集合,類似于 SysVinit 時代的 runlevel,但更靈活。Target 本身不做任何事情,它只是把一堆 Unit 聚合在一起,表示"系統到達了某個狀態"。
# 查看當前活躍的 target systemctl list-units --type=target --state=active # 常見 target 對應關系 # multi-user.target ≈ runlevel 3(多用戶命令行) # graphical.target ≈ runlevel 5(圖形界面) # rescue.target ≈ runlevel 1(單用戶模式)
服務啟動順序的核心就是圍繞 target 來編排的。比如大多數網絡服務都聲明After=network-online.target,意思是"等網絡就緒了再啟動我"。
2.2.3 Slice(切片)
Slice 是 cgroup 的 systemd 抽象層,用于對一組服務進行資源分配。默認的 slice 層級結構:
-.slice (根 slice) ├── system.slice # 系統服務(nginx、mysql 等) ├── user.slice # 用戶會話 │ ├── user-1000.slice │ └── user-1001.slice └── machine.slice # 虛擬機和容器
可以自定義 slice 來實現資源隔離。比如把所有業務服務放到同一個 slice 里,統一限制 CPU 和內存上限,防止業務進程把系統服務擠死。
2.3 Unit 文件的存放位置
Unit 文件有三個存放位置,優先級從高到低:
| 路徑 | 優先級 | 用途 |
|---|---|---|
| /etc/systemd/system/ | 最高 | 管理員自定義配置 |
| /run/systemd/system/ | 中 | 運行時動態生成 |
| /usr/lib/systemd/system/ | 最低 | 軟件包安裝的默認配置 |
實際操作原則:永遠不要直接修改/usr/lib/systemd/system/下的文件,包管理器更新時會覆蓋掉。自定義配置放/etc/systemd/system/,覆蓋默認配置用systemctl edit創建 drop-in 文件。
# 用 drop-in 方式覆蓋某個參數,不動原始文件 # 會創建 /etc/systemd/system/nginx.service.d/override.conf sudo systemctl edit nginx.service # 查看某個 unit 的最終生效配置(合并所有 drop-in) systemctl cat nginx.service
2.4 Unit 文件結構:三個 Section
一個標準的.service文件由三個 Section 組成:
[Unit] # 描述信息和依賴關系 Description=My Application Service Documentation=https://docs.example.com After=network-online.target postgresql.service Wants=network-online.target Requires=postgresql.service [Service] # 服務運行參數 Type=notify ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yaml Restart=on-failure RestartSec=5s User=myapp Group=myapp [Install] # 安裝信息(enable/disable 時使用) WantedBy=multi-user.target
[Unit] Section:定義 Unit 的元信息和依賴關系。Description是給人看的,After/Before控制啟動順序,Requires/Wants控制依賴強度。
[Service] Section:這是.service文件獨有的,也是最核心的部分。定義了進程怎么啟動、怎么停止、怎么重啟、以什么身份運行、資源限制多少。
[Install] Section:定義systemctl enable時的行為。WantedBy=multi-user.target的意思是"當系統進入多用戶模式時,把我也帶上"。執行enable時,systemd 會在multi-user.target.wants/目錄下創建一個指向這個 service 文件的符號鏈接。
# enable 的本質就是創建符號鏈接 sudo systemctlenablemyapp.service # 等價于: # ln -s /etc/systemd/system/myapp.service # /etc/systemd/system/multi-user.target.wants/myapp.service # 查看一個 unit 是否 enabled systemctl is-enabled myapp.service
2.5 Service Type 詳解
Type=是 [Service] Section 里最關鍵的一個參數,它決定了 systemd 如何判斷"服務已經啟動成功"。選錯了 Type,輕則systemctl start超時報錯,重則服務狀態判斷錯亂、重啟策略失效。
2.5.1 五種主要 Type
| Type | 啟動判定 | 適用場景 | 典型程序 |
|---|---|---|---|
| simple | ExecStart 進程啟動即視為就緒 | 前臺運行的程序 | Go 二進制、Node.js |
| exec | ExecStart 進程成功執行(exec()返回)即就緒 | 同 simple,但更嚴格 | 同 simple |
| forking | 主進程 fork 子進程后退出,子進程接管 | 傳統 daemon | Nginx、MySQL |
| oneshot | ExecStart 進程退出后才視為就緒 | 一次性任務 | 初始化腳本、數據遷移 |
| notify | 進程主動發送 sd_notify 通知就緒 | 支持 sd_notify 的程序 | systemd 自身組件、部分 Go 服務 |
simple vs exec:simple是默認值,進程被 fork 出來就算啟動成功,哪怕二進制文件路徑寫錯了,systemctl start也可能返回成功(因為 fork 本身成功了)。exec更嚴格,它會等到exec()系統調用真正執行成功才算就緒。systemd 256+ 推薦用exec替代simple。
forking 的坑:傳統 daemon 程序(如 Nginx 默認配置)啟動時會 fork 子進程,父進程退出。用Type=forking時,systemd 需要知道哪個是"主進程",通常通過PIDFile=指定 PID 文件路徑來追蹤。如果 PID 文件寫入不及時或路徑配錯,systemd 就會丟失對進程的追蹤。
# forking 類型的典型配置(Nginx 為例) [Service] Type=forking PIDFile=/run/nginx.pid ExecStartPre=/usr/sbin/nginx -t ExecStart=/usr/sbin/nginx ExecReload=/bin/kill -s HUP $MAINPID
notify 的優勢:這是生產環境最推薦的 Type。進程在完成所有初始化工作(加載配置、連接數據庫、預熱緩存)之后,主動調用sd_notify(0, "READY=1")告訴 systemd "我準備好了"。這樣 systemd 對服務狀態的判斷是最準確的。
// Go 程序中使用 sd_notify 的示例
import"github.com/coreos/go-systemd/v22/daemon"
funcmain(){
// 初始化工作...
loadConfig()
connectDB()
warmupCache()
// 通知 systemd 服務就緒
daemon.SdNotify(false, daemon.SdNotifyReady)
// 開始服務主循環
serve()
}
2.5.2 選型決策樹
你的程序啟動后會 fork 并退出父進程嗎?
├── 是 → Type=forking + PIDFile=
└── 否 → 程序支持 sd_notify 嗎?
├── 是 → Type=notify(最佳選擇)
└── 否 → 程序是一次性任務嗎?
├── 是 → Type=oneshot(可選 RemainAfterExit=yes)
└── 否 → Type=exec(推薦)或 Type=simple
2.6 啟動依賴管理
服務之間的依賴關系是 systemd 的核心能力之一。這里有兩個維度需要區分清楚:啟動順序和依賴強度。
2.6.1 啟動順序:After / Before
After和Before只控制順序,不控制依賴。聲明After=postgresql.service意味著"如果 postgresql 也要啟動,那先啟動它,再啟動我"。但如果 postgresql 根本沒有被激活,這條聲明不會自動把它拉起來。
[Unit] # 正確:先等網絡和數據庫就緒,再啟動本服務 After=network-online.target postgresql.service redis.service
2.6.2 依賴強度:Requires / Wants / BindsTo
| 指令 | 強度 | 行為 |
|---|---|---|
| Wants= | 弱依賴 | 嘗試啟動依賴,依賴失敗不影響本服務 |
| Requires= | 強依賴 | 依賴啟動失敗,本服務也不啟動 |
| BindsTo= | 綁定 | 依賴停止/重啟,本服務也跟著停止/重啟 |
| Requisite= | 前置斷言 | 依賴必須已經在運行,否則立即失敗 |
生產建議:大多數場景用Wants=+After=的組合就夠了。Requires=看起來更"安全",但它有一個副作用——如果被依賴的服務后來掛了,本服務也會被連帶停止。這在微服務架構下往往不是你想要的行為。
[Unit] Description=My Web Application After=network-online.target postgresql.service # 用 Wants 而不是 Requires,數據庫臨時不可用時服務自己處理重連 Wants=network-online.target postgresql.service
2.6.3 網絡依賴的正確寫法
這是一個高頻踩坑點。很多人寫After=network.target,結果服務啟動時網絡還沒通。原因是network.target只表示"網絡管理器已啟動",不代表網絡已經可用。正確的寫法:
[Unit] After=network-online.target Wants=network-online.target
同時需要確保systemd-networkd-wait-online.service或NetworkManager-wait-online.service是啟用的,否則network-online.target會被立即視為已達成。
2.7 進程管理:重啟策略與健康檢查
2.7.1 Restart 策略
Restart=控制進程退出后是否自動重啟:
| 值 | 行為 |
|---|---|
| no | 不重啟(默認值) |
| on-success | 僅在正常退出(exit code 0)時重啟 |
| on-failure | 非正常退出時重啟(非0退出碼、被信號殺死、超時、看門狗超時) |
| on-abnormal | 被信號殺死、超時、看門狗超時時重啟(不含非0退出碼) |
| on-abort | 僅被未捕獲信號殺死時重啟 |
| always | 無論什么原因退出都重啟 |
生產建議:大多數守護進程用Restart=on-failure。不要無腦用always——如果程序是正常退出(比如收到 SIGTERM 后優雅關閉),你通常不希望它被自動拉起來。always適合那些"只要沒在跑就是不正常"的核心服務。
2.7.2 重啟頻率控制
光有Restart=on-failure還不夠,還需要控制重啟的節奏,防止進程反復崩潰導致 CPU 空轉:
[Service] Restart=on-failure RestartSec=5s # 每次重啟前等待 5 秒 RestartSteps=5 # 重啟間隔逐步遞增的步數(systemd 256+) RestartMaxDelaySec=60s # 遞增的最大間隔(systemd 256+) StartLimitIntervalSec=300 # 在 300 秒的窗口內 StartLimitBurst=5 # 最多重啟 5 次,超過則放棄
systemd 256+ 新增了RestartSteps和RestartMaxDelaySec,可以實現指數退避式重啟。第一次重啟等 5 秒,第二次等 16 秒,逐步遞增到 60 秒封頂。這比固定間隔更合理——如果是瞬時故障,快速重啟能盡快恢復;如果是持續性故障,拉長間隔避免雪崩。
2.7.3 超時控制
[Service] TimeoutStartSec=30s # 啟動超時,超過 30 秒未就緒則判定失敗 TimeoutStopSec=30s # 停止超時,超過 30 秒未退出則發 SIGKILL TimeoutAbortSec=60s # 收到 abort 信號后的超時(用于生成 core dump)
TimeoutStopSec特別重要。systemctl stop時,systemd 先發 SIGTERM,等TimeoutStopSec秒后如果進程還沒退出,就發 SIGKILL 強殺。Java 應用通常需要把這個值調大一些(比如 60s),給 JVM 足夠的時間做優雅關閉。
2.7.4 Watchdog 看門狗
Watchdog 是 systemd 提供的進程健康檢查機制。服務進程需要定期向 systemd 發送心跳,如果超時沒收到,systemd 就認為進程卡死了,按照 Restart 策略處理。
[Service] Type=notify WatchdogSec=30s # 每 30 秒需要收到一次心跳 WatchdogSignal=SIGABRT # 超時后發送的信號(默認 SIGABRT,可生成 core dump)
程序端需要配合發送心跳:
// Go 程序中發送 watchdog 心跳
import"github.com/coreos/go-systemd/v22/daemon"
funcwatchdogLoop(){
interval, _ := daemon.SdWatchdogEnabled(false)
ifinterval ==0{
return// watchdog 未啟用
}
ticker := time.NewTicker(interval /2)// 以一半間隔發送,留足余量
forrangeticker.C {
daemon.SdNotify(false, daemon.SdNotifyWatchdog)
}
}
Watchdog 解決的是"進程還活著但已經卡死"的問題——進程沒崩潰、PID 還在、端口還監聽著,但內部死鎖了或者陷入無限循環,外部健康檢查可能還沒來得及發現。Watchdog 從進程內部檢測這種狀態,比外部探測更及時。
三、資源限制、安全加固與日志管理
3.1 資源限制(cgroup v2)
不做資源限制的服務就是在裸奔。一個內存泄漏的進程可以把整臺機器的內存吃光觸發 OOM Killer,一個死循環可以把所有 CPU 核心打滿。systemd 通過 cgroup v2 提供了細粒度的資源限制能力,直接在 Service 文件里配置就行,不需要手動操作 cgroup 文件系統。
3.1.1 CPU 限制
[Service] # CPU 配額:200% 表示最多使用 2 個核心 CPUQuota=200% # CPU 權重:默認 100,范圍 1-10000 # 只在 CPU 競爭時生效,空閑時不限制 CPUWeight=50 # 綁定到特定 CPU 核心(可選,通常不需要) AllowedCPUs=0-3
CPUQuota是硬限制,不管 CPU 是否空閑都不會超過這個值。CPUWeight是軟限制,只在多個服務競爭 CPU 時按權重分配,CPU 空閑時不起作用。生產環境建議兩個都配:CPUWeight保證公平調度,CPUQuota兜底防止單個服務吃滿所有核心。
3.1.2 內存限制
[Service] # 內存硬上限:超過直接 OOM Kill MemoryMax=2G # 內存軟上限:超過后內核會優先回收該 cgroup 的內存 MemoryHigh=1536M # 最低內存保障:內存緊張時至少保留這么多 MemoryMin=256M # 禁用 swap(推薦) MemorySwapMax=0
MemoryMax和MemoryHigh的區別很關鍵。MemoryHigh是軟限制,超過后內核會加大內存回收力度(進程會變慢但不會被殺);MemoryMax是硬限制,超過直接觸發 OOM Kill。生產環境建議MemoryHigh設為正常峰值的 120%,MemoryMax設為 150%,給一個緩沖區間。
3.1.3 IO 限制
[Service] # IO 權重:默認 100,范圍 1-10000 IOWeight=50 # 針對特定設備的帶寬限制 IOReadBandwidthMax=/dev/sda 50M IOWriteBandwidthMax=/dev/sda 20M # IOPS 限制 IOReadIOPSMax=/dev/sda 1000 IOWriteIOPSMax=/dev/sda 500
IO 限制在數據庫服務和日志密集型服務上特別有用。一個瘋狂寫日志的服務可以把磁盤 IO 打滿,影響同機器上的其他服務。
3.1.4 其他資源限制
[Service] # 最大文件描述符數 LimitNOFILE=65536 # 最大進程/線程數 LimitNPROC=4096 # core dump 大小(0 表示禁用) LimitCORE=infinity # 最大打開文件鎖數 LimitLOCKS=infinity # 任務數上限(cgroup 級別,比 NPROC 更準確) TasksMax=4096
LimitNOFILE是高并發服務的必配項。Linux 默認的 1024 對于任何生產服務都太小了,Nginx、Redis、數據庫類服務通常需要 65536 甚至更高。
3.2 安全加固
Systemd 提供了一整套沙箱機制,可以在不修改應用代碼的情況下大幅收窄進程的權限范圍。這些配置的成本幾乎為零,但安全收益很高。
3.2.1 文件系統保護
[Service] # 將 /usr 和 /boot 掛載為只讀 ProtectSystem=strict # 為進程創建獨立的 /tmp,與其他進程隔離 PrivateTmp=yes # 將 /home、/root、/run/user 設為不可訪問 ProtectHome=yes # 只允許讀寫指定目錄 ReadWritePaths=/var/lib/myapp /var/log/myapp ReadOnlyPaths=/etc/myapp # 禁止訪問指定目錄 InaccessiblePaths=/var/lib/mysql
ProtectSystem=strict是最嚴格的模式,整個文件系統變成只讀,只有通過ReadWritePaths顯式聲明的目錄才可寫。這意味著即使應用被攻破,攻擊者也無法篡改系統文件。
3.2.2 權限收窄
[Service] # 禁止獲取新的特權(防止 setuid 提權) NoNewPrivileges=yes # 以非 root 用戶運行 User=myapp Group=myapp DynamicUser=yes # 自動創建臨時用戶(systemd 256+ 推薦) # 移除所有 Linux capabilities CapabilityBoundingSet= # 如果需要綁定低端口,只給 NET_BIND_SERVICE # CapabilityBoundingSet=CAP_NET_BIND_SERVICE # 限制系統調用(白名單模式) SystemCallFilter=@system-service SystemCallErrorNumber=EPERM
NoNewPrivileges=yes是零成本的安全加固,沒有任何副作用,所有服務都應該加上。DynamicUser=yes是 systemd 的一個巧妙設計——它會在服務啟動時動態分配一個 UID/GID,服務停止后自動回收,不需要手動創建系統用戶。
3.2.3 網絡和內核隔離
[Service] # 創建獨立的網絡命名空間(完全斷網) PrivateNetwork=yes # 如果需要網絡但想限制,用 RestrictAddressFamilies RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX # 禁止加載內核模塊 ProtectKernelModules=yes # 禁止修改內核參數 ProtectKernelTunables=yes # 禁止訪問內核日志 ProtectKernelLogs=yes # 禁止修改系統時鐘 ProtectClock=yes # 禁止創建設備節點 PrivateDevices=yes
3.2.4 一鍵查看安全評分
systemd 提供了一個內置的安全審計工具,可以對 Service 文件進行安全評分:
# 查看某個服務的安全評分 systemd-analyze security myapp.service # 輸出示例(分數越低越安全,10 分最不安全) # → Overall exposure level for myapp.service: 2.1 OK
這個工具會逐項檢查所有安全相關的配置,給出評分和改進建議。新寫的 Service 文件跑一遍這個命令,把能加的安全配置都加上,目標是控制在 3 分以內。
3.3 日志管理
3.3.1 journald 基礎
Systemd 服務的標準輸出和標準錯誤默認會被 journald 捕獲。不需要在應用里配置日志文件路徑,直接往 stdout/stderr 寫就行,journald 會自動加上時間戳、服務名、PID 等元數據。
# 查看某個服務的日志 journalctl -u myapp.service # 實時跟蹤日志(類似 tail -f) journalctl -u myapp.service -f # 查看最近 1 小時的日志 journalctl -u myapp.service --since"1 hour ago" # 按優先級過濾(0=emerg 到 7=debug) journalctl -u myapp.service -p err # 輸出 JSON 格式(方便程序處理) journalctl -u myapp.service -o json-pretty
3.3.2 Service 文件中的日志配置
[Service] # 日志輸出目標 StandardOutput=journal StandardError=journal # 設置日志標識符(默認是服務名) SyslogIdentifier=myapp # 設置日志級別過濾 LogLevelMax=info # 只記錄 info 及以上級別 # 限制日志速率(防止日志風暴) LogRateLimitIntervalSec=30s LogRateLimitBurst=10000 # 30 秒內最多 10000 條
LogRateLimitIntervalSec和LogRateLimitBurst是生產環境的保命配置。見過太多次應用出 bug 后瘋狂打日志,每秒幾萬條,把磁盤 IO 打滿、把 journald 撐爆的情況。
3.3.3 journald 全局配置和日志輪轉
編輯/etc/systemd/journald.conf:
[Journal] # 持久化存儲(默認是 volatile,重啟丟失) Storage=persistent # 磁盤占用上限 SystemMaxUse=2G # 日志總大小上限 SystemMaxFileSize=128M # 單個日志文件上限 SystemKeepFree=4G # 至少保留 4G 磁盤空間 # 運行時(內存中)日志限制 RuntimeMaxUse=256M # 日志保留時間 MaxRetentionSec=30day # 壓縮 Compress=yes
journald 的日志輪轉是自動的,不需要像 logrotate 那樣配置 cron。當日志總量超過SystemMaxUse或單文件超過SystemMaxFileSize時,journald 會自動刪除最舊的日志。
# 手動清理日志 sudo journalctl --vacuum-size=1G # 只保留 1G sudo journalctl --vacuum-time=7d # 只保留 7 天 # 查看日志占用空間 journalctl --disk-usage
四、Timer 定時任務
4.1 為什么用 Timer 替代 Cron
Cron 用了幾十年,能跑但問題不少:沒有日志集成(輸出靠郵件或重定向)、沒有依賴管理、沒有資源限制、錯過的任務不會補執行、多實例并發沒有保護。Systemd Timer 解決了這些問題,而且和 Service 文件共享同一套管理體系。
| 特性 | Cron | Systemd Timer |
|---|---|---|
| 日志 | 無(靠重定向) | journald 自動記錄 |
| 依賴管理 | 無 | 支持 After/Requires |
| 資源限制 | 無 | 完整 cgroup 支持 |
| 錯過補執行 | 不支持 | Persistent=yes |
| 并發保護 | 無 | 天然單實例 |
| 隨機延遲 | 無 | RandomizedDelaySec |
| 精度 | 分鐘級 | 秒級甚至微秒級 |
4.2 Timer 文件結構
一個 Timer 由兩個文件組成:.timer文件定義觸發時間,.service文件定義要執行的任務。兩個文件同名(后綴不同),systemd 自動關聯。
# /etc/systemd/system/db-backup.timer [Unit] Description=Database Backup Timer [Timer] # 每天凌晨 2 點執行 OnCalendar=*-*-* 0200 # 如果錯過了(比如機器當時關機),開機后補執行 Persistent=yes # 隨機延遲 0-15 分鐘,避免多臺機器同時執行 RandomizedDelaySec=15min # 精度(默認 1min,設小一點更準時) AccuracySec=1s [Install] WantedBy=timers.target
# /etc/systemd/system/db-backup.service [Unit] Description=Database Backup Job [Service] Type=oneshot ExecStart=/usr/local/bin/backup-db.sh User=backup Group=backup # 資源限制和安全加固同樣適用 MemoryMax=512M CPUQuota=50% ProtectSystem=strict PrivateTmp=yes NoNewPrivileges=yes ReadWritePaths=/var/backups
4.3 OnCalendar 時間表達式
OnCalendar的語法比 cron 更直觀:
# 格式:星期 年-月-日 時:分:秒 OnCalendar=Mon..Fri *-*-* 0900 # 工作日每天 9 點 OnCalendar=*-*-* *:00/15:00 # 每 15 分鐘 OnCalendar=*-*-01 0000 # 每月 1 號零點 OnCalendar=weekly # 每周一零點 OnCalendar=hourly # 每小時整點 # 驗證時間表達式 systemd-analyze calendar"*-*-* 0200" # 輸出下次觸發時間,確認表達式寫對了
也可以用相對時間觸發:
[Timer] # 系統啟動 5 分鐘后執行 OnBootSec=5min # 上次執行完成后 30 分鐘再執行 OnUnitActiveSec=30min
4.4 Timer 管理命令
# 啟用并啟動 timer sudo systemctlenable--now db-backup.timer # 查看所有 timer 的狀態和下次觸發時間 systemctl list-timers --all # 手動觸發一次(不影響定時計劃) sudo systemctl start db-backup.service # 查看 timer 的執行歷史 journalctl -u db-backup.service --since"7 days ago"
五、Socket 激活
5.1 什么是 Socket 激活
Socket 激活是 systemd 的一個精巧設計:由 systemd 預先監聽端口,當第一個連接請求到達時,再啟動對應的服務進程,并把 socket 文件描述符傳遞給它。服務進程啟動后接管 socket,后續請求直接由服務處理。
這個機制帶來三個好處:
啟動加速:系統啟動時不需要等所有服務都起來,端口先占著,請求來了再啟動
按需啟動:不常用的服務平時不占資源,有請求才拉起來
零停機重啟:重啟服務時,systemd 繼續持有 socket,新連接排隊等待,服務重啟完成后繼續處理,客戶端感知不到中斷
5.2 Socket 激活配置示例
# /etc/systemd/system/myapp.socket [Unit] Description=My Application Socket [Socket] # 監聽地址和端口 ListenStream=0.0.0.0:8080 # 也可以監聽 Unix Socket # ListenStream=/run/myapp/myapp.sock # 連接隊列長度 Backlog=4096 # Socket 文件權限(Unix Socket 時有效) # SocketMode=0660 # SocketUser=myapp # SocketGroup=myapp # 接受連接后傳遞給哪個服務(默認同名 .service) # Service=myapp.service [Install] WantedBy=sockets.target
對應的 Service 文件不需要特殊修改,只要程序能從文件描述符 3 接收 socket 即可。Go 標準庫的net包、systemd 的sd_listen_fds()API 都支持這種模式。
# 啟用 socket 激活(注意:啟動的是 .socket 不是 .service) sudo systemctlenable--now myapp.socket # 此時 myapp.service 還沒啟動,但端口已經在監聽 ss -tlnp | grep 8080 # 輸出:systemd 在監聽 # 發送第一個請求,觸發 myapp.service 啟動 curl http://localhost:8080/health
六、生產級 Service 文件完整示例
6.1 Go Web 服務(推薦模板)
這是一個經過生產驗證的完整 Service 文件,覆蓋了前面講到的所有關鍵配置。可以作為模板,根據實際需求增刪參數。
# /etc/systemd/system/myapp.service # 生產級 Go Web 服務配置模板 # ============================================================ # [Unit] 元信息和依賴 # ============================================================ [Unit] Description=My Application API Server Documentation=https://docs.example.com/myapp After=network-online.target postgresql.service redis.service Wants=network-online.target # 用 Wants 而非 Requires,依賴服務臨時不可用時由應用自行處理重連 Wants=postgresql.service redis.service # 條件檢查:配置文件必須存在才啟動 ConditionPathExists=/etc/myapp/config.yaml # ============================================================ # [Service] 核心運行參數 # ============================================================ [Service] Type=notify NotifyAccess=main # --- 啟動命令 --- ExecStartPre=/usr/local/bin/myapp validate --config /etc/myapp/config.yaml ExecStart=/usr/local/bin/myapp serve --config /etc/myapp/config.yaml ExecReload=/bin/kill -s HUP $MAINPID # --- 運行身份 --- User=myapp Group=myapp # --- 工作目錄和環境 --- WorkingDirectory=/var/lib/myapp EnvironmentFile=-/etc/myapp/env # 減號表示文件不存在時不報錯 Environment=GOMAXPROCS=4 Environment=GIN_MODE=release # --- 重啟策略 --- Restart=on-failure RestartSec=5s RestartSteps=5 RestartMaxDelaySec=60s StartLimitIntervalSec=300 StartLimitBurst=5 # --- 超時和看門狗 --- TimeoutStartSec=30s TimeoutStopSec=60s WatchdogSec=30s # --- 資源限制 --- CPUQuota=200% CPUWeight=100 MemoryMax=2G MemoryHigh=1536M MemorySwapMax=0 TasksMax=4096 LimitNOFILE=65536 LimitNPROC=4096 # --- 安全加固 --- NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes PrivateDevices=yes ProtectKernelModules=yes ProtectKernelTunables=yes ProtectKernelLogs=yes ProtectClock=yes RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX SystemCallFilter=@system-service SystemCallErrorNumber=EPERM ReadWritePaths=/var/lib/myapp /var/log/myapp ReadOnlyPaths=/etc/myapp # --- 日志 --- StandardOutput=journal StandardError=journal SyslogIdentifier=myapp LogRateLimitIntervalSec=30s LogRateLimitBurst=10000 # ============================================================ # [Install] 安裝信息 # ============================================================ [Install] WantedBy=multi-user.target
6.2 部署流程
寫好 Service 文件后,部署流程如下:
# 1. 創建運行用戶 sudo useradd --system --no-create-home --shell /usr/sbin/nologin myapp # 2. 創建必要目錄 sudo mkdir -p /var/lib/myapp /var/log/myapp /etc/myapp sudo chown myapp:myapp /var/lib/myapp /var/log/myapp # 3. 部署二進制和配置文件 sudo cp myapp /usr/local/bin/myapp sudo chmod 755 /usr/local/bin/myapp sudo cp config.yaml /etc/myapp/config.yaml # 4. 部署 Service 文件 sudo cp myapp.service /etc/systemd/system/myapp.service # 5. 重新加載 systemd 配置 sudo systemctl daemon-reload # 6. 啟用并啟動服務 sudo systemctlenable--now myapp.service # 7. 驗證服務狀態 systemctl status myapp.service journalctl -u myapp.service -n 50 --no-pager
6.3 Java 服務示例
Java 服務和 Go 服務的主要區別在于:JVM 啟動慢需要更長的超時時間、內存模型不同需要調整限制策略、不支持 sd_notify 通常用Type=exec。
# /etc/systemd/system/myapp-java.service [Unit] Description=My Java Application After=network-online.target Wants=network-online.target [Service] Type=exec ExecStart=/usr/bin/java -Xms512m -Xmx1536m -XX:+UseZGC -jar /opt/myapp/myapp.jar --spring.config.location=/etc/myapp/ User=myapp Group=myapp WorkingDirectory=/opt/myapp Restart=on-failure RestartSec=10s StartLimitIntervalSec=300 StartLimitBurst=3 # JVM 啟動慢,給足時間 TimeoutStartSec=120s # 優雅關閉需要時間(Spring Boot shutdown hook) TimeoutStopSec=60s # 內存限制要考慮 JVM 堆外內存,設為 Xmx 的 1.5 倍左右 MemoryMax=3G MemoryHigh=2560M MemorySwapMax=0 CPUQuota=400% LimitNOFILE=65536 TasksMax=4096 # 安全加固 NoNewPrivileges=yes ProtectSystem=strict ProtectHome=yes PrivateTmp=yes PrivateDevices=yes ProtectKernelModules=yes ProtectKernelTunables=yes ReadWritePaths=/var/lib/myapp /var/log/myapp /tmp StandardOutput=journal StandardError=journal SyslogIdentifier=myapp-java [Install] WantedBy=multi-user.target
Java 服務的MemoryMax不能簡單等于-Xmx。JVM 除了堆內存還有 Metaspace、線程棧、直接內存、JIT 編譯緩存等堆外開銷,實際內存占用通常是-Xmx的 1.3 到 1.8 倍。MemoryMax設太小會導致 JVM 被 OOM Kill,設太大又失去了限制的意義。建議用-Xmx的 1.5 倍作為起點,再根據實際監控數據調整。
七、systemctl 常用命令速查
7.1 服務生命周期管理
# 啟動/停止/重啟/重載 sudo systemctl start myapp.service sudo systemctl stop myapp.service sudo systemctl restart myapp.service sudo systemctl reload myapp.service # 發送 SIGHUP,不中斷服務 sudo systemctl reload-or-restart myapp.service # 支持 reload 就 reload,否則 restart # 開機自啟 sudo systemctlenablemyapp.service # 設置開機自啟 sudo systemctldisablemyapp.service # 取消開機自啟 sudo systemctlenable--now myapp.service # 設置自啟并立即啟動 # 徹底屏蔽服務(防止被其他服務拉起) sudo systemctl mask myapp.service sudo systemctl unmask myapp.service
7.2 狀態查看和診斷
# 查看服務狀態(最常用) systemctl status myapp.service # 查看服務是否在運行 systemctl is-active myapp.service # 查看服務是否啟動失敗 systemctl is-failed myapp.service # 查看所有失敗的服務 systemctl --failed # 查看服務的完整配置(合并 drop-in) systemctl cat myapp.service # 查看服務的所有屬性 systemctl show myapp.service # 查看某個具體屬性 systemctl show myapp.service -p MainPID -p MemoryCurrent -p CPUUsageNSec
7.3 配置管理和分析
# 修改后重新加載配置(不重啟服務) sudo systemctl daemon-reload # 用 drop-in 方式修改配置(推薦) sudo systemctl edit myapp.service # 編輯完整的 service 文件(不推薦,會覆蓋原文件) sudo systemctl edit --full myapp.service # 分析啟動耗時 systemd-analyze # 總啟動時間 systemd-analyze blame # 各服務啟動耗時排序 systemd-analyze critical-chain myapp.service # 關鍵路徑分析 # 安全審計 systemd-analyze security myapp.service # 驗證 unit 文件語法 systemd-analyze verify /etc/systemd/system/myapp.service # 查看服務的依賴樹 systemctl list-dependencies myapp.service systemctl list-dependencies --reverse myapp.service # 反向:誰依賴我
7.4 日志查看速查
# 查看某個服務的日志 journalctl -u myapp.service # 實時跟蹤(tail -f 模式) journalctl -u myapp.service -f # 最近 N 條 journalctl -u myapp.service -n 100 # 時間范圍 journalctl -u myapp.service --since"2026-02-06 0000"--until"2026-02-06 1200" journalctl -u myapp.service --since"30 min ago" # 按級別過濾 journalctl -u myapp.service -p err # error 及以上 journalctl -u myapp.service -p warning # warning 及以上 # 輸出格式 journalctl -u myapp.service -o json-pretty # JSON 格式 journalctl -u myapp.service -o short-iso # ISO 時間格式 # 查看上一次啟動的日志(排查重啟前的崩潰原因) journalctl -u myapp.service -b -1 # 查看內核 OOM Kill 記錄 journalctl -k | grep -i"oom|killed"
7.5 資源監控
# 查看服務的實時資源占用 systemctl status myapp.service # 輸出中包含 Memory: 和 CPU: 行 # 查看 cgroup 級別的詳細資源數據 systemctl show myapp.service -p MemoryCurrent -p MemoryPeak -p CPUUsageNSec # 查看所有服務的資源占用排序 systemd-cgtop # 查看某個服務的 cgroup 路徑 systemctl show myapp.service -p ControlGroup # 直接查看 cgroup 文件(更詳細) cat /sys/fs/cgroup/system.slice/myapp.service/memory.current cat /sys/fs/cgroup/system.slice/myapp.service/cpu.stat
八、總結
8.1 技術要點回顧
Unit/Target/Slice是 systemd 的三個核心抽象:Unit 是管理單元,Target 是邏輯分組,Slice 是資源分組
Service Type 選型:優先用notify(程序支持的話),其次exec,傳統 daemon 用forking,一次性任務用oneshot
依賴管理:After/Before控制順序,Wants/Requires控制強度,生產環境優先用Wants+After組合
重啟策略:Restart=on-failure覆蓋大多數場景,配合RestartSteps實現指數退避,用StartLimitBurst防止無限重啟
Watchdog:解決"進程活著但卡死"的問題,需要程序端配合發送心跳
資源限制:MemoryMax硬限制兜底,MemoryHigh軟限制緩沖,CPUQuota防止單服務吃滿 CPU
安全加固:NoNewPrivileges=yes零成本必加,ProtectSystem=strict+ReadWritePaths最小化文件系統權限
日志管理:用 journald 統一管理,配置LogRateLimitBurst防日志風暴,配置SystemMaxUse防磁盤寫爆
Timer 替代 Cron:日志集成、依賴管理、資源限制、錯過補執行,全面優于 cron
Socket 激活:按需啟動、零停機重啟,適合低頻訪問或需要平滑重啟的服務
8.2 Service 文件編寫 Checklist
寫完一個 Service 文件后,對照這個清單檢查一遍:
[ ] Type 選對了嗎?程序的啟動行為和 Type 匹配嗎? [ ] 依賴關系配了嗎?After 和 Wants 寫對了嗎? [ ] 用非 root 用戶運行了嗎?User/Group 配了嗎? [ ] Restart 策略配了嗎?RestartSec 和 StartLimitBurst 配了嗎? [ ] 超時時間合理嗎?TimeoutStartSec/TimeoutStopSec 夠用嗎? [ ] 內存限制配了嗎?MemoryMax 和 MemoryHigh 設了合理的值嗎? [ ] CPU 限制配了嗎?CPUQuota 設了上限嗎? [ ] LimitNOFILE 夠大嗎?高并發服務至少 65536 [ ] NoNewPrivileges=yes 加了嗎? [ ] ProtectSystem=strict 加了嗎?ReadWritePaths 列全了嗎? [ ] 日志速率限制配了嗎?LogRateLimitBurst 設了嗎? [ ] systemd-analyze security 跑過了嗎?評分在 3 分以內嗎?
8.3 進階學習方向
Service 文件寫好只是起點,systemd 的能力遠不止于此。以下幾個方向在生產環境中有明確的落地價值,值得持續跟進。
1. systemd-nspawn 輕量級容器
systemd-nspawn 可以理解為"systemd 原生的容器運行時"。它不需要 Docker 或 containerd,直接用一個目錄樹作為根文件系統就能啟動一個隔離的 Linux 環境。典型場景是構建環境隔離和遺留應用封裝——比如在 RHEL 9 的宿主機上跑一個 CentOS 7 的 nspawn 容器來編譯老項目,或者把一個不方便容器化的傳統 Java 應用丟進 nspawn 里做資源隔離。machinectl命令管理 nspawn 實例,systemd-nspawn@.service模板讓它和普通 Service 一樣被 systemctl 管理。相比 Docker,nspawn 的優勢在于和 systemd 生態的深度集成——日志走 journald、資源限制走 cgroup slice、網絡走 systemd-networkd,運維工具鏈完全統一。
2. Portable Services
Portable Services 是 systemd 240+ 引入的特性,目標是在"傳統 Service 文件"和"完整容器化"之間找一個平衡點。它把應用和依賴打包成一個 OS 鏡像(通常是 raw 或 squashfs 格式),通過portablectl attach掛載到宿主機上,自動生成對應的 Service/Timer 文件。應用運行時共享宿主機內核但使用自己的用戶空間庫,既解決了依賴沖突問題,又不需要完整的容器編排棧。對于邊緣計算節點、嵌入式網關這類資源受限且不適合跑 K8s 的場景,Portable Services 是一個務實的選擇。
3. systemd-sysext 和 Composefs
systemd 254+ 的 sysext(System Extensions)機制允許在不可變根文件系統上疊加擴展層,配合 Composefs 實現內容尋址的只讀文件系統疊加。這個方向和 Flatcar Container Linux、Fedora CoreOS 等不可變基礎設施操作系統密切相關。如果團隊在推進不可變基礎設施或 GitOps 驅動的節點管理,sysext 是繞不開的技術點。
8.4 參考資料
systemd 官方文檔- 最權威的參數說明
systemd.service(5)- Service 文件完整參數列表
systemd.exec(5)- 執行環境配置(安全加固參數在這里)
systemd.resource-control(5)- 資源限制參數
Arch Wiki - systemd- 社區維護的實用指南
systemd-nspawn(1)- nspawn 容器完整參數說明
Portable Services 文檔- Portable Services 設計文檔和使用指南
systemd-sysext(8)- 系統擴展層管理工具
六、總結
6.1 技術要點回顧
回頭看整篇文章,有幾個核心認知需要釘死:
Unit 文件三段式結構([Unit]/[Service]/[Install])是基礎中的基礎。[Unit]管依賴和描述,[Service]管運行行為,[Install]管啟用方式。寫 Service 文件的第一步不是去查參數,而是先把這三個 Section 的職責分清楚。搞混了職責,參數放錯 Section,systemd 不會報錯但也不會生效,排查起來浪費時間。
Service Type 選擇直接決定 systemd 對進程生命周期的判定邏輯。大多數現代應用(Go 二進制、Node.js、Python 腳本)直接用simple就夠了,進程在前臺跑,PID 1 直接追蹤。傳統 daemon 類程序(比如老版本的 MySQL、Nginx)會 fork 子進程后父進程退出,這種必須用forking并配合PIDFile。一次性初始化腳本(建目錄、改權限、跑遷移)用oneshot,配合RemainAfterExit=yes讓 systemd 認為服務處于 active 狀態。Type 選錯了,輕則systemctl status顯示狀態不對,重則 systemd 誤判進程已死反復重啟。
資源限制和安全加固不是錦上添花,是生產級配置的標配。CPUQuota防止單個服務吃滿所有核心拖垮整機,MemoryMax在 OOM 之前主動干掉失控進程,ProtectSystem=strict把根文件系統鎖成只讀,PrivateTmp=yes隔離臨時目錄防止跨服務信息泄露。這些配置的成本幾乎為零,但缺了它們,一個失控的服務就能把整臺機器拉下水。
Timer 單元完全可以替代 cron,且在所有維度上更優。cron 的問題在于:沒有日志集成(輸出丟了就是丟了)、沒有依賴管理(不能聲明"等網絡就緒再跑")、沒有資源限制(定時腳本跑飛了沒人管)、錯過的任務不會補執行。systemd Timer 把這些問題全部解決了,Persistent=yes一個參數就能處理機器關機期間錯過的任務,OnCalendar的語法比 crontab 的五星表達式可讀性強得多。2026 年了,新項目沒有理由再用 cron。
6.2 進階學習方向
systemd 的能力邊界遠不止 Service 文件。以下三個方向在生產環境中有明確的落地價值,值得持續跟進。
1. systemd-nspawn 輕量級容器
systemd 自帶的容器運行時,不依賴 Docker 或 containerd,直接用一個目錄樹作為根文件系統就能拉起隔離環境。典型場景是構建環境隔離和遺留應用封裝。和 Docker 相比,nspawn 的核心優勢在于和 systemd 生態的深度集成——日志走 journald、資源限制走 cgroup slice、網絡走 systemd-networkd,運維工具鏈完全統一,不需要額外引入一套容器編排體系。對于不需要鏡像分發能力、只需要本地隔離的場景,nspawn 比 Docker 更輕量也更省心。
2. Portable Services(可移植服務單元)
systemd 240+ 引入的特性,定位在"裸 Service 文件"和"完整容器化"之間。把應用和依賴打包成 OS 鏡像,通過portablectl attach掛載到宿主機,自動生成對應的 Service/Timer 文件。應用共享宿主機內核但使用自己的用戶空間庫,既解決依賴沖突又不需要完整的容器編排棧。對于邊緣計算節點、嵌入式網關這類資源受限且不適合跑 K8s 的場景,Portable Services 是一個務實的選擇。
3. systemd-homed 用戶目錄管理
systemd 245+ 引入的用戶目錄管理方案,把用戶的家目錄封裝成一個可加密、可遷移的獨立單元(LUKS 加密鏡像或 fscrypt 目錄)。用戶登錄時自動掛載解密,登出時自動卸載鎖定。對于多用戶共享的開發服務器或需要滿足數據加密合規要求的場景,homed 提供了一種比傳統/home+ LDAP 更現代的方案。homectl命令管理用戶,用戶記錄以 JSON 格式存儲,支持跨機器遷移。
6.3 參考資料
systemd 官方文檔- 所有 man page 的在線版本,參數說明以這里為準
Lennart Poettering - The systemd for Administrators Blog Series- systemd 作者本人寫的系列博客,從設計哲學到具體用法都有覆蓋,雖然部分內容寫于早期版本,但核心思路至今適用
Arch Wiki - systemd- 社區維護的實用指南,示例豐富,更新及時,遇到具體問題時往往比官方文檔更容易找到答案
-
Linux
+關注
關注
88文章
11760瀏覽量
219016 -
JAVA
+關注
關注
20文章
3001瀏覽量
116422 -
文件
+關注
關注
1文章
594瀏覽量
26054
原文標題:Systemd 入門到精通:編寫一個生產級的 Service 配置文件
文章出處:【微信號:magedu-Linux,微信公眾號:馬哥Linux運維】歡迎添加關注!文章轉載請注明出處。
發布評論請先 登錄
編寫一個生產級的Service配置文件
評論