Как мы версию Sanic’а повышали

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Приветствую всех читателей, меня зовут Вадим, я — бэкенд-разработчик в компании Домклик. Я работаю в команде, которая разрабатывает CRM-систему для подготовки и осуществления ипотечных сделок. В этой статье я хотел бы поделиться своим интересным опытом мажорного повышения зависимостей в проекте, который свыше пяти лет находится в проде под ежедневной нагрузкой более 2000 RPS.

Предыстория

Итак, все сервисы нашей команды на бэке написаны на Python, большинство из них — с использованием фреймворка Sanic. До момента, приведшего впоследствии к этой статье, никаких серьёзных проблем с этим фреймворком мы не испытывали. Однако одним прекрасным декабрьским днём, когда сезонность оформления ипотечных сделок традиционно приводит к повышенной нагрузке на все сервисы Домклика, мы обнаружили проблему на центральном бэкенд-сервисе нашей системы. Суть этой проблемы заключалась в том, что в случайный момент времени воркеры приложения бесследно умирали, а у реализации мультипроцессинга в используемой нами на тот момент версии Sanic есть такая хитрая (нет) особенность, что состояние воркеров после запуска никак не отслеживается, и заданное количество никак не поддерживается в случае их смерти. Как результат, спустя некоторое (от нескольких минут до нескольких часов) время после развёртывания наши поды лишались всех воркеров, кроме одного единственного (от которого Sanic первоначально и форкает новые процессы), что драматически снижало перевариваемую нашим сервисом нагрузку: поды начинали тротлить по CPU, event loop забивался корутинами, приложение обжиралось коннектами к базе данных, запросы обрабатывались гораздо медленнее, и в конце концов мы начинали отдавать 500-ки.

В тот момент наш Саник выглядел как-то так
В тот момент наш Саник выглядел как-то так

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

А, собственно, зачем?

Вопрос вполне резонный, как обновление фреймворка поможет в сложившейся ситуации? Мы быстро смекнули, что проблема с производительностью сервиса связана с низким количеством живых воркеров. Понять с ходу причину их смерти было затруднительно, поэтому одним из треков решения стал автоматический запуск новых воркеров вместо умерших старых. Для этого нам понадобился какой-то внешний инструмент, позволяющий оркестрировать процессы нашего приложения, и выбор быстро пал на связку gunicorn + uvicorn, хорошо зарекомендовавшую себя в приложениях, написанных с использованием FastAPI. Одна проблема: поддержку ASGI-интерфейса в бета-режиме в Sanic завезли в более поздней версии 19.6.2, чем используемая нами на тот момент версия. Так мы пришли к выводу, что стоит обновить версию, причём чем выше, тем лучше. Бонусом, мы получили уникальную возможность поднять версии некоторых остальных зависимостей в нашем проекте, в том числе в надежде, что это поможет повысить производительность. Если кому интересно — под спойлером список наиболее важных изменений.

Список обновленных библиотек
  • aiofiles

  • aiohttp

  • aiopg

  • alembic

  • async-timeout

  • gunicorn

  • hiredis

  • httpx

  • pytest

  • sanic

  • Sanic-Cors

  • sanic-openapi

  • uvicorn

  • uvloop

  • pytest-sanic

NB: 27 декабря 2022 года, то есть спустя неделю, как мы столкнулись с нашей проблемой, вышла версия Sanic 22.12, добавляющая Worker Manager, реализующий так необходимую нам функциональность. Но это случилось после того, как наши мероприятия начались, поэтому мы пошли собственным путём, переезжая на gunicorn.

Осознание масштабов и решение проблем

С моей (не сильно опытной) точки зрения, повышение зависимости было достаточно простым: поднять версию библиотеки в requirements-файле и переустановить необходимые зависимости. Конечно же, я ошибался!

Достаём (свои же) палки из колес

Первой проблемой, с которой я столкнулся, была неразрешимость зависимостей. Мы пользовались специфической backport-версией внутренней библиотеки компании, которая по историческим причинам зависела от определённой версии Sanic и некоторых сопутствующих библиотек, что мешало нам обновиться. Было два пути: повышать зависимости в новой версии этой библиотеки, либо съезжать с неё вовсе. Я, получив разрешение техлида, избрал второй путь, благо из этой библиотеки мы использовали в основном только Enum’ы и некие общие функции-тулзы. Заведя эти вещи непосредственно в наш проект, зависимость благополучно выпилили, и я достиг успеха перешёл к устранению следующих проблем.

Метод научного п̶о̶д̶г̶о̶н̶а подбора версий

