NM3000告警框架简介

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的步骤

  1. 明确SNMP在传输层所使用的协议
    一般情况下,我们使用UDP协议作为SNMP的传输层协议,所以需要实例化一个DefaultUdpTransportMapping接口对象。

  2. 实例化SNMP对象
    我们需要将实例化的DefaultUdpTransportMapping接口对象作为参数,构造SNMP对象。
    另外,如果实现SNMPv3协议,还需要设计安全机制,添加安全用户等。

  3. 监听SNMP消息
    实例化实现CommandResponder接口的对象,设置为snmp对象的消息处理器,以后监听到的PDU报文由接口的processPdu方法处理。
    SNMP对象开启监听。

3、网管如何收到Trap

3.1 监听调度示意图

监听调度示意图

3.2 获取server ip + port

获取server ip + port

3.3 下发给engine,启动trap监听

下发给engine,启动trap监听

将TrapCallBack接口的实现类(TrapServiceImpl)添加到SnmpTrapManager的监听集合中。

此处输入图片的描述

3.4 实际监听类

SnmpTrapManager实现了CommandResponder接口,trap上来后会自动进入processPdu中处理。

此处输入图片的描述

3.5 小结

关于trap是如何从设备上到网管系统的,我们不必太过于关心,基本上都是snmp4j的封装实现。只需要大概知道是如何实现监听的即可。最重要的还是网管处理trap的过程。

4、 网管处理Trap流程

网管处理流程图

整个trap的处理流程较为复杂,我们把它分解为几个阶段:

  1. snmpTrapManager处理trap,发送给server
  2. trapparser将trap转成event
  3. 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。

trapParser

那么,如何让trap被正确的trapParser处理呢?这里使用的方式时,遍历调用每个trapParser的parse方法,命中则返回false,告知已经找到正确trapParser,遍历停止。如果返回true,表示trap不应被该trapParser处理,继续寻找。解析顺序:cos值越小越靠前。

此处输入图片的描述

我们可以关注下这种设计模式,它有几点值得我们学习:

  1. 将不同设备类型的trap处理封装成独立的类,避免难以维护的trap处理类。
  2. 采用各trapParser注册的模式,避免在分发trap给不同trapParser时硬编码各trap类。
  3. 巧妙使用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

    TRAP转成Event

  • EventSend

    EventSender是事件event分发器,它也采用了队列缓冲加处理线程消费队列的模式来解耦做什么(分发event)和怎么做(怎么分发)。

    以单例模式创建EventSend

    单例模式创建EventSend

    单例模式创建EventSend

    循环从event队列中消费,发送event

    此处输入图片的描述

    此处输入图片的描述

    insertEvent是不是很熟悉,eventParser的实现策略跟前面的trapParser是一致的,唯一不同的这里面符合条件的返回true,跟trapParser是反过来的。不是很清楚为什么弄成相反的。

4.3 eventparser根据event生成告警或者清除告警

最顶层的是抽象类EventParser,提供了一些抽象方法让子类实现,也提供了最重要的doEvent()方法,实现了几乎所有event都需要经历的处理逻辑。

EventParser

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种情况:

  1. 数据库中存在告警,当前event是清除事件
  2. 数据库中存在告警,当前event是产生事件
  3. 数据库中不存在告警,当前event是清除事件
  4. 数据库中不存在告警,当前event是产生事件

doEvent逻辑

坚持原创技术分享,您的支持将鼓励我继续创作!
0%