Libseccomp: RFE: seccomp_rule_add стал очень медленным, начиная с версии 2.4.0

Созданный на 1 мая 2019  ·  23Комментарии  ·  Источник: seccomp/libseccomp

Привет,
проблема была введена фиксацией ce3dda9a1747cc6a4c044eafe5a2eb653c974919 между версиями 2.3.3 и 2.4.0. Рассмотрим следующий пример: foo.c.zip .
Он добавляет очень большое количество правил. И работает примерно в 100 раз медленнее после вышеупомянутого коммита.

Время выполнения foo.c с использованием версии 2.4.1: 0.448
Время выполнения foo.c с использованием версии 2.3.3: 0.077

Я немного покопался и обнаружил, что db_col_transaction_start() копирует уже существующую коллекцию фильтров и использует arch_filter_rule_add() для дублирования правил фильтрации. Но arch_filter_rule_add() вызывает arch_syscall_translate(), который вызывает arch_syscall_resolve_name(), который работает за O (количество системных вызовов в данной архитектуре). Таким образом, добавление одного правила работает, по крайней мере, в O (количество уже добавленных правил * количество системных вызовов на используемых архитектурах), что, по-моему, очень плохо.
Я подсчитал количество вызовов arch_filter_rule_add() в приведенном выше примере, и оно равно 201152 .

До этой фиксации количество вызовов arch_filter_rule_add() было 896 . Насколько я понял из кода, db_col_transaction_start() также копирует уже существующую коллекцию фильтров и не использует arch_filter_rule_add(). Что дает нам оценку: время добавления правила около O(количество уже добавленных правил + количество системных вызовов на данной архитектуре), что намного лучше.

Однако IMO это не должно быть связано с количеством уже добавленных правил, потому что тогда добавление n правил работает за O (n ^ 2). Но это тема для другого обсуждения, так что это не должно быть проблемой для небольших фильтров или фильтров, генерируемых нечасто.

Почему эта проблема имеет значение?
Некоторым фильтрам требуется выполнение программ PID (например, разрешение потоку отправлять сигналы только самому себе). Таким образом, если ограниченную программу необходимо выполнить значительное количество раз, это становится очень заметным накладным расходом. У меня есть фильтр около 300 правил, а накладные расходы libseccomp составляют около 0,16 с на запуск изолированного процесса (я запускаю процесс десятки раз).

Заранее спасибо за вашу помощь!

enhancement prioritlow

Самый полезный комментарий

Мы наблюдаем тайм-ауты от пользователей из-за этого изменения. Это действительно замедлило работу на порядок.

Все 23 Комментарий

Привет @varqox.

Да, функции распознавателя системных вызовов нуждаются в некотором улучшении, на самом деле, если вы посмотрите на код, вы увидите несколько комментариев, подобных следующему:

/* XXX - plenty of room for future improvement here */

Если вы хотите посмотреть на улучшение этого кода, мы могли бы воспользоваться помощью!

Как упомянул @pcmoore , есть широкие возможности для ускорения _создания_ фильтра seccomp с помощью libseccomp. Ваше исследование выше выявило одну из нескольких областей, которые можно было бы улучшить. Моих пользователей это не беспокоило, поэтому я не заострял на этом внимание.

Что касается производительности _runtime_, в настоящее время я работаю над использованием двоичного дерева для больших фильтров, подобных тому, который вы предоставили в foo.c. Первоначальные результаты с моими внутренними клиентами выглядят многообещающе, но я хотел бы еще раз взглянуть на изменения. См. запрос на включение https://github.com/seccomp/libseccomp/pull/152 .

Хорошо, я вижу, что разрешение системных вызовов можно улучшить, но это не основная причина проблемы. Что, на мой взгляд, является созданием моментального снимка в db_col_transaction_start() . Там вызывается arch_filter_rule_add() , который работает медленно из-за разрешения системного вызова, который разрешается в исходном правиле.

