伺服器管理必學 100 招|Part 3:系統程序與 Systemd 治理
前言
在 Linux 伺服器中,程序的生命週期管理決定了系統的可用性與韌性。許多初學者往往用 nohup、& 或 screen 在背景偷跑程式,一旦伺服器重開或程式崩潰就無聲蒸發。本篇精選 10 個核心程序調度與 Systemd 治理技巧,教你如何編寫標準守護進程(Daemon)、設定自動重啟、精準隔離資源消耗,以及馴服殘酷的 Linux OOM Killer。
技巧 021:告別 nohup!用 Systemd 撰寫正規的 Production Service
- 痛點場景:
用nohup python app.py &跑背景程式,遇到伺服器重開機、記憶體溢位或未捕捉例外時,程式直接終止且無人知曉。 - 解決方案:
建立標準 Unit 檔案/etc/systemd/system/myapp.service:
[Unit]
Description=My Web Application
After=network.target
[Service]
Type=simple
User=deployer
Group=deployer
WorkingDirectory=/var/www/myapp
ExecStart=/var/www/myapp/venv/bin/python main.py
Restart=always
RestartSec=5s
Environment="PORT=8000"
EnvironmentFile=/var/www/myapp/.env
[Install]
WantedBy=multi-user.target
# 啟用並啟動服務:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
- 專家提醒:
ExecStart內的執行路徑必須是絕對路徑;使用專用非特權帳號(如deployer)執行,切勿預設以root身分跑應用。
技巧 022:防止重啟風暴!配置合理的 RestartSec 與重試門檻
- 痛點場景:
程式若是因為「資料庫連線失敗」或「設定檔語法錯誤」而啟動失敗,Restart=always會在 1 秒內狂閃重開數百次,瞬間把 CPU 打滿並洗爆系統 Log。 - 解決方案:
在 Unit 設定中加入冷卻緩衝與重試上限控制:
[Unit]
Description=Worker Service
# 10 秒內若重試超過 5 次,Systemd 將停止重試,避免拖垮系統
StartLimitIntervalSec=10
StartLimitBurst=5
[Service]
ExecStart=/usr/local/bin/worker
Restart=on-failure
# 每次崩潰後強制等待 5 秒再重啟
RestartSec=5s
- 專家提醒:
使用Restart=on-failure會比Restart=always更合理,因為它允許程式正常退出(Exit code 0)時不被強制拉起。
技巧 023:揮別 crontab!改用 Systemd Timer 實現高可靠定時排程
- 痛點場景:
傳統cron缺乏執行狀態監控、無法直觀調閱失敗日誌,遇到長時間執行的任務更難以追蹤卡在哪個步驟。 - 解決方案:
拆分為「Service」與「Timer」兩組檔案。
1. 定義要跑的任務/etc/systemd/system/daily-backup.service:
[Unit]
Description=Run Daily Database Backup
[Service]
Type=oneshot
ExecStart=/usr/local/bin/backup-script.sh
- 2. 定義排程計時器
/etc/systemd/system/daily-backup.timer:
[Unit]
Description=Trigger Backup Service Daily
[Timer]
# 每天凌晨 03:30 執行
OnCalendar=*-*-* 03:30:00
# 若關機錯過時間,下次開機補跑
Persistent=true
[Install]
WantedBy=timers.target
啟用並檢視排程狀態:
sudo systemctl daemon-reload
sudo systemctl enable --now daily-backup.timer
systemctl list-timers
- 專家提醒:
systemctl list-timers能一目了然下次觸發時間與上次執行結果,排查日誌只需直接打journalctl -u daily-backup.service。
技巧 024:單一服務吃爆主機?利用 Cgroups 限制 CPU 與記憶體
- 痛點場景:
某個資料處理背景工作或記憶體洩漏(Memory Leak)的程序,無節制地把全主機的 RAM 吃到 100%,導致連 SSH 都卡死登不進去。 - 解決方案:
直接在 Systemd Service 的[Service]區塊設定 Cgroups 資源上限:
[Service]
# 限制最高只能使用 2 顆 CPU 核心的運算量
CPUQuota=200%
# 限制實體記憶體最多使用 2GB
MemoryMax=2G
# 超過上限時直接拋棄 Swap,觸發該進程專屬的 OOM 處置
MemorySwapMax=0
更新後立即重載:sudo systemctl daemon-reload && sudo systemctl restart myapp
- 專家提醒:
利用systemd-cgtop指令,可以像查看htop一樣即時監看各 Systemd 服務單元的 CPU 與記憶體實際消耗率。
技巧 025:保住關鍵服務!調整 OOM Score 避免核心進程被誤殺
- 痛點場景:
當記憶體耗盡時,Linux 核心的 OOM Killer 會根據一套評分標準(OOM Score)隨機殺死大進程,常導致 SSHd 或最重要的 MariaDB / Nginx 被迫犧牲。 - 解決方案:
設定關鍵服務的保護分數,降低被殺機率(範圍 -1000 到 1000,負值越高越不容易被殺,-1000 代表完全免疫):
[Service]
# 讓核心在缺記憶體時優先放過這個重要服務
OOMScoreAdjust=-500
- 若要對非 Systemd 程序即時調整:
# 查閱進程目前的評分
cat /proc/<PID>/oom_score
# 手動調降優先權(需 root)
echo -500 | sudo tee /proc/<PID>/oom_score_adj
- 專家提醒:
不要將所有程式都設為-1000;只對 SSHd、資料庫或核心反向代理做降分保護,次要的 Worker 則應保持預設或調高分數讓它優先犧牲。
技巧 026:追殺打不死的殭屍程序(Zombie Processes)
- 痛點場景:
在top或htop中看到Zombie數量居高不下,程序狀態顯示為Z或<defunct>,無法使用kill -9強制刪除。 - 解決方案:
殭屍程序本身已經死亡,殺不掉是因為父程序(Parent Process)未執行wait()回收子程序的退出狀態。解決方法是找到父程序並將其重啟或終止:
# 1. 找出所有狀態為 Z 的殭屍進程及其父進程 PID (PPID)
ps -A -o stat,ppid,pid,cmd | grep -e '^[Zz]'
# 2. 檢視該父程序的詳細身分
ps -fp <PPID>
# 3. 終止或優雅重啟該父程序,讓 init/Systemd (PID 1) 認領並收割殭屍
kill -15 <PPID>
- 專家提醒:
殭屍程序雖然不消耗 CPU 與記憶體,但會佔用系統有限的 PID 名額,若 PID 號碼耗盡將導致整台伺服器無法建立任何新行程。
技巧 027:平滑重載(Zero-Downtime Reload)與環境變數注入
- 痛點場景:
每次修改設定檔就下systemctl restart nginx,會粗暴中斷正在傳輸中的使用者連線與長請求。 - 解決方案:
正確利用ExecReload實現零停機更新。以 Nginx 為例,它支援透過HUP訊號重載:
[Service]
# 啟動新 Worker 接收新流量,舊 Worker 處理完既有連線後自行退出
ExecReload=/usr/sbin/nginx -s reload
- 日常操作養成使用平滑重載習慣:
# 先檢查語法,確認無誤再平滑 reload
sudo nginx -t && sudo systemctl reload nginx
- 專家提醒:
如果是自己寫的 Node.js 或 Python 程式,需在程式碼中監聽process.on('SIGHUP')或SIGTERM實作優雅退出(Graceful Shutdown),才能支援真正的熱重載。
技巧 028:多實例模版服務!一個設定檔開 10 個 Worker(@ 語法)
- 痛點場景:
需要跑 4 個不同 Port 的 API 節點(如 8001、8002、8003、8004)做本機負載平衡,若複製 4 份 Service 檔案日後極難維護。 - 解決方案:
使用 Systemd 的 Template 機制,檔名末尾加上@,例如/etc/systemd/system/api-worker@.service:
[Unit]
Description=API Worker on port %I
After=network.target
[Service]
User=deployer
WorkingDirectory=/var/www/myapp
# %I 會動態替換為 @ 後面的參數
ExecStart=/var/www/myapp/venv/bin/uvicorn main:app --port %I
Restart=always
[Install]
WantedBy=multi-user.target
- 一鍵平行啟動不同 Port 的實例:
sudo systemctl enable --now api-worker@8001
sudo systemctl enable --now api-worker@8002
sudo systemctl enable --now api-worker@8003
- 專家提醒:
%I代表傳入的參數,也可以傳入使用者名稱、租戶 ID 或專案資料夾名稱,極適合多 Tenant 架構。
技巧 029:監控終極儀表!比 top 好用十倍的 btop 與 htop 必殺技
- 痛點場景:
預設的top介面黑白單調、缺乏硬碟 I/O 與網路狀態即時圖表,除錯時難以直觀定位系統瓶頸。 - 解決方案:
安裝現代化監控神器btop(或htop):
sudo apt install btop -y
# 執行 btop
btop
- 高頻快捷鍵操作指南:
- 按
P:依 CPU 消耗排序。 - 按
M:依記憶體消耗排序。 - 按
T:切換為樹狀圖檢視(Tree View),輕鬆找出哪個子程序是由哪個 Master 父行程長出來的。 - 按
K:直接在介面選取程序發送 Terminate / Kill 訊號,不用退回終端機手打 PID。
- 按
- 專家提醒:
如果伺服器沒有安裝第三方工具的權限,傳統top進入後直接按Shift + M依記憶體排序、按Shift + P依 CPU 排序,是最基礎的生存指令。
技巧 030:利用 /proc 檔案系統透視執行中程序的真實面貌
- 痛點場景:
遇到可疑或未知來源的程序在背景狂吃 CPU,但ps指令只顯示模糊的指令名稱,不知道它的實際執行路徑、環境變數與工作目錄在哪裡。 - 解決方案:
Linux 萬物皆檔案,進程所有細節全部攤在/proc/<PID>/:
# 1. 查出該程序實際對應的執行檔真實路徑
ls -l /proc/<PID>/exe
# 2. 查出該程序啟動時的 Working Directory
ls -l /proc/<PID>/cwd
# 3. 查出該程序啟動時載入的所有環境變數(以 Null 字符分隔)
cat /proc/<PID>/environ | tr '\0' '\n'
# 4. 查出該程序目前開啟了哪些檔案與網路連線
ls -l /proc/<PID>/fd/
- 專家提醒:
即便攻擊者把硬碟上的原始惡意腳本刪除了,只要進程還在跑,你甚至能直接用cp /proc/<PID>/exe /tmp/recovered_malware把該二進制檔完整 dump 出來進行逆向取證!