Хабрахабр

[Перевод] Использование strict-модулей в крупномасштабных Python-проектах: опыт Instagram. Часть 1

Публикуем первую часть перевода очередного материала из серии, посвящённой тому, как в Instagram работают с Python. В первом материале этой серии речь шла об особенностях серверного кода Instagram, о том, что он представляет собой монолит, который часто меняется, и о том, как статические средства проверки типов помогают этим монолитом управлять. Второй материал посвящён типизации HTTP-API. Здесь речь пойдёт о подходах к решению некоторых проблем, с которыми столкнулись в Instagram, используя Python в своём проекте. Автор материала надеется на то, что опыт Instagram пригодится тем, кто может столкнуться с похожими проблемами.

Обзор ситуации

Давайте рассмотрим следующий модуль, который, на первый взгляд, выглядит совершенно невинно:

import re
from mywebframework import db, route
VALID_NAME_RE = re.compile("^[a-zA-Z0-9]+$")
@route('/')
def home(): return "Hello World!"
class Person(db.Model): name: str

Какой код будет выполнен в том случае, если кто-то импортирует этот модуль?

  • Сначала выполнится код, связанный с регулярным выражением, компилирующий строку в объект шаблона.
  • Затем будет выполнен декоратор @route. Если основываться на том, что мы видим, то можно предположить, что тут, возможно, производится регистрация соответствующего представления в системе URL-мэппинга. Это означает, что обычный импорт этого модуля приводит к тому, что где-то ещё меняется глобальное состояние приложения.
  • Теперь мы собираемся выполнить весь код тела класса Person. Тут может содержаться всё, что угодно. У базового класса Model может иметься метакласс или метод __init_subclass__, который, в свою очередь, может содержать ещё какой-то код, выполняемый при импорте нашего модуля.

Проблема №1: медленные запуск и перезапуск сервера

Единственная строка кода этого модуля, которая (возможно) не выполняется при его импорте, это return "Hello World!". Правда, с уверенность мы это утверждать не можем! В результате оказывается, что импортировав этот простой модуль, состоящий из восьми строк (и при этом даже ещё не воспользовавшись им в своей программе) мы, возможно, вызываем запуск сотен или даже тысяч строк Python-кода. И это — не говоря о том, что импорт данного модуля вызывает модификацию глобального URL-мэппинга, находящегося в каком-то другом месте программы.

Перед нами — часть следствия того, что Python является динамическим интерпретируемым языком. Что делать? Но что, всё же, не так с этим кодом? Это позволяет нам успешно решать различные задачи методами метапрограммирования.

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

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

А иногда, когда мы не уделяем должного внимания оптимизации, это время увеличивается примерно до минуты. На запуск нашего сервера нужно более 20 секунд. Это относится и к тому, что можно видеть в браузере, и даже к скорости запуска модульных тестов. Это означает, что разработчику нужно 20-60 секунд на то, чтобы увидеть результаты изменений, внесённых в код. Большая часть данного времени, в буквальном смысле, тратится на импорт модулей и на создание функций и классов. Этого времени человеку, к сожалению, достаточно для того, чтобы на что-то отвлечься и забыть о том, что он до этого делал.

Но обычно компиляцию можно выполнять инкрементно. В некотором роде это — то же самое, что ждать результатов компиляции программы, написанной на каком-то другом языке. В результате обычно компиляция проектов, выполняемая после внесения в них небольших изменений, производится быстро. Речь идёт о том, что можно перекомпилировать только то, что поменялось, и то, что напрямую зависит от изменённого кода. При этом масштабы изменений неважны и нам приходится каждый раз полностью перезапускать сервер, импортируя все модули, пересоздавая все классы и функции, перекомпилируя все регулярные выражения, и так далее. Но при работе с Python, из-за того, что команды импорта могут иметь какие угодно побочные эффекты, не существует надёжного и безопасного способа инкрементной перезагрузки сервера. Обычно с момента последнего перезапуска сервера не меняется 99% кода, но нам, для ввода в строй изменений, всё равно приходится раз за разом делать одно и то же.

Дело в том, что мы работаем в режиме непрерывного развёртывания изменений, что означает постоянные перезагрузки кода продакшн-сервера. В дополнение к замедлению разработчиков это ведёт и к непродуктивной трате серьёзных объёмов системных ресурсов.

Эта проблема возникает из-за того, что во время импорта кода системе приходится постоянно проделывать большой объём повторяющихся действий. Собственно говоря, вот наша первая проблема: медленные запуск и перезапуск сервера.

Проблема №2: побочные эффекты небезопасных команд импорта

Вот ещё одна задача, которую, как оказалось, разработчики часто решают во время импорта модулей. Это — загрузка настроек из сетевого хранилища конфигураций:

MY_CONFIG = get_config_from_network_service()

В дополнение к тому, что это способствует замедлению запуска сервера, это ещё и небезопасно. Если сетевой сервис окажется недоступным, то это приведёт не просто к тому, что мы получим сообщения об ошибках, касающихся невозможности выполнить некоторые запросы. Это приведёт к тому, что сервер не сможет запуститься.

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

В результате же, через цепочку импортов глубиной в двенадцать модулей, это привело к тому, что модуль, загружающий настройки из сети, теперь импортируется до модуля, который инициализирует соответствующий сетевой сервис. Но потом ещё кто-то добавил ещё куда-то команду импорта, на первый взгляд безобидную.

Система, естественно, даёт сбой. Теперь получается, что мы пытаемся воспользоваться сервисом до его инициализации. Но в более сложных ситуациях это может привести к «падению» проекта в продакшне. В лучшем случае, если речь идёт о системе, взаимодействия в которой полностью детерминированы, это способно привести к тому, что разработчик потратит час-два на то, чтобы выяснить то, как незначительное изменение привело к сбою в чём-то, с ним, как кажется, не связанным. При этом нет универсальных способов использования линтеров для борьбы с подобными проблемами или для их предотвращения.

Корень проблемы кроется в двух факторах, взаимодействие которых и приводит к разрушительным последствиям:

  1. Python позволяет модулям иметь произвольные и небезопасные побочные эффекты, проявляющиеся во время импорта.
  2. Порядок импорта кода не задаётся явным образом и не контролируется. В масштабах какого-то проекта некий «всеобъемлющий импорт» — это то, что складывается из команд импорта, содержащихся во всех модулях. При этом порядок импорта модулей может меняться в зависимости от используемой входной точки системы.

Продолжение следует…

Уважаемые читатели! Сталкивались ли вы с проблемами, касающимися медленного запуска Python-проектов?

Теги
Показать больше

Похожие статьи

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *

Кнопка «Наверх»
Закрыть