运维监控,如何获取数据?

运维如果想做自动化高效化,则少不了搭建监控系统。目前市面上已经有大量成熟、开源的监控平台可供挑选。但如果想实现一个监控系统,或了解监控系统的原理,则可参见本文。

1. 常见运维监控系统划分

常见运维监控系统可按有/无Agent,使用Pull/Push获取数据进行简单划分。

有/无Agent互斥,是单选题;Pull/Push可并存,是多选题。使用这两种划分,共有C(1,2)*( C(1 ,2)+C(2,2) )=6 种情况。

监控实际上发生在监控主机和被监控主机的进程之间。监控主机内运行主动拉取、被动接收进程,分别实现Pull、Push能力;被监控主机开启通用功能(SNMP/SSH/Telnet/HTTP)进程,运行Agent进程,实现向外提供metric数据的能力。

1.1 什么是Agent?

先看定义,“在分布计算领域,人们通常把在分布式系统中持续自主发挥作用的、具有自主性、交互性、反应性、主动性的活着的计算实体称为Agent”。在各式各样的开源项目里,大家都喜欢把那些为了实现某些功能,需要额外部署在客户机上的轻量级程序,称为Agent。

1.2 什么时候需要Agent?

 “需要agent” 的潜台词是“目标需要额外装软件才支持新功能”,类似于打印机需要安装专门的驱动程序才能使用。 “不需要agent” 则意味着 “目标现在已经支持这个功能”,类似于现在无线网卡已经可以免驱动随插随用。需要目标没有的功能或自定义功能,就需要使用到Agent。

1.3 Pull和Push如何选择?

Pull明显会产生更多的流量,Push则流量相对较少,但是否Push就比Pull优秀呢?不是的。Pull在拉取过程中,可以顺便检测被监控机活跃状态,而Push,节点如果没有发送metric,没办法确定是节点ping不通/metric包被丢包/节点本身出现问题,这种情况下Pull会比Push更好定位问题原因。两者还有许多其他的区别,建议成年人不做选择题,Pull/Push全都要,都实现后确定其中某一项为主方式即可。两者的更多区别可以参考此文章

2. 不使用Agent时的数据获取

2.1 SNMP

SNMP是最适合做小流量监控的协议,一般服务器/网络设备/存储设备都会实现。但此协议需要手动配置开启,简要的开启和测试过程如下。

//centos启动SNMP服务(本机可以提供SNMP服务)
[[email protected] ~]# service snmpstart

//centos安装snmp工具包(本机可以拉取SNMP信息)
[[email protected] ~]# yum -y install net-snmp-utils

//已经安装好的SNMP工具命令
[[email protected] ~]# snmp
snmpbulkget    snmpconf       snmpdelta      snmpget        snmpinform     snmpset        snmptable      snmptls        snmptrap       snmpusm        snmpwalk
snmpbulkwalk   snmpd          snmpdf         snmpgetnext    snmpnetstat    snmpstatus     snmptest       snmptranslate  snmptrapd      snmpvacm     

//SNMP常用的两种拉取,get和walk示例
[[email protected] ~]# snmpget -v 2c -c public 10.6.16.128 1.3.6.1.4.1.2021.11.9.0
UCD-SNMP-MIB::ssCpuUser.0 = INTEGER: 0
[[email protected] ~]# snmpwalk -v 2c -c public 10.6.16.128 1.3.6.1.2.1.3.1.1
SNMPv2-SMI::mib-2.3.1.1.1.2.1.10.6.16.250 = INTEGER: 2
SNMPv2-SMI::mib-2.3.1.1.1.2.1.10.6.16.253 = INTEGER: 2
SNMPv2-SMI::mib-2.3.1.1.1.2.1.10.6.16.254 = INTEGER: 2
SNMPv2-SMI::mib-2.3.1.1.2.2.1.10.6.16.250 = Hex-STRING: 00 50 56 F6 1F 49 
SNMPv2-SMI::mib-2.3.1.1.2.2.1.10.6.16.253 = Hex-STRING: 00 50 56 C0 00 08 
SNMPv2-SMI::mib-2.3.1.1.2.2.1.10.6.16.254 = Hex-STRING: 00 50 56 FA 04 59 
SNMPv2-SMI::mib-2.3.1.1.3.2.1.10.6.16.250 = IpAddress: 10.6.16.250
SNMPv2-SMI::mib-2.3.1.1.3.2.1.10.6.16.253 = IpAddress: 10.6.16.253

//juniper交换机配置SNMP服务
set snmp community linux authorization read-only
set snmp community linux client-list-name snmp5
set snmp community linux_xxx authorization read-write
set snmp community linux_xxx client-list-name snmp5

//cisco n9k交换机配置SNMP服务
snmp-server community linux group network-operator
snmp-server community linux_xxx group network-operator
snmp-server community linux use-ipv4acl 50
snmp-server community linux_xxx use-ipv4acl 50

SNMP功能开启后,类似于开放了一个key-value数据库,我们需要使用一种名为OID的key去获取对应的value。OID对应的含义可查询此链接,笔者整理的通用OID如下。

#encoding=utf-8
#collect by paul hu 2021/04/05
#available for python 2.x/3.x

