Главная » Хабрахабр » Насколько эффективна виртуальная файловая система procfs и можно ли ее оптимизировать

Насколько эффективна виртуальная файловая система procfs и можно ли ее оптимизировать

Она — “прекрасный” пример интерфейсов следующих парадигме “все является файлом”. Файловая система proc (в дальнейшем просто procfs) является виртуальной файловой системой, которая предоставляет информацию о процессах. Однако время не стоит на месте, и сейчас серверы обслуживают сотни тысяч, а то и больше процессов одновременно. Procfs была разработана очень давно: во времена, когда серверы в среднем обслуживали несколько десятков процессов, когда открыть файл и вычитать информацию о процессе не было проблемой. В этой статье мы попробуем найти элементы procfs которые можно оптимизировать. В таком контексте идея “открыть файл для каждого процесса, чтобы вычитать интересующие данные” уже не выглядит такой привлекательной, и первое что приходит на ум чтобы ускорить чтение — это получение информации о группе процессов за одну итерацию.

image

Мы видели как подобная проблема была решена для сокетов, и решили сделать что-то похожее на sock-diag интерфейс, но только для procfs. Сама мысль улучшить procfs возникла когда мы обнаружили, что CRIU тратит значительное количество времени просто читая procfs файлы. Строго говоря, никто не знал, как должен выглядеть новый интерфейс, но сомнений в том, что procfs не удовлетворяет текущим требованиям по производительности нет. Конечно мы предполагали, насколько сложно будет поменять давнишний и вполне устоявшийся интерфейс в ядре, убедить сообщество, что игра стоит свеч… и были приятно удивлены количеством людей, которые поддержали создание нового интерфейса. В этой статье мы не будем рассматривать какой-то конкретный новый интерфейс, скорее попробуем описать проблемы и пути их решения. Например такой сценарий: сервер отвечает на запросы слишком долго, vmstat показывает, что память ушла в своп, а запуск “ps ax” выполняется от 10 секунд и более, top и вовсе ничего не показывает.

Поддиректории группируют данные по признакам. Каждый исполняющийся процесс procfs представляет директорией /proc/<pid>.
В каждой такой директории множество файлов и поддиректорий, которые предоставляют доступ к определенной информации о процессе. Например ($$ это специальная переменная оболочки, которая раскрывается в pid — идентификатор текущего процесса):

$ ls -F /proc/$$
attr/ exe@ mounts projid_map status
autogroup fd/ mountstats root@ syscall
auxv fdinfo/ net/ sched task/
cgroup gid_map ns/ schedstat timers
clear_refs io numa_maps sessionid timerslack_ns
cmdline limits oom_adj setgroups uid_map
comm loginuid oom_score smaps wchan
coredump_filter map_files/ oom_score_adj smaps_rollup
cpuset maps pagemap stack
cwd@ mem patch_state stat
environ mountinfo personality statm

Большинство в формате ASCII текста, который легко воспринимается человеком. Все эти файлы выдают данные в разных форматах. Ну почти легко:

$ cat /proc/$$/stat
24293 (bash) S 21811 24293 24293 34854 24876 4210688 6325 19702 0 10 15 7 33 35 20 0 1 0 47892016 135487488 3388 18446744073709551615 94447405350912 94447406416132 140729719486816 0 0 0 65536 3670020 1266777851 1 0 0 17 2 0 0 0 0 0 94447408516528 94447408563556 94447429677056 140729719494655 140729719494660 140729719494660 140729719496686 0

Например, второй элемент — это имя исполняемого файла в скобках, а девятнадцатый элемент — это текущее значение приоритета исполнения (nice). Чтобы понять, что значит каждый элемент этого множества, читателю придется открыть man proc(5), либо документацию ядра.

Некоторые файлы вполне читабельны сами по себе:

$ cat /proc/$$/status | head -n 5
Name: bash
Umask: 0002
State: S (sleeping)
Tgid: 24293
Ngid: 0

Сколько времени нужно ядру чтобы перевести бинарные данные в текстовый формат? Но как часто пользователи читают информацию напрямую из файлов procfs? Насколько удобен такой интерфейс для программ мониторов состояния, и сколько времени они тратят чтобы обработать эти текстовые данные? Какие накладные расходы у procfs? Насколько критична такая медленная реализация в аварийных ситуациях?

