查看源代码 高级代理主题

高级代理主题 章节介绍了 SNMP 开发工具中更高级的代理相关特性。涵盖以下主题:

  • 何时使用子代理
  • 代理语义
  • 子代理和依赖关系
  • 分布式表
  • 容错
  • 使用 Mnesia 表作为 SNMP 表
  • 审计跟踪日志
  • 与标准的偏差

何时使用子代理

何时使用子代理 部分描述了加载和卸载 MIB 的机制不足的情况。在这些情况下,需要子代理。

特殊的 Set 事务机制

每个子代理都可以实现自己的 setgetget-next 机制。例如,如果应用程序要求 get 机制是异步的,或者需要 N 阶段的 set 机制,则应使用专门的子代理。

该工具包允许同时使用不同类型的子代理。因此,不同的 MIB 可以具有不同的 setget 机制。

进程通信

一个简单的分布式代理可以在没有子代理的情况下进行管理。仪表函数可以使用分布式 Erlang 与应用程序的其他部分进行通信。但是,如果这产生过多不必要的流量,则可以在每个节点上使用子代理。子代理按传入的 SNMP 请求(而不是每个变量)处理请求。因此,网络流量被最小化。

如果仪表函数与 UNIX 进程通信,则最好使用特殊的子代理。此子代理在一个数据包中将 SNMP 请求发送到另一个进程,以最大限度地减少上下文切换。例如,如果整个 MIB 在 UNIX 中以 C 级别实现,但您仍然想使用 Erlang SNMP 工具,那么您可以有一个特殊的子代理,它将请求中的变量作为单个操作发送到 C。

频繁加载 MIB

加载和卸载 MIB 是相当廉价的操作。但是,如果应用程序非常频繁地执行此操作(可能每分钟几次),它应该将 MIB 一劳永逸地加载到子代理中。此子代理仅在另一个代理下注册和注销自身,而不是每次都加载 MIB。这比加载 MIB 更便宜。

与其他 SNMP 代理工具包的交互

如果 SNMP 代理需要与在另一个包中构建的子代理进行交互,则应使用特殊的子代理,该子代理通过另一个包指定的协议进行通信。

代理语义

代理可以配置为多线程,一次处理一个传入请求,或者启用请求限制(这可以用于负载控制或限制 DoS 攻击的影响)。如果是多线程的,读取请求(getget-nextget-bulk)和陷阱会与彼此和 set 请求并行处理。但是,所有 set 请求都是串行化的,这意味着如果代理正在等待应用程序完成复杂的写操作,它将不会处理任何新的写请求,直到此操作完成。它会并发处理读取请求和发送陷阱。不并行处理写入请求的原因是,即使在最简单的情况下也需要复杂的锁定机制。即使使用上述方案,用户也必须小心,不要违反 set 请求是原子的这一原则。如果很难做到这一点,请不要使用多线程功能。

请求内的顺序是未定义的,变量不是按定义的顺序处理的。不要假设 PDU 中的第一个变量将在第二个变量之前处理,即使代理按此顺序处理变量也是如此。甚至不能假设属于不同子代理的请求具有任何顺序。

如果管理器尝试在同一 PDU 中多次设置同一变量,代理可以自由发挥。没有定义决定仪表函数将被调用一次还是两次。如果只调用一次,则没有定义确定将提供哪个新值。

当代理收到请求时,它会在发送响应后保留请求 ID 一秒钟。如果代理在此期间从同一 IP 地址和 UDP 端口收到另一个具有相同请求 ID 的请求,则该请求将被丢弃。此机制与函数 snmpa:current_request_id/0 无关。

子代理和依赖关系

该工具包支持使用不同类型的子代理,但不支持构建子代理。

此外,该工具包不支持子代理之间的依赖关系。根据定义,子代理应该是独立的,因此在它们之间创建依赖关系不是一个好的设计。

分布式表

在更复杂的系统中,一个常见的情况是表中的数据是分布式的。不同的表行在不同的位置实现。一些 SNMP 工具包为表的每个部分指定一个 SNMP 子代理,并将相应的 MIB 加载到所有子代理中。主代理负责将分布式表作为单个表呈现给管理器。提供的工具包使用不同的方法。

使用此 SNMP 工具实现分布式表的方法是实现一个表协调器进程,负责协调保存表数据的进程,这些进程称为表持有者。所有表持有者必须以某种方式被协调器知晓;表数据的结构决定了如何实现这一点。协调器可能要求表持有者显式注册自己并指定其信息。在其他情况下,表持有者可以在编译时确定一次。

