Когда только Dropbox запустился, один пользователь на Hacker News прокомментировал, что реализовать его можно несколькими bash-скриптами с помощью FTP и Git. Сейчас такого сказать никак нельзя, это крупное облачное файловое хранилище с миллиардами новых файлов каждый день, которые не просто как-то хранятся в базе данных, а так, что любую базу можно восстановить на любую точку в течение последних шесть дней.

Под катом расшифровка доклада Славы Бахмутова (m0sth8) на Highload++ 2017, о том, как развивались базы данных в Dropbox и как они устроены сейчас.


О спикере: Слава Бахмутов — site reliability engineer в команде Dropbox, очень любит Go и иногда появляется в подкасте golangshow.com.

Содержание





Архитектура Dropbox простым языком


Dropbox появился в 2008 году. По сути, это облачное файловое хранилище. Когда только Dropbox запустился, пользователь на Hacker News прокомментировал, что реализовать его можно несколькими баш-скриптами с помощью FTP и Git. Но, тем не менее, Dropbox развивается, и сейчас это достаточно крупный сервис c более чем 1,5 миллиардами пользователей, 200 тысячами бизнесов и огромным количеством (несколько миллиардов!) новых файлов каждый день.

Как выглядит Dropbox?


У нас есть несколько клиентов (web интерфейс, API для приложений, которые пользуются Dropbox, desktop-приложения). Все эти клиенты используют API и общаются с двумя большими сервисами, которые логически можно разделить на:

  1. Metaserver.
  2. Blockserver.

На Metaserver хранится метаинформация о файле: размер, комментарии к нему, ссылки на этот файл в Dropbox и т.п. В Blockserver хранится информация только о файлах: папки, пути и т.д.

Как это работает?

Например, у вас есть файл video.avi с каким-то видео.
Ссылка со слайда

  • Клиент дробит этот файл на несколько чанков (в данном случае по 4 МБ), подсчитывает контрольную сумму и отправляет к Metaserver запрос: «У меня есть файл *.avi, я хочу его загрузить, хэш-суммы такие-то».
  • Metaserver возвращает ответ: «У меня нет этих блоков, давай загрузи!» Либо он может ответить, что у него есть все или некоторые блоки, и нужно загрузить только оставшиеся.

Ссылка со слайда

  • После этого клиент идет в Blockserver, отправляет хэш-сумму и сам блок данных, который сохраняется на Blockserver.
  • Blockserver подтверждает операцию.

Ссылка со слайда

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


Когда клиент сохраняет что-то на Metaserver, вся информация попадает в MySQL. Blockserver информацию о файлах, о том, как они структурированы, из каких блоков состоят, тоже хранит в MySQL. Также Blockserver хранит сами блоки в Block Storage, который в свою очередь информацию о том, где лежит какой блок, на каком сервере и как он обработан в данный момент, тоже сохраняет в MYSQL.

Для хранения экзабайтов пользовательских файлов мы параллельно сохраняем дополнительную информацию в базу данных на несколько десятков петабайт, раскиданных по 6 тысячам серверов.

История развития баз данных


Как развивались базы данных в Dropbox?


В 2008 году все начиналось с одного Metaserver и одной глобальной базы данных. Всю информацию, которую Dropbox нужно было куда-то сохранять, он сохранял в единственный глобальный MySQL. Так продолжалось недолго, потому что количество пользователей росло, и отдельные базы и таблички внутри баз разбухали быстрее, чем другие.


Поэтому в 2011 году несколько таблиц были вынесены на отдельные сервера:

  • User, с информацией о пользователях, например, логинами и oAuth токенами;
  • Host, с информацией о файлах от Blockserver;
  • Misc, которая не участвовала в обработке запросов с продакшена, но использовалась для служебных функций, вроде batch jobs.



Но после 2012 года Dropbox начал очень сильно расти, с тех пор мы растем примерно на 100 млн пользователей в год.


Нужно было учитывать такой огромный рост, и поэтому в конце 2011 года у нас появились шарды — база, состоящая из 1 600 шардов. Изначально всего 8 серверов по 200 шардов на каждом. Сейчас это 400 мастер-серверов по 4 шарда на каждом.
Ссылка со слайда

В 2012 году мы поняли, что создавать таблицы и обновлять их в БД на каждую добавляемую бизнес-логику очень сложно, муторно и проблематично. Поэтому в 2012 году мы изобрели свой собственный графовый storage, который назвали Edgestore, и с тех пор вся бизнес-логика и метаинформация, которую генерирует приложение сохраняется в Edgestore.

Edgestore, по сути, абстрагирует MySQL от клиентов. У клиентов есть некие сущности, которые соединены между собой ссылками из gRPC API к Edgestore Сore, который преобразует эти данные в MySQL и каким-то образом там хранит их (в основном он отдает все это из кэша).
Ссылка со слайда

В 2015 году мы ушли с Amazon S3, разработали собственное облачное хранилище под названием Magic Pocket. В нем информация о том, где какой блок-файл находится, на каком сервере, о перемещениях этих блоков между серверами, хранится в MySQL.
Ссылка со слайда

Но MySQL используется очень хитрым образом — по сути, как большая распределеннуя хэш-таблица. Это очень разная нагрузка, в основном, на чтение случайных записей. 90% утилизации это I/O.

Архитектура баз данных


Во-первых, мы сразу же определили некие принципы, по которым строим архитектуру нашей базы данных:

  1. Надежность и долговечность. Это самый главный принцип и то, чего от нас ждут клиенты — данные не должны теряться.
  2. Оптимальность решения — не менее важный принцип. Например, бэкапы должны делаться быстро и восстанавливаться тоже быстро.
  3. Простота решения — как архитектурно, так и с точки зрения обслуживания и дальнейшей поддержки разработки.
  4. Стоимость владения. Если что-то оптимизирует решение, но стоит очень дорого, это нам не подходит. Например, slave, который отстает от master на день, очень удобен для бэкапов, но тогда к 6 000 серверов нужно добавить еще 1 000 — стоимость владения таким slave очень высока.

Все принципы должны быть верифицируемыми и измеряемыми, то есть на них должны быть метрики. Если речь идет о стоимости владения, то мы должны посчитать, сколько у нас серверов, например, уходит под базы данных, сколько серверов уходит под бэкапы, и сколько это в итоге стоит для Dropbox. Когда мы выбираем новое решение, мы подсчитываем все метрики и ориентируемся на них. При выборе любого решения мы полностью руководствуемся этими принципами.

Базовая топология


База данных устроена примерно следующим образом:

  • В основном дата-центре у нас есть master, в который происходят все записи.
  • У master-сервера есть два slave-сервера, на которые происходит semisync репликация. Серверы часто умирают (порядка 10 в неделю), поэтому нам необходимо два slave-сервера.
  • Slave-серверы находятся в отдельных кластерах. Кластеры — это совершенно отдельные комнаты в дата-центре, которые не связаны друг с другом. Если одна комната сгорает, вторая остается вполне себе рабочей.
  • Также в другом дата-центре у нас есть так называемый pseudo master (intermediate master), который на самом деле просто slave, у которого есть другой slave.



Такая топология выбрана потому, что если у нас вдруг умирает первый дата-центр, то во втором дата-центре у нас уже практически полная топология. Мы просто меняем в Discovery все адреса, и клиенты могут работать.

Специализированные топологии


Также у нас есть специализированные топологии.

Топология Magic Pocket состоит из одного master-сервера и двух slave-серверов. Так сделано, потому что сам Magic Pocket дублирует данные среди зон. Если он теряет одни кластер, то может восстановить через erasure code все данные с других зон.


Топология active-active — кастомная топология, которая используется в Edgestore. В ней есть по одному master и двум slave в каждом из двух дата-центров, и они являются slave друг для друга. Это очень опасная схема, но Edgestore на своем уровне точно знает, какие данные на какой master по какому range он может записать. Поэтому эта топология не ломается.


Instance


У нас установлены достаточно простые сервера с конфигурацией 4-5 летней давности:

  • 2x Xeon 10 cores;
  • 5TB (8 SSD Raid 0*);
  • 384 GB memory.

* Raid 0 — потому что нам проще и намного быстрее заменить целый сервер, чем диски.

Single Instance


На этом сервере у нас есть один большой инстанс MySQL, на котором находятся несколько шардов. Этот MySQL инстанс сразу выделяет себе практически всю память. На сервере запущены и другие процессы: proxy, сбор статистики, логи и т.д.



Это решение хорошо тем, что:

? + Им легко управлять. Если нужно заменить MySQL инстанс, просто заменяем сервер.

? + Просто делать фейловеры.

С другой стороны:

? ? Проблематично то, что любые операции происходят над целым инстансом MySQL и сразу над всеми шардами. Например, если нужно сделать бэкап, мы делаем бэкап сразу всех шардов. Если нужно сделать фейловер, мы делаем фейловер сразу всех четырех шардов. Соответственно, доступность страдает в 4 раза больше.

? ? Проблемы с репликацией одного шарда влияют на другие шарды. Репликация в MySQL не параллельная, и все шарды работают в один поток. Если с одним шардом что-то происходит, то остальные тоже становятся жертвами.

Поэтому сейчас мы переходим на другую топологию.

Multi Instance




В новом варианте на сервере запущено сразу несколько инстансов MySQL, в каждом есть по одному шарду. Чем это лучше?

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

? + Шарды почти не влияют друг на друга.

? + Улучшение в репликации. Мы можем миксовать разные категории и классы баз данных. Edgestore занимает очень много места, например, все 4 Тб, а Magic Pocket занимает всего 1 Тб, но у него утилизация 90%. То есть мы можем объединять различные категории, которые по-разному используют I/O и ресурсы машины, и запустить 4 потока репликаций.

Конечно, у этого решения есть и минусы:

? ? Самый большой минус — намного сложнее всем этим управлять. Нужен какой-то умный планировщик, который будет понимать, куда он может вынести этот инстанс, где будет оптимальная нагрузка.

? ? Сложнее фейловеры.

Поэтому мы только сейчас переходим на это решение.

Discovery


Клиенты должны как-то знать, как подключаться к нужной базе, поэтому у нас есть Discovery, который должен:

  1. Очень быстро нотифицировать клиента об изменениях топологии. Если мы поменяли master и slave, клиенты должны узнать об этом практически мгновенно.
  2. Топология не должна зависеть от топологии репликации MySQL, потому что при некоторых операциях мы меняем топологию MySQL. Например, когда мы делаем split, на подготовительном шаге на target master, куда будем выносить часть шардов, часть slave-серверов перенастраивается на этот target master. Клиентам нет необходимости знать об этом.
  3. Важно, чтобы была атомарность операций и верификация состояния. Нельзя, чтобы два разных сервера одной базы данных стали были master в один и тот же момент.

Как развивался Discovery


Сначала все было просто: адрес базы данных в исходном коде в конфиге. Когда нам нужно было обновить адрес, то просто все деплоилось очень быстро.


К сожалению, это не работает, если серверов становится очень много.


Выше самый первый Discovery, который у нас появился. Были скрипты базы данных, которые изменяли табличку в ConfigDB — это была отдельная табличка MySQL, а клиенты уже слушали эту БД и периодически забирали оттуда данные.


Таблица очень простая, есть категория базы данных, ключ шарда, класс БД master/slave, proxy и адрес БД. По сути, клиент запрашивал категорию, класс БД, ключ шарда, и ему возвращался MySQL-ный адрес, по которому он уже мог устанавливать соединение.


Как только серверов стало очень много, добавился Memcaсhе и клиенты стали общаться уже с ним.

Но потом мы это переработали. MySQL скрипты начали общаться через gRPC, через тонкий клиент с сервисом, который мы назвали RegisterService. Когда какие-то изменения происходили, у RegisterService была очередь, и он понимал, как применять эти изменения. RegisterService сохранял данные в AFS. AFS — это наша внутренняя система, построенная на базе ZooKeeper.


Второе решение, которое здесь не показано, напрямую использовало ZooKeeper, и это создавало проблемы, потому что у нас каждый шард был узлом в ZooKeeper. Например, 100 тысяч клиентов подключаются к ZooKeeper, если они вдруг умерли из-за какого-то бага все вместе, потом прийдет сразу 100 тысяч запросов к ZooKeeper, что просто его уронит, и он не сможет подняться.

