Кеш inode ускорил eBPF-агент безопасности почти на 90%

Авторы блога (автор поста и его брат) разрабатывают open source eBPF-агент безопасности bomfather/agent, который на основе LSM-хука на открытие файла проверяет, действует ли для файла политика доступа (разрешить/запретить). Профилирование показало: самое дорогое в агенте, не само применение политики, а поиск того, какая политика вообще относится к файлу. Политики привязаны к путям, поэтому при каждом открытии файла агент восстанавливает путь и обходит родительские dentry вверх по дереву, проверяя на каждом уровне совпадение с политикой. Для файлов, к которым обращаются многократно (пример из текста, Postgres, повторно открывающий файлы внутри /var/lib/postgres), эта работа раз за разом повторяется впустую.

Решение, кешировать уже найденную для файла политику по номеру inode. Использовать сами dentry как ключ кеша нельзя: это указатели, а указатели нельзя хранить в eBPF-картах. Поэтому ключом кеша сделали связку из трёх полей: ID пространства имён монтирования (mount namespace), ID точки монтирования (mount ID) и номер inode. Один номер inode без этих двух добавок использовать нельзя: номера inode уникальны только в пределах одного дерева монтирования и могут повторяться в разных деревьях, а mount namespace ID дополнительно защищает от переиспользования кеша в другом пространстве имён. Значение в кеше состоит из access_index (позиции бита политики, политики агент хранит как битовые маски для экономии места) и поля состояния. Кеш реализован как карта BPF_MAP_TYPE_LRU_HASH с лимитом в 10 000 записей: при попадании агент сразу применяет закешированный результат, при промахе выполняет полный ("медленный") обход путей и сохраняет результат в кеш.

В бенчмарке один и тот же файл открывали 200 000 раз. Кеш снизил число циклов ядра с 28 млрд до 3,03 млрд, то самое падение примерно на 90%. По флейм-графам до кеша функция tail_call_security_check занимала 89,2% времени на стеке, is_restricted_filepath, 81,9%, path_check_callback, 63,7%; после добавления кеша доля каждой из двух последних упала примерно до 0,02% и фактически перестала быть заметна на графике.

Отдельно пришлось учесть хардлинки: несколько путей могут указывать на один и тот же inode, и использование кеша в этом случае могло бы дать неверный результат для одного из путей. Решение, не идеальное, а рабочий компромисс: агент читает счётчик ссылок на inode (i_nlink), и если он больше единицы, кеш для этого файла не используется, агент всегда идёт по медленному пути. Это сознательно приносит часть покрытия кеша в жертву ради точности результата. Автор отмечает, что кеш работает полностью "внутри" агента: пользовательские политики менять не нужно, ускорение работает прозрачно.

Ключевые факты

  • eBPF-агент безопасности bomfather/agent проверяет через LSM-хук на открытие файла, действует ли для него политика доступа, и именно этот поиск политики, а не само её применение, оказался самым дорогим шагом
  • Добавили кеш: ключ, тройка (ID пространства монтирования, ID точки монтирования, номер inode), значение, позиция бита политики (access_index) и состояние; реализован как LRU-карта eBPF на 10 000 записей
  • В бенчмарке с 200 000 повторных открытий одного файла число циклов ядра упало с 28 млрд до 3,03 млрд, снижение примерно на 90%
  • До кеша три функции обхода пути занимали 89,2%, 81,9% и 63,7% времени на стеке; после кеша доля двух из них упала примерно до 0,02%
  • Хардлинки (несколько путей на один inode) кеш не использует: если у inode больше одной ссылки (i_nlink != 1), агент всегда идёт по медленному пути, точность результата важнее покрытия кеша

Почему это важно

eBPF позволяет писать агенты безопасности прямо в ядре Linux, но пост показывает: узкое место такого агента редко там, где ожидаешь. Здесь дорогим оказался не сам запрет или разрешение доступа, а поиск подходящей политики, обход пути по dentry при каждом открытии файла. Мемоизация результата этого поиска по номеру inode сократила нагрузку на CPU ядра почти на 90% в бенчмарке с повторными открытиями одного файла.

Кому это важно

Инженерам, которые пишут или эксплуатируют eBPF-агенты безопасности и вообще любой код в горячем пути ядра с path-based политиками (контроль доступа к файлам, LSM-хуки, security-модули). Полезно и шире, всем, кто оптимизирует код с повторяющимися дорогими вычислениями над стабильными по времени данными: сам приём (кеш с составным ключом плюс аккуратная обработка edge case с хардлинками) переносится за пределы eBPF.

Как это применить

Идея, кешировать не сырые данные, а уже вычисленный результат (какая политика применима), да ещё привязать к самому дешёвому стабильному идентификатору, inode, а не к пути или указателю dentry. Ключевая деталь: одного номера inode недостаточно, потому что он уникален только в пределах дерева монтирования, нужно добавлять ID точки монтирования и ID пространства имён монтирования, иначе кеш начнёт путать файлы из разных деревьев или пространств имён. Реализация, LRU-карта eBPF с ограничением на число записей (в тексте, 10 000), что защищает от неограниченного роста кеша.

Можно ли доверять

Кода и цифр можно доверять в рамках того, что показано: проект bomfather/agent открыт на GitHub, а цифры (снижение с 28 до 3,03 млрд циклов ядра, доли функций на стеке 89,2%/81,9%/63,7% и падение до ~0,02%), результат одного конкретного бенчмарка (200 000 открытий одного и того же файла), а не независимого измерения на смешанной реальной нагрузке. Даты работ, версия ядра и тестовое железо в тексте не названы.

Риски и подводные камни

Главный риск такого кеша, неверный результат при переиспользовании ключа: авторы явно разбирают случай хардлинков (несколько путей на один inode) и намеренно отключают кеш для inode со счётчиком ссылок больше единицы, жертвуя частью производительности ради корректности. Эффект в 90% измерен на синтетическом бенчмарке с многократным повторным открытием одного файла, на нагрузке с более разнообразным набором файлов (когда попаданий в LRU-кеш меньше) выигрыш будет ниже, и в тексте нет цифр для такого сценария.

«Мы идём на этот компромисс и теряем часть покрытия кеша, но не считаю это большой проблемой, иметь точный кеш важнее всего.»

— автор поста, о решении отключать кеш для inode с хардлинками