Статьи

Проблемы масштабируемости InnoDB из-за таблиц без первичных ключей

Этот пост принадлежит Ovais Tariq из  блога MySQL Performance.

Каждый день, вероятно, проводится работа по повышению производительности механизма хранения InnoDB и устранению узких мест и проблем масштабируемости. Следовательно, был еще один, который я хотел бы выделить:

Проблемы масштабируемости из-за таблиц без первичных ключей

Эта проблема с масштабируемостью вызвана использованием таблиц без первичных ключей. Эта проблема обычно проявляется как конфликт на мьютексе dict_sys InnoDB. Теперь мьютекс dict_sys контролирует доступ к словарю данных. Этот мьютекс используется в разных местах. Я упомяну только некоторые из них:

  • Во время таких операций, как открытие и закрытие ручек стола или
  • При доступе к таблицам I_S или
  • Во время отмены недавно вставленной строки или
  • Во время других операций модификации словаря данных, таких как CREATE TABLE или
  • В подсистеме «Постоянная статистика», между прочим.

Конечно, этот список не является исчерпывающим, но должен дать вам хорошее представление о том, насколько интенсивно он используется

Но дело в том, что когда вы в основном отлаживаете конфликт, связанный со структурой элемента управления словаря данных, вы начинаете смотреть на то, что напрямую связано с модификациями словаря данных. Вы ищете выполнение CREATE TABLE, DROP TABLE, TRUNCATE TABLE и т. Д. Но что, если ничего из этого на самом деле не вызывает конфликт на мьютексе dict_sys? Знаете ли вы, что при генерации значений «row-id», для таблиц без явных первичных ключей или без необнуляемых уникальных ключей, получается мьютекс dict_sys. Таким образом, INSERT для таблиц с неявными первичными ключами является предметом спора для всей системы InnoDB.

Давайте также взглянем на соответствующий исходный код.

Во-первых, ниже находится функция, которая выполняет выделение идентификатора строки, который определен в хранилище файлов / innobase / row / row0ins.cc

3060 /***********************************************************//**
3061 Allocates a row id for row and inits the node->index field. */
3062 UNIV_INLINE
3063 void
3064 row_ins_alloc_row_id_step(
3065 /*======================*/
3066         ins_node_t*     node)   /*!< in: row insert node */ 3067 { 3068         row_id_t        row_id; 3069  3070         ut_ad(node->state == INS_NODE_ALLOC_ROW_ID);
3071
3072         if (dict_index_is_unique(dict_table_get_first_index(node->table))) {
3073
3074                 /* No row id is stored if the clustered index is unique */
3075
3076                 return;
3077         }
3078
3079         /* Fill in row id value to row */
3080
3081         row_id = dict_sys_get_new_row_id();
3082
3083         dict_sys_write_row_id(node->row_id_buf, row_id);
3084 }

Во-вторых, ниже находится функция, которая фактически генерирует идентификатор строки, который определен в хранилище файлов / innobase / include / dict0boot.ic

 26 /**********************************************************************//**
 27 Returns a new row id.
 28 @return the new id */
 29 UNIV_INLINE
 30 row_id_t
 31 dict_sys_get_new_row_id(void)
 32 /*=========================*/
 33 {
 34         row_id_t        id;
 35
 36         mutex_enter(&(dict_sys->mutex));
 37
 38         id = dict_sys->row_id;
 39
 40         if (0 == (id % DICT_HDR_ROW_ID_WRITE_MARGIN)) {
 41
 42                 dict_hdr_flush_row_id();
 43         }
 44
 45         dict_sys->row_id++;
 46
 47         mutex_exit(&(dict_sys->mutex));
 48
 49         return(id);
 50 }

Наконец, я хотел бы поделиться результатами нескольких тестов, которые я провел, чтобы показать вам, как это влияет на производительность.

Сравнительный анализ отсутствия первичных ключей

Прежде всего, позвольте мне поделиться информацией о хосте, который использовался в тестах. Я также поделюсь версией MySQL и используемой конфигурацией InnoDB.

аппаратные средства

Хост был «hi1.4xlarge» экземпляром Amazon EC2. Экземпляр поставляется с 16 виртуальными ЦП и 60,5 ГБ памяти. Хранилище экземпляров состоит из 2 × 1024 томов хранения на SSD-накопителе, и экземпляр подключен к 10-гигабитной сети Ethernet. Так что производительность IO очень приличная. Я создал массив RAID 0 из двух томов хранилища экземпляров и создал файловую систему XFS на результирующем программном томе RAID 0. Эта конфигурация позволит нам получить максимально возможную производительность ввода-вывода из экземпляра.

MySQL

Используемая версия MySQL была 5.5.34 MySQL Community Server, и конфигурация InnoDB выглядела следующим образом:

innodb-flush-method            = O_DIRECT
innodb-log-files-in-group      = 2
innodb-log-file-size           = 512M
innodb-flush-log-at-trx-commit = 2
innodb-file-per-table          = 1
innodb-buffer-pool-size        = 42G
innodb-buffer-pool-instances   = 8
innodb-io-capacity             = 10000
innodb_adaptive_hash_index     = 1

Я провел два разных типа тестов, и оба они были сделаны с помощью sysbench.

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