Поэтому была разработана система AFS, которой пользуется весь Dropbox. По сути, она абстрагирует работу с ZooKeeper для всех клиентов. AFS демон локально крутится на каждом сервере и предоставляет очень простой файловый API вида: создать файл, удалить файл, запросить файл, получить нотификацию на изменение файла и compare and swap операции. То есть можно попробовать заменить файл с какой-то версии, а если эта версия поменялась в процессе смены, то операция отменяется.

По существу, такая абстракция над ZooKeeper, в которой есть локальный backoff и джиттер-алгоритмы. ZooKeeper уже не падает под нагрузкой. С AFS мы снимаем бэкапы в S3 и в GIT, потом сам локальный AFS уведомляет клиентов о том, что данные изменились.


В AFS данные хранятся в виде файлов, то есть это API файловой системы. Например, выше приведен файл shard.slave_proxy — самый большой, он занимает порядка 28 Кб, и когда мы изменяем категорию shard и slave_proxy класс, то все клиенты, которые подписаны на этот файл, получают нотификацию. Они перечитывают этот файл, в котором есть вся нужная информация. По shard key получают категорию и перенастраивают пул соединения к базе данных.

Операции


Мы используем очень простые операции: promotion, clone, backups/recovery.


Операция — это простая стейт-машина. Когда мы заходим в операцию, мы производим какие-то проверки, например, spin-check, который несколько раз по таймауту проверяет, можно ли нам выполнить эту операцию. После этого мы делаем какое-то подготовительное действие, которое не влияет на внешние системы. Дальше собственно сама операция.

У всех шагов внутри операции есть rollback-step (отмена). Если с операцией возникла какая-то проблема, то операция пытается восстановить систему в исходное положение. Если все нормально, то происходит cleanup, и операция завершена.

Такая простая стейт-машина у нас на любой операции.

Promotion (смена мастера)


Это очень частая операция в БД. Были вопросы о том, как делать alter на горячем master-сервере, который работает — он же встанет колом. Просто все эти операции производятся на slave-серверах, и потом slave меняется с master местами. Поэтому операция promotion очень частая.


Нужно обновить kernel — делаем swap, нужно обновить версию MySQL — обновляем на slave, переключаем на master, обновляем там.


Мы добились очень быстрого promotion. Например, у нас для четырех шардов сейчас promotion порядка 10-15 с. На графике выше видно, что при promotion availability пострадало на 0,0003%.

Но нормальные promotion не так интересны, потому что это обычные операции, которые выполняются каждый день. Интересны фейловеры.

Фейловер (замена поломанного мастера)


Фейловер (failover) значит, что база данных умерла.

  • Если сервер действительно умер, это просто идеальный случай.
  • На самом деле бывает так, что серверы частично жив.
  • Иногда сервер очень медленно умирает. У него отказывают raid-контроллеры, дисковая система, какие-то запросы возвращают ответы, но какие-то потоки блокируются и не возвращают ответы.
  • Бывает такое, что master просто перегружен и не отвечает на наши health-check. Но если мы сделаем promotion, то новый master тоже будет перегружен, и станет только хуже.

Замена умерших master серверов у нас происходит примерно 2–3 раза в день, это полностью автоматизированный процесс, никакая интервенция человека не нужна. Критическая секция занимает примерно 30 с, и в ней есть куча дополнительных проверок того, жив ли сервер на самом деле, или, может быть, он уже умер.

Ниже примерная схема того, как работает фейловер.


В выделенной секции мы перезагружаем master-сервер. Это нужно, потому что у нас MySQL 5.6, а в нем semisync репликация не lossless. Поэтому возможны phantom reads, и нам нужно этот master, даже если он не умер, как можно быстрее убить, чтобы клиенты от него отключились. Поэтому мы делаем hard reset через Ipmi — это первая самая важная операция, которую мы должны сделать. В MySQL 5.7 версии это не так критично.

Синхронизация кластера. Зачем нам нужна синхронизация кластера?


Если вспомнить предыдущую картинку с нашей топологией, у одного master-сервера есть три slave-сервера: два в одном дата-центре, один — в другом. При promotion нам нужно, чтобы master был в том же основном дата-центре. Но иногда, когда slave нагружены, при semisync бывает так, что semisync-slave’ом становится slave в другом дата-центре, потому что он-то не нагружен. Поэтому нам нужно сначала синхронизировать весь кластер, а потом уже сделать promotion на slave в нужном нам дата-центре. Это делается очень просто:

  • Мы останавливаем все I/O thread на всех slave-серверах.
  • После этого мы уже точно знаем, что master «read-only», так как отключился semisync и туда больше никто ничего записать не может.
  • Дальше мы выбираем slave с наибольшим retrieved/executed GTID Set, то есть с наибольшей транзакцией, которую он либо скачал, либо уже применил.
  • Перенастраиваем все slave-серверы на этот выбранный slave, запускаем I/O thread, и они синхронизируются.
  • Ждем, пока они синхронизируются, после этого у нас весь кластер становится синхронизированным. В конце проверяем, что у нас везде executed GTID set установлен на одну и ту же позицию.

Вторая важная операция — синхронизация кластеров. Дальше начинается promotion, который происходит следующим образом:


  • Мы выбираем любой slave в нужном нам дата-центре, говорим ему, что он master, и запускаем операцию стандартного promotion.
  • Мы перенастраиваем все slave-сервера на этот master, останавливаем там репликацию, применяем ACLs, вбиваем пользователей, останавливаем какие-то proxy, возможно, что-то перезагружаем.
  • В конце концов мы делаем read_only = 0, то есть говорим, что теперь на master можно записывать, и обновляем топологию. С этого момента клиенты идут на этот master и у них все работает.
  • Дальше у нас есть не критичные пост-шаги обработки. В них мы перезапускаем какие-то сервисы на этом хосте, перерисовываем конфигурации, делаем дополнительные проверки, что все точно работает, например, что proxy пропускает трафик.
  • После этого вся операция завершена.

