[Из песочницы] Скрипт нагрузочного тестирования для проверки соответствия текущих параметров каналов связи заявленным

habr.png

Проблематика


В организациях, использующих арендованные у операторов связи каналы передачи данных VPN/Интернет для построения корпоративной филиальной сети, рано или поздно может возникнуть ситуация, когда внезапно выясняется, что каналы до некоторых удаленных подразделений не соответствуют заявленным требованиям по полосе пропускания, либо при незначительной загрузке каналов появляются потери, влияющие на качество работы сетевых сервисов.

При этом система мониторинга исправно мониторит доступность каналов, потери на них, задержку, но вследствие того, что каналы связи не всегда загружены трафиком, особенно резервные каналы, она не может своевременно выявить все отклонения их параметров от согласованных с операторами связи. Для таких целей требуется проведение периодического нагрузочного тестирования каналов, в результате которого производится проверка потерь на канале при одновременной его загрузке трафиком, утилизирующим канал практически до максимальных значений полосы пропускания, с последующим контролем объема принятого трафика удаленным маршрутизатором. Хочу поделиться своими наработками в части автоматизации данного процесса.

Идея


Идея заключается в разработке и периодическом использовании скрипта, который запускается в моем случае на Linux сервере в центральной точке корпоративной сети. Подразумевается, что скрипт взаимодействует с маршрутизаторами, на которых:

  • настроен протокол SNMP, одинаковые его версии и одинаковые community;
  • параметры bandwidth в настройках каналообразующих интерфейсов соответствуют заявленным операторами связи значениям полосы пропускания каналов.


На вход скрипта в качестве параметра поступает файл с ip адресами каналообразующих интерфейсов маршрутизаторов, каналы связи которых мы должны проверить. Количество строк файла не ограничивается каким-либо определенным значением. После последовательного считывания из входного файла соответствующего ip адреса скрипт:

  1. Запускает фоновый процесс генерации трафика в сторону считанного ip адреса, утилизирующий полосу пропускания тестируемого канала менее, чем на 100%, дабы полностью не загрузить канал. Процесс генерации трафика я ограничил 2 минутами с полосой генерируемого трафика 85% от максимальной полосы канала.
  2. Спустя несколько секунд после начала генерации трафика запрашивает по протоколу SNMP текущее значение принятых байт для интерфейса маршрутизатора, имеющего указанный ip адрес. Запоминает текущее время t0.
  3. Запускает серию из 60-ти ICMP пакетов. Фиксирует количество потерь.
  4. Повторно запрашивает по протоколу SNMP значение принятых байт этого же интерфейса. Запоминает текущее время t1. Вычисляет продолжительность замера: t1-t0.
  5. Вычисляет количество принятых интерфейсом байт в результате замера, на основании которого определяет полосу загрузки интерфейса входящим трафиком в момент тестирования. Хочу обратить внимание, что не весь генерируемый скриптом трафик может быть доставлен до маршрутизатора. Это возможно в случае несоответствия полосы пропускания канала заявленной. Подобные факты скрипт и должен выявлять.
  6. Выводит в качестве результата значения:
    • отношение полосы доставленного трафика к полосе сгенерированного трафика,
    • количество потерь на канале и
    • результат соответствия параметров канала заявленным, который формируется на основании предыдущих двух значений.


Особенности реализации


Генератор трафика


В качестве генератора трафика используется программа TCPBLAST/UDPBLAST, исходные коды которой размещены по ссылке: TCPBLAST/UDPBLAST

Эта программа как нельзя лучше подошла на роль генератора трафика для указанной задачи, т.к. помимо возможности указания в виде параметров командной строки полосы генерируемого трафика она обладает функционалом по запуску генерации трафика с лимитом по времени работы, что позволяет избежать рисков загрузки каналов связи в течение длительного времени в случае возникновения непредвиденных ситуаций, например, когда скрипт аварийно завершит свою работу.

Обнаружение SNMP индексов интерфейсов маршрутизаторов для тестируемых каналов


Скрипт получает на входе только ip адрес интерфейса маршрутизатора. Этого вполне достаточно, чтобы определить SNMP индекс интерфейса.

Используемый для этих целей OID: .1.3.6.1.2.1.4.20.1.2

Обнаружение полосы пропускания тестируемых каналов


Bandwidth интерфейса и прочие параметры определяются с помощью соответствующих OID с 
подстановкой к ним SNMP индекса интерфейса.

Код скрипта


#!/usr/bin/perl
use strict;
use warnings;
use POSIX qw(strftime);
use SNMP;

