Almaz Mubinov

ARCHIVE Storage Engine для ведения логов

11 Окт 2011 г. 

Всем привет. Сегодня мы будем сравнивать различные способы хранения логов. Логи бывают разные, это могут быть логи ошибок, доступа, отправки писем и все что только можно придумать. Но логи это вторичная операция, ведь прежде всего важно само действие (доступ, отправка письма, отображение сообщения об ошибке), и только потом уже запись информации о произошедшем событии в лог. Поэтому крайне важна высокая скорость записи данных в лог, так, чтобы процесс записи информации практически не влиял на скорость работы скрипта.

Такие логи, как лог доступа, на нагруженном проекте, разрастаются как снежный ком, и становится актуальным такой параметр как степень сжатия данных. Как правило, логи — это текстовые данные, и они легко поддаются сжатию и занимают не очень много места.

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

Как же будем хранить логи? Первое что приходит в голову — это файлы. В этом очень показательны логи Apache, где используется ротация логов. Там хранится лог только за определенный период, а старые логи архивируются и к их имени приписывается номер. Чем старше лог, чем больше номер. Как показывает практика на нагруженных проектах хранить лог доступа Apache более чем за один день неудобно, так как файл сильно разрастается. А анализ данных, например за месяц, используя архивы — это не самое приятное занятие.

Также можем хранить лог в виде таблицы базы данных MySQL. Но и здесь мы сталкиваемся с рядом проблем и он связан с выбранным движком базы данных. Если мы используем InnoDB, то здесь сталкиваемся с проблемой медленной записи, из-за транзакций. MyISAM — это неплохой выбор, однако этот движок не умеет сжимать данные, и придется делать это самостоятельно.

И тут нам на помощь приходит ARCHIVE Storage Engine. Его главные плюсы — высокая скорость записи (сопоставимая с MyISAM) и автоматическое сжатие таблицы с помощью библиотеки zlib. Однако вместе с тем есть и недостатки — нет поддержки индексов, и нет поддержки операций UPDATE/DELETE. То есть не получиться удалить или изменить единичную запись в логе. Для очистки таблицы придется делать DROP TABLE/CREATE TABLE, ведь операция TRUNCATE также не поддерживается. Я считаю, что это в некоторой степени является плюсом, ведь мы тем самым запрещаем любое изменение лога, гарантируя его целостность.

И так, сегодня сравниваем:

  • Текстовый файл без сжатия
  • MySQL / движок MyISAM
  • MySQL / движок InnoDB (точнее используется более производительный XtraDB от Percona)
  • MySQL / движок ARCHIVE Storage Engine

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

Создадим три таблицы для записи логов:

CREATE TABLE IF NOT EXISTS `log_archive` (
   `date` INT(11) NOT NULL,
   `text` text NOT NULL,
   `type` enum('error','warning','notice') NOT NULL
 ) ENGINE=ARCHIVE DEFAULT CHARSET=utf8;
 
CREATE TABLE IF NOT EXISTS `log_innodb` (
  `date` INT(11) NOT NULL,
  `text` text NOT NULL,
  `type` enum('error','warning','notice') NOT NULL
) ENGINE=INNODB DEFAULT CHARSET=utf8;
 
CREATE TABLE IF NOT EXISTS `log_myisam` (
  `date` INT(11) NOT NULL,
  `text` text NOT NULL,
  `type` enum('error','warning','notice') NOT NULL
) ENGINE=MYISAM DEFAULT CHARSET=utf8;

CREATE TABLE IF NOT EXISTS `log_archive` ( `date` int(11) NOT NULL, `text` text NOT NULL, `type` enum('error','warning','notice') NOT NULL ) ENGINE=ARCHIVE DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `log_innodb` ( `date` int(11) NOT NULL, `text` text NOT NULL, `type` enum('error','warning','notice') NOT NULL ) ENGINE=INNODB DEFAULT CHARSET=utf8; CREATE TABLE IF NOT EXISTS `log_myisam` ( `date` int(11) NOT NULL, `text` text NOT NULL, `type` enum('error','warning','notice') NOT NULL ) ENGINE=MYISAM DEFAULT CHARSET=utf8;

С помощью следующего теста (PHP) узнаем какой способ записи самый быстрый:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
/*
 * Запуск теста для каждого способа хранения логов
 */
function run_test($engine){
 
    // Тексты для генерации различных сообщений
    $texts = array(
         0 => 'Случилась ошибка. Мы ее записываем в лог. Программа дальше не исполняется.',
         1 => 'Произошел варнинг! что с этим делать пока не знаем но запишем на всякий случай сюда',
         2 => 'Всего лишь небольшое замечание к твоему коду. Здесь ее подробное описание'
    );
    // Типы ошибок
    $types = array(
        0 => 'error',
        1 => 'warning',
        2 => 'notice'
    );
 
    $t = microtime(true); // Текущее время
 
    if($engine != 'file'){
 
        // Работа с БД
        $connect = mysql_connect('localhost', 'root', '');
 
        if (!$connect) {
            die('Could not connect: ' . mysql_error());
        }
 
        mysql_select_db('dbname', $connect);
    }
 
    for($i = 0; $i<1000; $i++){
        $date = time();
 
        $result = rand(0, 2);
        $type = $types[$result];
        $text = $texts[$result];
 
        if($engine != 'file'){
	//Запись в БД
            $query = "INSERT INTO `log_$engine` VALUES($date, '$text', '$type');";
            mysql_query($query, $connect);
        }else{
	//Запись в файл
            file_put_contents( 'errors.log' , "DATE: $date; TYPE: $type;\nTEXT:\n$text\n\n", FILE_APPEND);
        }
 
    }
    if($engine != 'file'){
        mysql_close($connect);
    }
    print "ENGINE : $engine; TIME : " . (microtime(true) - $t) . "<br/>\n";
}
 
