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

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

В моно или в микро

В монолитной программной архитектуре все сервисы и компоненты приложения объединены в единую гигантскую кодовую базу и часто тесно связаны (сильно зависят друг от друга). Такой дизайн часто возникает на ранних стадиях разработки или роста программной системы и обычно считается плохим, потому что:

  • Изменение любой части приложения включает в себя перестройку, тестирование и развертывание всей кодовой базы, что может привести к менее частому графику развертывания.
  • Все модули/сервисы должны использовать один и тот же язык
  • Меньше гибкости — сложнее внедрять новые языки/технологии/фреймворки для новых модулей/сервисов.
  • Больший радиус распространения дефекта — одна проблема имеет больший потенциал для воздействия на всю систему.
  • Многим разработчикам может стать сложно работать над одной большой кодовой базой/репозиторием.
  • Может быть сложно масштабировать разные модули/сервисы независимо друг от друга, поскольку они связаны.

Вездесущее противоядие от кажущейся болезни монолитности — известная микросервисная архитектура, которая направлена ​​на устранение перечисленных выше недостатков. Это включает в себя разбиение приложения на несколько небольших независимых служб в зависимости от сферы деятельности, функциональности или владения ресурсами. Эти сервисы слабо связаны, то есть они взаимодействуют через стандартизированные внешние интерфейсы, не зависящие от языка (такие как REST API), а не через прямой импорт кода или вызовы функций. Подробнее о паттерне архитектуры микросервисов можно узнать здесь.

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

  • Общий общий код (зависимости библиотек, базовые классы (модели, представления, формы…), базовые шаблоны, служебные или вспомогательные функции и т. д.)
  • CSS и другие статические файлы
  • Инфраструктура тестирования и развертывания
  • Регистрация и оповещение
  • Управление конфигурацией
  • Контроль версий и управление изменениями
  • Инфраструктура базы данных и управление ею (миграция, резервное копирование, высокая доступность и т. д.)
  • Аутентификация и безопасность
  • API для доступа к данным или функциям других сервисов.

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

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

Примите монолит

Это сообщение в блоге исследует некоторые заблуждения относительно предполагаемых преимуществ микросервисов и того, сколько еще можно достичь с помощью монолитного дизайна. Если все сделано правильно, у монолитного подхода есть много преимуществ; даже Google использует массивное моно-репозиторий для всех своих продуктов (прочитайте статью по веским причинам), а Instagram поддерживается монолитом Django. Преимущества включают в себя:

  • Меньшая сложность (меньше независимых компонентов и движущихся частей)
  • Меньше накладных расходов на взаимодействие/аутентификацию между различными сервисами/модулями
  • Общее совместное использование кода и повторное использование между сервисами/модулями
  • Единый источник достоверной информации с единым управлением версиями и согласованием по всему проекту
  • Видимость кода и четкая структура проекта без границ репозитория между модулями
  • Простота развертывания, управления и обслуживания
  • Легче и быстрее разрабатывать
  • Более простое сквозное тестирование

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

Нет хороших общих советов по архитектуре программного обеспечения, потому что не существует общих архитектур программного обеспеченияНил Форд

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

«Монолитная кодовая база никоим образом не подразумевает монолитный дизайн программного обеспечения»

Пути решения проблем развития

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

Используйте модульную структуру проекта

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

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

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

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

python manage.py test module_name

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

В общем, модули должны быть нацелены на инкапсуляцию своей бизнес-логики и функциональности, а прямой доступ к моделям и другому коду между модулями должен быть сведен к минимуму. Любые жесткие зависимости должны быть однонаправленными, так что если модуль B зависит от модуля A, то модуль A не должен знать о существовании модуля B.

Отдельный фронтенд и бэкенд

Популярный выбор дизайна для проектов Django — иметь внутреннюю функцию только как конечную точку API (с помощью Django Rest Framework), а внешний интерфейс — это полностью отдельное одностраничное приложение (SPA), использующее JavaScript. такие как Angular, React или Vue.js.

Это имеет несколько преимуществ:

  • Фронтенд-разработчикам не нужно иметь какие-либо знания или опыт работы с Python или Django.
  • Front-end разработчики могут работать с отдельным репозиторием/кодовой базой, что снижает трудности при совместной работе.
  • Внешнее приложение можно обновлять и развертывать независимо от внутреннего — компоненты, ориентированные на пользователя, часто требуют более частых обновлений и итераций.
  • Отделяет интерфейс и отображение от внутренней бизнес-логики
  • Позволяет другим внешним интерфейсам/клиентам, таким как мобильное приложение, также использовать одни и те же внутренние API.
  • Снижает нагрузку на веб-сервер, поскольку ему больше не нужно отображать шаблоны и возвращать меньше данных ответа
  • Более отзывчивый и динамичный пользовательский интерфейс

Способы решения проблем с производительностью

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

Добавить кеш

Если производительность приложения является проблемой, нагрузку и перегрузку как веб-сервера, так и базы данных (два распространенных узких места в производительности) можно значительно уменьшить, внедрив соответствующее кэширование. Django имеет встроенную структуру кэширования, которая поддерживает кэширование для каждого сайта, для каждого просмотра или более детальное кэширование. Существуют также сторонние проекты, обеспечивающие кэширование на уровне базы данных, такие как django-cachalot, django-cache-machine и django-cacheops.

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

«В компьютерных науках есть только две сложные вещи: аннулирование кеша и присвоение имен», — Фил Карлтон

Оптимизация доступа к базе данных

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

  • Используйте .select_related() или .prefetch_related() в наборе запросов, у которого есть связанные модели, к которым вы хотите получить доступ. Это позволит собирать данные для всех связанных моделей в одном запросе вместо одного запроса на связанную модель.
  • Используйте метод набора запросов .update() для выполнения массового обновления SQL вместо выбора моделей, изменения значений полей и последующего сохранения каждого из них по отдельности (это также относится к обновлению значения поля в одной модели).
  • Предположим, вам нужно выполнить много проверок того, существует ли запись модели на основе некоторого значения поля, или у вас есть модель, которая эффективно работает как справочная таблица, преобразующая одно значение в другое. Вместо того, чтобы делать отдельный запрос для каждого значения, которое вы хотите проверить или найти, выберите все данные в одном запросе, используя .values_list(), чтобы выбрать только те поля, которые вам нужны. Затем используйте эти данные для создания set (для проверки существования) или dict (для поиска значений) в памяти и используйте их вместо этого.
  • Избегайте использования функций агрегирования, таких как суммирование или подсчет, в больших, часто используемых таблицах. Это может привести к блокировке всей таблицы во время расчета сводной метрики, что задержит другие запросы от доступа к ней, а в некоторых случаях даже приведет к взаимоблокировкам. В качестве альтернативы можно сохранить значение суммы или числа в другой таблице, которая обновляется всякий раз, когда добавляется новая запись.

Используйте несколько баз данных

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

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

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

Добавить больше веб-серверов

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

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

Заключение

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

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