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

推荐订阅源

F
Full Disclosure
Recorded Future
Recorded Future
T
Tenable Blog
S
Securelist
C
CERT Recently Published Vulnerability Notes
T
Threatpost
S
Schneier on Security
A
Arctic Wolf
The Hacker News
The Hacker News
C
CXSECURITY Database RSS Feed - CXSecurity.com
Know Your Adversary
Know Your Adversary
P
Privacy International News Feed
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
The Register - Security
The Register - Security
Cisco Talos Blog
Cisco Talos Blog
AWS News Blog
AWS News Blog
K
Kaspersky official blog
T
True Tiger Recordings
T
Threat Research - Cisco Blogs
V
Vulnerabilities – Threatpost
P
Palo Alto Networks Blog
T
The Exploit Database - CXSecurity.com
小众软件
小众软件
B
Blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
Microsoft Azure Blog
Microsoft Azure Blog
Cyberwarzone
Cyberwarzone
C
Cybersecurity and Infrastructure Security Agency CISA
T
Tor Project blog
Spread Privacy
Spread Privacy
Malwarebytes
Malwarebytes
P
Proofpoint News Feed
F
Fox-IT International blog
F
Fortinet All Blogs
P
Privacy & Cybersecurity Law Blog
G
GRAHAM CLULEY
量子位
Latest news
Latest news
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
博客园 - 叶小钗
Project Zero
Project Zero
T
Tailwind CSS Blog
N
Netflix TechBlog - Medium
Martin Fowler
Martin Fowler
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
IntelliJ IDEA : IntelliJ IDEA – the Leading IDE for Professional Development in Java and Kotlin | The JetBrains Blog
I
Intezer
博客园_首页
腾讯CDC
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Darknet – Hacking Tools, Hacker News & Cyber Security

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

