- Бэкапы
- Обход отказов
- Как выглядит тест отказа
- Хаос-тестирование
- Миграция схем
- Тестирование новых версий
“Мы должны иметь возможность выполнять такие задачи, как очистки под нагрузкой, текущие обновления, миграции схем, рефакторинг топологии кластеров, пулинг, балансировка нагрузки и многое другое. У нас есть инфраструктура для автоматизации таких операций; в этом посте мы поделимся примерами того, как мы непрерывно тестируем нашу инфраструктуру.
Бэкапы
Невероятно важно создавать резервные копии данных. Если вы не создаете резервные копии БД, то, скорее всего, эпичный отказ это вопрос времени. Percona Xtrabackup — инструмент, который мы используем для создания полных резервных копий наших баз данных MySQL. Есть данные, в сохранности которых мы должны быть уверены, и у нас есть сервер, который выполняет резервное копирование.
В дополнение к полному резервному копированию бинарников мы выполняем логическое резервное копирование несколько раз в день. Эти резервные копии позволяют инженерам получать копии последних данных. Бывает, что им нужен полный набор данных из таблицы, чтобы протестировать изменение индекса в таблице продакшен-размера или посмотреть данные за определенный период времени. Hubot позволяет восстановить резервную копию таблицы и сообщит нам, когда таблица будет готова к использованию:
Данные загружаются в не-продакшен базу данных, которая доступна инженеру, запрашивающему восстановление.
Последний способ хранения «резервной копии» данных — использование отложенных реплик. Это не столько резервное копирование, сколько защита. Для каждого продакшен-кластера у нас есть хост, репликация на котором задерживается на 4 часа. Если выполняется запрос, который не должен был выполняться, мы можем запустить mysql panic
в chatops. Это приведет к тому, что все отложенные реплики немедленно прекратят репликацию. Это действие также отправит вызов дежурному DBA-администратору. Далее мы можем использовать отложенную реплику для проверки наличия проблемы, а затем “перемотать” бинарные логи до момента, предшествующего ошибке. Затем мы можем восстановить эти данные на мастер, таким образом восстанавливая данные до этой точки.
Резервные копии это здорово, однако они ничего не стоят, если произойдет какая-то неизвестная или не обрабатываемая ошибка, которая испортит резервную копию. Преимущество наличия скрипта для восстановления резервных копий заключается в том, что он позволяет автоматизировать проверку резервных копий с помощью cron
. Мы создали выделенный хост для каждого кластера, который запускает восстановление последней резервной копии. Это гарантирует, что резервное копирование выполнено правильно и что мы получим данные из резервной копии.
В зависимости от размера датасета мы выполняем несколько восстановлений в день. Ожидается, что восстановленные серверы присоединятся к потоку репликации и смогут “догнать” его. При этом проверяется не только то что мы сделали восстанавливаемую резервную копию, но и то что мы правильно определили момент времени, в который она была сделана, и можем в дальнейшем применять изменения с этого момента. Мы получаем предупреждение, если в процессе восстановления что-то пошло не так.
Кроме того, мы отслеживаем время, затраченное на восстановление, чтобы иметь представление о том, сколько времени потребуется для создания новой реплики или восстановления в экстренных случаях.
Ниже приведен результат автоматического процесса восстановления, написанный Hubot в чате наших роботов.
Hubot gh-mysql-backup-restore: db-mysql-0752: restore_log.id = 4447 gh-mysql-backup-restore: db-mysql-0752: Determining backup to restore for cluster ‘prodcluster’. gh-mysql-backup-restore: db-mysql-0752: Enabling maintenance mode gh-mysql-backup-restore: db-mysql-0752: Setting orchestrator downtime gh-mysql-backup-restore: db-mysql-0752: Disabling Puppet gh-mysql-backup-restore: db-mysql-0752: Stopping MySQL gh-mysql-backup-restore: db-mysql-0752: Removing MySQL files gh-mysql-backup-restore: db-mysql-0752: Running gh-xtrabackup-restore gh-mysql-backup-restore: db-mysql-0752: Restore file: xtrabackup-notify-2017-07-02_0000.xbstream gh-mysql-backup-restore: db-mysql-0752: Running gh-xtrabackup-prepare gh-mysql-backup-restore: db-mysql-0752: Starting MySQL gh-mysql-backup-restore: db-mysql-0752: Update file ownership gh-mysql-backup-restore: db-mysql-0752: Upgrade MySQL gh-mysql-backup-restore: db-mysql-0752: Stopping MySQL gh-mysql-backup-restore: db-mysql-0752: Starting MySQL gh-mysql-backup-restore: db-mysql-0752: Backup Host: db-mysql-0034 gh-mysql-backup-restore: db-mysql-0752: Setting up replication gh-mysql-backup-restore: db-mysql-0752: Starting replication gh-mysql-backup-restore: db-mysql-0752: Replication catch-up gh-mysql-backup-restore: db-mysql-0752: Restore complete (replication running) gh-mysql-backup-restore: db-mysql-0752: Enabling Puppet gh-mysql-backup-restore: db-mysql-0752: Disabling maintenance mode gh-mysql-backup-restore: db-mysql-0752: Setting orchestrator downtime gh-mysql-backup-restore: db-mysql-0752: Restore process complete.
Еще одна причина, по которой мы используем резервные копии — это добавление новой реплики к существующему набору серверов MySQL. Мы инициируем сборку нового сервера, а когда получим уведомление о его готовности, сможем запустить восстановление последней резервной копии для данного кластера. У нас есть сценарий, который выполняет все команды восстановления, которые в противном случае нам пришлось бы выполнять вручную. Наша автоматизированная система восстановления, по сути, использует тот же скрипт. Это упрощает процесс создания системы и позволяет запустить хост с помощью нескольких команд в чате, а не десятков ручных процессов. Ниже показано восстановление, запущенное вручную в чате:
jessbreckenridge .mysql backup-restore -H db-mysql-0007 -o -r magic_word=daily_rotating_word hubot Hubot @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Determining backup to restore for cluster ‘mycluster’. @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: restore_log.id = 4449 @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Enabling maintenance mode @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Setting orchestrator downtime @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Disabling Puppet @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Stopping MySQL @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Removing MySQL files @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Running gh-xtrabackup-restore @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Restore file: xtrabackup-mycluster-2017-07-02_0015.xbstream @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Running gh-xtrabackup-prepare @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Update file ownership @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Starting MySQL @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Upgrade MySQL @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Stopping MySQL @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Starting MySQL @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Setting up replication @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Starting replication @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Backup Host: db-mysql-0201 @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Replication catch-up @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Replication behind by 4589 seconds, waiting 1800 seconds before next check. @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Restore complete (replication running) @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Enabling puppet @jessbreckenridge gh-mysql-backup-restore: db-mysql-0007: Disabling maintenance mode
Обход отказов
Мы используем orchestrator для выполнения автоматического восстановления после сбоев для мастеров и промежуточных мастеров. Мы ожидаем, что orchestrator правильно обнаружит отказ мастера, назначит реплику для продвижения, вылечит топологию под указанной репликой и выполнит продвижение. Мы ожидаем, что VIP-клиенты изменятся, пулы изменятся, клиенты переподключатся, puppet запустит важные компоненты на продвигаемом мастере и многое другое. Обход отказа — сложная задача, затрагивающая множество аспектов нашей инфраструктуры.
Чтобы повысить доверие к нашим обходам отказа, мы создали тестовый кластер, похожий на продакшен, и непрерывно ложим его, для наблюдения за обходами отказа.
Кластер, похожий на продакшен, представляет собой репликационную сборку, которая во всех аспектах идентична нашим продакшен-кластерам: типы оборудования, операционные системы, версии MySQL, сетевое окружение, VIP-клиенты, конфигурации puppet, настройки haproxy и т. д. Единственное отличие этого кластера в том, что он не отправляет и не принимает продакшен-трафик.
Мы эмулируем нагрузку записи на тестовый кластер, избегая при этом задержек репликации. Нагрузка при записи не слишком велика, но в ней присутствуют запросы, намеренно претендующие на запись в одни и те же датасеты. Это не слишком интересно в обычное время, но оказывается полезным при обходах отказа (о чем мы когда-нибудь расскажем).
В нашем тестовом кластере представлены серверы из трех дата-центров. Мы хотим, чтобы при обходе отказа продвигалась запасная реплика из того же дата-центра. Мы хотели бы иметь возможность спасти как можно больше реплик при таких ограничениях. Мы требуем, чтобы оба варианта применялись по возможности. orchestrator не имеет никаких предположений о топологии; он должен реагировать на то состояние, которое было в момент отказа.
Однако мы заинтересованы в создании сложных и разнообразных сценариев. Наш сценарий тестирования подготавливает обход отказа:
- Определяет существующий мастер
- Перестраивает топологию так, чтобы под мастером находились представители всех трех дата-центров. Разные дата-центры имеют разные сетевые задержки и, как ожидается, будут реагировать на падение мастера в разное время.
- Выбирает способ падения. Мы выбираем между “расстрелом” мастера (
kill -9
) и его сетевым разделением:iptables -j REJECT
(нормально) илиiptables -j DROP
(не отвечает).
Сценарий запускает аварийное завершение работы мастера выбранным способом и ждет, пока оркестратор надежно обнаружит аварийное завершение и выполнит обход отказа. Хотя мы ожидаем, что обнаружение и продвижение будут завершены в течение 30 секунд, сценарий немного ослабляет это ожидание и “спит” в течение определенного времени, прежде чем просмотреть результаты обхода отказа. Затем он:
- Проверяет, установлен ли новый (другой) мастер
- Что в кластере имеется достаточное количество реплик
- Мастер доступен для записи
- Записи в мастер видны на репликах
- Внутренние записи обнаружения сервисов обновлены (идентификация нового мастера соответствует ожидаемой; старый мастер удален)
- Другие внутренние проверки
Эти тесты подтверждают, что обход отказа прошел успешно, причем не только с точки зрения MySQL, но и в масштабах всей нашей инфраструктуры. VIP был принят; определенные службы были запущены; информация попала туда, куда должна была попасть.
Далее сценарий приступает к восстановлению упавшего сервера:
- Восстанавливает его из резервной копии, тем самым неявно тестируя нашу процедуру резервного копирования/восстановления
- Проверяет, что конфигурация сервера соответствует ожиданиям (сервер больше не считает себя мастером)
- Возвращает его в кластер репликации, ожидая найти данные, записанные на мастер-сервере
Рассмотрим следующую визуализацию запланированного теста обхода отказа: от хорошо работающего кластера до проблем с некоторыми репликами, диагностирование того, что мастер (7136
) мертв, выбор сервера для продвижения (a79d
), рефакторинг топологии этого сервера, его продвижение (обход отказа успешен), восстановление “мёртвого” мастера и его возвращение в кластер.
Как выглядит тест отказа
Наш сценарий использует “катастрофический” подход. Единичный сбой в любом из компонентов приводит к сбою всего теста, отключая все последующие автоматические тесты до тех пор, пока человек не решит этот вопрос. Мы получаем предупреждение и проверяем состояние и логи.
Сценарий может не сработать при неприемлемом времени обнаружения или обхода отказа; при проблемах с резервным копированием/восстановлением; при потере слишком большого количества серверов; при ошибке конфигурации после обхода отказа и т. д.
Нам нужно убедиться, что orchestrator правильно подключает серверы. Вот тут-то и пригодится “конкурирующая” запись: при неправильной настройке репликация легко может быть нарушена. Мы получим DUPLICATE KEY или другие ошибки, указывающие на то, что что-то пошло не так.
Это особенно важно, поскольку мы улучшаем и вводим новое поведение в orchestrator, что позволяет нам тестировать изменения в безопасном окружении.
Хаос-тестирование
Процедура тестирования, описанная выше, позволит выявить (и уже многократно выявляла) проблемы во многих частях нашей инфраструктуры. Но достаточно ли этого?
В продакшен-окружении всегда есть что-то неправильное. Что-то в конкретном методе тестирования, что не применимо к нашим продакшен-кластерам. В них нет одинакового трафика и манипуляций с трафиком, а также нет одинакового набора серверов. Виды отказов могут быть разными.
Поэтому мы выполняем хаос-тестирование наших продакшен-кластеров. Хаос-тестирование буквально уничтожает наш прод по частям, но по ожидаемому графику и под достаточным контролем.
Миграция схем
Мы используем gh-ost для запуска live-миграции схем. gh-ost стабилен, но активно развивается, добавляются новые функции.
gh-ost мигрирует таблицы, копируя данные в ghost-таблицу, применяя текущие изменения, перехваченные бинарными логами, к ghost-таблице, даже когда в исходную таблицу ведется запись. Затем он меняет ghost-таблицу на место исходной таблицы. По завершении миграции GitHub продолжает работать с таблицей, созданной и заполненной gh-ost.
На данный момент почти все данные GitHub в MySQL были воссозданы gh-ost, причем большая их часть — несколько раз. Мы должны очень доверять gh-ost, чтобы позволить ему обрабатывать наши данные даже в условиях активной разработки. Вот как мы завоевываем это доверие.
gh-ost предоставляет возможность тестирования в проде. Он поддерживает запуск миграции на реплике практически так же, как и на мастере: gh-ost подключается к реплике и обращается с ней так, как если бы она была мастером. Он анализирует ее бинарные логи так же, как и при миграции на настоящий мастер. При этом он копирует строки и применяет binlog-события к реплике, избегая записи на мастер.
Мы запускаем gh-ost-выделенные реплики в продакшене. Эти реплики не обслуживают продакшен-трафик. Каждая такая реплика получает текущий список продакшен-таблиц и перебирает их в случайном порядке. Она выбирает таблицы по очереди и выполняет миграцию реплики на эту таблицу. На самом деле миграция не изменяет структуру таблицы, а вместо этого запускает ENGINE=InnoDB
. Тест запускает миграцию даже в то время, когда таблица используется в продакшене, таким образом копируя реальные данные и применяя реальный трафик из бинарных логов.
Эти миграции можно подвергать аудиту. Вот как мы проверяем состояние запущенных тестов из чата:
Когда тестовая миграция завершает копирование данных таблицы, она останавливает репликацию и выполняет cut-over, заменяя исходную таблицу ghost-таблицей, а затем меняет ее обратно. Мы не заинтересованы в реальной замене данных. Вместо этого у нас остается исходная таблица и ghost-таблица, которые должны быть идентичны. Мы проверяем это путем контрольного суммирования всех данных обеих таблиц.
Тест может завершиться с результатом:
- success: Все прошло хорошо, и контрольная сумма идентична. Это ожидаемый результат.
- failure: Проблема при выполнении. Это иногда происходит из-за завершения процесса миграции, проблем с репликацией и т. д. и обычно не связано с самим gh-ost.
- checksum failure: несоответствие данных в таблице. Для тестируемой ветки это означает необходимость исправления. Для продолжающегося тестирования мастер-ветки это означало бы немедленное блокирование продакшен-миграций. Мы не получаем такой результат.
Результаты тестов проверяются, отправляются в чаты роботов, поступают в виде событий в наши системы метрик. Каждая вертикальная линия на следующем графике представляет собой успешный тест миграции:
Эти тесты выполняются непрерывно. В случае сбоев мы получаем оповещения. И, конечно, мы проверяем чат роботов, чтобы узнать, что происходит.
Тестирование новых версий
Мы постоянно улучшаем gh-ost. Наш поток разработки основан на ветках git, которые мы предлагаем объединить через pull request.
Переданный gh-ost-ом pull request проходит через непрерывную интеграцию (CI), которая запускает базовую компиляцию и юнит-тесты. После этого PR становится технически пригодным для слияния, но что еще более интересно, он становится пригодным для развертывания через Heaven. Поскольку Heaven является чрезвычайно важным компонентом нашей инфраструктуры, мы заботимся о том, чтобы развернуть gh-ost ветки для интенсивного тестирования перед слиянием с master.
Некоторые PR небольшие и не влияют на сами данные. Изменения в статус-сообщениях, интерактивных командах и т. д. оказывают меньшее влияние на gh-ost. Другие изменения вносят существенные изменения в логику и процессы миграции. Мы тщательно проверяем их, прогоняя через наш парк продакшен-таблиц, пока не убедимся, что эти изменения не представляют угрозы повреждения данных.
Продакшен всегда преподносит сюрпризы в виде сценариев, не охваченных тестами. Чем больше мы тестируем в продакшене, тем больше информации получаем о возможностях нашей инфраструктуры.”