die "Usage: $0 " if ($#ARGV < 0);
my $community = 'community';

### pingLoss function ###
sub pingLoss {
  my ($param) = @_;
  my @result = `ping $param`;
  foreach my $str (@result) {
    if ($str =~ /(\d+)%/ && $1 < 100) {
      return $1;
    }
  }
  return 100;
}

### stressTest function for one host ### sub stressTest {
  my $ifIp = shift;

  my $ifIndex;
  my $ifName = "";
  my $ifAlias = "";
  my $ifSpeed = "";
  my $ifInOctets;
  my $ifInOctetsBegin;
  my $ifInOctetsEnd;
  my $sendBytes;

  my $sysName = "";
  my $testPeriod;
  my $pingLossCount = "";
  my $inOutPercent = "";
  my $testStatus;

  my $nowDateTime = strftime "%Y.%m.%d %H:%M:%S", localtime;

  if (&pingLoss("$ifIp -c 3 -i 0.2 -W 2") > 90) {
    print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\tping error\n";
    return 0;
  }

  my $sess = new SNMP::Session(DestHost => "$ifIp:161",
                              Community => $community,
                                Version => "2c",
                          NonIncreasing => 1,
                           UseLongNames => 1,);

  $sysName = $sess->get('.1.3.6.1.2.1.1.5.0');
  if ($sess->{ErrorNum}) {
    print "$nowDateTime\t$ifIp\t\t$ifName\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\tsnmp sysName error\n";
    return 0;
  }
  if ($sysName =~ m/^([\w_-]+)\./) {
    $sysName = $1;
  }

  $ifIndex = $sess->get('.1.3.6.1.2.1.4.20.1.2.' . $ifIp);
  if ($sess->{ErrorNum}) {
    print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\tsnmp ifIndex error\n";
    return 0;
  }

  $ifName = $sess->get('.1.3.6.1.2.1.31.1.1.1.1.' . $ifIndex);
  if ($sess->{ErrorNum}) {
    print "$nowDateTime\t$ifIp\t$sysName\t\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\tsnmp ifName error\n";
    return 0;
  }

  $ifAlias = $sess->get('.1.3.6.1.2.1.31.1.1.1.18.' . $ifIndex);
  if ($sess->{ErrorNum}) {
    print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t\t$ifSpeed\t$inOutPercent\t$pingLossCount\tsnmp ifAlias error\n";
    return 0;
  }

  $ifSpeed = $sess->get('.1.3.6.1.2.1.2.2.1.5.' . $ifIndex);
  if ($sess->{ErrorNum}) {
    print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t$ifAlias\t\t$inOutPercent\t$pingLossCount\tsnmp ifSpeed error\n";
    return 0;
  }

  if ($ifSpeed > 30000000) {
    print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\tHigh speed channel\n";
    return 0;
  }

  my $sendBytesSec = $ifSpeed * 0.85 / 8;
  system("killall udpblast > /dev/null 2> /dev/null");
  system("udpblast -c 1000000 --rate $sendBytesSec,100s $ifIp > /dev/null 2> /dev/null &");
  sleep(5);

  my $testBegin = strftime "%s", localtime;

  $ifInOctetsBegin = $sess->get('.1.3.6.1.2.1.2.2.1.10.' . $ifIndex);
  if ($sess->{ErrorNum}) {
    print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\tsnmp ifInOctetsBegin error\n";
    return 0;
  }

  $pingLossCount = &pingLoss("$ifIp -c 60 -W 1.5");
  system("killall udpblast > /dev/null 2> /dev/null");

  $ifInOctetsEnd = $sess->get('.1.3.6.1.2.1.2.2.1.10.' . $ifIndex);
  if ($sess->{ErrorNum}) {
    print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\tsnmp ifInOctetsEnd error\n";
    return 0;
  }

  my $testEnd = strftime "%s", localtime;
  $testPeriod = $testEnd - $testBegin;
  $ifInOctets = $ifInOctetsEnd - $ifInOctetsBegin;
  $sendBytes = $sendBytesSec * $testPeriod * 1.04;
  $inOutPercent = sprintf("%d", $ifInOctets * 100 / $sendBytes);
  $testStatus = $inOutPercent > 89 && $pingLossCount < 6 ? "Good" : "Bad";

  print "$nowDateTime\t$ifIp\t$sysName\t$ifName\t$ifAlias\t$ifSpeed\t$inOutPercent\t$pingLossCount\t$testStatus\n";
}

### Main ###

open (inputFile, $ARGV[0])
  or die "Failed to open $ARGV[0]: $!\n";

print "date time \tipAddress \tsysName \tifName \tifAlias \tifSpeed \tinOutPercent\tpingLossCount\ttestStatus\n";
foreach my $readString () {
  my $ipAddress = "";
  if ($readString =~ m/([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})/ && $1 < 256 && $2 < 256 && $3 < 256 && $4 < 256) {
    $ipAddress = "$1.$2.$3.$4";
  }
  else {last};
  &stressTest($ipAddress);
}


Буду очень рад конструктивной критике в целях оптимизации алгоритма скрипта.

© Habrahabr.ru