Статьи

Пользователи OpenStack проливают свет на проблемы тупиковой ситуации в кластере Percona XtraDB

OpenStack_Percona[Эта статья была написана Питером Боросом.]

Мне повезло принять участие в дискуссии Ops о базах данных на саммите OpenStack Atlanta в мае этого года в качестве одного из участников дискуссии. Дискуссия была о проблемах тупиковой ситуации, которые операторы OpenStack видят в кластере Percona XtraDB (конечно, это применимо к любому решению на базе Galera). Я попросил описать то, что они видят, и, как оказалось, нова и нейтрон довольно сильно используют SQL-конструкцию SELECT … FOR UPATE. Это тема, о которой я думал, стоит написать.

Напишите репликацию набора в двух словах (с упрощением)

Любой узел доступен для записи, и репликация происходит в наборах записи. Набор записи — это практически двоичное событие или события двоичного журнала и «некоторые дополнительные вещи». «Некоторые дополнительные вещи» хороши для 2 вещей.

  • Два набора записей можно сравнить и сказать, конфликтуют ли они или нет.
  • Набор записи может быть проверен по базе данных, если это применимо.

Перед фиксацией на исходном узле набор записи передается на все остальные узлы в кластере. Исходный узел проверяет, что транзакция не конфликтует ни с одной из транзакций в очереди приема, и проверяет, применима ли она к базе данных. Этот процесс называется сертификацией. После того, как набор записей сертифицирован, транзакция совершается. Удаленные узлы будут выполнять сертификацию асинхронно по сравнению с локальным узлом. Поскольку сертификация является детерминированной, они получат тот же результат. Также набор записи на удаленных узлах может быть применен позже по этой причине. Этот вид репликации называется практически синхронным, что означает, что передача данных является синхронной, а фактическое применение — нет.

У нас есть хороший поток об этом.

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

ВЫБРАТЬ… ДЛЯ ОБНОВЛЕНИЯ

Конструкция SELECT… FOR UPDATE считывает данные записи в InnoDB и блокирует строки, считываемые из индекса, используемого запросом, а не только строки, которые он возвращает. Учитывая, как работает репликация набора записей, блокировки строк SELECT… FOR UPDATE не реплицируются.

Положить его вместе

Давайте создадим тестовую таблицу.

CREATE TABLE `t` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `ts` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1

И некоторые записи мы можем заблокировать.

pxc1> insert into t values();
Query OK, 1 row affected (0.01 sec)
pxc1> insert into t values();
Query OK, 1 row affected (0.01 sec)
pxc1> insert into t values();
Query OK, 1 row affected (0.01 sec)
pxc1> insert into t values();
Query OK, 1 row affected (0.00 sec)
pxc1> insert into t values();
Query OK, 1 row affected (0.01 sec)
pxc1> select * from t;
+----+---------------------+
| id | ts                  |
+----+---------------------+
|  1 | 2014-06-26 21:37:01 |
|  4 | 2014-06-26 21:37:02 |
|  7 | 2014-06-26 21:37:02 |
| 10 | 2014-06-26 21:37:03 |
| 13 | 2014-06-26 21:37:03 |
+----+---------------------+
5 rows in set (0.00 sec)

На первом узле заблокируйте запись.

pxc1> start transaction;
Query OK, 0 rows affected (0.00 sec)
pxc1> select * from t where id=1 for update;
+----+---------------------+
| id | ts                  |
+----+---------------------+
|  1 | 2014-06-26 21:37:01 |
+----+---------------------+
1 row in set (0.00 sec)

На втором обновите его с помощью транзакции автоматического подтверждения.

pxc2> update t set ts=now() where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
pxc1> select * from t;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

Давайте рассмотрим, что здесь произошло. Локальная блокировка записи, удерживаемая запущенным переходом на pxc1, не сыграла никакой роли в репликации или сертификации (репликация происходит во время фиксации, там еще не было фиксации). Как только узел получил набор записи от pxc2, у этого набора записи возник конфликт с локальной транзакцией, все еще находящейся в процессе выполнения. В этом случае наша транзакция на pxc1 должна быть откатана. Это тоже тип конфликта, но здесь конфликт не улавливается временем сертификации. Это называется прерыванием грубой силы. Это происходит, когда транзакция, выполняемая подчиненным потоком, конфликтует с транзакцией, находящейся в полете на узле. В этом случае выигрывает первый коммит (который уже реплицирован), а исходная транзакция прерывается. Джей Янссен обсуждает многоузловой писать конфликты подробно в этом сообщении,

