Как сделать более 50 000 вставок в секунду с помощью tokudb?

В настоящее время я тестирую TokuDB и очень впечатлен. На данный момент количество вставок в секунду достигло чуть более 50 000 в секунду при одновременном выполнении двух заданий. Средняя скорость вставки составляет от 38 000 до 42 000 вставок в секунду.

Я хотел бы пойти еще выше, 100 000 вставок в секунду, так как мне нужно будет вставить 1,2 миллиарда вычисляемых строк на данный момент и еще около 6 миллиардов в ближайшем будущем. Хочу посоветовать, как этого добиться :-)

Моя текущая установка:

  1. Аппаратное обеспечение: VPS с 4 ГБ ОЗУ, 150 ГБ SSD, 2 ядра: Intel Westmere E56xx/L56xx/X56xx (Nehalem-C), процессор 2,59 ГГц.
  2. Варианты монтирования диска: по умолчанию, noatime
  3. ОС: CentOS 6.8 64bit
  4. База данных: Percona Server 5.7.14-8

Настройки My.cnf:

# TokuDB #
tokudb_cache_size = 2G
tokudb_commit_sync = 0
tokudb_fsync_log_period = 1000

Макет таблицы TokuDB:

CREATE TABLE `t1` (
  `id` int(15) NOT NULL AUTO_INCREMENT,
  `m_id` int(11) NOT NULL,
  `c1` decimal(6,2) DEFAULT NULL,
  `c2` decimal(6,2) DEFAULT NULL,
  `c3` decimal(6,2) DEFAULT NULL,
  `c4` decimal(6,2) DEFAULT NULL,
  `c5` decimal(6,2) DEFAULT NULL,
  `c6` decimal(6,2) DEFAULT NULL,
  `c7` decimal(6,2) DEFAULT NULL,
  `factor` decimal(4,2) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1

CREATE TABLE `t2` (
  `id` int(15) NOT NULL AUTO_INCREMENT,
  `v_id` int(15) NOT NULL,
  `pid` int(11) DEFAULT NULL,
  `amount` decimal(6,2) DEFAULT NULL,
  `unit` int(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=TokuDB DEFAULT CHARSET=latin1

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

Дополнительный параметр командной строки MySQL:

SET unique_checks=OFF;

Почему-то я не могу получить это в my.cnf.. Если кто-то знает, как это сделать, это будет очень признательно (в настоящее время unique_checks = off блокирует запуск MySQL из-за неизвестной переменной в my.cnf) .

Операторы SQL сгруппированы в пакеты по 15 000. PHP-скрипт генерирует операторы SQL и отправляет запрос через mysqli_multiquery на сервер MySQL:

<?PHP        
    foreach (generateCombinations($Arr) as $c) {

            $QueryBatch[] = "insert into t1 values (NULL"
                            . ", " . $record->id
                            . ", " . rand(1, 35)
                            . ", " . rand(1, 140)
                            . ", " . rand(1, 20)
                            . ", NULL"
                            . ", " . rand(1, 14)
                            . ", " . rand(1, 300)
                            . ", " . rand(1, 4)
                            . ", NULL );";
            $QueryBatch[] = "SET @t1id = LAST_INSERT_ID();";

            $cntBatch++;

            $pquery = array();
            foreach ( $c as $key => $pid){

                    if ( is_null($pid) )
                            continue;

                    $pquery[] = "(NULL, @t1id, " . $pid . ", " . rand(1, 800) . ", 0)";

                    $cntBatch++;
            }

            $QueryBatch[] = "insert into t2 values " . implode(',', $pquery) . ";";

            if ($cntBatch > 15000) {

                    $query = implode($QueryBatch);

                    if ( $mysqli->multi_query($query) ){
                            while ($mysqli->next_result()) {;}
                    } else {
                            printf("Errormessage: %s\n", $mysqli->error);
                            echo $query . "\n";
                    }

                    $cntBatch = 0;
                    unset($QueryBatch);
            }

    }
?>

Пример оператора вставки SQL:

insert into t1 values (NULL, 1 , 30, 100, 15, NULL, 10, 250, 2, NULL );
SET @t1id = LAST_INSERT_ID();
insert into t2 values (NULL, @t1id, 1, 750, 0),(NULL, @t1id, 1, 600, 0),(NULL, @t1id, 1, 500, 0),(NULL, @t1id, 1, 400, 0),(NULL, @t1id, 1, 300, 0),(NULL, @t1id, 1, 200, 0),(NULL, @t1id, 1, 100, 0);
insert into t1 values (NULL, 2 , 25, 95, 14, NULL, 11, 200, 3, NULL );
SET @t1id = LAST_INSERT_ID();
insert into t2 values (NULL, @t1id, 1, 600, 0),(NULL, @t1id, 1, 100, 0),(NULL, @t1id, 1, 300, 0),(NULL, @t1id, 1, 443, 0),(NULL, @t1id, 1, 521, 0),(NULL, @t1id, 1, 213, 0),(NULL, @t1id, 1, 433, 0);
[.. At least 14982 more..]

person Robbert    schedule 07.10.2016    source источник


Ответы (1)


Если бы это был я, я бы сократил количество выполняемых операторов и количество коммитов. Я предполагаю, что AUTO_COMMIT включен, учитывая, что мы не видим никаких операторов BEGIN TRANSACTION или COMMIT.

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

Если бы мне нужно было, чтобы это было быстро, я бы

  1. сгенерировать значения id для таблицы t1 и включить их в оператор INSERT
  2. покончить с призывом к LAST_INSERT_ID()
  3. использовать многострочную вставку для t1 (вместо отдельного оператора INSERT для каждой строки)
  4. используйте BEGIN TRANSACTION и COMMIT
  5. запустить одиночный процесс для выполнения вставок в t1 (сериализация), чтобы избежать потенциальной конкуренции за блокировки

Если бы это было для InnoDB, я бы тоже сделал SET FOREIGN_KEY_CHECKS=0.

В коде уже есть куча вызовов функции rand; поэтому увеличение целого числа id для t1 не сдвинет стрелку. Когда мы начнем, нам понадобится запрос, чтобы получить текущее значение AUTO_INCREMENT или получить MAX (id), в зависимости от того, что...

По сути, я бы сократил количество выполняемых операторов и выполнял больше работы для каждого оператора, а перед каждым COMMIT выполнял больше работы.

Вставка десяти (10) t1 строк на оператор значительно сократит количество операторов, которые необходимо выполнить.

BEGIN TRANSACTION;
-- insert ten rows into t1
INSERT INTO t1 (id,m_id,c1,c2,c3,c4,c5,c6,c7,factor) VALUES
 (444055501, 1 , 30, 100, 15, NULL, 10, 250, 2, NULL )
,(444055502, 2 , 25, 95, 14, NULL, 11, 200, 3, NULL )
, ...
,(444055510, 10 , 7, 45, 12, NULL, 10, 300, 4, NULL )
;
-- batch together the t2 rows associated with the ten t1 rows we just inserted
INSERT INTO t2 VALUES
-- 444055501  
 (NULL, 444055501, 1, 750, 0)
,(NULL, 444055501, 1, 600, 0)
,(NULL, 444055501, 1, 500, 0)
,(NULL, 444055501, 1, 400, 0)
,(NULL, 444055501, 1, 300, 0)
,(NULL, 444055501, 1, 200, 0)
,(NULL, 444055501, 1, 100, 0)
-- 444055502  
,(NULL, 444055502, 1, 600, 0)
,(NULL, 444055502, 1, 100, 0)
,(NULL, 444055502, 1, 300, 0)
,(NULL, 444055502, 1, 443, 0)
,(NULL, 444055502, 1, 521, 0)
,(NULL, 444055502, 1, 213, 0)
,(NULL, 444055502, 1, 433, 0)
-- 444055503
, ...
;

-- another ten rows into t1
INSERT INTO t1 (id,m_id,c1,c2,c3,c4,c5,c6,c7,factor) VALUES
 (444055511, 11 , 27, 94, 15, NULL, 10, 250, 11, NULL )
,(444055512, 12 , 24, 93, 14, NULL, 11, 200, 12, NULL )
, ...
,(444055520, 10 , 7, 45, 12, NULL, 10, 300, 4, NULL )
;
INSERT INTO t2 VALUES
 (NULL, 444055511, 1, 820, 0)
,(NULL, 444055511, 1, 480, 0)
, ...
;

-- repeat INSERTs into t1 and t2, and after 1000 loops
-- i.e. 10,000 t1 rows, do a commit
COMMIT;
BEGIN TRANSACTION;
INSERT INTO t1 ...

ЗАГРУЗИТЬ ФАЙЛ ДАННЫХ

Любое обсуждение производительности вставок было бы неполным без хотя бы упоминания LOAD DATA INFILE.

Для лучшей производительности это не может быть лучше. Но так как у нас нет данных в файле и у нас нет ключевых значений (необходимых для внешнего ключа в t2, и у нас есть все вызовы rand для генерации данных, LOAD DATA INFILE не кажется быть в хорошей форме.

person spencer7593    schedule 07.10.2016
comment
Уважаемый Spencer7593, спасибо за подробный ответ! Я понимаю, что вы говорите, и согласен, но дело в том; Мне нужно знать идентификатор для t1, чтобы соответствующие записи в t2 относились к нему. Поле идентификатора в t1 является автоматически увеличивающимся. Вы бы предложили удалить автоинкремент и создать идентификатор в скрипте? Это можно сделать, поскольку в основном только одно задание будет создавать записи. - person Robbert; 08.10.2016
comment
Можно указать значение для столбца AUTO_INCREMENT, не удаляя и не отключая AUTO_INCREMENT. Мы получаем поведение AUTO_INCREMENT, указав значение NULL. Я предлагаю, чтобы вместо предоставления значения NULL мы генерировали значение id и передавали это значение как во вставке, так и в t1, и в t2. Вот что я бы сделал, чтобы ускорить его... сократить количество операторов SQL и количество COMMIT. - person spencer7593; 08.10.2016
comment
В InnoDB мы настраиваем поведение auto_increment для получения последовательных значений auto_increment. Таким образом, многострочная вставка, мы знаем, что первая строка — LAST_INSERT_ID()+0, вторая вставленная строка — LAST_INSERT_ID()+1, вплоть до LAST_INSERT_ID()+ROW_COUNT(). Если бы мне пришлось использовать значения AUTO_INCREMENT, я бы группировал вставки в t1 (вставка нескольких строк), SELECT LAST_INSERT_ID() INTO @lid, а затем мои вставки в t2 использовали бы @lid+0 для строк, связанных с первой вставленной строкой t1, @lid+1 для строк, связанных со вторым t1. строка вставлена. (Мое предостережение заключается в том, что я не уверен, что поведение AUTO_INCREMENT с tokudb такое же, как у InnoDB.) - person spencer7593; 08.10.2016
comment
Я выполнил ваши предложения, и в настоящее время скорость вставки составляет от 120 000 до 145 000 записей в секунду. Однако, поскольку я только что обновил свой VPS до 4 ядер и 8 ГБ оперативной памяти, я также увеличил размер tokudb_cache_size до 5 ГБ. Я хотел бы знать, сможем ли мы подняться еще выше (200 000+/с) :-) - person Robbert; 08.10.2016
comment
Некоторые характеристики аппаратного обеспечения: общая загрузка ЦП составляет 25%, дисковые операции ввода-вывода в секунду — 40 и могут увеличиваться до 1000 операций ввода-вывода в секунду. - person Robbert; 08.10.2016
comment
Если tokudb позволяет отключить проверку внешнего ключа (как это делает InnoDB), вы можете запускать два разных соединения с базой данных для одновременной вставки... одно соединение работает с таблицей t1, другое подрыв одновременно к таблице t2. Тест с отключенным ведением двоичного журнала (sql_log_bin). Я сильно подозреваю, что одним из ваших больших узких мест были отдельные операторы, все накладные расходы на синтаксический анализ, проверку синтаксиса, выполнение семантической проверки, подготовку плана выполнения и выполнение плана, получение блокировок, создание отката, запись в журнал... - person spencer7593; 08.10.2016