Перейти к содержанию

Ядро без зависимостей: зачем инфраструктурным библиотекам необязательные зависимости

Инфраструктурная библиотека попадает во многие сервисы вместе со всеми своими зависимостями. Обычно об этом рассуждают отвлечённо. Я измерил восемь библиотек: что устанавливает базовый пакет, сколько занимает на диске и сколько длится импорт. Самое маленькое ядро устанавливает один дистрибутив и импортируется за 1,2 мс. Вариант с обязательным драйвером БД устанавливает девять и требует около сотни миллисекунд.

Числа получены в эксперименте статьи: временное окружение на пакет, подсчёт установленных файлов и пять измерений импорта. Python 3.13, опубликованные на день измерения версии.

Цена каждой библиотеки

    package                     deps      MB  import ms   with the extra
    deadline-budget                1     0.1        1.2
    clientwright                   1     0.3       23.9
    grpc-client-kit                3    38.3       27.6   [deadline]: +1 deps, +0.7 MB
    redis-client-kit               2     2.6       43.4   [settings]: +7 deps, +8.7 MB
    servicewright                  1     0.4       30.1   [fastapi]: +22 deps, +16.4 MB
    sqlalchemy-foundation-kit      9    18.1      124.7   [metrics]: +1 deps, +7.9 MB
    aiokafka-foundation-kit        5     2.0       33.4   [models]: +4 deps, +7.1 MB
    pg-partsmith                  10    17.4      107.6   [cli]: +9 deps, +14.1 MB

Три библиотеки устанавливают только себя. deadline-budget считает передаваемый бюджет и не требует зависимостей. clientwright создаёт HTTP-клиенты без обязательной HTTP-библиотеки: её выбираете вы. servicewright управляет жизненным циклом без обязательного веб-фреймворка.

Показателен [fastapi]: двадцать два дистрибутива и шестнадцать мегабайт при ядре из одного дистрибутива и четырёхсот килобайт. Сервис с gRPC без HTTP за это не платит.

Какую проблему это предотвращает

Главное — разрешение зависимостей, а не диск.

Обязательная FastAPI задаёт диапазон её версий, который задаёт диапазоны Starlette и Pydantic. Другая библиотека сервиса может потребовать несовместимый диапазон, о котором первый автор не знал. Чем больше общих библиотек и сервисов, тем выше вероятность несовместимой пары и необходимости обновлять несвязанное по всей системе.

Каждая обязательная зависимость ограничивает все использующие библиотеку сервисы. Необязательная — только выбравшие интеграцию.

Время импорта тоже имеет значение: 1,2 против 124,7 мс — два порядка, оплачиваемые при запуске процесса, CI, serverless cold start и --help.

Хорошее ядро

Три строки с одним дистрибутивом даёт принцип: ядро содержит решения, extras — интеграции.

Самый ясный пример — clientwright. Ядро знает таймауты, повторы, бюджеты, circuit breaker и метрики. Оно не импортирует httpx, aiohttp, requests или urllib3; адаптеры доступны через extras и выбираются по имени при сборке:

    registered adapters without any HTTP library: ('aiohttp', 'httpx', 'httpx2', 'requests', 'urllib3')

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

    build('httpx') -> ImportError: httpx support requires clientwright[httpx]; install it.
ИДЕЯ В СХЕМЕНеобязательные интеграции зависят от ядра
---
config:
  theme: default
  look: classic
  flowchart:
    useMaxWidth: false
    wrappingWidth: 150
    padding: 12
    nodeSpacing: 24
    rankSpacing: 32
---
flowchart BT
    accTitle: Необязательные интеграции зависят от ядра
    accDescr: Ядро не импортирует фреймворки и необязательные адаптеры. Extras добавляют интеграции вокруг его контрактов; пакет, посвящённый интеграции с SQLAlchemy, при этом может напрямую зависеть от SQLAlchemy.
    A["Необязательный адаптер транспорта"] -->|"Импортирует"| C["Ядро: контракты, состояния, политики"]
    B["Необязательная интеграция фреймворка"] -->|"Импортирует"| C
    O["Необязательная интеграция наблюдаемости"] -->|"Импортирует"| C

Ядро не импортирует фреймворки и необязательные адаптеры. Extras добавляют интеграции вокруг его контрактов; пакет, посвящённый интеграции с SQLAlchemy, при этом может напрямую зависеть от SQLAlchemy.

Сообщение об ошибке — часть возможности

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

    servicewright     ImportError: FastAPI support requires servicewright[fastapi]; install it.
    redis-client-kit  ImportError: pydantic-settings not installed. Install
                      redis-client-kit[settings] to use BaseRedisSettings.
    grpc-client-kit   imported; HAS_DEADLINE_BUDGET = False

Первые две называют extra и точно говорят, что установить. Голое ModuleNotFoundError: No module named 'starlette', бывшее до исправления, называет не запрошенный пользователем пакет без очевидной связи с extra.

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

Моё правило: отказывать, если отсутствие нарушает корректность; ограничивать возможности, если это допустимо; в обоих случаях сообщать явно.

Когда обязательная зависимость правильна

В таблице есть библиотеки с настоящими обязательными зависимостями, и это оправданно.

sqlalchemy-foundation-kit требует SQLAlchemy и asyncpg: девять дистрибутивов, 18 МБ, около 124 мс. Библиотека посвящена async SQLAlchemy с PostgreSQL; делать драйвер необязательным без реального сценария — искусственно. Для pg-partsmith похожую роль играют SQLAlchemy и Pydantic.

Вопрос не «можно ли сделать optional?», а «есть ли реальный пользователь без этой зависимости?». Клиент Redis нужен с redis-py. Жизненный цикл сервиса нужен многим без FastAPI.

Когда одним нужно, другим нет, подходят extra и протокол: ядро структурно определяет контракт, extra даёт реализацию. Так устроены метрики: ядро принимает нужные методы, [metrics] добавляет Prometheus тем, кто его выбрал.

Проверочный список

  • Можно ли использовать библиотеку без X? Если да, рассмотрите extra.
  • Нет ли импорта X на уровне модуля ядра? Один случайный импорт делает optional фактически обязательной. Проверяйте импорты необязательных модулей в окружении без интеграции.
  • Называет ли ошибка нужный extra? Проверяйте сообщение, не только тип исключения.
  • Безопасен ли запасной путь? Если нет, отказывайте явно.
  • Готовы ли вы установить эту зависимость во все свои сервисы? Именно это означает обязательное объявление.

Инструменты

Измерены библиотеки Bedrock Python: минимально необходимое ядро, интеграции за extras, структурные протоколы там, где достаточно контракта. Два сообщения исправлены в день написания: статья заставила их измерить.

Один дистрибутив и 1,2 мс либо десятки дистрибутивов и шестнадцать мегабайт. Разницу определяет, кто выбирает интеграцию.