То же самое происходит, когда 2 узла удерживают блокировки записи с помощью выбора для обновления. Какой бы узел не зафиксировался первым, победит, другая транзакция столкнется с ошибкой взаимоблокировки и будет откатана. Поведение правильное.

Вот та же транзакция SELECT… FOR UPDATE, перекрывающаяся на 2 узлах.

pxc1> start transaction;
Query OK, 0 rows affected (0.00 sec)
pxc2> start transaction;
Query OK, 0 rows affected (0.00 sec)
pxc1> select * from t where id=1 for update;
+----+---------------------+
| id | ts                  |
+----+---------------------+
|  1 | 2014-06-26 21:37:48 |
+----+---------------------+
1 row in set (0.00 sec)
pxc2> select * from t where id=1 for update;
+----+---------------------+
| id | ts                  |
+----+---------------------+
|  1 | 2014-06-26 21:37:48 |
+----+---------------------+
1 row in set (0.00 sec)
pxc1> update t set ts=now() where id=1;
Query OK, 1 row affected (0.01 sec)
Rows matched: 1  Changed: 1  Warnings: 0
pxc2> update t set ts=now() where id=1;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1  Changed: 1  Warnings: 0
pxc1> commit;
Query OK, 0 rows affected (0.00 sec)
pxc2> commit;
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

Где это происходит в OpenStack?

Например, в OpenStack Nova (вычислительный проект в OpenStack) для отслеживания использования квот используется конструкция SELECT… FOR UPDATE.

# User@Host: nova[nova] @  [10.10.10.11]  Id:   147
# Schema: nova  Last_errno: 0  Killed: 0
# Query_time: 0.001712  Lock_time: 0.000000  Rows_sent: 4  Rows_examined: 4  Rows_affected: 0
# Bytes_sent: 1461  Tmp_tables: 0  Tmp_disk_tables: 0  Tmp_table_sizes: 0
# InnoDB_trx_id: C698
# QC_Hit: No  Full_scan: Yes  Full_join: No  Tmp_table: No  Tmp_table_on_disk: No
# Filesort: No  Filesort_on_disk: No  Merge_passes: 0
#   InnoDB_IO_r_ops: 0  InnoDB_IO_r_bytes: 0  InnoDB_IO_r_wait: 0.000000
#   InnoDB_rec_lock_wait: 0.000000  InnoDB_queue_wait: 0.000000
#   InnoDB_pages_distinct: 2
SET timestamp=1409074305;
SELECT quota_usages.created_at AS quota_usages_created_at, quota_usages.updated_at AS quota_usages_updated_at, quota_usages.deleted_at AS quota_usages_deleted_at, quota_usages.deleted AS quota_usages_deleted, quota_usages.id AS quota_usages_id, quota_usages.project_id AS quota_usages_project_id, quota_usages.user_id AS quota_usages_user_id, quota_usages.resource AS quota_usages_resource, quota_usages.in_use AS quota_usages_in_use, quota_usages.reserved AS quota_usages_reserved, quota_usages.until_refresh AS quota_usages_until_refresh
FROM quota_usages
WHERE quota_usages.deleted = 0 AND quota_usages.project_id = '12ce401aa7e14446a9f0c996240fd8cb' FOR UPDATE;

Так откуда это?

Эти конструкции генерируются SQLAlchemy с использованием with_lockmode (‘update’). Даже в pydoc от nova рекомендуется по возможности избегать with_lockmode (‘update’) . Репликация Galera не упоминается среди причин, чтобы избежать этой конструкции, но знание того, сколько развертываний OpenStack используют Galera для высокой доступности (либо кластер Percona XtraDB, кластер MariaDB Galera, либо собственный mysql-wsrep Codership), это может быть очень веской причиной чтобы избежать этого. Решение, предложенное в связанном выше pydoc, также является хорошим, используя INSERT INTO… ON DUPLICATE KEY UPDATE — это единичная атомарная запись, которая будет реплицироваться, как и ожидалось, и будет также правильно отслеживать использование квот.

Самый простой способ преодолеть эту проблему с точки зрения оператора — использовать только один узел записи для этих типов транзакций. Обычно это включает изменение конфигурации на уровне балансировщика нагрузки. См. Этот пост для возможных конфигураций балансировки нагрузки.