Скорее всего, не будет ошибкой сказать, что пользователи предпочитают программы типа top или ps, вместо того, чтобы читать данные из procfs напрямую.

Во-первых, найдем где именно ядро тратит время, чтобы сгенерировать файлы procfs. Для ответа на остальные вопросы проведем несколько экспериментов.

Затем, в каждой из них, нам необходимо открыть файл, прочитать его и закрыть. Чтобы получить определенную информацию у всех процессов в системе, нам придется пройти по директории /proc/ и выбрать все поддиректории, имя которых представлено десятичными цифрами.

Системные вызовы open() и close() сами по себе не дают нам никакой информации, так что их можно отнести к накладным расходам интерфейса procfs. Суммарно мы исполним три системных вызова, причем один из них создаст файловый дескриптор (в ядре файловый дескриптор ассоциируется с набором внутренних объектов, для которых выделяется дополнительная память).

Попробуем просто сделать open() и close() для каждого процесса в системе, но не будем читать содержимое файлов:

$ time ./task_proc_all --noread stat
tasks: 50290 real 0m0.177s
user 0m0.012s
sys 0m0.162s

$ time ./task_proc_all --noread loginuid
tasks: 50289 real 0m0.176s
user 0m0.026s
sys 0m0.145

task-proc-all — небольшая утилита, с кодом которой можно ознакомится по ссылке снизу

Неважно какой именно файл открыть, поскольку реальные данные генерируются только в момент read().

А теперь посмотрим на вывод профилировщика ядра perf:

- 92.18% 0.00% task_proc_all [unknown] - 0x8000 - 64.01% __GI___libc_open - 50.71% entry_SYSCALL_64_fastpath - do_sys_open - 48.63% do_filp_open - path_openat - 19.60% link_path_walk - 14.23% walk_component - 13.87% lookup_fast - 7.55% pid_revalidate 4.13% get_pid_task + 1.58% security_task_to_inode 1.10% task_dump_owner 3.63% __d_lookup_rcu + 3.42% security_inode_permission + 14.76% proc_pident_lookup + 4.39% d_alloc_parallel + 2.93% get_empty_filp + 2.43% lookup_fast + 0.98% do_dentry_open 2.07% syscall_return_via_sysret 1.60% 0xfffffe000008a01b 0.97% kmem_cache_alloc 0.61% 0xfffffe000008a01e - 16.45% __getdents64 - 15.11% entry_SYSCALL_64_fastpath sys_getdents iterate_dir - proc_pid_readdir - 7.18% proc_fill_cache + 3.53% d_lookup 1.59% filldir + 6.82% next_tgid + 0.61% snprintf - 9.89% __close + 4.03% entry_SYSCALL_64_fastpath 0.98% syscall_return_via_sysret 0.85% 0xfffffe000008a01b 0.61% 0xfffffe000008a01e 1.10% syscall_return_via_sysret

Ядро тратит почти 75% времени просто чтобы создать и удалить файловый дескриптор, и около 16% чтобы вывести список процессов.

Нам надо сравнить полученные величины с чем-то. Хотя мы и знаем сколько времени нужно на вызовы open() и close() для каждого процесса, мы пока не можем оценить насколько оно значительно. Обычно, когда надо вывести список процессов, используется утилита ps или top. Попробуем сделать тоже самое с наиболее известными файлами. Они обе читают /proc/<pid>/stat и /proc/<pid>/status для каждого процесса в системе.

Начнем с /proc/<pid>/status — это массивный файл с фиксированным количеством полей:

$ time ./task_proc_all status
tasks: 50283 real 0m0.455s
user 0m0.033s
sys 0m0.417s

- 93.84% 0.00% task_proc_all [unknown] [k] 0x0000000000008000 - 0x8000 - 61.20% read - 53.06% entry_SYSCALL_64_fastpath - sys_read - 52.80% vfs_read - 52.22% __vfs_read - seq_read - 50.43% proc_single_show - 50.38% proc_pid_status - 11.34% task_mem + seq_printf + 6.99% seq_printf - 5.77% seq_put_decimal_ull 1.94% strlen + 1.42% num_to_str - 5.73% cpuset_task_status_allowed + seq_printf - 5.37% render_cap_t + 5.31% seq_printf - 5.25% render_sigset_t 0.84% seq_putc 0.73% __task_pid_nr_ns + 0.63% __lock_task_sighand 0.53% hugetlb_report_usage + 0.68% _copy_to_user 1.10% number 1.05% seq_put_decimal_ull 0.84% vsnprintf 0.79% format_decode 0.73% syscall_return_via_sysret 0.52% 0xfffffe000003201b + 20.95% __GI___libc_open + 6.44% __getdents64 + 4.10% __close

