如何使用 tokudb 每秒执行超过 50.000 次插入?

How to do more then 50.000 inserts per second with tokudb?

目前我正在测试 TokuDB,给我留下了深刻的印象。此时此刻,每秒插入的数据已达到峰值,超过每秒 50.000 次,同时有两个作业 运行。平均插入率在每秒 38.000 到 42.000 次插入之间。

我想要更高,每秒 100.000 次插入,因为我现在需要插入 12 亿计算行,在不久的将来需要插入大约 60 亿行。我想要一些关于如何实现这一点的建议:-)

我当前的设置:

  1. 硬件:VPS 4GB RAM,150GB SSD,2 核:Intel Westmere E56xx/L56xx/X56xx (Nehalem-C) 2.59GHz CPU
  2. 磁盘安装选项:默认值、noatime
  3. OS: 分OS 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 table 布局:

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

I'm aware of the fact that I'm not using any indexes other then the primary key index. This is due to the negative time impact the keys will have on inserting. A cluster key for each table will be created at the end of the insert job.

额外的 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..]

如果是我,我会减少执行语句的数量,减少提交的数量。我假设 AUTO_COMMIT 已启用,因为我们没有看到任何 BEGIN TRANSACTIONCOMMIT 语句。

这是一大堆单独的 INSERTSET 语句。至少对子 table 的插入使用的是多行插入,而不是每行的单独插入语句。

如果我需要它更快,我会

  1. t1 table 生成 id 值,并将这些值包含在 INSERT 语句中
  2. 取消对 LAST_INSERT_ID()
  3. 的调用
  4. t1 使用多行插入(而不是对每一行单独的 INSERT 语句)
  5. 使用BEGIN TRANSACTIONCOMMIT
  6. 运行 一个 单个 进程执行插入 t1 (序列化)以避免潜在的锁争用

如果是 InnoDB,我也会做 SET FOREIGN_KEY_CHECKS=0

代码中已经有大量对 rand 函数的调用;因此,为 t1 递增整数 id 不会改变方向。当我们开始时,我们需要一个查询来获取当前的 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 似乎不太合适。