基于Go语言开发篇一个命令行进程监控工具

 更新时间:2025年09月15日 08:18:42   作者:程序员爱钓鱼  
在生产和开发环境中,监控关键进程的存活与资源使用是非常常见的需求,本篇将开发一个轻量级的命令行进程监控工具,感兴趣的小伙伴可以了解下

在生产和开发环境中,监控关键进程的存活与资源使用是非常常见的需求:当进程 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,主循环统一调度与统计

项目结构(示意)

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 表示对单个进程的监控配置
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
}

代码说明 & 要点提醒

依赖:本示例使用 github.com/shirou/gopsutil/v3/process。在项目目录运行:

go mod init procmon
go get github.com/shirou/gopsutil/v3/process

采样 CPUprocess.CPUPercent() 的行为受平台和调用频率影响。更精确的 CPU 百分比通常需要两次采样间的时间差(gopsutil 提供相关接口),但本例为简洁使用了库的默认方法。若需精确长期统计,可以保存上次样本并计算 delta。

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

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

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

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

使用示例

简单按进程名监控 nginx,CPU 超过 70% 或内存超过 300MB 时发 webhook:

./procmon -names nginx -cpu 70 -mem 300 -webhook "https://example.com/webhook"

使用 JSON 配置(config.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"
  }
]

运行:

./procmon -names nginx -cpu 70 -mem 300 -webhook "https://example.com/webhook"

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

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

小结

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

到此这篇关于基于Go语言开发篇一个命令行进程监控工具的文章就介绍到这了,更多相关Go进程监控内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 详解Go语言的错误处理和资源管理

    详解Go语言的错误处理和资源管理

    资源处理是什么?打开文件需要关闭,打开数据库连接,连接需要释放。这些成对出现的就是资源管理。有时候我们虽然释放了,但是程序在中间出错了,那么可能导致资源释放失败。如何保证打开的文件一定会被关闭呢?这就是资源管理与错误处理考虑的一个原因
    2021-06-06
  • Golang语言学习拿捏Go反射示例教程

    Golang语言学习拿捏Go反射示例教程

    这篇文章主要为大家介绍了Golang语言中Go反射示例的教程,教你拿捏Go反射,再也不用被Go反射折磨,有需要的朋友可以共同学习参考下
    2021-11-11
  • 解读 Go 中的 constraints包完整案例

    解读 Go 中的 constraints包完整案例

    Go1.18引入constraints包,提供泛型类型约束接口(如Signed、Unsigned、Ordered、Comparable),用于限制类型参数并提升类型安全与代码复用,内置any和comparable约束,当前处于实验阶段,本文给大家介绍解读 Go 中的 constraints包,感兴趣的朋友一起看看吧
    2025-07-07
  • Go语言WaitGroup使用时需要注意的坑

    Go语言WaitGroup使用时需要注意的坑

    Go语言中WaitGroup的用途是它能够一直等到所有的goroutine执行完成,并且阻塞主线程的执行,直到所有的goroutine执行完成。之前一直使用也没有问题,但最近通过同事的一段代码引起了关于WaitGroup的注意,下面这篇文章就介绍了WaitGroup使用时需要注意的坑及填坑。
    2016-12-12
  • go语言基础语法示例

    go语言基础语法示例

    这篇文章主要介绍了go语言基础语法示例,介绍了go语言较为全面的基础知识,具有一定参考价值,需要的可以了解下。
    2017-11-11
  • go语言中函数与方法介绍

    go语言中函数与方法介绍

    这篇文章介绍了go语言中的函数与方法,文中通过示例代码介绍的非常详细。对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-07-07
  • GoLang基础学习之go test测试

    GoLang基础学习之go test测试

    相信每位编程开发者们应该都知道,Golang作为一门标榜工程化的语言,提供了非常简便、实用的编写单元测试的能力,下面这篇文章主要给大家介绍了关于GoLang基础学习之go test测试的相关资料,需要的朋友可以参考下
    2022-08-08
  • Golang实现按比例切分流量的示例详解

    Golang实现按比例切分流量的示例详解

    我们在进行灰度发布时,往往需要转发一部分流量到新上线的服务上,进行小规模的验证,随着功能的不断完善,我们也会逐渐增加转发的流量,这就需要按比例去切分流量,那么如何实现流量切分呢,接下来小编就给大家详细的介绍一下实现方法,需要的朋友可以参考下
    2023-09-09
  • 使用Go语言玩转 RESTful API 服务

    使用Go语言玩转 RESTful API 服务

    RESTful API是一种基于HTTP协议的API设计风格,遵循REST架构风格,这篇文章主要为大家介绍了如何通过Go语言构建RESTful API服务,有需要的可以了解下
    2025-02-02
  • 一文带你搞懂Golang如何正确退出Goroutine

    一文带你搞懂Golang如何正确退出Goroutine

    在Go语言中,Goroutine是一种轻量级线程,它的退出机制对于并发编程至关重要,下午就来介绍几种Goroutine的退出机制,希望对大家有所帮助
    2023-06-06

最新评论