От оценки количества маленьких кусочков мыла, которые гости заберут домой, до расчета количества ромуланского эля, которое нужно заказать для городских съездов по Звездному пути, гостиничная индустрия сталкивается с множеством инженерных и логистических проблем. И хотя они могут быть не такими обширными, как весь Альфа-квадрант, инженерные задачи HotelTonight достаточно серьезны, чтобы исказить работу, которую мы выполняем на сервере.

Одна из задач масштаба предприятия , с которой мы недавно столкнулись, заключалась в том, чтобы выяснить, как управлять большим объемом обновлений тарифов на отели, быстро их читая. Для нас решение было бесстыдным: распределенное хранилище данных без схемы, предназначенное только для добавления, которое мы построили на основе MySQL.

Вот как мы это сделали.

Обновление нашей системы обновлений

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

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

Наше первоначальное решение сохранения скоростей в типичной реляционной таблице SQL достигло своих пределов из-за перегрузки при записи, беспокойства по поводу миграции и высоких затрат на обслуживание. Вот как это выглядело до Shameless:

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

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

Разделение SQL без схемы

Основная идея Shameless заключалась в том, чтобы разделить обычную таблицу SQL на таблицы индекса и таблицы содержимого. Индексные таблицы сопоставляют поля, по которым вы хотите запросить, с UUID. Таблицы содержимого сопоставляют UUID с содержимым (телами) модели. Кроме того, сегментированы как таблицы индекса, так и таблицы содержимого. Например:

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

Тело модели не схематичное; в нем можно хранить произвольные структуры данных. А внутри тело сериализуется с помощью MessagePack и хранится в виде большого двоичного объекта в одном столбце базы данных - отсюда и необходимость в индексных таблицах.

Поскольку нет возможности запросить полезную нагрузку MessagePack, нам нужно извлечь поля, которые мы хотим запрашивать, в таблицу индексов. В нашем случае таблица индексов содержит три столбца для полей, по которым мы хотим запросить - hotel_id, checkin_date и stay_length - и столбец uuid, сопоставляющий поля запроса с записью. Следовательно, каждый запрос к Shameless становится n+1 запросами к базовой базе данных - один для получения n UUID совпадающих записей и n для получения последних версий каждой из этих записей (в правильном сегменте).

Вот как теперь распространяется содержимое старой таблицы в Shameless:

Еще одна замечательная особенность заключается в том, что Shameless скрывает всю сложность сегментирования и запросов за простым API.

Бесстыдное использование, 101

Вот как это можно использовать. На первом этапе добавьте Shameless в ваш Gemfile:

Затем определите свой магазин и назначьте его объекту (например, константе):

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

В первый раз вам нужно будет создать все базовые таблицы для ваших моделей и индексов. Сделать это можно из консоли или рейк-задачи:

Записать запись в магазин Shameless довольно просто. Метод put выполнит «upsert», вставив первую версию новой записи или вставив новую версию существующей записи:

Чтобы вернуть ставки, используйте метод where:

Это основы Shameless. Для более продвинутого использования (например, несколько ячеек на модель, переключение между версиями, использование Shameless в качестве журнала для потоковой обработки, аналогичной Kafka и т. Д.), Ознакомьтесь с README на GitHub.

Плюсы и минусы сегментирования индексных таблиц

Благодаря распределенной архитектуре Shameless мы можем поддерживать нормальную скорость около 2000 операций записи в секунду и около 3000 операций чтения в секунду с задержкой около 5 мс для записи и 2 мс для чтения.

Но - как это обычно бывает с проектами такого рода - нам пришлось пойти на некоторые компромиссы. Самым большим компромиссом было то, что мы больше не могли полагаться на базу данных, чтобы гарантировать ограничения ACID. Нам нужно было перенести управление транзакциями и логику одновременной записи в код приложения. Поскольку хранилище распределено, мы больше не могли выполнять СОЕДИНЕНИЯ или агрегации. Но Shameless того стоило.

Типсы от HT

Мы черпали вдохновение для создания Shameless от команд, которые создавали аналогичные решения для аналогичных проблем. Вот некоторые наиболее примечательные упоминания: Uber, FriendFeed и Pinterest.