Я держал кафе 16 лет и кормил полгорода. Потом пришли зумеры и всё посыпалось Есть ли жизнь на фазе: откуда берёт энергию умный выключатель без подключённой нейтрали Go Computer. История удивительного планшета из 1992 года с графическим интерфейсом Экономия GPU-часов в 2,5 раза, уход ИИ в бэкенд и новые стандарты агентских систем: ML-дайджест Что скрывается за AI-стратегией SAP, Oracle и Palantir: зачем корпоративному ИИ семантическое ядро Почему RAG — фундамент любой AI-трансформации Персонализация как баг Одна на 9 команд: как я внедряла квартальное планирование в трайбе, который сопротивлялся переменам После ИИ писать код руками ощущается уже не как норма Языковые модели без машинного обучения Обмен через интернет между мобильными приложениями ТСД и 1С От плановых ремонтов к предиктивному обслуживанию: дорожная карта для главного инженера Параллельный импорт техники закрыли или нет? Юридический разбор Резервное электрообеспечение для ЦОДов: патенты в мире и в России 256 зелёных тестов на нерабочем коде. Так выглядит «услужливый клерк» внутри нейросети Бизнес-аналитика для сети из 300 аптек: прогноз продаж и другие показатели Impact Analysis в дизайн-системе: как мы сделали CI осмысленнее, а review понятнее Топ-5 лучших нейросетей 2026 года: полный список на любой случай в SpeShu.AI Что делает сотрудников по-настоящему эффективными: процессы, знания или технологии Как за один вечер я написал сервис инвентаризации оргтехники для филиальной сети из 16 локаций Склад нанимает — и не может остановиться. Дефицит складских работников в 2026 году: причины и решения Штраф в размере 155 000 рублей получил владелец сайта по заявлению Роскомнадзора Индивидуальный план развития: от формальной процедуры к инструменту управления экспертизой команды Как понять, что вы не управляете финансами, а просто смотрите на цифры Водоросли и микропластик Масштабирование LLM: от одного чипа до ЦОДа. Глава 3. Траснформеры Бомба замедленного действия взорвалась: эпоха ИИ «бери сколько унесёшь» закончилась Стимпанк как часть жизни. История паровых двигателей и место, которое они занимали в мире в XIX-XX веках. Часть 2 288-ядерный Xeon 6+ и другие серверные CPU От OCR к смыслу: как мы научили модель понимать, кто кому отец, мать, жених и свидетель Насколько плох был Intel iAPX 432 — проверяем на практике Приручаем железо: внедряем DevOps в промышленной разработке Когда Reality не хватает: добавляем Hysteria2 + Salamander в iOS-мессенджер, и как всегда грабли по дороге (ч.2) Разработчики не экстрасенсы: как мы перестали приносить туман вместо ТЗ Дайджест C++: новости, полезные материалы и “свой язык” на десерт Ещё один репозиторий моделей для Archi 10 простых шагов, чтобы создать позиционирование для продукта Загадочная поэма древнего Китая, работающая как компьютер CLOUD Act, GDPR и ваш DNS: что на самом деле может ваш провайдер Ускоряем и оптимизируем numpy, pandas, scipy и sklearn Idempotency keys: 5 граблей, которые мы поймали на проде Gamedev. Парсинг данных из Google Sheets и Excel в json без привлечения программистов Nano Banana Google AI: как использовать Нано Банана для генерации и редактирования изображений Два игрока на весь российский рынок ИИ: что показал ЦИПР-2026 Менеджер ресурсов ЯНДЕКС 360 (YANDEX 360) промокоды июнь 2026: промокод Yandex 360 скидка 40% на годовые тарифы Open-Source инструмент для автоматического перевода книг Ищу ранних тестировщиков для Android-версии agent harnesses Не используйте LLM для текста Увеличиваем продажи без слез аналитика Оптимизация запросов к PostgreSQL: 5 неочевидных настроек для продакшена 45 лет тюрьмы за DROP TABLE и переход Карпатого в Anthropic Планирование движения для ровера на ходовой Ackerman'а Революция в изучении языков Java — быстрая. Ваш код может таким не быть Как я опоздал на конкурс OpenAi с новой архитектурой нейросети Быстрые интеграции в 1С: прощайте, бесконечные переделки Как получить субсидию 300 миллионов от Минпромторга? preIPO Anthropic, OpenAI, SpaceX. Разбираемся — стоит ли участвовать? Entaxy ION + OPC UA: два способа получить данные с промышленного оборудования Память на миллион, а толку ноль: как мы спасали ИИ-агента от «тупости» РСЯ, AdSense или myTarget: что на самом деле в 2026 приносит больше денег сайту и причем тут монетизаторы Практическое построение сервисов на Go под реальный трафик PostgreSQL и аналитика: что меняется, когда хранилище становится общим Codex за 5 месяцев 2026: мой топ-5 релизов, что не зашло и где OpenAI обогнал Anthropic Как создать короткое видео с помощью нейросетей: Полный гайд по Veo 3.1, Kling 3.0 и Happy Horse 1.0 Алгоритм проверок физлиц от экс сотрудника ФНС Как ИИ портит резюме студентам Системные вызовы в сфере ИТ в 2026: стратегический взгляд для ИТ-руководителей Вайбкодинг заканчивается на localhost: как я строю SaaS для цифровизации коттеджных поселков с Codex Производственные риски в небольшом кастомном производстве. С чем я сталкивалась и как научилась это учитывать Подключаем ИИ органы чувств: bash-демон, пайка и самосознание на Raspberry Pi Я хотел повторить Growing Neural CA за вечер. Ушёл месяц Промт для генерации текста без ИИ следа — как писать уникальные тексты через нейросеть От capabilities к AppArmor: что реально остановит атакующего в контейнере CactOS Вектора интересов: как находить настоящую мотивацию и усиливать команды Цена безопасности [Перевод] Цена безопасности “Рубик” от пет-проекта до прода или ITIL 4 для строительно-торговых центров Чего ждать (и не ждать) от ремейка AC4 Black Flag Архитектурный тупик корпоративного хранения: почему смена модели не снимает ограничений и что с этим делать Атаки через подрядчиков, дефицит кадров и квест с импортозамещением: главные вызовы ИБ в 2026 году Я не оставлю детям наследства Почему порты стали «дверями» в сервер, и кто решил, что SSH будет 22 Почему зарубежные разработчики чипов возвращаются на китайские фабрики Как у меня НЕ получился торговый бот на Polymarket Проектирование архитектуры в нотации ArchiMate с использованием ИИ. Часть 2 Как превратить домашнюю файлопомойку в умную AI-галерею на основе сборки из x99+Xeon и видеокарты за 2 тыс рублей Перспективы заселения нашей галактики Кризис менеджмент в ИТ Reactive Programming не спасёт вас. Если вы не решили эти 5 проблем — у вас просто медленный монолит с Flux Как я делаю DIY-контроллер для ПК: громкость, приложения, MIDI, OBS Миграция микросервисов на Python с помощью LLM: экономим месяцы для разработчиков Программирование микросхем GAL и им подобных Почему таск-трекер не заменяет ИСУП: из чего состоит полноценный контур управления проектами Всё об информационной безопасности. Кибербезопасность. DevOps, CI/CD. Хакеры. Алексей Федулаев Как импортировать базу клиентов в amoCRM и навести порядок в контактах Как мы четыре раза переписали Outbox Google предлагает единый «водяной знак» для изображений, видео и текста, созданных ИИ
Шёл за утечкой памяти, нашёл утечку диска: SXSSFWorkbook без dispose() в Apache POI
igoresha_s · 2026-05-27 · via Все публикации подряд на Хабре