На любом шаге, в случае ошибки мы пытаемся сделать rollback до того момента, до которого можем. То есть мы не можем сделать rollback для reboot. Но для операций, для которых это возможно, например, переназначения — change master — мы можем вернуть master на предыдущий шаг.

Бэкапы


Бэкапы — очень важная тема в базах данных. Я не знаю, делаете ли вы бэкапы, но мне кажется, все должны их делать, это уже избитая шутка.

Паттерны использования

? ? Добавить новый slave

Самый главный паттерн, который мы используем при добавлении нового slave-сервера, мы просто его восстанавливаем из бэкапа. Это происходит постоянно.

? ? Восстановление данных на точку в прошлом

Достаточно часто пользователи удаляют данные, а потом просят их восстановить, в том числе из базы данных. Эта достаточно частая операция восстановления данных на точку в прошлом у нас автоматизирована.

? ? Восстановить целиком весь кластер с нуля

Все думают, что бэкапы нужны для того, чтобы восстановить все данные с нуля. На самом деле эта операция практически никогда нам не требовалась. Последний раз мы использовали ее 3 года назад.

Мы смотрим на бэкапы, как на продукт, поэтому говорим клиентам, что у нас есть гарантии:

  1. Мы можем восстановить любую базу данных. В нормальных условиях ожидаемая скорость восстановления 1Тб за 40 минут.
  2. Любую базу можно восстановить на любую точку в течение последних шесть дней.

Это наши основные гарантии, которые мы даем нашим клиентам. Скорость в 1 Тб за 40 минут, потому что есть ограничения по сети, мы не одни на этих стойках, на них еще продакшен трафик.

Цикл


Мы ввели такую абстракцию, как цикл. В одном цикле мы стараемся забэкапить практически все наши базы данных. У нас одновременно крутится 4 разных цикла.


  • Первый цикл выполняется каждые 24 часа. Мы бэкапим все наши шардированные базы данных на HDFS, это порядка тысячи с лишним хостов.
  • Каждые 6 часов мы делаем бэкапы для unsharded databases, у нас еще есть некоторые данные на Global DB. Мы очень хотим от них избавиться, но, к сожалению, они до сих пор есть.
  • Каждые 3 дня мы сохраняем полностью всю информацию шардированных баз данных на S3.
  • Каждые 3 дня мы полностью сохраняем на S3 всю информацию нешардированных баз данных.



Все это хранится в течении нескольких циклов. Допустим, если мы храним 3 цикла, то в HDFS у нас есть последние 3 дня, и последние 6 дней в S3. Так мы поддерживаем наши гарантии.

Это пример, как они работают.


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

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

Горячие бэкапы



Сейчас у нас происходит hot-бэкап, для которого мы используем инструмент Percona xtrabackup. Запускаем его в режиме —stream=xbstream, и он нам возвращает на рабочей базе данных, поток бинарных данных. Дальше у нас есть script-splitter, который этот бинарный поток делит на четыре части, и потом мы сжимаем этот поток.

MySQL хранит данные на диске очень странным образом и у нас получилась компрессия больше 2x. Если база данных занимает 3 Тб, то, в результате сжатия, бэкап занимает примерно 1 500 Гб. Дальше мы шифруем эти данные, чтобы никто не мог их прочитать, и отправляем в HDFS и в S3.

В обратную сторону работает абсолютно точно так же.


Подготавливаем сервер, куда будем устанавливать бэкап, достаем бэкап из HDFS или из S3, декодируем и декомпрессируем его, splitter сжимает это все и отправляет в xtrabackup, который восстанавливает все данные на сервер. Потом происходит crash-recovery.

Некоторое время самой главной проблемой hot бэкапов было то, что crash-recovery занимает достаточно длительное время. В целом нужно проиграть все транзакции за то время, пока вы делаете бэкап. После этого мы проигрываем binlog, чтобы наш сервер догнал текущий master.

Как мы сохраняем binlogs?

Раньше мы сохраняли файлики binlog’ов. Мы собирали на master файлики, чередовали их каждые 4 минуты, либо по 100 Мб, и сохраняли в HDFS.

Сейчас у нас используется новая схема: есть некий Binlog Backuper, который подключен к репликациям и ко всем базам данных. Он, по сути, постоянно сливает binlog к себе и сохраняет их на HDFS.


Соответственно, в предыдущей реализации мы могли потерять 4 минуты бинарных логов, если потеряли все 5 серверов, в этой же реализации, в зависимости от нагрузки, мы теряем буквально секунды. Все сохраненное в HDFS и в S3 хранится в течение месяца.

Холодные бэкапы

Мы подумываем перейти на холодные бэкапы.

Предпосылки для этого:

  1. Скорость каналов на наших серверах стала больше — было 10 Гб, стало 45 Гб — можно утилизировать больше.
  2. Хочется восстанавливать и создавать клоны быстрее, потому что нам нужен более умный scheduler для multi instance и хочется очень часто перекидывать slave и master с сервера на сервер.
  3. Самый важный момент — при холодном бэкапе мы можем гарантировать, что бэкап работает. Потому что, когда мы делаем холодный бэкап, мы просто копируем файл, потом запускаем базу данных, и как только она запустилась, мы знаем, что этот бэкап работает. После pt-table-checksum мы точно знаем, что данные на файловой системе консистентны.

Гарантии, которые получились при холодных бэкапах в наших экспериментах:

  1. В нормальных условиях ожидаемая скорость восстановления 1Тб за 10 минут, потому что это просто копирование файлов. Не нужно делать crash-recovery, а это самое проблемное место.
  2. Любую базу можно восстановить на любой период времени за последние шесть дней.



В нашей топологии есть slave в другом дата-центре, который практически ничего не делает. Мы периодически его останавливаем, делаем холодный бэкап и запускаем обратно. Все очень просто.

Планы ++


Это планы на дальнее будущее. Когда мы будем делать обновление нашего Hardware парка, мы хотим добавить на каждый сервер дополнительный шпиндельный диск (HDD) порядка 10 Тб, и делать на него горячие бэкапы + crash recovery xtrabackup, а после этого загружать уже бэкапы. Соответственно, у нас будут бэкапы на всех пяти серверах одновременно, в разные точки времени. Это, конечно, усложнит всю обработку и оперирование, но снизит стоимость, потому что HDD стоит копейки, а огромный кластер HDFS стоит дорого.

Клон


Как я уже говорил, клонирование — это простая операция:

  1. это либо восстановление из бэкапа и проигрывание бинарных логов;
  2. либо процесс бэкапа сразу на целевой сервер.

В диаграмме, где мы копируем на HDFS, также данные просто копируются на другой сервер, где есть ресивер, который принимает все данные и восстанавливает их.

Автоматизация


Конечно же, на 6 000 серверах никто ничего не делает вручную. Поэтому у нас есть различные скрипты и сервисы автоматизации, их очень много, но основные из них — это:

  • Auto-replace;
  • DBManager;
  • Naoru, Wheelhouse

Auto-replace


Этот скрипт нужен, когда сервер умер, и нужно понять, правда ли он умер, и что за проблема — может, сеть поломалась или еще что-то. Это нужно решить, как можно быстрее.

Availability (доступность) — это функция от времени между возникновением ошибок и временем, за которое вы можете детектировать и починить эту ошибку. Починить мы можем очень быстро — у нас recovery очень быстрый, поэтому нам нужно как можно скорее определить существование проблемы.


На каждом сервере MySQL запущен сервис, которые пишет heartbeat. Heartbeat — это текущий timestamp.


Есть также другой сервис, который пишет значение некоторых предикатов, например, что master в режиме read-write. После этого второй сервис отправляет в центральное хранилище этот heartbeat.

У нас есть auto-replace скрипт, работающий по такой схеме.
Схема в лучшем качестве и отдельно ее увеличенные фрагменты есть в презентации доклада, начиная с 91 слайда.

Что здесь происходит?

  • Есть основной цикл, в котором мы проверяем heartbeat в глобальной базе данных. Смотрим, зарегистрирован этот сервис или нет. Подсчитываем heartbeat’ы, например, есть ли два heartbeat’а за 30 с.
  • Далее, смотрим, удовлетворяет ли их количество пороговому значению. Если нет, то значит, что-то с сервером не так — раз он не послал heartbeat.
  • После этого мы делаем reverse check на всякий случай — вдруг эти два сервиса умерли, что-то с сетью, или глобальная база данных не может почему-то записать heartbeat. В reverse check мы подсоединяемся к поломанной базе данных и проверяем ее состояние.
  • Если уже ничего не помогло, мы смотрим, прогрессирует ли master position или нет, происходят ли на него записи. Если ничего не происходит, то этот сервер точно не работает.
  • Последний этап — собственно auto-replace.

Auto-replace очень консервативен Он никогда не хочет делать много автоматических операций.

  1. Во-первых, мы проверяем, не было ли операций с топологией недавно? Может быть, этот сервер только что был добавлен и что-то на нем еще не запущено.
  2. Проверяем, не было ли каких-то замен в этом же кластере в какой-то промежуток времени.
  3. Проверяем, какой у нас failure limit. Если у нас много проблем одномоментно — 10, 20 — то мы не будем автоматически их все решать, потому что можем ненароком нарушить работу всех баз данных.

Поэтому решаем только одну проблему за раз.

Соответственно, для slave-сервера мы запускаем клонирование и просто удаляем его из топологии, а если это master, то запускаем фейловер, так называемый emergency promotion.

DBManager


DBManager — это сервис для управления нашими базами данных. В нем есть:

  • умный планировщик задач, который точно знает, когда какой job запустить;
  • логи и вся информация: кто, когда и что запускал — это источник правды;
  • точка синхронизации.



DBManager достаточно прост архитектурно.

  • Есть клиенты, это либо DBA, которые что-то делают через web интерфейс, либо скрипты/сервисы, которые написали DBA, которые обращаются по gRPC.
  • Есть внешние системы вроде Wheelhouse и Naoru, которая по gRPC ходит в DBManager.
  • Есть планировщик, который понимает, какую операцию, когда и где он может запустить.
  • Есть очень тупой worker, который, когда к нему приходит операция, запускает ее, проверяет по PID. Worker может перезагружаться, процессы не прерываются. Все worker’ы расположены как можно ближе к серверам, на которых происходят операции, чтобы, например, при обновлении ACLS нам не нужно было делать много раунд-трипов.
  • На каждом SQL-хосте у нас есть некий DBAgent — это RPC сервер. Когда нужно провести какую-то операцию на сервере, мы отправляем RPC запрос.

У нас есть web интерфейс для DBManager, где можно посмотреть текущие запущенные задачи, логи к этим задачам, кто и когда что запустил, какие операции были проведены для сервера конкретной базы данных и т.д.


Есть достаточно простой CLI интерфейс, где можно запускать задачи и также просматривать их в удобных представлениях.


Remediations


Еще у нас есть система реагирования на проблемы. Когда у нас что-то поломалось, например, диск вышел из строя, либо какой-то сервис не работает, срабатывает Naoru. Это система, которая работает во всем Dropbox, все ею пользуются, и она построена именно для таких небольших задач. Про Naoru я рассказывал в своем докладе в 2016 году.

Система Wheelhouse основана на базе стейт-машины и предназначена для долгих процессов. Например, нам нужно обновить ядро на всех MySQL на всем нашем кластере из 6000 машин. Wheelhouse четко это делает — обновляет на slave-сервере, запускает promotion, slave становится master, обновляет на master-сервере. Эта операция может занять месяц или даже два.

Мониторинг




Это очень важно.

Если вы не мониторите систему, то скорее всего она не работает.

Мы мониторим все в MySQL — вся информация, которую мы можем получить из MySQL, у нас где-то сохраняется, мы можем получить к ней доступ по времени. Мы сохраняем информацию по InnoDb, статистику по запросам, по транзакциям, по длине транзакций, перцентили на длины транзакции, по репликации, по сети — все-все-все — огромное количество метрик.

Alert


У нас настроено 992 алерта. Вообще-то на метрики никто не смотрит, мне кажется, что нет людей, которые приходят на работу и начинают смотреть на график метрик, есть более интересные задачи.


