Горячая подмена DLL в Directum RX глазами эксплуатации: учёт, мониторинг, откат и ИБ
Что меняется для администраторов, когда исправления логики Directum RX приходят плагинами через AssemblyLoadContext, без публикации пакета и режима обслуживания. Учёт версий на нескольких серверах и подах, память после выгрузки, откат, контроль того, кто кладёт код в серверный процесс, разговор с вендором и проверка на тестовом контуре.
Коротко. Плагины на
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 съел всю память».
Что сделать
- Завести учёт. У каждого плагина есть версия и контрольная сумма. Механизм при загрузке пишет в журнал имя, версию, хэш и идентификатор процесса или пода. Из журналов собирается ответ на вопрос «что где работает», и расхождение между экземплярами видно сразу.
- Сделать источник единственным и версионным. Плагины лежат в одном месте, откуда их берут все экземпляры, и каждая версия сохраняется. Ручное копирование DLL на отдельные серверы исключено, иначе после перезапуска экземпляры разъедутся.
- Проверить поведение после перезапуска. Процесс, поднятый с нуля, должен загрузить ту же версию, которая работает «горячо» на остальных. Это отдельный тест, его легко пропустить.
- Подготовить откат. Предыдущая версия DLL хранится рядом и переключается тем же механизмом, что и новая. Если выгрузка старой версии не завершилась (контекст жив, память не вернулась), запасной ход один: перезапуск процесса, поочерёдно по экземплярам, чтобы не уронить весь веб-сервер. Время такого перезапуска нужно знать заранее. Общий порядок отката для обычных обновлений описан в статье «Обновление Directum RX без билета в один конец».
- Закрыть канал поставки кода. Писать в источник плагинов могут только сборка из CI и узкий круг людей. Загрузчик принимает сборку, только если она подписана или её хэш есть в разрешённом списке. Каждая замена фиксируется: кто, когда, какая версия, по какой заявке. Это то же требование к управлению изменениями, которое проверяют при аудите по требованиям ФСТЭК. Подходы описаны в статьях про GitOps и аудит изменений, про цепочку поставок образов и пакетов и про события безопасности через auditd.
- Обсудить с вендором или партнёром. Загрузка кода в серверные процессы идёт мимо штатного пути публикации, и платформа такой механизм не описывает. Как это сочетается с условиями технической поддержки, лучше уточнить у Directum или у партнёра, который сопровождает вашу систему, до того как плагины появятся на проде. При обращении в поддержку стоит сразу сообщать, какие плагины загружены, чтобы не тратить время на разбор поведения, которого нет в пакете.
- Проверять на тестовом контуре. Каждая версия плагина проходит тот же путь, что и пакет: загрузка,
смоук веб-клиента (например, автотесты на Playwright), затем несколько циклов
замены под нагрузкой (JMeter) с наблюдением за
assembly-countи кучей. Если после десятка замен память не вернулась, выгрузка где-то держится, и на проде будет так же. - Поставить алерты. Рост управляемой кучи после замены плагина, расхождение версий между экземплярами, замена в источнике без связанной заявки.
Чего не делать
- Не использовать плагины для изменений структуры. Новые свойства, сущности и таблицы требуют публикации пакета, механизм их не заменяет.
- Не копировать DLL на серверы руками. Через неделю никто не скажет, какая версия где работает.
- Не считать вызов
Unload()выгрузкой. Выгрузка завершилась, только когда контекст исчез и память вернулась. - Не заменять плагин на проде много раз подряд без перезапуска. Каждая неудачная выгрузка оставляет в памяти ещё одну копию.
- Не давать запись в источник плагинов всем разработчикам. Это прямой путь чужого кода в серверный процесс.
- Не снимать
dotnet-gcdumpс продуктивного процесса. Пауза на снимок ударит по пользователям. - Не скрывать плагины от поддержки. Без этой информации разбор обращения уйдёт не туда.
Похожая картина у вас?
Разберём ваш контур за 30 минут бесплатного созвона: версия RX, состав сервисов, что болит сильнее всего.
Написать