#归类到通用的oid
OidsInternet = {

    "ipNetToMediaEntry":{
        # 用于拉取物理地址,ip地址,ifindex三者之间的映射(linux服务器适用,网络设备尚未测试)
        # 除了增加了映射类型,表项与下面的arptable基本一致,但要注意如果是在大二层架构下,arp表只有核心可以查询到,cam是每台机器都可以查到
        # 1.3.6.1.2.1 = iso.identified-organization.dod.internet.mgmt.mib-2
        # .4.22.1     = .ip.ipNetToMediaTable.ipNetToMediaEntry
        'ipNetToMediaIfIndex'     : '1.3.6.1.2.1.4.22.1.1',  #唯一标识->ifindex
        'ipNetToMediaPhysAddress' : '1.3.6.1.2.1.4.22.1.2',  #唯一标识->物理地址
        'ipNetToMediaNetAddress'  : '1.3.6.1.2.1.4.22.1.3',  #唯一标识->实际对应的ip地址
        'ipNetToMediaType'        : '1.3.6.1.2.1.4.22.1.4',  #唯一标识->映射类型
    },

    "atEntry":{
        #  用于拉取Arp表数据(linux服务器/网络设备通用) at= arp table
        #  1.3.6.1.2.1.3 = iso.org.dod.internet.mgmt.mib-2.at
        #  .1.1.2        = .atTable.atEntry.atPhysAddress
        'atIfIndex'     : '1.3.6.1.2.1.3.1.1.1',  # 唯一标识->ifindex
        'atPhysAddress' : '1.3.6.1.2.1.3.1.1.2',  # 唯一标识->物理地址
        'atNetAddress'  : '1.3.6.1.2.1.3.1.1.3',  # 唯一标识->ip地址
    },

    "ifEntry":{
        #  接口的各种相关信息,包括类型,速率,状态等(linux服务器/网络设备通用)
        #  1.3.6.1.2.1 = iso.org.dod.internet.mgmt.mib-2
        #  2.2.1       = interfaces.ifTable.ifEntry
        'ifIndex'       : '1.3.6.1.2.1.2.2.1.1', #ifindex
        'ifDescr'       : '1.3.6.1.2.1.2.2.1.2', #描述
        'ifType'        : '1.3.6.1.2.1.2.2.1.3', #类型
        'ifSpeed'       : '1.3.6.1.2.1.2.2.1.5', #速率/带宽
        'ifPhysAddress' : '1.3.6.1.2.1.2.2.1.6', #物理地址
        'ifAdminStatus' : '1.3.6.1.2.1.2.2.1.7', #管理状态
        'ifOperStatus'  : '1.3.6.1.2.1.2.2.1.8', #操作状态
        'ifLastChange'  : '1.3.6.1.2.1.2.2.1.9', #上次变更时间
        'IfInOctet'     : '1.3.6.1.2.1.2.2.1.10',#接口接收的字节数
        'IfInUcastPkts' : '1.3.6.1.2.1.2.2.1.11',#接口接收的数据包数
        'IfOutOctet'    : '1.3.6.1.2.1.2.2.1.16',#接口发送的字节数
        'IfOutUcastPkts': '1.3.6.1.2.1.2.2.1.17',#接口发送的数据包数
    },

    "ifXEntry":{
        # 接口附加信息
        #  1.3.6.1.2.1 = iso.identified-organization.dod.internet.mgmt.mib-2
        # .31.1.1.1    = .ifMIB.fMIBObjects.ifXTable.ifXEntry
        'ifName': '1.3.6.1.2.1.31.1.1.1.1',  # 接口名
        'ifHighSpeed': '1.3.6.1.2.1.31.1.1.1.15',  # 接口当前带宽的估计值
        'ifAlias': '1.3.6.1.2.1.31.1.1.1.18',  # 接口别名
        'ifStackStatus': '1.3.6.1.2.1.31.1.2.1.3',
    },

    "system":{
        #  系统信息(linux服务器/网络设备通用)
        #  1.3.6.1.2.1 = iso.org.dod.internet.mgmt.mib-2
        'sysDescr': '1.3.6.1.2.1.1.1.0',  # 系统描述
        'sysObjectID': '1.3.6.1.2.1.1.2.0',  # 系统oid
        'sysUpTime': '1.3.6.1.2.1.1.3.0',  # 系统运行时间
        'sysContact': '1.3.6.1.2.1.1.4.0',  # 系统联系人
        'sysName': '1.3.6.1.2.1.1.5.0',  # 系统名
        'SysLocation': '1.3.6.1.2.1.1.6.0',  # 系统位置
        'SysService': '1.3.6.1.2.1.1.7.0',  # 系统提供服务
    },

    "hrSWRunEntry":{
        #  运行的进程信息(linux服务器适用,网络设备待测试)
        # 1.3.6.1.2.1   = identified-organization.dod.internet.mgmt.mib-2
        # .25.4.2.1     = host.hrSWRun.hrSWRunTable
        'hrSWRunIndex': '1.3.6.1.2.1.25.4.2.1.1',  # 进程index
        'hrSWRunName': '1.3.6.1.2.1.25.4.2.1.2',  # 进程名
        'hrSWRunID': '1.3.6.1.2.1.25.4.2.1.3',  # 进程id
        'hrSWRunPath': '1.3.6.1.2.1.25.4.2.1.4',  # 进程运行路径
        'hrSWRunParameters': '1.3.6.1.2.1.25.4.2.1.5',  # 进程运行参数
        'hrSWRunType': '1.3.6.1.2.1.25.4.2.1.6',  # 进程运行类型
        'hrSWRunStatus': '1.3.6.1.2.1.25.4.2.1.7',  # 进程运行状态
        'hrSWRunPriority': '1.3.6.1.2.1.25.4.2.1.8',  # 进程运行优先级

    },

    "hrSWInstalledEntry":{
        #  安装的软件列表(linux服务器适用,网络设备待测试)
        # 1.3.6.1.2.1   = identified-organization.dod.internet.mgmt.mib-2
        # .25.6.3.1     = host.hrSWInstalled.hrSWInstalledTable.hrSWInstalledEntry
        'hrSWInstalledIndex': '1.3.6.1.2.1.25.6.3.1.1',  # 安装index
        'hrSWInstalledName': '1.3.6.1.2.1.25.6.3.1.2',  # 安装软件名
        'hrSWInstalledID': '1.3.6.1.2.1.25.6.3.1.3',  # 安装id
        'hrSWInstalledType': '1.3.6.1.2.1.25.6.3.1.4',  # 安装类型
        'hrSWInstalledDate': '1.3.6.1.2.1.25.6.3.1.5',  # 安装时间
        'hrSWInstalledDescription': '1.3.6.1.2.1.25.6.3.1.6',  # 安装描述
        'hrSWInstalledVersion': '1.3.6.1.2.1.25.6.3.1.7',  # 安装版本
    },

    "ipCidrRouteEntry":{
        #  路由表相关(linux服务器/开启了三层功能的网络设备)
        #  1.3.6.1.2.1 = iso.org.dod.internet.mgmt.mib-2
        #  4.24.4.1    = ip.ipForward.ipCidrRouteTable.ipCidrRouteEntry
        #  更详细的路由表内容请自行搜索添加,此处仅添加前4项
        'ipCidrRouteDest': '1.3.6.1.2.1.4.24.4.1.1',  # 目标网段
        'ipCidrRouteMask': '1.3.6.1.2.1.4.24.4.1.2',  # 目标网段掩码
        'ipCidrRouteTos': '1.3.6.1.2.1.4.24.4.1.3',  # TYPE OF SERVICE
        'ipCidrRouteNextHop': '1.3.6.1.2.1.4.24.4.1.4',  # 下一跳地址
    },

    "hrStorage": {
        # 存储相关
        "hrStorageTypes": "1.3.6.1.2.1.25.2.1",  # 获取存储类型
        "hrMemorySize": "1.3.6.1.2.1.25.2.2",  # 获取内存大小
        "hrStorageIndex": "1.3.6.1.2.1.25.2.3.1.1",  # 存储设备编号
        "hrStorageType": "1.3.6.1.2.1.25.2.3.1.2",  # 存储设备类型
        "hrStorageDescr": "1.3.6.1.2.1.25.2.3.1.3",  # 存储设备描述
        "hrStorageAllocationUnits": "1.3.6.1.2.1.25.2.3.1.4",  # 簇的大小
        "hrStorageSize": "1.3.6.1.2.1.25.2.3.1.5",  # 簇的的数目
        "hrStorageUsed": "1.3.6.1.2.1.25.2.3.1.6",  # 使用多少,跟总容量相除就是占用率
    },

    "extra":{
        #  其他未分类oid
        #  二层相关拉取(一般仅交换机,具体使用请结合品牌进行拉取测试)
        #  1.3.6.1.2.1 = iso.org.dod.internet.mgmt.mib-2
        #  17.1.4.1    = dot1dBridge.dot1dBase.dot1dBasePortTable.dot1dBasePortEntry
        'jnxdot1qTpFdbPort': '1.3.6.1.2.1.17.7.1.2.2.1.2',
        'dot1dBasePortIfIndex': '1.3.6.1.2.1.17.1.4.1.2',
        'dot1dTpFdbPort': '1.3.6.1.2.1.17.4.3.1.2',
        'dot1dTpFdbAddress': '1.3.6.1.2.1.17.4.3.1.1',
        'dot1qNumVlans': '1.3.6.1.2.1.17.7.1.1.4',
        'dot1qTpFdbTable': '1.3.6.1.2.1.17.7.1.2.2',
        'dot1qPvid': '1.3.6.1.2.1.17.7.1.4.5.1.1',  # portid->vlan
    },

    "hrProcessorTable":{
        # 处理器表
        "hrProcessorLoad": "1.3.6.1.2.1.25.3.3.1.2",  # CPU的当前负载,N个核就有N个负载
    },
}

