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

Автор оригинала: Carl Meyer
  • Перевод
Публикуем первую часть перевода очередного материала из серии, посвящённой тому, как в 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-проектов?


  • +30
  • 4,1k
  • 7
RUVDS.com
1 501,91
RUVDS – хостинг VDS/VPS серверов
Поделиться публикацией

Похожие публикации

Комментарии 7

    +7

    ощущение такое, словно статью недописали

      0

      Часть 1 же.

        +1
        А в чём интересно смысл делить статью на части, особенно если в итоге получаются вот такие статьи вообще без полезной нагрузки.
          0
          Обычно целей несколько. Но основная — увеличить количество «сливок» снятых с статьи. Это ж SMM, маркетинг, все дела.
        0
        Опубликовали вторую часть
        0
        Перед нами — часть следствия того, что Python является динамическим интерпретируемым языком. Это позволяет нам успешно решать различные задачи методами метапрограммирования.

        Метапрограммирование вовсе не обязано приводить к такому аду.


        Собственно, потом в статье и пишут про неконтролируемые побочные эффекты, а они для метапрограммирования совсем не обязательны.


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

        Люди не знают, что есть компилируемые (и, чуть более общо, статически типизированные) языки с REPL'ом, ок.

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

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое