Всем привет, меня зовут Андрей Колнооченко, я разрабатываю бэкенд Яндекс Диска и отвечаю за ядро файловой системы. Мы решаем задачу хранения методанных о миллиардах файлов сотен миллионов пользователей. Часто бывает так, что в ходе работы над базой данных не продумывают то, как она будет расти и развиваться. Это ведет к деградации времени запросов и к неожиданному поведению при росте нагрузки.
Рассмотрим на практических примерах те детали, на которые мы обращаем внимание при проектировании баз данных и разработке кода для работы с базой данных. Для начала посмотрим на первый этап развития баз данных. Когда мы только-только начали проект, радостно пишем свежий код для обработки пользовательских сценарий. Постараемся предугадать заранее, где могут быть проблемы и подстелить солонку, чтобы было не больно, когда проект запустится и начнет расти по количеству запросов и по количеству данных. В зависимости от того, насколько вам дорогие данные пользователей и какой даунтайм можно допустить, нужно выбрать стратегию резервирования ваших данных.
Например, для Диска потерять информацию даже об одном файле недопустимо. И простои должны быть настолько небольшими, что для любого пользователя проблемы на клиенте должны решаться в рамках простого повторного запроса. Поэтому, помимо регулярных бэкапов, каждый инстанс базы данных реплицируется в два дополнительных дата-центра. При этом в репликации соблюдается принцип строгой консистентности записи, то есть запись не считается завершенной, пока две реплики из трёх не подтвердят запись данных на диск. Это делает запись не такой быстрый, но отлично подходит для нашего случая, когда изменение происходит сильно реже, чем чтение. Помимо этого, большая часть изменений является либо вставкой, либо удалением, а такие операции в современных СУБД исполняются очень быстро.
В наших инсталляциях мы размещаем три реплики в разных дата-центрах. И в обычных случаях картина выглядит следующим образом. Есть мастер. В одной из реплик он записал изменения синхронно. А в другую – асинхронно. Что будет, если дата-центры потеряют связность с друг с другом, и, например, мастер не сможет записать изменения ни в одну из реплик? Для защиты данных пользователя на такой запрос будет сгенерирована ошибка, и запись в старый мастер не будет сделана для защиты от ситуации split-brain. СУБД же запустит процесс смены мастера.
Допустим, реплики договорились, и реплика 1 теперь стала мастером, а в реплику 2 записи идут синхронно. Когда связанность сети восстанавливается, старый мастер становится репликой и начинает применять изменения, которые пропустил. Таким образом, его отставание от нового мастера будет уменьшаться, и когда он полностью догонит лог транзакций, то сможет тоже принимать синхронные изменения с мастера.
Мы рассмотрели резервирование данных и репликацию. Посмотрим, как происходит рост количества данных. Обычно первые пользователи дают нам информацию о примерном количестве данных в различных таблицах. И можно прогнозировать рост данных с ростом посещаемости. Первое, о чем стоит подумать, — насколько хорошо будут выполняться запросы при росте количества данных в таблицах. Если мы видим, что время запроса будет линейно расти в зависимости от количества пользователей или количества данных у них, стоит заранее озаботиться об индексах для запросов.
Конечно, чудес не бывает. На самом деле индекс — это небольшая вспомогательная табличка, которая позволяет обменять время изменений и занимаемое место на время чтения. Для разных типов сравнений используются разные индексы. Например, если вам нужно искать по полному совпадению, например, айдишника, то подойдет хэш-индекс. А если нужно искать like-ом по префиксу, то нужно создать B3-индекс. В идеале индексы должны помещаться в память на инстансе базы данных. Для оценки размера индекса мы можем использовать один из простых способов:
Первый способ — посмотреть на размер похожих индексов. Второй способ — это склонировать какой-то инстанс базы данных, офлайн построить индекс на нем и примерно оценить, как размер индекса зависит от общего количества данных в таблице или от количества пользователей. Конечно, нужно понимать, что использование индексов небесплатное. Так как помимо дополнительного места индекс требует вычислений на его подсчёт. И на каждое изменение нужно его пересчитывать.
Существуют инструменты, которые могут указать на неиспользуемые индексы. Их удаление поможет сократить потребление памяти и ускорить добавление данных. Если появляются редкие, но очень тяжелые запросы, которые либо затрагивают данные многих пользователей, либо анализируют исторические данные, то стоит к таким запросам присмотреться. Возможно, строить индексы для них не очень рационально. Лучше будет данные для таких запросов переложить в какое-то OLAP-хранилище, такое как ClickHouse, и уже в нем выполнять запросы.
Итак, мы рассмотрели рост количества данных в таблицах. Давайте же рассмотрим рост количества запросов. При росте количества запросов важную роль начинает играть консистентность данных, чтобы записи в разных таблицах соответствовали друг другу. Количество инстансов в приложении растет, вероятность сбоев в произвольный момент времени тоже растет, могут появляться частичные записи в случае возникновения ошибок. Также возникает проблема конкурирующих запросов, которые меняют данные одного пользователя или просто связанные данные.