В одной из предыдущих публикаций мы уже затронули проблематику трассировки и профилирования ядра Linux.
Сегодня мы хотели бы вновь вернуться к этой проблематике и подробно поговорить об одном интересном инструменте — трассировщике ядра LTTng, разработанном канадским программистом и исследователем Матьё Денуайе. С его помощью можно получать информацию о событиях как в пространстве ядра, так и в пользовательском пространстве.
LTTng может быть использован, например, для решения следующих задач (список далеко не полный):
- анализ межпроцессного взаимодействия в системе;
- анализ взаимодействия приложений в пользовательском пространстве с ядром;
- измерение времени, затрачиваемого ядром на обслуживание запросов приложений;
- анализ особенностей работы системы под высокими нагрузками.
LTTng уже давно включён в официальные репозитории современных дистрибутивов Linux. Его используют на практике такие компании, как Google, Siemens, Boeing и другие. На русском языке публикаций о нём практически нет.
В этой статье мы рассмотрим принципы работы LTTng и покажем, как осуществляется трассировка ядра с его помощью.
Немного истории
В прошлой статье мы подробно разобрали механизм точек трассировки (tracepoints) и даже привели примеры кода. Но мы ничего не сказали о том, что механизм tracepoints был создан в процессе работы над LTTng.
Ещё в 1999 году сотрудник компании IBM Карим Ягмур (Karim Yaghmour) начал работу над проектом LTT (Linux Trace Toolkit). В основе LTT лежала следующая идея: чтобы статически инструментировать наиболее важные фрагменты в коде ядра и благодаря этому получать информацию о работе системы. Несколько лет спустя эта идея была подхвачена и развита Матьё Денуайе в рамках проекта LTTng (Linux Tracing Tool New Generation). Первый релиз LTTng состоялся в 2005 году.
Слова New Generation используются в названии инструмента не просто так: Денуайе внёс огромный вклад в развитие механизмов трассировки и профилирования Linux. Он добавил статическое инструментирование для важнейших ядерных функций: так получился механизм kernel markers, в результате усовершенствования которого появился хорошо знакомый нам tracepoints. Tracepoints активно используется в LTTng. Именно благодаря этому механизму стало возможным осуществлять трассировку, не создавая дополнительной нагрузки на работу системы.
На материале проделанной работы Денуайе подготовил диссертацию, которая была защищена в 2009 году.
Матьё Денуайе ведёт постоянную работу по усовершенствованию LTTng. Последняя на сегодняшний день стабильная версия 2.7 вышла в свет в октябре 2015 года. Вот-вот должна выйти версия 2.8, которая на текущий момент находится в статусе релиз-кандидата и доступна для скачивания здесь.
Установка
LTTng включен в репозитории большинства современных дистрибутивов Linux, и установить её можно стандартным способом. В новых версиях популярных дистрибутивов (например, в недавно вышедшей Ubuntu 16.04) для установки по умолчанию доступна свежая версия — 2.7:
$ sudo apt-get install lttng-tools lttng-modules-dkms
Если вы используете более старую версию Linux, для установки LTTng 2.7 понадобится добавить PPA-репозиторий:
$ sudo apt-add-repository ppa:lttng/ppa
$ sudo apt-get update
$ sudo apt-get install lttng-tools lttng-modules-dkms
Пакет lttng-tools содержит следующие утилиты:
- babeltrace — утилита для просмотра выводов трассировки в формате CTF (Common Trace Format);
- lttng-sessiond — демон для управления трассировкой;
- lttng-consumerd — демон, собирающий данные и записывающий их в кольцевой буфер;
- lttng-relayd — демон, передающий данные по сети.
В пакет lttng-modules-dkms входят многочисленные модули ядра, с помощью которых осуществляется взаимодействие со встроенными механизмами трассировки и профилирования… Просмотреть их полный список можно с помощью команды
$ lsmod | grep lttng
Все эти модули можно условно разделить на три группы:
- модули для работы с точками трассировки (tracepoints);
- модули для работы с кольцевым буфером;
- модули-пробы, предназначенные для динамической трассировки ядра.
Из официальных репозиториев для установки доступен также пакет LTTng-UST, с помощью которого осуществляется трассировка событий в пользовательском пространстве. В рамках этой статьи мы его рассматривать не будем. Заинтересованных читателей отсылаем к  статье, которая может служить вполне неплохим введением в тему.
Все команды LTTng должны выполняться либо от имени root-пользователя, либо от имени пользователя, включённого в специальную группу tracing (она создаётся автоматически при установке).
Основные понятия: сессии, события и каналы
Процесс взаимодействия всех компонентов LTTng можно представить в виде следующей схемы:
Чтобы лучше понять, как всё это работает, сделаем небольшой теоретический экскурс и разберём содержание основных понятий.
Как уже было сказано выше, трассировка в LTTng осуществляется на основе как статического, так и динамического инструментирования кода.
Во время выполнения инструментированного кода вызывается специальная функция-проба (англ. probе, что можно также перевести как «датчик» или «зонд»), которая считывает статус сессии и записывает информацию о событиях в каналы.
Проясним содержание понятий «сессия» и «канал». Сессией называется процедура трассировки с заданными пользователем параметрами. На рисунке выше показана всего одна сессия, но в LTTng можно запускать и несколько сессий одновременно.Сессию можно в любой момент остановить, изменять её настройки и затем запускать снова.
Для передачи отладочной информации каждая сессия использует каналы. Канал — это набор событий с определёнными характеристиками и дополнительной контекстной информацией. К числе характеристик (более подробно мы расскажем о них ниже) канала относятся: размер буфера, режим трассировки, период очистки буфера.
Зачем нужны каналы? Прежде всего для того, чтобы поддерживать общий кольцевой буфер, в который при трассировки записываются события и откуда их затем забирает демон-приёмник (consumer daemon). Кольцевой буфер в свою очередь подразделяется на многочисленные секции (sub-buffers) одинакового размера. Данные о событиях записываются в секцию, пока она не будет заполнена. После этого запись данных будет продолжена, но уже в другую секцию. Данные из переполненных секций запирает демон-приёмник и сохраняет их на диске (или передаёт по сети).
В идеале ситуаций, когда в секции заполнены и данные записывать некуда, возникать не должно. В реальной практике, однако, такие случаи иногда бывают. В официальной документации прямо сказано, что в LTTng производительность поставлена во главу угла: лучше потерять некоторую часть событий, чем замедлить работу системы. При создании канала можно выбирать один из двух режимов работы при переполнении буфера:
- режим сброса (discard mode) — все новые события не будут записываться до тех пор, пока не освободится одна из секций;
- режим перезаписи (overwrite mode) — самые старые события будут удалены, а на их место будут записываться новые.
Более подробно о настройке каналов можно прочитать здесь.
Трассировка событий
Приступим к изучению LTTng на практике и запустим первую трассировочную сессию.
Просмотреть список доступных для трассировки событий можно так (приводим лишь небольшой фрагмент, на самом деле этот список гораздо больше):
$ lttng list --kernel
Kernel events:
-------------
writeback_nothread (loglevel: TRACE_EMERG (0)) (type: tracepoint)
writeback_queue (loglevel: TRACE_EMERG (0)) (type: tracepoint)
writeback_exec (loglevel: TRACE_EMERG (0)) (type: tracepoint)
writeback_start (loglevel: TRACE_EMERG (0)) (type: tracepoint)
writeback_written (loglevel: TRACE_EMERG (0)) (type: tracepoint)
writeback_wait (loglevel: TRACE_EMERG (0)) (type: tracepoint)
writeback_pages_written (loglevel: TRACE_EMERG (0)) (type: tracepoint)
…….
snd_soc_jack_irq (loglevel: TRACE_EMERG (0)) (type: tracepoint)
snd_soc_jack_report (loglevel: TRACE_EMERG (0)) (type: tracepoint)
snd_soc_jack_notify (loglevel: TRACE_EMERG (0)) (type: tracepoint)
snd_soc_cache_sync (loglevel: TRACE_EMERG (0)) (type: tracepoint)
Попробуем отследить какое-нибудь событие — например, sched_switch. Сначала создадим сессию:
$ lttng create test_session
Session test_session created.
Traces will be written in /user/lttng-traces/test_session-20151119-134747
Итак, сессия создана. Все собранные в ходе этой сессии данные будут записываться в файл /user/lttng-traces/test_session-20151119-134747. Затем активируем интересующее нас событие:
$ lttng enable-event -k sched_switch
Kernel event sched_switch created in channel channel0
Далее выполним:
$ lttng start
Tracing started for session test_session
$ sleep 1
$ lttng stop
Информация обо всех событиях sched_switch будет сохранятся в отдельном файле. Просмотреть данные его содержимое можно так:
$ lttng view
Список событий будет слишком большим. Более того, он будет включать слишком много информации. Попробуем конкретизировать запрос и получить информацию только о событиях, которые имели место при выполнении команды sleep:
$ babeltrace /user/lttng-traces/test_session-20151119-134747 | grep sleep
[13:53:23.278672041] (+0.001249216) luna sched_switch: { cpu_id = 0 }, { prev_comm = "sleep", prev_tid = 10935, prev_prio = 20, prev_state = 1, next_comm = "swapper/0", next_tid = 0, next_prio = 20 }
[13:53:24.278777924] (+0.005448925) luna sched_switch: { cpu_id = 0 }, { prev_comm = "swapper/0", prev_tid = 0, prev_prio = 20, prev_state = 0, next_comm = "sleep", next_tid = 10935, next_prio = 20 }
[13:53:24.278912164] (+0.000134240) luna sched_switch: { cpu_id = 0 }, { prev_comm = "sleep", prev_tid = 10935, prev_prio = 20, prev_state = 0, next_comm = "rcuos/0", next_tid = 8, next_prio = 20 }
Вывод содержит информацию обо всех событиях sched_switch, зарегистрированных в системном ядре за время сессии. Он состоит из нескольких полей. Первое поле — это метка времени, второе — так называемая дельта (количество времени между предыдущим и текущим событием). В поле cpu_id указывается номер CPU, для которого было зарегистрировано событие. Далее следует дополнительная контекстуальная информация.
По завершении трассировки сессию нужно удалить:
$ lttng destroy
Трассировка системных вызовов
Для отслеживания системных вызовов у команды lttng enable-event имеется специальная опция — syscall. В статье, посвящённой ftrace, мы разбирали пример с отслеживанием системного вызова chroot. Попробуем проделать то же самое с помощью LTTng:
$ lttng create
$ lttng enable-event -k --syscall chroot
$ lttng start
Создадим изолированное окружение с помощью команды chroot, а затем выполним:
$ lttng stop
$ lttng view
[12:05:51.993970181] (+?.?????????) andrei syscall_entry_chroot: { cpu_id = 0 }, { filename = "test" }
[12:05:51.993974601] (+0.000004420) andrei syscall_exit_chroot: { cpu_id = 0 }, { ret = 0 }
[12:06:53.373062654] (+61.379088053) andrei syscall_entry_chroot: { cpu_id = 0 }, { filename = "test" }
[12:06:53.373066648] (+0.000003994) andrei syscall_exit_chroot: { cpu_id = 0 }, { ret = 0 }
[12:07:36.701313906] (+43.328247258) andrei syscall_entry_chroot: { cpu_id = 1 }, { filename = "test" }
[12:07:36.701325202] (+0.000011296) andrei syscall_exit_chroot: { cpu_id = 1 }, { ret = 0 }
Как видим, вывод содержит информация обо всех входах в системный вызов syscall и выходах из него. По сравнению с выводом ftrace он выглядит несколько более структурированным и человекопонятным.
Динамическая трассировка
Выше мы рассмотрели примеры статической трассировки с использованием механизма tracepoints.
Для отслеживания событий в Linux также можно использовать механизм динамической трассировки kprobes (сокращение от kernel probes, как не трудно догадаться). Он позволяет добавлять новые точки трассировки (пробы) в ядро «на лету». Именно на kprobes основывается известный инструмент SystemTap. Работает он так: чтобы добавить в ядро пробу, нужно написать скрипт на специальном C-подобном языке; затем этот скрипт транслируется в код на C, который компилируется в отдельный модуль ядра. Использовать такой инструмент на практике очень сложно.
Начиная с версии ядра 3.10 поддержка kprobes была добавлена в ftrace. Благодаря этому стала возможной динамическая трассировка без написания скриптов и добавления новых модулей.
Реализована поддержка kprobes и в LTTng. Поддерживается два вида проб: собственно kprobes («базовые» пробы, которые могут быть вставлены в любое место в ядре) и kretprobes (ставятся перед выходом из функции и дают доступ к её результату). Рассмотрим несколько практических примеров,
В LTTng для установки «базовых» проб используется опция, которая так и называется — probe:
$ lttng create
$ lttng enable-event --kernel sys_open --probe sys_open+0x0
$ lttng enable-event --kernel sys_close --probe sys_close+0x0
$ lttng start
$ sleep 1
$ lttng stop
Полученный в результате трассировки вывод будет выглядеть так (приводим небольшой фрагмент):
…..
[12:45:26.842026294] (+0.000028311) andrei sys_close: { cpu_id = 1 }, { ip = 0xFFFFFFFF81209830 }
[12:45:26.842036177] (+0.000009883) andrei sys_open: { cpu_id = 1 }, { ip = 0xFFFFFFFF8120B940 }
[12:45:26.842064681] (+0.000028504) andrei sys_close: { cpu_id = 1 }, { ip = 0xFFFFFFFF81209830 }
[12:45:26.842097484] (+0.000032803) andrei sys_open: { cpu_id = 1 }, { ip = 0xFFFFFFFF8120B940 }
[12:45:26.842126464] (+0.000028980) andrei sys_close: { cpu_id = 1 }, { ip = 0xFFFFFFFF81209830 }
[12:45:26.842141670] (+0.000015206) andrei sys_open: { cpu_id = 1 }, { ip = 0xFFFFFFFF8120B940 }
[12:45:26.842179482] (+0.000037812) andrei sys_close: { cpu_id = 1 }, { ip = 0xFFFFFFFF81209830 }
В приведённом выводе содержится поле ip — адрес отслеживаемоей функции в ядре.
С помощью опции −−function можно выставлять динамические пробы на вход в функцию и выход из неё, например:
$ lttng enable-event call_rcu_sched -k --function call_rcu_sched
.....
[15:31:39.092335027] (+0.000000742) cs31401 call_rcu_sched_return: { cpu_id = 0 }, { }, { ip = 0xFFFFFFFF810E7B10, parent_ip = 0xFFFFFFFF810A206D }
[15:31:39.092398089] (+0.000063062) cs31401 call_rcu_sched_entry: { cpu_id = 0 }, { }, { ip = 0xFFFFFFFF810E7B10, parent_ip = 0xFFFFFFFF810A206D }
[15:31:39.092398883] (+0.000000794) cs31401 call_rcu_sched_return: { cpu_id = 0 }, { }, { ip = 0xFFFFFFFF810E7B10, parent_ip = 0xFFFFFFFF810A206D }
В приведённом выводе присутствует ещё одно поле: parent_ip — адрес функции, которая вызвала отслеживаемую функцию.
В этом разделе мы привели лишь самые простые примеры. Однако возможности LTTng гораздо шире: сочетая различные способы инструментации, с его помощью можно получать такую информацию о работе системы, которую с помощью других инструментов получить сложно или вовсе невозможно. Прочитать об этом подробнее и ознакомиться с интересными примерами можно, например, в этой статье на LWN.net.
Заключение
LTTng — инструмент интересный и перспективный.Чтобы рассказать обо всех его возможностях, одной статьи явно не хватит. Поэтому будем признательны за любые замечания и дополнения.
А если вы используете LTTng на практике, приглашаем поделиться опытом в комментариях.
Для всех, кто хочется познакомиться с LTTng поближе, приводим подборку ссылок на интересные и полезные материалы:
- https://lttng.org/docs/ — официальная документация LTTng;
- http://mdh.diva-portal.org/smash/get/diva2:325301/FULLTEXT01 — исследование эффективности работы LTTng в многоядерном окружении, много интересных технических деталей;
- https://sourceware.org/ml/systemtap/2011-q1/msg00244.html — сравнение производительности LTTng и SystemTap;
- https://events.linuxfoundation.org/slides/2011/lfcs/lfcs2011_tracing_goulet.pdf — презентация о трассировке событий в пользовательском пространстве.
Если вы по тем или иным причинам не можете оставлять комментарии здесь, добро пожаловать в наш корпоративный блог.
Автор: Селектел