Уровень сложностиСредний

Время на прочтение9 мин

Охват и читатели3.3K

Кейс

Меня зовут Игорь Симаков, работаю engineering manager’ом и руковожу командами разработки

На одном из наших сервисов, который работает с XLSX-файлами, прилетел production-алерт на высокое потребление памяти. Стандартный P3, обычно решается рестартом. Пошёл смотреть поды и нашёл проблему, к памяти отношения не имеющую, но представляющую больший риск, чем сам алерт. Об этом и расскажу ниже: чем «утечка диска» отличается от «утечки памяти», как мы наткнулись на грабли в Apache POI и как закрыли их на уровне архитектуры

Утечка памяти и утечка диска: в чём разница

В обоих случаях логика одинаковая: приложение выделило ресурс и не освободило. Со временем расход растёт, пока не упрётся в потолок

Утечка памяти. Объекты копятся в куче Java (heap) или в native-памяти за её пределами - direct buffers (память для сетевого I/O в обход heap), нативные библиотеки, стеки потоков. Ссылка живёт там, где не должна, GC её не собирает. Память пода растёт, упирается в limits.memory, Kubernetes делает OOMKill, под перезапускается. Локальная проблема: страдает только сам сервис, лечится рестартом

Утечка диска. Приложение создаёт файлы и не удаляет. Здесь начинается интересное: упирается это не в потолок пода, а в свободное место на диске ноды. По умолчанию /tmp в поде - это emptyDir без sizeLimit, физически он лежит на диске ноды (тот же /dev/vda2, где живёт ОС, container runtime, логи контейнеров). У пода лимита нет, он пишет, пока место есть на ноде. Когда место заканчивается, write() возвращает ENOSPC, и падает не только наш сервис - падает всё, что пишет на этот диск, включая чужие поды и kubelet

Утечка памяти

Утечка диска (host disk)

Где предел

limits.memory пода

свободное место на ноде

Что происходит при упоре

OOMKill, под перезапускается

I/O начинает возвращать ENOSPC

Кого задевает

только сам под

все поды на ноде

Автоматическое лечение

да (рестарт)

нет

Утечка памяти - локальная: Kubernetes отрабатывает её рестартом. Утечка диска распределённая, физический диск ноды один на всех, и автоматически его никто не «перезагрузит»

Что такое RSS

Дальше я буду писать «RSS пода = N MiB». Расшифрую сразу, чтобы не отвлекало.

RSS (Resident Set Size) — это сколько физической оперативной памяти процесс реально занимает прямо сейчас. Сюда входит всё: JVM heap, non-heap (метаспейс, code cache), native-аллокации (direct buffers, пулы Netty), стеки потоков, замапленные в память файлы

Виртуальной памяти процесс может «зарезервировать» больше, чем есть физической. Но в RSS считается только то, что реально лежит в RAM. Именно RSS показывает top в одноимённой колонке, и именно RSS Kubernetes сравнивает с limits.memory пода, чтобы решить, не пора ли OOMKill

Чем мы измеряем память: контейнер и JVM

Под «потреблением памяти» в Kubernetes обычно смешивают два разных набора метрик. Разведу их явно.

Метрики контейнера. Собирает cAdvisor, отдаёт в Prometheus. Ключевая — container_memory_working_set_bytes (грубо: RSS пода минус inactive file cache). По ней Kubernetes решает делать OOMKill, и по ней же триггерится наш алерт PodMemoryHigh85 - working_set / limits.memory > 0.85. Нотификация идёт через Alertmanager в Grafana OnCall

Метрики JVM. Spring Boot Actuator + Micrometer на /actuator/prometheus. Полезные: jvm.memory.used{area="heap"}, jvm.memory.used{area="nonheap"} (метаспейс, code cache), jvm.gc.pause. Видна только память, которой управляет JVM. Direct buffers, нативные либы, стеки потоков, OTel-агент для этих метрик невидимы