Поэтому есть алерты, которые срабатывают при достижении определенных пороговых значений. У нас 992 алерта, что бы ни случилось, мы об этом узнаем.

Инциденты




У нас есть PagerDuty — это сервис, через который распространяются алерты на ответственных лиц, которые начинают принимать меры.


В данном случае произошла ошибка в emergency promotion и сразу после этого зарегистрировался алерт о том, что упал master. После этого дежурный проверил, что помешало emergency promotion, и сделал необходимые ручные операции.

Мы обязательно разбираем каждый произошедший инцидент, для каждого инцидента у нас есть задача в task tracker. Даже если этот инцидент — проблема в наших алертах, мы тоже создаем задачу, потому что, если проблема в логике алерта и порогах срабатывания, то их надо поменять. Алерты не должны просто так портить людям жизнь. Алерт — это всегда больно, особенно в 4 часа ночи.

Тестирование


Как и с мониторингом, я уверен, что тестированием занимаются все. Помимо юнит-тестов, которыми мы покрываем наш код, у нас есть интеграционные тесты, в которых мы тестируем:

  • все топологии, которые у нас есть;
  • всех операции над этими топологиями.

Если у нас есть promotion операции, мы тестируем в интеграционном тесте promotion операции. Если у нас есть клонирования, мы делаем клонирование для всех топологий, которые у нас есть.

Пример топологии


У нас есть топологии на все случаи жизни: 2 дата-центра с multi instance, с шардами, без шардов, с кластерами, один дата-центр — вообще практически любая топология — даже те, которые мы не используем, просто, чтобы посмотреть.


В этом файле у нас просто есть настройки, какие сервера и с чем нам нужно поднимать. Например, нам нужно поднять master, и мы говорим, что сделать это нужно с такими-то данными инстансов, с такими-то базами данных на таких-то портах. У нас практически все собирается с помощью Bazel, который на базе этих файлов создает топологию, запускает MySQL сервера, после этого запускается тест.


Тест выглядит очень просто: мы указываем, какая используется топология. В данном тесте Уы тестируем auto_replace.

  • Создаем сервис auto_replace, стартуем его.
  • Убиваем master в нашей топологии, ждем некоторое время и смотрим, что target-slave, стал master. Если нет, то тест не пройден.

Stages


Stage-окружение — это такие же базы данных, как и в продакшене, но на них нет пользовательского трафика, а есть некий синтетический трафик, который похож на продакшен, через Percona Playback, sysbench и похожие системы.

В Percona Playback мы записываем трафик, потом проигрываем его на stage-окружении с различной интенсивностью, можем в 2-3 раза быстрее проиграть. То есть это искусственная, но очень близкая к реальной нагрузка.

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

Плюс мы тестируем все автоматизации вместе, потому что в интеграционных тестах, скорее всего, тестируется одна часть системы, а в стейджинге все автоматизированные системы работают одновременно. Иногда вы думаете, что система поведет себя так, а не иначе, но она может повести себя вообще по-другому.

DRT (Disaster recovery testing)


Также мы проводим тесты в продакшене — прямо на реальных базах. Это называется Disaster recovery testing. Почему нам это нужно?

? ? Мы хотим протестировать наши гарантии.

Это делают многие крупные компании. Например, в Google есть один сервис, который работал настолько стабильно — 100% времени, — что все сервисы, которые его использовали, решили, что этот сервис реально 100% стабилен и никогда не падает. Поэтому Google пришлось этот сервис ронять специально, чтобы пользователи учитывали и такую возможность.

Так и мы — у нас есть гарантия, что MySQL работает — а иногда не работает! И у нас есть гарантия, что он может не работать какой-то промежуток времени, клиенты должны это учитывать. Периодически мы убиваем production master, либо, если мы хотим сделать фейловер, убиваем все slave-серверы, чтобы посмотреть, как поведет себя semisync репликация.

? ? Клиенты готовы к этим ошибкам (замена и смерть мастера)

Почему это хорошо? У нас был случай, когда при promotion 4 шардов из 1600, availability падала до 20%. Кажется, что что-то не так, для 4 шардов из 1600 должны быть какие-то другие цифры. Фейловеры для этой системы происходили достаточно редко, примерно раз в месяц, и все решили: «Ну, это фейловер, бывает».

В какой-то момент, когда мы переходили на новую систему, один человек решил оптимизировать те два сервиса записи heartbeat и объединил их в одни. Этот сервис делал еще что-то и, в конечном итоге, умирал и переставали записываться heartbeat’ы. Так получилось, что для этого клиента у нас стало 8 фейловеров в день. Все лежало — 20% availability.

Оказалось, что в этом клиенте keep-alive 6 часов. Соответственно, как только master умирал, у нас все соединения держались еще 6 часов. Пул не мог дальше работать — у него коннекты держатся, он ограничен и не работает. Это починили.

Делаем фейловер опять — уже не 20%, но все равно много. Что-то все-равно не так. Оказалось, что баг в реализации пула. Пул при запросе обращался ко многим шардам, а потом соединял все это. Если какие-то шарды фейловерились, происходил какой-то race condition в Go коде, и весь пул забивался. Все эти шарды не могли больше работать.

Disaster recovery тестирование очень полезно, потому что клиенты должны быть готовы к этим ошибкам, они должны проверять свой код.

? ? Плюс Disaster recovery testing хорош тем, что проходит в бизнес часы и все на месте, меньше стресса, люди знают, что сейчас произойдет. Это происходит не ночью, и это здорово.

Заключение


? 1. Всё нужно автоматизировать, никогда не лезть руками.
Каждый раз, когда у нас кто-то лезет в систему руками, у нас все умирает и ломается — каждый божий раз! — даже на простых операциях. Например, умер один slave, человек должен был добавить второй, но решил удалить умерший slave руками из топологии. Однако вместо умершего он скопировал в команду живой — master остался вообще без slave. Такие операции не должны делаться вручную.

? 2. Тесты должны быть постоянные и автоматизированные (и в продакшене).
Ваша система меняется, ваша инфраструктура меняется. Если вы один раз проверили, и она вроде работала, это не значит, что она будет и завтра работать. Поэтому нужно постоянно, каждый день делать автоматизированное тестирование, в продакшене в том числе.

