惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Hacker News: Ask HN
Hacker News: Ask HN
D
DataBreaches.Net
Microsoft Security Blog
Microsoft Security Blog
U
Unit 42
V
Visual Studio Blog
GbyAI
GbyAI
云风的 BLOG
云风的 BLOG
博客园 - Franky
C
CXSECURITY Database RSS Feed - CXSecurity.com
大猫的无限游戏
大猫的无限游戏
P
Privacy & Cybersecurity Law Blog
T
The Exploit Database - CXSecurity.com
Simon Willison's Weblog
Simon Willison's Weblog
L
LangChain Blog
I
Intezer
V2EX - 技术
V2EX - 技术
Google DeepMind News
Google DeepMind News
T
Threat Research - Cisco Blogs
Apple Machine Learning Research
Apple Machine Learning Research
V
V2EX
腾讯CDC
博客园 - 【当耐特】
Know Your Adversary
Know Your Adversary
TaoSecurity Blog
TaoSecurity Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
F
Fortinet All Blogs
Project Zero
Project Zero
Blog — PlanetScale
Blog — PlanetScale
S
Security @ Cisco Blogs
量子位
M
MIT News - Artificial intelligence
美团技术团队
C
Cisco Blogs
S
Schneier on Security
Recent Commits to openclaw:main
Recent Commits to openclaw:main
G
Google Developers Blog
N
News and Events Feed by Topic
MongoDB | Blog
MongoDB | Blog
The Hacker News
The Hacker News
H
Help Net Security
S
Secure Thoughts
Scott Helme
Scott Helme
SecWiki News
SecWiki News
T
Troy Hunt's Blog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
博客园 - 叶小钗
O
OpenAI News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
博客园 - 司徒正美
T
Tenable Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Как забытые JVM ломали наш CI (и как мы это починили)
Александр Чесноков · 2026-06-19 · via Все публикации подряд на Хабре

Средний

7 мин

181

Привет, Хабр! Меня зовут Александр Чесноков, я разработчик команды Platform V DataGrid в СберТехе. Наш продукт — форк Apache Ignite с нашими доработками, и мы активно поддерживаем open‑source версию: контрибьютим в основной репозиторий, исправляем баги, развиваем новые фичи, а также поддерживаем инфраструктуру для тестирования.

В этой статье расскажу, как в Ignite мы столкнулись с orphan JVM на тестовых стендах, как они поломали нам тестирование и как мы решили эту проблему, научив дочерние JVM отслеживать завершение родителя через pipe, используя System.in. В конце разберем конкретную реализацию решения.

Статья будет полезна тем, кто работает с CI, интеграционными тестами, или запускает дочерние процессы в своих приложениях.

Apache Ignite

Apache Ignite — это распределённая in‑memory-система баз данных, поддерживающая datagrid-операции и написанная на Java. Каждый pull request от контрибьюторов идёт сразу в master, поэтому мы прогоняем каждый патч через тесты на TeamCity. У нас более 80 000 тестов, в основном интеграционных. Типичный сценарий: подняли серверный или клиентский узел Ignite, выполнили на нём операции и сравниваем полученные результаты с нашими ожиданиями.

Если хочется глубже погрузиться в тему CI и тестирования в Apache Ignite, то мы уже рассказывали об этом на Хабре — например, как боремся с flaky‑тестами и поддерживаем стабильность прогонов.

Отдельно отмечу совместимость: в Apache Ignite мы заботимся о поддержке взаимодействия различных версий продукта. Например, у нас есть понятие тонкого клиента — это клиентская нода в Ignite, которая может подключаться к серверной ноде любой версии. Также мы сейчас активно разрабатываем механизм Rolling Upgrade, когда в кластере могут одновременно быть серверные ноды двух разных версий. Такая функциональность позволит обновлять кластер без полной остановки.

Чтобы поддерживать совместимость, у нас есть соответствующие тесты. Типичный сценарий выглядит так: в исходной JVM запускаем тест, который запускает клиентскую или серверную ноду, а затем — новую JVM в дочернем процессе, где уже запускается нода старой версии. Старая нода выполняет некие операции, может подключиться к новой, и мы проверяем, чтобы поведение соответствало ожиданиям. Если старая нода падает, то родительская JVM по таймауту завершает запущенный процесс в блоке finally.

Казалось, что такие тесты должны работать как швейцарские часы, но однажды всё пошло не так…

Падение тестов

