NM3000网管系统核心架构之一:trap告警框架简介
1、告警概念
设备在发生变化(比如断纤/断电/上线)时,会向trapserver中配置的各ip地址发送trap,上报这些变化信息。
一般我们网管的ip都会配置到各设备的trapserver中,也就是说,设备发出的trap,网管是能够收到的。
网管根据收到的trap作出对应的响应,包括产生告警、清除告警等。
2、snmp4j简介
网管是利用snmp4j在指定端口监听SNMP消息。
2.1 重要类及接口
Snmp类
SNMP4J的最核心的类,负责SNMP报文的接受和发送。
PDU类和ScopedPDU类
该类是SNMP报文单元的抽象,其中PDU类适用于SNMPv1、SNMPv2。ScopedPDU类继承于PDU类,适用于SNMPv3。
Target接口和UserTarget类
对应于SNMP代理的地址信息,包括地址和端口号。其中Target接口适用于SNMPv1、SNMPv2。UserTarget类实现了UserTarget接口,适用于SNMPv3。
TransportMapping接口
该接口代表了SNMP4J所使用的传输层协议。这也是SNMP4J的一大特色的地方。按照RFC规定,SNMP是只使用UDP作为传输层协议。而SNMP4J支持管理端和代理端使用UDP或TCP进行传输。该接口有两个子接口。
CommandResponder接口
该接口接收PDU报文。实现了该接口的类,可以在processPdu方法中处理收到的报文。
2.2 两种消息发送模式
SNMP支持两种消息发送模式:同步发送模式和异步发送模式。
同步发送模式也称阻塞模式。当管理端发送出一条消息后,线程会被阻塞,直到收到对方的回应或者超时。
异步发送模式也称非阻塞模式。当管理端发出一条消息后,线程将会继续执行,当收到消息的回应的时候,程序会对消息做出相应的处理。要实现异步模式,需要实例化一个实现了ResponseListener接口的对象。ResponseListener接口有一个onResponse的函数。这是一个回调函数,当程序收到响应的时候,会自动调用该函数。由该函数完成对响应的处理。
2.3 监听trap的步骤
明确SNMP在传输层所使用的协议
一般情况下,我们使用UDP协议作为SNMP的传输层协议,所以需要实例化一个DefaultUdpTransportMapping接口对象。实例化SNMP对象
我们需要将实例化的DefaultUdpTransportMapping接口对象作为参数,构造SNMP对象。
另外,如果实现SNMPv3协议,还需要设计安全机制,添加安全用户等。监听SNMP消息
实例化实现CommandResponder接口的对象,设置为snmp对象的消息处理器,以后监听到的PDU报文由接口的processPdu方法处理。
SNMP对象开启监听。
3、网管如何收到Trap
3.1 监听调度示意图
3.2 获取server ip + port
3.3 下发给engine,启动trap监听
将TrapCallBack接口的实现类(TrapServiceImpl)添加到SnmpTrapManager的监听集合中。
3.4 实际监听类
SnmpTrapManager实现了CommandResponder接口,trap上来后会自动进入processPdu中处理。
3.5 小结
关于trap是如何从设备上到网管系统的,我们不必太过于关心,基本上都是snmp4j的封装实现。只需要大概知道是如何实现监听的即可。最重要的还是网管处理trap的过程。
4、 网管处理Trap流程
整个trap的处理流程较为复杂,我们把它分解为几个阶段:
- snmpTrapManager处理trap,发送给server
- trapparser将trap转成event
- eventparser根据event生成告警或者清除告警
4.1 SnmpTrapManager处理trap,发送给server
为何使用多线程消费缓存的trap队列
最开始的模型是一个长度为1024的队列缓存接收到的trap,一个线程不断的处理这些trap。
后来在珠江数码实网中发生了trap丢失的问题,经过抓取log日志发现,由于华为的部分OLT不断的向我们网管发送trap,平均每天收到43W条trap。而单线程的处理速度不够,在接收到trap频率达到一定程度后,导致缓存队列达到上限,丢失了部分trap。为了提升我们trap接收端的处理能力,需要改为多线程来进行处理。多线程处理注意事项
每个ip发过来的trap处理顺序必须保证,因为告警间存在清除关系(断电/上线)。
trap缓存数据模型
1
Map<String, ConcurrentTrapQueue> deviceTraps
以设备IP为key,该设备上报的trap都存在单独的ConcurrentTrapQueue中。
ConcurrentTrapQueue
这个自定义非阻塞队列非常关键,它继承自ConcurrentLinkedQueue,使用锁的机制来确保同时只有一个线程能操作队列数据,从而保证了trap的处理顺序。
因为如果同时可以从队列里面取出多条trap进行处理,后面的trap就可能先处理完成,从而破坏了顺序,导致告警无法产生或者告警无法清除等bug。ConcurrentLinkedQueue使用了可重入锁ReentrantLock,来保证在一个trap处理完成前,该队列数据不能被其他线程取出。具体实现机制为:
1、判断当前队列是否被锁住,如果被锁住,表明有其他线程在处理该队列,不做尝试,直接返回。
2、如果没有被锁住,先加锁,保证处理期间其他线程不能操作队列。
3、取出队列中所有的trap,一个一个发送给server
4、释放锁,使得其他线程可以访问该队列。
线程池-ThreadPoolExecutor
ConcurrentTrapQueue提供了支持并发的缓冲数据结构,我们还需要执行并发的线程池。我们使用executor框架提供的ThreadPoolExecutor来达到目的。
实现机制:
在SnmpTrapManager初始化时,创建线程池(线程池的创建),并不断的将各IP对应的ConcurrentTrapQueue丢到线程池中去执行。如何进行循环,一遍遍的将ConcurrentTrapQueue丢到线程池中去执行,这里使用了JAVA同步机制中非常重要的wait/notify机制。
wait/notify机制的简单介绍
JAVA中每个对象都有一个monitor对象(监视器)对象。线程获取了monitor对象,就获取了对应JAVA对象的锁。其他线程就会进入锁对应的等待队列/阻塞队列中。等待持有锁的线程释放锁,通知这些等待的线程去争抢锁。 Object.wait():在对应对象上等待,等待其他持有该对象的线程通知唤醒 Object.notify()/notifyAll():通知唤醒在该对象上wait的一个线程/所有线程
这是处理消费线程,每处理完一轮,最多等1s。如果有新来的trap,及时进行响应处理。
在接收trap端,每收到一条trap,就通知消费线程立即处理。
如何发送给server端
实现机制大致如下:
在server端的trapServiceImpl调用engine端的TrapFacadeImpl来开始监听的时候,就注册在发送trap时应该调用的回调函数callback类。从而对每一条trap,就知道怎样进行发送。代码见上面的3.3小节。
4.2 trapparser将trap转成event
不同设备的trap有不同的特征,在转成event时也需要不同的处理。所以,按照设备类型进行划分为几个不同的trapParser,分别用来处理不同设备类型的trap。
那么,如何让trap被正确的trapParser处理呢?这里使用的方式时,遍历调用每个trapParser的parse方法,命中则返回false,告知已经找到正确trapParser,遍历停止。如果返回true,表示trap不应被该trapParser处理,继续寻找。解析顺序:cos值越小越靠前。
我们可以关注下这种设计模式,它有几点值得我们学习:
- 将不同设备类型的trap处理封装成独立的类,避免难以维护的trap处理类。
- 采用各trapParser注册的模式,避免在分发trap给不同trapParser时硬编码各trap类。
- 巧妙使用parse()接口,不能处理的返回true,继续遍历寻找合适的trapParser,自己能处理的trap返回false停止遍历。而不是if-else或者switch这种硬编码。
TrapParser.parser()区分机制:
设备上报的trap,1.3.6.1.6.3.1.1.4.1.0 这个节点标识trap的类型,区分是哪一种alarm还是event
OLT发送的trap,1.3.6.1.4.1.17409.2.2.11.1.2.4.0 标识trap的code
CCMTS发送的trap,1.3.6.1.2.1.69.1.5.8.1.6.0 标识trap的code
结合这些节点,根据业务逻辑的需要,判断上报的trap是否应该被当前trapParser解析。
Trap如何转成event
EventSend
EventSender是事件event分发器,它也采用了队列缓冲加处理线程消费队列的模式来解耦做什么(分发event)和怎么做(怎么分发)。
以单例模式创建EventSend
循环从event队列中消费,发送event
insertEvent是不是很熟悉,eventParser的实现策略跟前面的trapParser是一致的,唯一不同的这里面符合条件的返回true,跟trapParser是反过来的。不是很清楚为什么弄成相反的。
4.3 eventparser根据event生成告警或者清除告警
最顶层的是抽象类EventParser,提供了一些抽象方法让子类实现,也提供了最重要的doEvent()方法,实现了几乎所有event都需要经历的处理逻辑。
EventParser和TrapParser有个最重要的区别在于:
TrapParser目前基本定死了有4种,如果某条trap一个都不符合,就会被丢弃,不处理。
EventParser不一样,有太多种不同的event,所以这里面的设计策略是自定义的EventParser按照一定的优先级排号顺序依次执行,匹配上就结束。如果没有跟自定义的EventParser匹配上,就会调用默认实现的DefaultEventParser去处理,起到一个兜底的作用。
我们一起看看如何实现eventParser的优先级排序机制: eventPriority
自定义的eventParser只需要将自己的cos设置为大于0即可。
潜在隐患:
有些eventParser没有修改默认值,就为0。而defaultEventParser也没有修改默认值,也是0。这样,就没有办法保证defaultEventParser一定是在最后,建议将defaultEventParser的cos设置为-1。
4.4 EventParser
EventParser主要用于拿event跟数据库中的alert进行比对,来判断是否生成/清除告警。而操作数据库属于I/O操作,可能会被阻塞住。所以与trapParser不同,eventParser使用了队列缓冲+消费线程的模式来处理。parse()方法将event存入缓冲队列,消费线程不断取出event,进行处理。
4.5 doEvent
这个方法非常关键,理解它是理解告警触发机制的核心。
我们先来了解一些基本概念:
事件event与告警alert的关系存放在event2alert表中,并且:
每一个告警事件都可能对应多种网管侧的告警类型(eventType),生成多条告警(一般产生告警只会对应一个)
每一个清除事件都可能清除多种告警类型(eventType)的告警
针对每一个event,找到与之对应的所有alertType,针对每一个alertType,查找系统中当前是否有对应的alert。由此hyansheng衍生出以下4种情况:
- 数据库中存在告警,当前event是清除事件
- 数据库中存在告警,当前event是产生事件
- 数据库中不存在告警,当前event是清除事件
- 数据库中不存在告警,当前event是产生事件