Я вижу это следующим образом: мы хотим продублировать весь набор текущих фильтров (он же struct db_filter) со всеми их правилами, поэтому мы _конструируем_ все фильтры с нуля вместо того, чтобы пользоваться тем, что у нас уже есть, и просто _копируем_ все фильтры. Нам не нужно создавать с нуля, у нас есть полностью собранный фильтр, который мы просто хотим скопировать. Возможно, я что-то упустил, но похоже, что функцию db_col_transaction_start() можно значительно улучшить.

Поскольку все состояние находится во внутренней коллекции libseccomp db, его дублирование не является тривиальной задачей, а воссоздание коллекции из исходных правил намного проще (с точки зрения кода). Отслеживание исходных правил также позволяет нам предлагать возможность «удалить» существующее правило (возможно, в будущем).

Это не означает, что код транзакций нельзя улучшить — определенно можно, — но текущий код таков, каков он есть, по причине, прежде всего, простоты.

Мы наблюдаем тайм-ауты от пользователей из-за этого изменения. Это действительно замедлило работу на порядок.

Еще одна мысль: возможно, мы можем изменить это так, чтобы мы дублировали правила только при запуске транзакции, а не всего дерева, и воссоздавали дерево только при неудачной транзакции. Это не идеально, но это должно вернуть хороший кусок времени.

Нам нужно что-то делать, потому что время запуска для контейнеров и исполнительных процессов сильно падает в производительности и заставляет людей привязываться к 2,3x.

