В реалиях современного мира новые сайты в интернете появляются каждый день. Их разработке и разработке веб-приложений посвящена большая часть ИТ индустрии. Часто перед разработчиками ставятся задачи быстрой и бюджетной разработки веб-приложений. Для решениях таких задач хорошо подходит популярный веб-фреймворк Django [1], написанный на Python. К основным достоинствам Django относятся простота его использования, широкий инструментарий, включенный в него, открытость его исходного кода и наличие большого количества документации и статей о нем.
Одним из финальных этапов разработки веб-приложения является его развертывание на сервере. В случае Django-приложений есть несколько важных нюансов их развертки. Большинство веб-серверов не понимают язык Python, поэтому были разработаны два стандарта (интерфейса) WSGI и ASGI.
WSGI – основной стандарт Python, который поддерживает только синхронный код.
ASGI – относительно новый стандарт, который дает доступ к функциям параллельного выполнения кода: WebSocket, Server-Sent Events, HTTP/2 и др. ASGI является наследником WSGI и имеет с ним обратную совместимость.
Для развертывания WSGI-приложения часто используются связки, представленные на рис. 1.
Рис. 1. Популярные схемы развертки WSGI-приложений
Наибольшей популярностью из них пользуется первая схема. Можно много рассуждать о преимуществах и недостатках Nginx и Apache, поэтому сравним данные сборки по промежуточным веб-серверам.
Связка с использованием промежуточного слоя определённого mod_wsgi, который в настоящее время является одним из популярнейших модулей-адаптеров WSGI Python, продолжительное время активно использовалась разработчиками распределённых систем [2]. Основным преимуществом mod_wsgi является простота развёртывания и его использования на практике, но в то же время главным недостатком mod_wsgi является то, что он может не поддерживаться крупными хостинг-провайдерами, и его можно использовать только с конкретным сервером приложений, а именно Apache. Такое обстоятельство может быть критичным для некоторых проектов.
uWSGI – это программная платформа, которая включает в себя как непосредственно сервер приложения, так и другие механизмы, такие как прокси-серверы, плагины запросов и т.д. [3]. Всё это достигается благодаря модульной конструкции системы. uWSGI был написан специально для Python, поэтому его легко установить через pip. Другими достоинствами данного сервера является то, что он обладает относительно низким порогом вхождения и позволяет создать ещё один промежуточный слой программного обеспечения, что делает возможным снять часть функционала непосредственно с сервера (однако из-за этого необходимо проксировать запросы). Другим несомненным преимуществом данной платформы можно считать, относительную простоту его настройки.
Gunicorn [4] – это HTTP-сервер Python WSGI, который очень похож на uWSGI. Gunicorn – это зрелый, полнофункциональный сервер и менеджер процессов. Достоинства и недостатки у него такие же, как у uWSGI, однако Gunicorn лучше справляется с высокими нагрузками [5], легче настраивается и требует меньше ресурсов [6].
По итогу видим, что популярность связки Nginx-Gunicorn вполне оправдана.
Для развертывания ASGI-приложения официальная документация Django советует использовать следующие ASGI сервера: Daphne, Hypercorn, Uvicorn.
Daphne – это сервер протоколов HTTP, HTTP2 и WebSocket для ASGI и ASGI-HTTP, который был разработан специально для Django Channels, поэтому он считается эталонным [7].
Uvicorn – это быстрый и легкий ASGI-сервер, использующий uvloop и httptools [8]. Uvicorn поддерживает HTTP/1.1 и WebSockets. Uvicorn включает рабочий класс Gunicorn, позволяющий запускать приложения ASGI со всеми преимуществами производительности Uvicorn, а также предоставляет вам полнофункциональное управление процессами Gunicorn. Это позволяет увеличить или уменьшить количество рабочих процессов на лету, плавно перезапускать рабочие процессы или выполнять обновление сервера без простоев.
Hypercorn – это веб-сервер ASGI, основанный на библиотеках sans-io hyper, h11, h2 и wsproto и вдохновленный Gunicorn. Hypercorn поддерживает спецификации HTTP/1, HTTP/2, WebSockets (через HTTP/1 и HTTP/2), ASGI/2 и ASGI/3. Hypercorn может использовать типы рабочих asyncio, uvloop или trio [9].
Использование того или иного сервера для ASGI, помимо их характеристик, обуславливается и тем, какой фреймворк (Django Channels, Quart, FastAPI, BlackSheep) будет привносить асинхронность в проект. Так как в этой статье мы говорим о Django, то будем рассматривать преимущественно Django Channels.
Библиотека Django Channels [10] появилась недавно, около 4–5 лет назад. Эта библиотека привнесла в Django асинхронное сетевое программирование. Channels расширяет инструментарий Django и дает возможность работать не только с HTTP протоколом, но и с WebSocket (ws), протоколом чата, IoT-протоколом и другими. Django Channels использует ASGI интерфейс.
При разработке реальных проектов часто бывают ситуации, когда нужно в уже развёрнутый проект включить новые функции. К примеру, в готовое Django-WSGI-приложение необходимо включить Dajngo Channels, которые привносят вместе с собой ASGI. Для подобной ситуации в данной статье предложена конфигурация развертки веб-приложения, которую можно считать эталонной для перехода от WSGI к ASGI в рамках Django-приложений, использующих Django Channels.
Материалы и методы исследования
Рассмотрим случай, когда имеется рабочее развернутое Django-WSGI-приложение, работающее на популярной связке, показанной на рис. 2.
Рис. 2. Текущая архитектура сервера
Рис. 3. Файл конфигурации Nginx и сервис для Gunicorn
Nginx тут используется как обратный прокси-сервер, а также отвечает за выдачу статического контента. Gunicorn является прослойкой между Nginx и Django. HTTP запросы Nginx передает Gunicorn, который передает их уже в Django, который взаимодействует со слоем базы данных. Возможная конфигурация Nginx и Gunicorn представлена на рис. 3.
Если добавить в приложение Django Channels с каким-либо функционалом и попытаться проверить работу системы в данной конфигурации можно увидеть, что не работают лишь функции, которые используют WebSocket. Это происходит, во-первых, потому, что в конфигурации Nginx не указано, куда отправлять ws/ запросы, во-вторых, если бы мы направили их на Gunicorn, то вероятно получили бы ошибку, так как Gunicorn – HTTP-сервер. Функции же, которые не используют асинхронность, то есть которые работали в WSGI приложении, работают так, как ASGI имеет обратную совместимость.
После установки Daphne настраиваем Nginx и Daphne, как показано на рис. 4, 5.
Рис. 4. Файл конфигурации Nginx
Рис. 5. Файл сокет и файл сервис для Daphne
Рис. 6. Гибридная архитектура клиент-серверного приложения со шлюзом-адаптером
Таким образом, получаем видоизменённую гибридную архитектуру, представленную на рис. 6.
В представленной выше архитектуре Nginx не только является обратным прокси-сервером, который отвечает за статический контент, но еще и является балансировщиком нагрузки, отправляя HTTP запросы на Gunicorn, а WS запросы – на Daphne. При такой конфигурации происходит выгодное распределение нагрузки. Так как Daphne способен обрабатывать HTTP запросы, можно впоследствии улучшить архитектуру, сделав Daphne резервным сервером для HTTP.
В качестве второго сервера Daphne был выбран потому, что он разрабатывался специально под Django-Channels и максимально с ним совместим, обладая при этом функциями, которые могут пригодиться при дальнейшей разработке системы и ее масштабировании.
Если в аналогичной ситуации, когда нужно перевести свое приложение с WSGI на ASGI при уже развернутом Nginx+Gunicorn, и это необходимо сделать быстро и не потеряв в производительности, то вместо Daphne можно использовать Uvicorn. В описанной ситуации основным плюсом использования Uvicorn является то, что его настройка очень похожа на настройку Gunicorn. Хотя Uvicorn больше подходит для использования с FastAPI и имеет меньший функционал.
Модель обработки запросов в многоуровневых клиент-серверных системах
Рассмотрим процесс обработки разнородных запросов для разработанной системы. В рамках распределённой системы выделим следующие сущности:
1. Пользователь – это актор, который хочет получить данные либо доступ к функциональности удалённого объекта.
2. Клиент-потребитель – это актор, через который пользователь осуществляет авторизированный доступ к удалённому поставщику сервиса.
3. Поставщик сервиса – это актор, который запущен в рамках отдельного процесса, нежели клиент-потребитель, предоставляющий механизмы доступа к защищённым информационным ресурсам.
4. Защищённый ресурс – данные или бизнес-функционал на удалённом сервисе, которые представляют ценность для конечного пользователя системы.
Распределённая программная система, с учётом вышеобозначенных сущностей, будет определена как
, (1)
где – множество пользователей распределённой программной системы;
– множество гетерогенных акторов клиентов – потребителей распределённой программной системы;
θ – множество всех запросов в системе;
– множество поставщиков сервиса;
– множество представлений ресурса r.
Вместе с тем соотношение определяет упорядоченные пары акторов авторизованных пользователей и аутентифицированных клиентов системы в определённый момент времени. Множество всех запросов для рассматриваемой выше системы может быть определено через представление Куратовского как
, (2)
где – запросы типа WebSocket, сгенерированные парой ;
– запросы типа http, сгенерированные парой .
Исходя из (2), общее количество запросов в системе в момент времени t определяется как
, (3)
где – i-й запрос актора типа WebSocket;
– i-й запрос актора типа HTTP.
Введём понятие пороговый барьер Ψκ, который будет определяться как программный слой, в рамках многослойной архитектуры, осуществляющий процесс маршрутизации поступающих запросов в систему. В современных системах пороговый барьер может быть определён через архитектурный паттерн Gateway [11]. Пороговый барьер определяет область σbarrier. Область, определяемую программными слоями перед пороговым барьером, будем называть предпороговой областью σfront барьера Ψκ. Область, определяемую промежуточным программным обеспечением, находящимся за барьером Ψκ, будем называть областью σback за пороговым барьером. Такое разделение оправдано тем, что в каждой из этих областей слой программного обеспечения, реализованный посредством использования современных фреймворков, может кешировать запросы, предобрабатывать, формировать цепочки обработки, обрабатывать как синхронно, так и асинхронно, а в некоторых случаях даже отклонять соответствующие типы запросов от их дальнейшей обработки [12]. Процесс обработки запросов всех типов в системе будет определяться как
, (4)
где S – конечное множество объектов ответа защищенного ресурса r из всего множества представлений Ri.
Отношение между множествами определено через биективное соотношение, что означает, что между множествами определено однозначное соотношение и, следовательно, .
Далее введём критерий качества К обработки запросов в системе. В рассматриваемой модели разработанной системы запросы, инициатором которых является конечный авторизованный пользователь, обрабатываются удалённым сервисом, следовательно, в самом лучшем случае каждый запрос должен быть однозначно преобразован в объект ответа. Исходя из того, что в каждой из ранее определённых областей происходит воздействие на соответствующий запрос, что приводит к трансформации запроса, что в конечном итоге может привести к снижению точности объектов конечного ответа удалённого сервиса. Соответственно, под точностью в данной работе будем понимать долю объектов запроса, соответствующих определённому классу запроса, которые система однозначно относит к данному классу. В силу того, что в разработанной системе могут существовать два типа запросов и , точность идентификации объектов будет определена формулой
, (5)
где + – однозначно определённые запросы типа предпороговой, пороговой и запороговой барьерных областей,
– однозначно определённые запросы типа предпороговой, пороговой и запороговой барьерных областей,
Q – общее число запросов в системе.
Следовательно, исходя из формулы (5), математическая интерпретация задачи идентификации запросов в системе может быть определена как соотношение
(6)
где – параметр оптимизации, определённый как общее количество однозначно определённых запросов, j-й пары .
Каждый запрос, в самом лучшем случае, формирует один ответ, определённый как представление ресурса . При этом пользователь системы заинтересован в представлении всех требуемых ему данных, для поддержания своих бизнес-процессов за один запрос. Однако в силу архитектурных особенностей современных распределённых систем получить требуемую порцию данных за один запрос бывает очень затруднительно. Для решения этой задачи, в зависимости от назначения и архитектурных особенностей системы могут использоваться такие механизмы, как оркестрация и хореография в рамках микросервисной архитектуры, графовые модели программных интерфейсов, например GraphQL и т.д. Исходя из этого, определим новую характеристику как полнота ответа поставщика сервиса. Данную характеристику можно определить как
(7)
где Si – общее число запросов для извлечения i-го ресурса.
Введём понятие индекс отзывчивости распределённой системы, под которым будем понимать степень предоставления ресурсов (данных) поставщиком сервиса. Индекс отзывчивости системы может быть определён как
, (8)
где – частота удовлетворения потребности пользователя в ресурсе r за один запрос из пары ,
– частота удовлетворения потребности пользователя в ресурсе r за несколько запросов из пары .
Соответственно, является нашей основной целей, что можно интерпретировать как свойство системы полностью удовлетворять потребность в ресурсах (данных) пользователя за один запрос из пары . Максимизация этого параметра позволяет снизить лишние накладные расходы на обслуживание дополнительных запросов к поставщику сервиса.
Исходя из вышеизложенного, критерий качества регулирования разработанной системы можно определить как
, (9)
где – установленное значение индекса отзывчивости системы,
– действительное значение индекса отзывчивости системы.
Для процесса управления разработанной системы важно определить сбалансированность между двумя отслеживаемыми параметрами, введёнными нами ранее, а именно, между точностью идентификации объектов и индексом отзывчивости. Для решения данной задачи можно использовать метод среднего гармонического взвешенного [13, 14], который определяется следующим образом. Пусть имеется массив вещественных чисел , определённый значениями точности идентификации объектов, и массив вещественных чисел , сформированный из значений отзывчивости системы, тогда среднее гармоническое взвешенное может быть рассчитано как
(10)
Подход, основанный на методе среднего гармонического взвешенного, обладает явным преимуществом, в отличие от, например, невзвешенных средних, в связи с тем, что он учитывает дисбаланс между классами. В рамках нашей задачи явно сформированы два класса значений, определяемых величинами такими, как точность идентификации объектов и индекс отзывчивости, которые при первом взгляде на эти величины могут показаться взаимосвязанными, а в некоторых случаях и взаимодополняющими друг друга. Однако взаимосвязь между этими значениями довольно-таки сложна. В зависимости от наших целей мы можем быть заинтересованы в различных соотношениях между точностью идентификации объектов и индексом отзывчивости, то есть в распределённой переменной. Классическая оценка на основе метода среднего гармонического взвешенного может не давать качественные оценки, например, в случае, если значение класса отзывчивости системы равно 0, то данный метод просто не учтёт значение другого класса и итоговое значение будет равно 0. Помимо этого, существенным недостатком классического метода, является то обстоятельство, что отсутствует какая-либо явная возможность управлять балансом между точностью идентификации объектов и индексом отзывчивости, то есть отсутствует возможность определить, насколько для нас важна точность идентификации объектов, нежели чем индекс отзывчивости. На практике для решения данной задачи либо используются другие методы и оценки, либо разработаны трансформированные методы расчёта среднего гармонического взвешенного. Исходя из вышеизложенного, в данной работе введём коэффициент баланса γ, следовательно, модернизированная оценка взвешенного гармонического среднего будет определена как
, (11)
где γi – коэффициент баланса i-го класса (можно отождествить с вероятностью выбора соответствующего класса),
Hi – значение среднего гармонического взвешенного соответствующего класса,
n – общее количество классов.
Так как в рамках нашей задачи выделены 2 класса (n = 2), то модернизированная оценка взвешенного гармонического среднего для данного случая будет определена как
(12)
Такой подход позволяет посредством установки значения параметров γ1 и γ2 балансировать между точностью идентификации объектов и индексом отзывчивости. Так, например, установка говорит о сбалансированном весе между точностью идентификации объектов и индексом отзывчивости системы.
Заключение
В данной работе предложен способ создания программных шлюзов для интеграции гетерогенных серверов в рамках клиент-серверного взаимодействия, который основан на использовании распространённых программных компонентов, что упрощает процесс внедрения предложенной системы в существующие проекты, которые написаны с использованием языка программирования Python и фреймворка Django. Предложенный способ даёт возможность интеграции серверов в рамках программного шлюза, основная задача которого заключается в ретрансляции запросов клиентского приложения во внешние удалённые серверы, которые могут быть связаны с программным шлюзом посредством WebSocket и HTTP. Это стало возможным благодаря внедрению и использованию django channels. Такой подход позволяет в существующие Django проекты внедрять как WebSocket клиентов, так и HTTP клиентов, а также даёт возможность обработки как синхронных, так и асинхронных функций, тем самым расширив возможности разработчиков клиентских приложений. Помимо этого, используемые программное обеспечение и библиотеки для формирования данного программного шлюза, а именно стек Ngnix+Gunicorn+Daphne, позволяют осуществить балансировку HTTP запросов клиентов, что, в свою очередь, позволит повысить скорость и стабильность всего клиент-серверного приложения. При таком конфигурировании системы Daphne будет являться резервным сервером/каналом, который может принимать проксированные Ngnix в случае неисправности Gunicorn или миграции со шлюзового протокола WSGI на ASGI. Предложенная математическая модель распределённой системы учитывает характер поступающих запросов, с возможностью тонкой настройки выделенных в данной работе параметров, что можно использовать при решении задачи управления вычислительной нагрузки [13] в рамках клиент-серверного приложения с многослойной архитектурой.