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


В организациях, использующих арендованные у операторов связи каналы передачи данных 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 <inputFile>" 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 (<inputFile>) {
  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);
}

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

Комментарии (10)


  1. saboteur_kiev
    01.04.2018 22:04

    А почему бы не юзать стандартный iperf, который умеет
    1. передавать указанное количество данных в нужную сторону
    2. Делать это с указанным количеством потоков
    3. Не требовать место под сами файлы, то есть задействовать только те ресурсы, которые мы тестируем — сеть
    4. Есть также iperf под windows, то есть это кроссплатформенно


    1. alexk038 Автор
      02.04.2018 07:17

      По поводу iperf за меня очень правильно ответил DmitriyPanteleev.
      Для тестирования с помощью iperf нужна ответная часть, которую запустить на имеющемся активном оборудовании не представляется возможным. А вот поддержка SNMP имеется у всех увожающих себя вендоров.
      Что касается кроссплатформенности, то существуют генераторы трафика и под Windows, которые запускаются в том числе и из командной строки. Могу поделиться.


      1. iddqda
        02.04.2018 18:35

        Ну не знаю
        Для цели ткнуть носом провайдера в несоответствие bandwidth я поступаю следующим образом:


        1. iperf -c -u wan-ip-addr-of-remote-device -t600
          2a. Захожу на observium (cacti, prtg, etc) и смотрю realtime загрузку интерфейса
          2b. Захожу на удаленное устройство ( как правило это джунипер) и смотрю monitor interface

        А параметры канала в это время меряются вечными тестами cisco ip sla или juniper rpm с сигнализацией в zabbix


        Так как задачу «ткнуть носом провайдера» приходится делать нечасто, то автоматизация мне пока не требуется. Но смысл в том, что iperf на udp никакой ответки не требует. Да без ответной части он не посчитает количество принятых пакетов, но и ваш скрипт эту информацтю не из генератора тянет.


        1. alexk038 Автор
          03.04.2018 03:32

          iddqda, если Вы предлагаете использовать iperf в качестве только генератора трафика, то он на эту роль, думаю, подходит точно также, как и используемый мной udpblast — тут кому что больше по душе, либо кому что быстрее подвернется/подвернулось под руку. Для меня главное, чтобы у iperf была опция по генерации трафика с определенной полосой. Я такую опцию после беглого изучения документации по iperf не нашел, да и в Вашем примере она не указана. Подскажите её, если кто знает.
          Без неё канал загрузится под самую полку. Моя же цель была — выявить проблемные каналы на рабочей сети, при этом полного простоя сервисов быть не должно. Для достижения этой цели скрипт загружает каналы не на 100%.
          А озвученный Вами способ отслеживания загрузки интерфейса для штучных целей я также применял ранее с некоторыми отличиями по используемому ПО, пока не появилась задача проверить на деградацию более 400-от каналов связи.
          Могу поделиться своей статистикой. При ежемесячном запуске ещё не было случая, чтобы скрипт выявил менее 3-4 проблемных каналов.


          1. iddqda
            04.04.2018 10:13

            Извиняюсь самую главну опцию то и забыл

            iddqd@hostname:~$ iperf --help
            --
              -b, --bandwidth #[KM]    for UDP, bandwidth to send at in bits/sec
                                       (default 1 Mbit/sec, implies -u)
            


            ну т.е. моя команда часто выглядит так:
            iddqd@hostname:~$  sudo iperf -u -c $remote-if-addr -b8M -t600


            И я не знаю может udpblast и вправду ничем не хуже
            просто встал на защиту любимого iperf :)


            1. alexk038 Автор
              04.04.2018 10:33

              В таком случае скрипт будет замечательно работать и в связке с iperf.


  1. DmitriyPanteleev
    02.04.2018 03:20

    saboteur — в Вашем случае нужна «ответка» с айперфом на той стороне, у автора «ответки» не нужно! Это очень большой и жирный плюс! К тому же он вендоро-независимый, пойдет для тестирования как цисок, так прочих длинков/микротиков. У меня есть скрипт/метод тестирования для цисок на tlc, ему и сервер не нужен, достаточно и маршрутизатора в центре. Но он как уже сказал циско онли. Ещё для мониторинга пропускной способности можно использовать джиттеры, но это опять-таки циско онли:(… Вообщем автору респект и уважуха, а скрипт забрал в «коробочку»:). З.Ы.: Отдельное спасибо что не забыли udp, а то у операторов, часто бывает, что с тсп все норм., а удп зажали под самый ноль:)…


    1. e1t1
      02.04.2018 11:23

      Джиттер и udp есть респондер для Linux, поддерживает в качестве сервера не только циску но и джун.


  1. Chosen_One
    02.04.2018 09:03

    Например для Cisco я бы не полагался на пинг. Намеки здесь http://blog.ipspace.net/2009/05/ping-priority-on-cisco-ios.html можно найти. Если вкратце, то роутер по разному обрабатывает обычные пакеты и ICMP.


    1. alexk038 Автор
      02.04.2018 09:24

      Chosen_One, не соглашусь с Вашей позицией относительно ICMP и control plane policy (CPP).
      Аргументы: CPP у той же Cisco ограничивают любой не транзитный трафик, адресованный данному конкретному оборудованию. Это может быть не только ICMP. В описанной вами ситуации имеет смысл сконцентрироваться на том, чтобы CPP удаленных роутеров были правильно настоены и не дропали ICMP трафик, который мы отправдяем из скрипта. К тому же объем ICMP трафика от скрипта не большой — периодичность отправки пингов задана всего лишь 1 пакет в секунду. Я не сталкивался ни разу с проблемами отсутствия отклика на ICMP request'ы с такой периодичностью ни у одного из вендоров: Cisco, Juniper, Huawei, Mikrotik.