Краеугольным камнем и одновременно гарантией базовой работоспособности приложения после всех манипуляций стали тесты. В проекте у нас их более 6000, это обеспечивает более 65 % покрытия (но мы стремимся его улучшить!), да и все прекрасно понимают, что без тестов в современной разработке совсем никуда. Наш проект с самого своего рождения использовал для тестов плагин pytest-sanic. В какой-то момент он перестал поддерживаться, и, к сожалению, на новых версиях Sanic его использование в наших проектах стало невозможным. Перепробовав несколько доступных версий и не достигнув успеха, я пошёл штудировать списки изменений Sanic’а, и наконец обнаружил версию, на которую мы смогли перейти без радикального изменения тестов. Но несмотря на трудности, созданные тестами, их обширное наличие помогло мне отловить все те проблемы, с которыми я столкнулся в дальнейшем, так что каким бы тяжким не был этот путь, я всё-таки бесконечно благодарен каждому из коллег, написавшему даже самый маленький и простенький тест.

Работоспособность нашего приложения сразу после обновления зависимостей
Работоспособность нашего приложения сразу после обновления зависимостей

Breaking changes, или Как заставить разработчика изменить 90 % файлов проекта

Одним из ключевых (с точки зрения взаимодействия с кодом) моментов повышения версии стало ломающее обратную совместимость изменение в классе Request: теперь у него появились __slots__ и исчезли методы __getitem__ и __setitem__, а для хранения дополнительного пользовательского контекста был создан новый атрибут ctx. Таким образом, во всех ручках, где используются какие-либо данные из контекста, все строки вида

async def handler(request: Request):
    auth_session = request['auth_session']
	...

пришлось заменить на:

async def handler(request: Request):
    auth_session = request.ctx.auth_session
    ...

Схожим образом я обновил и все middleware, в которых эти самые экземпляры и обогащались контекстом по примеру auth_session.

На мой взгляд, новый вариант выглядит чуточку лаконичнее и логичнее, но в момент переноса было немного больно найти все места, где используются объекты Request, потому что, во-первых, далеко не в каждой функции коллеги-разработчики давали переменной одноимённое название, а во-вторых, далеко не всё, что названо в коде request, является тем самым нужным нам объектом. Ещё в объекте запроса поменялись такие ранее существовавшие атрибуты, как, например, server_path — шаблонизированная часть пути, по которому пришел обрабатываемый запрос — в новой версии он стал называться uri_template. Этот атрибут мы используем в метриках, группируя по нему запросы для подсчёта их количества по каждой из наших ручек.

На этом функциональность “Find and replace” в IDE не оставили в покое: в новой версии pytest-sanic асинхронный тестовый HTTP-клиент стал базироваться на HTTPX вместо AIOHTTP, что немного изменило интерфейс выполнения запроса и получения его результата в тестах. Поэтому в финальном pull request было много изменений следующего вида:

- response_json = await result.json()
- assert result.status == 200
+ response_json = result.json()
+ assert result.status_code == 200
- app_client.session.cookie_jar.update_cookies({
+ app_client.session.cookies.update({
        SESSION_COOKIE: session_cookie,
    })

Ключевым во всей этой затее было сначала «воскресить» максимум тестов, а затем, раз за разом прогоняя их, исправить уже проблемы изменённых интерфейсов классов Sanic. Одному PyСharm’у известно, сколько таких циклов я повторил, но рано или поздно все улизнувшие от моего взора места были обнаружены и исправлены.

Возможно, в этот раз Ваас оказался не прав, ведь я своего все-таки достиг 						</div>

Источник: https://habr.com/ru/companies/domclick/articles/761838/<br/><br/>




<div style=

Интересные статьи

Интересные статьи

При выпуске нового релиза сборки её версию обычно меняют. Это особенно актуально, если разрабатывается библиотека, от которой зависят другие проекты. Но что, если этого не делать? Предлагаю вашему вни...
Паяльники для тонких работ, в большинстве своем это микропайка, становятся все умнее. Современный паяльник — это уже далеко не просто толстенный нагревательный элемент, который включается, когда его...
Исторически мы использовали GitLab 8, который работал на хосте Mac на VirtualBox. Потом конфигурация перестала устраивать, поэтому в локальной сети завели отдельную полноценную Ubuntu-машину. Заодно и...
Разработка мобильных приложений — одна из наиболее конкурентных сфер в IT. Согласно статистике, ежемесячно в App Store появляется более 30 000 новых приложений для i...
Скоро выходит Android 12, но в этом августе уже с 11-й версии разработчикам придётся использовать новые стандарты доступа приложений к внешним файлам. Если раньше можно б...