Книга: BPF для мониторинга Linux
Назад: 4. Трассировка с помощью BPF
Дальше: 6. Сетевое взаимодействие в Linux и BPF

5. Утилиты BPF

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

В этой главе речь пойдет об инструментах для повседневной работы с BPF. Начнем с описания BPFTool — утилиты командной строки, которая поможет вам получить больше информации о своих программах BPF. Рассмотрим BPFTrace и kubectl-trace, которые позволят более эффективно писать программы BPF с помощью краткого предметно-ориентированного языка (DSL). Наконец, поговорим о eBPF Exporter — проекте с открытым исходным кодом для интеграции BPF с Prometheus.

BPFTool

BPFTool — это утилита ядра для проверки программ и карт BPF. Этот инструмент не устанавливается по умолчанию ни в одном дистрибутиве Linux, к тому же сейчас он в стадии разработки, поэтому нужно скомпилировать версию, которая лучше всего поддерживает ваше ядро Linux. Мы рассмотрим версию BPFTool, распространяемую с версией 5.1 ядра Linux.

В следующих разделах обсудим, как установить BPFTool в вашу систему и использовать его для наблюдения программ и карт BPF с терминала и изменения их поведения.

Установка

Чтобы установить BPFTool, вам необходимо скачать копию исходного кода ядра. В вашем конкретном онлайн-дистрибутиве Linux могут быть некоторые пакеты, но мы расскажем, как установить его из исходного кода — это не слишком сложно.

1. Используйте Git для клонирования хранилища GitHub с помощью команды gitclonehttps://github.com/torvalds/linux.

2. Проверьте конкретный тег версии ядра с помощью gitcheckoutv5.1.

3. В исходном коде ядра перейдите в каталог, где хранится исходный код BPFTool, с помощью cdtools/bpf/bpftool.

4. Скомпилируйте и установите этот инструмент с помощью make&&sudomakeinstall.

Можете убедиться в правильности установки BPFTool, проверив его версию:

# bpftool --version

bpftool v5.1.0

Вывод функциональных возможностей

Одна из основных операций, которую можно выполнять с BPFTool, — это сканирование системы. Так вы сможете узнать, к каким функциям BPF у вас есть доступ. Это очень полезно, если вы не помните версию ядра, версию программы или если у вас включен JIT-компилятор BPF. Чтобы получить ответ на эти и многие другие вопросы, выполните следующую команду:

# bpftool feature

Вы получите длинный вывод с подробной информацией обо всех поддерживаемых в ваших системах функциях BPF. Для краткости покажем его неполную версию:

Scanning system configuration...

bpf() syscall for unprivileged users is enabled

JIT compiler is enabled

...

Scanning eBPF program types...

eBPF program_type socket_filter is available

eBPF program_type kprobe is NOT available

...

Scanning eBPF map types...

eBPF map_type hash is available

eBPF map_type array is available

В этом выводе можно увидеть, что наша система позволяет непривилегированным пользователям выполнять syscallbpf и этот вызов ограничен определенными операциями. Также видно, что JIT включен. Более новые версии ядра включают JIT по умолчанию, и это очень помогает при компиляции BPF-программ. Если в вашей системе он не включен, можете выполнить следующую команду, чтобы сделать это:

# echo 1 > /proc/sys/net/core/bpf_jit_enable

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

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

Инспекция программ BPF

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

Лучшей отправной точкой для изучения способов применения BPFTool является работа с программами проверки того, что функционирует в вашей системе. Для этого вы можете запустить команду bpftoolprogshow. Если используете Systemd в качестве системы инициализации, у вас, вероятно, уже есть несколько программ BPF, загруженных и подключенных к некоторым контрольным группам (поговорим об этом чуть позже). Результат выполнения этой команды будет выглядеть так:

52: cgroup_skb  tag 7be49e3934a125ba

        loaded_at 2019-03-28T16:46:04-0700  uid 0

        xlated 296B  jited 229B  memlock 4096B  map_ids 52,53

53: cgroup_skb  tag 2a142ef67aaad174

        loaded_at 2019-03-28T16:46:04-0700  uid 0

        xlated 296B  jited 229B  memlock 4096B  map_ids 52,53