当调用分布式表的仪表函数时,应将请求转发到表协调器。协调器在表持有者中查找请求的信息,然后将答案返回给仪表函数。SNMP 工具包不包含对表协调的支持,因为这必须独立于实现。

将表协调器与 SNMP 工具分离的优点是

  • 我们不需要为每个表持有者使用子代理。通常,需要子代理来处理通信,但在分布式 Erlang 中,我们使用普通的消息传递。
  • 最有可能的是,已经存在某种类型的表协调器。此进程应处理表的仪表函数。
  • 用于呈现分布式表的方法在很大程度上取决于应用程序。不同的掩码技术的使用仅对一小部分问题有效,并且在分布式表中注册每一行会使其变为非分布式。

容错

SNMP 代理工具包从三个不同的来源获取输入

  • 来自网络的 UDP 数据包
  • 用户定义的仪表函数的返回值
  • 来自 MIB 的返回值。

代理具有很高的容错能力。如果管理器从代理收到意外响应,则可能是某些仪表函数返回了错误值。即使仪表函数崩溃,代理也不会崩溃。应该注意的是,如果仪表函数进入无限循环,代理也会永远被阻塞。主管或应用程序指定如何重新启动代理。

在分布式环境中使用 SNMP 代理

在分布式环境中使用代理的通常方法是在一个节点上使用一个主代理,并在其他节点上使用零个或多个子代理。但是,此配置使主代理节点成为单点故障。如果该节点发生故障,代理将无法工作。

解决此问题的一种方法是将 snmp 应用程序设为分布式 Erlang 应用程序,这意味着可以配置代理以在多个节点之一上运行。如果它运行所在的节点发生故障,则另一个节点会重新启动代理。这称为故障转移。当节点再次启动时,它可能会接管应用程序。此问题解决方案带来了另一个问题。通常,新节点具有与第一个节点不同的 IP 地址,这可能会导致 SNMP 管理器和代理之间的通信出现问题。

如果 snmp 代理配置为分布式 Erlang 应用程序,它将在接管期间尝试加载旧节点上加载的相同 MIB。它使用与旧节点相同的文件名。如果 MIB 未位于不同节点的相同路径中,则必须在接管后显式加载 MIB。

使用 Mnesia 表作为 SNMP 表

Mnesia DBMS 可用于存储 SNMP 表的数据。这意味着 SNMP 表可以实现为 Mnesia 表,并且可以通过 SNMP 使 Mnesia 表可见。此映射在很大程度上是自动化的。

使用此映射主要有三个原因

  • 我们获得了 Mnesia 的所有功能,例如容错、持久数据存储、复制等。
  • 涉及的大部分工作都是自动化的。这包括 get-next 处理和 RowStatus 处理。
  • 该表可以用作普通的 Mnesia 表,在应用程序内部使用 Mnesia API,同时可以通过 SNMP 可见。

使用此映射时,在原始 Mnesia 表中插入和删除的速度较慢,因子为 O(log n)。读取访问不受影响。

将 SNMP 表格实现为 Mnesia 表格的一个缺点是,内部资源被迫使用 MIB 中的表格定义,这意味着外部数据模型必须在内部使用。实际上,这只是部分正确。Mnesia 表格可以扩展 SNMP 表格,这意味着 Mnesia 表格可以有内部使用的列,这些列对于 SNMP 是不可见的。尽管如此,SNMP 的数据模型必须保持。虽然这并不理想,但在许多情况下,简单高效的实现优先于抽象,这是一种务实的折衷方案。

创建 Mnesia 表格

表格必须在管理器可以使用它之前在 Mnesia 中创建。表格必须声明为 snmp 类型。这使得表格按照 SNMP 的字典排序规则进行排序。Mnesia 表格的名称必须与 SNMP 表格名称相同。必须指定相应 SNMP 表格中 INDEX 字段的类型。

如果 SNMP 表格有多个 INDEX 列,则相应的 Mnesia 行是一个元组,其中第一个元素是一个包含 INDEX 列的元组。通常,如果 SNMP 表格有 N 个 INDEX 列和 C 个数据列,则 Mnesia 表格的元数为 (C-N)+1,其中键是一个元数为 N 的元组(如果 N > 1),或者一个单项(如果 N = 1)。

有关如何将 Mnesia 表格声明为 SNMP 表格的信息,请参阅 Mnesia 用户指南。

