一、背景

在过去的IT日常支持场景中,因为服务的用户、终端、系统等等因业务而异,往往会遇到以下类似这些问题或需求:

IT工程师定位终端问题跨越不同的平台或系统,低效繁琐
用户想要获取一些个人相关的IT环境信息,只能咨询IT部门
电脑终端的软硬件资源类性能消耗无法集中宏观监控、数字化管理
主动预判终端问题的客观依据和快速定位能力有差异化

通过建设一套“IT用户终端信息一体化管理平台”,采集用户信息、机器信息、软件信息、权限信息、网络信息等等,实现数据入库、实时更新、可视化。具备提供一站式查询管理、缩短case定位时间、集中数字化管理、提前预警终端软硬件风险等能力。

其中针对电脑及性能数据的采集,面临着指标多样性、数据实时性存储、系统差异性等高要求,下面针对电脑终端信息采集的Quark 2.0体系进行详细的介绍。

二、架构

Quark
2.0体系是为“IT用户终端信息一体化管理平台”建立的一套计算机性能监控体系,作为IT资产管理的延伸,帮助IT人员完成日常维护、故障排查和资源统计等工作。

该体系架构示意图如下:

image.png

安装在办公电脑的Agent按单位时间一次的心跳频率,采集计算机的CPU、内存、磁盘、网络、电池、进程占用等信息上报给Master集群,并把历史数据打点存入InfluxDB。支持Windows和MacOS两种主流的办公电脑操作系统。Master集群接收到Agent上报的心跳数据,将其存入Redis,并通过Etcd进行节点注册和健康检测。Registry集群作为IT一体化自助查询平台的后端服务器集群,承担了多个数据来源的集中查询、日志记录、任务调度等功能。
Agent客户端支持采集的数据类型如下:
心跳上报:实时获取最新数据,新数据会覆盖旧数据
数据打点:存储最新数据,保留旧数据
类型 指标 采集方式 备注
系统 操作系统 心跳上报 Windows/MacOS,包括具体版本号

计算机名    心跳上报    
内核架构    心跳上报    例:x86_64

硬件 序列号 心跳上报

制造厂商    心跳上报    
产品型号    心跳上报    

CPU 型号 心跳上报

厂商    心跳上报    
核数    心跳上报    
占用核数    数据打点    
频率    心跳上报    
使用率    数据打点    user、sys、iowait、idle、busy
温度    数据打点    

内存 容量 心跳上报/数据打点 单位:GiB

使用量    心跳上报/数据打点    单位:GiB
频率    心跳上报    

磁盘 总空间 心跳上报/数据打点 单位:GiB

已使用空间    心跳上报/数据打点    单位:GiB
分区    心跳上报/数据打点    单位:GiB
驱动    心跳上报    包括型号、类型、状态
读写字节数    数据打点    单位:GiB
读写速度    数据打点    单位:KiB/s
读写次数    数据打点    

网络 网络名 心跳上报

MTU    心跳上报    
Mac地址    心跳上报    
IP地址    心跳上报    
驱动    心跳上报    包括名称、描述、厂商、产品号
上下行字节数    数据打点    单位:GiB
上下行数据包总数    数据打点    
上下行速度    数据打点    单位:KiB/s

电池 状态 心跳上报/数据打点

状态码    心跳上报/数据打点    例:有无电池、使用电池/AC电源、是否满电
剩余电量    心跳上报/数据打点    
剩余使用时间    心跳上报/数据打点    部分系统版本不支持此指标

进程 占用CPU进程 数据打点 记录前五个

占用内存进程    数据打点    
占用IO进程    数据打点    

WiFi ESSID 数据打点

BSSID    数据打点    
信号强度    数据打点    

联网情况 ping内网 数据打点 min_rtt、max_rtt、avg_rtt、std_dev_rtt、loss

ping外网    数据打点    
ping网关    数据打点    
DNS解析    数据打点    是否能解析特定域名

