Go语言实战案例 — 工具开发篇:编写一个进程监控工具

在生产和开发环境中,监控关键进程的存活与资源使用是非常常见的需求:当进程 CPU/内存超限或意外退出时自动告警、记录历史、甚至重启进程,能显著提升系统可靠性。本篇给出一个可运行的 Go 实战案例 :一个轻量级的命令行进程监控工具(procmon),支持按进程名或 PID 监控、采样统计、阈值告警(HTTP webhook)、并能执行重启命令。

下面从目标、设计、实现到运行示例一步步展开,并给出可以直接拿去编译运行的完整代码。


功能目标

  • 监控指定的进程(按名称或 PID),周期性采样 CPU% 与内存 RSS。
  • 当某个进程 CPU% 或内存(MB)超出阈值时触发告警(支持 HTTP webhook + 本地日志)。
  • 支持在告警时运行自定义重启命令(可用于 systemd restart、docker restart、或自定义脚本)。
  • 支持本地日志、控制台输出、并优雅退出(SIGINT/SIGTERM)。
  • 支持批量监控多个进程、简单配置(命令行 flags / JSON)。

技术选型

  • 语言:Go
  • 进程信息:github.com/shirou/gopsutil/v3/process(跨平台,常用)
  • 告警:HTTP POST 到 webhook(简单可扩展到邮件/钉钉/Slack)
  • 并发:每个监控项使用独立 goroutine,主循环统一调度与统计

项目结构(示意)

go 复制代码
procmon/
├── main.go
├── go.mod

完整代码(main.go)

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 表示对单个进程的监控配置
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"`   // 告警时执行的重启命令(可空)
}

// AlertPayload 告警时发送的 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", "", "要监控的进程名,逗号分隔(例如: nginx,mysqld)")
	pids := flag.String("pids", "", "要监控的 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("没有任何监控配置。请通过 -config 或 -names/-pids 提供配置。")
	}

	hostname, _ := os.Hostname()
	ctx, cancel := context.WithCancel(context.Background())
	wg := &sync.WaitGroup{}

	// 信号优雅退出
	sigc := make(chan os.Signal, 1)
	signal.Notify(sigc, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		<-sigc
		log.Println("收到退出信号,正在优雅停止...")
		cancel()
	}()

	// 启动每个监控项的 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 进行轮询监控
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("停止监控(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)

					// 发送 webhook(如果配置)
					if webhook != "" {
						go func(pl AlertPayload) {
							if err := postAlert(webhook, pl); err != nil {
								logger.Printf("发送 webhook 失败: %v\n", err)
							} else {
								logger.Printf("告警已发送到 %s\n", webhook)
							}
						}(payload)
					}

					// 执行重启命令(config 中或全局传入) ------ 先尝试 graceful terminate 再执行重启命令(如果提供)
					if cfg.RestartCmd != "" {
						go func(cmdStr string, targetPid int32) {
							logger.Printf("尝试杀掉 pid=%d 并执行重启命令: %s\n", targetPid, cmdStr)
							// 发送 TERM
							_ = p.SendSignal(syscall.SIGTERM)
							// 等待短时间让进程退出
							time.Sleep(2 * time.Second)
							// 强制 kill 如果还存在
							exists, _ := process.PidExists(targetPid)
							if exists {
								_ = p.Kill()
							}
							// 执行重启命令(通过 shell)
							cmd := exec.Command("/bin/sh", "-c", cmdStr)
							out, err := cmd.CombinedOutput()
							if err != nil {
								logger.Printf("执行重启命令失败: %v. output: %s\n", err, string(out))
							} else {
								logger.Printf("重启命令已执行, output: %s\n", string(out))
							}
						}(cfg.RestartCmd, p.Pid)
					}
				}
			} // end for procs
		} // end ticker select
	} // end for
}

// postAlert 以 JSON POST 方式发送告警
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
}

代码说明 & 要点提醒

  1. 依赖 :本示例使用 github.com/shirou/gopsutil/v3/process。在项目目录运行:
bash 复制代码
go mod init procmon
go get github.com/shirou/gopsutil/v3/process
  1. 采样 CPUprocess.CPUPercent() 的行为受平台和调用频率影响。更精确的 CPU 百分比通常需要两次采样间的时间差(gopsutil 提供相关接口),但本例为简洁使用了库的默认方法。若需精确长期统计,可以保存上次样本并计算 delta。

  2. 权限问题:在某些系统上读取其他用户的进程信息需要更高权限(root)。如果监控不到目标进程,请以合适权限运行。

  3. 重启策略 :示例中通过 RestartCmd 执行自定义 shell 命令来重启服务(例如 systemctl restart myservicedocker restart container)。这是最灵活的方式,但要确保命令安全(不要盲目执行来自不可信配置的命令)。

  4. 告警去重 :示例里使用 alertCooldown 防止短时间内重复告警。你可以把告警状态持久化到 Redis/文件以跨重启保留告警状态。

  5. 跨平台:gopsutil 支持多平台,但信号、kill 等行为在 Windows 与 Unix 上不同。Windows 上需用不同方法停止进程。


使用示例

  1. 简单按进程名监控 nginx,CPU 超过 70% 或内存超过 300MB 时发 webhook:
bash 复制代码
./procmon -names nginx -cpu 70 -mem 300 -webhook "https://example.com/webhook"
  1. 使用 JSON 配置(config.json)支持多项监控(文件示例):
json 复制代码
[
  {
    "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"
  }
]

运行:

bash 复制代码
./procmon -config config.json -interval 5s -webhook "https://example.com/webhook"

可行的扩展与改进(工程化建议)

  • 持久化历史:把采样结果写入 InfluxDB/Prometheus 或本地文件,方便后续分析与告警策略优化。
  • 更智能的告警:支持平均值/移动窗口、抑制波动(例如短时 spike 不告警)、按时间段不同阈值。
  • 进程自恢复:把重启策略从单条命令扩展为"逐步恢复":先重启、再报警、再回滚;并记录重启次数以避免重启风暴。
  • UI 或 API:提供 HTTP 管理接口查看当前监控状态、触发测试告警或调整阈值。
  • 容器/Pod 支持:在容器环境下识别容器内进程或直接对容器做重启(Kubernetes 中可使用 K8s API 触发重启)。
  • 权限和安全:限制能够执行的 restart_cmd、对 webhook 使用签名/鉴权避免被滥用。

小结

本文实现了一个简单但实用的 Go 进程监控工具,涵盖进程扫描、资源采样、阈值检测、告警与重启动作。示例代码足够作为生产工具的原型,通过增加持久化、更多告警通道与更安全的重启策略,可以逐步把它演化为完整的运维监控组件。

相关推荐
lypzcgf6 小时前
Coze源码分析-资源库-编辑工作流-后端源码-流程/技术/总结
go·源码分析·工作流·coze·coze源码分析·ai应用平台·agent平台
小蒜学长7 小时前
springboot多功能智能手机阅读APP设计与实现(代码+数据库+LW)
java·spring boot·后端·智能手机
追逐时光者8 小时前
精选 4 款开源免费、美观实用的 MAUI UI 组件库,助力轻松构建美观且功能丰富的应用程序!
后端·.net
你的人类朋友9 小时前
【Docker】说说卷挂载与绑定挂载
后端·docker·容器
间彧9 小时前
在高并发场景下,如何平衡QPS和TPS的监控资源消耗?
后端
间彧9 小时前
QPS和TPS的区别,在实际项目中,如何准确测量和监控QPS和TPS?
后端
间彧9 小时前
消息队列(RocketMQ、RabbitMQ、Kafka、ActiveMQ)对比与选型指南
后端·消息队列
brzhang10 小时前
AI Agent 干不好活,不是它笨,告诉你一个残忍的现实,是你给他的工具太难用了
前端·后端·架构
brzhang10 小时前
一文说明白为什么现在 AI Agent 都把重点放在上下文工程(context engineering)上?
前端·后端·架构
Roye_ack11 小时前
【项目实战 Day9】springboot + vue 苍穹外卖系统(用户端订单模块 + 商家端订单管理模块 完结)
java·vue.js·spring boot·后端·mybatis