#归类到私有的oid
OidsPrivate = {

    "systemStats": {
        # 系统状态,负载cpu等(linux服务器适用,网络设备需要测试)
        "ssCpuUser": "1.3.6.1.4.1.2021.11.9.0",  # 用户CPU百分比
        "ssCpuSystem": "1.3.6.1.4.1.2021.11.10.0",  # 系统CPU百分比
        "ssCpuIdle": "1.3.6.1.4.1.2021.11.11.0",  # 空闲CPU百分比
        "ssCpuRawUser": "1.3.6.1.4.1.2021.11.50.0",  # 原始用户CPU使用时间
        "ssCpuRawNice": "1.3.6.1.4.1.2021.11.51.0",  # 原始nice占用时间
        "ssCpuRawSystem": "1.3.6.1.4.1.2021.11.52.0",  # 原始系统CPU使用时间
        "ssCpuRawIdle": "1.3.6.1.4.1.2021.11.53.0",  # 原始CPU空闲时间
        "ssSwapIn": "1.3.6.1.4.1.2021.11.3.0",
        "SsSwapOut": "1.3.6.1.4.1.2021.11.4.0",
        "ssIOSent": "1.3.6.1.4.1.2021.11.5.0",
        "ssIOReceive": "1.3.6.1.4.1.2021.11.6.0",
        "ssSysInterrupts": "1.3.6.1.4.1.2021.11.7.0",
        "ssSysContext": "1.3.6.1.4.1.2021.11.8.0",
        "ssCpuRawWait": "1.3.6.1.4.1.2021.11.54.0",
        "ssCpuRawInterrupt": "1.3.6.1.4.1.2021.11.56.0",
        "ssIORawSent": "1.3.6.1.4.1.2021.11.57.0",
        "ssIORawReceived": "1.3.6.1.4.1.2021.11.58.0",
        "ssRawInterrupts": "1.3.6.1.4.1.2021.11.59.0",
        "ssRawContexts": "1.3.6.1.4.1.2021.11.60.0",
        "ssCpuRawSoftIRQ": "1.3.6.1.4.1.2021.11.61.0",
        "ssRawSwapIn": "1.3.6.1.4.1.2021.11.62.0",
        "ssRawSwapOut": "1.3.6.1.4.1.2021.11.63.0",
        "Load5": "1.3.6.1.4.1.2021.10.1.3.1",
        "Load10": "1.3.6.1.4.1.2021.10.1.3.2",
    },

    "memTotalSwap": {
        # 交换内存相关(linux服务器适用,网络设备需要测试)
        "memTotalSwap": "1.3.6.1.4.1.2021.4.3.0",  # Total Swap Size(虚拟内存)
        "memAvailSwap": "1.3.6.1.4.1.2021.4.4.0",  # Available Swap Space
        "memTotalReal": "1.3.6.1.4.1.2021.4.5.0",  # Total RAM in machine
        "memAvailReal": "1.3.6.1.4.1.2021.4.6.0",  # Total RAM used
        "memTotalFree": "1.3.6.1.4.1.2021.4.11.0",  # Total RAM Free
        "memShared": "1.3.6.1.4.1.2021.4.13.0",  # Total RAM Shared
        "memBuffer": "1.3.6.1.4.1.2021.4.14.0",  # Total RAM Buffered
        "memCached": "1.3.6.1.4.1.2021.4.15.0",  # Total Cached Memory
    },
    "dskEntry": {
        # 磁盘相关(linux服务器适用,网络设备需要测试)
        "dskPath": "1.3.6.1.4.1.2021.9.1.2",  # Path where the disk is mounted
        "dskDevice": "1.3.6.1.4.1.2021.9.1.3",  # Path of the device for the partition
        "dskTotal": "1.3.6.1.4.1.2021.9.1.6",  # Total size of the disk/partion (kBytes)
        "dskAvail": "1.3.6.1.4.1.2021.9.1.7",  # Available space on the disk
        "dskUsed": "1.3.6.1.4.1.2021.9.1.8",  # Used space on the disk
        "dskPercent": "1.3.6.1.4.1.2021.9.1.9",  # Percentage of space used on disk
        "dskPercentNode": "1.3.6.1.4.1.2021.9.1.10",  # Percentage of inodes used on disk
    },

    "chassisGrp":{
        # CISCO私有,集群相关
        # 1.3.6.1.4.1 = iso.org.dod.internet.private.enterprises
        # 9.5.1.2.16 = cisco.wkgrpProducts.stack.chassisGrp.chassisModel
        'chassisModel': '1.3.6.1.4.1.9.5.1.2.16.0',
    },

    "vmVoiceVlanEntry":{
        # CISCO私有,vmVoice相关
        # 1.3.6.1.4.1.9  = iso.org.dod.internet.private.enterprises.cisco
        # 9.68.1         = ciscoMgmt.ciscoVlanMembershipMIB.ciscoVlanMembershipMIBObjects
        # 5.1.1.1        = vmVoiceVlan.vmVoiceVlanTable.vmVoiceVlanEntry,vmVoiceVlanId
        'vmVoiceVlanId' : '1.3.6.1.4.1.9.9.68.1.5.1.1.1',
        'vmVlan'        : '1.3.6.1.4.1.9.9.68.1.2.2.1.2',
    },

    "c2900PortEntry":{
        # CISCO私有
        # 1.3.6.1.4.1.9 = iso.org.dod.internet.private.enterprises.cisco
        # 9.87.1.4      = ciscoMgmt.ciscoC2900MIB.c2900MIBObjects.c2900Port
        # 1.1.32        = c2900PortTable.c2900PortEntry.c2900PortDuplexStatus
        'c2900PortLinkbeatStatus' : '1.3.6.1.4.1.9.9.87.1.4.1.1.18',
        'c2900PortDuplexState'    : '1.3.6.1.4.1.9.9.87.1.4.1.1.31',
        'c2900PortDuplexStatus'   : '1.3.6.1.4.1.9.9.87.1.4.1.1.32',
        'c2900PortVoiceVlanId'    : '1.3.6.1.4.1.9.9.87.1.4.1.1.37',
    },

    "moduleEntry":{
        # CISCO私有
        # 1.3.6.1.4.1   iso.org.dod.internet.private.enterprises
        # 9.5.1.3       cisco.wkgrpProducts.stack.moduleGrp
        # 1.1.2         moduleTable.moduleEntry.moduleType
        'moduleType': '1.3.6.1.4.1.9.5.1.3.1.1.2',
        'moduleSerialNumber': '1.3.6.1.4.1.9.5.1.3.1.1.3',
        'moduleName': '1.3.6.1.4.1.9.5.1.3.1.1.13',
        'moduleModel': '1.3.6.1.4.1.9.5.1.3.1.1.17',
        'moduleHwVersion': '1.3.6.1.4.1.9.5.1.3.1.1.18',
        'moduleFwVersion': '1.3.6.1.4.1.9.5.1.3.1.1.19',
        'moduleSwVersion': '1.3.6.1.4.1.9.5.1.3.1.1.20',
        'moduleSerialNumberString': '1.3.6.1.4.1.9.5.1.3.1.1.26',
    },

    "vlanPortEntry": {
        # CISCO私有
        # 1.3.6.1.4.1.9  = iso.org.dod.internet.private.enterprises.cisco
        # 5.1.9.3.1.3    = wkgrpProducts.stack.vlanGrp.vlanPortTable.vlanPortEntry.vlanPortVlan
        'vlanPortVlan'          : '1.3.6.1.4.1.9.5.1.9.3.1.3',
        'vlanPortIslAdminStatus': '1.3.6.1.4.1.9.5.1.9.3.1.7',
        'vlanPortIslOperStatus' : '1.3.6.1.4.1.9.5.1.9.3.1.8',
    },

    "cdpCacheEntry": {
        # CISCO私有,CDP协议相关
        # 1.3.6.1.4.1.9 = iso.org.dod.internet.private.enterprises.cisco
        # 9.23.1.2      = ciscoMgmt.ciscoCdpMIB.ciscoCdpMIBObjects.cdpCache
        # 1.1.8         = cdpCacheTable.cdpCacheEntry.cdpCachePlatform
        'cdpCacheDeviceId'  : '1.3.6.1.4.1.9.9.23.1.2.1.1.6',
        'cdpCacheDevicePort': '1.3.6.1.4.1.9.9.23.1.2.1.1.7',
        'cdpCachePlatform'  : '1.3.6.1.4.1.9.9.23.1.2.1.1.8',
    },

    "vtpVlanEntry":{
        # CISCO私有,VTP相关
        # 1.3.6.1.4.1.9  = iso.org.dod.internet.private.enterprises.cisco
        # 9.46.1.6.1.1   = ciscoMgmt.ciscoVtpMIB.vtpMIBObjects.vlanTrunkPorts.vlanTrunkPortTable.vlanTrunkPortEntry
        'vtpVlanState'               : '1.3.6.1.4.1.9.9.46.1.3.1.1.2',
       'vlanTrunkPortVlansEnabled'  : '1.3.6.1.4.1.9.9.46.1.6.1.1.4',
        'vlanTrunkPortNativeVlan'    : '1.3.6.1.4.1.9.9.46.1.6.1.1.5',
        'vlanTrunkPortDynamicState'  : '1.3.6.1.4.1.9.9.46.1.6.1.1.13',
        'vlanTrunkPortDynamicStatus' : '1.3.6.1.4.1.9.9.46.1.6.1.1.14',
       'vlanTrunkPortVlansEnabled2k': '1.3.6.1.4.1.9.9.46.1.6.1.1.17',
       'vlanTrunkPortVlansEnabled3k': '1.3.6.1.4.1.9.9.46.1.6.1.1.18',
       'vlanTrunkPortVlansEnabled4k': '1.3.6.1.4.1.9.9.46.1.6.1.1.19',
    },

    "hwL2IfEntry":{
        # 华为私有,二层表相关,需要其他的信息可以参考下面的oid自行添加
        "hwVlanTrunkPortDynamicStatus" : "1.3.6.1.4.1.2011.5.25.42.1.1.1.3.1.3",
    },

    "hh3cifXXEntry":{
        # h3c私有,二层表相关,需要其他的信息可以参考下面的oid自行添加
        "h3cVlanTrunkPortDynamicStatus": "1.3.6.1.4.1.25506.8.35.1.1.1.5",
    },

    "myVlanIfStateEntry":{
        # 锐捷私有,二层表相关,需要其他的信息可以参考下面的oid自行添加
        "RuijieVlanTrunkPortDynamicStatus": "1.3.6.1.4.1.4881.1.1.10.2.9.1.6.1.2",
    },

    "jnxExVlanPortGroupEntry":{
        # JUNIPER私有
        # 1.3.6.1.2.1 = iso.org.dod.internet.private.enterprise
        # 2636.3.40.1 = 2636.jnxMibs.jnxExMibRoot.jnxExSwitching
        # 5.1.7       = jnxExVlan.jnxVlanMIBObjects.jnxExVlanPortGroupTable
        # 1.5         = jnxExVlanPortGroupEntry.jnxExVlanPortAccessMode
        'jnxExVlanPortAccessMode' : '1.3.6.1.4.1.2636.3.40.1.5.1.7.1.5',
    },
}