error_reporting(E_ALL);
ini_set('display_errors', 1);
 
run_test('file');
run_test('myisam');
run_test('innodb');
run_test('archive');

/* * Запуск теста для каждого способа хранения логов */ function run_test($engine){ // Тексты для генерации различных сообщений $texts = array( 0 => 'Случилась ошибка. Мы ее записываем в лог. Программа дальше не исполняется.', 1 => 'Произошел варнинг! что с этим делать пока не знаем но запишем на всякий случай сюда', 2 => 'Всего лишь небольшое замечание к твоему коду. Здесь ее подробное описание' ); // Типы ошибок $types = array( 0 => 'error', 1 => 'warning', 2 => 'notice' ); $t = microtime(true); // Текущее время if($engine != 'file'){ // Работа с БД $connect = mysql_connect('localhost', 'root', ''); if (!$connect) { die('Could not connect: ' . mysql_error()); } mysql_select_db('dbname', $connect); } for($i = 0; $i<1000; $i++){ $date = time(); $result = rand(0, 2); $type = $types[$result]; $text = $texts[$result]; if($engine != 'file'){ //Запись в БД $query = "INSERT INTO `log_$engine` VALUES($date, '$text', '$type');"; mysql_query($query, $connect); }else{ //Запись в файл file_put_contents( 'errors.log' , "DATE: $date; TYPE: $type;\nTEXT:\n$text\n\n", FILE_APPEND); } } if($engine != 'file'){ mysql_close($connect); } print "ENGINE : $engine; TIME : " . (microtime(true) - $t) . "<br/>\n"; } error_reporting(E_ALL); ini_set('display_errors', 1); run_test('file'); run_test('myisam'); run_test('innodb'); run_test('archive');

Результат теста для записи 1000 сообщений об ошибке в лог:

ENGINE : file; TIME : 0.026916980743408
ENGINE : myisam; TIME : 0.11324405670166
ENGINE : innodb; TIME : 54.609144210815
ENGINE : archive; TIME : 0.14003896713257

Результат для 10 000 сообщений:

ENGINE : file; TIME : 0.31891481042545
ENGINE : myisam; TIME : 2.2111179828644
ENGINE : innodb; TIME : 529.59462714195
ENGINE : archive; TIME : 2.0362560749054

Занимаемое место для 10 000 сообщений:

file 175 КБ
MyISAM 1.5 МБ
InnoDB 2.5 МБ
ARCHIVE 23.7 КБ

Сравнительная таблица:

Способ хранения Скорость записи 1000 сообщений Скорость записи 10000 сообщений Занимаемое место 10000 сообщений
Текстовый файл 0.0269 0.3189 175 КБ
MySQL/MyISAM 0.1132 2.2111 1.5 МБ
MySQL/InnoDB 54.6091 529.5946 2.5 МБ
MySQL/ARCHIVE 0.1400 2.0363 23.7 КБ

Подведем некоторые итоги нашего теста:

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

MyISAM на мой взгляд предпочтителен для тех у кого нет возможности использовать ARCHIVE Storage Engine. Его единственный минус — это то, что таблица с логами будет занимать слишком много места.

InnoDB приведен здесь скорее для общего сравнения, чем пример для реального использования. Мы видим, что из-за транзакций запись происходит очень медленно, а места занимает неоправданно много. Однако если очень хочется, то всегда можно найти выходы. Например, записывать лог сначала в оперативную память (с помощью memcached, xcache или даже MEMORY Storage Engine for MySQL), а уже потом по крону забирать лог из памяти, сжимать его zlib и записывать в бинарные поля таблицы InnoDB. И места будет занимать мало и на скорости загрузки страницы практически никак не отразится. Шутка, не надо так делать.

ARCHIVE Storage Engine — отличная вещь для ведения логов. Использую его в нескольких проектах, и самое главное его преимущество — это прозрачная интеграция, ведь для его внедрения не нужно придумывать какие то схемы и механизмы, достаточно обычных SQL-запросов. Единственное замечание — это отсутствие операций UPDATE/DELETE и TRUNCATE, о чем я уже писал выше.
Посмотрим сколько места занимает таблица ARCHIVE при увеличении числа данных:

 

10 000 23.7 КБ
100 000 156.4 КБ
10 000 000 14.3 МБ

 

Меньше 15 МБ за 10 миллионов небольших записей об ошибках — это действительно неплохо.

При желании Вы можете провести свой benchmark ARCHIVE Storage Engine и убедиться, что он и в самом деле неплох в хранении логов.

Спасибо за внимание, немного ссылок:
ARCHIVE Storage Engine
MariaDB — форк MySQL, который идет с установленным ARCHIVE Storage Engine.


Разработка

Комментарии

  1. PHPDiablo
    20 октября 2011 г. в 23:29

    Спасибо за сравнение. Искал нечто подобное. Результаты удовлетворяют меня, но меня удивил InnoDB — неужели он настолько тормозной? это значит что придется перекладывать все записи на отдельный воркер? ужас..

  2. Vik
    01 декабря 2011 г. в 22:46

    Спасибо, Алмаз. Отличная тема, буду использовать.

  3. Игорь
    04 марта 2013 г. в 15:34

    Сразу скажу, что при сопоставимых со статьей показателях на разных типах, у меня 1000 строк в InnoDB вставляется за 1.7 секунды, а не за 54 секунды.
    Видимо у автора что-то не так с настройками InnoDB. Этот движок однозначно медленнее, чем другие для лога, но не настолько.

Almaz Mubinov © 2010-2023