Если контейнер растёт, а heap стоит, то это утечка в native-памяти, -Xmx тут не поможет

Декорации

  • Spring Boot 3, JDK 21, Apache POI 5.x

  • Сервис отдаёт продавцам XLSX-шаблон с их остатками и принимает обратно заполненный. Файлы бывают большие, поэтому используется SXSSFWorkbook, streaming-вариант POI: он держит в памяти только окно последних N строк, а остальное сбрасывает на диск

  • Прод: Kubernetes, два пода, limits.memory=2Gi, requests.memory=512Mi. /tmp смонтирован на host disk, не tmpfs

Что произошло

В понедельник в 05:41 UTC прилетел PodMemoryHigh85 на одном из подов: RSS выше 85% от limits.memory=2Gi

Состояние пода:

  • uptime 6 суток 4 часа, не рестартился;

  • RSS 1731 MiB (84.5% от 2Gi);

  • JVM heap занимает всего 263 MiB, non-heap 327 MiB. То есть около 1.1 GiB RSS лежит вне JVM;

Тот самый разрыв между метриками контейнера и метриками JVM, про который я писал выше: 1.1 GiB лежит вне JVM, и actuator его не видит. Чтобы понять, кто конкретно ест память, нужен heap dump плюс знание про native-аллокаторы

Я снял heap dump через kubectl port-forward + jcmd, открыл в Eclipse MAT и нашёл главного аллокатора за пределами heap: Netty PoolArena, пул direct buffers для сетевого I/O. 21 арена по 16 MiB, около 336 MiB. Поведение штатное: Netty по умолчанию делает арену на каждый CPU, а в k8s availableProcessors() возвращает 21 ядро ноды. Лечится флагами -Dio.netty.allocator.numDirectArenas=2 -XX:MaxDirectMemorySize=256m. Оформил отдельным action item - вопрос с memory alert закрыт

Но пока копался в поде через kubectl exec, краем глаза заметил странное в du. Вот как выглядит состояние прода прямо сейчас (фикс ещё на ревью, баг живёт):

$ kubectl -n pl-seller-storage-service exec seller-storage-service-...-fpznf -c app -- bash

$ du -sh /tmp/poifiles
1.9G    /tmp/poifiles

$ ls /tmp/poifiles | wc -l
10046

$ ls -lh /tmp/poifiles | head -7
total 1.9G
-rw------- 1 10001 root  564K May 26 13:48 poi-sxssf-sheet10003417958849786613.xml
-rw------- 1 10001 root  607K May 19 11:05 poi-sxssf-sheet10004469281468030521.xml
-rw------- 1 10001 root  248K May 26 18:34 poi-sxssf-sheet10004610036281220997.xml
-rw------- 1 10001 root  142K May 26 20:45 poi-sxssf-sheet10006089183010237705.xml
-rw------- 1 10001 root     0 May 21 16:07 poi-sxssf-sheet10006300192782898050.xml
-rw------- 1 10001 root     0 May 21 05:17 poi-sxssf-sheet10009984612842940696.xml

$ df -h /tmp
Filesystem      Size  Used Avail Use% Mounted on
/dev/vda2       367G  276G   76G  79% /tmp

Почти 2 GiB временных файлов на одном поде, на втором - 10103 файла, тоже 1.9 GiB. Для масштаба: RSS пода в этот момент 1.7 GiB, то есть на диске под держит даже больше, чем занимает в памяти. Файлы датированы от 19 мая (дата старта uptime), часть нулевого размера - следы незакрытых workbook’ов после исключений

Я понаблюдал ещё час: счётчик растёт по мере работы сервиса. Каждый раз, когда пользователь загружает Excel, POI создаёт новые временные файлы и оставляет их на диске. По данным с момента первого разбора это даёт около 800 файлов в сутки на под, или полгигабайта в сутки на ноду от одного нашего сервиса

И последняя строчка вывода df -h /tmp показывает, что диск ноды уже на 79% (76 GiB свободно из 367 GiB). На той же ноде живут чужие сервисы, и часть «работы» по заполнению диска уже на нас. Тут P3 на сервис превращается в потенциальный P1 на инфраструктуру: если на ноде живут ещё десять сервисов и пара из них тоже «протекает», диск кончается за дни и кладёт всех соседей

Почему SXSSFWorkbook.close() не удаляет временные файлы

