Я неправильно работаю с HTTP

в 10:52, , рубрики: api, python, Веб-разработка

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

Главная проблема в том, что я никогда не добавлял достаточно абстракций в свои HTTP-библиотеки.
Объекты запроса и ответа (request и response) практически не скрывают деталей HTTP. Я имею в виду, что вы не можете просто сериализовать эти объекты и ожидать что все будут работать. Сериализация возможно работает для response объектов в werkzeug и других библиотеках и фреймворках. Если вы хорошо знаете внутренности своей библиотеки, то вы возможно сможете сериализовать и Request объект.
Но главное, что это не было предусмотрено при проектировании этих фреймворков — все они тонкие обертки над внешними ресурсами — над TCP соединением с браузером.

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

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

Что если отказаться от стриминга? Я знаю, вы скажете, — «ты сошел с ума, Армин?», но это о чем я думаю. Как много запросов в вашем приложении действительно требуют стриминга? А как много ответов? Конечно мы не можем просто взять и полностью отказаться от стриминга, и это не то к чему я призываю. Но если бы начал все с начала, то создал бы отдельный API для стриминга, который надо было бы подключать отдельно.

Уровнем выше над HTTP

В коде, над которым мы работает в Fireteam, мы немного исследовали этот подход в связи с некоторыми соображениями и требованиями к проектам. Наше API доступно не только по HTTP, но и по нескольким другим протоколам для поддержки консольных игр. В нашем коде еще встречаются детали реализации связанные с HTTP, но в конечном счете мы полностью от него независимы.

Внутри мы просто пересылаем данные и это великолепно. Если запрос приходит через HTTP, мы смотрим путь и выбираем обработчик. Обработчик не просто функция, у него есть метаданные. Они описывают семантику обработчика (обновление или удаление данных etc), какие данные он принимает.

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

Для чего это нужно? Мы по большей части не зависим от внутренностей HTTP, что делает процесс невероятно удобным. У нас одна точка, где проверяется формат входящих данных, и клиент не сможет сломать сервис послав слишком большой объем данных и это все происходит автоматически. А еще это означает что мы можем свободно пересылать эти данные между процессами и даже между разными языками программирования.

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

Это работает?

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

Насколько хорошо такой подход будет работать в python фреймворке общего назначения? Я думаю хорошо.
Представьте, что мы всегда принимаем данные в буфер полностью. Приходит запрос, и маршрутизация происходит только по заголовкам HTTP-запроса (HTTP method, path, hostname и тд.), и затем мы понимаем какую функцию нужно вызвать для обработки запроса.

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

Реализации объектов запроса и ответа должны быть написаны таким образом, чтобы обеспечить возможность легко передавать их, сериализовать и десериализовать, и тд. Нужно обеспечить возможность игнорировать, что у нас открыт сокет, и передавать данные в удобном формате (если сильно хочется, то в HTTP).

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

Автор: Deepwalker


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js