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

На Хабре вышла статья Артёма Торгашина «Плагины в Directum RX на AssemblyLoadContext». В ней описан способ загружать и выгружать сборки с логикой прямо внутри серверных процессов RX (версии 25.1 и выше, .NET 8). Исправление доезжает до пользователей без публикации пакета и без режима технического обслуживания. Автор честно перечисляет границы: метаданные и структуру базы так менять нельзя, а выгрузка работает только если на код плагина не осталось живых ссылок. Здесь тот же механизм с другой стороны: что он добавляет в работу тех, кто сопровождает систему, и что подготовить до первого плагина на проде.

Симптом

Обычный путь изменений в RX понятен эксплуатации. Разработчик собирает пакет, администратор публикует его через Directum Launcher, на время публикации включает режим технического обслуживания. Справка 25.2 (статья «Публикация») предупреждает, что веб-сервер при этом перезагружается и недоступен пользователям, поэтому публиковать в прод рекомендуют в нерабочее время. Отсюда окна работ, согласования и понятный след: пакет, версия, дата.

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

  • какая версия плагина сейчас работает на каждом сервере или поде;
  • почему на одном экземпляре веб-сервера поведение новое, а на соседнем старое;
  • почему память сервиса растёт ступеньками после каждой «горячей» замены;
  • кто положил эту DLL и проходила ли она ревью.

Почему так

Код живёт вне пакета. Состав прикладной разработки виден по пакету и по истории публикаций. Плагин грузится из своего источника (каталог, хранилище, документ в системе, зависит от реализации), и платформа о нём не знает. Учёт приходится вести самому.

Каждый процесс грузит сам. Сборку загружает конкретный процесс. Если серверов несколько или RX работает в Kubernetes с несколькими репликами, каждая реплика должна получить новую версию и перейти на неё. Пока одна не перешла, разные пользователи видят разное поведение. После перезапуска процесс грузит то, что лежит в источнике в этот момент, и это не обязательно та версия, которую загрузили «горячо» час назад.

Выгрузка не гарантирована. По документации Microsoft о выгружаемости AssemblyLoadContext вызов Unload() только запускает выгрузку. Контекст и его сборки уходят из памяти, когда сборщик мусора убедится, что ссылок на них не осталось. Ссылку держат статические поля за пределами контекста, подписки на события, таймеры, потоки и фоновые задачи, кэши, сохранённые делегаты и объекты рефлексии. Если хоть одна такая ссылка осталась, старая версия остаётся в памяти рядом с новой. Ошибки при этом нет, есть только рост памяти. Автор исходной статьи пишет об этом прямо, и это главный риск для эксплуатации.

Код выполняется с правами сервера. Сборка в серверном процессе RX может всё, что может процесс: читать конфигурацию со строками подключения, ходить в базу, в брокер и в хранилище. Источник плагинов по сути становится каналом поставки кода в прод, и защищать его нужно так же, как сам пакет.

Диагностика

Что стоит уметь посмотреть до того, как плагины пойдут в прод.

Что смотрим Где Что должно быть
Версия плагина на каждом экземпляре ответ самого механизма плагинов (служебная страница, журнал при загрузке) одна и та же на всех серверах и подах
Число загруженных сборок dotnet-counters, счётчик assembly-count в System.Runtime после замены и выгрузки возвращается к прежнему уровню
Размер управляемой кучи dotnet-counters, gc-heap-size, вместе с памятью контейнера после нескольких замен нет ступенек вверх
Живые контексты загрузки сам механизм: AssemblyLoadContext.All перечисляет контексты процесса старые контексты исчезают после выгрузки
Кто и когда менял источник журнал репозитория или хранилища, auditd на каталоге каждая замена имеет автора и заявку

Счётчики снимаются без остановки сервиса:

dotnet-counters monitor --process-id <PID> --counters System.Runtime

Кто держит старый контекст, показывает снимок кучи dotnet-gcdump. Его снятие запускает полную сборку мусора и останавливает процесс на время снимка, на большой куче это заметно. Поэтому снимаем его только на тестовом контуре, где воспроизводим ту же последовательность замен. Подробно про кучу, режимы сборщика и счётчики .NET в сервисах RX написано в статье «Сервис Directum RX съел всю память».

Что сделать

  1. Завести учёт. У каждого плагина есть версия и контрольная сумма. Механизм при загрузке пишет в журнал имя, версию, хэш и идентификатор процесса или пода. Из журналов собирается ответ на вопрос «что где работает», и расхождение между экземплярами видно сразу.
  2. Сделать источник единственным и версионным. Плагины лежат в одном месте, откуда их берут все экземпляры, и каждая версия сохраняется. Ручное копирование DLL на отдельные серверы исключено, иначе после перезапуска экземпляры разъедутся.
  3. Проверить поведение после перезапуска. Процесс, поднятый с нуля, должен загрузить ту же версию, которая работает «горячо» на остальных. Это отдельный тест, его легко пропустить.
  4. Подготовить откат. Предыдущая версия DLL хранится рядом и переключается тем же механизмом, что и новая. Если выгрузка старой версии не завершилась (контекст жив, память не вернулась), запасной ход один: перезапуск процесса, поочерёдно по экземплярам, чтобы не уронить весь веб-сервер. Время такого перезапуска нужно знать заранее. Общий порядок отката для обычных обновлений описан в статье «Обновление Directum RX без билета в один конец».
  5. Закрыть канал поставки кода. Писать в источник плагинов могут только сборка из CI и узкий круг людей. Загрузчик принимает сборку, только если она подписана или её хэш есть в разрешённом списке. Каждая замена фиксируется: кто, когда, какая версия, по какой заявке. Это то же требование к управлению изменениями, которое проверяют при аудите по требованиям ФСТЭК. Подходы описаны в статьях про GitOps и аудит изменений, про цепочку поставок образов и пакетов и про события безопасности через auditd.
  6. Обсудить с вендором или партнёром. Загрузка кода в серверные процессы идёт мимо штатного пути публикации, и платформа такой механизм не описывает. Как это сочетается с условиями технической поддержки, лучше уточнить у Directum или у партнёра, который сопровождает вашу систему, до того как плагины появятся на проде. При обращении в поддержку стоит сразу сообщать, какие плагины загружены, чтобы не тратить время на разбор поведения, которого нет в пакете.
  7. Проверять на тестовом контуре. Каждая версия плагина проходит тот же путь, что и пакет: загрузка, смоук веб-клиента (например, автотесты на Playwright), затем несколько циклов замены под нагрузкой (JMeter) с наблюдением за assembly-count и кучей. Если после десятка замен память не вернулась, выгрузка где-то держится, и на проде будет так же.
  8. Поставить алерты. Рост управляемой кучи после замены плагина, расхождение версий между экземплярами, замена в источнике без связанной заявки.

Чего не делать

  • Не использовать плагины для изменений структуры. Новые свойства, сущности и таблицы требуют публикации пакета, механизм их не заменяет.
  • Не копировать DLL на серверы руками. Через неделю никто не скажет, какая версия где работает.
  • Не считать вызов Unload() выгрузкой. Выгрузка завершилась, только когда контекст исчез и память вернулась.
  • Не заменять плагин на проде много раз подряд без перезапуска. Каждая неудачная выгрузка оставляет в памяти ещё одну копию.
  • Не давать запись в источник плагинов всем разработчикам. Это прямой путь чужого кода в серверный процесс.
  • Не снимать dotnet-gcdump с продуктивного процесса. Пауза на снимок ударит по пользователям.
  • Не скрывать плагины от поддержки. Без этой информации разбор обращения уйдёт не туда.