三、数据采集
Agent客户端基于Go语言开发,通过gopsutil库、wmic等采集计算机数据。对于MacOS,则以解析ioreg、system_profiler等指令获取对应指标。由于采集指标众多,下面仅以电池和WiFi作为示例,讲解具体的数据采集原理。
电池信息
对于Windows,通过wmic指令调用Win32_Batteryapi,获取电脑的状态、状态码、剩余电量等信息。
//go:build windows
// +build windows

package battery

import (

"errors"
"math"

"git.ppd.com/quark/pkg/logger"
"git.ppd.com/quark/pkg/metrics"
"github.com/shopspring/decimal"
"github.com/yusufpapurcu/wmi"

)

type batteryInfo struct {

Availability             aStatus
BatteryStatus            bStatus
Status                   string
EstimatedChargeRemaining uint16
EstimatedRunTime         uint32
DesignCapacity           uint32
FullChargeCapacity       uint32

}

func win32BatteryInfo() (*batteryInfo, error) {

var batteryInfo []batteryInfo
err := wmi.Query("SELECT * FROM Win32_Battery", &batteryInfo)
if err != nil {
    return nil, err
}
if len(batteryInfo) > 0 {
    return &batteryInfo[0], nil
}
return nil, errors.New("empty battery info")

}

func BatteryInfo() metrics.BatteryInfo {

battery, err := win32BatteryInfo()
if err != nil {
    logger.Errorf("get BatteryInfo error: %s", err.Error())
    return metrics.BatteryInfo{Status: "NoBattery"}
}
var estimatedRunTime float64
if battery.EstimatedRunTime == uint32(math.Pow(2, 32)/60) {
    estimatedRunTime = 0
} else {
    tmpRunTime := decimal.NewFromFloat(float64(battery.EstimatedRunTime) / 60)
    estimatedRunTime, _ = tmpRunTime.Round(1).Float64()
}
return metrics.BatteryInfo{
    Status: battery.Status,
    Availability: metrics.BatStatus{
        Code: uint16(battery.Availability),
        Desc: battery.Availability.String(),
    },
    BatteryStatus: metrics.BatStatus{
        Code: uint16(battery.BatteryStatus),
        Desc: battery.BatteryStatus.String(),
    },
    CurrentCap:     float64(battery.FullChargeCapacity),
    DesignCap:      float64(battery.DesignCapacity),
    EstimatedTime:  estimatedRunTime,
    EstimatedPower: int64(battery.EstimatedChargeRemaining),
}

}
对于MacOS,则通过解析system_profiler SPPowerDataType和pmset -g batt指令的返回获取电池的对应信息。
WiFi信息
对于Windows,通过解析netsh wlan show interfaces指令的返回获取WiFi的BSSID、ESSID和信号强度。
对于MacOS,则通过解析/System/Library/PrivateFrameworks/Apple80211.framework/Versions/Current/Resources/airport -I和ioreg -l -n AirPortDriver | perl -lne print $1 if $_ =~ /IO80211BSSID.<(.)>/指令获取WiFi的对应信息。

四、心跳上报
QRPC组件
Master集群与Agent客户端之间基于自研的QRPC组件进行长连接通信。与市面上常见的开源RPC组件相比,QRPC组件具有通信简单、连接管理透明、完全可控等优点。
名称 开发者 简述 优点 缺点
GRPC Google 功能强大,是一款非常完善的rpc框架 功能强大完善;多语言支持;序列化效率高 学习、维护的成本较高;引入大量第三方库,存在潜在风险
net/rpc(标准库) GO Team 内置RPC包 标准库内置,无需引入新库 TCP网络连接管理缺失;序列化(GO内置)效率较低
QRPC 自研 用于Master集群与Agent客户端的通信 轻量化框架;通信简单;连接管理透明 适用性较窄
通信过程
首次心跳上报时,Agent需要先从Etcd中获取一个可用的Master节点,并采用Hash算法保证Master集群的负载均衡。获取到Master节点之后,建立长连接并写入Agent配置。Master节点接收到连接请求之后完成握手连接,并将心跳数据存储到Redis中,完成本次通信。如果发生意外导致连接中断或Master节点挂掉,Agent会自动重新获取新的可用节点。
获取Master节点实现如下:
import (

"encoding/json"
"fmt"
"hash/fnv"

"git.ppd.com/quark/agent_v2/config"
"git.ppd.com/quark/agent_v2/stats"
"git.ppd.com/quark/pkg/client/etcd"
"git.ppd.com/quark/pkg/logger"
"git.ppd.com/quark/pkg/metrics"

)