SXSSFWorkbook это streaming-вариант POI для записи XLSX. Идея простая: вместо того чтобы держать весь документ в памяти, библиотека сохраняет последние N строк в RAM, а более старые сериализует в файлы poi-sxssf-sheet*.xml в java.io.tmpdir. Это даёт возможность писать XLSX на сотни тысяч строк без OOM

Подвох в том, что SXSSFWorkbook реализует AutoCloseable. То есть выглядит, что try-with-resources сделает всё сам, и временные файлы тоже подчистятся. На практике нет

В Javadoc Apache POI это сказано прямо:

dispose() — Dispose of temporary files backing this workbook on disk. Calling this method will render the workbook unusable

close() корректно закрывает базовый workbook, дописывает ZIP-стрим, отдаёт ресурсы. А вот удаление временных sheet-файлов с диска это отдельная операция, dispose(). В POI 5.x внутри close() dispose() вызывается только в определённой ветке: если запись прошла без исключений. А если посреди записи что-то упало (например, IOException на OutputStream), temps остаются. На практике именно это и копится в проде

Наш старый код выглядел как образец try-with-resources:

try (InputStream in = ...;
     XSSFWorkbook template = new XSSFWorkbook(Objects.requireNonNull(in));
     SXSSFWorkbook wb = new SXSSFWorkbook(template, ROW_ACCESS_WINDOW_SIZE)) {

    wb.setCompressTempFiles(true);
    // ... запись XLSX ...
    wb.write(os);
    os.flush();

} catch (IOException e) {
    log.error("XLSX write failed: {}", e.getMessage(), e);
    throw new ServiceException(FILE_GENERATION_FAILED, "xlsx");
}

Фикс

dispose() нужно вызывать явно, в finally, и до того, как закроется обёрнутый XSSFWorkbook-template. Если template закроется первым, dispose() может бросить IllegalStateException, потому что внутри он лезет в template за списком sheet’ов. Поэтому я вытащил SXSSFWorkbook из try-with-resources в обычное объявление и обернул в свой try/finally

@Override
@SuppressWarnings("PMD.CloseResource")
public <T> void write(List<T> records, OutputStream os, Class<T> schemaClass) {
    var xlsxProfile = xlsxProfileResolver.resolve(schemaClass);
    try (InputStream in = Thread.currentThread().getContextClassLoader()
             .getResourceAsStream(xlsxProfile.templatePath());
         XSSFWorkbook template = new XSSFWorkbook(Objects.requireNonNull(in))) {

        SXSSFWorkbook wb = new SXSSFWorkbook(template, ROW_ACCESS_WINDOW_SIZE);
        try {
            wb.setCompressTempFiles(true);
            writeWorkbook(wb, records, schemaClass, xlsxProfile, os);
        } finally {
            disposeQuietly(wb);
        }

    } catch (IOException e) {
        log.error("XLSX write failed: {}", e.getMessage(), e);
        throw new ServiceException(FILE_GENERATION_FAILED, "xlsx");
    }
}

private static void disposeQuietly(SXSSFWorkbook wb) {
    try {
        wb.dispose();
    } catch (Exception e) {
        log.warn("SXSSFWorkbook dispose failed", e);
    }
    try {
        wb.close();
    } catch (IOException e) {
        log.warn("SXSSFWorkbook close failed", e);
    }
}

dispose() идёт первым: он удаляет sheet-temps. close() потом закрывает zip-output. Оба завёрнуты в try/catch с log.warn — если из write() уже летит ServiceException, не хочется маскировать его cleanup-ошибкой

Тест, который ловит регресс

Apache POI даёт удобный хук: TempFile.setTempFileCreationStrategy(...). Через него можно подменить директорию, куда POI создаёт временные файлы. В тесте это позволяет показать на @TempDir и проверить, что после write() там не осталось ничего с префиксом poi-sxssf-sheet

class XlsxStreamingFileServiceTest extends BaseTest {

    private static final String POI_SHEET_PREFIX = "poi-sxssf-sheet";

    @Autowired
    private XlsxStreamingFileService xlsxStreamingFileService;

    @AfterEach
    void restorePoiTempStrategy() {
        TempFile.setTempFileCreationStrategy(
            new DefaultTempFileCreationStrategy(new File(System.getProperty("java.io.tmpdir")))
        );
    }

    @Test
    void writeShouldDisposePoiTempFilesAfterSuccess(@TempDir Path poiTempDir) throws IOException {
        TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(poiTempDir.toFile()));
        var records = List.of(/* ... */);