Мне досталась, на первый взгляд, простая задача: добавить тест на совместимость, который проверяет, что Rolling Upgrade корректно падает для серверной ноды при определённых условиях.

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

Однако через неделю мы заметили, что многие тесты начали падать по таймауту. Более того, падали наборы, которые вообще тестировали клиентские узлы, написанные на Python. Основной симптом — ошибка Address already in use, указывающая на занятый порт.

В итоге мы провели небольшое расследование, где вышли на меня и мой коммит. Посмотрели список всех запущенных процессов на тестовых стендах, благо в наших тестах на совместимость мы пишем понятные метки для новых процессов.

Я перепроверил свой тест: он корректно освобождал ресурсы и завершал дочернюю JVM. Тогда я задумался: в каком случае дочерний процесс вообще может остаться живым?

Ответ оказался прост — в том случае, если родительская JVM завершается резко. Я полез в историю прогонов в TeamCity, и тут всё сошлось. После добавления моего теста compatibility‑набор начал выполняться заметно дольше, и в какой‑то момент TeamCity просто принудительно завершал весь запуск.

В этот момент родительская JVM умирала, а дочерние процессы оставались жить — и чистить их уже было некому. Тут я пришёл к следующей задаче: как дочерняя JVM может узнавать о том что её родитель неожиданно завершился и пора завершать работу?

Для простоты мы называли такие забытые дочерние машины zombie JVM, поскольку по аналогии они уже не должны были жить. Однако корректнее говорить, что это orphan-процессы (orphan — сирота): в отличие от zombie-процессов, которые уже завершились, но ещё не «подобраны» родителем, orphan‑процессы продолжают выполняться после завершения родителя (подробнее про различия можно почитать, например, здесь).

Далее я начал думать над решениями.

ProcessHandle

Первой идеей было при запуске дочерней JVM запоминать родительский pid:

long parentPid = ProcessHandle.current()
    .parent()
    .map(ProcessHandle::pid)
    .orElse(-1);

И периодически проверять родителя. Если он поменялся, значит что-то пошло не так и надо завершаться. Мне не понравился этот вариант по двум причинам:

  • Непонятно, как часто проверять родителя. Конечно, в моей задаче было достаточно проверять каждые 1-5 секунд, но и тратить лишний раз ресурсы на банальную проверку не хотелось.

  • Возможна ситуация reparenting: когда дочернюю JVM может усыновить другой процесс. Например, parent pid может стать 1 (см. подробности на SO). В итоге может произойти гонка: дочерняя JVM только запустится, родитель сразу остановится и произойдёт reparenting на pid 1, который дочерняя JVM и запомнит как своего родителя.

File as lock

Следующей идеей было создать некий аналог блокировки, но для процессов. Её бы захватывал родитель, а дочерний процесс тоже в цикле пытался бы захватить. Если смог, значит родитель отвалился. В роли такой блокировки можно было взять некий файл, а процессы конкурировали бы за права на запись. Такой подход хотя бы устойчив к reparenting, но мне не нравилась его хрупкость: где именно создавать файл, как его называть, когда удалять? А что если остался файл с предыдущего прогона? Слишком много вопросов и движений для банального parent listening.

Parent pipe listener

Первое решение полностью реализуется пользовательским кодом, а второе делегирует захват блокировки операционной системе. Возникла идея: можно ли как‑то полностью делегировать процесс listening & notification операционной системе, которая явно может дать более строгие гарантии? Оказалось что для этого можно использовать механизм pipe.

Что такое pipe? Если на пальцах, то это некая «труба» между двумя процессами (producer & consumer), по которой можно передавать данные. Процесс producer пишет в write-end, а consumer читает из read-end. Если один из концов закрывается, то ОС сама закрывает pipe. Одним из примеров consumer end является System.in — тот самый, с которого мы можем читать пользовательский ввод в Java.

В случае создания дочерней JVM через ProcessBuilder, producer end для System.in в дочернем процессе принадлежит родительскому процессу. В итоге получается, что если родительский процесс завершается, то pipe закрывается и System.in.read() в дочерней JVM вернёт -1, что будет означать закрытие. Более того, System.in.read является синхронной операцией, то есть он ещё и сам будет ожидать закрытие файла (или получения данных), поэтому нам не надо делать какие-то таймауты для проверки родителя: ОС сама нам скажет, когда надо завершаться.

