基于Go語言開發(fā)篇一個命令行進程監(jiān)控工具
在生產和開發(fā)環(huán)境中,監(jiān)控關鍵進程的存活與資源使用是非常常見的需求:當進程 CPU/內存超限或意外退出時自動告警、記錄歷史、甚至重啟進程,能顯著提升系統可靠性。本篇給出一個可運行的 Go 實戰(zhàn)案例:一個輕量級的命令行進程監(jiān)控工具(procmon),支持按進程名或 PID 監(jiān)控、采樣統計、閾值告警(HTTP webhook)、并能執(zhí)行重啟命令。
下面從目標、設計、實現到運行示例一步步展開,并給出可以直接拿去編譯運行的完整代碼。
功能目標
- 監(jiān)控指定的進程(按名稱或 PID),周期性采樣 CPU% 與內存 RSS。
- 當某個進程 CPU% 或內存(MB)超出閾值時觸發(fā)告警(支持 HTTP webhook + 本地日志)。
- 支持在告警時運行自定義重啟命令(可用于 systemd restart、docker restart、或自定義腳本)。
- 支持本地日志、控制臺輸出、并優(yōu)雅退出(SIGINT/SIGTERM)。
- 支持批量監(jiān)控多個進程、簡單配置(命令行 flags / JSON)。
技術選型
- 語言:Go
- 進程信息:
github.com/shirou/gopsutil/v3/process(跨平臺,常用) - 告警:HTTP POST 到 webhook(簡單可擴展到郵件/釘釘/Slack)
- 并發(fā):每個監(jiān)控項使用獨立 goroutine,主循環(huán)統一調度與統計
項目結構(示意)
procmon/
├── main.go
├── go.mod
完整代碼(main.go)
// main.go
package main
import (
"bytes"
"context"
"encoding/json"
"flag"
"fmt"
"log"
"net/http"
"os"
"os/exec"
"os/signal"
"strconv"
"strings"
"sync"
"syscall"
"time"
"github.com/shirou/gopsutil/v3/process"
)
// MonitorConfig 表示對單個進程的監(jiān)控配置
type MonitorConfig struct {
Names []string `json:"names"` // 按進程名匹配
PIDs []int32 `json:"pids"` // 指定 pid
CPUThreshold float64 `json:"cpu_threshold"` // 百分比,如 80.0
MemThreshold float64 `json:"mem_threshold"` // MB,如 500.0
RestartCmd string `json:"restart_cmd"` // 告警時執(zhí)行的重啟命令(可空)
}
// AlertPayload 告警時發(fā)送的 JSON 結構
type AlertPayload struct {
Time time.Time `json:"time"`
Host string `json:"host"`
Process string `json:"process"`
PID int32 `json:"pid"`
CPU float64 `json:"cpu_percent"`
MemoryMB float64 `json:"memory_mb"`
Triggered string `json:"triggered"`
Msg string `json:"msg"`
}
func main() {
// CLI 參數
cfgFile := flag.String("config", "", "配置 JSON 文件(可選),與命令行參數組合使用")
names := flag.String("names", "", "要監(jiān)控的進程名,逗號分隔(例如: nginx,mysqld)")
pids := flag.String("pids", "", "要監(jiān)控的 pid,逗號分隔(例如: 123,456)")
interval := flag.Duration("interval", 5*time.Second, "采樣間隔")
cpuTh := flag.Float64("cpu", 80.0, "默認 CPU 百分比閾值(%)")
memTh := flag.Float64("mem", 500.0, "默認 內存閾值(MB)")
webhook := flag.String("webhook", "", "告警 webhook URL(POST 接收 JSON)")
restart := flag.String("restart", "", "全局重啟命令(可選,覆蓋 config 中 restart_cmd)")
flag.Parse()
// 解析配置
var monitors []MonitorConfig
if *cfgFile != "" {
f, err := os.ReadFile(*cfgFile)
if err != nil {
log.Fatalf("讀取配置文件失敗: %v", err)
}
if err := json.Unmarshal(f, &monitors); err != nil {
log.Fatalf("解析配置文件失敗: %v", err)
}
}
// 命令行 args 補充單一配置(如果用戶沒傳配置文件)
if len(monitors) == 0 && (*names != "" || *pids != "") {
m := MonitorConfig{
CPUThreshold: *cpuTh,
MemThreshold: *memTh,
}
if *names != "" {
for _, n := range strings.Split(*names, ",") {
n = strings.TrimSpace(n)
if n != "" {
m.Names = append(m.Names, n)
}
}
}
if *pids != "" {
for _, ps := range strings.Split(*pids, ",") {
if s := strings.TrimSpace(ps); s != "" {
id, err := strconv.Atoi(s)
if err == nil {
m.PIDs = append(m.PIDs, int32(id))
}
}
}
}
if *restart != "" {
m.RestartCmd = *restart
}
monitors = append(monitors, m)
}
if len(monitors) == 0 {
log.Fatalln("沒有任何監(jiān)控配置。請通過 -config 或 -names/-pids 提供配置。")
}
hostname, _ := os.Hostname()
ctx, cancel := context.WithCancel(context.Background())
wg := &sync.WaitGroup{}
// 信號優(yōu)雅退出
sigc := make(chan os.Signal, 1)
signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
go func() {
<-sigc
log.Println("收到退出信號,正在優(yōu)雅停止...")
cancel()
}()
// 啟動每個監(jiān)控項的 goroutine
for idx, mc := range monitors {
wg.Add(1)
go func(id int, cfg MonitorConfig) {
defer wg.Done()
monitorLoop(ctx, id, cfg, *interval, *webhook, hostname)
}(idx, mc)
}
// 等待退出
wg.Wait()
log.Println("procmon 已退出")
}
// monitorLoop 對單個 MonitorConfig 進行輪詢監(jiān)控
func monitorLoop(ctx context.Context, id int, cfg MonitorConfig, interval time.Duration, webhook, host string) {
logPrefix := fmt.Sprintf("[monitor-%d] ", id)
logger := log.New(os.Stdout, logPrefix, log.LstdFlags)
ticker := time.NewTicker(interval)
defer ticker.Stop()
// 用于去重告警(避免短時間內頻繁告警)
alerted := make(map[int32]time.Time)
alertCooldown := 30 * time.Second // 同一 pid 告警最小間隔
for {
select {
case <-ctx.Done():
logger.Println("停止監(jiān)控(context canceled)")
return
case <-ticker.C:
procs, err := process.Processes()
if err != nil {
logger.Printf("獲取進程列表失敗: %v\n", err)
continue
}
now := time.Now()
for _, p := range procs {
match := false
// 匹配 PID 列表
for _, pid := range cfg.PIDs {
if p.Pid == pid {
match = true
break
}
}
// 匹配名字列表(如果未通過 pid 匹配)
if !match && len(cfg.Names) > 0 {
name, err := p.Name()
if err == nil {
for _, nm := range cfg.Names {
if strings.EqualFold(name, nm) {
match = true
break
}
}
}
}
if !match {
continue
}
// 獲取 CPU & Mem
// Percent 需要傳入一個間隔來計算;這里使用 0 來獲取自上次調用以后的值(某些平臺)
// 更可靠的做法是調用 Percent(interval);為了簡單與跨平臺,這里使用 Percent(0)
cpuPercent, errCpu := p.CPUPercent()
memInfo, errMem := p.MemoryInfo()
if errCpu != nil || errMem != nil || memInfo == nil {
// 有時權限原因無法讀取某些信息
logger.Printf("讀取進程 %d 信息失敗: cpuErr=%v memErr=%v\n", p.Pid, errCpu, errMem)
continue
}
memMB := float64(memInfo.RSS) / 1024.0 / 1024.0
// 打印日志
name, _ := p.Name()
logger.Printf("進程 %s pid=%d cpu=%.2f%% mem=%.2fMB\n", name, p.Pid, cpuPercent, memMB)
// 判斷閾值
triggered := ""
if cfg.CPUThreshold > 0 && cpuPercent >= cfg.CPUThreshold {
triggered = "cpu"
}
if cfg.MemThreshold > 0 && memMB >= cfg.MemThreshold {
if triggered == "" {
triggered = "mem"
} else {
triggered = "cpu+mem"
}
}
if triggered != "" {
lastAlert, ok := alerted[p.Pid]
if ok && now.Sub(lastAlert) < alertCooldown {
// 跳過頻繁告警
logger.Printf("已在 cooldown 中,跳過 pid=%d 的告警\n", p.Pid)
continue
}
alerted[p.Pid] = now
payload := AlertPayload{
Time: now,
Host: host,
Process: name,
PID: p.Pid,
CPU: cpuPercent,
MemoryMB: memMB,
Triggered: triggered,
Msg: fmt.Sprintf("process %s (pid=%d) exceeded threshold (%s)", name, p.Pid, triggered),
}
// 本地日志告警
logger.Printf("ALERT: %s\n", payload.Msg)
// 發(fā)送 webhook(如果配置)
if webhook != "" {
go func(pl AlertPayload) {
if err := postAlert(webhook, pl); err != nil {
logger.Printf("發(fā)送 webhook 失敗: %v\n", err)
} else {
logger.Printf("告警已發(fā)送到 %s\n", webhook)
}
}(payload)
}
// 執(zhí)行重啟命令(config 中或全局傳入) —— 先嘗試 graceful terminate 再執(zhí)行重啟命令(如果提供)
if cfg.RestartCmd != "" {
go func(cmdStr string, targetPid int32) {
logger.Printf("嘗試殺掉 pid=%d 并執(zhí)行重啟命令: %s\n", targetPid, cmdStr)
// 發(fā)送 TERM
_ = p.SendSignal(syscall.SIGTERM)
// 等待短時間讓進程退出
time.Sleep(2 * time.Second)
// 強制 kill 如果還存在
exists, _ := process.PidExists(targetPid)
if exists {
_ = p.Kill()
}
// 執(zhí)行重啟命令(通過 shell)
cmd := exec.Command("/bin/sh", "-c", cmdStr)
out, err := cmd.CombinedOutput()
if err != nil {
logger.Printf("執(zhí)行重啟命令失敗: %v. output: %s\n", err, string(out))
} else {
logger.Printf("重啟命令已執(zhí)行, output: %s\n", string(out))
}
}(cfg.RestartCmd, p.Pid)
}
}
} // end for procs
} // end ticker select
} // end for
}
// postAlert 以 JSON POST 方式發(fā)送告警
func postAlert(webhook string, payload AlertPayload) error {
bs, _ := json.Marshal(payload)
req, err := http.NewRequest("POST", webhook, bytes.NewReader(bs))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{Timeout: 8 * time.Second}
resp, err := client.Do(req)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return fmt.Errorf("webhook 返回非 2xx: %s", resp.Status)
}
return nil
}
代碼說明 & 要點提醒
依賴:本示例使用 github.com/shirou/gopsutil/v3/process。在項目目錄運行:
go mod init procmon go get github.com/shirou/gopsutil/v3/process
采樣 CPU:process.CPUPercent() 的行為受平臺和調用頻率影響。更精確的 CPU 百分比通常需要兩次采樣間的時間差(gopsutil 提供相關接口),但本例為簡潔使用了庫的默認方法。若需精確長期統計,可以保存上次樣本并計算 delta。
權限問題:在某些系統上讀取其他用戶的進程信息需要更高權限(root)。如果監(jiān)控不到目標進程,請以合適權限運行。
重啟策略:示例中通過 RestartCmd 執(zhí)行自定義 shell 命令來重啟服務(例如 systemctl restart myservice 或 docker restart container)。這是最靈活的方式,但要確保命令安全(不要盲目執(zhí)行來自不可信配置的命令)。
告警去重:示例里使用 alertCooldown 防止短時間內重復告警。你可以把告警狀態(tài)持久化到 Redis/文件以跨重啟保留告警狀態(tài)。
跨平臺:gopsutil 支持多平臺,但信號、kill 等行為在 Windows 與 Unix 上不同。Windows 上需用不同方法停止進程。
使用示例
簡單按進程名監(jiān)控 nginx,CPU 超過 70% 或內存超過 300MB 時發(fā) webhook:
./procmon -names nginx -cpu 70 -mem 300 -webhook "https://example.com/webhook"
使用 JSON 配置(config.json)支持多項監(jiān)控(文件示例):
[
{
"names": ["nginx"],
"cpu_threshold": 70.0,
"mem_threshold": 300,
"restart_cmd": "systemctl restart nginx"
},
{
"names": ["mysqld"],
"cpu_threshold": 85.0,
"mem_threshold": 2048,
"restart_cmd": "systemctl restart mysql"
}
]
運行:
./procmon -names nginx -cpu 70 -mem 300 -webhook "https://example.com/webhook"
可行的擴展與改進(工程化建議)
- 持久化歷史:把采樣結果寫入 InfluxDB/Prometheus 或本地文件,方便后續(xù)分析與告警策略優(yōu)化。
- 更智能的告警:支持平均值/移動窗口、抑制波動(例如短時 spike 不告警)、按時間段不同閾值。
- 進程自恢復:把重啟策略從單條命令擴展為“逐步恢復”:先重啟、再報警、再回滾;并記錄重啟次數以避免重啟風暴。
- UI 或 API:提供 HTTP 管理接口查看當前監(jiān)控狀態(tài)、觸發(fā)測試告警或調整閾值。
- 容器/Pod 支持:在容器環(huán)境下識別容器內進程或直接對容器做重啟(Kubernetes 中可使用 K8s API 觸發(fā)重啟)。
- 權限和安全:限制能夠執(zhí)行的 restart_cmd、對 webhook 使用簽名/鑒權避免被濫用。
小結
本文實現了一個簡單但實用的 Go 進程監(jiān)控工具,涵蓋進程掃描、資源采樣、閾值檢測、告警與重啟動作。示例代碼足夠作為生產工具的原型,通過增加持久化、更多告警通道與更安全的重啟策略,可以逐步把它演化為完整的運維監(jiān)控組件。
到此這篇關于基于Go語言開發(fā)篇一個命令行進程監(jiān)控工具的文章就介紹到這了,更多相關Go進程監(jiān)控內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
win7下配置GO語言環(huán)境 + eclipse配置GO開發(fā)
這篇文章主要介紹了win7下配置GO語言環(huán)境 + eclipse配置GO開發(fā),需要的朋友可以參考下2014-10-10