以下示例说明了一种情况,我们希望将 SNMP 表格实现为 Mnesia 表格。该表格存储公司员工的信息。每位员工都以部门编号和姓名作为索引。

       empTable OBJECT-TYPE
              SYNTAX      SEQUENCE OF EmpEntry
              ACCESS      not-accessible
              STATUS      mandatory
              DESCRIPTION
                      "A table with information about employees."
       ::= { emp 1}
       empEntry OBJECT-TYPE
              SYNTAX      EmpEntry
              ACCESS      not-accessible
              STATUS      mandatory
              DESCRIPTION
                 ""
              INDEX      { empDepNo, empName }
       ::= { empTable 1 }
       EmpEntry ::=
              SEQUENCE {
                  empDepNo         INTEGER,
                  empName          DisplayString,
                  empTelNo         DisplayString,
                  empStatus        RowStatus
              }

相应的 Mnesia 表格指定如下

mnesia:create_table([{name, employees},
                     {snmp, [{key, {integer, string}}]},
                     {attributes, [key, telno, row_status]}]).

注意

在 Mnesia 表格中,两个键列存储为包含两个元素的元组。因此,表格的元数为 3。

检测函数

上一节中显示的 MIB 表格可以按如下方式编译

1> snmpc:compile("EmpMIB", [{db, mnesia}]).

这就是全部需要做的!现在管理器可以读取、添加和修改行。此外,您可以使用普通的 Mnesia API 从您的程序访问表格。唯一的显式操作是创建 Mnesia 表格,这是一个用户为了创建所需的表格模式而必须执行的操作。

添加自己的操作

通常需要在修改表格时执行一些特定的操作。这可以通过检测函数来实现。它在设置表格时执行一些特定的代码,并将所有其他请求传递给预定义的函数。

以下示例说明了这个想法

emp_table(set, RowIndex, Cols) ->
    notify_internal_resources(RowIndex, Cols),
    snmp_generic:table_func(set, RowIndex, Cols, {empTable, mnesia});
emp_table(Op, RowIndex, Cols) ->
    snmp_generic:table_func(Op, RowIndex, Cols, {empTable, mnesia}).

默认的检测函数在模块 snmp_generic 中定义。详细信息请参阅参考手册,SNMP 部分,模块 snmp_generic

扩展 Mnesia 表格

表格可能包含在内部使用,但不应被管理器看到的列。这些内部列必须是表格中的最后一列。set 操作将无法使用此安排,因为存在代理不知道的列。这种情况通过在 set 函数中添加内部列的值来处理。

为了说明这一点,假设我们将 Mnesia 的 empTable 扩展一列内部列。我们像以前一样创建它,但通过添加另一个属性使其元数为 4。

mnesia:create_table([{name, employees},
                     {snmp, [{key, {integer, string}}]},
                     {attributes, {key, telno, row_status, internal_col}}]).

最后一列是内部列。执行 set 操作(创建行)时,我们必须为内部列提供一个值。检测函数现在如下所示

-define(createAndGo, 4).
-define(createAndWait, 5).

emp_table(set, RowIndex, Cols) ->
  notify_internal_resources(RowIndex, Cols),
  NewCols =
    case is_row_created(empTable, Cols) of
      true -> Cols ++ [{4, "internal"}]; % add internal column
      false -> Cols                      % keep original cols
  end,
  snmp_generic:table_func(set, RowIndex, NewCols, {empTable, mnesia});
emp_table(Op, RowIndex, Cols) ->
  snmp_generic:table_func(Op, RowIndex, Cols, {empTable, mnesia}).

is_row_created(Name, Cols) ->
  case snmp_generic:get_status_col(Name, Cols) of
    {ok, ?createAndGo} -> true;
    {ok, ?createAndWait} -> true;
    _ -> false
  end.

如果创建了一行,我们总是将内部列设置为 "internal"

与标准的偏差

在某些方面,代理并未完全实现 SNMP。以下是差异之处

  • 默认函数和 snmp_generic 不能将 NetworkAddress 类型的对象作为 INDEX 处理(仅限 SNMPv1!)。请改用 IpAddress
  • 代理不会检查为 INTEGER 对象指定的复杂范围。在这些情况下,它仅检查该值是否在指定的最小值和最大值范围内。例如,如果范围指定为 1..10 | 12..20,则代理会允许 11 通过,但不允许 0 或 21。检测函数必须自行检查复杂范围。
  • 代理永远不会生成 wrongEncoding 错误。如果变量绑定编码错误,则 asn1ParseError 计数器将递增。
  • SNMPv1 数据包中的 tooBig 错误将始终在所有变量绑定中使用 'NULL' 值。
  • 默认函数和 snmp_generic 不会检查从 OCTET STRING 派生的文本约定中每个 OCTET 的范围,例如 DisplayStringDateAndTime。这必须在重载的 is_set_ok 函数中进行检查。