Если же посмотреть профиль более внимательно, то обнаруживается, что 45% времени использовано внутри функций ядра seq_printf, seq_put_decimal_ull. Видно, что только около 60% времени потрачено внутри системного вызова read(). Что вызывает вполне обоснованный вопрос: а действительно ли нам нужен текстовый интерфейс, чтобы вытащить данные из ядра? А значит, конвертирование из бинарного формата в текстовый достаточно затратная операция. И почему утилитам top и ps приходится конвертировать эти текстовые данные обратно в бинарный вид? Как часто пользователи хотят работать с сырыми данными?

Наверное интересно было бы узнать, насколько быстрее был бы вывод, если бы использовались бинарные данные напрямую, и если бы не требовалось три системных вызова.

В 2004 пробовали использовать netlink движок. Попытки создать такой интерфейс уже были.

[0/2][ANNOUNCE] nproc: netlink access to /proc information (https://lwn.net/Articles/99600/) nproc is an attempt to address the current problems with /proc. In
short, it exposes the same information via netlink (implemented for a
small subset).

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

[PATCH 0/15] task_diag: add a new interface to get information about processes (https://lwn.net/Articles/683371/)

Интерфейс task-diag базируется на следующих принципах:

  • Транзакционность: отправил запрос, получил ответ;
  • Формат сообщений в виде netlink (такой же как у sock_diag интерфейса: бинарный и расширяемый);
  • Возможность запросить информацию о множестве процессов в одном вызове;
  • Оптимизированная группировка атрибутов (любой атрибут в группе не должен увеличивать время ответа).

Его интегрировали в утилиты pstools, CRIU, а также David Ahern интегрировал task_diag в perf, в качестве эксперимента. Этот интерфейс был презентован на нескольких конференциях.

Основным предметом обсуждений стал выбор транспорта между ядром и пространством пользователя. Сообщество разработчиков ядра заинтересовалось интерфейсом task_diag. Частично из-за нерешенных проблем в коде самого netlink движка, а частично потому, что многие думают, что интерфейс netlink был разработан исключительно для сетевой подсистемы. Начальная идея использования netlink сокетов была отклонена. Как обычно, оказались и противники данного подхода. Потом было предложено использовать транзакционные файлы внутри procfs, то есть пользователь открывает файл, записывает в него сам запрос, а затем просто читает ответ. Решения, которое понравилось бы всем, пока не найдено.

Давайте сравним производительность task_diag с procfs.

Предположим, что мы хотим запросить идентификаторы процесса и его права. У task_diag движка есть тестовая утилита, которая удачно подходит для наших экспериментов. Ниже приведен вывод для одного процесса:

$ ./task_diag_all one -c -p $$
pid 2305 tgid 2305 ppid 2299 sid 2305 pgid 2305 comm bash
uid: 1000 1000 1000 1000
gid: 1000 1000 1000 1000
CapInh: 0000000000000000
CapPrm: 0000000000000000
CapEff: 0000000000000000
CapBnd: 0000003fffffffff

А теперь для всех процессов в системе, то есть тоже самое, что мы делали для эксперимента с procfs, когда читали файл /proc/pid/status:

$ time ./task_diag_all all -c real 0m0.048s
user 0m0.001s
sys 0m0.046s

05 секунды потребовалось, чтобы получить данные для построения дерева процессов. Всего лишь 0. 177 секунды только на открытие одного файла для каждого процесса, причем без чтения данных. А с procfs требовалось 0.

Вывод perf для task_diag интерфейса:

- 82.24% 0.00% task_diag_all [kernel.vmlinux] [k] entry_SYSCALL_64_fastpath - entry_SYSCALL_64_fastpath - 81.84% sys_read vfs_read __vfs_read proc_reg_read task_diag_read - taskdiag_dumpit + 33.84% next_tgid 13.06% __task_pid_nr_ns + 6.63% ptrace_may_access + 5.68% from_kuid_munged - 4.19% __get_task_comm 2.90% strncpy 1.29% _raw_spin_lock 3.03% __nla_reserve 1.73% nla_reserve + 1.30% skb_copy_datagram_iter + 1.21% from_kgid_munged 1.12% strncpy

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

Посмотрим на вывод perf при чтении информации обо всех процессах в системе:

$ perf trace -s ./task_diag_all all -c -q Summary of events: task_diag_all (54326), 185 events, 95.4% syscall calls total min avg max stddev (msec) (msec) (msec) (msec) (%) --------------- -------- --------- --------- --------- --------- ------ read 49 40.209 0.002 0.821 4.126 9.50% mmap 11 0.051 0.003 0.005 0.007 9.94% mprotect 8 0.047 0.003 0.006 0.009 10.42% openat 5 0.042 0.005 0.008 0.020 34.86% munmap 1 0.014 0.014 0.014 0.014 0.00% fstat 4 0.006 0.001 0.002 0.002 10.47% access 1 0.006 0.006 0.006 0.006 0.00% close 4 0.004 0.001 0.001 0.001 2.11% write 1 0.003 0.003 0.003 0.003 0.00% rt_sigaction 2 0.003 0.001 0.001 0.002 15.43% brk 1 0.002 0.002 0.002 0.002 0.00% prlimit64 1 0.001 0.001 0.001 0.001 0.00% arch_prctl 1 0.001 0.001 0.001 0.001 0.00% rt_sigprocmask 1 0.001 0.001 0.001 0.001 0.00% set_robust_list 1 0.001 0.001 0.001 0.001 0.00% set_tid_address 1 0.001 0.001 0.001 0.001 0.00%

Для procfs нам нужно выполнить более 150000 системных вызовов, чтобы вытащить информацию о всех процессах, а для task_diag — чуть более 50.

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

Для интерфейса task_diag программа отправляет один запрос, чтобы получить все параметры разом:

$ time ./task_diag_all all --cmdline -q real 0m0.096s
user 0m0.006s
sys 0m0.090s

Для оригинального procfs нам необходимо читать /proc//status and /proc//cmdline у каждого процесса:

$ time ./task_proc_all status
tasks: 50278 real 0m0.463s
user 0m0.030s
sys 0m0.427s

$ time ./task_proc_all cmdline
tasks: 50281 real 0m0.270s
user 0m0.028s
sys 0m0.237s

096 против 0. Нетрудно заметить, что task_diag в 7 раз быстрее procfs (0. 46). 27 + 0. Обычно улучшение производительности на несколько процентов уже хороший результат, а тут скорость увеличилась почти на порядок.

Особенно в случае, когда подсистема памяти под сильной нагрузкой. Стоит также упомянуть, что создание внутренних объектов ядра тоже сильно влияет на производительность. Сравним количество созданных объектов для procfs и task_diag:

$ perf trace --event 'kmem:*alloc*' ./task_proc_all status 2>&1 | grep kmem | wc -l
58184
$ perf trace --event 'kmem:*alloc*' ./task_diag_all all -q 2>&1 | grep kmem | wc -l
188

А также надо выяснить сколько создается объектов при запуске простого процесса, например утилиты true:

$ perf trace --event 'kmem:*alloc*' true 2>&1 | wc -l
94

Это одна из причин, почему procfs работает так плохо, когда сильная нагрузка по памяти. Procfs создает в 600 раз больше объектов, чем task_diag. Хотя бы поэтому стоит ее оптимизировать.

Надеемся, что статья привлечёт больше разработчиков к оптимизации состояния procfs подсистемы ядра.

Trevor King, Arnd Bergmann, Eric W. Огромная благодарность David Ahern, Andy Lutomirski, Stephen Hemming, Oleg Nesterov, W. Biederman и многим другим, кто помогал разрабатывать и улучшать task_diag интерфейс.

Спасибо cromer и k001 за помощь в написании этой статьи.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Введение в ptrace или инъекция кода в sshd ради веселья

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

Дайджест свежих материалов из мира фронтенда за последнюю неделю №339 (12 — 18 ноября 2018)

Предлагаем вашему вниманию подборку с ссылками на новые материалы из области фронтенда и около него.     Медиа    |    Веб-разработка    |    CSS    |    Javascript    |    Браузеры    |    Занимательное Медиа • Подкаст «Frontend Weekend» #79 – Олег Поляков об основании CodeDojo и о том, как это стало основным местом работы• Подкаст «Пятиминутка React» ...