? 3. Обязательно нужно владеть клиентами (библиотеками).
Пользователи могут не знать, как работают базы данных. Они могут не понимать, зачем нужны таймауты, keep-alive. Поэтому лучше владеть этими клиентами — вам будет спокойнее.

? 4. Нужно определить свои принципы построения системы и свои гарантии, и всегда соблюдать их.

Таким образом можно поддерживать 6 тысяч серверов баз данных.

В вопросах после доклада, и особенно ответах на них, тоже много полезной информации.

Вопросы и ответы


— Что будет, если есть дисбаланс нагрузки на шарды — какая-то метаинформация о каком-то файле оказалась популярнее? Есть ли возможность этот шард расплитить, или нагрузка на шарды не отличается нигде на порядки?

Она не отличается на порядки. Она практически нормально распределена. У нас есть троттлинг, то есть мы не можем перегрузить шард по сути, мы троттлим на уровне клиента. Вообще бывает такое, что какая-нибудь звезда выкладывает фотографию, и шард практически взрывается. Тогда мы баним эту ссылку

— Вы говорили у вас 992 алерта. Можно поподробнее, что это такое — это из коробки или это создается? Если создается, то это ручной труд или что-то вроде машинного обучения?

Это все создается вручную. У нас есть собственная внутренняя система, называется Vortex, где хранятся метрики, в ней поддерживаются алерты. Есть yaml-файл, в котором написано, что есть условие, например, что бэкапы должны выполняться каждый день, и если это условие выполняется, то алерт не срабатывает. Если не выполняется, тогда приходит алерт.

Это наша внутренняя разработка, потому что мало кто умеет хранить столько метрик, сколько нам нужно.

— Насколько крепкие должны быть нервы, чтобы делать DRT? Ты уронил, CODERED, не поднимается, с каждой минутой паники все больше.

Вообще работать в базах данных — это реально боль. Если база данных упала, сервис не работает, весь Dropbox не работает. Это реальная боль. DRT полезно тем, что это бизнес-часы. То есть я готов, я сижу за рабочим столом, я выпил кофе, я свеж, я готов сделать все, что угодно.

Хуже, когда это происходит в 4 часа ночи, и это не DRT. Например, последний сильный сбой у нас был недавно. При вливании новой системы мы забыли выставить OOM Score для нашего MySQL. Там еще был другой сервис, который читал binlog. В какой-то момент наш оператор вручную — опять же вручную! — запускает команду по удалению в Percona checksum-table какой-то информации. Просто обычное удаление, простая операция, но эта операция породила огромный binlog. Сервис прочитал этот binlog в память, OOM Killer пришел и думает, кого бы убить? А мы забыли OOM Score выставить, и он убивает MySQL!

У нас в 4 часа ночи умирают 40 мастеров. Когда умирает 40 мастеров, это реально очень страшно и опасно. DRT — это не страшно и не опасно. Мы лежали где-то час.

Кстати, DRT — это хороший способ отрепетировать такие моменты, чтобы мы точно знали, какая последовательность действий нужна, если что-то массово поломается.

— Хотел бы подробнее узнать про переключение master-master. Во-первых, почему не используется кластер, к примеру? Кластер баз данных, то есть не master-slave с переключением, а master-master аппликация, чтобы если один упал, то и не страшно.

Вы имеете в виду что-нибудь вроде group replication, galera cluster и т.п.? Мне кажется, group application еще не готов к жизни. Galera мы, к сожалению, еще не пробовали. Это здорово, когда фейловер есть внутри вашего протокола, но, к сожалению, у них есть очень многих дргуих проблем, и не так просто перейти на это решение.

— Кажется, в MySQL 8 есть что-то типа InnoDb кластера. Не пробовали?

У нас до сих пор еще 5.6 стоит. Я не знаю, когда мы перейдем на 8. Может, попробуем.

— В таком случае, если у вас есть один большой master, при переключении с одного на другой, получается на slave-серверах высокой нагрузкой скапливается очередь. Если master погасить, надо, чтобы очередь добежала, чтобы slave переключить в режим master — или как-то по-другому это делается?

Нагрузка на master регулируется semisync’ом. Semisync ограничивает запись на мастер производительностью slave-серверов. Конечно, может быть такое, что транзакция пришла, semisync отработал, но slave’ы очень долго проигрывают эту транзакцию. Нужно тогда подождать, пока slave проиграет эту транзакцию до конца.

— Но тогда на master будут поступать новые данные, и надо будет...

Когда мы запускаем процесс promotion, мы отключаем I/O. После этого master не может ничего записать, потому что semisync репликация. Может прийти фантомное чтение, к сожалению, но это другая проблема уже.

— Эти все красивые стейт—машины — на чем написаны скрипты и как сложно добавить новый шаг? Что нужно сделать тому, кто пишет эту систему?

Все скрипты написаны на Python, все сервисы написаны на Go. Это наша политика. Логику поменять несложно — просто в Python-коде, по которому генерируется стейт-диаграмма.

— А можно подробнее про тестирование. Как написаны тесты, как они разворачивают ноды в виртуалке — это контейнеры?

Да. Тестирование у нас собирается с помощью Bazel. Есть некие настроечные файлы (json) и Bazel поднимает скрипт, который по этому настроечному файлу создает топологию для нашего теста. Там описаны разные топологии.

У нас это все работает в docker-контейнерах: либо это работает в CI, либо на Devbox. У нас есть система Devbox. Мы все разрабатываем на некоем удаленном сервере, и это может на нем работать, например. Там это тоже запускается внутри Bazel, внутри docker-контейнера или в Bazel Sandbox. Bazel очень сложный, но прикольный.

— Когда вы сделали на одном сервере 4 инстанса, не потеряли ли вы в эффективности использования памяти?

Каждый инстанс стал меньше. Соответственно, чем с меньшей памятью MySQL оперирует, тем ему проще жить. Любой системе проще оперировать небольшим количеством памяти. В этом месте мы ничего не потеряли. У нас есть простейшие С-группы, которые ограничивают по памяти эти инстансы.