OID实际为树状结构,比如1.3.6.1.4.1和1.3.6.1.4.2分别是1.3.6.1.4下面的两个树分支。SNMP获取数据一般为GET和WALK两种,GET需精确到树状结构的叶子节点级别,适合拉取CPU使用率这样的值;WALK则会对整个树状结构进行遍历,适合拉取整个ARP表或接口表。至于如何实现SNMP拉取,调用不同语言的SNMP包即可,比如GO的"github.com/soniah/gosnmp" 包、PYTHON的pysnmp包,不展开。

2.2 SSH

SSH用于远程管理,一般服务器/网络设备/存储设备都会实现。相信运维/开发对此协议都很熟悉,用于监控时,它可以直接输入系统命令从而获得监控数据输出。优点是一次就能获取大量的信息,缺点是交互不好控制和获取到的输出往往需要清洗处理。SSH示例如下。

//go version go1.14.6 windows/amd64
package main

import (
   "bytes"
   "fmt"
   "golang.org/x/crypto/ssh"
   "net"
   "time"
)

//建立ssh client
func InitClient(user, password, host string, port int)(*ssh.Client, error){
   var (
      auth  []ssh.AuthMethod
      addr  string
      clientConfig *ssh.ClientConfig
      client *ssh.Client
      err  error
   )
   // 获取认证method
   auth = make([]ssh.AuthMethod, 0)
   auth = append(auth, ssh.Password(password))
   clientConfig = &ssh.ClientConfig{
      User: user,
      Auth: auth,
      Timeout: 30 * time.Second,
      //需要验证服务端,不做验证返回nil就可以,编辑器点HostKeyCallback看源码
      HostKeyCallback: func(hostname string, remote net.Addr, key ssh.PublicKey) error {
         return nil
      },
   }
   addr = fmt.Sprintf("%s:%d", host, port)
   // 三次tcp dial防止失败
   for i:=0; i<3 ;i++{
      if client, err = ssh.Dial("tcp", addr, clientConfig); err == nil {
         return client, nil
      }
   }
   return nil,err
}

