За последние годы NoSQL-системы стали господствующим направлением. Многие разработчики видят в NoSQL-системах, таких как MongoDB, Cassandra, Redis или Hadoop, оптимальный вариант для построения своих приложений, считая их единой семьей продуктов, которая обесценивает старые SQL-системы.
Зачастую, в основе решения об использовании базы данных NoSQL лежит рекламная шумиха или ошибочное убеждение, что реляционные базы данных не могут обеспечить такую же производительность, как базы данных NoSQL. Когда доходит до выбора базы данных, инженеры часто упускают из виду эксплуатационные расходы, а также соображения стабильности и зрелости технологии. Чтобы узнать больше об ограничениях и изъянах различных NoSQL (а также SQL) систем, обратите внимание на серию статей проекта Jepsen, опубликованную на Aphyr.com.
В этой статье мы объясним, почему, по нашему мнению, использовать MySQL для хранения пар ключ-значение лучше, чем большинство специализированных NoSQL-систем, а также предоставим инструкции для использования MySQL.
Определение сайтов Wix
Когда кто-нибудь нажимает ссылку на сайте Wix, его браузер посылает на сервер Wix HTTP-запрос с адресом сайта. Так происходит и с премиум-сайтом Wix, имеющим собственный домен (напр. domain.com), и с бесплатным сайтом на субдомене Wix (напр. user.wix.com/site). Сервер должен распознать запрашиваемый сайт по адресу сайта, выполняя поиск типа ключ-значение для пары URL-сайт. В дальнейшем обсуждении мы будем обозначать URL как «путь» (route).
Таблица routes используется, чтобы преобразовать адрес сайта в объект site. Поскольку для попадания на сайт могут использоваться различные пути, здесь имеет место отношение «многие к одному». Когда сайт найден, приложение загружает его, чтобы работать с ним. Объект site, в свою очередь, имеет сложную структуру, которая включает два списка дочерних объектов – различные сервисы, используемые сайтом. Ниже приведена примерная модель наших объектов. Здесь мы предполагаем, что используется стандартная SQL база данных с нормализованной структурой:
При обновлении сайта с традиционной нормализованной моделью нам необходимо использовать транзакцию для обновления множественных таблиц, чтобы обеспечить целостность данных (обратите внимание, что транзакция использует блокировку на уровне базы данных, что предотвращает одновременную запись, а иногда и чтение из задействованных таблиц). Продолжая работать с такой моделью, мы, вероятно, дойдем до ключа типа SERIAL в каждой таблице, внешних ключей и индекса в поле URL таблицы routes.
Однако, моделирование данных на основе нормализованной схемы чревато рядом осложнений:
• блокировки ограничивают доступ к таблице, поэтому при больших объемах данных это может ограничить нашу производительность;
• чтение объекта требует либо нескольких SQL запросов (4 в нашем случае) либо использования JOIN – и это тоже влияет на время задержки;
• ключи с атрибутом SERIAL требуют блокировки, что снова ограничивает производительность записи.
Эти проблемы ограничивают пропускную способность и распараллеливание запросов, которые нам способна обеспечить MySQL (или любая другая SQL-система). Из-за этих слабых мест, а также в силу того факта, что речь идет по сути о паре ключ-значение, многие разработчики предпочитают искать NoSQL-решение, которое обеспечивает лучшую производительность и распараллеливание, даже ценой стабильности, целостности и доступности.
Мы в Wix обнаружили, что MySQL, если ее творчески использовать в качестве хранилища пар ключ-значение, может работать лучше, чем MySQL с нормализованной моделью данных (приведенной выше), а также лучше, чем большинство NoSQL-систем. Наша нынешняя система обеспечивает такие параметры масштабирования, пропускной способности, распараллеливания запросов, времени задержки, которые сделали бы честь любой NoSQL-системе. Вот некоторые данные нашей системы:
• активная установка на три дата-центра (active-active-active);
• пропускная способность порядка 200 000 RPM;
• таблица routes имеет объем порядка 100 000 000 записей, 10 ГБ дискового пространства;
• таблица sites имеет порядка 100 000 000 записей, 200 ГБ дискового пространства;
• средняя задержка чтения – 1,0-1,5 миллисекунды (фактически, 0,2-0,3 мс. в одном дата-центре).
Заметьте, задержка порядка 1,0 мс. считается впечатляющим показателем в большинстве систем, построенных на паре ключ-значение, включая как облачные системы, так и системы с открытым кодом. И мы достигли этого, используя MySQL (простейшую, как принято считать, SQL-систему).
Вот схема, которую мы используем:
CREATE TABLE `routes` (
`route` varchar(255) NOT NULL,
`site_id` varchar(50) NOT NULL,
`last_update_date` bigint NOT NULL,
PRIMARY KEY (`key`),
KEY (`site_id`)
)
CREATE TABLE `sites` (
`site_id` varchar(50) NOT NULL,
`owner_id` varchar(50) NOT NULL,
`schema_version` varchar(10) NOT NULL DEFAULT '1.0',
`site_data` text NOT NULL,
`last_update_date` bigint NOT NULL,
PRIMARY KEY (`site_id`)
) /*ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=COMPRESSED KEY_BLOCK_SIZE=16*/;
Все поля, которые не используются в качестве условий запроса, были свернуты в единое поле типа blob (текстовое поле site_data). Оно включает таблицы sub-obj, а также любые поля самого объекта table. Заметьте, мы не используем ключей типа SERIAL, вместо этого мы используем поле типа VARCHAR(50), которое содержит созданные пользователем переменные GUID (об этом подробней в следующем разделе).
Ниже приведен используемый нами запрос, у него высокая пропускная способность и низкое время задержки:
select * from sites where site_id = (
select site_id from routes where route = ?
)
Здесь сначала выполняется запрос к таблице routes по уникальному индексу, запрос должен вернуть только одно значение. Затем мы просматриваем сайты по первичному ключу, снова ищем одно значение. Вложенный синтаксис позволяет обработать оба SQL-запроса за одно обращение к базе данных.
Результат, показанный выше, требует примерно 1 мс. последовательной работы в условиях высокого трафика и высокой частоты обновлений. Обновления полутранзакционные, даже без использования транзакций. Это становится возможным благодаря тому, что мы вводим весь сайт одной командой INSERT и, пока мы не введем данные в routes, запросы не будут его обнаруживать. То есть, когда мы вводим сначала данные о сайте, а затем о путях, мы уверены в целостности своих данных, даже в пограничной ситуации, пока наши данные в таблице sites не связаны.
Инструкции по использованию MySQL в качестве NoSQL-системы
Используя опыт, приобретенный в описанном выше примере (и других подобных случаях из практики Wix), мы разработали короткий список рекомендаций для использования MySQL в качестве NoSQL системы.
Главное, что следует помнить при использовании MySQL в качестве NoSQL-системы – это избегать блокировок на уровне базы и сложных запросов.
• Не используйте транзакции, которые предполагают блокировки. Вместо этого, используйте транзакции в приложении;
• не используйте ключи типа SERIAL. Такие ключи влекут за собой блокировки и усложняют конфигурацию вида active-active;
• используйте уникальные ключи, созданные клиентами. Мы используем GUID.
Оптимизируя структуру для чтения, обратите внимание еще на несколько дополнительных рекомендаций:
• не проводите нормализацию;
• если есть поле – оно должно индексироваться. Если поле для индекса не нужно, сохраните его в одном поле типа BLOB / TEXT (как JSON или XML);
• не используйте внешние ключи;
• проектируйте вашу структуру так, чтобы по запросу можно было прочитать отдельный ряд;
• не используйте оператор ALTER TABLE. Эти команды влекут за собой блокировки и периоды временной неработоспособности. Вместо этого, используйте перенос данных Live Migration.
При запросе данных:
• ищите записи по первичному ключу или по индексу;
• не пользуйтесь JOIN;
• не пользуйтесь функциями агрегирования;
• запускайте функции проверки (BI, исследование данных) на репликах, а не на головной базе.
Мы планируем написать еще одну статью, где расскажем подробней о переносе данных Live Migrations и транзакциях через приложение.
Вы можете думать по-новому
Пожалуй, это важнейший вывод из этой статьи. Это здорово – использовать MySQL в качестве NoSQL-системы, то есть не так, как она была спроектирована. Как показано в этой статье, примером служит использование MySQL для работы с парами ключ-значение вместо специально спроектированных для этого NoSQL-систем. Мы в Wix выбрали MySQL для работы с парами ключ-значение (и не только), поскольку ее легко использовать, ей легко управлять, и это отличная экосистема. В качестве бонуса, она обеспечивает показатели задержки, пропускной способности и распараллеливания, едва ли не превосходящие большинство NoSQL-систем.
Главный архитектор программного обеспечения конструктора для создания сайтов Wix,
Йоав Абрахами
Оригинал статьи: блог инженеров компании Wix
Комментарии (20)
xhumanoid
23.05.2016 18:13+16а теперь главные вопросы которые почему-то остаются за кадром:
1. sharding — у вас он ручной,
1.1 что делать если клиенты в разных шардах растут с разной скоростью и у нас на одном шарде 100млн, а на другом 1млн записей?
1.2 как делать перебалансировку в случае если нам нужно добавить +1 сервер в кластер
2. fault tolerance
2.1 мультимастер еще нужно уметь настраивать, в master-slave при выпадении master тоже веселье
если у вас 1 мастер и пачка слейвов, так как нагрузка на запись минимальная, почти все это чтение, то вам конечно повезло,
но ведь nosql любят в первую очередь за то, что у вас ключи разбиты по бакетам которых заметно больше чем серверов и эти бакеты свободно мигрируют между узлами (решаем задачу 1 с балансировкой и добавлением новых узлов), а выпавшего мастера автоматически подхватывает кто-то другой (в hbase и mongodb на основе кворума, у cassandra вообще мастера нету) (решаем задачу сложности конфигурации для задачи 2).
правда если уж начали про mysql в качестве nosql, то я бы ожидал услышать про handlersocket, чтобы проходить уже даже и мимо планировщика запросов, а сразу ломиться на получение данных
oxidmod
23.05.2016 18:39-2Почему не использовать представлення?
samizdam
24.05.2016 22:28Представление это результат определённого SQL запроса.
Запрос к представлению, т.о. запрос к результату другого запроса. Очевидно представления не могут дать прирост производительности, а только наоборот, увеличить нагрузку на БД.
gibson_dev
23.05.2016 20:07А почему varchar(50) а не char(50)? Как минимум компактнее будет на диске.
ValdikSS
23.05.2016 20:10Наоборот, varchar имеет разную длину, а char(50) всегда выделяет 50 символов.
gibson_dev
23.05.2016 20:13+1Вот в том и дело, данные меньше фрагментированы и на диске меньше места занимают, т.к. в поле varchar еще и длина хранится.
oxidmod
23.05.2016 20:41+1если вопрос в экономии места, то можно вообще в бинари (25).
а для клиента можно уже в пыхе развернуть обратно в стринг при необходимости
skovpen
23.05.2016 20:28+4А где тесты скорости?
Из документации mysql:
A LEFT [OUTER] JOIN can be faster than an equivalent subquery because the server might be able to optimize it better—a fact that is not specific to MySQL Server alone. Prior to SQL-92, outer joins did not exist, so subqueries were the only way to do certain things. Today, MySQL Server and many other modern database systems offer a wide range of outer join types.
zapimir
23.05.2016 22:12+1интересно, а почему
`last_update_date` bigint NOT NULL,
почему не int или timestamp?mnv
23.05.2016 22:28Предполагаю, что не int из-за того, что там хранят дату в виде целого числа наподобие 20160523222415 (ГГГГММДДччммсс). Но почему тогда не timestamp — хз. Возможно наткнулись на то, что индекс по timestamp в огромных таблицах не всегда эффективно работает в MySQL.
Lure_of_Chaos
24.05.2016 00:00+2Но ведь NoSQL используется не для хранения пар ключ-значение, а именно для хранения неструктурированных данных. А таковые пары — уже определенная структура.
Далее — почему MySQL, а не Postgres, который поддерживает многие типы данных, в т.ч. имеет тип JSON и неплохо работает с ним (не надо хранить блобы)?
Как итог — Вам вообще не нужен NoSQL.
orthanner
24.05.2016 08:47+4Используете MySQL в качестве NoSQL-базы — пусть (выше вам объяснили, почему так делать не стоит); но почему при таком раскладе вы не пользуетесь HandlerSocket?!
youROCK
24.05.2016 11:27+1Кстати говоря, если вы используете индексы по varchar-полям, и при этом там нет UTF-8, можно поменять сравнение (collation) на, скажем, latin1. Для нашего сервера с логами это позволило увеличить скорость вставки в 5-10 раз (!). Но лучше сначала проверять это предположение с помощью perf top — если видите uca_scanner_next_key или что-то похожее, жрущее 30-40% CPU, то изменение collation поможет :). Это уточнение к предлагаемой структуре для key-value значений.
Кстати говоря, получить десятки и даже сотни тысяч RPS (именно в секунду, а не в минуту, как у wix) вполне реально даже без использования handlersocket. Но с handlersocket многие вещи сильно дешевле по CPU и памяти, особенно коннект.
Вроде как в MySQL 5.7 стоимость коннекта была сильно снижена, так что может быть можно выжать даже большую производительность из одного сервера, если оно зачем-то нужно.
itcoder
25.05.2016 00:51+1не используйте внешние ключи
Вы поосторожнее с такими советами, а то люди придут, начитаются, а потом данные у них разъедутся — не делайте так никогда.
velvetcat
> В этой статье мы объясним, почему, по нашему мнению, использовать MySQL для хранения пар ключ-значение лучше, чем большинство специализированных NoSQL-систем
То, что Вы изобрели, ни разу не решает тех проблем, которые заставляют переходить на NoSQL.
lesha_firs
А вот у меня вопрос, какие основные принципы покрывает NoSql и почему реляционные базы данных уступают?
Я просто смотрю, вы разберитесь, мне просто кажется что при правильной архитектуре того-же postgres нет необходимости в NoSql.
Для меня NoSql это — что-то типа redis ключ-значение, для легкого кеша. Если я ошибаюсь, можно подробный ответ? или ссылки на литературу.
Razaz
Как вы слили все базы в один простой key-value :)
Тынц по базам