Прогресс не стоит на месте, поэтому причины обновиться на актуальные версии MySQL становятся всё более весомыми. Не так давно в одном из наших проектов настало время обновлять уютные кластеры Percona Server 5.7 до 8-й версии. Всё это происходило на платформе Ubuntu Linux 16.04. Как выполнить подобную операцию с минимальным простоем и с какими проблемами мы столкнулись при обновлении — читайте в этой статье.
Подготовка
Любое обновление сервера баз данных скорее всего связано с перенастройкой базы: изменений требований к лимитам на системные ресурсы и исправлением конфигов базы, которые надо очистить от устаревших директив.
Перед обновлением мы обязательно обратимся к официальной документации:
- MySQL 8 release notes;
- руководство по обновлению от MySQL;
- руководство по обновлению от Percona;
- руководство MySQL по обновлению реплик и мастеров.
И составим план действий:
- Исправить конфигурационные файлы, удалив устаревшие директивы.
- Проверить совместимость утилитами.
- Обновить slave-базы, поставив пакет
percona-server-server
. - Обновить мастер, поставив тот же пакет.
Разберём каждый пункт плана и посмотрим, что же может пойти не так.
ВАЖНО! Процедура обновления MySQL-кластера на базе Galera имеет свои тонкости, которые в статье не описаны. Не стоит использовать эту инструкцию в таком случае.
Часть 1: Проверка конфигов
В 8-й версии MySQL убрали query_cache
. Вообще-то он был признан устаревшим еще в версии 5.7, но теперь и удалён вовсе. Соответственно, необходимо убрать связанные директивы. А для кэширования запросов теперь можно использовать внешние инструменты — например, ProxySQL.
Так же в конфиге нашлись устаревшие директивы про innodb_file_format
. Если в MySQL 5.7 имелась возможность выбора формата InnoDB, то 8-я версия уже работает только с форматом Barracuda.
Наш итог — удаление следующих директив:
-
query_cache_type
,query_cache_limit
иquery_cache_size
; -
innodb_file_format
иinnodb_file_format_max
.
Для проверки воспользуемся Docker-образом Percona Server. Конфиг сервера поместим в директорию mysql_config_test
, а рядом создадим директории для данных и логов. Пример теста конфигурации percona-server:
mkdir -p {mysql_config_test,mysql_data,mysql_logs}
cp -r /etc/mysql/conf.d/* mysql_config_test/
docker run --name some-percona -v $(pwd)/mysql_config_test:/etc/my.cnf.d/ -v $(pwd)/mysql_data/:/var/lib/mysql/ -v $(pwd)/mysql_logs/:/var/log/mysql/ -e MYSQL_ROOT_PASSWORD=${MYSQL_PASSWORD} -d percona:8-centos
Итог: либо в логах Docker, либо в директории с логами — в зависимости от ваших конфигов — появится файл, в котором будут описаны проблемные директивы.
Вот что было у нас:
2020-04-03T12:44:19.670831Z 0 [Warning] [MY-011068] [Server] The syntax 'expire-logs-days' is deprecated and will be removed in a future release. Please use binlog_expire_logs_seconds instead.
2020-04-03T12:44:19.671678Z 0 [Warning] [MY-013242] [Server] --character-set-server: 'utf8' is currently an alias for the character set UTF8MB3, but will be an alias for UTF8MB4 in a future release. Please consider using UTF8MB4 in order to be unambiguous.
2020-04-03T12:44:19.671682Z 0 [Warning] [MY-013244] [Server] --collation-server: 'utf8_general_ci' is a collation of the deprecated character set UTF8MB3. Please consider using UTF8MB4 with an appropriate collation instead.
Таким образом, нам потребовалось еще разобраться с кодировками и заменить устаревшую директиву expire-logs-days
.
Часть 2: Проверка работающих установок
В документации по обновлению есть 2 утилиты для проверки базы на совместимость. Их использование помогает администратору проверить совместимость имеющейся структуры данных.
Начнём с классической утилиты mysqlcheck. Достаточно просто запустить:
mysqlcheck -u root -p --all-databases --check-upgrade
Если проблемы не обнаружены, утилита завершится с кодом 0:
Кроме того, в современных версиях MySQL доступна утилита mysql-shell (в случае Percona это пакет percona-mysql-shell
). Она является заменой классическому клиенту mysql и совмещает в себе функции клиента, редактора SQL-кода и инструменты администрирования MySQL. Для проверки сервера перед обновлением можно через неё выполнить следующую команды:
mysqlsh -- util check-for-server-upgrade { --user=root --host=1.1.1.1 --port=3306 } --config-path=/etc/mysql/my.cnf
И вот какие замечания мы получили:
В общем, ничего критичного — только предупреждения о кодировках (см. ниже). Общий результат выполнения:
Мы решили, что обновление должно пойти без проблем.
Замечание о предупреждениях выше, свидетельствующих проблемы с кодировками. Дело в том, что UTF-8 в MySQL до недавнего времени не являлась «настоящей» UTF-8, так как хранила всего 3 байта вместо 4. В MySQL 8 это наконец-то решили исправить: алиас utf8
вскоре будет вести на кодировку utf8mb4
, а старые столбцы в таблицах станут utf8mb3
. В дальнейшем кодировка utf8mb3
будет удалена, но не в данном релизе. Поэтому мы решили исправить кодировки уже на работающей инсталляции СУБД, после её обновления.
Часть 3: Обновление серверов
Что же может пойти не так, когда есть столь шикарный план?.. Прекрасно понимая, что нюансы всегда случаются, первый эксперимент мы провели на dev-кластере MySQL.
Как уже упоминалось, официальная документация освещает вопрос обновления MySQL-серверов с репликами. Суть сводится к тому, что сначала стоит обновлять все реплики (slave), так как MySQL 8 умеет реплицироваться с мастера версии 5.7. Некоторая сложность заключается в том, что у нас используется режим master <-> master, когда удалённый мастер находится в режиме read-only. То есть фактически боевой трафик поступает в один ЦОД, а 2-й является резервным.
Топология выглядит следующим образом:
Обновление должно начаться с реплик mysql replica dc 2, mysql master dc 2 и mysql replica dc 1
, а закончиться — сервером mysql master dc 1. Для пущей надёжности мы остановили виртуальные машины, сделали их снапшоты, а непосредственно перед обновлением остановили репликацию командой STOP SLAVE
. В остальном же обновление выглядит так:
- Каждую реплику перезапускаем, добавив в конфиги 3 опции:
skip-networking
,skip-slave-start
,skip-log-bin
. Дело в том, что обновление базы генерирует бинарные логи с обновлением системных таблиц. Данные директивы гарантируют, что в базе не будет изменения данных приложения, а в бинарные логи не попадет информация об обновлении системных таблиц. Это позволит избежать проблем при возобновлении репликации. - Устанавливаем пакет
percona-server-server
. Важно отметить, что в версии MySQL 8 не требуется запускать командуmysqlupgrade
после обновления сервера. - После успешного старта еще раз перезапускаем сервер — уже без параметров, которые добавлялись в первом пункте.
- Убеждаемся, что репликация успешно работает: проверяем
SHOW SLAVE STATUS
и смотрим, что обновляются таблицы со счетчиками в базе приложения.
Всё это выглядит достаточно просто: обновление dev прошло успешно. Ок, можно спокойно планировать ночное обновление для production.
Не было печали — prod мы обновляли
Однако перенос успешного опыта dev на production не обошёлся без сюрпризов.
К счастью, сам процесс обновления начинается с реплик, поэтому, встретив сложности, мы остановили работы и восстановили реплику из снапшота. Исследование проблем перенесли на следующее утро. В логах оказались следующее записи:
2020-01-14T21:43:21.500563Z 2 [ERROR] [MY-012069] [InnoDB] table: t1 has 19 columns but InnoDB dictionary has 20 columns
2020-01-14T21:43:21.500722Z 2 [ERROR] [MY-010767] [Server] Error in fixing SE data for db1.t1
2020-01-14T21:43:24.208365Z 0 [ERROR] [MY-010022] [Server] Failed to Populate DD tables.
2020-01-14T21:43:24.208658Z 0 [ERROR] [MY-010119] [Server] Aborting
Исследование архивов различных почтовых рассылок в Google привело к пониманию, что такая проблема возникает из-за бага MySQL. Хотя скорее это даже баг утилит mysqlcheck
и mysqlsh
.
Оказывается, в MySQL сменили способ представления данных для десятичных полей (int, tinyint и т.п.), поэтому внутри mysql-server используется другой способ их хранения. Если ваша база данных изначально была в версии 5.5 или 5.1, а затем вы обновлялись до 5.7, то, возможно, требуется произвести OPTIMIZE
для некоторых таблиц. Тогда MySQL обновит файлы с данными, переведя их на актуальный формат хранения.
Также это можно проверить утилитой mysqlfrm
:
mysqlfrm --diagnostic -vv /var/lib/mysql/db/table.frm
...
'field_length': 8,
'field_type': 246, # формат поля
'field_type_name': 'decimal',
'flags': 3,
'flags_extra': 67,
'interval_nr': 0,
'name': 'you_deciaml_column',
...
Если field_type
у вас равен 0, то в таблице используется старый тип — надо проводить OPTIMIZE
. Однако, если стоит значение 246 — у вас уже новый тип. Подробнее с типами можно ознакомиться в коде.
Более того, в данном баге рассматривается вторая возможная причина, которая обошла нас стороной, — это отсутствие InnoDB-таблиц в системной таблице INNODB_SYS_TABLESPACES
, если они, таблицы, создавались в версии 5.1. Чтобы избежать проблем при обновлении, можно воспользоваться приложенным SQL-скриптом.
Почему же у нас не возникло таких проблем на dev? База туда периодически копируется с production — таким образом, таблицы пересоздаются.
К сожалению, на реально работающей большой БД не получится просто взять и выполнить повсеместный OPTIMIZE
. Здесь поможет percona-toolkit: для операции online OPTIMIZE отлично подходит утилита pt-online-schema-change.
Обновленный план стал получился таким:
- Провести оптимизацию всех таблиц.
- Провести обновление баз данных.
Чтобы проверить его и заодно выяснить время обновления, мы отключили одну из реплик, а для всех таблиц запустили следующую команду:
pt-online-schema-change --critical-load Threads_running=150 --alter "ENGINE=InnoDB" --execute --chunk-size 100 --quiet --alter-foreign-keys-method auto h=127.0.0.1,u=root,p=${MYSQL_PASSWORD},D=db1,t=t1
Обновление таблиц производится без продолжительных блокировок благодаря тому, что утилита создает новую временную таблицу, в которую копирует данные из основной таблицы. В момент, когда обе таблицы идентичны, исходная таблица блокируется и подменяется новой. В нашем случае тестовый запуск показал, что для обновления всех таблиц потребуется около суток, но при этом копирование данных вызывало слишком большую нагрузку на диски.
Чтобы этого избежать, на production мы добавили к команде аргумент --sleep
со значением 10 — этот параметр регулирует длину ожидания после переноса пачки данных в новую таблицу. Так можно снизить нагрузку, если реально запущенное приложение требовательно к времени ответа.
После выполнения оптимизации обновление прошло успешно.
… но не до конца!
Уже через полчаса после обновления клиент пришел с проблемой. База работала очень странно: периодически начинались сбросы подключений. Вот как это выглядело в мониторинге:
На скриншоте виден пилообразный график, связанный с тем, что часть потоков MySQL-сервера периодически падали с ошибкой. В приложении появились ошибки:
[PDOException] SQLSTATE[HY000] [2002] Connection refused
Беглый осмотр логов выявил, что демон mysqld не мог получить требуемые ресурсы у операционной системы. Разбираясь с ошибками, мы обнаружили в системе «бесхозные» файлы политик apparmor:
# dpkg -S /etc/apparmor.d/cache/usr.sbin.mysqld
dpkg-query: no path found matching pattern /etc/apparmor.d/cache/usr.sbin.mysqld
# dpkg -S /etc/apparmor.d/local/usr.sbin.mysqld
dpkg-query: no path found matching pattern /etc/apparmor.d/local/usr.sbin.mysqld
# dpkg -S /etc/apparmor.d/usr.sbin.mysqld
mysql-server-5.7: /etc/apparmor.d/usr.sbin.mysqld
# dpkg -l mysql-server-5.7
rc mysql-server-5.7 5.7.23-0ubuntu0.16.04.1 amd64
Эти файлы образовались при обновлении на MySQL 5.7 пару лет назад и принадлежат удалённому пакету. Удаление файлов и перезапуск службы apparmor решил проблему:
systemctl stop apparmor
rm /etc/apparmor.d/cache/usr.sbin.mysqld
rm /etc/apparmor.d/local/usr.sbin.mysqld
rm /etc/apparmor.d/usr.sbin.mysqld
systemctl start apparmor
В заключение
Любая, даже самая простая операция, может привести к неожиданным проблемам. И даже наличие продуманного плана не всегда гарантирует ожидаемый результат. Теперь в любые планы обновления у нашей команды входит еще и обязательная чистка лишних файлов, которые могли появиться в результате последих действий.
А этим не очень профессиональным графическим творчеством я бы хотел сказать огромное спасибо компании Percona за их отличные продукты!
P.S.
Читайте также в нашем блоге:
- «Базы данных и Kubernetes (обзор и видео доклада)»;
- «Kubernetes tips & tricks: ускоряем bootstrap больших баз данных»;
- «6 практических историй из наших SRE-будней»;
- «Одна история с оператором Redis в K8s и мини-обзор утилит для анализа данных этой БД»;
- «Беспростойная миграция MongoDB в Kubernetes».
Автор: Николай Богданов