Мне было любопытно проверить, как ведет себя 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"); ?>