В итоге я реализовал и влил такое решение:

    /**
     * Checks that parent process is alive.
     *
     * <p>We listen on {@code System.in} because this compatibility node is started by the parent JVM via
     * {@link ProcessBuilder}, where {@code System.in} is a pipe from the parent. When the parent process exits,
     * the write-end of the pipe is closed and {@code System.in.read()} returns EOF. We treat this as a signal
     * to stop Ignite and terminate the process.</p>
     */
    private static void startParentPipeWatcher() {
        Thread thread = new Thread(() -> {
            try {
                while (System.in.read() != -1) {
                    // No-op
                }
            }
            catch (IOException e) {
                X.println("Failed to read parent stdin pipe, stopping compatibility node: " + e);
            }

            X.println("Parent JVM stdin pipe is closed, stopping compatibility node");

            try {
                if (ignite != null)
                    Ignition.stop(ignite.name(), true);
            }
            catch (Throwable e) {
                X.println("Failed to stop compatibility node after parent pipe closure: " + e);
            }
            finally {
                System.exit(0);
            }
        }, "compatibility-parent-pipe-watcher");

        thread.setDaemon(true);
        thread.start();
    }

Этот watcher запускается в самом начале жизненного цикла compatibility-ноды, ещё до запуска Ignite и выполнения пользовательских настроек. Таким образом каждая JVM, поднятая в рамках compatibility-тестов, автоматически отслеживает состояние родительского процесса и корректно завершается при его аварийном завершении.

// Запускаем watcher
startParentPipeWatcher();

// Делаем прочую конфигурацию
final Thread watchdog = delayedDumpClasspath();
IgniteConfiguration cfg = CompatibilityTestsFacade.getConfiguration();

…

// Запускаем ноду Ignite
ignite = Ignition.start(cfg);

Отстрел старых orphans-JVM

Я влил исправление, но остался ещё вопрос как убить старые orphan JVM на тестовых машинах. Перезапускать целиком было неразумно: у команды разработчиков нет прямого доступа к машинам, пришлось бы просить команду DevOps внимательно просмотреть процессы на более чем 20 серверах и убить PID, что было трудоемко.

Возникла идея: да, тестовые машины являются чёрными ящиками для разработчиков и мы не можем на них действовать прямо, однако наши PR прогоняются на тестах внутри этих ящиков, а значит мы можем влиять на них изнутри с помощью тех же PR. Поэтому я навайбкодил тестовый pull request, который на этапе проверки кодового стиля собирал информацию о машине.

package org.apache.ignite.tools.checkstyle;

import com.puppycrawl.tools.checkstyle.api.AbstractCheck;

public class IgniteAbbrevationsRule extends AbstractCheck {

    public IgniteAbbrevationsRule() {
        dumpDiagnosticsIfEnabled();
    }

    @Override public void visitToken(DetailAST ast) {
        // обычная проверка кодстайла
    }

    private static void dumpDiagnosticsIfEnabled() {
        System.err.println("=== Ignite system diagnostics dump start ===");

        dumpProcesses();
        dumpPortState(System.getProperty("os.name", "unknown"));

        System.err.println("=== Ignite system diagnostics dump end ===");
    }
}

В реальном PR внутри dumpProcesses() собирался список процессов через ProcessHandle.allProcesses(), а внутри dumpPortState() запускались системные команды вроде ps, lsof, ss и netstat.

Конкретные PID orphan JVM передал DevOps, что позволило им точечно почистить стенды. Более того, с помощью такого троянского PR можно было даже попробовать самим почистить машины, но мы решили не рисковать.

Выводы

После внедрения этого решения дочерние JVM начали корректно завершаться даже при аварийном завершении родителя. Проблема с занятыми портами исчезла, а стабильность CI заметно выросла.

Из всей этой истории я сделал несколько выводов:

  1. При создании кода, который захватывает какие-то ресурсы, обязательно думайте об освобождении этих ресурсов и учитывайте ситуации, что код может экстренно завершиться в любой строке и не дойти до ваших блоков catch-final.

  2. Старайтесь делегировать сложную низкоуровневую логику операционной системе или готовым механизмам, если они дают нужные гарантии.

  3. Не оставляйте DevOps-команду один на один с такими проблемами: вы лучше знаете свой код и можете быстро подсказать, какие процессы безопасно чистить.

Как вы у себя контролируете утечки ресурсов в тестах и что делаете, когда они всплывают в CI? Делитесь примерами в комментариях.