Я не буду далее комментировать _"огромный"_ характер проблемы, эта точка зрения уже высказывалась несколько раз, и я считаю ее относительной и зависящей от варианта использования. Тем не менее, я хотел напомнить всем, что версии libseccomp до версии 2.4 подвержены потенциальной уязвимости, о которой стало известно (issue #139).

Для тех, кто обеспокоен этой проблемой, в настоящее время она отмечена для выпуска v2.5.

Вы сделали рефакторинг, и он оказал «огромное» влияние на производительность в второстепенном выпуске, и вы не помогаете, говоря, что это зависит от варианта использования. Пожалуйста, отнеситесь к этому серьезно, так как люди начнут замечать, как дистрибутивы обновятся до 2.4.

@crosbymichael изменение было не просто рефакторингом, необходимо было исправить проблемы и поддержать изменения в ядре (в первую очередь необходимость поддержки как мультиплексированных, так и прямых системных вызовов, например системных вызовов сокетов на 32-разрядной платформе x86).

Я _не_ отмахиваюсь от этого, я продолжал думать о способах решения этой проблемы (см. мои комментарии выше) и тот факт, что я пометил это как что-то для следующего второстепенного выпуска. На данный момент мне трудно не воспринять ваши комментарии как подстрекательские, и если это не является вашим намерением, я предлагаю быть более осторожным при комментировании в будущем. Если вы недовольны ходом решения этой проблемы, вы всегда можете помочь, отправив патч/PR на проверку.

Обратите внимание на себя и всех, кто рассматривает возможность попытки решить эту проблему ...

Недавно мне напомнили, почему мы делаем то, что делаем в отношении транзакций (копируем все заранее); мы делаем это, потому что нам нужно иметь возможность откатить транзакцию без сбоев. Почему?
Обычная операция seccomp_rule_add() должна сохранять фильтр неповрежденным даже в случае сбоя; если мы теряем транзакцию, состоящую из нескольких частей (например, системные вызовы socket/ipc на x86/s390/s390x/и т. д.) как часть обычного добавления правила, мы ДОЛЖНЫ иметь возможность вернуться к фильтру в начале транзакции в обязательном порядке ( независимо от нагрузки на память и т. д.).

Дублирование дерева без правил по-прежнему будет сложной задачей из-за природы дерева и связей внутри дерева, но мы могли бы выборочно выбирать, когда нам нужно создавать внутреннюю транзакцию, пропуская ее для многих случаях, когда это не нужно.

Я потратил немного больше времени, изучая это, и из-за того, как мы деструктивно модифицируем дерево решений во время добавления правила, я не уверен, что мы сможем избежать добавления правил в транзакцию. Это означает, что вместо того, чтобы искать способы ограничить использование транзакций внутри, нам нужно найти способ их ускорить, к счастью, я думаю, что нашел решение: теневые деревья.

В настоящее время мы строим новое дерево каждый раз, когда создаем новую транзакцию, и отбрасываем ее в случае успеха, что, как мы видели, может быть чрезмерно медленным в некоторых случаях использования. Я думаю, что вместо того, чтобы отбрасывать дерево-дубликат при фиксации, мы пытаемся добавить правило, которое мы только что добавили, в дерево-дубликат (сделав его копией текущего фильтра) и сохранить его как «теневую транзакцию», чтобы ускорить следующий снимок транзакции. Некоторые примечания:

  • db_col_transaction_start() должен попытаться использовать теневую транзакцию, если она присутствует, но если нет, она должна вернуться к текущему поведению.
  • db_col_transaction_abort() должен вести себя так же, как сейчас; это означает, что неудачная транзакция очистит теневую транзакцию (для восстановления фильтра необходимо дерево), но следующая успешная транзакция восстановит тень. Неудачная транзакция должна быть достаточно редкой, чтобы это не было серьезной проблемой.
  • Нам может понадобиться очистить теневую транзакцию для других операций, например, для операций arch/ABI?, но это то, что нам нужно проверить. Несмотря на это, очистка теневой транзакции должна быть тривиальной.
  • Преимущество этого заключается не только в ускорении добавления правил, но и в ускорении транзакций в целом. Сейчас это может быть неважно, но будет полезно, когда мы предоставим пользователям функциональные возможности транзакций (это будет необходимо, если мы когда-нибудь захотим использовать механизм, подобный «залогу» BSD).

Сегодня вечером у меня было немного времени после ужина, поэтому я быстро приступил к реализации идеи теневой транзакции, описанной выше. Код все еще сырой, а мои тесты (ниже) еще более грубые, но похоже, что мы наблюдаем некоторый прирост производительности с этим подходом:

  • Накладные расходы базового теста
# time for i in {0..20000}; do /bin/true; done
real    0m10.479s
user    0m7.641s
sys     0m3.924s
  • Текущая основная ветвь
# time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

real    0m16.303s
user    0m12.584s
sys     0m4.501s
  • Исправлено
time for i in {0..20000}; do ./42-sim-adv_chains > /dev/null; done

real    0m15.021s
user    0m11.540s
sys     0m4.387s

Если мы вычтем накладные расходы на тест, мы увидим увеличение производительности примерно на 20% в этом «тесте», но я ожидаю, что преимущества для сложных наборов фильтров будут лучше (намного лучше?), чем это.

@varqox и/или @crosbymichael, как только я немного подчищу исправления и создам PR, сможете ли вы протестировать это в своей среде?

Мой тестовый пример уже здесь:

Привет,
проблема была введена фиксацией ce3dda9 между версиями 2.3.3 и 2.4.0. Рассмотрим следующий пример: foo.c.zip .
Он добавляет очень большое количество правил. И работает примерно в 100 раз медленнее после вышеупомянутого коммита.

Время выполнения foo.c с использованием версии 2.4.1: 0.448
Время выполнения foo.c с использованием версии 2.3.3: 0.077

Но как только PR будет готов, я смогу протестировать его в своей среде.

Привет @varqox , да, я видел, что вы включили тестовый пример в исходный отчет, но мне больше интересно услышать, как он работает в реальных условиях. Если бы вы могли попробовать PR # 180 и сообщить о результатах, я был бы очень признателен - спасибо!

Привет @pcmoore ,

Спасибо за этот пиар.
Я построил и протестировал ваш PR #180, результат многообещающий для моего теста. Я слежу за этой проблемой, потому что клиенты используют проверку работоспособности докеров и страдают от проблем с производительностью в libseccomp 2.4.x .
В моем тестовом примере производительность этого PR сравнима с libseccomp 2.3.3 . Подробности приведены ниже:

Окружающая обстановка

Виртуальная машина Ubuntu 19.04 (2 ЦП, 2 ГБ памяти) на MacBook Pro (15 дюймов, середина 2015 г.)
Ядро 5.0.0-32-универсальное
Докер СЕ 19.03.2

Прецедент:

Подготовьте 20 контейнеров:

for i in $(seq 1 20)
do
  docker run -d --name bb$i busybox sleep 3d
done

Запустите тест, запустив docker exec во всех контейнерах одновременно.

for i in $(seq 1 20)
do 
  /usr/bin/time -f "%E real" docker exec bb$i true & 
done

Результаты

libseccomp 2.3.3

0:01.05 real
0:01.12 real
0:01.16 real
0:01.20 real
0:01.23 real
0:01.27 real
0:01.31 real
0:01.35 real
0:01.37 real
0:01.38 real
0:01.40 real
0:01.41 real
0:01.40 real
0:01.40 real
0:01.45 real
0:01.46 real
0:01.47 real
0:01.48 real
0:01.48 real
0:01.49 real

libseccomp 2.4.1

0:00.98 real
0:01.63 real
0:01.67 real
0:01.95 real
0:02.55 real
0:02.70 real
0:02.70 real
0:02.96 real
0:03.04 real
0:03.16 real
0:03.17 real
0:03.21 real
0:03.23 real
0:03.27 real
0:03.24 real
0:03.29 real
0:03.27 real
0:03.29 real
0:03.28 real
0:03.27 real

Ваша PR-сборка

0:00.95 real
0:01.12 real
0:01.20 real
0:01.23 real
0:01.28 real
0:01.29 real
0:01.31 real
0:01.37 real
0:01.38 real
0:01.40 real
0:01.43 real
0:01.43 real
0:01.44 real
0:01.45 real
0:01.42 real
0:01.47 real
0:01.48 real
0:01.48 real
0:01.48 real
0:01.50 real

Разные примечания

  • Я установил 2.4.1 в AC_INIT в configure.ac до создания этого PR.
  • Эта пользовательская сборка установлена ​​в /usr/local/lib , я проверил ее, запустив ldd /usr/bin/runc , чтобы убедиться, что пользовательская сборка используется во время теста.
  • Я проводил тест несколько раз, результаты очень маленькие. Поэтому я уверен, что это не случайные результаты.

Это здорово, спасибо за помощь @xinfengliu!

Привет @pcmoore ,
Спасибо за этот пиар.
В моем случае он восстанавливает производительность libseccomp до уровня, сравнимого с v2.3.3.

Результаты

foo.c

g++ foo.c -lseccomp -o foo -O3
for ((i=0; i<10; ++i)); do time ./foo; done 

libseccomp 2.3.3

./foo  0.01s user 0.00s system 98% cpu 0.018 total
./foo  0.02s user 0.00s system 98% cpu 0.020 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.018 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total
./foo  0.02s user 0.00s system 98% cpu 0.018 total
./foo  0.02s user 0.00s system 98% cpu 0.019 total

В среднем: 0.0188 s

libseccomp 2.4.2

./foo  0.19s user 0.00s system 99% cpu 0.195 total
./foo  0.19s user 0.00s system 99% cpu 0.194 total
./foo  0.19s user 0.00s system 99% cpu 0.193 total
./foo  0.19s user 0.00s system 99% cpu 0.196 total
./foo  0.19s user 0.00s system 99% cpu 0.195 total
./foo  0.20s user 0.00s system 99% cpu 0.196 total
./foo  0.19s user 0.00s system 99% cpu 0.194 total
./foo  0.20s user 0.00s system 99% cpu 0.197 total
./foo  0.19s user 0.00s system 99% cpu 0.195 total
./foo  0.19s user 0.00s system 99% cpu 0.194 total

В среднем: 0.1949 s

ПР #180

./foo  0.01s user 0.01s system 98% cpu 0.012 total
./foo  0.01s user 0.00s system 97% cpu 0.013 total
./foo  0.01s user 0.00s system 96% cpu 0.013 total
./foo  0.01s user 0.01s system 97% cpu 0.014 total
./foo  0.01s user 0.00s system 97% cpu 0.012 total
./foo  0.01s user 0.00s system 98% cpu 0.013 total
./foo  0.01s user 0.00s system 98% cpu 0.012 total
./foo  0.01s user 0.00s system 98% cpu 0.013 total
./foo  0.01s user 0.00s system 97% cpu 0.013 total
./foo  0.01s user 0.00s system 97% cpu 0.011 total

В среднем: 0.0126 s

Кажется, что этот PR дает некоторое ускорение по сравнению с v2.3.3 в этом синтетическом тесте.

Сборка и загрузка (от seccomp_init() до seccomp_load()) фильтров в моей песочнице плюс некоторые накладные расходы на инициализацию песочницы

libseccomp 2.3.3

Measured: 0.0052 s
Measured: 0.0040 s
Measured: 0.0046 s
Measured: 0.0042 s
Measured: 0.0038 s
Measured: 0.0038 s
Measured: 0.0039 s
Measured: 0.0036 s
Measured: 0.0042 s
Measured: 0.0044 s
Measured: 0.0036 s
Measured: 0.0037 s
Measured: 0.0044 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0040 s
Measured: 0.0037 s
Measured: 0.0043 s
Measured: 0.0042 s
Measured: 0.0035 s
Measured: 0.0034 s
Measured: 0.0038 s
Measured: 0.0035 s
Measured: 0.0035 s
Measured: 0.0037 s
Measured: 0.0038 s

В среднем: 0.0039 s

libseccomp 2.4.2

Measured: 0.0496 s
Measured: 0.0480 s
Measured: 0.0474 s
Measured: 0.0475 s
Measured: 0.0479 s
Measured: 0.0479 s
Measured: 0.0492 s
Measured: 0.0485 s
Measured: 0.0491 s
Measured: 0.0490 s
Measured: 0.0484 s
Measured: 0.0483 s
Measured: 0.0480 s
Measured: 0.0482 s
Measured: 0.0474 s
Measured: 0.0483 s
Measured: 0.0507 s
Measured: 0.0472 s
Measured: 0.0482 s
Measured: 0.0471 s
Measured: 0.0498 s
Measured: 0.0489 s
Measured: 0.0474 s
Measured: 0.0494 s
Measured: 0.0483 s
Measured: 0.0498 s
Measured: 0.0492 s

В среднем: 0.0466 s

ПР #180

Measured: 0.0058 s
Measured: 0.0059 s
Measured: 0.0054 s
Measured: 0.0046 s
Measured: 0.0059 s
Measured: 0.0048 s
Measured: 0.0045 s
Measured: 0.0051 s
Measured: 0.0052 s
Measured: 0.0053 s
Measured: 0.0048 s
Measured: 0.0048 s
Measured: 0.0045 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0059 s
Measured: 0.0044 s
Measured: 0.0046 s
Measured: 0.0046 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0062 s
Measured: 0.0047 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0044 s
Measured: 0.0044 s

В среднем: 0.0049 s

Хотя в синтетическом тесте PR дает лучшее время, чем v2.3.3, в реальном мире он немного медленнее (возможно, из-за более сложных правил и запуска seccomp_merge() для слияния двух больших фильтров). Тем не менее, он по-прежнему дает примерно десятикратное ускорение по сравнению с v2.4.2.

Спасибо за проверку производительности @varqox! Как только @drakenclimber ответит на последний раунд комментариев (и я исправлю все оставшиеся проблемы, которые он может поднять), мы объединим это.

Ах, неважно, я только что заметил, что @drakenclimber пометил PR как одобренный. Я собираюсь пойти дальше и объединить это сейчас.

Я только что объединил PR # 180, поэтому я думаю, что мы можем пометить это как закрытое, если кто-то заметит какие-либо оставшиеся проблемы с производительностью, не стесняйтесь комментировать и / или повторно открывать. Спасибо всем за терпение и помощь!

@pcmoore , вы планируете скоро выпустить эти изменения?

В настоящее время это часть этапа выпуска libseccomp v2.5, вы можете отслеживать наш прогресс в выпуске v2.5, используя ссылку ниже:

Была ли эта страница полезной?
0 / 5 - 0 рейтинги