//建立SSH会话
func InitSession(client *ssh.Client) (*ssh.Session, error) {
   var (
      session *ssh.Session
      err  error
   )
   // 三次建立ssh连接防止失败
   for i:=0; i<3 ;i++{
      if session, err = client.NewSession(); err == nil {
         return session, nil
      }
   }
   return nil, err
}

//初始化SSH命令执行函数
func InitExcutor(client *ssh.Client,session *ssh.Session)( func(string) error, func() (string,string,error), error ){
   var err error
   printer :=func(err error){  //打印函数
      fmt.Printf("error in InitExcutor:%s",err)
   }
   if session==nil {  //如果没有传入session,重新建立session
      session, err=InitSession(client)
      if err != nil{
         printer(err)
         return nil,nil,err
      }
   }
   stdinBuf, err := session.StdinPipe()  //开启一个名为stdinBuf的stdin pipe,以stdinBuf.Write()模拟输入
   if err != nil{
      printer(err)
      return nil,nil,err
   }
   var outbt, errbt bytes.Buffer  //创建buffer
   session.Stdout = &outbt //buffer地址给stdout,即输出到outbt
   session.Stderr = &errbt
   err = session.Shell()  //开启shell,不然一个session只能执行一条命令
   if err != nil{
      printer(err)
      return nil,nil,err
   }

   excuteFunc:=func(cmd string) error{  //执行函数
      fmt.Printf("发送命令:%s\\n",cmd)
      _,err=stdinBuf.Write([]byte(cmd+"\\n"))
      return err
   }
   readRes:= func() (string,string,error) { //读取退出函数
      _,err=stdinBuf.Write([]byte("exit\\n"))  //发送exit以退出
      if err!=nil{return "","",err}
      //session.Wait()
      err=session.Wait()  //等到退出信号,一般要先发送exit才会收到这个信号,但发送exit后往往会自动关闭session,所以下面的session关闭仅为保证关闭
      if err!=nil{
         fmt.Printf("error happens when session wait :%s",err)
      }
      err=session.Close()
      if err!=nil{
         fmt.Printf("error happens when session close :%s",err)
      }
      return outbt.String(),errbt.String(),nil
   }
   return excuteFunc,readRes,nil
}