func (a *Agent) getMasterIP() (string, error) {

// 获取本机IP
localIP := stats.IP()
if localIP == "" {
    return "", fmt.Errorf("get local_ip failed: local_ip is <nil>")
}

// 获取所有可用的Master节点
// 要求:当前节点负载低于预设的最大负载
var assignAddrs []string
maxLoad := config.Config().MasterConfig.MaxLoad
masterInfo := registryMasterNode()
for _, v := range masterInfo {
    if v.ReportNum < maxLoad {
        assignAddrs = append(assignAddrs, v.IP)
    }
}

// Hash算法获取Master节点
hashValue := hash(localIP)
index := int(hashValue) % len(assignAddrs)
masterNode := assignAddrs[index]

return masterNode, nil

}

// 获取已注册的Master节点
func registryMasterNode() map[string]*metrics.MasterHeartBeat {

tmp := make(map[string]*metrics.MasterHeartBeat, 0)

// etcd初始化
cfg := config.Config().EtcdConfig
etcd.Init(cfg.EtcdAddr)
defer func() {
    if err := etcd.EClient.Close(); err != nil {
        logger.Errorf("close etcd client failed: %s", err.Error())
    }
}()

resp, err := etcd.EClient.Get(cfg.MasterPath, clientv3.WithPrefix())
if err != nil {
    logger.Errorf("从etcd获取master节点列表失败: %s", err.Error())
    return tmp
}

for k, v := range resp.Kvs {
    master := &metrics.MasterHeartBeat{}
    err := json.Unmarshal(v.Value, &master)
    if err != nil {
        logger.Errorf("master节点%d序列化失败: %s", k, err.Error())
        continue
    }
    tmp[master.IP] = master
}

return tmp

}

// Hash: string to int
func hash(key string) uint32 {

h := fnv.New32a()
h.Write([]byte(key))
return h.Sum32()

}
Agent在线状态管理
Master获取Agent在线状态,主要是通过Redis的键过期订阅机制。预先设置Agent键的过期时间,每次心跳上报时更新值,同时重置初始时间。超过过期时间仍未更新,则视为Agent已离线。

五、历史数据打点
InfluxDB
InfluxDB是一个用于存储和分析时间序列数据的开源数据库,主要特性如下:
• 内置HTTP接口,使用方便
• 数据可以打标记,查询很灵活
• 类SQL的查询语句
• 安装管理简单,并且读写数据高效
• 支持实时查询
Quark 2.0体系基于InfluxDB v1的HTTP POST方式,对Agent采集的历史数据进行打点存储。
InfluxDB-Relay
由于InfluxDB v1开源版不支持集群模式,故采用官方推荐的社区开源高可用方案InfluxDB-Relay。

image.png

InfluxDB-Relay为InfluxDB提供双写能力,确保其中一个节点挂掉后数据不会丢失。注意InfluxDB-Relay只代理写流量,查询数据时直接访问InfluxDB。
influxdb-relay.toml 配置如下:
[[http]]
name = "influx-http"
bind-addr = "127.0.0.1:9096"
output = [

{ name="db1", location = "http://XX.XXX.XX.XXX:8086/write" },
{ name="db2", location = "http://XX.XXX.XX.XXX:8086/write" },
{ name="db3", location = "http://XX.XXX.XX.XXX:8086/write" },

]
页面交互
“IT用户终端信息一体化管理平台”支持根据域账号和计算机名查询对应信息,包括用户的基本信息、名下资产和所在群组、办公电脑的IT资产信息、心跳数据和历史数据等。
image.png
image.png
image.png

六、未来展望

在后续的平台建设中,“IT用户终端信息一体化管理平台”还将完善接入更完整的其他用户信息,例如各相关系统权限、入网认证各环节状态、虚拟资产等信息,进行一体化关联,为实现终端智能化管理夯实基础。

 


信也科技布道师
12 声望10 粉丝