        xlsxStreamingFileService.write(records, new ByteArrayOutputStream(), MySchemaDto.class);

        assertNoPoiSheetFilesLeft(poiTempDir);
    }

    @Test
    void writeShouldDisposePoiTempFilesEvenIfOutputStreamFails(@TempDir Path poiTempDir) throws IOException {
        TempFile.setTempFileCreationStrategy(new DefaultTempFileCreationStrategy(poiTempDir.toFile()));
        var records = List.of(/* ... */);

        try (OutputStream failing = new OutputStream() {
            @Override public void write(int b) throws IOException { throw new IOException("boom"); }
        }) {
            assertThatThrownBy(() -> xlsxStreamingFileService.write(records, failing, MySchemaDto.class))
                .isInstanceOf(ServiceException.class);
        }

        assertNoPoiSheetFilesLeft(poiTempDir);
    }

    private static void assertNoPoiSheetFilesLeft(Path poiTempDir) throws IOException {
        try (Stream<Path> walk = Files.walk(poiTempDir)) {
            var leftover = walk
                .filter(Files::isRegularFile)
                .filter(p -> p.getFileName().toString().startsWith(POI_SHEET_PREFIX))
                .toList();
            assertThat(leftover)
                .as("POI streaming temp files must be deleted via SXSSFWorkbook.dispose() after write")
                .isEmpty();
        }
    }
}

Второй тест я считаю важнее первого. Он покрывает кейс, когда OutputStream.write(...) бросает IOException посреди записи. Без dispose() в finally именно этот сценарий и оставлял файлы. На happy path старый код тоже проходил без ошибок, поэтому проблема пряталась именно в exception-ветке

Защита от регрессии через ArchUnit

Тест выше проверяет один конкретный flow. Но SXSSFWorkbook- публичный класс POI, и завтра кто-то использует его в другом месте и снова получит утечку. Это случай для ArchUnit: правило архитектурного уровня закрывает класс проблем целиком, а не один эндпоинт

@ArchTest
static final ArchRule SXSSF_WORKBOOK_USAGE_LIMITED_TO_FILE_PACKAGE =
    noClasses()
        .that().resideOutsideOfPackage("..file..")
        .should().dependOnClassesThat()
        .haveFullyQualifiedName("org.apache.poi.xssf.streaming.SXSSFWorkbook")
        .because("SXSSFWorkbook создаёт POI tmp-файлы в системном tmp-dir и требует явного dispose() "
            + "для их удаления; использовать только через сервис в пакете ..file..");

Перевод на русский: никакой класс вне пакета ..file.. не имеет права зависеть от SXSSFWorkbook. Если завтра кто-то напишет new SXSSFWorkbook(...) в другом месте, билд упадёт с понятным сообщением. Проблема решится на ревью за пять минут, а не через шесть суток uptime на проде

Логика общая: один раз нашёл нетривиальные грабли в библиотеке, закрепи запрет на уровне архитектуры. Тест на конкретный flow и ArchUnit-правило - два разных уровня защиты, и они дополняют друг друга, а не дублируют

Что бы я сделал по-другому

  • Про устройство /tmp в Kubernetes я узнал, только когда полез копать утечку. По умолчанию emptyDir —-это диск ноды, а tmpfs нужно включать явно через medium: Memory. Стоит закладывать это в Pod spec осознанно, не оставлять «как-нибудь сложится»

  • Алерт на свободное место на диске ноды у нас был, но триггерился на 90%. Для роста 250 MiB в сутки это даёт мало времени на реакцию - стоит понизить порог

  • Полезно иметь отдельную метрику по размеру emptyDir каждого пода (cAdvisor умеет). Тогда утечка ловится отдельно от RSS, и сразу видно, на каком сервисе она копится, а не «диск ноды растёт, ищите виноватого»

Итоги

  • SXSSFWorkbook.close() в Apache POI не удаляет временные sheet-файлы. Это поведение по доке, а не баг библиотеки. Нужен явный dispose() в finally

  • try-with-resources не гарантирует cleanup, если у класса в цепочке «закрытие» и «освобождение ресурсов» - разные операции

  • В Kubernetes /tmp по умолчанию лежит на диске ноды. Утечка диска одного пода ставит под угрозу всех соседей по ноде. Это сильно меняет приоритет таких багов

  • Если нашёл нетривиальный гвоздь в библиотеке, имеет смысл закрепить запрет на уровне ArchUnit. Это сильнее, чем тест на конкретный flow