BackIcon使用 Go 实现网络监控系统

2023年7月2日

Openai logomark

Hamster1963

Untitled

项目背景

入职第二天,主管就马上下来了一个需求,监控终端的网络。原因是公司的网络很差,总是有同事反映网络很慢,钉钉不断断开重连,连接本地的群晖文件服务器也有问题,但目前的同事的应对手段主要就是重启路由器与服务器,总是没有从根源上解决问题,因此想找到一个可以稳定监控的方法。

网络架构

公司主营业务是跨境电商,因此在宽带的数量上达到了惊人的6条宽带,其中4条为专线宽带,不需要太多操心,其他例如运营,设计,开发与IT部门共用2条宽带,分别是电信的500M与联通的1000M宽带,从网络设备上来看就比较有意思了。

  1. 联通光猫→蒲公英路由
  2. 电信光猫→蒲公英路由
  3. 电信光猫→蒲公英路由

采用了链路聚合的形式,将两条宽带结合为一条宽带进行使用,蒲公英后台设置中为双wan模式,由蒲公英对数据包的分发进行管理。

而在AP方面,都采用了钉钉的C1路由器作为AP进行使用,接入钉钉的智能办公网络,在新人入职后可以很方便的直接在钉钉中点击智能办公室网络小程序进行网络的连接,进行打卡或是访问内网。

从架构来看,主要就是在蒲公英端或者是在钉钉AP端出现了某些问题,需要对网络继续一个基本的测试后再进行问题的排查。

Untitled

项目构想

想要持续监控终端网络的状况,主要还是得看到达员工端电脑上的网络速率,很多时候自己拿着去测觉得网络状态不错,但实际使用中由于不同电脑的各种硬件不同也可能会产生许多问题。

马上第一个想法便是利用speedtest的功能,定时在终端上进行测速,再通过某个渠道将数据进行汇总,这样便可以很方便的看到不同设备中的速度了,也很容易看到问题的大概方向。因此花了一个整体架构的草图,非常的简单,便是在终端,例如手提或者台式机器上利用speedtest进行测速,再构建一个后端系统进行数据的汇总,架构就完成了。

Untitled

项目选型

简单的架构完成后,马上遇到的问题就是,如何利用speedtest以及用什么语言去构建安装在终端上的定时测速程序呢?

首先第一个问题很快得到了解决,Ookla公司旗下的Speedtest工具为开发者提供了Speedtest CLI,这意味着我只需要去调用CLI并且获得数据即可完成测速的功能。简单了解指令后便设置用json格式获取测速CLI的输出以获得测速的数据。

Untitled

完整的命令为:

cmd := exec.Command("speed_cli/speedCLI/speedtest.exe", "--accept-gdpr", "--accept-license", "56634",
		"--progress=yes", "--format=json", "--progress-update-interval=200")

其中,"--accept-gdpr"与"--accept-license"可以跳过初次启动CLI需要输入YES同意的条款提示,

"56634"则是指定的测速服务器id,可以使得结果比较具有统一性,后续主要为获取数据的配置,--format=json将CLI的输出配置为JSON格式,可以很好的用map的形式去管理测速数据。

在语言选择方面,首先这门语言必须占用资源少,不会影响到终端的正常工作,其次语言应该具有良好的多架构适配性以可以安装在公司不同架构的电脑上,比如arm,amd64等,最后是最好可以编译为二进制文件,便于适配系统后台服务以及工具的分发。

没错,在考虑了开发需求与开发成本中,结果很明显,使用Golang语言进行客户端测速程序的编写明显是一个很好的选择。同时在框架上,选用了后端的goframe框架进行魔改后可以方便的利用到框架内的例如缓存,HTTP客户端以及定时任务的功能,降低了很多的开发成本。

Untitled

核心的问题解决了,那就进行开发吧。

客户端开发

到了开发更多就是根据语言特性选择不同的模块去实现功能了。这里主要介绍定时任务这个功能。

定时任务直接采用goframe框架中的gcron-定时任务进行构建。

