运维如果想做自动化高效化,则少不了搭建监控系统。目前市面上已经有大量成熟、开源的监控平台可供挑选。但如果想实现一个监控系统,或了解监控系统的原理,则可参见本文。
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是最适合做小流量监控的协议,一般服务器/网络设备/存储设备都会实现。但此协议需要手动配置开启,简要的开启和测试过程如下。
代码语言:txt复制//centos启动SNMP服务(本机可以提供SNMP服务)
[root@localhost ~]# service snmpstart
//centos安装snmp工具包(本机可以拉取SNMP信息)
[root@localhost ~]# yum -y install net-snmp-utils
//已经安装好的SNMP工具命令
[root@localhost ~]# 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示例
[root@localhost ~]# 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
[root@localhost ~]# 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如下。
代码语言:txt复制#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示例如下。
代码语言:txt复制//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("发送命令:%sn",cmd)
_,err=stdinBuf.Write([]byte(cmd "n"))
return err
}
readRes:= func() (string,string,error) { //读取退出函数
_,err=stdinBuf.Write([]byte("exitn")) //发送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文档。
代码语言:txt复制#常用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': "user@itbilu.com", #发起此请求的用户的邮件地址
'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”命令启动。
代码语言:txt复制//centos配置syslog发送
[root@hecs-197747 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接收
[root@hecs-197747 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/* 文件实例解析》。
代码语言:txt复制//下面的代码截取自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监控整体运行的各种指标和变量。
代码语言:txt复制//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:%s@tcp(%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"这个包,里面封装了许多系统调用,我们可以利用这些封装的函数获取到很多系统信息,实现各层级的监控。
代码语言:txt复制//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时,可以利用读取文件、命令行、其他系统调用来实现。