54: cgroup_skb  tag 7be49e3934a125ba

        loaded_at 2019-03-28T16:46:04-0700  uid 0

        xlated 296B  jited 229B  memlock 4096B  map_ids 54,55

Стоящие слева числа с двоеточием — это идентификаторы программ, мы используем их позже, чтобы выяснить, что представляют собой эти программы. В данном выводе вы также можете увидеть, какие программы запущены в вашей системе. Сейчас система выполняет три программы BPF, подключенные к буферам сокетов контрольных групп. Время запуска программ, скорее всего, будет совпадать с моментом загрузки вашей системы, если они были действительно запущены Systemd. Вы также можете увидеть количество памяти, используемой этими программами в настоящее время, и идентификаторы карт, связанных с ними. Вся эта информация полезна для начала, дальнейшие исследования будут обширнее, так как у нас есть идентификаторы программ.

Вы можете добавить идентификатор программы к предыдущей команде в качестве дополнительного аргумента: bpftoolprogshowid52. При этом BPFTool покажет вам ту же информацию, которую вы видели ранее, но только для программы с идентификатором 52. Таким образом можно отфильтровать ненужную информацию. Эта команда также поддерживает флаг --json для генерации вывода JSON. Вывод JSON очень удобен. Например, такие инструменты, как jq, позволят получить более структурированный вывод данных:

# bpftool prog show --json id 52 | jq

{

  "id": 52,

  "type": "cgroup_skb",

  "tag": "7be49e3934a125ba",

  "gpl_compatible": false,

  "loaded_at": 1553816764,

  "uid": 0,

  "bytes_xlated": 296,

  "jited": true,

  "bytes_jited": 229,

  "bytes_memlock": 4096,

  "map_ids": [

    52,

    53

  ]

}

Вы также можете выполнить более сложные манипуляции и отфильтровать только ту информацию, которая вас интересует. В следующем примере нам нужны только идентификатор программы BPF, ее тип и время загрузки в ядро:

# bpftool prog show --json id 52 | jq -c '[.id, .type, .loaded_at]'

[52,"cgroup_skb",1553816764]

Зная идентификатор программы, вы можете получить дамп всей программы, применяя BPFTool. Это может быть полезно, когда нужно отладить байт-код BPF, сгенерированный компилятором:

# bpftool prog dump xlated id 52

   0: (bf) r6 = r1

   1: (69) r7 = *(u16 *)(r6 +192)

   2: (b4) w8 = 0

   3: (55) if r7 != 0x8 goto pc+14

   4: (bf) r1 = r6

   5: (b4) w2 = 16

   6: (bf) r3 = r10

   7: (07) r3 += -4

   8: (b4) w4 = 4

   9: (85) call bpf_skb_load_bytes#7151872

   ...

Эта программа, которую Systemd загружает в ядро, инспектирует пакетные данные с применением помощника bpf_skb_load_bytes.

Если вы хотите получить более наглядное представление о программе, включая переходы инструкций, используйте ключевое слово visual в этой команде. Выходные данные будут выведены в формате, который вы можете преобразовать в графическое представление с помощью таких инструментов, как dotty, или любой другой программы для отображения графиков:

# bpftool prog dump xlated id 52 visual &> output.out

# dot -Tpng output.out -o visual-graph.png

Визуальное представление для маленькой программы Hello World показано на рис. 5.1.

6868.png 

Рис. 5.1. Визуальное представление программы BPF

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

# sysctl -w kernel.bpf_stats_enabled=1

После включения сбора статистики при запуске BPFTool вы получите данные еще двух счетчиков: общее количество времени, которое ядро потратило на выполнение данной программы (run_time_ns), и сколько раз ядро выполняло программу (run_cnt):

52: cgroup_skb  tag 7be49e3934a125ba  run_time_ns 14397 run_cnt 39

        loaded_at 2019-03-28T16:46:04-0700  uid 0

        xlated 296B  jited 229B  memlock 4096B  map_ids 52,53

Но BPFTool не только обеспечивает проверку работы ваших программ, он также позволяет загружать новые программы в ядро и подключать некоторые из них к сокетам и контрольным группам. Например, мы можем загрузить одну из рассмотренных ранее программ и привязать ее к файловой системе BPF с помощью следующей команды:

# bpftool prog load bpf_prog.o /sys/fs/bpf/bpf_prog

Поскольку программа привязана к файловой системе, она не завершится после запуска и мы сможем увидеть, что она все еще загружена с помощью упомянутой команды show:

# bpftool prog show

52: cgroup_skb  tag 7be49e3934a125ba

        loaded_at 2019-03-28T16:46:04-0700  uid 0

xlated 296B  jited 229B  memlock 4096B  map_ids 52,53

53: cgroup_skb  tag 2a142ef67aaad174

        loaded_at 2019-03-28T16:46:04-0700  uid 0

        xlated 296B  jited 229B  memlock 4096B  map_ids 52,53

54: cgroup_skb  tag 7be49e3934a125ba

        loaded_at 2019-03-28T16:46:04-0700  uid 0

        xlated 296B  jited 229B  memlock 4096B map_ids 54,55

60: perf_event  name bpf_prog tag c6e8e35bea53af79

        loaded_at 2019-03-28T20:46:32-0700  uid 0

        xlated 112B  jited 115B  memlock 4096B

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

Инспекция карт BPF

Помимо предоставления вам доступа для инспекции программ BPF и управления ими, BPFTool может обеспечить доступ к картам BPF, которые используются этими программами. Команда для отображения всех карт и их фильтрации по идентификаторам аналогична команде show, которую вы видели ранее. Вместо того чтобы запрашивать у BPFTool информацию для prog, попросим показать нам информацию для карты:

# bpftool map show

52: lpm_trie  flags 0x1

        key 8B  value 8B  max_entries 1  memlock 4096B

53: lpm_trie  flags 0x1

        key 20B  value 8B  max_entries 1  memlock 4096B

54: lpm_trie  flags 0x1

        key 8B  value 8B  max_entries 1  memlock 4096B

55: lpm_trie  flags 0x1

        key 20B  value 8B  max_entries 1  memlock 4096B

Эти карты соответствуют идентификаторам, которые вы видели прежде в своих программах. Можете фильтровать карты по их идентификатору, так же как фильтровали программы ранее.

Можно использовать BPFTool для создания и обновления карт, а также для вывода списка всех элементов на карте. Для создания новой карты требуется такая же информация, как и при инициализации карты одной из ваших программ. Нужно указать, какой тип карты мы хотим создать, размер ключей и значений и имя карты. Поскольку мы не инициализируем карту одновременно с программой, нужно также привязать ее к файловой системе BPF, чтобы можно было работать с ней позже:

# bpftool map create /sys/fs/bpf/counter

    type array key 4 value 4 entries 5 name counter

Если вы выведете список карт в системе после выполнения этой команды, то увидите новую карту внизу списка:

52: lpm_trie  flags 0x1

        key 8B  value 8B  max_entries 1  memlock 4096B

53: lpm_trie  flags 0x1

        key 20B  value 8B  max_entries 1  memlock 4096B

54: lpm_trie  flags 0x1

        key 8B  value 8B  max_entries 1  memlock 4096B

55: lpm_trie  flags 0x1

        key 20B  value 8B  max_entries 1  memlock 4096B

56: lpm_trie  flags 0x1

        key 8B  value 8B  max_entries 1  memlock 4096B

57: lpm_trie  flags 0x1

        key 20B  value 8B  max_entries 1  memlock 4096B

58: array  name counter flags 0x0

        key 4B  value 4B  max_entries 5  memlock 4096B

После создания карты вы можете обновлять и удалять элементы, как мы это делали в программе BPF.

6886.png

Помните, что вы не можете удалять элементы из массивов фиксированного размера — разрешается только обновить их. Но можно полностью удалить элементы из других карт, например из хеш-карт.

Если вы хотите добавить в карту новый элемент или обновить существу­ющий, используйте команду обновления карты. Как получить идентификатор карты, показано в предыдущем примере:

# bpftool map update id 58 key 1 0 0 0 value 1 0 0 0

Если вы попытаетесь обновить элемент с неверным ключом или значением, BPFTool возвратит ошибку:

# bpftool map update id 58 key 1 0 0 0 value 1 0 0

Error: value expected 4 bytes got 3

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

# bpftool map dump id 58

