awesome-everything EN
↑ Обратно к восхождению

Архитектура бэкенда

DI-контейнеры в продакшене: графы разрешения, циклы и когда не стоит

Суть Контейнер — резолвер графа: топологически сортирует зависимости и инстанцирует их один раз. Острые края — циклы, которые он не упорядочит, жадный старт, вскрывающий баги конфига на бусте, и реальная цена: контейнер нужен не каждому приложению.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на senior-высоте — в орбите
◷ 17 min

Приложение нормально стартует в разработке и падает на продакшен-сервере с Nest can't resolve dependencies of the UserService (?, AuthService). В UserService ничего не менялось. Изменилось то, что AuthService теперь импортирует UserService, а UserService уже импортировал AuthService — цикл. В dev модули случайно загрузились в порядке, который замаскировал это; в prod — нет. Контейнер не глючит. Он говорит вам, что не может уложить круг в линию.

Контейнер — резолвер графа

Снимите декораторы — и DI-контейнер делает одно: читает объявления зависимостей, строит направленный граф и инстанцирует в порядке зависимостей — сперва листья, затем то, что от них зависит, вверх до корня. Это топологическая сортировка. Чтобы создать OrderService(db, payments), он сперва должен создать db и payments; чтобы создать их, разрешает их зависимости, рекурсивно. Для синглтонов он делает это один раз и кеширует экземпляр, так что второй потребитель db получает тот же объект. Контейнер — автоматизированный корень композиции из начала юнита: единственное место, знающее конкретику, теперь механизированное.

Почему циклические зависимости его ломают

Топологическая сортировка существует лишь для направленного ациклического графа. Если A зависит от B, а B от A, нет порядка «листья сперва»: чтобы построить A, нужен B, но чтобы построить B, нужен A. Контейнер обнаруживает цикл и отказывается — это ошибка из Hook. Фреймворки дают лазейку — forwardRef(() => Other) в NestJS — которая ломает дедлок, откладывая разрешение одной стороны до конца конструирования. Но лазейка — запах, а не решение: цикл почти всегда означает два класса, которым стоило бы быть одним, или недостающий третий класс, от которого должны зависеть оба. Тянуться к forwardRef, чтобы заглушить ошибку, сохраняет дефект дизайна.

Почему это работает

Почему цикл иногда появляется только в продакшене? Порядок разрешения может зависеть от порядка загрузки модулей, а тот может различаться между dev-сервером с hot-reload и холодным prod-бустом или между двумя сборщиками. При настоящем цикле некоторые порядки случайно разрешаются (первый класс частично сконструирован, когда второй просит его и получает полусобранную ссылку), а другие кидают. Это делает цикл латентным багом, всплывающим недетерминированно — зелено локально, красно на деплое. Реальный фикс — убрать цикл: вынести общую логику, нужную обоим классам, в третий класс, от которого зависят оба, превратив круг в дерево. forwardRef лишь прячет, что граф не ацикличен.

Жадный против ленивого: когда всплывает отказ

Контейнеры различаются тем, когда инстанцируют. Жадное инстанцирование строит весь граф синглтонов на старте, до обслуживания трафика — NestJS делает так по умолчанию. Ленивое инстанцирование создаёт каждый провайдер при первом использовании. Компромисс — когда кусает плохой конфиг: жадный старт означает, что отсутствующая env-переменная или неразрешимая зависимость роняет буст — громко, немедленно, до того как затронут пользователь. Ленивый означает, что та же мисконфигурация прячется до первого запроса, попавшего в этот путь кода, возможно часами позже, возможно лишь на одном эндпоинте. Для серверных приложений сеньорское предпочтение подавляюще жадное: заплати слегка медленный буст, чтобы превратить ночной рантайм-пейджер в деплой-тайм отказ, который ловит CI.

Скрытые цены

Контейнер не бесплатен. Он добавляет время старта (построение графа), налог на обучение и отладку (стектрейсы идут через код разрешения фреймворка, а ошибки «cannot resolve» — отдельный навык чтения) и соблазн скрытого глобального состояния — контейнер становится местом заначки синглтонов, которые на деле просто глобалы с лишними шагами. Для маленького сервиса, CLI или serverless-функции с крошечным графом ручное связывание в корне композиции — обычные вызовы new в одном файле точки входа — часто яснее и быстрее контейнера. Контейнер окупает цену, когда граф большой, времена жизни смешаны и связывание иначе было бы расползшимся ручным месивом. «Используй DI-контейнер» — решение по масштабу, а не дефолт.

ЗаботаЖадное (старт)Ленивое (первое использование)
Отсутствующая зависимостьПадает на бусте, до трафикаПадает посреди запроса, позже
Время бустаМедленнее (строит весь граф)Быстрее
Лучше дляДолгоживущих серверовCLI, редко задеваемых путей
Видимость отказаГромко, на деплоеТихо, в рантайме
Викторина

Почему DI-контейнер не может разрешить циклическую зависимость между двумя сервисами?

Викторина

Почему серверные приложения обычно предпочитают жадное инстанцирование графа зависимостей на старте?

Викторина

Когда ручное связывание в корне композиции часто предпочтительнее принятия DI-контейнера?

Вспомните перед уходом
  1. 01
    Что на самом деле делает DI-контейнер под капотом и как он обращается с синглтонами?
  2. 02
    Почему циклические зависимости ломают контейнер, почему цикл может появиться только в продакшене и каков реальный фикс?
  3. 03
    Сравните жадное и ленивое инстанцирование и объясните, когда контейнер стоит своей цены против ручного связывания.
Итог

Под декораторами DI-контейнер — резолвер графа: он превращает объявления зависимостей в направленный граф и инстанцирует их в топологическом порядке, строя синглтоны один раз и кешируя их — корень композиции, механизированный. Это объясняет его острейший край: у циклической зависимости нет валидного порядка конструирования, поэтому контейнер отказывается, и поскольку разрешение может следовать порядку загрузки модулей, тот же цикл может пройти в dev и упасть в prod. forwardRef откладывает одну сторону, чтобы разорвать дедлок, но лишь маскирует граф, который стоило сделать ацикличным, вынеся общий третий класс. Контейнеры также выбирают, когда строить: жадное инстанцирование на старте превращает мисконфигурацию в громкое падение на деплое, что долгоживущим серверам стоит предпочесть тихому отказу при первом запросе. И машина не бесплатна — цена старта, налог на отладку и тяга к скрытому глобальному состоянию означают, что маленький граф часто лучше связать руками в обычном корне композиции. С понятыми middleware и DI — осью запроса и осью связывания, их механикой, скоупами, швами и продакшен-краями — трек может двинуться к тому, как блокирующая и асинхронная работа формируют пропускную способность под нагрузкой.

Связанные уроки
встречается в185
Продолжить восхождение ↑Middleware и DI: тест с множественным выбором
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.