Стартап-продукт Datafold — платформа для мониторинга аналитических данных.
Они подключаются к хранилищам данных, а также к системам ETL и BI, помогая ученым и инженерам, работающим с данными, отслеживать потоки данных, качество данных и аномалии.
И однажды стартап решил сменить стек.
Как это произошло? Это затраты и все трудности переезда, которые возникают. Но если вам нужен короткий, эргономичный и легко читаемый код, вам придется пойти на жертвы.
Но давайте обо всём по порядку.
Алексей Морозов, сооснователь и технический директор в компании Datafold , рассказал, как перешли с Flask на FastAPI и поделился собственным опытом такого перехода.
Это не просто то, что написано в документации, а конкретные проблемы, с которыми они столкнулись при переезде и способы их преодоления.
Давайте рассмотрим примеры того, что следует делать и каких решений следует избегать.
Полтора года назад стартап Datafold квалифицировался в Ycombinator, ведущий акселератор для небольших компаний.
Это означало, что у них было 4 месяца, чтобы показать результаты, и 1-2 месяца, чтобы написать MVP.
У основателей не было глубокого опыта в веб-разработке — до этого они занимались Data Engineering и системным программированием.
Поэтому решили начать с готового проекта и построить на его основе необходимый функционал.
Рассмотрев несколько вариантов, мы остановились на Redash — он оказался очень близок по тематике.
Имеет те же аналитические базы, возможности подключения и стандартное управление пользователями.
Кроме того, Redash имеет лицензию MIT, а это значит, что разработчики разрешают использовать код даже в закрытых форках.
И, наконец, это современное одностраничное приложение.
Почему вы решили сменить стек?
В Redash все от начала до конца было построено на Flask: адаптеры sqlalchemy, миграции перегонных кубов, REST, аутентификация, ограничители скорости, CSRF и т. д. и даже CLI!.
А фронтенд выполнен с использованием стандартной комбинации React и JS. Например, код типичного обработчика Flask-RESTful:
Обработчик получает данные, извлекает их из тела запроса в формате JSON, проверяет и преобразует поля.
То есть простые вещи требуют много кода.
А если вы еще и хотите получать осмысленные сообщения об ошибках (например, поле неправильного типа или отсутствует обязательное поле), то шаблонный шаблон выходит в несколько раз больше.
Существует несколько вариантов упрощения и ускорения преобразования и проверки.
Например, вы можете использовать схему JSON, но тогда дату и время все равно придется анализировать вручную.
Другой вариант — использовать модули pydantic или marshmallow, которые не использовались в Redash. Дальше видно, что привязка маршрутов делается позже, в отдельном месте.
Хотя лучше было бы сделать это декоратором сразу на обработчике.
Переменная idx объявлена в обработчике, а ее тип дублируется в маршруте.
Кроме того, вся логика смещена на 2 отступа, что вроде бы мелочь.
Но в реальном коде могут быть, например, менеджер контекста, обработка исключений, цикл, пара вложенных if/else. Собственно еще ничего не написано, а код уже размазан по правому краю экрана.
И дополнительный второй отступ в этом очень помогает. Flask-RESTful также ничего не знает о типах данных и поэтому не может экспортировать схему в какой-либо машиночитаемый формат. Также есть проблемы с автодокументацией.
Flask-RESTful находится в режиме поддержки и не разрабатывается, хотя существуют более современные форки.
Или вот еще одна часть, вызывающая трения — Flask-SQLAlchemy:
Существует стандартный интерфейс выборки данных SQLAlchemy, с которым все знакомы.
Flask-SQLAlchemy добавляет урезанную версию выбора, которая короче на десять символов.
Эта экономия хороша для серверов CRUD. Но если вы делаете что-то более сложное, то помимо урезанного интерфейса вам придется использовать стандартный.
Полученная смесь не стоит. Также следует добавить, что все модели оставляют за собой слово «запрос», которое используется повсеместно в сфере Data Engineering, и нам приходится придумывать для него синонимы.
Использование Flask-SQLAlchemy в проекте Datafold привело к циклическим зависимостям: для инициализации приложения нужно было получить конфигурацию из базы данных, а чтобы получить что-то из базы данных через Flask-SQLAlchemy, нужно было инициализированное приложение.
Эту проблему можно решить с помощью уродливых хаков, но за них приходится платить ясностью кода.
Есть еще один момент — Flask-SQLAlchemy использует ограниченные сеансы базы данных.
То есть в начале каждого запроса он помещает сессию в локальные потоки, а затем каждый раз волшебным образом извлекает ее оттуда.
Когда дело выходит за рамки обработчиков http, становится удобнее передавать сеанс явно.
Хотя во многом это дело вкуса.
До того момента, пока в Celery воркеров придётся инициализировать приложение Flask, чтобы бизнес-логика могла работать с базой воркеров.
Были проблемы и с передней частью.
При использовании React + JS во время выполнения возникали постоянные ошибки типа.
А поскольку все данные по всему приложению передаются с помощью Object (в Python это аналог передачи по словарям), то иногда сложно понять, с каким типом данных вы имеете дело.
Поэтому они решили перевести проект на React + TypeScript. При этом типы интерфейсов переносились в TS из backend API вручную.
Из-за ручного переноса он часто рассинхронизировался, и снова возникали ошибки во время выполнения.
Необходимо было искать более подходящее решение, поэтому мы начали изучать другие варианты и остановились на FastAPI.
Сравнение с FastAPI
Код того же обработчика с FastAPI будет выглядеть так:Здесь мы видим несколько положительных моментов.
Проверка выполняется с помощью pydantic, вам просто нужно определить модель (CiRun) и установить типы полей.
Pydantic делает все остальное: анализирует, проверяет, обрабатывает и генерирует текст ошибки самостоятельно, избавляя вас от шаблонного кода.
Также FastAPI позволяет избавиться от проблемы дублирования аннотаций типов в URL-адресах.
Здесь достаточно указать тип только в одном месте.
И теперь в коде всего один отступ! Выглядит сомнительно, что тип возвращаемого значения указывается не в аннотации метода, а в декораторе.
Но если задуматься, это разумно — часто приходится возвращать типы в обертках, например с нестандартными HTTP-статусами.
FastAPI — OpenAPI/Swagger
Поскольку FastAPI «знает» формат данных на входе и выходе конечной точки, он может выводить полную схему API в виде JSON. Его можно загрузить с помощью Curl из работающей системы или экспортировать непосредственно из кода Python. На выходе получается описание схемы в JSON в формате Open API/Swagger, которое уже можно конвертировать в структуры и шаблонные вызовы для разных языков.Например, как сгенерировать описание интерфейса для TypeScript:
Если генерация типов производится во время сборки, то проблема синхронизации между фронтом и бэком решается.$ curl https://localhost:8000/openapi.json >openapi.json $ npx openapi-typescript schema.json --output schema.ts
асинхронный FastAPI
Одной из ключевых особенностей FastAPI является высокая производительность.Но для Datafold это не имело значения.
Хоть их SaaS и использует большое количество компаний, из-за предметной области здесь нет тысяч запросов в секунду.
Поддержку асинхронности часто путают с производительностью.
Если вдруг понадобится, то обработчик просто объявляется асинхронным и в нем пишется асинхронный код как обычно: @router.post('/api/.
') async def ci_run_async(.
): .
Здесь стоит отметить, что асинхронность действительно необходима только в небольшом количестве случаев, например, если вы используете веб-сокеты или длинный опрос.
В случае с Datafold внутренняя асинхронность FastAPI вызвала только проблемы.
Переход
Поскольку у стартапа не было возможности остановить разработку и переделать все за пару недель, переход производили частями, передавая в производство небольшими партиями.Пришлось заменить около десятка плагинов Flask: аутентификацию, ограничитель скорости, Flask-SQLAlchemy и другие.
Поэтому, когда масштаб стал понятен, они провели дополнительное Proof Of Concept, чтобы убедиться, что FastAPI точно выполняет свои обещания.
Сначала мы начали удалять модули, которые можно было удалить на работающем Flask. Мы заменили Flask-Migrate на стандартный перегонный куб, flask.cli на click и сделали плагин для Flask-SQLAlchemy. Мы подставили атрибут Model.query для всех моделей, чтобы не разрушать интерфейс выбора, иначе пришлось бы сразу рефакторить 90% кода.
После этого они вставили сервер FastAPI прямо перед Flask. Запросы сначала приходили к нему, и он передавал их Фласку: fastapi_app.mount(
"/api",
WSGIMiddleware(flask_app, workers=10)
)
Аутентификация должна была осуществляться с использованием файлов cookie и токенов.
Для этого мне пришлось написать небольшой код на основе имеющихся в FastAPI классов и тщательно его проверить.
Процесс перехода все еще продолжается.
Сейчас конвертируют эндпойнты и везде добавляют Pydantic, но основные результаты уже достигнуты.
проблемы с синхронизацией/асинхронностью
Теперь о проблемах, с которыми Datafold столкнулся при переходе.С FastAPI всё асинхронно, включая middleware — оно сделано как надстройка над асинхронным сервером Starlette. Это приводит к определенным проблемам в синхронных обработчиках.
Например, при интеграции с GitHub вам необходимо иметь возможность читать HMAC тела запроса, чтобы убедиться, что запрос пришел из GitHub, а не откуда-то еще.
Это предполагает очевидный код, который извлекает тело запроса: @router.post('/api/.
')
def ci_run(request: Request):
body = request.body()
Но это не работает так, потому что функция request.body() возвращает сопрограмму, а не данные, и она должна выполняться в асинхронном цикле событий.
Из синхронного кода это сделать не так-то просто.
Чтобы обойти эту проблему, вы можете создать отдельную асинхронную функцию, которая извлекает тело запроса из запроса: async def get_body(request: Request):
return await request.body()
@router.post('/api/.
')
def ci_run(body=Depends(get_body)):
pass
Затем эта функция включается в обработчик FastAPI как зависимость.
FastAPI разрешает все зависимости перед вызовом функции.
Поскольку это делается в асинхронном контексте, get_body() работает корректно, а тело самого запроса передается синхронному обработчику в качестве входных данных.
То же самое можно сделать с формами и JSON в теле запроса.
В модульных тестах тело можно передавать явно, без каких-либо сложных макетов.
Проблемы с SQLAlchemy
В проекте использован фреймворк Ariadne GraphQL, построенный на асинхронности, что имеет некоторый смысл — сервер GraphQL ориентирован на сбор данных из нескольких источников, а потому желательно собирать данные из них одновременно, чтобы сократить общее время ответа..
Одним из таких источников в проекте Datafold является PostgreSQL со всеми моделями приложений, основанными на SQLAlchemy. Но в версии 1.3 SQLAlchemy асинхронность не поддерживается.
Поэтому приходилось либо переходить на версию 1.4, которая уже поддерживала асинхронность, либо как-то обойти это в асинхронных эндпоинтах.
Может показаться, что можно просто написать обычную выборку:
Но сделать этого нельзя, потому что все сопрограммы событийного цикла будут заблокированы до завершения выбора.
Это увеличит задержку для всех остальных сопрограмм, сделает ее плохо предсказуемой и полностью устранит конкуренцию.
Вы можете попробовать сделать обычную ссылку, что делается при вызове кода синхронизации (run_in_executor) из асинхронного кода.
В этом случае выборка начнется в пуле потоков и создаст впечатление, что все в порядке.
Но, скорее всего, этого не произойдет, поскольку потоки пула потоков запускаются без контекста, управляющего соединениями SQLAlchemy. Запрос сеанса базы данных из отсутствующего контекста явно не сработает. Это показывает, что неявное имеет тенденцию приводить к неприятным сюрпризам.
Вы можете решить эту проблему, написав оболочку, которая создаст ScopedSession, а затем сделает выборку внутри ScopedSession: async def graphql_resolver():
def wrap():
with ScopedSession():
return session.query(DataSource).
all() objs = await ( asyncio.get_event_loop().
run_in_executor(
None, wrap
)
)
Другие проблемы
Во время перехода возникли и другие проблемы.Те самые ScopedSession поначалу были написаны не совсем корректно, поэтому в некоторых случаях соединения с базой данных оставались открытыми, что и было отловлено при тестировании.
Еще одна проблема, дошедшая до рынка, была связана с запуском FastAPI. В документации в качестве http-сервера рекомендуется использовать Uvicorn (асинхронный аналог Gunicorn).
В то же время в качестве последней линии защиты от утечек памяти проект перезапустил рабочие процессы.
Например, после 1000 запросов рабочий процесс перезапускается и освобождает всю память.
Проблема в том, что Ювикорн останавливает старый процесс, но не запускает новый.
В результате все процессы останавливаются и сервер по сути перестает работать.
В производстве проблема была обнаружена через 20 минут после развертывания и оперативно устранена.
Чтобы обойти эту проблему, вы можете использовать Gunicorn в качестве супервизора с плагином Uvicorn. В этой конфигурации Gunicorn отвечает за управление рабочими процессами, а Uvicorn работает внутри воркеров и обрабатывает запросы: gunicorn \
--worker-class uvicorn.workers.UvicornWorker \
.
Результат
После перехода с Flask на FastAPI код проекта стал более лаконичным, эргономичным и легко читаемым.Теперь можно документировать API, экспортировать его структуру и автоматически генерировать типы.
В конечных точках API за счет использования pydantic значительно меньше шаблонного кода и автоматически появляются качественные сообщения об ошибках проверки входных данных.
Видео моего выступления на Moscow Python Conf++ 2021:
На конференции Фонд HighLoad++ 2022 будет также раздел , посвященный BigData и машинному обучению.Теги: #python #программирование #postgresql #Big Data #bigdata #fastapi #sql #flask #redash #react #gunicorn #datafold17 и 18 марта Python-разработчики высоконагруженных систем встретятся в «Крокус-экспо» в Москве.
Присоединяйтесь к нам :) На конференции также будет представлена платформа с открытым исходным кодом, где 10 лучших авторов смогут рассказать о своем решении.
Сейчас приходящий прием заявок.
-
Основы Идеального Поста В Блоге
19 Oct, 24 -
Как Я Стал Паяльником
19 Oct, 24 -
Японское Очень Высокое Искусство
19 Oct, 24 -
Веб-Ос Для Программистов: Реализация
19 Oct, 24