key: 00 00 00 00  value: 00 00 00 00

key: 01 00 00 00  value: 01 00 00 00

key: 02 00 00 00  value: 00 00 00 00

key: 03 00 00 00  value: 00 00 00 00

key: 04 00 00 00  value: 00 00 00 00

Одна из наиболее мощных возможностей, предлагаемых BPFTool, — то, что вы можете прикреплять предварительно созданные карты к новым программам и заменять карты, которые они будут инициализировать, этими предварительно подобранными картами. Таким образом, вы можете предоставить программам доступ к сохраненным данным с самого начала, даже если не написали программу для чтения карты из файловой системы BPF. Для этого нужно определить карту, которую вы хотите инициализировать при загрузке программы с помощью BPFTool. Можете указать карту по упорядоченному идентификатору, который будет у нее при загрузке программы, например 0 для первой карты, 1 — для второй и т.д. Или по ее имени — обычно это удобнее:

# bpftool prog load bpf_prog.o /sys/fs/bpf/bpf_prog_2 \

    map name counter /sys/fs/bpf/counter

В этом примере мы присоединяем карту, которую только что создали, к новой программе. Карту заменили ее именем, потому что знаем, что программа инициализирует карту с именем counter. Вы также можете использовать позицию индекса карты с ключевым словом idx, например, idx0, если это легче запомнить.

Доступ к картам BPF напрямую из командной строки полезен, когда вам нужно отладить передачу сообщений в режиме реального времени. BPFTool предоставляет вам удобный прямой доступ. Можете применять BPFTool не только для самоанализа программ и карт, но и для извлечения гораздо более обширной информации из ядра. Давайте посмотрим, как получить доступ к определенным интерфейсам.

Инспекция программ, подключенных к определенным интерфейсам

Иногда требуется узнать, какие программы подключены к определенным интерфейсам. BPF может загружать программы, работающие на основе контрольных групп, событий Perf и сетевых пакетов. Подкоманды cgroup, perf и net помогут вам отследить подключения по интерфейсам.

Подкоманда perf перечисляет все программы, связанные с точками трассировки в системе, такими как kprobes, uprobes и tracepoints. Увидеть этот список можно, выполнив bpftoolperfshow.

Подкоманда net перечисляет программы, подключенные к XDP и Traffic Control. Другие подсоединения, такие как фильтры сокетов и программы повторного использования порта, доступны только тогда, когда применен iproute2. Вы можете просмотреть подключения к XDP и TC с помощью bpftoolnetshow, как это делалось для других объектов BPF.

Наконец, подкоманда cgroup перечисляет все программы, прикрепленные к контрольным группам. Она немного отличается от тех, что вы видели раньше. bpftoolcgroupshow требуется путь к контрольной группе, которую вы проверяете. Если вы хотите перечислить все подключения во всех контрольных группах в системе, используйте bpftoolcgrouptree, как показано в примере:

# bpftool cgroup tree

CgroupPath

ID       AttachType      AttachFlags     Name

/sys/fs/cgroup/unified/system.slice/systemd-udevd.service

    5        ingress

    4        egress

/sys/fs/cgroup/unified/system.slice/systemd-journald.service

    3        ingress

    2        egress

/sys/fs/cgroup/unified/system.slice/systemd-logind.service

    7        ingress

    6        egress

Благодаря BPFTool вы можете убедиться, что ваши программы правильно подключены к любому интерфейсу в ядре и быстро предоставляют вам визуальный отчет о контрольных группах, Perf и сетевом интерфейсе.

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

Загрузка команд в пакетном режиме

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

В пакетном режиме запишите команды, которые хотите выполнить, в файл и запускайте их все сразу. Можно сопроводить этот файл комментариями, начиная строки с #. Однако этот режим выполнения не является атомарным. BPFTool выполняет команды построчно, и в случае сбоя одной из них прервет выполнение, оставив систему в том состоянии, в котором она находилась после выполнения последней успешной команды.

Вот краткий пример файла, который может обрабатываться в пакетном режиме:

# Создать новую хеш-карту

map create /sys/fs/bpf/hash_map type hash key 4 value 4 entries 5 name hash_map

# Теперь показать все карты в системе

map show

