Когда скорость имеет значение

27 января 2011

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

Что это за задачи? Ну например, на сайт компании внезапно началась DDoS атака. Нужно максимально быстро определить 180 000 IP-адресов, с которых идет HTTP-флуд, и заблокировать их фаерволом. Для этого нужно пропарсить логи веб-сервера и собрать список IP, удовлетворяющих неким критериям. Например, время появления в логах, число обращений к сайту, время первого захода, принадлежность определенной стране и тд.

Другой пример — вы работаете на некого интернет-провайдера. Отвечаете за работу DNS. Начальник отдела едет на конференцию, где ему предстоит зачитать доклад. В связи этим вам поручено провести исследование — какие сайты пользовались наибольшей популярностью в прошедшем месяце. А также, сколько времени проводят пользователи в интернете, в какое время они обычно заходят в сеть и тп. И сделать это нужно быстро, потому что конференция — завтра утром.

Честно говоря, в своей работе я сталкивался с совершенно другими задачами, но проблема в них была та же, что и у перечисленных. Нужно было обработать большие объемы данных настолько быстро, насколько это возможно. Здесь я решил собрать несколько рекомендаций относительно того, как можно сэкономить время при решении таких задач.

Как правило, данных, которые нам нужно обработать, не только много, но они еще и раскиданы по разным местам. Первая половина хранится на одном сервере в базе данных MySQL, вторая половина — на другом сервере и доступна только через API, а третья половина — это текстовый файл, который еще предстоит пропарсить. С парсингом, как правило, проблем не возникает. Берем Perl, регулярные выражения и поехали. Так что не будем останавливаться на этом вопросе.

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

Согласитесь, неудобно обрабатывать данные, если одна их половина хранится в XML, а вторая половина — это дамп базы данных в кодировке koi8-r. Поэтому лучше всего экспортировать данные из каждого источника в какой-нибудь простой и понятный формат. Идеальное решение — текстовые файлы в кодировке utf-8 с табуляцией в качестве разделителя полей.

Для преобразования кодировок я использую утилиту enconv (см 9-ый пункт второго выпуска «мини-заметок») или, на худой конец, iconv. Худшее, что можно придумать при экспорте данных из БД — это начать писать скрипт, мучительно вспоминая интерфейс модуля DBI. Зачем такие сложности, когда можно произвести экспорт одной командой:

cat query.sql | \
  mysql -udb_user -pdb_password -hdb_host db_name -N > ./export1.txt

где db_{user,password,host,name} — это, очевидно, параметры подключения к БД, а ключ -N означает «не выводить названия полей». Если планируется несколько обращений к одной БД, можно объявить алиас:

alias dosql="mysql -udb_user -pdb_password -hdb_host db_name -N"

Конечно, нехорошо светить пароль от базы данных в списке процессов и bash_history, но мы же торопимся. Когда работа будет сделана, bash_history можно почистить, а пароль от БД сменить.

Итак, теперь, благодаря описанным выше действиям, а также утилитам scp и, возможно, tar, данные собраны на одной машине. Если после экспорта их объем относительно небольшой (в пределах 500 000 строк), значит пишем небольшой скриптик на Perl, который загрузит все данные в память и там сделает с ними все, что нужно. Если строк больше, значит понадобится база данных.

По условию задачи у нас нет времени на то, чтобы вспоминать, как там правильно написать запрос CREATE TABLE, потому воспользуемся генератором схемы БД, о котором я недавно писал. Прежде, чем заполнять таблицы реальными данными, нужно написать все нужные нам запросы и, опционально, протестировать их на фейковых данных. Затем, внимательно глядя на запросы, нужно подумать, для каких столбцов нам понадобятся индексы и выполнить для них

ALTER TABLE table_name ADD INDEX (col_name);

Если таблицы заполнялись тестовыми данными, их нужно почистить (truncate table), после чего заполнить реальными данными. Проще всего это сделать, написав для каждой таблицы вот такой незамысловатый скрипт:

#!/usr/bin/perl
# table-name-import.pl script
# (c) Alexandr A Alexeev 2011 | http://eax.me/
use strict;

while(my $line = <>) {
  chomp($line);
  my ($foo, $bar) = split /\t/, $line;
  print "INSERT INTO table_name (foo, bar) VALUES('$foo', '$bar');\n";
}

а затем сказав:

cat table-name-export.txt | ./table-name-import.pl | dosql

Вы же не забыли объявить алиаc dosql на машине со всеми собранными данными, верно? Теперь, когда все необходимое нам находится в одной базе данных, выполняем заранее приготовленные (и отлаженные) запросы:

cat first-query.sql | dosql > first-result.txt

После этого можно скопировать результат к себе на машину с помощью scp или отправить себе же на мыло с помощью mail или mutt. При необходимости — создать электронную таблицу в OpenOffice, построить графики и так далее.

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

Вот как-то так. Если вы также сталкивались с задачами, требующими быстрого решения, и вам есть, что добавить к написанному, я был бы благодарен за ваши рекомендации.

Дополнение: Тут в комментариях подсказывают, что скорость обработки данных можно существенно увеличить с помощью tmpfs:

# выделяем 200 Мб памяти
mount -t tmpfs -o size=`expr 200 \* 1024 \* 1024` tmpfs /mnt/tmpfs

Главное, чтобы оперативной памяти было достаточно.

Дополнение: Совсем забыл приплести упомянуть многопоточность. Если вы парсите выдачу Яндекса или обращаетесь к API какого-нибудь веб-сервиса, без нее не обойтись!

Метки: , , , .

Понравился пост? Узнайте, как можно поддержать развитие этого блога.

Также подпишитесь на RSS, Facebook, ВКонтакте, Twitter или Telegram.