Статьи

Исследование задержки репликации MySQL в кластере Percona XtraDB

 

Мне было любопытно проверить, как ведет себя Percona XtraDB Cluster, когда дело доходит до задержки репликации MySQL — или, еще лучше, назвать это задержкой распространения данных. Было интересно посмотреть, когда я смогу получить устаревшие данные, прочитанные с других узлов кластера, после выполнения записи в какой-то конкретный узел. Чтобы проверить это, я написал довольно простой скрипт (вы можете найти его в конце поста), который подключается к одному узлу в кластере, выполняет обновление, а затем немедленно выполняет чтение со второго узла. Если данные уже были распространены — хорошо, если нет, мы продолжим повторное чтение, пока оно, наконец, не распространится, и затем измерим задержку. Это используется, чтобы видеть всякий раз, когда приложение может видеть любые устаревшие чтения.

У меня 3 узла Percona XtraDB Cluster, которые общаются через выделенную кластерную сеть 1 Гбит (DPE1, DPE2, DPE3), и я провожу тест с 4-го сервера (SMT2), так что это довольно реалистичная установка с точки зрения типичной задержки центра обработки данных, хотя серверное оборудование не самое новое.

Сначала давайте посмотрим на базовый уровень, когда кластер не загружен, но запускает скрипт, который выполняет запись в DPE1 и сразу читает из DPE2

Summary: 94 out of 10000 rounds (0.94%) Delay distribution: Min: 0.71 ms; Max: 2.16 ms Avg: 0.89 ms

Эти результаты говорят мне 2 вещи. Первая репликация по умолчанию в кластере Percona XtraDB является асинхронной с точки зрения распространения данных — требуется время (хотя и короткое в данном случае), чтобы изменения, зафиксированные на одном узле, стали видимыми для другого. Во-вторых, на самом деле дела идут неплохо: менее 1% тестов способны увидеть любую несогласованность, а задержка составляет в среднем менее 1 мс с довольно стабильными результатами.

Но мы не настраиваем кластеры на бездействие, верно? Итак, перейдем к другому тесту, теперь запускаю загрузку Sysbench на DPE1. При параллельности 32 это соответствует довольно значительной нагрузке.

sysbench --test=oltp --mysql-user=root --mysql-password="" --oltp-table-size=1000000 --num-threads=32 --init-rng=on --max-requests=0 --oltp-auto-inc=off --max-time=3000 run

Результаты становятся следующими:

Summary: 3901 out of 10000 rounds (39.01%) Delay distribution: Min: 0.66 ms; Max: 201.36 ms Avg: 3.81 ms Summary: 3893 out of 10000 rounds (38.93%) Delay distribution: Min: 0.66 ms; Max: 42.9 ms Avg: 3.76 ms

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

Теперь, если мы запустим sysbench на DPE2 (нагрузка на узел, с которого мы читаем)

Summary: 3747 out of 10000 rounds (37.47%) Delay distribution: Min: 0.86 ms; Max: 108.15 ms Avg: 8.62 ms Summary: 3721 out of 10000 rounds (37.21%) Delay distribution: Min: 0.81 ms; Max: 291.81 ms Avg: 8.54 ms

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

Вспомним хотя бы то, что Sysbench OLTP имеет лишь сравнительно небольшую часть записей. Что делать, если мы посмотрим на рабочие нагрузки, которые составляют 100% пишет. Мы можем сделать это с помощью Sysbench, например:

sysbench --test=oltp --oltp-test-mode=nontrx --oltp-nontrx-mode=update_key --mysql-user=root --mysql-password="" --oltp-table-size=1000000 --num-threads=32 --init-rng=on --max-requests=0 --max-time=3000 run

Запустив эту загрузку на DPE1, я получаю:

Summary: 1062 out of 10000 rounds (10.62%) Delay distribution: Min: 0.71 ms; Max: 285.07 ms Avg: 3.21 ms Summary: 1113 out of 10000 rounds (11.13%) Delay distribution: Min: 0.81 ms; Max: 275.94 ms Avg: 5.06 ms

Сюрприз! результаты на самом деле лучше, чем если бы мы ставили смешанную нагрузку, поскольку мы можем наблюдать любую задержку только примерно в 11%.

Однако если мы запустим ту же самую боковую нагрузку на DPE2, мы получим:

Summary: 5349 out of 10000 rounds (53.49%) Delay distribution: Min: 0.81 ms; Max: 519.61 ms Avg: 5.02 ms Summary: 5355 out of 10000 rounds (53.55%) Delay distribution: Min: 0.81 ms; Max: 526.95 ms Avg: 5.06 ms

Что является худшим результатом: более 50% выборок дают противоречивые данные, а средняя задержка для тех, кто превышает 5 мс и выбросы, составляет полсекунды.

Из этих результатов я прочитал, что боковая нагрузка на узел TO, на котором распространяются обновления, вызывает наибольшую задержку.

В этот момент я вспомнил, что могу провести еще один тест. Что если я поставлю боковую нагрузку на сервер DPE3, с которой я вообще не касаюсь теста?

Summary: 833 out of 10000 rounds (8.33%) Delay distribution: Min: 0.66 ms; Max: 353.61 ms Avg: 2.76 ms

Неудивительно, что DPE3 не читается напрямую или не записывается в нагрузку, что должно привести к минимальным задержкам распространения данных от DPE1 к DPE2.