func main(){
   usr :="root"
   pswd:="Justjokeforyou"
   host:="120.139.213.44"
   port:= 22
   client,_ := InitClient(usr,pswd,host,port)
   sn,_ := InitSession(client)
   excute,exRead,_:=InitExcutor(client,sn)
   excute("ps -axu")
   out,_,_:=exRead()
   fmt.Printf("%v",out)
}

2.3 Telnet

类似于SSH,一般服务器/网络设备/存储设备都会实现。此处不展开。

2.4 HTTP/HTTPS

HTTP用于提供所谓API接口数据,以前的网络/存储设备很少有自带HTTP功能,但现在基本上都已经有HTTP功能可选。只需在设备上开启此功能,然后参看接口文档,调用对应接口即可取到相应的数据。但服务器安装centos,默认是没有内置HTTP功能的,需要自己挂个HTTP服务或者运行agent,才能提供HTTP服务。下面为仅列出使用HTTP如何构造Header,以及常用认证方式,具体如何取数据见API文档。

#常用header
commonHeaders = {
            'Accept' : "text/plain, text/ html",    #可接受的响应内容类型
            'Accept-Encoding' : "gzip, deflate, sdch",      #可接受的响应内容的编码方式
            'Accept-Language':"zh_CN,en",       #可接受的语言
            'Accept-Charset' : "utf-8",    #可接受的字符集
            'Authorization' : "",      #用于表示HTTP协议中需要认证资源的认证信息
            'Cache-Control' : "no-cache",   #用来指定当前的请求/回复中的,是否使用缓存机制
            'Connection' : "keep-alive",    #keepalive/Upgrade
            'Content-Type' : "application/json; charset=utf-8",     #请求体类型
            'Cookie': "",      #由之前服务器通过Set-Cookie设置的一个HTTP协议Cookie
            'Content-Length' : "348",     #以8进制表示的请求体的长度
            'Date' : "Tue, 15 Nov 2010 08:12:31 GMT",   #发送该消息的日期和时间
            'Expect': "100-continue",      #表示客户端要求服务器做出特定的行为
            'From': "[email protected]",     #发起此请求的用户的邮件地址
            'Host' : "{}:{}",   #表示服务器的域名以及服务器所监听的端口号。如果所请求的端口是对应的服务的标准端口(80),则端口号可以省略
            'Origin' : "http://www.baidu.com",     #发起一个针对跨域资源共享的请求
            'Pragma' : "no-cache",     #与具体的实现相关,这些字段可能在请求/回应链中的任何时候产生
            'Proxy-Authorization' : "",     #用于向代理进行认证的认证信息
            'Range' : "bytes=500-999",     #表示请求某个实体的一部分,字节偏移以0开始
            'Referer' : "http://www.baidu.com",      #表示浏览器所访问的前一个页面,可以认为是之前访问页面的链接将浏览器带到了当前页面。
            'User-Agent' : "Mozilla/5.0(Windows NT 6.1)AppleWebKit/537.36(KHTML, like Gecko)Chrome/38.0.2125.111Safari/537.36",     #浏览器的身份标识字符串
            'X-Requested-With' : "XMLHttpRequest",      #非标,通常在值为“XMLHttpRequest”时使用
}

def getHeaders(selectors=[], filters=[],  **kwargs):
    """获取一个定制的header"""
    if not selectors:
        selectors = [i for i in commonHeaders]
    if selectors and filters:
        diff = set(selectors).difference(set(filters))
        selectors = list(diff)
    tmpHeaders = {}
    for s in selectors:
        tmpHeaders[s] = commonHeaders[s]
    keys = kwargs.keys()
    if keys:
        for key in keys:
            tmpHeaders[key] = kwargs.get(key) or ""
    return tmpHeaders


def getNewHeaders(host=""):
    """获取一个新的headers"""
    selectors = [ 'Accept', 'Accept-Encoding', 'Accept-Language', 'Cache-Control', 'Connection',
                  'Content-Type', 'Host', 'User-Agent', 'X-Requested-With']
    tmpHeaders = getHeaders(selectors, [])
    if host != "":
         tmpHeaders["host"] = host
    return tmpHeaders


# HTTP basic auth,无状态,在每个请求里带user和password,类似下面,部分国外厂商默认用这个
import requests
usr,pwd = "root","root123"
requests.get(url=url,auth=(usr,pwd))

# 返回session的auth,一般认证信息放入data,大部分认证都类似这个
usr,pwd = "root","root123"
authurl = "www.xxx.com/login"
requests.post(url=authurl,data={'usr':usr,'pwd':pwd})

2.5 Syslog