gcron

模块提供了对定时任务的实现,支持类似

crontab

的配置管理方式,并支持最小粒度到

的定时任务管理。

使用起来非常的简单,只需要在客户端程序启动的时候,进行定时任务的注册即可。

func addSpeedCron(ctx context.Context, initData g.Map, timePattern string) (err error) {
	glog.Notice(ctx, "开始定时测速服务", timePattern)
	_, err = gcron.AddSingleton(ctx, timePattern, func(ctx context.Context) {
		err := cli_utils.CmdCore.StartSpeedCmd(ctx, initData)
		if err != nil {
			glog.Error(ctx, "定时测速服务失败: ", err)
			return
		}
	}, "Speed-Cron")
	if err != nil {
		glog.Warning(ctx, "添加定时测速服务失败: ", err)
		return err
	}
	return
}

timePattern选择gcorn库中的预定义格式,默认为

@every 1h

同时定时任务选择为单例模式,避免上次的测速未结束又进行了第二次测速造成测速CLI的冲突。

在goframe项目启动中,定时任务便也启动,按照定义好的时间间隔进行测速命令的执行。

为了后端对于客户端有足够的控制权限,timePattern内容与测速节点id是从后端进行获取的,这便需要客户端定时向后端获取最新的客户端配置,并且根据配置的内容再进行测速定时任务的创建。

glog.Debug(ctx, "开始初始化定时任务管理器")
	_, err = gcron.AddSingleton(ctx, "@every 30s", func(ctx context.Context) {
		err := cron_utils.CronManage.GetConfigAndStart(ctx, initData)
		if err != nil {
			glog.Error(ctx, "初始化定时任务管理器服务失败: ", err)
			return
		}
	}, "Cron-Manager")
	if err != nil {
		glog.Warning(ctx, "添加初始化定时任务管理器服务失败: ", err)
		return err
	}

采用配置定时任务管理器的形式进行测速定时任务的管理。

// 获取测速定时任务
	localSpeedCron := gcron.Search("Speed-Cron")
	if localSpeedCron == nil {
		glog.Notice(ctx, "本地不存在定时任务,添加Speed-Cron定时任务")
		err = addSpeedCron(ctx, initData, speedInterval)
		if err != nil {
			glog.Warning(ctx, "添加Speed-Cron定时器失败")
			return
		}
	} else {
		speedEntryPattern := reflect.ValueOf(localSpeedCron).Elem().FieldByName("schedule").Elem().FieldByName("pattern")
		if speedEntryPattern.String() != speedInterval {
			glog.Notice(ctx, "更新Speed-Cron定时器")
			// 更新定时任务
			gcron.Stop("Speed-Cron")
			gcron.Remove("Speed-Cron")
			err = addSpeedCron(ctx, initData, speedInterval)
			if err != nil {
				glog.Warning(ctx, "更新Speed-Cron定时器失败")
				return
			}
			return
		} else {
			glog.Notice(ctx, "Speed-Cron定时器无需更新")
		}
	}

在获取配置后,将本地的配置与服务器的配置进行比对,如果有改变,则找到原本的测速定时任务进行销毁再根据新配置创建新的定时任务。在这里比较特别的是,在gcron中,定时任务在创建完成后,其中时间间隔字段是私有字段,不能从外部直接获取,因此这里采用反射对测速定时任务的结构体进行了私有字段的获取以用以与服务器配置进行比对。

最后比较核心的模块便是与Speedtest CLI的交互了,十分的简单,这里直接给出完整的命令行信息获取函数。