Сохранив эти команды в файле /tmp/batch_example.txt, вы сможете загрузить его командой bpftoolbatchfile/tmp/batch_example.txt. Когда запустите ее в первый раз, получите вывод, подобный следующему фрагменту:

# bpftool batch file /tmp/batch_example.txt

2: lpm_trie  flags 0x1

        key 8B  value 8B  max_entries 1  memlock 4096B

3: lpm_trie  flags 0x1

        key 20B  value 8B  max_entries 1  memlock 4096B

18: hash  name hash_map  flags 0x0

        key 4B  value 4B  max_entries 5  memlock 4096B

processed 2 commands

Если попытаетесь запустить команду снова, она отработает без вывода, потому что в системе уже есть карта с именем hash_map, так что пакетное выполнение оборвется на первой же строке.

Пакетный режим — одна из наших любимых возможностей BPFTool. Мы рекомендуем хранить пакетные файлы в системе контроля версий, чтобы поделиться ими со своей командой и создать собственный набор утилит. Прежде чем перейти к нашей следующей любимой утилите, давайте посмотрим, как BPFTool может помочь вам лучше понять BPF Type Format.

Отображение информации BTF

BPFTool может отображать информацию о формате типа BPF (BTF) для любого заданного двоичного объекта, если эта информация существует. Как упоминалось в главе 2, BTF аннотирует программные структуры метаданными, чтобы помочь вам отлаживать программы. Например, он может дать вам исходный файл и номера строк для каждой инструкции в программе BPF, когда вы добавляете ключевое слово linum в progdump.

Более поздние версии BPFTool включают новую подкоманду btf, которая поможет вам разобраться в своих программах. Изначальной целью этой команды являлась визуализация типов структуры. Например, bpftoolbtfdumpid54 покажет все типы BFT для программы, загруженной с идентификатором 54.

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

BPFTrace

BPFTrace — язык трассировки высокого уровня для BPF. Он позволяет писать программы BPF с кратким DSL и сохранять их как сценарии, которые для выполнения не требуется компилировать и загружать в ядро вручную. Язык основан на других известных инструментах, таких как awk и DTrace. Если вы знакомы с DTrace и вам всегда не хватало возможности работать с ним в Linux, то BPFTrace станет отличной заменой.