Однорядные вставки

Таблицы были сгенерированы следующим образом для теста с таблицами с первичными ключами:

sysbench --test=/root/sysbench/sysbench/tests/db/insert.lua --oltp-tables-count=64 --oltp-table-size=1000000 --mysql-table-engine=innodb --mysql-user=root --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-db=test prepare

Это привело к созданию следующей таблицы:

CREATE TABLE `sbtest1` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `k` int(10) unsigned NOT NULL DEFAULT '0',
  `c` char(120) NOT NULL DEFAULT '',
  `pad` char(60) NOT NULL DEFAULT '',
  PRIMARY KEY (`id`),
  KEY `k_1` (`k`)
) ENGINE=InnoDB

При этом таблицы без первичных ключей генерировались следующим образом:

sysbench --test=/root/sysbench/sysbench/tests/db/insert.lua --oltp-tables-count=64 --oltp-table-size=1000000 --oltp-secondary --mysql-table-engine=innodb --mysql-user=root --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-db=test prepare

В результате были созданы таблицы со следующей структурой:

CREATE TABLE `sbtest1` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `k` int(10) unsigned NOT NULL DEFAULT '0',
  `c` char(120) NOT NULL DEFAULT '',
  `pad` char(60) NOT NULL DEFAULT '',
  KEY `xid` (`id`),
  KEY `k_1` (`k`)
) ENGINE=InnoDB

Фактический тест для таблицы с первичными ключами был выполнен следующим образом:

sysbench --test=/root/sysbench/sysbench/tests/db/insert.lua --oltp-tables-count=64 --oltp-table-size=1000000 --oltp-dist-type=uniform --mysql-table-engine=innodb --mysql-user=root --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-db=test --max-time=300 --num-threads=16 --max-requests=0 --report-interval=1 run

Фактический тест для таблицы без первичных ключей выполнялся следующим образом:

sysbench --test=/root/sysbench/sysbench/tests/db/insert.lua --oltp-tables-count=64 --oltp-table-size=1000000 --oltp-secondary --oltp-dist-type=uniform --mysql-table-engine=innodb --mysql-user=root --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-db=test --max-time=300 --num-threads=16 --max-requests=0 --report-interval=1 run

Обратите внимание, что тесты выполнялись с тремя вариациями количества параллельных потоков, используемых sysbench: 16, 32 и 64.
Ниже показано, как выглядят графики для каждого из этих тестов.

Пишет в секунду 16 потоков
Пишет в секунду 32 потока
Пишет в секунду 64 потока

Некоторые ключевые моменты, которые следует отметить из графиков, заключаются в том, что пропускная способность INSERT для таблиц без явных первичных ключей никогда не превышает 87% пропускной способности INSERT для таблиц с определенными первичными ключами. Кроме того, когда мы увеличиваем параллелизм, начинают появляться нисходящие пики. Это становится более очевидным, когда мы переходим к параллельности из 64 потоков. Это ожидается, потому что конкуренция должна увеличиваться по мере увеличения параллелизма операций, которые конкурируют с мьютексом dict_sys.

Теперь давайте посмотрим, как это влияет на производительность массовой загрузки.

Навальные грузы

Основная загрузка таблиц с первичными ключами выполнялась следующим образом:

sysbench --test=/root/sysbench/sysbench/tests/db/parallel_prepare.lua --oltp-tables-count=64 --oltp-table-size=1000000 --mysql-table-engine=innodb --mysql-user=root --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-db=test --num-threads=16 run

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

sysbench --test=/root/sysbench/sysbench/tests/db/parallel_prepare.lua --oltp-tables-count=64 --oltp-table-size=1000000 --oltp-secondary --mysql-table-engine=innodb --mysql-user=root --mysql-host=127.0.0.1 --mysql-port=3306 --mysql-db=test --num-threads=16 run

Обратите внимание, что тесты снова выполнялись с тремя вариациями в количестве одновременных потоков, используемых sysbench: 16, 32 и 64.
Ниже показано, как изображение изображается на графике.

Параллельная массовая загрузка таблиц

Здесь снова вы можете увидеть, как увеличивается время массовой загрузки при увеличении количества одновременных потоков. Это против указывает на увеличение конкуренции за мьютекс dict_sys. С 16 потоками время массовой загрузки для таблиц без первичных ключей на 107% больше, чем время массовой загрузки для таблиц с первичными ключами. Это увеличивает до 116% с 32 потоками и, наконец, 124% с 64 потоками.

Вывод

Таблицы без первичных ключей вызывают широкий диапазон разногласий, поскольку они полагаются на получение мьютекса dict_sys для генерации значений идентификатора строки. Этот мьютекс используется в критических местах в InnoDB. Следовательно, влияние большого количества INSERT на таблицы без первичных ключей не только изолировано от этой таблицы, но и очень широко видно. Я неоднократно видел таблицы без использования первичных ключей во многих различных сценариях, которые включают в себя простые INSERT для этих таблиц, а также многострочные INSERT в результате, например, INSERT… SELECT для таблицы, которая находится в процессе временно создан. Совет всегда должен иметь первичные ключи в ваших таблицах. Надеюсь, мне удалось подчеркнуть истинное влияние отсутствия первичных ключей.