// StartSpeedCmd
//
//	@dc: 开始测速命令行交互
//	@params:
//	@response:
//	@author: hamster   @date:2023/6/20 10:06:06
func (u *uCmdCore) StartSpeedCmd(ctx context.Context, initData g.Map) (err error) {
	cmd := CliUtils.CreateSpeedCmd()
	if cmd == nil {
		glog.Warning(ctx, "创建命令失败,获取测速节点失败")
		err = gerror.New("创建命令失败,获取测速节点失败")
		return
	}
	// 获取命令的标准输出管道
	stdout, err := cmd.StdoutPipe()
	if err != nil {
		glog.Warning(ctx, "获取标准输出管道时发生错误:", err)
		return
	}
	// 启动命令
	err = cmd.Start()
	if err != nil {
		glog.Warning(ctx, "启动命令时发生错误:", err)
		return
	}
	var (
		scanner             = bufio.NewScanner(stdout)
		defaultBars         = Bar.InitDefaultBar()
		uploadNetDataStruct = &NetInfoUploadData{}
	)
	uploadNetDataStruct.Department = gconv.String(initData["department"])
	uploadNetDataStruct.StaffName = gconv.String(initData["name"])
	// 持续获取输出
	for scanner.Scan() {
		// 获取输出行
		line := scanner.Bytes()
		ok, err := CmdProgress.CmdCoreProgress(ctx, string(line), defaultBars, uploadNetDataStruct)
		if err != nil {
			glog.Warning(ctx, "处理命令行输出时发生错误:", err)
			return err
		}
		if ok {
			return nil
		}
	}
	// 等待命令执行完成
	err = cmd.Wait()
	if err != nil {
		glog.Warning(ctx, "等待命令执行完成时发生错误:", err)
	}
	return
}

核心就是创建了一个命令行的输出管道,通过

for scanner.Scan()

的方式不断获取命令行的输出再进行处理。

测速完成后,组装信息上传至后端即可。

系统服务

在终端上进行,那就需要服务可以一直在后台运行,最好具有错误重启,开机自启动的功能,在对比了nvvm与winsw后,选用winsw进行Windows端系统服务的构建。

配置十分简单,配置一些基础的信息,搭配github上的预编译文件加命令即可完成系统服务的注册与开启。

<service>
  <id>v0.1.0</id>
  <name>My Go Application</name>
  <description>This is a Windows service for running my Go application.</description>
  <executable>%BASE%\core_bin\speed_cron.exe</executable>
  <workingdirectory>%BASE%\core_bin</workingdirectory>
  <arguments>-department=IT -name=方大同</arguments>
  <logpath>%BASE%\logs</logpath>
  <logmode>roll</logmode>
  <onfailure action="restart" delay="10 sec"></onfailure>
</service>

定义完成后利用

install

start

便可以将定时测速客户端注册为服务运行在系统后台

后端构建

增删查改,建好数据库的表就可以了,再添加一个定时检测机器状态的定时任务即可。

Untitled

监控一段时间后发现的问题

在部分网络状态不佳的设备上安装并等待一段数据的上报后,首先发现,怎么网段都不一样…

Untitled

在清一色的10网段中,怎么混入了172网段?!

艰辛万苦的排查后,发现是某台钉钉的路由器错误地设置为了路由模式,造成DHCP直接变成又钉钉路由器进行,就造成了网段的不一样,而由于钉钉路由器在路由模式下性能很差…就造成了连到这台路由器的设备会出现网络卡顿,断线的情况。

第一个问题解决了,还剩下一个就是网络总是定期有波动,从测速数据来看波动还是挺大的,最后从外部ip中发现了问题,发现蒲公英切换到电信500M的时候网络状态就十分不理想,解决办法很简单,把蒲公英路由器上电信那条网线拔了,对你没有听错….

问题都解决了。

一些后话

解决完这个问题后总感觉为了一碟醋包了一盘饺子…但是转念一想,没有这些大量数据的支撑,也很难通过测试发现问题的所在,同时在过程中也发现有一些是网卡硬件或者网线的问题造成协商速率只有100M,这些通过人工去测试当然也行,但是程序员就是懒嘛!让同事小手一点安装一下,等个半天看看数据再去处理,也是非常的轻松惬意,同时系统还可以接入比如预警,消息通知的第三方以完成更高程度监控自动化,也是非常灵活的!(嘴硬)

最后谢谢你看到这里,希望其中的一些golang實作可以给你一些启发。