Syslog用于传递日志信息,一般服务器/网络/存储设备都会具备此功能。Syslog有发送方和接收方,网络/存储设备一般为发送方,服务器一般为接受方。但服务器也可以配置成发送方,如centos一般都自带了rsyslog功能,可以根据需求配置成接收方/接受方,然后使用“service rsyslog restart”命令启动。

//centos配置syslog发送
[[email protected] etc]# cat -n rsyslog.conf 
    ...
    89  # remote host is: name/ip:port, e.g. 192.168.0.1:514, port optional
    90  #*.* @@remote-host:514
    91  # ### end of the forwarding rule ###
主要就是将90行的“#”去掉,然后将“*.* @@remote-host:514”换成日志服务器的ip,如“ *.*@@10.22.11.185:514 ”

//centos配置syslog接收
[[email protected] etc]# cat -n rsyslog.conf 
 
     6  #### MODULES ####
     7
     8  # The imjournal module bellow is now used as a message source instead of imuxsock.
     9  $ModLoad imuxsock # provides support for local system logging (e.g. via logger command)
    10  $ModLoad imjournal # provides access to the systemd journal
    11  #$ModLoad imklog # reads kernel messages (the same are read from journald)
    12  #$ModLoad immark  # provides --MARK-- message capability
    ...
    14  # Provides UDP syslog reception
    15  #$ModLoad imudp
    16  #$UDPServerRun 514
    ...
    18  # Provides TCP syslog reception
    19  #$ModLoad imtcp
    20  #$InputTCPServerRun 514
    ...
主要就是将上面的“#”去掉,其他如日志等级,日志到哪个文件夹可以后面再调整。

//cisco n9k配置syslog发送
logging server 111.99.36.82 4
logging server 111.99.36.86 4
logging server 19.1.9.212 16
logging source-interface Vlan2004
logging timestamp milliseconds
logging level daemon 2

//juniper配置syslog发送
set system syslog user * any emergency
set system syslog host 19.20.81.12 any info
set system syslog host 19.20.81.12 match "!LBCM-L2,pfe_bcm_l2_mac_add"
set system syslog host 19.20.81.12 log-prefix DCC-ITB-SW-14
set system syslog host 19.20.81.12 source-address 10.2.92.23
set system syslog host 19.21.131.134 any info
set system syslog host 19.21.131.134 match "!LBCM-L2,pfe_bcm_l2_mac_add"
set system syslog host 19.21.131.134 log-prefix DCC-ITB-SW-14
set system syslog host 19.21.131.134 source-address 10.2.92.23

3. 使用Agent时的数据获取

不使用Agent时,不必了解数据如何被收集。需要了解的是SNMP、SSH等协议的内容,而不需要了解这些协议的进程在被监控机上是如何从OS处收集数据的。但如果使用Agent获取数据,在动手写一个Agent之前,需了解Agent一般是怎么去从OS处收集数据的。通常地,Agent从OS收集数据有文件读取、命令行获取、其他系统调用三种方式。监控程序和Agent之间的沟通,可以自行使用任意协议,但一般地,会选用HTTP/HTTPS进行通信。

3.1 文件读取

