Собрались однажды 2 разработчика. И нужно было им новую HTTP API реализовать для игрового магазина. Дошло дело до выбора БД, которую стоит применить в проекте:
- Слушай, а как мы выберем? Реляционную БД использовать или NoSQL. В частности, может нужна документоориентированная?
- Сперва нужно понять какие данные будут в нашей предметной области!
- Да, вот я уже набросал схемку:
Для построения каталога нам потребуются все данные… У каждой игры свой каталог, отличать будем по game_id.
- Выглядит так, что у нас уже есть четко описанная структурированная модель, да и опыт использования MySQL есть в компании. Предлагаю использовать его!
И реализовали разработчики успешно свою задумку. Аккуратно нормализовали данные, использовали ORM для работы с БД из приложения. Написали красивый и аккуратный код.
Начали интегрироваться партнеры, и тут начались проблемы… Оказывается, API работает очень медленно и совсем не держит нагрузки! Масштабирование дает смехотворный прирост, подключение кэшей всевозможных уровней дает профит, но зависимость запросов от параметров пользователей (сегментирование и атрибутирование) снижают его практически до ноля.
Корнем проблемы являлось большое (очень) количество запросов к БД - на каждое отношение между сущностями ORM генерировала дополнительный запрос.
Разработчики опустили руки и приуныли. Что же делать? Повезло им, ведь нашли они мудрость, увековеченную на бумажном носителе (милая книга с кабанчиком от Клеппмана).
А книга и порекомендовала еще раз внимательно взглянуть на свою предметную область... Ведь отношения между сущностями образуют дерево! А дерево можно уместить в одном документе (или представить с помощью одного JSON), что позволит избежать такого количества запросов.
Вооружились идеей разработчики и просто сериализовали сущность в JSON и сложили в 1 столбец MySQL (+ несколько генерируемых столбцов с индексами, для поиска):
95 перцентиль уменьшилась более чем в 3 раза, пропорционально увеличился и выдаваемый rps одного инстанса приложения.
А ведь просто поменяли формат хранимых данных, никак не затрагивая инфраструктуру и конфигурацию приложения…
Какой вывод можно сделать?
Подход “мы всегда так делали” безопаснее в большинстве случаев за счет предыдущего опыта, но может быть неэффективен для новых задач.
Важнее понимать какие концепты помогут достичь требуемого качества, а затем выбирать технологии, реализующие их, нежели просто выбирать между технологиями
Стереотипное мышление вроде “MongoDB для неструктурированных, для структурированных что-нибудь реляционное” или “Ну Redis небезопасный, поэтому не будем там хранить ничего” и т.п. скорее вредно. Зачастую все зависит от реализации приложения и конфигурации сервисов.
Tiendil
Вывод в том, что итерационная разработка и контроль метрик рулят. Потому, что никогда не угадаешь какой уровень нормализации данных потребуется.
aspirin4k Автор
Полностью согласен.
Но технические метрики ты можешь собирать эффективно только когда у приложения есть реальный трафик. Можно конечно на искусственных сценариях гонять разные нагрузочные (если мы говорим о производительности)… так и надо делать по-хорошему ). Но практика показывает, что первая боевая интеграция пойдет не так ) кажется, что более вдумчивый подход к проектированию может уменьшить шансы факапа…
AlexLeonov
Во-первых то, что вы забыли о существовании нагрузочного тестирования говорит лишь о том, что вы забыли о существовании нагрузочного тестирования, а не о том, что «приложению нужен реальный трафик».
Пропускаете этапы разработки — получаете факап с разработкой. Всё логично.
Во-вторых, конечно, любой вменяемый разработчик начал бы проектирование архитектуры не с сущностей ORM, а с вопроса «А какие типовые сценарии выборки и манипуляции данными предполагаются?»
А вот уже из типовых запросов и стали бы рождаться схемы данных. А не наоборот, как у вас.
aspirin4k Автор
Во-первых, не понятно, каким образом вы сделали вывод, что я забыл о существовании нагрузочного тестирования? )
Во-вторых, разве в начальном диалоге не описано, какие данные и каким образом будут доставаться?
В-третьих, ошибки могут совершаться, и, шарив опыт, можно позволить другим (более умным, но менее опытным людям) их избежать ) поэтому мне непонятен ваш хейт )
BugM
У вас ошибка уровня джунов. Даже типовой мидл на глазок напишет код держащий десяток РПС. Никакого нагрузочного тестирования тут не нужно. Достаточно совсем глупостей не делать и все хорошо будет работать.
Исключение это тяжелая аналитика, но это точно не ваш случай.
aspirin4k Автор
Можете подробнее описать, каких глупостей не надо делать?
swelf
В защиту оратора выше, нет же никаких подробностей, какая ОРМ, что за запросы. Почему в json сложить данные получилось, а дернуть кастомным select с join'ами не получилось?
aspirin4k Автор
Мне кажется, это должно работать в обе стороны на самом деле ) Т.е. подробностей хватает, чтобы назвать меня невменяемым разработчиком, но не хватает, чтобы ответить на мои вопросы? )
Вот подробностей охапка (которая на самом деле не коррелирует с посылом статьи, но я понял, что посыл плохой):
1. ORM — Doctrine. Да, мы проверили все режимы fetch: extra lazy, lazy и eager. Избавиться от лишних запросов они не позволяют
2. Мы пробовали DQL и руками джойнить сущности. Не помогло, т.к. доктрина не использует полученные данные для гидратации и все равно делает дополнительные запросы после этого
3. Мы попробовали прототип на MongoDB с использованием Doctrine ODM — это дало такой же прирост производительности, как и финальное решение (Что кажется логичным). Почему удалось сложить данные в документ? Потому что связи между сущностями образуют дерево (За редкими исключениями, для которых мы оставили связь по внешнему ключу)
4. Почему остановились на JSON столбце? Потому что нужно было перепиливать все аннотации и часть логики (потому что не весь функционал между ORM и ODM одинаковый) + в самом приложении для части сущностей остается ORM + поднимать новую СУБД, опыта сопровождения которой в команде нет. Мы просто запилили надстройку над ORM, которая позволила указать столбец для чтения сериализованной в JSON сущности. С т.з. разработчика ничего не поменялось: он как работал, так и работает с доктриной.
5. Можно было действительно сделать кастомный селект на все 20 таблиц, участвующих в запросе. Но потом эти данные каким-то образом нужно перегнать в объекты. Получить иерархию объектов из строк показалось сложнее, чем из JSON.
6. Ну и я не отрицаю, что были совершены ошибки при выборе технологического стека. И мысль была как раз в том, чтобы поделиться опытом: возможно человек сразу заметит подобный паттерн данных у себя и рассмотрит альтернативы реляционной модели.
BugM
В общем для приложения:
Понимать где возможна нагрузка и какая. На пальцах.
Квоты - много и чтения и записи.
Витрины, карточки и тому подобное. Много чтения. Записи нет.
Админка - небольшая нагрузка.
Аналитика - каждый запрос нагрузка любая, на запросов мало.
Не забыть сделать балк операции по загрузке всего.
И соответсвенно не делать глупостей. Вроде квот на sql или упарывания в оптимизацию админки.
БД:
Посчитать количество запросов в бд. Можно тупо логировать все и прогнать все типовые сценарии. Не забыть про списки, скролы и вообще пообщаться с продактами и тестировщикам. Если количество запросов вызывает удивление чинить. Прямо на этапе разработки фич.
Делать БД сразу так чтобы индексы работали, а не делали вид что работают. На глазок понять когда чтение будет по индексу обычно можно. Нагрузка небольшая даже плюс минус два три раза нас устроит.
БД надо проектировать. И не забыть подумать и поговорить со всеми менеджерами об их идеях развития и использования перед этим.
Код:
Просто не делать глупостей. Квадраты не писать, данные фильтровать и джойнить поближе к базе, возможность кешей предусмотреть (делать не надо скорее всего) и тому подобное.
nekt
Про типового миддла — не соглашусь. Если взять двоих миддлов и отдать им архитектуру интернет магазина, они сделают работающее решение, но присмотр и контроль их фантазий необходим. А то сменят еще систему хранения на документ-ориентированную.
BugM
Присмотр да. Бывают странные идеи. Например, людям захотелось попробовать новенькую технологию.
Но в общем они могут написать сами или почти сами. И это будет работать на 10 рпс.
nekt
чего в большинстве случаев вполне достаточно.