— Если у вас 6 000 серверов хранят базы данных, можете назвать, сколько миллиардов петабайт хранится в ваших файлах?

Это десятки экзабайт, мы переливали данные с Амазона в течение года.

— Получается, у вас вначале было 8 серверов, на них по 200 шардов, потом 400 серверов по 4 шарда. У вас 1600 шардов — это какое-то жестко заданное значение? Вы больше не сможете никогда сделать? Это будет больно, если вам понадобится, например, 3 200 шардов?

Да, изначально было 1600. Это было сделано чуть меньше 10 лет назад, и до сих пор живем. Но у нас еще есть 4 шарда — в 4 раза мы можем еще увеличить место.

— Как умирают сервера, в основном по каким причинам? Что происходит чаще, что реже, и особенно интересно, происходят ли спонтанные карапты блоков?

Самое главное — это диски вылетают. У нас RAID 0 — диск вылетел, мастер умер. Это самая главная проблема, но нам проще заменить этот сервер. Google проще заменить дата-центр, нам сервер пока еще. Corruption checksum у нас практически не бывало. Если честно, я не помню, когда последний раз такое было. Просто мы достаточно часто обновляем мастера. У нас время жизни одного мастера ограничено 60 днями. Он не может жить дольше, после этого мы его заменяем на новый сервер, потому что почему-то в MySQL постоянно что-то накапливается, и через 60 дней мы видим, что начинают проблемы происходить. Может быть, не в MySQL, может быть, в Linux.

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

— Вы сказали, что за последние 6 дней можете восстановиться из бэкапа на любое состояние. Например, человек залил JPEG с одним названием, потом залил такой же JPEG, но измененный, то вы можете достать первую версию? То есть, получается, вы храните версионность файлов и какие-то метаданные с версиями? Если человек попросит — я хочу достать первую версию файла, вы можете ему это отдать или нет?

Мы храним информацию о файле, о блоках. Мы можем — в Dropbox есть возможность восстанавливать файлы.

— Как вы потом вычищаете это все? Нет проблем с фрагментацией на дисках и так далее? Много данных стирается с диска, получается, через какое-то время, когда версия становится ненужной, протухшей? Допустим, человек залил 10 версий файлов поочередно. Очевидно, через 7 дней в бэкапе вы поймете, что вам первые 6 версий уже не нужны, и их нужно удалить. Или они вечно хранятся?

Вообще в Dropbox есть какие-то гарантии, за какой промежуток времени сколько версий хранится. Это немножко другое. Есть система, которая умеет восстанавливать файлы, и там файлы просто не удаляются сразу, они в какую-то корзину кладутся.

Есть проблема, когда совершенно все удалено. Файлы есть, есть блоки, но в базе данных нет информации, как из этих блоков файл собрать. В такой момент мы можем проиграть до какого-то момента, то есть восстановились за 6 дней, проиграли до момента, когда этот файл был удален, не стали его удалять, восстановили и отдали пользователю.

Следите за блогом или подпишитесь на рассылку, в facebook или youtube-канал — мы регулярно публикуем свежие материалы и обновления в подготовке Highload++ 2018. В последнем можно принять деятельное участие, до 1 сентября отправив заявку на доклад.

Комментарии (9)


  1. Vadem
    17.07.2018 14:46

    Крутой доклад. На мой взгляд, один из самых интересных на Highload++ 2017.


  1. servekon
    17.07.2018 15:48
    +2

    -Делаете ли вы бекапы?
    -Я делаю бекапы и сохраняю копии в Dropbox.


  1. TimsTims
    17.07.2018 17:57
    +1

    Помню помогал сисадмином в одной небольшой конторе, которая все горячие файлы хранила в общей сетевой папке. Там были куча накладных, счетов, всякие договоры со всякими поставщиками итд, файлы очень нужные без них никак. Всего около 15 гбайт, и 20 000 файлов.
    И вот, один из менеджеров (их всего 3) словил шифровальщика. Все файлы на шаре были зашифрованы…
    Как же хорошо, что за 3 месяца до инцидента, я поставил на комп с которого осуществляется раздача — Dropbox (тогда еще было бесплатно 25гбайт по акции от htc). Все файлы удалось откатить на 1 день назад через суппорт. 4 часа загрузки(канал слабенький), и утром все файлы были на своих местах. Никто даже не понял что произошло (ну вирус, ну переустановить заново винду).


  1. alexkrash
    17.07.2018 19:24

    В 2012 году мы поняли, что создавать таблицы и обновлять их в БД на каждую добавляемую бизнес-логику очень сложно, муторно и проблематично.

    Не очень понятно — теперь альтеров нет вообще? Вроде далее описывается, что есть шарды, куча нод, и всё такое.
    Репликация в MySQL не параллельная, и все шарды работают в один поток. Если с одним шардом что-то происходит, то остальные тоже становятся жертвами.

    ? Самый большой минус — намного сложнее всем этим управлять. Нужен какой-то умный планировщик, который будет понимать, куда он может вынести этот инстанс, где будет оптимальная нагрузка.

    Насколько я понимаю, на момент выхода доклада MySQL 5.7, поддерживающий многопоточную репликацию, уже применялся в ряде коммерческих проектов.


    1. M0sTH8
      17.07.2018 20:12

      1) Изменения схемы всё ещё есть, но их теперь единицы
      2) Переход между мажорными релизами(иногда даже и между минорными) не самый тривиальный процесс при большом количестве не гомогенного железа/софта. На момент доклада в dropbox использовался mysql 5.6


  1. johndow
    18.07.2018 07:18

    А что вы используете для автоматизации свопа master-slave?
    Какое-то самописное решение? Или что-то open source?


    1. M0sTH8
      18.07.2018 08:50

      Самописное.


      1. johndow
        18.07.2018 09:03

        Может быть можете посоветовать что-то open source на эту тему?
        Нам не нужно это делать постоянно, но иногда надо и хочется чтобы это было автоматизировано, без ручных операций и максимально быстро.
        Спасибо.


        1. M0sTH8
          18.07.2018 09:05

          Я сам не пользовался, но можно посмотреть на github.com/github/orchestrator