Одним из преимуществ использования BPFTrace над написанием программ непосредственно с помощью BCC или других инструментов BPF является то, что BPFTrace предоставляет множество встроенных функций, которые вам не нужно реализовывать самостоятельно, таких как компоновка информации и создание гистограмм. В то же время возможности языка, который применяет BPFTrace, намного меньше, и он не позволит реализовать сложные программы. В этом разделе мы покажем наиболее важные аспекты языка. Рекомендуем посетить репозиторий BPFTrace в GitHub (https://github.com/iovisor/bpftrace), чтобы подробнее прочитать об этом.

Установка

Установить BPFTrace можно несколькими способами, хотя его разработчики рекомендуют использовать один из готовых пакетов для конкретного дистрибутива Linux. В своем хранилище они поддерживают актуальность документации со всеми вариантами установки и предварительными условия­ми для вашей системы. Вы найдете инструкции в справочнике по установке (oreil.ly/h9Pha).

Справочник по языку

BPFTrace не содержит сложных команд. Синтаксис краток. Можно разделить команды на три раздела: заголовок, блоки действий и окончание. Заголовок — это специальный блок, который BPFTrace выполняет при загрузке программы. Обычно он используется для печати в верхней части вывода какой-то информации, например преамбулы. Точно так же окончание — это специальный блок, который BPFTrace выполняет один раз перед завершением программы. Заголовок и окончание — необязательные разделы программы BPFTrace. Однако она должна иметь хотя бы один блок действий. В них мы указываем зонды, которые хотим отслеживать, и действия, которые выполняем, когда ядро запускает события для этих зондов. Следующий простой пример показывает, как используются эти три раздела:

BEGIN

{

  printf("starting BPFTrace program\n")

}

 

kprobe:do_sys_open

{

  printf("opening file descriptor: %s\n", str(arg1))

}

 

END

{

  printf("exiting BPFTrace program\n")

}

Раздел заголовка всегда помечается ключевым словом BEGIN, а раздел окончания — ключевым словом END. Эти ключевые слова зарезервированы в BPFTrace. Идентификаторы блоков действий определяют зонд, к которому вы хотите присоединить действие BPF. В предыдущем примере мы выводили строку в журнал каждый раз, когда ядро открывало файл.

В предыдущих примерах, помимо определения разделов программы, мы видим несколько дополнительных деталей синтаксиса языка. BPFTrace предоставляет помощники, которые преобразуются в код BPF при компиляции программы. Помощник printf — это оболочка для функции C printf, которая печатает из программы то, что вам нужно. str — встроенный помощник, который переводит указатель C в его строковое представление. Многие функции ядра получают указатели на символы в качестве аргументов, этот помощник переводит данные указатели в строки.

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

Чтобы выполнить этот пример, вы можете сохранить его в файл и запустить BPFTrace с путем к файлу в качестве первого аргумента:

# bpftrace /tmp/example.bt

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

# bpftrace -e "kprobe:do_sys_open { @opens[str(arg1)] = count() }"

Теперь, когда вы немного больше узнали о языке BPFTrace, посмотрим, как его использовать, на примере нескольких сценариев.

Фильтрация

Запуская код из предыдущего примера, вы, вероятно, получаете список файлов, которые система постоянно открывает, пока не будет нажато сочетание клавиш Ctrl+C для выхода из программы. Так происходит потому, что мы указали BPF печатать каждый дескриптор файла, который открывает ядро. Существуют ситуации, когда нужно выполнить блок действий только в определенных условиях. В BPFTrace это называется фильтрацией.

Можете использовать по одному фильтру на каждый блок действий. Они считаются блоками действий, но действие не выполняется, если фильтр возвращает ложное значение. У них также есть доступ к остальной части языка, включая аргументы зондов и помощники. Эти фильтры заключены в две косые черты и стоят после заголовка действия:

kprobe:do_sys_open /str(arg1) == "/tmp/example.bt"/

{

  printf("opening file descriptor: %s\n", str(arg1))

}

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

# bpftrace /tmp/example.bt

Attaching 3 probes...

starting BPFTrace program

opening file descriptor: /tmp/example.bt

opening file descriptor: /tmp/example.bt

opening file descriptor: /tmp/example.bt

^Cexiting BPFTrace program

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

Динамическое отображение

Динамические ассоциации карт — удобная особенность BPFTrace. Карты BPF можно генерировать динамически и использовать для многих операций, которые вы видели в книге. Все ассоциации карт начинаются с символа @, за ним следует имя карты, которую вы хотите создать. Можно также связать элементы обновления в этих картах, присвоив им значения.

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

kprobe:do_sys_open

{

  @opens[str(arg1)] = count()

}

Снова запустив программу, вы получите примерно такой вывод:

# bpftrace /tmp/example.bt

Attaching 3 probes...

starting BPFTrace program

^Cexiting BPFTrace program

 

@opens[/var/lib/snapd/lib/gl/haswell/libdl.so.2]: 1

@opens[/var/lib/snapd/lib/gl32/x86_64/libdl.so.2]: 1

...

@opens[/usr/lib/locale/en.utf8/LC_TIME]: 10

@opens[/usr/lib/locale/en_US/LC_TIME]: 10

@opens[/usr/share/locale/locale.alias]: 12

@opens[/proc/8483/cmdline]: 12

Как видите, BPFTrace печатает содержимое карты, когда останавливает выполнение программы. И, как мы и ожидали, собирает сведения о том, как часто ядро открывает файлы в системе. По умолчанию BPFTrace всегда выводит содержимое каждой карты, которую создает при завершении работы. Вам не нужно указывать, что требуется напечатать карту, — всегда предполагается, что это будет сделано по умолчанию. Вы можете изменить такое поведение, очистив карту внутри блока END с помощью встроенной функции clear. Это работает, потому что печать карт всегда происходит после выполнения завершающего блока.

Динамическое отображение BPFTrace очень удобно. Оно позволяет избежать множества рутинных задач, необходимых для работы с картами, и помогает легко собирать данные.

BPFTrace — это мощный инструмент для выполнения повседневных задач. Его язык сценариев обеспечивает гибкость, достаточную для доступа ко всем аспектам системы, не требуя вручную компилировать и загружать BPF-программы в ядро, что поможет отследить и устранить проблемы в системе с самого начала работы. Обратитесь к справочному руководству в своем репозитории GitHub, чтобы узнать, как воспользоваться всеми встроенными возможностями, такими как автоматические гистограммы и агрегирование трассировки стека.

В следующем разделе рассмотрим, как применять BPFTrace внутри Kuber­netes.

kubectl-trace

kubectl-trace — фантастический плагин для командной строки Kubernetes, kubectl. Он помогает планировать программы BPFTrace в вашем кластере Kubernetes, не требуя устанавливать какие-либо дополнительные пакеты или модули. Это достигается планированием задания Kubernetes с помощью образа контейнера, в котором есть все необходимое для запуска уже установленной программы. Данное изображение называется trace-runner, оно доступно также в общем реестре Docker.

Установка

Необходимо установить kubectl-trace из исходного репозитория, используя набор инструментов Go, потому что его разработчики не предоставляют никаких бинарных пакетов:

go get -u github.com/iovisor/kubectl-trace/cmd/kubectl-trace

Система плагинов kubectl автоматически обнаружит это дополнение после того, как набор инструментов Go скомпилирует программу и пропишет путь к ней. kubectl-trace автоматически загружает образы Docker, необходимые при первом запуске в кластере.

Инспекция узлов Kubernetes

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

Если вы хотите запустить программу BPF на определенном узле, вам нужен правильный идентификатор, чтобы Kubernetes планировал задание в соответствующем месте. После получения этого идентификатора запуск программы аналогичен запуску программ, которые вы видели ранее. Запустим нашу однострочную систему для подсчета открытий файлов:

# kubectl trace run node/node_identifier -e \

  "kprobe:do_sys_open { @opens[str(arg1)] = count() }"

Как видите, программа точно такая же, как и прежде, но мы вводим команду kubectltracerun, чтобы запланировать ее на конкретном узле кластера. Воспользуемся синтаксисом node/..., чтобы сообщить kubectl-trace, что мы нацеливаемся на узел в кластере. Если требуется нацелиться на конкретный модуль, меняем node/ на pod/.

Запуск программы в определенном контейнере требует более длинного синтаксиса. Сначала посмотрим на пример и пройдемся по нему:

# kubectl trace run pod/pod_identifier -n application_name -e <<PROGRAM

uretprobe:/proc/$container_pid/exe:"main.main" {

  printf("exit: %d\n", retval)

}

PROGRAM

В этой команде нужно выделить две интересные вещи. Во-первых, нам требуется имя приложения, запущенного в контейнере, чтобы найти его процесс. В нашем примере это соответствует application_name. Далее захочется использовать имя двоичного файла, который выполняется в контейнере, например nginx или memcached. Обычно контейнеры запускают только один процесс, но это дает дополнительные гарантии того, что мы присоединяем нашу программу к правильному процессу. Вторым аспектом, который стоит выделить, является включение $container_pid в программу BPF. Это не помощник BPFTrace, а заполнитель, который kubectl-trace применяет в качестве замены для идентификатора процесса. Перед запуском программы BPF трассировщик заменяет заполнитель соответствующим идентификатором и присоединяет программу к правильному процессу.

Если вы запускаете Kubernetes в производственной среде, kubectl-trace намного упростит вашу жизнь, когда вам нужно будет проанализировать поведение ваших контейнеров.

В этом и предыдущих разделах мы сосредоточились на инструментах, которые помогут вам более эффективно запускать программы BPF даже в контейнерных средах. В следующем разделе поговорим о хорошем инструменте для интеграции сбора данных из программ BPF с Prometheus — известной системой мониторинга с открытым исходным кодом.

eBPF Exporter

eBPF Exporter — это инструмент, который позволяет вам экспортировать пользовательские метки трассировки BPF в Prometheus. Prometheus — это масштабируемая система мониторинга и оповещения. Одним из ключевых факторов, который отличает Prometheus от других систем мониторинга, является то, что он задействует стратегию извлечения для получения метрик, вместо того чтобы ожидать, что клиент предоставит их ему. Это позволяет пользователям создавать собственные экспортеры, которые могут собирать метрики из любой системы, а Prometheus будет извлекать их по четко определенной схеме API. eBPF Exporter реализует этот API для получения метрик трассировки из программ BPF и их импорта в Prometheus.

Установка

Хотя eBPF Exporter предлагает бинарные пакеты, мы рекомендуем устанавливать его из исходных кодов, потому что часто релизы запаздывают. К тому же сборка из исходных кодов дает вам доступ к новым функциям, созданным на основе современных версий BCC — коллекции компилятора BPF.

Чтобы установить eBPF Exporter из исходных кодов, на вашем компьютере уже должен быть установлен набор инструментов BCC и Go. Выполнив эти требования, вы можете использовать Go для загрузки и сборки двоичного файла:

go get -u github.com/cloudflare/ebpf_exporter/...

Экспорт метрик из BPF

eBPF Exporter настраивается с помощью файлов YAML, в которых вы можете указать метрики, которые хотите собирать из системы, программу BPF, генерирующую их, и то, как их переводить в Prometheus. Когда Prometheus отправляет запрос в eBPF Exporter для получения метрик, этот инструмент преобразует информацию, которую собирают программы BPF, в значения метрик. К счастью, eBPF Exporter объединяет множество программ, собирающих очень полезную информацию из системы, например инструкции, выполняемые в каждом цикле, и частоту обращений в кэш ЦП.

Простой файл конфигурации для eBPF Exporter состоит из трех основных разделов. В первом из них вы определяете метрики, которые Prometheus должен извлечь из системы. Здесь вы переводите данные, собранные в карты BPF, в метрики, понятные Prometheus. Далее приведен пример таких переводов:

programs:

  - name: timers

    metrics:

      counters:

        - name: timer_start_total

          help: Timers fired in the kernel

          table: counts

          labels:

            - name: function

              size: 8

              decoders:

                - name: ksym

Мы определяем метрику timer_start_total, которая собирает частоту запуска таймера ядром. Кроме того, указываем, что хотим получить эту информацию из карты BPF, которая называется counts. Наконец, определяем функцию перевода для ключей карты. Это необходимо, потому что ключи карты обычно являются указателями на информацию, а мы хотим отправить Prometheus реальные имена функций.

Во втором разделе примера описаны зонды, к которым мы хотим присоединить программу BPF. В данном случае требуется отследить запуск таймера, для этого мы используем точку трассировки timer:timer_start:

tracepoints:

  timer:timer_start: tracepoint__timer__timer_start

Здесь мы указываем eBPF Exporter, что хотим присоединить функцию BPF tracepoint__timer__timer_start к конкретной точке трассировки. Посмотрим, как объявить эту функцию:

code: |

  BPF_HASH(counts, u64);

  // Генерирует функцию tracepoint__timer__timer_start

  TRACEPOINT_PROBE(timer, timer_start) {

      counts.increment((u64) args->function);

      return 0;

}

Программа BPF встроена в файл YAML. Это одна из самых нелюбимых нами частей данного инструмента, потому что для YAML особенно важны пробелы, но он работает с небольшими программами, подобными рассматриваемой. eBPF Exporter использует BCC для компиляции программ, поэтому у нас есть доступ ко всем его макросам и помощникам. В предыдущем фрагменте задействован макрос TRACEPOINT_PROBE для генерации последней функции, которую мы прикрепим к точке трассировки tracepoint__timer__timer_start.

Cloudflare задействует eBPF Exporter для мониторинга показателей во всех своих центрах обработки данных. Компания позаботилась о том, чтобы объединить наиболее распространенные метрики, которые вы хотите экспортировать из своих систем. Но, как видите, список довольно легко расширить с помощью новых метрик.

Резюме

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

Есть много других инструментов, которые применяют BPF для аналогичных целей, таких как Cilium и Sysdig, рекомендуем попробовать поработать с ними.

Эта глава и глава 4 касались в основном системного анализа и трассировки, но с BPF вы можете сделать гораздо больше. В дальнейшем углубимся в его сетевые возможности. Мы покажем, как анализировать трафик в любой сети и использовать BPF для управления сообщениями в вашей сети.

Назад: 4. Трассировка с помощью BPF
Дальше: 6. Сетевое взаимодействие в Linux и BPF