读取的文件分为两种,系统文件和应用数据文件。系统文件读取的系统的运行数据,应用数据文件读取的是应用的运行数据。仅以系统文件举例,例如Linux系统的监控,大多可以靠读取/proc/目录下的文件实现。/proc/下文件对应的用途和含义详见笔者的另一篇文章《Linux Procfs (一) /proc/* 文件实例解析》

//下面的代码截取自open-falcon的agent实现,用的都是读取文件的方法获取数据。
//因为是部分截取函数用于说明,缺少引用部分,虽是源码但并不能直接运行。但相信读者读完可以自己写出类似的代码。

//centos返回Cpu的频率
func CpuMHz() (mhz string, err error) {
   f := "/proc/cpuinfo"  //被访问的文件路径,本质就是读取/proc/cpuinfo文件,然后做二次加工
   var bs []byte
   bs, err = ioutil.ReadFile(f)
   if err != nil {
      return
   }
   reader := bufio.NewReader(bytes.NewBuffer(bs))
   for {
      var lineBytes []byte
      lineBytes, err = file.ReadLine(reader)
      if err == io.EOF {
         return
      }
      line := string(lineBytes)
      if !strings.Contains(line, "MHz") {
         continue
      }
      arr := strings.Split(line, ":")
      if len(arr) != 2 {
         return "", fmt.Errorf("%s content format error", f)
      }
      return strings.TrimSpace(arr[1]), nil
   }
   return "", fmt.Errorf("no MHz in %s", f)
}

//centos返回内核最大的文件句柄数
func KernelMaxFiles() (uint64, error) {
   return file.ToUint64(Root() + "/proc/sys/fs/file-max") //被访问的文件路径
}

//centos返回内核已分配的文件句柄数
func KernelAllocateFiles() (ret uint64, err error) {
   var content string
   file_nr := Root() + "/proc/sys/fs/file-nr" //被访问的文件路径
   content, err = file.ToTrimString(file_nr)
   if err != nil {
      return
   }
   arr := strings.Fields(content)
   if len(arr) != 3 {
      err = fmt.Errorf("%s format error", file_nr)
      return
   }
   return strconv.ParseUint(arr[0], 10, 64)
}

//centos返回最大的进程号
func KernelMaxProc() (uint64, error) {
   return file.ToUint64(Root() + "/proc/sys/kernel/pid_max") //被访问的文件路径
}

//centos返回平均负载情况
func LoadAvg() (*Loadavg, error) {
   loadAvg := Loadavg{}
   data, err := file.ToTrimString(Root() + "/proc/loadavg") //被访问的文件路径
   if err != nil {
      return nil, err
   }
   L := strings.Fields(data)
   if loadAvg.Avg1min, err = strconv.ParseFloat(L[0], 64); err != nil {
      return nil, err
   }
   if loadAvg.Avg5min, err = strconv.ParseFloat(L[1], 64); err != nil {
      return nil, err
   }
   if loadAvg.Avg15min, err = strconv.ParseFloat(L[2], 64); err != nil {
      return nil, err
   }
   processes := strings.SplitN(L[3], "/", 2)
   if len(processes) != 2 {
      return nil, errors.New("invalid loadavg " + data)
   }
   if loadAvg.RunningProcesses, err = strconv.ParseInt(processes[0], 10, 64); err != nil {
      return nil, err
   }
   if loadAvg.TotalProcesses, err = strconv.ParseInt(processes[1], 10, 64); err != nil {
      return nil, err
   }
   return &loadAvg, nil
}

3.2 命令行获取

类UINX系统一般都有着类似的命令行,用于和系统进行直接交互。使用3.1节读取系统文件的方式,如读取上面/proc目录下的文件,如非对文件内容非常熟悉,往往不知道具体的数值含义,此时我们可以用平时常用的命令去取到易读性很高的内容。例如在centos中,我们可以调用某个成熟的包,利用"netstat -antup"命令快速获取所有连接信息;而在系统之上的应用层,这种方式也会有大量使用场景,比如调用成熟的包,利用“show status”,“show variables”这种命令获取MYSQL监控整体运行的各种指标和变量。

//centos获取socket信息
func SocketStatSummary() (m map[string]uint64, err error) {
   m = make(map[string]uint64)
   var bs []byte
   bs, err = sys.CmdOutBytes("sh", "-c", "ss -s") //调用命令行,相当于输入“sh -c ss -s”命令
   if err != nil {
      return
   }

   reader := bufio.NewReader(bytes.NewBuffer(bs))

   // ignore the first line
   line, e := file.ReadLine(reader)
   if e != nil {
      return m, e
   }

   for {
      line, err = file.ReadLine(reader)
      if err != nil {
         return
      }

      lineStr := string(line)
      if strings.HasPrefix(lineStr, "TCP") {
         left := strings.Index(lineStr, "(")
         right := strings.Index(lineStr, ")")
         if left < 0 || right < 0 {
            continue
         }

         content := lineStr[left+1 : right]
         arr := strings.Split(content, ", ")
         for _, val := range arr {
            fields := strings.Fields(val)
            if fields[0] == "timewait" {
               timewait_arr := strings.Split(fields[1], "/")
               m["timewait"], _ = strconv.ParseUint(timewait_arr[0], 10, 64)
               if len(timewait_arr) > 1 {
                            m["slabinfo.timewait"], _ = strconv.ParseUint(timewait_arr[1], 10, 64)
                    } else {
                            m["slabinfo.timewait"] = 0
                    }
               continue
            }
            m[fields[0]], _ = strconv.ParseUint(fields[1], 10, 64)
         }
         return
      }
   }

   return
}

----------------------------分割线--------------------------

//MySQL获取status值、variable值
package main
 
import (
    "fmt"
    _ "github.com/go-sql-driver/mysql"
    "github.com/jmoiron/sqlx"
)
 
var (
    userName  string = "root"
    password  string = "test123"
    ipAddrees string = "12.16.0.11"
    port      int    = 3306
    dbName    string = "test"
    charset   string = "utf8"
)
 
func connectMysql() (*sqlx.DB) {
    dsn := fmt.Sprintf("%s:%[email protected](%s:%d)/%s?charset=%s", userName, password, ipAddrees, port, dbName, charset)
    Db, err := sqlx.Open("mysql", dsn)
    if err != nil {
        fmt.Printf("mysql connect failed, detail is [%v]", err.Error())
    }
    return Db
}
  
func main() {
    var Db *sqlx.DB = connectMysql()
    defer Db.Close()
    result, _ := Db.Exec("show varibles")
    fmt.Printf("%v \\n %v",result)
    result, _ := Db.Exec("show varibles")
    fmt.Printf("%v \\n %v",result)
}

MySQL的show variables命令可以返回>500个变量的值,show status可以返回>400个状态值。这两个命令再配合对/proc/下MySQL进程所在文件夹的文件读取,即可完成80%以上的MySQL监控。

3.3 其他系统调用

本质上,3.1的读取文件、3.2的利用命令行也算系统调用。操作系统提供的其他调用可以在某些文档上查询到,还劳请读者自己去发现了。很多语言在实现的时候都有"os"这个包,里面封装了许多系统调用,我们可以利用这些封装的函数获取到很多系统信息,实现各层级的监控。

//centos获取系统变量

package nux
import (
   "os"
   "strings"
)

const nuxRootFs = "NUX_ROOTFS"

// Root 获取系统变量
func Root() string {
   root := os.Getenv(nuxRootFs)
   if !strings.HasPrefix(root, string(os.PathSeparator)) {
      return ""
   }
   root = strings.TrimSuffix(root, string(os.PathSeparator))
   if pathExists(root) {
      return root
   }
   return ""
}

func pathExists(path string) bool {
   fi, err := os.Stat(path)
   if err == nil {
      return fi.IsDir()
   }
   return false
}

4. 小结

  • 运维监控系统可按“有/无agent”、“使用pull/push获取数据”划分成6类。
  • Agent实际是一个轻量程序,用于提供系统无法直接提供的数据。
  • Pull相对复杂,Push相对简单,如果想从最基础的搭建起,选用push这种方式即可。
  • SNMP、SSH、HTTP、Syslog是常见的无agent获取数据方式,需要针对协议进行编程。
  • 使用Agent获取数据时,如果想自行编写Agent时,可以利用读取文件、命令行、其他系统调用来实现。

版权声明:
作者:晴日飞鸢
链接:https://jkboy.com/archives/16015.html
来源:随风的博客
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
海报
运维监控,如何获取数据?
运维如果想做自动化高效化,则少不了搭建监控系统。目前市面上已经有大量成熟、开源的监控平台可供挑选。但如果想实现一个监控系统,或了解监控系统的原理,则可参见本文。
<<上一篇
下一篇>>