From: Skif <http://skif.in.ua>
Newsgroups: email
Date: Mon, 22 Aug 2005 18:21:07 +0000 (UTC)
Subject: Сага о биллинге или считаем траффик на FreeBSD (ng_ipacct + perl+ MySQL)
Первая часть.
Сага о биллинге, или Считаем трафик на FreeBSD (ng_ipacct + perl+ MySQL)
(Данная статья была опубликована во 2 и 3 номерах за 2005 год журнала
"Системный администратор" http://samag.ru/)
Рано или поздно перед каждым системным администратором встает вопрос
подсчета интернет-трафика. И тут уже не важны причины - то ли проверить
провайдера, то ли проконтролировать, какой объем трафика израсходовал
подключенный пользователь и выставить счет. Конечно, систем биллинга
сейчас много. И найти их в интернете не проблема. Но многие хорошие и
гибкие системы учета трафика, как правило, дороги или имеют достаточно
сложный интерфейс, а некоторые затрудняют использование тех же SQUID или
OOPS. (фраза прокси-серверов несколько не уместна. Ибо иногда это
отражается не на них, а на ряде другого софта)
В общем, из этой ситуации мне виделось два выхода - либо писать что-то
свое, либо переделывать существующее. Исходя из соображений, что в
компании уже создан корпоративный сервер статистики с единой системой
авторизации и прочего, у меня не возникало особого желания
прикручивать, например, NetAms к нему, хотя последний и не лишен ряда
достоинств и преимуществ.
Да и организациям, которые получают интернет от нашей компании, не нужно
то море статистики, которое выдает система биллинга. Ну, и, конечно же,
не обошлось без желания создать что-то свое, единственное и не
повторимое.
В итоге, решение создать свою систему учета перевесило все остальное.
Необходимо было всего ничего, просто посчитать сколько трафика прошло
через интерфейс.
Так всё начиналось. Развилось это в большой набор скриптов, которые не
только считали объем трафика, но и определяли локальный он или нет и
какова его доля от общего объема. Они так же позволили просмотреть все
задействованные порты и протоколы, и сколько именно трафика пришлось на
каждый из них.
Что ж, скрипты это хорошо, но без самого главного, сердца всей этой
системы, ничего бы и не было. Сердце ? ng_ipacct, автором которой
является Роман Палагин. Эта программа, если так можно выразиться,
вариация на тему ipacctd.
ipacctd работает с ipfw, а вот ng_ipacct уже с NETGRAPH плюс она
работает как модуль ядра. Почему именно ng_ipacct, а не просто ipacctd?
NETGRAPH имеет ряд преимуществ. Наверняка, многие, замечали, как
отличается объем трафика, который считаешь при помощи ipfw, и тот,
который прислал провайдер со счетом. Объясняется все достаточно просто,
ipfw отрабатывает не все пакеты, поступившие в bpf - пакетный фильтр
системы. NETGRAPH выступает в данном случае как промежуточное звено,
как маленькое кольцо, через которое проходят пакеты, считаются и
перенаправляются дальше. Одно из его преимуществ - он работает на уровне
ядра, используя минимум времени процессора и памяти. Тонкости работы и
возможности его описаны в статье "Все о NETGRAPH" Арчи Коббса (перевод
статьи на русский язык можно посмотреть на
http://www.opennet.ru/docs/RUS/netgraph_freebsd/index.html).
Мы же разберем, как установить ng_ipacct и сам NETGRAPH.
Перед тем, как делать какие либо шаги, скажу что все это протестировано
на FreeBSD 5.2.1-RELEASE-p10, 5.3-RELEASE-p4, 4.10-RELEASE-p3,
4.11-RELEASE. Стоит обратить внимание, что с переходом на 5.3 и выше
потребуется заново откомпилировать и собрать ng_ipacct. Так же это
потребуется и при каждой новой компиляции ядра(на 5-й ветке).
Таким образом, исходные данные есть. Возьмемся за netgraph. Загружать в
память его можно используя два метода: запускать нужные модули при
старте либо вкомпилировать (нет имено так) сразу же в ядро. Мне
предпочтителен последний вариант.
Делается все достаточно просто. Рассмотрим на примере для FreeBSD-4.10.
Первым делом идем в /usr/src/sys/i386/conf/ и смотрим LINT-файл:
То есть опций достаточно много и есть из чего выбрать. Для избежания
проблем с разного рода устройствами можно их все включить в наше ядро,
но в самом простом случае (считаем только с (нет это обозначает что ?с
такого типа? то есть предлог уместен) ethernet устройства) нам
потребуются только такие опции в ядре:
Дальнейшие наши действия заключаются в компиляции ядра:
root@ostwest :config SKIF
Don't forget to do a ``make depend''
Kernel build directory is ../../compile/SKIF
root@ostwest : cd ../../compile/SKIF && make depend && make && make install && make clean && rehash
Объясним, что же тут сделано. Первая команда:
config SKIF - конфигурирование файла ядра, в моем случае это SKIF
Если ошибок в файле не было выявлено, то она выдаст такое:
Don't forget to do a ``make depend''
Kernel build directory is ../../compile/SKIF
Это маленькое напоминание о том, что необходимо сделать make depend и
где это сделать.
cd ../../compile/SKIF && make depend && make && make install && make clean && rehash
- это полный список команд, необходимый для того, что бы
перейти и скомпилировать наше ядро. Достаточно удобный, если никаких
ошибок не ожидается, но, если возникнут, то выяснить на каком этапе они
произошли будет проблематично. Посему, команды лучше выполнять по
отдельности.
После всех этих манипуляций перезагрузим сервер.
root@ostwest : shutdown -r now
В принципе эту команду можно было добавить сразу же в верхнюю строку.
После перезагрузки мы получаем чистое ядро с поддержкой NETGRAPH.
Что ж, часть работы выполнена. Устанавливаем ng_ipacct. Первым делом
смотрим порты, имеющиеся в системе. Там присутствует только ipacct:
На сервере присутствуют версии как для четвертой ветки FreeBSD, так и
для пятой. Они неидентичны, так как реализация NETGRAPH в этих версиях
FreeBSD заметно отличается. Основное отличие - синхронизация. В RELENG_4
она осуществляется через уровни прерываний, о которых можно почитать в
man 9 spl. Весь код netgraph должен выполняться на уровне splnet.
Все граничные ноды, осуществляющие связь между netgraph и другой
подсистемой, например ng_ether ,переходят в уровень splnet перед тем как
отправить данные в граф. Если это невозможно, то данные ставятся в
очередь и позже раздаются в нужной последовательности. Любые внешние
вызовы, которые работают с netgraph, тоже должны первым делом вызывать
splnet(). Таким образом, в одну единицу времени может существовать
только один контекст выполнения netgraph и конфликтовать ему не с кем.
В RELENG_5 ядро многонитевое(multithreads) и синхронизация netgraph
осуществляется с помощью мьютексов (блокировок, используемых для
реализации гарантированной исключительности) и атомарных операций. Ноды
передают друг другу объекты (items) различных типов: данные (mbufs),
сообщения (ng_mesg), ссылки на функции. У объекта есть атрибут - reader
или writer.
Нода может одновременно обрабатывать сколько угодно reader items или
только одну writer item. По умолчанию объекты с данными - readers, а все
остальные writers. Однако это можно указать как на уровне конкретных
объектов, так и на уровне хуков(hooks).
Важным является то, что в момент, когда выполняется код внутри ноды,
тред не держит ни одного мьютекса, что позволяет граничным нодам
вызывать методы других подсистем избегая LOR(Lock order reversal ?
блокирования устанавливаемых изменений).
То есть, это грозит нам как минимум тем, что один и тот же ng_ipacct не
будет работать на разных ветках FreeBSD.
Что ж, скачиваем и распаковываем.
root@ostwest : tar xfvz ng_ipacct-20040109.tar.gz
root@ostwest :cd ng_ipacct/
root@ostwest :make && make install && make clean && rehash
Ничего особо сложного здесь нет и программа без особых проблем
проинсталируется. В принципе это и все, что было необходимо для
установки ng_ipacct. В комплекте к ней идут четыре скрипта, которые
объясняют, как запустить программу для подсчета трафика и как
остановить. Готовый скрипт для запуска и остановки: ng_ipacct_init.sh,
он находиться в распакованной папке ng_ipacct/script. Этот скрипт,
слегка подкорректировав, можно смело поместить /usr/local/etc/rc.d/
Все что нужно в нем прописать это:
прослушиваемые интерфейсы INTERFACES="ed0" - здесь это будет ed0. Для
того, что бы указать более одного интерфейса ? перечислите их через
запятую.
VERBOSE=1 - уровень расширенного вывода статистики, по умолчанию в
скрипте 1, которая выведет нам дополнительно кроме IP-адреса источника и
назначения количества пакетов и байт, еще и порты и протоколы, которые
использовались. Стоит обратить внимание, что названия протоколов, если
указан расширенный вывод(VERBOSE=1), будут отображены в числовом, а не
буквенном виде. Что значит каждый номер, можно посмотреть в
/etc/protocols
THRESHOLD=50000 - количество записей, которые будут храниться программой
в памяти. На этот параметр стоит обратить особое внимание, так как
неправильно подобранный размер threshold может привести к потери части
данных или даже к панике ядра. Это возможно по той причине, что
ng_ipacct работает на уровне ядра и ей не будет доступна полностью вся
память, имеющаяся на машине, а только малая часть, зарезервированная
непосредственно под ядро. В результате переполнения памяти выделенной
системе на ядро может произойти паника со всеми вытекающими
последствиями, как-то, в лучшем случае, остановка сервера и потеря
записей, относительно трафика прошедшего через него. Поэтому если у вас
менее 128 Mb памяти стоит себя ограничить на уровне менее 4000-5000
записей и чаще снимать статистику, чтобы не потерять нужные данные.
Для снятия статистики в ng_ipacct необходимо проделать следующее:
Передать данные в checkpoint (контрольную точку), вывести ее при помощи
show из контрольной точки и очистить контрольную точкку.
В принципе, если вас интересует исключительно возможность поднять
ng_ipacct, то на этом можно остановиться.
Мы же проследуем дальше, ибо этого мне было мало. Мне требовалось,
чтобы все данные хранились в базе MySQL для каждого хоста и интерфейса,
разнесенные по дате и времени.
Вот теперь опишем основные требования, которые были предъявлены биллингу:
Первое:
Система должна хранить данные не только по-интерфейсно, но и по хостам.
Объясню для чего это нужно - что бы быстро разделить трафик между
разными хостами/роутерами с которых считывается статистика. При этом
количество интерфейсов различно и их наименование может совпадать (почти
везде есть rl0 или fxp0).
Второе:
База должна разделять трафик за текущий и предыдущий месяцы
самостоятельно и иметь возможность предоставить пользователю отчет за
каждый из них. Для чего это нужно? Что бы таблицы бессмысленно не
росли. Гораздо проще обработать одну маленькую за месяц, чем одну
большую за год с выборкой за месяц. Просмотр статистики за предыдущие
месяцы, может быть необходим дляотчета перед начальством или выставления
счета клиенту, если такой имеется.
Третье:
В случае недоступности MySQL-сервера необходимо хранить полученные
данные локально до тех пор, пока не будет устранена причина
недоступности сервера базы данных. После чего данные автоматически
должны быть перенесены в базу при следующем сеансе.
Четвертое:
Единый конфигурационный файл с удобным и интуитивно понятным
содержанием.
Пятое:
Графический или web-интерфейс, для удобоваримого отображения статистики.
Шестое:
Неплохо было, что бы система, где необходимо, отличала локальный трафик
от внешнего.
В принципе этот список можно продолжить, но, как по мне, выше
приведенные требования являются ключевыми.
Итак, требования перечислены. Создадим, исходя из этого, наш
конфигурационный файл. Все свои скрипты и программы я размещаю в папки
расположенные в /usr/local/script . Если у вас такой нет, рекомендую
создать. Если у вас путь будет отличен от моего, тогда внесите
необходимые коррективы.
Смена владельца выполняется с целью защитить систему, в случае если
наши скромные потуги в области программирования окажутся небезопасны. По
крайней мере, никто не увидит что написано внутри скрипта, а значит,
ломать его будет труднее.
Этим мы создали папки, где будут лежать наши конфигурационные и
исполняемые файлы.
Что ж создадим конфигурационный файл и внесем первые параметры. По мере
продвижения мы будем дополнять его нужными параметрами.
skif@ostwest : cd /usr/local/script/ng_stat/etc
Здесь мы создадим файл настройки ng_stat.conf и внесем следующие строки.
# Имя сервера, где находиться база данных статистики
server_db = freebsd
# Имя базы данных, где будет сохраняться статистика
db_name = ng_stat
# Имя пользователи для доступа к базе
db_user = nguser
# Пароль для доступа к базе
db_pass = rfn.if
# Имя хоста с которого снимается статистика
listen_host = freebsd2
# Имена интерфейсов, которые прослушиваются на компьютере.
# Указывать через запятую
listen_interfaces = rl0
Думаю пояснений к строкам приведенного конфигурационного файла не нужно.
Итак, первым делом откажемся от поставляемого в комплекте с ng_ipacct
скрипта для его старта и остановки. Лучшенапишемсвой
skif@ostwest : cd /usr/local/script/ng_stat/bin
skif@ostwest : touch ng_stat_start.pl
Данный скрипт будет служить нам скелетом для последующих, и мы будем
частенько от него отталкиваться.
Итак первое что мы сделаем это объявим основной набор переменных:
#!/usr/bin/perl -w
#########################
# Список основных переменных
#########################
my $serverdb = "test";
my $dbname = "test";
my $dbuser = "test";
my $dbpass = "test";
my $table_auth = "test";
my $table_proto = "test";
my $listen_host = "test";
my @listen_interf;
Все переменные созвучны описанным в конфиге и являются глобальными для
данного файла. Внеся заранее значение "test" в них, мы избежали проблемы
получить в самом не подходящем месте undef. Но обратите внимание что,
прослушиваемые интерфейсы обозначены не переменной, а массивом. Сделано
это потому, что интерфейсов может быть несколько, а не один. Вот мы и
используем массив.
Почему были внесены такие непонятные значения переменных? Объясняется
все достаточно просто. Во-первых, сюда можно внести значения реальных
данных по умолчанию, которые будут считываться. Во вторых, если на этапе
отладки будут проблемы ? изменив значения, вы сможете выяснить, с какой
переменной у вас непорядок и где.
Теперь откроем конфигурационный файл и прочитаем значения наших
переменных:
open (CONFIG, "/usr/local/script/ng_stat/etc/ng_stat.conf");
while (<CONFIG>) {
}
close (CONFIG);
Этими строками открывается конфигурационный файл и, при помощи while,
полностью считывается и закрывается. Обратите внимание, что в данном
случае используется полное указание пути к файлу в явном виде, а в
последствии будем указывать его неявно, через переменные.
Что ж первое, что нам нужно сделать, это разобрать строки, которые
поочередно считывает while до тех пор, пока не дойдет до конца файла. Но
среди полезной информации конфигурационный файл несет в себе
комментарии. От них нужно избавиться. Для этого в perl имеется мощнейшие
инструменты поиска в строках/словах. Один из них - конструкция вида
m/шаблон/ограничитель, им и воспользуемся, условившись, что комментарием
будет символ # :
Объясним конструкцию if ... else : если вначале строки присутствует
символ комментария, то на экран будет выведено сообщение "Комментарий",
в противном случае строка пойдет по else. Вывод сообщений о наличии
комментариев нам необходим только на этапе отладки. Кстати, можете
проверить, как скрипт работает, в последствии он будет
закомментирован.
Но этого мало, необходимо разобрать и полезную строку.
Для разбора использовалась функция split, которая на основе разделителя
?=?, заданного еще в конфигурационном файле, разбила все полезные строки
на две части: параметр и аргумент.Что бы избавиться от пробельных
символов используется оператор замены s/шаблон/замена/ограничитель.Так
как необходимо избавиться от пробельных символов, а не поменять их на
что-то другое, мы не используем параметр ?замена?, оставляя его пустым.
Модификатор s означает любой пробельный символ.
Перед этим были убраны из обоих переменных символы перевода строки при
помощи chomp.
Если в строке присутствуют не только символы пробела, но и табуляции
или если их несколько, то придется прибегнуть к следующей конструкции:
$param =~ s/[st]+//g;
$arg =~ s/[st]+//g;
Теперь необходимо присвоит каждой объявленной переменной ее истинное
значение, находящееся в конфигурационном файле. В этом нам поможет
конструкция следующего вида:
if ($param eq "server_db"){
$serverdb = $arg;
}
Объясним. Если левая часть полученной из файла строки соответствует
server_db (смотрим наш конфигурационный файл), то правая часть
присвоится необходимой переменной.
Но у нас же есть еще несколько значений параметра в одной из строк. Их
мы должны, предварительно разобрав, занести в массив.
Листинг приведен ниже:
#!/usr/bin/perl -w
use DBI;
use POSIX ":sys_wait_h";
#########################
# Список основных переменных
#########################
my $serverdb = "test";
my $dbname = "test";
my $dbuser = "test";
my $dbpass = "test";
my $table_auth = "test";
my $table_proto = "test";
my $listen_host = "test";
my @listen_interf;
my $iface_set = "no";
my @ng_modules;
my $ng_modules_def = "netgraph,ng_ether,ng_socket,ng_tee,ng_ipacct";
my$threshold = 5000;
#########################
# Читаем конфиг. файл.
#########################
open (CONFIG, "/usr/local/script/ng_stat/etc/ng_stat.conf");
while (<CONFIG>) {
$comment = '#';
if(/^$comment/) {
# print "Коментарийn";
}
else {
($param,$arg) = split("=",$_);
chomp $param;
chomp $arg;
my $razdel = "";
$param =~ s/[st]+/$razdel/g;
$arg =~ s/[st]+/$razdel/g;
if ($param eq "server_db"){
$serverdb = $arg;
}
if ($param eq "db_name"){
$dbname = $arg;
}
if ($param eq "db_user") {
$dbuser = $arg;
}
if ($param eq "db_pass") {
$dbpass = $arg;
}
if ($param eq "table_auth") {
$table_auth = $arg;
}
if ($param eq "table_protocols") {
$table_proto = $arg;
}
if ($param eq "listen_host") {
$listen_host = $arg;
}
if ($param eq "listen_interfaces") {
my $coma = ',';
if (defined $arg) {
$iface_set = "ok";
if ($arg ne ""){
if ($arg =~ m/$coma/ ) {
@listen_interf=split($coma,$arg);
}
else {
@listen_interf = $arg;
}
}
}
}
if ($param eq "ng_modules") {
my $coma = ',';
if ($arg =~ m/$coma/ ){
@ng_modules = split($coma,$arg);
}
else {
@ng_modules = split ($coma,$ng_modules_def);
}
}
}
}
close (CONFIG);
if (!defined $listen_interf[0]) {
print "Установите пожалуйста в режим прослушивания хотя бы один интерфейс.n";
}
else {
&check_kld_modules;
&listening;
}
Как видите, мы считали все параметры, и в случае, если интерфейс по
какой либо причине установлен не будет, то на экран будет выдано
сообщение об этом. А если все нормально, то в массив будут внесены
необходимые имена интерфейсов (например, rl0, rl1,rl2,fxp0) и, после
проверки массива @listen_interf на наличие в нем не пустых значений,
будут выполнены подпрограммы: &check_kld_modules и &listening.
Первая проверяет, какие из обязательных модулей загружены. При
необходимости, будет проведена их загрузка.
Вторая включает режим прослушивания интерфейсов.
Рассмотрим первую.
subcheck_kld_modules {
my @modules;
my $pid;
my $ng_module_cfg;
my $chk_ng_file = "/tmp/ng_file";
my $check_ng = 'kldstat -v | grep ng';
$check_ng = "$check_ng";# " > $chk_ng_file";
my $check_netgraph = 'kldstat -v | grep netgraph';
$check_netgraph = "$check_netgraph";#" >> $chk_ng_file";
# $pid = fork;
@modules =split ("n", `$check_ng && $check_netgraph`);
my $mod;
if (defined $modules[0]) {
foreach my $modules (@modules) {
$modules=~ s/d+//g;
if ($modules =~ s/.ko//g) {
#
}
else {
$modules =~ s/[st]+//g;
$mod = "$mod $modules ";
}
}
chop $mod;
foreach my $ng_modules (@ng_modules) {
if ($mod=~m/$ng_modules/g){
# print "$mod содержит $ng_modulesn";
}
else {
my ($pid,$kid);
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1" or die "Ошибка загрузки модуля $ng_modules !n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
}
}
}
else {
foreach my $ng_modules (@ng_modules) {
my ($pid,$kid);
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1" or die "Ошибка загрузки модуля $ng_modules !n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
}
}
}
Итак, первым делом объявляются действующие только в переделах этого
модуля массивы и переменные. В нашем случае это @modules, куда будут
заноситься все модули netgraph присутствующие в ядре или загруженные на
данный момент. $check_netgraph и $check_ng переменные, в которых
записаны команды, проверки загруженных модулей ядра.
Как вы можете заметить вывод не маленький, поэтому пришлось его урезать.
Нам нужны не все модули, а только те, которые имеют отношение к
netgraph. Этим и займутся переменные, когда их используют как значения
для оператораexec.
Что бы получить список загруженных модулей используется split и обратные
кавычки, в качестве разделителя выступает символ переноса строки:
Дальше пойдем по проторенному пути, а именно ? выясним, имеется в
массиве хоть какие то данные. Если полученный массив не пустой, то мы
выполним проверку, какие модули нам необходимо подгрузить для работы.
В данном случае информация о том из какого файла был загружен
модуль(linux.ko, logo_server.ko или что-то другое) не нужна. Так же не
нужны ID загруженных модулей. Для их удаления используется все тот же m//:
$modules=~ s/d+//g;
?d? означает любой цифровой символ.
После удаления ID проверяется, что присутствует в выводе, информация о
том из какого модуля загрузился файл или сам модуль. Однозначно на файл
указывает присутствие расширения ?.ko? в строке. А потому все полученные
строки, где присутствует ?.ko? подлежат удалению. В листинге вы видите,
что на месте совпадения if с ".ko" стоит комментарий. Если хотите,
можете провести синтаксически разбор и вывести на экран имя того
модуля, который был загружен вручную.
Нам же интересно только то, что находиться после else. Вывод kldstat
имеет пять колонок (Id,Refs,Address,Size,Name). Все они разделены между
собой пробельными символами. К тому же первые колонки пусты и заполнены
именно этими самыми пробельными символами. Так как нам необходима только
одна колонка Name, то необходимо удалить все пробельные символы. Для
удобства дальнейших манипуляций мы заносим отобранные элементы в одну
строку:
Здесь необходимо остановиться и вернуться немного назад. Только что мы
получили список загруженных модулей. Это хорошо, но мало. Необходимо еще
знать, какие нам НУЖНЫ для работы и если их нет ? загрузить.
# Загружаемые модули NETGRAPH, необходимые для интерфейсов,
# которые будет обслуживать программа
# По умолчанию загружаются следующие модули: netgraph,
# ng_ether,ng_socket,ng_tee,ng_ipacct
ng_modules = netgraph,ng_ether,ng_socket,ng_tee,ng_ipacct
И соответственно считать их. Для этого нужно так же ввести еще несколько
основных переменных. Точнеемассивипеременную.
Данные из последней переменной будут загружены в массив в случае
отсутствия в конфигурационном файле хотя бы одного модуля netgraph.
Считывание необходимых к загрузке модулей нужно добавить к open ...
close(CONFIG):
if ($param eq "ng_modules") {
my $coma = ',';
if ($arg =~ m/$coma/ ){
@ng_modules = split($coma,$arg);
} else {
@ng_modules = split ($coma,$ng_modules_def);
}
Теперь у нас есть необходимый список модулей. Можем проверить, нужно
что-то загружать или нет.
Для этого необходимо проделать достаточно простую операцию. Проверить
наличие значения каждого элемента полученного массива @ng_modules в
строке $mod. Основываясь на том, есть или нет такое значение массива в
строке, и будет производиться загрузка соответствующего модуля.
foreach my $ng_modules (@ng_modules) {
if ($mod=~m/$ng_modules/g){
# print "$mod содержит $ng_modulesn";
}
else {
my $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Загрузка необходимого модуля ",$ng_modules,"n";
exec "/sbin/kldload $ng_modules > /dev/null 2>&1" or die "Ошибка загрузки модуля $ng_modules !n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
undef $pid;
}
}
В этом примере выполнение внешней команды ? загрузка модуля ?
производиться посредством exec. Особенностью exec является то, что по
выполнении этой функции производится останов программы и выход из
процесса. Но так как присутствует необходимость загрузить не один
модуль, то логичнее было бы использовать system. Но, по соображениям
безопасности, это произвести нельзя. Решением этой проблемы является
разделение программы на различные процессы. Для этого уже существует
функция fork.
Немного поясню, как она работает. По выполнении функции существующий
процесс разделяется на два: родительский и дочерний. Сама функция
возвращает два значения в случае удачного выполнения: номер ID для
дочернего процесса в родительский и 0 в дочерний. Почему ноль, а не
номер полученного процесса? Потому что дочерний процесс может в любой
момент времени получить ID родительского вызвав функцию getppid.
Родительский же процесс получает ID дочернего потому, что способов
узнать, из всего объема процессов, дочерний у него просто нет. Или я его
не знаю.
Возможен так же и третий вариант. Когда fork возвращает неопределенное
значение undef. Это означает, что по какой либо причине разделение на
процессы не произошло.
Так же обратите внимание на такие строчки кода:
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
На данном этапе они не так актуальны. Но ниже, когда будет происходить
запуск сбора статистики на интерфейсах, придерживание последовательности
выполняемых команд будет первостепенным. Что же делает этот блок?
Ключевым к нему является всего одна функция waitpid. Данная функция
аналогична в некоторой степени wait, но ждет завершения определенного
дочернего процесса с указанным ID, в данном случае полученного при fork
$pid. Функция возвращает одно из трех возможных значений:
1) PID завершенного процесса
2) 0 ? если флаги, что указаны, задают не блокирующий вызов, а процесс еще не завершен.
3) -1 ? если дочерних процессов нет.
Родительский процесс на время выполнения waitpid как бы засыпает, ожидая
результата. В итоге комбинацией fork + exec + waitpid мы добиваемся
жесткой очередности выполнения, как всех команд, так и сопутствующего
программного кода.
Вот эти особенности и использовались для запуска внешних программ.
Но проверить и загрузить нужные модули мало. Нужно еще начать собирать
статистику на интерфейсе. Для этого считываем параметр threshold из
конфигурационного файла. Следовательно, необходимо создать глобальную
переменную:
my $threshold = 5000;
Мы ее создали и присвоили значение 5000 строк, по умолчанию. В
конфигурационном файле можно задать и другое значение.
# Отнеситесь внимательно к выбору этого параметра. Он
# указывает сколько записей будет храниться в буфере
# По умолчанию значение равно 5000, но если у вас меньше
# 128 Мегабайт памяти - уменьшите его. Значение во многом
# зависит от того, какая полоса пропускания на вашем канале
# и от того на сколько он загружен. Для 128k и 64 Мб можно будет
# смело установить и 10000 записей, при условии снятия
# cтатистики хотя бы раз в 15-20 минут. Для канала в 2 Мбита
# этого времени будет уже через чур много
threshold = 5000
Задали. Теперь считаем параметр из файла:
if ($param eq "threshold") {
$threshold = $arg;
}
Все. Основные переменные заданы, конфигурационный файл на данном этапе
заполнен полностью.
Что ж, приступим к запуску.
Я сразу приведу полный листинг модуля, а потом лишь поясню некоторые
моменты, ибо сам по себе модуль достаточно прост, в нем только команды
fork и exec.
sub listening{
my $pid;
$ngctl = "/usr/sbin/ngctl";
$ipacctctl = "/usr/local/sbin/ipacctctl";
while (@listen_interf){
$interface = shift @listen_interf;
#/usr/sbin/ngctl mkpeer ${IFACE}: tee lower right
$mkpeer = "$ngctl mkpeer $interface: tee lower right";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Создание и подключение нового NETGRAPH-узла к уже существующему:n $mkpeern";
exec "$mkpeer" or die "Ошибка создания нового узла NETGRAPH!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Соединение двух NETGRAPH-узлов на интерфейсе:n$connectn";
exec "$connect" or die "Ошибка соединения двух NETGRAPH-узлов!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
#/usr/sbin/ngctl name ${IFACE}:lower ${IFACE}_acct_tee
$name = "$ngctl name $interface:lower $interface_acct_tee ";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Присвоение имени созданному узлу:n$namen";
exec "$name" or die "Ошибка на этапе присвоения имени созданному узлу!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Создание и подключение нового NETGRAPH-узла к уже существующему:n $mkpeern";
exec "$mkpeer" or die "Ошибка создания нового узла NETGRAPH!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
#/usr/sbin/ngctl name ${IFACE}_acct_tee:right2left ${IFACE}_ip_acct
$name = "$ngctl name $interface_acct_tee:right2left $interface_ip_acct";
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Присвоение имени созданному узлу:n$namen";
exec "$name" or die "Ошибка на этапе присвоения имени созданному узлу!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Соединение двух NETGRAPH-узлов на интерфейсе:n$connectn";
exec "$connect" or die "Ошибка соединения двух NETGRAPH-узлов!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Установка режима вывода информации:n$verbosen";
exec "$verbose" or die "Ошибка установки режима вывода информацииn";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Установка THRESHOLD:n$set_thresholdn";
exec "$set_threshold" or die "Ошибка установки параметра THRESHOLDn";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
}
}
Первые две встречающиеся переменные - исполняемые файлы для netgraph и
ng_ipacct, точнее пути к ним.
Следующим шагом является чтение из массива @listen_interf поочередно
всех занесенных туда интерфейсов и включение на них "прослушивания".
При помощи mkpeer мы создаем новый узел (nodes) к уже существующему.
При помощи connect соединяет узлы
name - присваивает имя узлу
Наиболее интересными является $ipacctctl $interface_ip_acct:$interface
verbose 1 - здесь мы задаем, в каком режиме будет отображаться
статистика. Нам необходим расширенный, посему устанавливаем значение 1.
Должен отметить, что в man ipacctctl стоит значение on - вероятнее
всего, что это ошибка ибо такое значение не влияет на формат вывода
статистики.
В последнем - $ipacctctl $interface_ip_acct:$interface threshold
$threshold - мы указываем количество записей threshold.
По ходу выполняется разветвление процессов, ожидание завершения
дочернего, с последующим обнулением $pid, куда записывалось значение ID
дочернего процесса. Здесь и всплывает важность waitpid для скрипта. Ибо
выполняться все эти команды должны именно в строгой последовательности,
а не как им заблагорассудиться.
В принципе стартовый скрипт создан.
Что в итоге получилось можно глянуть в ng_stat_start.pl
Сделав при помощи chmod файл исполняемым, можно пробовать его выполнить.
Тут поджидает первый неприятный сюрприз. Данный скрипт выполняется с
правами root. Что ж, на данном этапе можно его запустить и с такими
правами.
Внимательно смотрите за выводом. Отсутствие ?лишнего? говорит о том, что
старт прошел без замечаний. Вот пример того, что скрипт выводит при
старте:
Загрузка необходимого модуля ng_ether
Загрузка необходимого модуля ng_socket
Загрузка необходимого модуля ng_tee
Создание и подключение нового NETGRAPH-узла к уже существующему:
/usr/sbin/ngctl mkpeer fxp1: tee lower right
Соединение двух NETGRAPH-узлов на интерфейсе:
/usr/sbin/ngctl connect fxp1: lower upper left
Присвоение имени созданному узлу:
/usr/sbin/ngctl name fxp1:lower fxp1_acct_tee
Создание и подключение нового NETGRAPH-узла к уже существующему:
/usr/sbin/ngctl mkpeer fxp1_acct_tee: ipacct right2left fxp1_in
Присвоениеимени созданному узлу:
/usr/sbin/ngctl name fxp1_acct_tee:right2left fxp1_ip_acct
Соединение двух NETGRAPH-узлов на интерфейсе:
/usr/sbin/ngctl connect fxp1_acct_tee: fxp1_ip_acct: left2right fxp1_out
Установка режима вывода информации:
/usr/local/sbin/ipacctctl fxp1_ip_acct:fxp1 verbose 1
Установка THRESHOLD:
/usr/local/sbin/ipacctctl fxp1_ip_acct:fxp1 threshold 7000
Для себя можете добавить что-либо, если необходима дополнительная
информация при запуске. Для отработки разных этапов работы скрипта
советую ввести конструкции типа print "Проверяем переменную $lin". Это
поможет проконтролировать получение значений переменными в скрипте и
получить своеобразный отладчик. Но в данном случае это полностью рабочий
скрипт, а посему весь мусор отладки убран.
Запустить мало, необходимо еще и уметь остановить.
Для этого создадим похожий на ng_stat_start.pl скрипт ng_stat_stop.pl. В
принципе, их можно было бы объединить в один, но так проще.
Итак, содержимое абсолютно идентично первому файлу, за исключением того,
что отсутствуют sub и в конструкции if else содержится следующее:
if (!defined $listen_interf[0]) {
print "Установите пожалуйста в режим прослушивания хотя бы один интерфейс.n";
}
else {
foreach my $interface (@listen_interf){
#/usr/sbin/ngctl shutdown ${IFACE}_acct_tee:
$shutdown = "$ngctl shutdown $interface_acct_tee:";
my $pid;
$pid = fork;
if (defined $pid) {
($pid == 0){
print "Отключение созданных узлов на интерфейсе:n$shutdownn";
exec "$shutdown";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
# sleep 1;
$shutdown = "$ngctl shutdown $interface:";
$pid = fork;
if (defined $pid) {
if ($pid == 0){
print "Отключение NETGRAPH на интерфейсе:n$shutdownn";
exec "$shutdown";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
}
}
Здесь производится чтение из массива всех нужных интерфейсов и
последовательно выполняется для них отключение.
Посмотреть как он выглядит полностью можно в ng_stat_stop.pl
Теперь у нас есть скрипты для старта и остановки ng_ipacct. Но и этого
мало. Нужно сделать запуск и останов системы при включении и отключении
сервера. А посему напишем простенький скриптик на shell:
Сохраним его под названием ng_stat.sh Когда система учета будет готова
достаточно лишь скопировать скрипт в /usr/local/etc/rc.d/ , что бы
ng_stat запустился при старте или отключился при выключении питания.
Половина дела, самая важная его часть, готова. Система стартовала там,
где надо и с нужными параметрами. Осталось за малым ? получить
статистику.
В следующей части статьи будет рассмотрено как получить статистику от
ng_ipacct и передать ее в mysql, для последующего хранения и
использования, а так же приведен пример того, как получить наши
результаты обратно.
Сага о биллинге, или Считаем трафик на FreeBSD (ng_ipacct + perl+ MySQL) часть 2
В предыдущей части статьи мы рассмотрели, как установить и запустить
ng_ipacct, а также создание своих собственных скриптов для запуска и
остановки разрабатываемой системы учета трафика.
Дальнейшая цель ? получить статистику и поместить ее в базу. Что нам для
этого нужно? В первой части статьи, когда описывался ng_ipacct,
указывалось, что для снятия статистики необходимо последовательно
проделать следующее: передать данные в checkpoint базу, потом вывести
данные при помощи show (перенаправить в файл) и очистить checkpoint, для
получения следующей порции данных.
Таким образом, мы сразу же определили, что нам нужно сложить статистику
в файл при помощи перенаправления вывода show. А после этого, уже
считывая из файла данные, отправить в базу. Для того чтобы не было
смешивания всех интерфейсов в одном файле, мы так же должны условиться
заранее, что для каждого интерефейса будет создан свой собственный файл
статистики, а так же один общий, куда будет складываться статистика со
всех интерфейсов. В этих файлах будет указанно, имя хоста, время
получения порции записей, дата и самое главное - интерфейс. Почему так
акцентируется внимание на интерфейсе? Очень просто. У нас могут быть
каналы на одной машине, где локальный трафик считается, а так же где он
бесплатный. У честь нам необходимо платный. Соответственно, нужно знать
какой интерфейс принял или отправил пакет.
Что ж основная установка сделана. Остальное - по ходу повествования.
Для начала создадим две вещи: базу, куда будут записываться данные и
папку, где будут распологаться временные файлы со статистикой
интерфейсов.
Создаем базу данных:
mysql> create database ng_stat;
Query OK, 1 row affected (0.04 sec)
mysql> grant insert,create,update,select,delete on ng_stat.* to nguser@'%' identifiedby 'ngpassword';
Query OK, 0 rows affected (0.08 sec)
mysql>
Одновременно были даны права пользователю nguser на добавление,
обновление, удаление записей и их выборку, а так же на создание таблиц.
Для чего проделано последнее? Чтобы не вести учет того, создана таблица
за текущий месяц или нет, а создавать по мере необходимости.
Итак, вновь возвращаемся к написанию скриптов:
skif@ostwest : touch ng_stat_in.pl
И начинаем вносить данные.
Первым делом необходимо подключить два модуля perl, которые будут
использоваться:
#!/usr/bin/perl -w
use DBI;
use Time::localtime;
Последний у вас должен быть по умолчанию в системе, а вот присутствие
DBI необходимо проверить. Самый простой способ - отправить на
исполнение скрипт уже в таком виде ? выдаст ошибку ? значит, нет или не
соответствует текущей версии perl (например, вы обновили perl, а все
сопутствующие модули нет). Что ж, это поправимо:
root@ostwest : cd /usr/ports/databases/p5-DBI/
root@ostwest : make && make install && make clean && rehash
root@ostwest : cd /usr/ports/databases/p5-DBD-mysql
root@ostwest : make && make install && make clean && rehash
Если у вас стоит MySQL не версии 3.23, а 4-й и выше, то выберите
соответствующий вариант, вместо p5-DBD-mysql. После этого можно смело
приступать к дальнейшим манипуляциям.
Для подключения к базе данных нужно снова считать конфигурационный файл
и все параметры необходимые для того, что бы выяснить
а) какие интерфейсы подключены,
б) имя сервера базы данных,
в) имя базы данных,
г) имя и пароль пользователя для доступа к базе.
Все это описано в конфигурационном файле (смотрите первую часть статьи).
Но сначала опять нужно задать основные переменные.
#########################
# Список основных переменных
#########################
my $serverdb = "test";
my $dbname = "test";
my $dbuser = "test";
my $dbpass = "test";
my $table_auth = "test";
my $table_proto = "test";
my $listen_host = "test";
my @listen_interf;
my @ng_modules;
my $ng_modules_def = "netgraph,ng_ether,ng_socket,ng_tee,ng_ipacct";
my $threshold = 5000;
my $ipacct_log = '/usr/local/script/ng_stat/log/ng.log';
Некоторые из них нам не потребуются. Но это удобная заготовка для всех
скриптов. Мы по очереди вносим необходимые параметры, всего лишь
модернизируя уже имеющийся скрипт. ?Лишние? переменные можно будет
убрать на этапе отладки.
Самое важное в этом списке my $ipacct_log =
'/usr/local/script/ng_stat/log/ng.log' - мы указали расположение
основного файла статистики, куда по умолчанию будет записываться ВСЯ
статистика (с интерфейсами, временем и т.д.).
Что ж, читаем дальше конфигурационный файл. Он остается без изменений,
так что приводить его не буду.
Проверяем время на машине. Именно это время и будет записываться в базу:
Почему в переменной $year мы добавляем 1900? Очень просто - она ведет
отсчет от 1900 года. Почему в месяцах прибавляем единицу - переменная
возвращает значения от 0 до 11.
Функция sprintf вернет значения переменных $hour, $sec и $min числом из
двух цифр, если полученное значение будет меньше 10. Например, одна
секунда, после получения ее значения, будет 1, а нужно 01.
Последний параметр $table_date определяет имя таблицы в базе данных.
Далее идет конструкция для проверки, установлены интерфейсы или нет.
Если все в порядке, начинаем подготовку к тому, что бы закачивать данные
на сервер.
Первым делом необходимо получить данные с интерфейсов и записать во
временные файлы.
#
while (@listen_interf){
$interface = shift @listen_interf;
my $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE checkpoint
exec "/usr/local/sbin/ipacctctl $interface_ip_acct:$interface checkpoint" or die "Ошибка передачи записи в checkpoint-базу!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE show >> $DIR/$SDIR/$NAME
exec "/usr/local/sbin/ipacctctl $interface_ip_acct:$interface show >> $ipacct_log.$interface" or die "Ошибка передачи записей из checkpoint-базы в файл!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE clear
exec "/usr/local/sbin/ipacctctl $interface_ip_acct:$interface clear"
or die "Ошибка при очистке checkpoint-базы! nБаза не очищена. Возможно переполнение. Очистите базу в ручнуюn";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$TMPLOG= "$ipacct_log.$interface";
open (TMPLOG, "$TMPLOG");
$TMPLOG =~ s/||`|&&|<|>//gi; #Очистка ряда символов | ` && < > из пути к файлу.
while (<TMPLOG>){
$tmp_log_line=$_;
chomp $tmp_log_line;
$tmp_log_line = "$tmp_log_line $date $time $listen_host $interface";
push @ipacct_arr,$tmp_log_line;
}
close (TMPLOG);
truncate ($TMPLOG,0);
undef $pid;
}
Обращаю внимание на то, что полный путь к ipacctctl храниться в
переменной $ipacctctl - так как скрипт будет работать по cron, то здесь
желательно указать полный путь к нему, ибо не всегда cron сможет
получить переменные из профиля того пользователя, от имени которого
будет исполняться команда или программа.
Как видите, первыми идут checkpoint, show, clear. На этапе show мы
перенаправляем данные во временный файл. Временный файл определяется
основным файлом статистики с приставкой имени интерфейса, то есть для
rl0 он будет выглядеть как /usr/local/script/ng_stat/log/ng.log.rl0 . И
так поочередно для каждого из интерфейсов.
После занесение данных эти файлы считываются. Каждая строка из них будет
дополнена необходимой информацией (дата, время, имя хоста, интерфейс) и
занесена в массив.
После того, как временный файл считан до конца, мы его очищаем (можно, в
принципе и удалить, хотя это не эффективно).
truncate(?$TMPLOG?,0);
так поочередно мы заполним данными со всех интерфейсов массив
@ipacct_arr. Его, кстати, необходимо внести в список основных
переменных, которые были объявлены в начале скрипта.
my @ipacct_arr;
my @ipacct_arr_in;
Я указал кроме него еще один массив - он сейчас тоже потребуется.
open (IPCTLOG,">>$ipacct_log");
while (@ipacct_arr){
$line_arr = shift @ipacct_arr;
$line_arr = "$line_arrn";
print IPCTLOG $line_arr;
}
close(IPCTLOG);
Этим действием все содержимое массива, полученного на предыдущем шаге,
заносится в основной файл статистики. Теперь в случае любых перипетий
(недоступность сервера, отсутствие созданной базы или неправильный
логин/пароль) вся статистика будет накапливаться в нем. Именно по этому
и было указано, то, что в случае пополнения записей они должны
дописываться в конец файла.
open (IPCTLOG,">>$ipacct_log").
Статистика получена. Теперь наступил самый ответственный этап.
Необходимо привести к нужному виду каждую строку в файле статистики.
Проверить доступность сервера и необходимой базы и таблицы. Если все в
полном порядке, то внести данные. Вот как полностью будет выглядеть этот
блок:
#
while (@listen_interf){
$interface = shift @listen_interf;
my $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE checkpoint
exec "/usr/local/sbin/ipacctctl $interface_ip_acct:$interface checkpoint" or die "Ошибка передачи записи в checkpoint-базу!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE show >> $DIR/$SDIR/$NAME
exec "/usr/local/sbin/ipacctctl $interface_ip_acct:$interface show >> $ipacct_log.$interface" or die "Ошибка передачи записей из checkpoint-базы в файл!n";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$pid = fork;
if (defined $pid) {
if ($pid == 0){
#$IPACCTCTL ${IFACE}_ip_acct:$IFACE clear
exec "/usr/local/sbin/ipacctctl $interface_ip_acct:$interface clear"
or die "Ошибка при очистке checkpoint-базы! nБаза не очищена. Возможно переполнение. Очистите базу в ручнуюn";
exit;
}
}
else {
print "Фатальная ошибка ветвления!n.................n";
die "Разделение на процессы не возможно.n Принудительный выход из дочернего процесса: $!n";
}
do {
$kid = waitpid $pid,0;
if ($kid == -1) {
print "Дочерних процессов в системе нет или система не поддерживает их.n Ошибка!" and die "Выход!n";
} elsif ($kid == 0) {
print "Задан не блокирующий вызов и процесс еще не завершен!n";
}
} until $kid=$pid;
undef $pid;
$TMPLOG= "$ipacct_log.$interface";
open (TMPLOG, "$TMPLOG");
$TMPLOG =~ s/||`|&&|<|>//gi; #Очистка ряда символов | ` && < > из пути к файлу.
while (<TMPLOG>){
$tmp_log_line=$_;
chomp $tmp_log_line;
$tmp_log_line = "$tmp_log_line $date $time $listen_host $interface";
push @ipacct_arr,$tmp_log_line;
}
close (TMPLOG);
truncate ($TMPLOG,0);
undef $pid;
}
open (IPCTLOG,">>$ipacct_log");
while (@ipacct_arr){
Как видно из кода, присутствует вызов трех подпрограмм. Выполняемые ими
функции интуитивно понятны из названия(&parse_log_file; &check_in_mysql;
&insert_data_db;).
Рассмотрим их поочередно.
sub parse_log_file {
open (PARSFILE, "$ipacct_log");
while ($line_parse=<PARSFILE>) {
chomp $line_parse;
$line_parse =~ s/[st]+/t/g;
push @ipacct_arr_in, $line_parse;
}
close (PARSFILE);
truncate ("$ipacct_log",0);
}
Все, что мы делаем здесь, - производим разбор строки основного файла. И
все имеющиеся символы пробела или табуляции заменяем на единичные
символы табуляции. И вносим данные в объявленный выше массив
@ipacct_arr_in . После того как все данные из фала были внесены в
массив, этот файл обнуляется для записи последующей порции данных.
Что ж, проверим доступность mysql и наличия таблиц:
my ($dbh,$sth,$count);
$dbh = DBI->connect("DBI:mysql:host=$serverdb;database=$dbname", "$dbuser", "$dbpass")
or &error_connection;
$sth = $dbh->prepare("SHOW tables");
$sth->execute ();
Первой строкой мы объявили переменные, которые будут использоваться для
соединения. Второй устанавливаем соединение с MySQL. В ней указываем,
что необходимо использовать драйвер mysql DBI, так же расположение
сервера, БД, имя и пароль, которые получили из файла настройки. В случае
если произойдут ошибки, будет выполнена подпрограмма &error_connection.
Ее опишем несколько позже, а пока условимся, что соединение прошло
успешно. Следующим пунктом будет запрос. В данном случае проверяется
наличие необходимых таблиц в базе (SHOW TABLES), а последняя строка
означает выполнение запроса.
Теперь полученный результат занесем в массив:
my @row;
my $tables;
while (@row = $sth->fetchrow_array) {
foreach $tables (@row){
push @dbtables, $tables;
}
}
Самое интересное во всем этом ? оператор foreach, который присваивает
переменной $table значения массива @row. Значения этой переменной
заносятся в @tables.
$crt_tbl="yes";
while (@dbtables) {
$table = shift @dbtables;
if (defined $table) {
if ($table eq $table_date) {
$crt_tbl="no";
}
}
}
В данном блоке устанавливается значение переменной $crt_tbl в yes, что
бы в случае необходимости создать таблицу, определенную в переменной
$table_date. Последующие действия как раз и описывают этап сравнения
элементов массива с переменной. Если таблица с таким именем
присутствует, то $crt_tbl принимает значение no.
Если такой таблицы нет, она будет создана при вызове подпрограммы &crt_table_log.
В этом модуле встречаются две новые подпрограммы. Опишем первую, так
как она используется еще в нескольких местах. Итак, в случае ошибки
соединения необходимо срочно остановить выполнение скрипта и сбросить
данные обратно в файл.
sub error_connection {
print "Проверьте правильность имени и пароля на базу в MySQL, ее существованиеn";
print "Возможной причиной ошибки так же может являться то, что сервер временно не доступенn";
print "Будет произведено копирование всех данных в файл:nn$ipacct_log nn";
print "Накопление статистики в файл не лимитипровано, но это может повлечь за собой";
print " всплеск нагрузки на сеть и сервера. По этому обратите внимание на данное";
print " сообщение и выясните конкретную причину.n";
foreach $line_arr(@ipacct_arr_in) {
open (DUMPFILE, ">>$ipacct_log");
$line_arr = "$line_arrn";
print DUMPFILE $line_arr;
close (DUMPFILE);
}
die "Выход.n";
}
Вторая создает таблицу, в которую будет производиться запись данных.
sub crt_table_log {
my ($dbh,$sth,$count);
$dbh = DBI->connect("DBI:mysql:host=$serverdb;database=$dbname", "$dbuser", "$dbpass")
or &error_connection;
$select = "CREATE TABLE $table_date (ip_from varchar(255),s_port varchar(128),ip_to varchar(255),
d_port varchar(128), proto varchar(32), packets int(8), bytes int(16) default 0,
date_ins varchar(32), time_ins time,host varchar(128), interface varchar(8),
index (ip_from),index (ip_to),index (proto),index (packets), index (bytes),
index (host), index (time_ins), index (date_ins), index (interface))";
sub insert_data_db {
my ($dbh,$sth,$count);
$dbh = DBI->connect("DBI:mysql:host=$serverdb;database=$dbname","$dbuser","$dbpass")
or &error_connection_in;
$insert = "INSERT INTO $table_date (ip_from,s_port,ip_to,d_port,proto,packets,bytes,date_ins,time_ins,host,interface) VALUES (?,?,?,?,?,?,?,?,?,?,?)";
$sth = $dbh->prepare("$insert");
print "$insertn";
while (@ipacct_arr_in) {
$line_in = shift @ipacct_arr_in; ($ip_from,$s_port,$ip_to,$d_port,$proto,$packets,$bytes,$date_ins,$time_ins,$host,$interface)=split(/[st]+/,$line_in);
if (!defined $proto){
$proto="0";
}
if (!defined $packets){
$packets="0";
}
if (!defined $bytes){
$bytes="0";
}
$sth->execute ($ip_from,$s_port,$ip_to,$d_port,$proto,$packets,$bytes,$date_ins,$time_ins,$host,$interface);
}
$sth->finish;
$dbh->disconnect;
}
Как видите, снова идет пошаговое считывание данных из массива.
Полученные строки разбиваются на составляющие при помощи split. Если
значения в этих переменных отсутствуют, им присваивается значение равное
нулю.
Вот в принципе все. Мы занесли данные в таблицу.
Теперь можно извлекать нужные данные соответствующими запросами на выборку.
Полностью одержимое можно посмотреть в ng_stat_in.pl
Последние штрихи ? помещение созданного сценария в /usr/local/etc/rc.d и
добавление подобной записи в /etc/crontab
Конечно, данная система не лишена ряда недостатков. Таковыми можно
считать то, что работает она именно от пользователя root, и не имеет
возможности на лету менять конфигурацию.
Самое главное ее преимущество - простота. Она предоставляет самое
удобное хранилище данных, откуда их можно вытянуть и при помощи web, и с
консоли, и даже с машины под управлением Windows. Я думаю, оттолкнуться
от этого и поплыть (думаю лучше оставить как есть ? все такие это часто
употребляемый фразеологизм ?оттолкнуться и поплыть?, так что разночтений
не будет)сможет каждый.
Итак, были выполнены практически все требования к биллингу. Мы
остановимся пока на этом, ибо не маленький объем вышел, для трех
простеньких скриптов. Если у читателей появится желание, приведу
дополнительный набор скриптов и их описание, систему авторизации через
web-интерфейс и, если необходимо, графические клиенты под X-Window и MS
Windows.
В принципе вы и сами можете написать самые простые запросы уже сейчас.
Приведу еще раз заголовки таблицы:
mysql> show columns from 2004_10;
+-----------+--------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-----------+--------------+------+-----+---------+-------+
| ip_from | varchar(255) | YES | MUL | NULL | |
| s_port | varchar(128) | YES | | NULL | |
| ip_to | varchar(255) | YES | MUL | NULL | |
| d_port | varchar(128) | YES | | NULL | |
| proto | varchar(32) | YES | MUL | NULL | |
| packets | int(8) | YES | MUL | NULL | |
| bytes | int(16) | YES | MUL | 0 | |
| date_ins | varchar(32) | YES | MUL | NULL | |
| time_ins | time | YES | MUL | NULL | |
| host | varchar(128) | YES | MUL | NULL | |
| interface | varchar(8) | YES | MUL | NULL | |
+-----------+--------------+------+-----+---------+-------+
11 rows in set (0.00 sec)
mysql>
И простенький запрос к базе на выборку за 10 месяц 2004 года сумы
прошедшего трафика через интерфейс rl0 сервера freebsd2:
mysql> select sum(bytes) from 2004_10 where host='freebsd2' and interface='rl0';
+-----------+
|sum(bytes) |
+-----------+
|993453162 |
+-----------+
1 row in set (0.12 sec)
mysql>
Все остальные скрипты, вне зависимости от своего назначения, являются
вариациями на тему запроса.
Подведем итоги.
---------------
Разобранные выше примеры написания системы учета трафика не полноценный
биллинг, так как для такой системы нужно хорошо просчитать структуру
самой БД, ее нагрузку, выбрать оптимальные типы полей в таблицах. Для
примера, в более серьезном и нагруженнном варианте(сервер провайдера и
порядка 20 хостов), необходимо изменить типы полей s_port, d_port,
ip_from, ip_to на тип int(преобразование ip-адреса выполняется
встроенными функциями MySQL), одним словом уделить очень большое
внимание настройке оптимальной производительности самой СУБД ? здесь она
станет узким местом, и возможно перейти на альтернативную СУБД.
Но подводя итоги этой статьи можно с увереностью сказать самое главное:
ng_ipacct является очень удобный программным пакетом для сбора полной и
детализированной статистики. Причем он не требует каких-то особых
заоблочных знаний и предоставляет удобную в отображении информацию. Он
единолично может заменить вам систему учета трафика, даже если вы будете
каждый день мигрировать с одного типа брендмауера на другой. При этом
особо не нагрузит ваш сервер.
Так же можно добавить, что сам процесс сбора статистики и запись
полученных данных, средствами perl не представляет особой сложности ?
весь необходимый набор инструментов встроен в perl либо присутствует в
виде портов и пакетов.
Большую часть подпрограмм (например чтение конфигурационного файла)
можно вынести в отдельный модуль/пакет.
Так же, вполне возможно, что вы несколько перепишете код под себя ? тут
уж никто никого не сдерживает. Цель есть, а как к ней добраться ? каждый
выбирает сам.
Архив со скриптами находиться по адресу:
http://www.samag.ru/source/f0205-1.zip
а так же
http://skif.in.ua/soft/ng_stat_script.tar.gz
http://www.opennet.ru/soft/ng_stat_script.tar.gz
P.S.: Касательно скриптов расположенных на SKIF.IN.UA - эти скрипты
время от времени дорабатываются, так что там может быть разночтенние с
базовыми и приведенным в статье кодом.
P.P.S: Так как статья писалась достаточно давно, то там отображен
период, когда ng_ipacct не входил в порты, на данный момент он там
наличиствует
P.P.S: Nfr
Благодарности: Особая благодарность Глебу Смирнову (Gleb Smirnoff) из
UAFUG рассылки за подробное объяснение того как дышит netgraph. К
сожалению благодарности из статьи были вырезаны редакторами журнала и
мне было несколько не удобно за сей прискорбный факт, думаю теперь я
себя реабилитировал.
974 Прочтений • [Сага о биллинге или считаем траффик на FreeBSD (ng_ipacct + perl+ MySQL) (freebsd isp billing traffic netgraph perl mysql)] [08.05.2012] [Комментариев: 0]