Задержка распространения, которую мы наблюдали в тесте, пока достаточно хороша, но это не синхронное поведение репликации — мы все еще не можем обработать кластер, как если бы он был одним сервером из общего приложения. Правильно. Конфигурация по умолчанию для кластера Percona XtraDB на этом этапе заключается в асинхронной репликации данных, но, тем не менее, гарантируется отсутствие конфликтов и несогласованности данных, после чего обновления выполняются на нескольких узлах. Существует опция, которую вы можете включить, чтобы получить полностью синхронную репликацию:

mysql> set global wsrep_causal_reads=1;
Query OK, 0 rows affected (0.00 sec)

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

Все идет нормально. Мы можем заставить кластер обрабатывать значительную нагрузку с небольшими транзакциями и при этом иметь очень уважительную задержку распространения данных, или мы можем включить опцию wsrep_causal_reads = 1 и получить полную согласованность данных. Но что произойдет, если у нас будут большие транзакции? Чтобы проверить это, я создал копию таблицы sbtest и во время выполнения теста запустю длинное обновление, чтобы увидеть, как влияет задержка:

mysql> update sbtest2 set k=k+1;
Query OK, 1000000 rows affected (1 min 14.12 sec)
Rows matched: 1000000  Changed: 1000000  Warnings: 0

Запустив этот запрос в окне DPE1, я получаю следующий результат:

...
Result Mismatch for Value 48;  Retries: 1   Delay: 0.76 ms
Result Mismatch for Value 173;  Retries: 1   Delay: 1.21 ms
Result Mismatch for Value 409;  Retries: 1   Delay: 0.86 ms
Result Mismatch for Value 460;  Retries: 142459   Delay: 46526.7 ms
Result Mismatch for Value 461;  Retries: 65   Delay: 22.92 ms
Result Mismatch for Value 464;  Retries: 1   Delay: 0.71 ms
Result Mismatch for Value 465;  Retries: 1   Delay: 0.76 ms
...
Summary: 452 out of 10000 rounds (4.52%)  Delay distribution: Min: 0.66 ms;  Max: 46526.7 ms Avg: 104.28 ms

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

Если мы выполним тот же запрос на DPE2, произойдет интересная вещь. Скрипт не показывает каких-либо задержек распространения данных, но он явно останавливается, как я полагаю, потому что инструкция UPDATE, выданная DPE1, заблокирована на некоторое время. Чтобы проверить эту идею, я решил использовать сценарий sysbench с очень простыми точечными запросами на обновление, чтобы увидеть, получим ли мы какие-либо существенные задержки. Мой базовый прогон на DPE1 выглядит следующим образом:

root@dpe01:/etc/mysql# sysbench --test=oltp --oltp-auto-inc=off --oltp-test-mode=nontrx --oltp-nontrx-mode=update_key  --mysql-user=root --mysql-password="" --oltp-table-size=1000000 --num-threads=1  --init-rng=on --max-requests=0 --max-time=300 run
....
    per-request statistics:
         min:                                  0.68ms
         avg:                                  0.88ms
         max:                                306.80ms
         approx.  95 percentile:               0.94ms
....

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

    per-request statistics:
         min:                                  0.69ms
         avg:                                  1.12ms
         max:                              52334.76ms
         approx.  95 percentile:               0.97ms

Как мы видим, происходит обновление в течение 50+ секунд, опять же, пока идет сертификация. Таким образом, сертификация не только задерживает распространение данных, но может остановить обновления, сделанные для разных таблиц на разных узлах.

Резюме:

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

Приложение:
Как и обещал скрипт, который я использовал для тестирования.

<!--?

# The idea with this script is as follows. We have 2 nodes. We write to one node and when read from second node
# To see whenever we get the same data or different

$writer_host="dpe01";
$reader_host="dpe02";
$user="test";
$password="test";
$table="test.sbtest";

$increment=2;
$offset=1;
$max_id=1000;
$rounds=10000;

$writer=new mysqli($writer_host,$user,$password);
$reader=new mysqli($reader_host,$user,$password);

$total_delay=0;
$min_delay=100000000;
$max_delay=0;
$delays=0;
$sum_delay=0;
for($val=0; $val<$rounds;$val++)
{

  $id=rand(1,$max_id);
  $id=floor($id/$increment)*$increment+$offset;
  $writer--->query("UPDATE $table set k=$val where id=$id");
  $tw=microtime(true);
  /* Loop while we get the right result */
  $retries=0;
  while(true)
  {
    $result=$reader->query("SELECT k from $table where id=$id");
    $row=$result->fetch_row();
    if ($row[0]!=$val)
        $retries++;
    else
        {
            $tr=microtime(true);
            break;
        }
    $result->close();
  }
  if ($retries!=0)  /* If we had to retry compute stats */
  {
    $delay=round(($tr-$tw)*1000,2);
    $delays++;
    $sum_delay+=$delay;
    $min_delay=min($min_delay,$delay);
    $max_delay=max($max_delay,$delay);
    echo("Result Mismatch for Value $val;  Retries: $retries   Delay: $delay ms\n");
  }
}
 if ($delays>0)
  $avg_delay=round($sum_delay/$delays,2);
 else
  $avg_delay=0;
  $delay_pct=round($delays/$val*100,3);

 echo("Summary: $delays out of $val rounds ($delay_pct%)  Delay distribution: Min: $min_delay ms;  Max: $max_delay ms Avg: $avg_delay ms\n");

?>