Всемирную паутину, какой я ее себе представлял, мы еще не видели. Будущее все еще намного больше, чем прошлое.
Тим Бернерс-Ли
Когда-то Всемирная паутина была маленькой и простой. Разработчикам было так весело отправлять вызовы PHP, HTML и MySQL в отдельные файлы и с гордостью говорить всем, что они могут заглянуть на свой веб-сайт. Но со временем Сеть разрослась до невообразимого количества страниц и развивающаяся игровая площадка превратилась в метавселенную тематических парков.
В этой главе отмечены некоторые все более актуальные для современной Всемирной паутины области:
• сервисы и API;
• конкурентность;
• уровни (слои);
• данные.
В следующей главе я расскажу о том, какие возможности предоставляет Python для работы в этих областях. После этого погрузимся в веб-фреймворк FastAPI и посмотрим, что он может предложить.
Паутина — это отличная соединительная ткань. Несмотря на то что большая часть деятельности по-прежнему происходит на стороне контента — HTML, JavaScript, изображений и т.д., все большее внимание уделяется интерфейсам прикладного программирования (API), соединяющим различные элементы программ.
Обычно веб-сервис управляет низкоуровневым доступом к базе данных и бизнес-логикой среднего уровня (часто их объединяют в бэкенд), а JavaScript или мобильные приложения обеспечивают богатый фронтенд верхнего уровня (интерактивный пользовательский интерфейс). Эти миры становятся все более сложными и разнообразными, что обычно требует от разработчиков специализации в том или ином направлении.
Быть разработчиком полного стека (фулстека) сейчас сложнее, чем раньше.
Эти два мира общаются друг с другом с помощью API. В современном Интернете дизайн API так же важен, как и дизайн самих сайтов. API — это контракт, подобный схеме базы данных. Определение и модификация API — это уже серьезная работа.
Каждый API определяет следующее:
• протокол — структуру управления;
• формат — структуру содержимого.
Многочисленные методы API развивались по мере эволюции технологий от изолированных машин до многозадачных систем и сетевых серверов. Вероятно, в какой-то момент вы столкнетесь с одним или несколькими из них, поэтому далее приведу краткое описание, прежде чем перейти к языку HTTP и его друзьям, описанным в этой книге.
• До появления сетей API обычно означал очень тесную связь, например вызов функции из библиотеки на том же языке, что и ваше приложение, — скажем, вычисление квадратного корня в математической библиотеке.
• Удаленные вызовы процедур (remote procedure call, RPC) были придуманы для вызова функций в других процессах на той же или другой машине, как если бы они находились в вызывающем приложении. Популярным примером в настоящее время служит система gRPC (https://grpc.io).
• С помощью системы передачи сообщений отправляются небольшие фрагменты данных по конвейеру между процессами. Сообщения могут быть глагольными командами или просто обозначать интересующие вас события, похожие на существительные. В настоящее время популярными решениями для обмена сообщениями, которые варьируются от наборов инструментов до полноценных серверов, являются брокеры Apache Kafka (https://kafka.apache.org), RabbitMQ (https://www.rabbitmq.com), NATS (https://nats.io) и ZeroMQ (https://zeromq.org). Взаимодействие может строиться по разным схемам:
• запрос — ответ. Точно так, как браузер вызывает веб-сервер;
• издатель — подписчик, или pub-sub. Издатель (publisher, или pub) рассылает сообщения, а подписчики (subscribers, или sub) обрабатывают каждое из них в соответствии с некоторыми данными, содержащимися в сообщении, например с темой;
• очереди. Работают как подход pub-sub, но только один подписчик из пула получает сообщение и действует в соответствии с ним.
Любой из этих подходов может использоваться вместе с веб-сервисом, например, для выполнения медленной задачи бэкенда, такой как отправка электронной почты или создание уменьшенного изображения.
Бернерс-Ли предложил для своей Всемирной паутины три компонента:
• HTML — язык для отображения данных;
• HTTP — протокол «клиент — сервер»;
• URL — схему адресации для веб-ресурсов.
Хотя в ретроспективе все кажется очевидным, на деле это оказалось до смешного полезной комбинацией. По мере развития Интернета люди экспериментировали, и некоторые идеи, такие как тег IMG, выжили в борьбе по Дарвину. По мере того как потребности пользователей прояснялись, люди всерьез занялись определением стандартов.
В одной из глав докторской диссертации (https://oreil.ly/TwGmX) Роя Филдинга есть определение передачи репрезентативного состояния (Representational State Transfer, REST) — архитектурного стиля для использования HTTP. Несмотря на то что на эту работу часто ссылаются, ее по большей части неправильно понимают (https://oreil.ly/bsSry).
В современной Паутине развилась и доминирует примерно одинаковая адаптация. Она называется RESTful и обладает следующими характеристиками:
• использует HTTP и протокол «клиент — сервер»;
• не имеет состояния (каждое соединение независимо);
• кэшируема;
• основана на ресурсах.
Ресурс — это данные, которые можно определять и с которыми можно выполнять операции. Веб-сервис предоставляет конечную точку — отдельный URL и HTTP-глагол (действие) — для каждой функции. Конечную точку называют также маршрутом, поскольку она направляет URL к функции.
Пользователи баз данных знакомы с акронимом CRUD для процедур: создание (create), чтение (read), модификация (update), удаление (delete). HTTP-глаголы довольно хорошо вписываются в понятие CRUD:
• POST — создание (запись);
• PUT — полная модификация (замена);
• PATCH — частичная модификация (обновление);
• GET — получение (считывание, извлечение);
• DELETE — удаление.
Клиент отправляет запрос на конечную точку RESTful с данными в одной из таких областей HTTP-сообщения, как:
• заголовки;
• строка URL;
• параметры запроса;
• значения в теле сообщения.
В свою очередь, HTTP-ответ возвращает:
• целочисленное значение кода состояния (https://oreil.ly/oBena), определяющее такие состояния, как:
• группа кодов 100 — информация, продолжение выполнения;
• группа кодов 200 — успешное выполнение;
• группа кодов 300 — перенаправление;
• группа кодов 400 — ошибка на стороне клиента;
• группа кодов 500 — ошибка на стороне сервера;
• различные заголовки;
• тело сообщения, которое может быть пустым, единым или разделенным на части (последовательные фрагменты).
По крайней мере один код состояния можно считать пасхалкой — 418 (I’m a teapot, https://www.google.com/teapot). На странице должен появиться подключенный к сети чайник. Если попросить, он нальет вам чашечку чая.
В широком доступе существует множество сайтов и книг о проектировании RESTful API, и все они содержат полезные практические указания. Эта книга станет вашим помощником в пути.
Фронтенд-приложения могут обмениваться обычным текстом на основе стандарта ASCII с веб-сервисами бэкенда, но как выразить структуры данных, такие как списки элементов?
Как раз в момент острой нужды появился формат «обозначения объектов JavaScript» (JavaScript Object Notation, JSON) — еще одна простая идея, решающая важную проблему и кажущаяся очевидной в ретроспективе. Хотя J означает JavaScript, синтаксис очень похож на Python.
JSON в значительной степени заменил такие более ранние попытки реализации этой идеи, как XML и SOAP. В оставшейся части этой книги вы увидите, что JSON — это формат для ввода и вывода у веб-сервисов по умолчанию.
Сочетание RESTful-дизайна и форматов данных JSON уже стало привычным. Но некоторые возможности для двусмысленности и занудства все же остаются. Недавнее предложение JSON:API (https://jsonapi.org) направлено на то, чтобы немного ужесточить спецификации. В этой книге используется свободный подход RESTful, но JSON:API или что-то подобное может оказаться полезным, если у вас возникнут серьезные затруднения.
Для некоторых целей RESTful-интерфейсы могут быть громоздкими. Facebook (сейчас Meta) разработала язык под названием Graph Query Language (GraphQL) (https://graphql.org). Он позволяет определить более гибкие запросы. В этой книге GraphQL не рассматривается, но, возможно, стоит обратить на него внимание, если вы считаете, что RESTful-дизайн не подходит для вашего приложения.
Наряду с ростом ориентированности на сервисы стремительное увеличение количества подключений к веб-сервисам требует все большей эффективности и масштабирования. Нужно снизить следующие показатели:
• время ожидания — предварительное время ожидания;
• пропускную способность — количество байтов в секунду между сервисом и его абонентами.
В давние времена, работая во Всемирной паутине, люди мечтали о поддержке сотен одновременных подключений, затем беспокоились о «проблеме 10 000», а теперь обсуждают миллионные значения.
Термин «конкурентность» не означает полный параллелизм. Множественная обработка не происходит в одну и ту же наносекунду в одном процессоре. Конкурентность в основном позволяет избежать напряженного ожидания (простаивания центрального процессора (ЦП) до получения ответа). ЦП работают быстро, а сети и диски — в тысячи и миллионы раз медленнее. Поэтому при обращении к сети или диску никто не хочет просто сидеть с пустым взглядом, пока не поступит ответ.
Обычное выполнение Python синхронизировано — выполняется одно действие за раз в порядке, указанном кодом. Иногда требуется работа в асинхронном режиме — выполнить немного одного, потом немного другого, вернуться к первому и т.д. Если весь код использует центральный процессор для вычислений (CPU bound), то у нас нет свободного времени на асинхронность. Но если выполняется процесс, указывающий процессору ждать завершения операции от внешнего источника (I/O bound), можно организовать асинхронность.
Асинхронные системы обеспечивают цикл событий: запросы на медленные операции отправляются и отмечаются, но в ожидании их ответов не происходит задержка работы ЦП. Вместо этого при каждом проходе через цикл выполняется немедленная обработка, а все ответы, поступившие за это время, обрабатываются при следующем проходе.
Эффект от этих действий может быть драматическим. Далее в книге вы увидите, как поддержка асинхронной обработки в FastAPI делает его намного быстрее, чем типичные веб-фреймворки. Асинхронная обработка — это не волшебство. Вам все еще нужно проявлять осторожность, чтобы не выполнять слишком много работы, требующей больших затрат ресурсов ЦП, во время цикла событий, потому что это замедлит весь процесс выполнения. Далее в книге вы увидите, как используются ключевые слова async и await языка Python и как FastAPI позволяет сочетать синхронную и асинхронную обработку.
Поклонники Шрека, возможно, помнят, как он отметил слои своей личности, на что Осел уточнил: «Как луковица?»
Ну, если у людоедов и вызывающих слезотечение овощей есть слои, то и у программного обеспечения тоже. Чтобы управлять размером и сложностью, многие приложения уже давно используют так называемую трехуровневую модель. Это не так уж и ново. Термины могут быть различными, но в этой книге я подразумеваю следующее простое разделение понятий (рис. 1.1):
• веб-уровень — уровень ввода/вывода поверх HTTP. Он собирает клиентские запросы, вызывает сервисный уровень и возвращает ответы;
• сервис — бизнес-логика, при необходимости выполняющая обращения к уровню данных;
• данные — доступ к хранилищам данных и другим сервисам;
• модель — определения данных, общие для всех уровней;
• веб-клиент — веб-браузер или другое программное обеспечение на стороне клиента HTTP;
• база данных — хранилище данных, часто SQL- или NoSQL-сервер.
Эти компоненты помогут вам масштабировать сайт, не начиная с нуля. Их нельзя сравнивать с законами квантовой механики, поэтому считайте их руководством к изложению материала в этой книге.
Рис. 1.1. Вертикальные уровни
Уровни взаимодействуют друг с другом через API. Это могут быть простые вызовы функций к отдельным модулям Python, но можно обращаться к внешнему коду через любой метод. Как я показывал ранее, это могут быть RPC, сообщения и т.д. В этой книге я предполагаю наличие одного веб-сервера с кодом Python, импортирующим другие модули Python. Разделение и сокрытие информации выполняется модулями.
Пользователи видят веб-уровень, задействуя клиентские приложения и API. Обычно мы говорим о RESTful-веб-интерфейсе с URL-адресами, запросами и закодированными в формате JSON ответами. Но наряду с веб-слоем могут быть созданы альтернативные текстовые клиенты или интерфейс командной строки (Command-Line Interface, CLI). Веб-код Python может импортировать модули сервисного уровня, но не должен импортировать модули уровня данных.
Сервисный уровень содержит фактические данные о том, что предоставляет этот веб-сайт. По сути, этот уровень похож на библиотеку. Он импортирует модули уровня данных для доступа к базам данных и внешним сервисам, но не должен получать от них детальную информацию.
Уровень данных предоставляет уровню сервисов доступ к данным через файлы или клиентские вызовы других сервисов. Могут существовать и альтернативные уровни данных, взаимодействующие с одним сервисным уровнем.
Блочная модель (model box) — это не настоящий уровень, а источник определений данных, общих для всех уровней. Он не требуется, если вы передаете между уровнями встроенные структуры данных Python. Позже вы увидите, что включение библиотеки Pydantic в FastAPI позволяет определять структуры данных с множеством полезных функциональных возможностей.
Зачем проводить такие разделения? По многим причинам каждый уровень может быть:
• написан специалистами;
• изолированно протестирован;
• заменен или дополнен — вы можете добавить второй веб-уровень, использующий другой API, например gRPC, наряду с веб-уровнем.
Следуйте одному правилу из фильма «Охотники за привидениями» — не скрещивайте лучи. То есть не позволяйте частям веб-сайтов просачиваться за пределы веб-уровня, а деталям баз данных — за пределы уровня данных.
Вы можете представить себе уровни в виде вертикальной стопки, как торт в телепередаче «Лучший пекарь Британии».
Вот несколько причин для разделения уровней.
• Если вы не разделите уровни, то ожидайте, что станете широко известным веб-мемом: «Теперь у вас две проблемы».
• Разделить уровни, если они смешаются, будет очень сложно.
• Вам потребуется знание двух или более специальностей, чтобы понять и написать тесты, если логика кода запуталась.
Кстати, хотя я и называю их уровнями, не нужно считать, что один из них находится выше или ниже другого и что команды перемещаются с помощью гравитации. Это было бы проявлением вертикального шовинизма! Можете рассматривать уровни как блоки, стоящие бок о бок друг с другом (рис. 1.2).
Как бы вы их ни представляли, единственными путями связи между блоками/уровнями могут служить стрелки (API). Это важно для тестирования и отладки. Если на фабрике есть неизвестные двери, ночной сторож неизбежно будет удивлен.
Рис. 1.2. Блоки, стоящие рядом
Стрелки между веб-клиентом и веб-уровнем задействуют протокол HTTP или HTTPS для передачи текста, преимущественно в формате JSON. Стрелки между уровнями данных и баз данных используют особый протокол для баз данных и передают текст в формате SQL или в другом. Стрелки между уровнями — это вызовы функций, переносящие модели данных.
Кроме того, рекомендуемые форматы данных, проходящих через стрелки, таковы:
• клиент ⇔ веб-уровень — RESTful HTTP с помощью JSON;
• веб-уровень ⇔ сервис — модели;
• сервис ⇔ данные — модели;
• данные ⇔ базы данных и сервис — специализированные API.
Основываясь на собственном опыте, я выбрал именно такую структуру тем для этой книги. Она вполне работоспособная и подходит для довольно сложных сайтов, но не единственно верная. Вы можете создать лучший дизайн! Как бы вы ни поступили, обратите внимание на основные ключевые точки.
• Отделите свойственные домену детали.
• Определите стандартные API между уровнями.
• Не обманывайте, не допускайте утечек.
Иногда решить, какой уровень лучше всего подходит для кода, бывает непросто. Например, в главе 11 рассматриваются требования к аутентификации и авторизации и способы их реализации — в качестве дополнительного уровня между веб- и сервисным уровнем или внутри одного из них. Разработка программного обеспечения — это порой не только искусство, но и наука.
Веб-уровень часто использовался как фронтенд для реляционных баз данных, хотя в настоящее время появилось множество других способов хранения данных и доступа к ним, например базы данных типа NoSQL или NewSQL.
Помимо баз данных, кардинально меняет технологический ландшафт машинное обучение (Machine Learning, ML), или глубокое обучение, или просто искусственный интеллект (ИИ). Разработка больших моделей требует значительной работы с данными, традиционно называемой извлечением, преобразованием, загрузкой (Extract, Transform, Load, ETL).
Будучи универсальной сервисной архитектурой, веб может помочь в решении многих сложных задач, связанных с системами ML.
Во Всемирной паутине используется много API, но особенно много взаимодействий на основе RESTful. Асинхронные вызовы обеспечивают лучшую конкурентность, что ускоряет общий процесс выполнения. Приложения веб-сервисов часто бывают достаточно большими для того, чтобы разделить их на уровни. Данные стали самостоятельной важной областью. Все эти понятия рассматриваются в языке программирования Python. О нем и пойдет речь в следующей главе.
Я бросил попытки несколько лет назад.
Под стилем понимается шаблон более высокого уровня, например «клиент — сервер», а не конкретная конструкция.
Деятельность запрещена в РФ.
Примерно тогда, когда пещерные люди играли в футбэг с гигантскими наземными ленивцами.
Выберите свой вариант: уровень/слой, помидор/томат.
Часто можно встретить термин «модель — представление — контроллер» (Model — View — Controller, MVC) и похожие варианты. Обычно это сопровождается религиозными войнами, но здесь я агностик.
Как известно, если слои вашего торта сложены небрежно, вы можете не вернуться в шатер на следующей неделе.