Русский
Русский
English
Статистика
Реклама

Ebpf

EBPF современные возможности интроспекции в Linux, или Ядро больше не черный ящик

22.09.2020 14:11:12 | Автор: admin


У всех есть любимые книжки про магию. У кого-то это Толкин, у кого-то Пратчетт, у кого-то, как у меня, Макс Фрай. Сегодня я расскажу вам о моей любимой IT-магии о BPF и современной инфраструктуре вокруг него.

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

В этой статье не будет уникальных открытий. Я просто постараюсь показать, что такое BPF, на что он способен и как может помочь лично вам. Также мы рассмотрим особенности, связанные с Go.

Я бы очень хотел, чтобы после прочтения моей статьи у вас зажглись глаза так, как зажигаются глаза у ребёнка, впервые прочитавшего книгу о Гарри Поттере, чтобы вы пришли домой или на работу и попробовали новую игрушку в деле.

Что такое eBPF?


Итак, о какой такой магии вам тут собирается рассказывать 34-летний бородатый мужик с горящими глазами?

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



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

BIOS, EFI, операционная система, драйвера, модули, библиотеки, сетевое взаимодействие, базы данных, кеши, оркестраторы типа K8s, контейнеры типа Docker, наконец, наш с вами софт с рантаймами и сборщиками мусора. Настоящий профессионал может отвечать на вопрос о том, что происходит после того, как вы вбиваете ya.ru в браузере, несколько дней.

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

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

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

Второй популярный подход изучить метрики. Три самые популярные системы для работы с метриками написаны на Go. Метрики очень полезны, однако, позволяя увидеть симптомы, они не всегда помогают понять причины.

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

Дать возможность менять уровень логирования на лету? Приконнектиться дебаггером к работающей программе и что-то там сделать, не прерывая её работу? Понять, какие запросы поступают в систему, визуализировать источники медленных запросов, посмотреть, на что уходит память через pprof, и получить график её изменения во времени? Замерить latency одной функции и зависимость latency от аргументов? Все эти подходы я отнесу к observability. Это набор утилит, подходов, знаний, опыта, которые вместе дадут вам возможность сделать если не всё что угодно, то очень многое наживую, прямо в работающей системе. Современный швейцарский IT-ножик.



Но как это реализовать? Инструментов на рынке было и есть много: простых, сложных, опасных, медленных. Но тема сегодняшней статьи BPF.

Ядро Linux event-driven-система. Практически всё, что происходит в ядре, да и в системе в целом, можно представить в виде набора событий. Прерывание событие, получение пакета по сети событие, передача процессора другому процессу событие, запуск функции событие.
Так вот, BPF это подсистема ядра Linux, дающая возможность писать небольшие программы, которые будут запущены ядром в ответ на события. Эти программы могут как пролить свет на происходящее в вашей системе, так и управлять ею.

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

В 1994 году появилась первая версия BPF, с которой некоторые из вас наверняка сталкивались, когда писали простые правила для утилиты tcpdump, предназначенной для просмотра, или сниффанья сетевых пакетов. tcpdump можно было задать фильтры, чтобы видеть не все, а только интересующие вас пакеты. Например, только протокол tcp и только порт 80. Для каждого проходящего пакета запускалась функция, чтобы решить, нужно сохранять этот конкретный пакет или нет. Пакетов может быть очень много это значит, что наша функция должна быть очень быстрой. Наши tcpdump фильтры как раз преобразовывались в BPF-функции, пример которой виден на картинке ниже.


Простенький фильтр для tcpdump представлен в виде BPF-программы

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

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



В 2014 году Алексей Старовойтов расширил функциональность BPF. Он увеличил количество регистров и допустимый размер программы, добавил JIT-компиляцию и сделал верификатор, который проверял программы на безопасность. Но самым впечатляющим было то, что новые BPF-программы могли запускаться не только при обработке пакетов, но и в ответ на многочисленные события ядра, и передавали информацию туда-сюда между kernel и user space.

Эти изменения открыли возможности для новых вариантов использования BPF. Некоторые вещи, которые раньше реализовывались путём написания сложных и опасных модулей ядра, теперь делались относительно просто через BPF. Почему это круто? Да потому что любая ошибка при написании модуля часто приводила к панике. Не к пушистой Go-шной панике, а к панике ядра, после которой только ребут.

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

Новая версия BPF от Алексея получила название eBPF (от слова extended расширенная). Но сейчас она заменила все старые версии BPF и стала настолько популярной, что для простоты все называют её просто BPF.

Где используют BPF?


Итак, что же это за события, или триггеры, к которым можно прицепить BPF-программы, и как люди начали использовать новоприобретённую мощь?

На данный момент есть две большие группы триггеров.

Первая группа используется для обработки сетевых пакетов и для управления сетевым трафиком. Это XDP, traffic control-ивенты и ещё несколько.

Эти ивенты нужны, чтобы:

  • Создавать простые, но очень эффективные файрволы. Компании вроде Cloudflare и Facebook с помощью BPF-программ отсеивают огромное количество паразитного трафика и борются с самыми масштабными DDoS-атаками. Так как обработка происходит на самой ранней стадии жизни пакета и прямо в ядре (иногда BPF-программа даже пушится сразу в сетевую карту для обработки), то таким образом можно обрабатывать колоссальные потоки трафика. Раньше такие вещи делали на специализированных сетевых железках.
  • Создавать более умные, точечные, но всё ещё очень производительные файрволы такие, которые могут проверить проходящий трафик на соответствие правилам компании, на паттерны уязвимостей и т. п. Facebook, например, занимается таким аудитом внутри компании, несколько проектов продают такого рода продукты наружу.
  • Создавать умные балансировщики. Самым ярким примером является проект Cilium, который чаще всего используется в кластере K8s в качестве mesh-сети. Cilium управляет трафиком: балансирует, перенаправляет и анализирует его. И всё это с помощью небольших BPF-программ, запускаемых ядром в ответ на то или иное событие, связанное с сетевыми пакетами или сокетами.

Это была первая группа триггеров, связанная с сетевыми вопросами и обладающая возможностью влиять на поведение. Вторая группа связана с более общим observability; программы из этой группы чаще всего не имеют возможности на что-то влиять, а могут только наблюдать. Она интересует меня гораздо больше.

В данной группе есть такие триггеры, как:

  • perf events ивенты, связанные с производительностью и с Linux-профилировщиком perf: железные процессорные счётчики, обработка прерываний, перехват minor/major-исключений памяти и т. п. Например, вы можете поставить обработчик, который будет запускаться каждый раз, когда ядру надо вытащить из свопа какую-то страницу памяти. Представьте себе, например, утилиту, которая отображает программы, которые в данный момент используют своп.
  • tracepoints статические (определённые разработчиком) места в исходниках ядра, при прикреплении к которым можно достать статическую же информацию (ту, которую заранее подготовил разработчик). Может показаться, что в данном случае статичность это плохо, ведь я говорил, что один из недостатков логов заключается в том, что они содержат только то, что изначально добавил программист. В каком-то смысле это так, но tracepoints обладают тремя важными преимуществами:
    • их довольно много раскидано по ядру в самых интересных местах;
    • когда они не включены, они не тратят ресурсы;
    • они являются частью API, стабильны и не меняются, что очень важно, так как другие триггеры, о которых пойдёт речь, не имеют стабильного API.

      В качестве примера можете представить себе утилиту, показывающую программы, которым ядро по той или иной причине не даёт время на выполнение, вы сидите и гадаете, почему она тормозит, а pprof при этом ничего интересного не показывает.

  • USDT то же самое, что tracepoints, только для user space-программ. То есть вы как программист можете добавлять такие места в свою программу. И многие крупные и известные программы и языки программирования уже обзавелись такими трейсами: MySQL, например, или языки PHP, Python. Часто они выключены по умолчанию и для их включения нужно пересобрать интерпретатор с параметром enable-dtrace или подобным. Да, в Go у нас тоже есть возможность регистрировать такие трейсы. Кто-то, может быть, узнал слово DTrace в названии параметра. Дело в том, что такого рода статические трейсы были популяризированы в одноимённой системе, которая зародилась в ОС Solaris: например, момент создания нового треда, запуска GC или чего-то, связанного с конкретным языком или системой.

Ну а дальше начинается ещё один уровень магии:

  • ftrace-триггеры дают нам возможность запускать BPF-программу в начале практически любой функции ядра. Полностью динамически. Это значит, что ядро вызовет вашу BPF-функцию до начала выполнения любой выбранной вами функции ядра. Или всех функций ядра как угодно. Вы можете прикрепиться ко всем функциям ядра и получить на выходе красивую визуализацию всех вызовов.
  • kprobes/uprobes дают почти то же самое, что ftrace, только у нас есть возможность прицепиться к любому месту при исполнении функции как ядра, так и в user space. В середине функции есть какой-то if по переменной и вам надо построить гистограмму значений этой переменной? Не проблема.
  • kretprobes/uretprobes здесь всё аналогично предыдущим триггерам, но мы можем стриггернуться при завершении выполнения функции ядра и программы в user space. Такого рода триггеры удобны для просмотра того, что функция возвращает, и для замера времени её выполнения. Например, можно узнать, какой PID вернул системный вызов fork.

Самое замечательное в этом всём, повторюсь, то, что, будучи вызванной на любой из этих триггеров, наша BPF-программа может хорошенько осмотреться: прочитать аргументы функции, засечь время, прочитать переменные, глобальные переменные, взять стек-трейс, сохранить что-то на потом, передать данные в user space для обработки, получить из user space данные для фильтрации или какие-то управляющие команды. Красота!

Не знаю, как для вас, но для меня новая инфраструктура как игрушка, которую я долго и трепетно ждал.

API, или Как это использовать


Окей, Марко, ты нас уговорил посмотреть в сторону BPF. Но как к нему подступиться?

Давайте посмотрим, из чего состоит BPF-программа и как с ней взаимодействовать.



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

У BPF-программы есть возможность взаимодействовать со второй частью с user space-программой. Для этого есть два способа. Мы можем писать в циклический буфер, а user space-часть может из него читать. Также мы можем писать и читать в key-value-хранилище, которое называется BPF map, а user space-часть, соответственно, может делать то же самое, и, соответственно, они могут перекидывать друг другу какую-то информацию.

Прямолинейный путь


Самый простой способ работы с BPF, с которого ни в коем случае не надо начинать, состоит в написании BPF-программ на подобии языка C и компиляции данного кода с помощью компилятора Clang в код виртуальной машины. Затем мы загружаем этот код с помощью системного вызова BPF напрямую и взаимодействуем с нашей BPF-программой также с помощью системного вызова BPF.



Первое доступное упрощение использование библиотеки libbpf, которая поставляется с исходниками ядра и позволяет не работать напрямую с системным вызовом BPF. По сути, она предоставляет удобные обёртки для загрузки кода, работы с так называемыми мапами для передачи данных из ядра в user space и обратно.

bcc


Понятно, что такое использование далеко от удобного для человека. Благо под брендом iovizor появился проект BCC, который значительно упрощает нам жизнь.



По сути, он готовит всё сборочное окружение и даёт нам возможность писать единые BPF-программы, где С-часть будет собрана и загружена в ядро автоматически, а user space-часть может быть сделана на простом и понятном Python.

bpftrace


Но и BCC выглядит сложным для многих вещей. Особенно люди почему-то не любят писать части на С.

Те же ребята из iovizor представили инструмент bpftrace, который позволяет писать BPF-скрипты на простеньком скриптовом языке а-ля AWK (либо вообще однострочники).



Знаменитый специалист в области производительности и observability Брендан Грегг подготовил следующую визуализацию доступных способов работы с BPF:



По вертикали у нас простота инструмента, а по горизонтали его мощь. Видно, что BCC очень мощный инструмент, но не суперпростой. bpftrace гораздо проще, но при этом уступает в мощности.

Примеры использования BPF


Но давайте рассмотрим магические способности, которые стали нам доступны, на конкретных примерах.

И BCC, и bpftrace содержат папку Tools, где собрано огромное количество готовых интересных и полезных скриптов. Они одновременно являются и местным Stack Overflow, с которого вы можете копировать куски кода для своих скриптов.

Вот, например, скрипт, который показывает latency для DNS-запросов:

 marko@marko-home ~$ sudo gethostlatency-bpfccTIME   PID  COMM         LATms HOST16:27:32 21417 DNS Res~ver #93    3.97 live.github.com16:27:33 22055 cupsd         7.28 NPI86DDEE.local16:27:33 15580 DNS Res~ver #87    0.40 github.githubassets.com16:27:33 15777 DNS Res~ver #89    0.54 github.githubassets.com16:27:33 21417 DNS Res~ver #93    0.35 live.github.com16:27:42 15580 DNS Res~ver #87    5.61 ac.duckduckgo.com16:27:42 15777 DNS Res~ver #89    3.81 www.facebook.com16:27:42 15777 DNS Res~ver #89    3.76 tech.badoo.com :-)16:27:43 21417 DNS Res~ver #93    3.89 static.xx.fbcdn.net16:27:43 15580 DNS Res~ver #87    3.76 scontent-frt3-2.xx.fbcdn.net16:27:43 15777 DNS Res~ver #89    3.50 scontent-frx5-1.xx.fbcdn.net16:27:43 21417 DNS Res~ver #93    4.98 scontent-frt3-1.xx.fbcdn.net16:27:44 15580 DNS Res~ver #87    5.53 edge-chat.facebook.com16:27:44 15777 DNS Res~ver #89    0.24 edge-chat.facebook.com16:27:44 22099 cupsd         7.28 NPI86DDEE.local16:27:45 15580 DNS Res~ver #87    3.85 safebrowsing.googleapis.com^C%

Утилита в режиме реального времени показывает время выполнения DNS-запросов, так что вы сможете поймать, например, какие-то неожиданные выбросы.

А это скрипт, который шпионит за тем, что другие набирают на своих терминалах:

 marko@marko-home ~$ sudo bashreadline-bpfccTIME   PID  COMMAND16:51:42 24309 uname -a16:52:03 24309 rm -rf src/badoo

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

Скрипт для просмотра флоу вызовов высокоуровневых языков:

 marko@marko-home ~/tmp$ sudo /usr/sbin/lib/uflow -l python 20590Tracing method calls in python process 20590... Ctrl-C to quit.CPU PID  TID  TIME(us) METHOD5  20590 20590 0.173  -> helloworld.py.hello5  20590 20590 0.173   -> helloworld.py.world5  20590 20590 0.173   <- helloworld.py.world5  20590 20590 0.173  <- helloworld.py.hello5  20590 20590 1.174  -> helloworld.py.hello5  20590 20590 1.174   -> helloworld.py.world5  20590 20590 1.174   <- helloworld.py.world5  20590 20590 1.174  <- helloworld.py.hello5  20590 20590 2.175  -> helloworld.py.hello5  20590 20590 2.176   -> helloworld.py.world5  20590 20590 2.176   <- helloworld.py.world5  20590 20590 2.176  <- helloworld.py.hello6  20590 20590 3.176  -> helloworld.py.hello6  20590 20590 3.176   -> helloworld.py.world6  20590 20590 3.176   <- helloworld.py.world6  20590 20590 3.176  <- helloworld.py.hello6  20590 20590 4.177  -> helloworld.py.hello6  20590 20590 4.177   -> helloworld.py.world6  20590 20590 4.177   <- helloworld.py.world6  20590 20590 4.177  <- helloworld.py.hello^C%

Здесь на примере показан стек вызовов программы на Python.

Тот же Брендан Грегг сделал картинку, на которой собрал все существующие скрипты со стрелочками, указывающими на те подсистемы, которые каждая утилита позволяет обсёрвить. Как видно, нам уже доступно огромное количество готовых утилит практически на любой случай.


Не пытайтесь что-то здесь углядеть. Картинка используется как справочник

А что у нас с Go?


Теперь давайте поговорим о Go. У нас два основных вопроса:

  • Можно ли писать BPF-программы на Go?
  • Можно ли анализировать программы, написанные на Go?

Пойдём по порядку.

На сегодняшний день единственный компилятор, который умеет компилировать в формат, понимаемый BPF-машиной, Clang. Другой популярный компилятор, GСС, пока не имеет BPF-бэкенда. И единственный язык программирования, который может компилироваться в BPF, очень ограниченный вариант C.

Однако у BPF-программы есть и вторая часть, которая находится в user space. И её можно писать на Go.

Как я уже упоминал выше, BCC позволяет писать эту часть на Python, который является первичным языком инструмента. При этом в главном репозитории BCC также поддерживает Lua и C++, а в стороннем ещё и Go.



Выглядит такая программа точно так же, как программа на Python. В начале строка, в которой BPF-программа на C, а затем мы сообщаем, куда прицепить данную программу, и как-то с ней взаимодействуем, например достаём данные из EPF map.

Собственно, все. Рассмотреть пример подробнее можно на Github.
Наверное, основной недостаток заключается в том, что для работы используется C-библиотека libbcc или libbpf, а сборка Go-программы с такой библиотекой совсем не похожа на милую прогулку в парке.

Помимо iovisor/gobpf, я нашёл ещё три актуальных проекта, которые позволяют писать userland-часть на Go.


Версия от Dropbox не требует никаких C-библиотек, но вот kernel-часть BPF-программы вам придётся собрать самостоятельно с помощью Clang и затем загрузить в ядро Go-программой.

Версия от Cilium имеет те же особенности, что версия от Dropbox. Но она стоит упоминания хотя бы потому, что делается ребятами из проекта Cilium, а значит, обречена на успех.

Третий проект я привёл для полноты картины. Как и два предыдущих, он не имеет внешних C-зависимостей, требует ручной сборки BPF-программы на C, но, похоже, не имеет особых перспектив.

На самом деле, есть ещё один вопрос: зачем вообще писать BPF-программы на Go? Ведь если посмотреть на BCC или bpftrace, то BPF-программы в основном занимают меньше 500 строк кода. Не проще ли написать скриптик на bpftrace-языке или расчехлить немного Python? Я тут вижу два довода.

Первый: вы ну очень любите Go и предпочитаете всё делать на нём. Кроме того, потенциально Go-программы проще переносить с машины на машину: статическая линковка, простой бинарник и всё такое. Но всё далеко не так очевидно, так как мы завязаны на конкретное ядро. Здесь я остановлюсь, а то моя статья растянется еще на 50 страниц.

Второй вариант: вы пишете не простой скриптик, а масштабную систему, которая в том числе использует BPF внутри. У меня даже есть пример такой системы на Go:



Проект Scope выглядит как один бинарник, который при запуске в инфраструктуре K8s или другого облака анализирует всё, что происходит вокруг, и показывает, какие есть контейнеры, сервисы, как они взаимодействуют и т. п. И многое из этого делается с использованием BPF. Интересный проект.

Анализируем программы на Go


Если помните, у нас был ещё один вопрос: можем ли мы анализировать программы, написанные на Go, с помощью BPF? Первая мысль конечно! Какая разница, на каком языке написана программа? Ведь это просто скомпилированный код, который так же, как и все остальные программы, что-то считает на процессоре, кушает память как не в себя, взаимодействует с железом через ядро, а с ядром через системные вызовы. В принципе, это правильно, но есть особенности разного уровня сложности.

Передача аргументов


Одна из особенностей состоит в том, что Go не использует ABI, который использует большинство остальных языков. Так уж получилось, что отцы-основатели решили взять ABI системы Plan 9, хорошо им знакомой.

ABI это как API, соглашение о взаимодействии, только на уровне битов, байтов и машинного кода.

Основной элемент ABI, который нас интересует, то, как в функцию передаются её аргументы и как из функции передаётся обратно ответ. Если в стандартном ABI x86-64 для передачи аргументов и ответа используются регистры процессора, то в Plan 9 ABI для этого использует стек.

Роб Пайк и его команда не планировали делать ещё один стандарт: у них уже был почти готовый компилятор для C для системы Plan 9, простой как дважды два, который они в кратчайшие сроки переделали в компилятор для Go. Инженерный подход в действии.

Но это, на самом деле, не слишком критичная проблема. Во-первых, мы, возможно, скоро увидим в Go передачу аргументов через регистры, а во-вторых, получать аргументы со стека из BPF несложно: в bpftrace уже добавили алиас sargX, а в BCC появится такой же, скорее всего, в ближайшее время.

Upd: с момента, когда я сделал доклад, появился даже подробный официальный proposal по переходу на использование регистров в ABI.

Уникальный идентификатор треда


Вторая особенность связана с любимой фичей Go горутинами. Один из способов измерить latency функций сохранить время вызова функции, засечь время выхода из функции и вычислить разницу; и сохранять время старта с ключом, в котором будет название функции и TID (номер треда). Номер треда нужен, так как одну и ту же функцию могут одновременно вызывать разные программы или разные потоки одной программы.

Но в Go горутины гуляют между системными тредами: сейчас горутина выполняется на одном треде, а чуть позже на другом. И в случае с Go нам бы в ключ положить не TID, а GID, то есть ID горутины, но получить мы его не можем. Чисто технически этот ID существует. Грязными хаками его даже можно вытащить, так как он где-то в стеке, но делать это строго запрещено рекомендациями ключевой группы разработчиков Go. Они посчитали, что такая информация нам не нужна будет никогда. Как и Goroutine local storage, но это я отвлёкся.

Расширение стека


Третья проблема самая серьёзная. Настолько серьёзная, что, даже если мы как-то решим вторую проблему, это нам никак не поможет измерять latency Go-функций.

Наверное, большинство читателей хорошо понимает, что такое стек. Тот самый стек, где в противоположность куче или хипу можно выделять память под переменные и не думать об их освобождении.

Если говорить о C, то стек там имеет фиксированный размер. Если мы вылезем за пределы этого фиксированного размера, то произойдёт знаменитый stack overflow.

В Go стек динамический. В старых версиях он представлял собой связанные куски памяти. Сейчас это непрерывный кусок динамического размера. Это значит, что, если выделенного куска нам не хватит, мы расширим текущий. А если расширить не сможем, то выделим другой большего размера и переместим все данные со старого места на новое. Это чертовски увлекательная история, которая затрагивает гарантии безопасности, cgo, сборщик мусора, но это уже тема для отдельной статьи.

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

Тут и кроется основная проблема: uretprobes, которые используют для прикрепления BPF-функции, к моменту завершения выполнения функции динамически изменяют стек, чтобы встроить вызов своего обработчика, так называемого trampoline. И такое неожиданное для Go изменение его стека в большинстве случаев заканчивается падением программы. Упс!

Впрочем, эта история не уникальна. Разворачиватель стека C++ в момент обработки исключений тоже падает через раз.

Решения у данной проблемы нет. Как обычно в таких случаях, стороны перекидываются абсолютно обоснованными аргументами виновности друг друга.

Но если вам очень нужно поставить uretprobe, то проблему можно обойти. Как? Не ставить uretprobe. Можно поставить uprobe на все места, где мы выходим из функции. Таких мест может быть одно, а может быть 50.

И здесь уникальность Go играет нам на руку.

В обычном случае такой трюк не сработал бы. Достаточно умный компилятор умеет делать так называемый tail call optimization, когда вместо возврата из функции и возврата по стеку вызовов мы просто прыгаем в начало следующей функции. Такого рода оптимизация критически важна для функциональных языков вроде Haskell. Без неё они и шагу бы не могли ступить без stack overflow. Но с такой оптимизацией мы просто не сможем найти все места, где мы возвращаемся из функции.

Особенность в том, что компилятор Go версии 1.14 пока не умеет делать tail call optimization. А значит, трюк с прикреплением ко всем явным выходам из функции работает, хоть и очень утомителен.

Примеры


Не подумайте, что BPF бесполезен для Go. Это далеко не так: все остальное, что не задевает вышеуказанные нюансы, мы делать можем. И будем.
Давайте рассмотрим несколько примеров.

Для препарирования возьмём простенькую программку. По сути, это веб-сервер, который слушает на 8080 порту и имеет обработчик HTTP-запросов. Обработчик достанет из URL параметр name, параметр Go и сделает какую-то проверку сайта, а затем все три переменные (имя, год и статус проверки) отправит в функцию prepareAnswer(), которая подготовит ответ в виде строки.



Проверка сайта это HTTP-запрос, проверяющий, работает ли сайт конференции, с помощью канала и горутины. А функция подготовки ответа просто превращает всё это в читабельную строку.

Триггерить нашу программу будем простым запросом через curl:



В качестве первого примера с помощью bpftrace напечатаем все вызовы функций нашей программы. Мы здесь прикрепляемся ко всем функциям, которые попадают под main. В Go все ваши функции имеют символ, который выглядит как название пакета-точка-имя функции. Пакет у нас main, а рантайм функции был бы runtime.



Когда я делаю curl, то запускаются хендлер, функция проверки сайта и подфункция-горутина, а затем и функция подготовки ответа. Класс!

Дальше я хочу не только вывести, какие функции выполняются, но и их аргументы. Возьмём функцию prepareAnswer(). У неё три аргумента. Попробуем распечатать два инта.
Берём bpftrace, только теперь не однострочник, а скриптик. Прикрепляемся к нашей функции и используем алиасы для стековых аргументов, о которых я говорил.

В выводе мы видим то, что передали мы 2020, получили статус 200, и один раз передали 2021.



Но у функции три аргумента. Первый из них строка. Что с ним?

Давайте просто выведем все стековые аргументы от 0 до 4. И что мы видим? Какая-то большая цифра, какая-то цифра поменьше и наши старые 2021 и 200. Что же это за странные цифры в начале?



Вот здесь уже полезно знать устройство Go. Если в C строка это просто массив символов, который заканчивается нулём, то в Go строка это на самом деле структура, состоящая из указателя на массив символов (кстати, не заканчивающийся нулём) и длины.



Но компилятор Go при передаче строчки в виде аргумента разворачивает эту структуру и передаёт её как два аргумента. И получается, что первая странная цифра это как раз указатель на наш массив, а вторая длина.

И правда: ожидаемая длина строки 22.

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



Ну и заглянем в рантайм. Например, мне захотелось узнать, какие горутины запускает наша программа. Я знаю, что горутины запускаются функциями newproc() и newproc1(). Подконнектимся к ним. Первым аргументом функции newproc1() является указатель на структуру funcval, которая имеет только одно поле указатель на функцию:



В данном случае воспользуемся возможностью прямо в скрипте дефайнить структуры. Это чуть проще, чем играться с оффсетами. Вот мы вывели все горутины, которые запускаются при вызове нашего хендлера. И если после этого получить имена символов для наших оффсетов, то как раз среди них мы увидим нашу функцию checkSite. Ура!



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

Заключение


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

BPF одно из самых модных и перспективных направлений в Linux. И я уверен, что в ближайшие годы мы увидим ещё очень и очень много интересного не только в самой технологии, но и в инструментах и её распространении.

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

Что же до Go, то мы оказались, как обычно, довольно уникальными. Вечно у нас какие-то нюансы: то компилятор другой, то ABI, нужен какой-то GOPATH, имя, которое невозможно загуглить. Но мы стали силой, с которой принято считаться, и я верю, что жизнь станет только лучше.
Подробнее..

Вышел минималистичный Linux-дистрибутив Bottlerocket для запуска контейнеров. Самое главное о нём

11.09.2020 12:04:11 | Автор: admin


Компания Amazon объявила о финальном релизе Bottlerocket специализированного дистрибутива для запуска контейнеров и эффективного управления ими.

Bottlerocket (кстати, так называют мелкие самодельные ракеты на дымном порохе) не первая ОС для контейнеров, но вполне вероятно, что она получит широкое распространение благодаря дефолтной интеграции с сервисами AWS. Хотя система ориентирована на облако Amazon, открытый исходный код позволяет собрать её где угодно: локально на сервере, на Raspberry Pi, в любом конкурирующем облаке и даже в среде без контейнеров.

Это вполне достойная замена дистрибутиву CoreOS, который похоронила Red Hat.

Вообще, у подразделения Amazon Web Services уже есть Amazon Linux, который недавно вышел во второй версии: это дистрибутив общего назначения, который можно запустить в контейнере Docker или с гипервизорами Linux KVM, Microsoft Hyper-V и VMware ESXi. Он был оптимизирован для работы в облаке AWS, но с выходом Bottlerocket всем рекомендуется сделать апгрейд на новую систему, которая более безопасная, современная и потребляет меньше ресурсов.

AWS анонсировала Bottlerocket в марте 2020 года. Она сразу признала, что это не первый Linux для контейнеров, упомянув в качестве источников вдохновения CoreOS, Rancher OS и Project Atomic. Разработчики написали, что операционная система является результатом уроков, которые мы извлекли за долгое время работы производственных служб в масштабе Amazon, и с учётом опыта, который мы получили за последние шесть лет о том, как запускать контейнеры.

Экстремальный минимализм


Linux очищен от всего, что не нужно для запуска контейнеров. Такой дизайн, по словам компании, сокращает поверхность атаки.

Это означает, что в базовой системе устанавливается меньше пакетов, что упрощает поддержку и обновление ОС, а также снижает вероятность возникновения проблем из-за зависимостей, снижает использование ресурсов. В принципе, здесь всё работает внутри отдельных контейнеров, а базовая система практически голая.

Amazon также удалила все оболочки и интерпретаторы, исключив риск их использования или случайного повышения привилегий пользователями. В базовом образе ради минимализма и безопасности отсутствует командная оболочка, сервер SSH и интерпретируемые языки типа Python. Инструменты для администратора вынесены в отдельный служебный контейнер, который отключён по умолчанию.

Управление системой предусмотрено двумя способами: через API и оркестровку.

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

Фреймворк TUF (The Update Framework) загружает обновления на основе образов в альтернативные или размонтированные разделы. Под систему выделяется два дисковых раздела, один из которых содержит активную систему, а на второй копируется обновление. При этом корневой раздел монтируется в режиме только для чтения, а раздел /etc монтируется с файловой системой в оперативной памяти tmpfs и восстанавливает исходное состояние после перезапуска. Прямое изменение конфигурационных файлов в /etc не поддерживается: для сохранения настроек следует использовать API или выносить функциональность в отдельные контейнеры.


Схема обновления через API

Безопасность


Контейнеры создаются штатными механизмами ядра Linux cgroups, пространства имён и seccomp, а в качестве системы принудительного контроля доступа, то есть для дополнительной изоляции используется SELinux в режиме "enforcing".

По умолчанию включены политики для разделения ресурсов между контейнерами и ядром. Бинарники защищены флагами, чтобы пользователи или программы не могли их выполнять. И если кто-то добрался до файловой системы, Bottlerocket предлагает инструмент для проверки и отслеживания любых внесённых изменений.

Режим проверенной загрузки реализован через функцию device-mapper-verity (dm-verity), которая проверяет целостность корневого раздела во время загрузки. AWS описывает dm-verity как функцию ядра Linux, обеспечивающую проверку целостности, чтобы предотвратить работу зловредов в ОС, таких как перезапись основного системного программного обеспечения.

Также в системе присутствует фильтр eBPF (extended BPF, разработка Алексея Старовойтова), который позволяет заменять модули ядра более безопасными программами BPF для низкоуровневых системных операций.

Модель выполнения Задаётся пользователем Компиляция Безопасность Режим сбоя Доступ к ресурсам
Юзер задача да любая права пользователей прерывание выполнения системный вызов, fault
Ядро задача нет статическая нет паника ядра прямой
BPF событие да JIT, CO-RE верификация, JIT сообщение об ошибке ограниченные хелперы

Отличие BPF от обычного кода уровня пользователя или ядра, источник

AWS заявила, что Bottlerocket применяет операционную модель, которая ещё больше повышает безопасность, препятствуя подключению к продакшн-серверам с привилегиями администратора, и подходит для больших распределённых систем, в которых ограничен контроль над каждым отдельным хостом.

Для системных администраторов предусмотрен контейнер администратора. Но AWS не думает, что админу часто придется работать внутри Bottlerocket: Акт входа в отдельный инстанс Bottlerocket предназначен для нечастых операций: расширенной отладки и устранения неполадок, пишут разработчики.

Язык Rust


Инструментарий ОС поверх ядра в основном написан на Rust. Этот язык по своей природе уменьшает вероятность небезопасного доступа к памяти, а также устраняет условия гонки между потоками.

При сборке по умолчанию применяются флаги --enable-default-pie и --enable-default-ssp для включения рандомизации адресного пространства исполняемых файлов (position-independent executable, PIE) и защиты от переполнения стека.

Для пакетов на C/C++ дополнительно включаются флаги -Wall, -Werror=format-security, -Wp,-D_FORTIFY_SOURCE=2, -Wp,-D_GLIBCXX_ASSERTIONS и -fstack-clash-protection.

Кроме Rust и C/C++, некоторые пакеты написаны на языке Go.

Интеграция с сервисами AWS


Отличие от аналогичных контейнерных операционных систем заключается в том, что Amazon оптимизировала Bottlerocket для работы на AWS и интеграции с другими сервисами AWS.

Самым популярным оркестратором контейнеров является Kubernetes, поэтому AWS внедрила интеграцию с собственным Enterprise Kubernetes Service (EKS). Инструменты для оркестровки идут в отдельном управляющем контейнере bottlerocket-control-container, который включён по умолчанию и управляется через API и AWS SSM Agent.

Будет интересно посмотреть, взлетит ли Bottlerocket, учитывая провал некоторых подобных инициатив в прошлом. Например, PhotonOS от Vmware оказалась невостребованной, а RedHat купила CoreOS и закрыла проект, который считался пионером в данной области.

Интеграция Bottlerocket в сервисы AWS делает эту систему в своём роде уникальной. Возможно, это главная причина, почему некоторые пользователи могут предпочесть Bottlerocket другим дистрибутивам, таким как CoreOS или Alpine. Система изначально спроектирована для работы с EKS и ECS, но повторим, что это не обязательно. Во-первых, Bottlerocket можно собрать самостоятельно и использовать, например, как hosted-решение. Во-вторых, пользователи EKS и ECS по-прежнему сохранят возможность выбора ОС.

Исходный код Bottlerocket опубликован на GitHub под лицензией Apache 2.0. Разработчики уже реагируют на баг-репорты и запросы фич.



На правах рекламы


VDSina предлагает VDS с посуточной оплатой. Возможно установить любую операционную систему, в том числе из своего образа. Каждый сервер подключён к интернет-каналу в 500 Мегабит и бесплатно защищён от DDoS-атак!

Подробнее..

Перевод Calico Enterprise обзор

09.02.2021 02:22:17 | Автор: admin

Translation of this article written by John Armstrong on Jan 20, 2021

Вступая в новый год, самое время поразмышлять о достижениях компании Tigera и о том, насколько Calico Enterprise изменилась за последний год и как она стала ведущим решением в сфере безопасности и мониторинга сетей и микросервисов Kubernetes. Опыт работы с пользователями корпоративного класса помог Tigera определить наиболее важные требования пользователей для успешного развертывания кластеров Kubernetes и успешного перехода от пилотных проектов к промышленным проектам. Эти знания помогли Tigera создать систему Calico Enterprise, архитектура которой и представлена ниже. Давайте рассмотрим этот многофункциональный слоёный пирог, снизу вверх.

Архитектура корпоративных решений Calico:

Calico Enterprise является родным для Kube

Cначала следует вспомнить несколько важных вещей. Calico Enterprise - это Kubernetes-native, Kube-native решение, в котором все, что делается, является расширением примитивов Kubernetes. Вся мощь Kubernetes используется путем интеграции с Kubernetes через API плюс путем создания собственного агрегированного API сервера. Операторная модель, взятая целиком из Kubernetes, используется для доступа и управления ресурсами, для выполнения таких функций, как, например, RBAC. Calico Enterprise, будучи родным для Kubernetes, по мере развития Kubernetes автоматически поддерживает взаимно совместимость.

Модель безопасности, работающая всюду

В качестве важнейшего основополагающего элемента Calico Enterprise предоставляет общую модель безопасности, которая работает в гетерогенных средах. На практике это означает, что она работает в любом общедоступном облаке в гибридной или мульти облачной конфигурации, с любым дистрибутивом Kubernetes, с виртуальными машинами и с голым металлом. Большинство крупнейших поставщиков общедоступных облаков и дистрибутивов Kubernetes выбрали решение Tigera и работают совместно, чтобы обеспечить тесную интеграцию с Calico Enterprise.

Почему это важно? Многие из клиентов Calico Enterprise используют стратегию нескольких облаков или гибридного облака (один из клиентов использует аж четыре облака) и в результате выбрали Kubernetes, потому что это абстракция, которая может работать в любой облачной инфраструктуре. А поскольку Calico Enterprise может работать в любом облаке и на любом дистрибутиве, это логичный выбор для облачных архитекторов и команд разработчиков, которым нужно решение, легко адаптирующееся при изменении требований. Многие компании сочетают Kubernetes и не Kubernetes среды и выбрали Calico Enterprise, потому что им требовалось единое решение, работающее в обеих этих средах.

Подключаемый, ориентированный на будущее Data Plane - Linux, Windows и eBPF

Calico Enterprise с самого начала разрабатывался с полностью подключаемым data planes, поэтому клиенты могут выбрать между разными data planes. Сетевой уровень Kubernetes выделяет три уровня data planes, и все они представлены в Calico Enterprise. Большинство пользователей используют стандартный data plane ядра Linux, потому что они еще не используют последние версии Linux, необходимые для поддержки eBPF. Некоторое количество рабочих приложений выполняется в Windows, и для этих клиентов предлагается то же единое решение, которое работает в Linux. Для пользователей, которые хотят расширить пределы производительности с использованием последних ядер Linux, Calico Enterprise предлагает eBPF data plane. В ближайшие несколько лет неизбежно появятся более быстрые data plane технологии, и Calico Enterprise планирует добавить их поддержку. Это еще один способ, которым Tigera защищает будущие инвестиции клиентов Calico Enterprise и демонстрирует приверженность предоставлять наиболее продвинутые, масштабируемые и надежные решения для пользователей Kubernetes.

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

Управление трафиком North-South

Calico Enterprise обеспечивает контроль безопасности как North-South, так и East-West трафика. В среде Kubernetes North-South принято обозначать трафик, который следует внутрь сети и из сети, а East-West - трафик, следующий внутри сети. Именно на стыке North-South между кластером Kubernetes и внешней средой мы сталкиваемся с наибольшими проблемами безопасности. Используя средства Calico Enterprise для управления политикой (policy) DNS и средства контроля входящего / исходящего доступа (ingress/egress access controls), пользователи контролируют North-South трафик. Корпоративный универсальный межсетевой экран Calico и интеграция с SIEM (Calico Enterprise universal firewall and SIEM integration) - это два метода, которые поддерживают интеграцию существующих средств управления безопасностью предприятия со средой Kubernetes.

Управление трафиком East-West

Контроль трафика East-West ограничивает область разрушения в случае нарушений безопасности, которые приводят к APT (advanced persistent threat). Есть несколько способов, с помощью которых Calico Enterprise помогает настроить эти средства контроля. Подход Calico Enterprise с глубокой защитой (defense-in-depth) обеспечивает защиту на трех уровнях: хост, контейнер / виртуальная машина и приложение. Используя единую структуру политики безопасности, вы настраиваете все эти уровни с помощью декларативной модели. Можно установить очень мелкие элементы управления доступом и фильтровать трафик на уровне протокола приложения, например, по протоколам http, https или MongoDB. Можно выполнять микросегментацию как для контейнерных рабочих нагрузок, так и для рабочих нагрузок виртуальных машин.

Безопасность и постоянное соответствие нормативам (Continuous Compliance)

По мере расширения зоны использования Kubernetes наблюдается потребность в еще более глубоком подходе к защите конфиденциальных данных, подпадающих под действие нормативных требований. Для обеспечения безопасности и постоянного соответствия нормативам (Continuous Compliance) Calico Enterprise обеспечивает шифрование данных в пути (data-in-transit encryption) с лучшей в отрасли производительностью, а также непрерывную отчетность о соответствии политик безопасности и средств управления. В Calico Enterprise есть богатый набор функций обнаружения вторжений (Intrusion Detection), который включает обнаружение различных угроз, настраиваемые оповещения об известных атаках, обнаружение аномалий поведения и приманки (honeypod). Calico Enterprise использует автоматизированный подход к обнаружению вредоносных программ и реагированию на них. Например, Tigera создала алгоритм машинного обучения в Calico Enterprise, который специально нацелен на обнаружение DGA (Domain Generation Algorithm), облегчая группам безопасности обнаружение DGA активности. Calico Enterprise может запускать автоматическое исправление, удаляя мошенническую микросервис из сети за миллисекунды, и генерируя затем рекомендации по политике предотвращения возможных атак.

Мониторинг и устранение неисправностей

Время простоя обходится дорого, а мониторинг распределенных приложений очень сложен. Calico Enterprise динамически генерирует диаграмму сервисов (Service Graph), которая позволяет легко понять, как микросервисы ведут себя и взаимодействуют друг с другом во время работы, упрощая тем самым процесс отладки и мониторинга. Динамическая диаграмма сервисов предоставляет очень богатый набор информации, включая информацию о том, в каких пространствах имен взаимодействуют рабочие нагрузки, подробную DNS информацию , подробные журналы событий (flow logs) для каждого отдельного потока в вашем кластере и то, как оцениваются сетевые политики. Горячие точки производительности автоматически идентифицируются и выделяются, а предупреждения (alerts) выдаются в контексте диаграммы сервисов. Пользуясь этой диаграммой, а также с помощью функции автоматического захвата пакетов, инженеры и программисты могут быстро находить источник проблемы на уровне приложения, процесса и сокета.

Единое управление и автоматизация

Унифицированные элементы управления обеспечивают безопасность и наблюдаемость в средах с несколькими кластерами, несколькими облаками и гибридными облачными средами, а также предоставляют единую панель для обеспечения согласованного применения элементов управления безопасностью. Они также поддерживают непрерывную интеграцию CI / CD для опытных пользователей. Calico Enterprise ввела в действие концепцию уровней политик, которые поддерживают делегирование полномочий по организационной структуре и областям ответственности для различных групп с разными потребностями (безопасность, сеть, платформа, DevOps, SRE, разработка). Уровни политик определяют порядок, в котором оцениваются политики сетевой безопасности, и используются для реализации средств управления безопасностью, которые не могут быть изменены или отменены неавторизованными пользователями. Например, средства контроля доступа в масштабе предприятия, созданные группой безопасности, имеют первостепенное значение и находятся на самом высоком уровне. Уровни политик могут объединяться в несколько кластеров с использованием возможностей централизованного управления Calico Enterprise, что позволяет использовать единое управление безопасностью, применяющиеся ко всем кластерам.

Подробнее..

Отлаживаем ядро из командной строки с bpftrace

16.02.2021 00:14:13 | Автор: admin

Это очередная статья из цикла BPF для самых маленьких (0, 1, 2) и первая из серии практических статей про трассировку Linux современными средствами.


Из нее вы узнаете о программе и языке bpftrace самом простом способе погрузиться в мир BPF с практической точки зрения, даже если вы не знаете про BPF ровным счетом ничего. Утилита bpftrace позволяет при помощи простого языка прямо из командной строки создавать программы-обработчики и подсоединять их к огромному количеству событий ядра и пространства пользователя. Посмотрите на КПДВ ниже поздравляю, вы уже умеете трейсить системные вызовы при помощи bpftrace!


В отличие от предыдущих статей серии, эта относительно короткая и ее основная часть написана в формате туториала, так что уже после пяти минут чтения вы сможете создавать обработчики и собирать статистику о любом событии в ядре Linux прямо из командной строки. В конце статьи рассказывается об альтернативах ply и BCC. Минуты во фразе после пяти минут чтения подразумеваются меркурианские. Появление уникальных навыков после пяти минут чтения не гарантируется.



Содержание



Установка bpftrace


Короткая версия. Попробуйте выполнить команду sudo apt install bpftrace (скорректированную под ваш дистрибутив). Если bpftrace установился, то переходите к следующему разделу.


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


При каждом обновлении master-ветки репозитория bpftrace собирается и публикуется новый docker image с упакованным внутри bpftrace. Таким образом, мы можем просто скачать и скопировать бинарник:


$ docker pull quay.io/iovisor/bpftrace:latest$ cd /tmp$ docker run -v $(pwd):/o quay.io/iovisor/bpftrace:latest /bin/bash -c "cp /usr/bin/bpftrace /o"$ sudo ./bpftrace -Vbpftrace v0.11.4

Если bpftrace ругается на слишком старую версию glibc, то вам нужен другой docker image со старой glibc.


Проверим, что программа работает. Для этого запустим пример из КПДВ, который трейсит системный вызов execve(2) и в реальном времени показывает какие программы запускаются в системе, и кем:


$ sudo ./bpftrace -e 'tracepoint:syscalls:sys_enter_execve { printf("%s -> %s\n", comm, str(uptr(args->filename))); }'Attaching 1 probe...bash -> /bin/echobash -> /usr/bin/lsgnome-shell -> /bin/shsh -> /home/aspsk/bin/geeqiesh -> /usr/local/sbin/geeqie...

Наконец, когда мы убедились, что все работает, давайте положим бинарник в правильное место:


$ sudo mv /tmp/bpftrace /usr/local/bin

Если вам этого мало, то можете скачать исходники, запустить чуть больше проверок, собрать свое ядро с поддержкой BTF, а также свою версию bpftrace, при помощи docker или локально. Инструкции по самостоятельной сборке bpftrace см. в официальной документации.


Важная деталь. Если bpftrace и/или ядро собрано без поддержки BTF, то для полноценной работы нужно установить kernel headers. Если вы не знаете как это сделать, то в документации bpftrace есть универсальный дистрибутивонезависимый рецепт.


Мы не будем в этой статье подробно рассказывать про BTF, скажем только, что BTF убирает зависимость от kernel headers, позволяет писать более простые программы, а также расширяет набор событий, к которым мы можем подключиться. Если внешние обстоятельства требуют от вас изучения BTF прямо сейчас, то начните с этой статьи и продолжите этой и этой.


Какие события мы можем трейсить?


Вы сидите в лесу с фотоаппаратом на удобном раскладном стуле. Идет мелкий дождь, но он вам не страшен, ведь шалаш из еловых веток надежно укрывает вас как от дождя, так и от случайных взглядов. Рядом стоит термос с пуэром, йогурт и поднос с копчеными селедками. Но тут вы вдруг задумываетесь: а зачем я сюда пришел и что я буду снимать?! Когда вы просыпаетесь, на лице краснеет слепок клавиатуры, а в терминале написано


$ sudo apt install bpftraceThe following NEW packages will be installed:  bpftracePreparing to unpack .../bpftrace_0.11.0-1_amd64.deb ...Unpacking bpftrace (0.11.0-1) ...Setting up bpftrace (0.11.0-1) ...Processing triggers for man-db (2.9.3-2) ...$

Итак, bpftrace у нас уже установлен. Какие события мы можем инструментировать? Давайте посмотрим на них по очереди, а заодно познакомимся с синтаксисом языка bpftrace. Вот спойлер-оглавление данной секции:



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


Bpftrace: hello world


Язык bpftrace создавался по аналогии с языком awk и поэтому в нем есть два специальных события, BEGIN и END, которые случаются в момент запуска и выхода из bpftrace, соответственно. Вот первая простая программа:


# bpftrace -e 'BEGIN { printf("Hello world!\n"); }'Attaching 1 probe...Hello world!^C

Программа сразу после старта напечатала "Hello world!". Заметьте, что нам пришлось нажимать Ctrl-C, чтобы завершить работу bpftrace это его поведение по умолчанию. Мы можем завершить работу bpftrace из любого события при помощи функции exit. Продемонстрируем это, а заодно добавим и обработку END:


# bpftrace -e ' BEGIN { printf("Hello world!\n"); exit(); } END { printf("So long\n"); } 'Attaching 2 probes...Hello world!So long

Kprobes динамическая инструментация ядра


Ядро это большая программа, функции этой программы, как водится, состоят из инструкций, а механизм ядра под названием kprobes (Kernel Probe ядерный зонд) позволяет нам поставить точку останова практически на любой инструкции, а точнее, по началу конкретной функции или коду внутри нее. В контексте данной статьи нам, вообще говоря, не важно как именно создаются обработчики kprobes, но вы можете узнать об этом из предыдущих статей этой серии, ссылки на которые есть в конце, а также из будущих статей, в которых мы разберем все технические подробности трассировки Linux с BPF.


В качестве примера давайте посмотрим на то, как часто и кем вызывается функция schedule:


$ sudo bpftrace -e '    k:schedule { @[comm] = count(); }    i:s:2 { exit();}    END { print(@, 10); clear(@); }'Attaching 3 probes...@[Timer]: 147@[kworker/u65:0]: 147@[kworker/7:1]: 153@[kworker/13:1]: 158@[IPC I/O Child]: 170@[IPC I/O Parent]: 172@[kworker/12:1]: 185@[Web Content]: 229@[Xorg]: 269@[SCTP timer]: 1566

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


Много ли функций можно потрейсить при помощи кей-проб? Это легко проверить:


$ sudo bpftrace -l 'k:*' | wc -l61106

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


kretprobes


Для каждой kprobe мы можем создать обработчик kretprobe. Если kprobe запускается в момент входа в функцию, то kretporobe запускается в момент выхода из функции. При этом код возврата функции содержится в специальной встроенной переменной retval.


Например, вот что на отрезке в две секунды возвращала функция vfs_write на моей системе (в виде логарифмической гистограммы):


$ sudo bpftrace -e 'kr:vfs_write { @ = hist(retval); } i:s:2 { exit(); }'Attaching 2 probes...@:[1]                  606 |@@@@@@@@@@@@@@@@@@@@@@@@@                           |[2, 4)                 0 |                                                    |[4, 8)                 0 |                                                    |[8, 16)             1223 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[16, 32)               0 |                                                    |[32, 64)              25 |@                                                   |

uprobes и uretprobes


Кроме инструментации функций ядра, мы можем инструментировать каждую программу (и библиотеку), работающую в пространстве пользователя. Реализуется это при помощи тех же kprobes. Для этого в bpftrace определены события uprobes и uretprobes вызов и возврат из функции.


Вот как мы можем подглядывать за тем, что печатают пользователи баша (в квадратных скобках печатается UID пользователя):


$ sudo bpftrace -e 'uretprobe:/bin/bash:readline { printf("readline: [%d]: \"%s\"\n", uid, str(uptr(retval))); }'Attaching 1 probe...readline: [1000]: "echo "hello habr""readline: [0]: "echo "hello from root""^C

Динамическая инструментация ядра, версия 2


Для счастливых обладателей CONFIG_DEBUG_INFO_BTF=y в конфиге ядра существует более дешевый, по сравнению с kprobes, способ динамической инструментации ядра, основанный на bpf trampolines. Так как BTF в дистрибутивных ядрах обычно выключен, я про эти события дальше не рассказываю. Если интересно, то смотрите сюда и/или задавайте вопросы в комментариях.


Tracepoints статическая инструментация ядра


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


$ sudo bpftrace -l 't:*'

Их сильно меньше, чем kprobes:


$ sudo bpftrace -l 't:*' | wc -l1782

но самой важной особенностью tracepoints является то, что они предоставляют стабильный API: вы можете быть уверены, что tracepoint, на основе которой вы написали свой код для отладки или сбора информации, не пропадет или не поменяет семантику в самый неудобный момент. Еще одним удобным отличием является то, что bpftrace может нам рассказать о том, какие аргументы передаются в конкретный tracepoint, например:


$ sudo bpftrace -lv tracepoint:thermal:thermal_temperaturetracepoint:thermal:thermal_temperature    __data_loc char[] thermal_zone;    int id;    int temp_prev;    int temp;

В случае kprobe, если у вас не включен BTF, вам придется читать исходники ядра, причем той версии, которую вы используете. А с BTF вы можете смотреть и на строение kprobes и kfuncs.


usdt статическая инструментация в пространстве пользователя


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


#include <sys/sdt.h>#include <sys/time.h>#include <unistd.h>int main(int argc, char **argv){    struct timeval tv;    for (;;) {        gettimeofday(&tv, NULL);        DTRACE_PROBE1(test, probe, tv.tv_sec);        sleep(1);    }}

Мы добавили одну статическую точку останова под названием test:probe, в которую передаем один аргумент tv.tv_sec текущее время в секундах. Чтобы скомпилировать эту программу, нужно поставить пакет systemtap-sdt-dev (или аналогичный для вашего дистрибутива). Дальше мы можем посмотреть на то, что получилось:


$ cc /tmp/test.c -o /tmp/test$ sudo bpftrace -l 'usdt:/tmp/test'usdt:/tmp/test:test:probe

Запустим /tmp/test в одном терминале, а в другом скажем


$ sudo bpftrace -e 'usdt:/tmp/test:test:probe { printf("московское время %u\n", arg0); }'Attaching 1 probe...московское время 1612903991московское время 1612903992московское время 1612903993московское время 1612903994московское время 1612903995...

Здесь arg0 это значение tv.tv_sec, которое мы передаем в breakpoint.


События Perf


Программа bpftrace поддерживает множество событий, предоставляемых подсистемой ядра Perf. Мы сейчас коротко посмотрим на следующие типы событий, поддерживаемые bpftrace:


  • software: статически-сгенерированные софтверные события
  • hardware: железные PMCs
  • interval: интервальное событие
  • profile: интервальное событие для профилирования

События типа `software`


В ядре определяется несколько статических событий perf, посмотреть их список можно так:


# bpftrace -l 's:*'software:alignment-faults:software:bpf-output:software:context-switches:software:cpu-clock:software:cpu-migrations:software:dummy:software:emulation-faults:software:major-faults:software:minor-faults:software:page-faults:software:task-clock:

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


# bpftrace -e 'software:cpu-migrations:10 { @[comm] = count(); }'Attaching 2 probes...^C@[kworker/u65:1]: 1@[bpftrace]: 1@[SCTP timer]: 2@[Xorg]: 2

Подсчитает каждое десятое событие миграции процесса с одного CPU а другой. Значение событий из списка выше объясняется в perf_event_open(2), например, cpu-migrations, которую мы использовали выше можно найти в этой man-странице под именем PERF_COUNT_SW_CPU_MIGRATIONS.


События типа `hardware`


Ядро дает нам доступ к некоторым hardware counters, а bpftrace может вешать на них программы BPF. Точный список событий зависит от архитектуры и ядра, например:


bpftrace -l 'h*'hardware:backend-stalls:hardware:branch-instructions:hardware:branch-misses:hardware:bus-cycles:hardware:cache-misses:hardware:cache-references:hardware:cpu-cycles:hardware:frontend-stalls:hardware:instructions:hardware:ref-cycles:

Посмотрим на то как мой процессор предсказывает инструкции перехода (считаем каждое стотысячное событие, см. PERF_COUNT_HW_BRANCH_MISSES):


bpftrace -e 'hardware:branch-misses:100000 { @[tid] = count(); }'Attaching 3 probes...@[1055]: 4@[3078]: 4@[1947]: 5@[1066]: 6@[2551]: 6@[0]: 29

События типа `interval` и `profile`


События типов interval и profile позволяют пользователю запускать обработчики через заданные интервалы времени. Событие первого типа запустится один раз на одном CPU, а событие второго на каждом из CPU.


Мы уже использовали интервал раньше, чтобы выйти из программы. Давайте посмотрим на этот пример еще раз, но чуть пропатчим его:


$ sudo bpftrace -e '    kr:vfs_write { @ = hist(retval); }    interval:s:2 { print(@); clear(@); }'

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


Аналогично можно использовать и profile:


# bpftrace -e '  profile:hz:99 { @[kstack] = count(); }  i:s:10 { exit(); }  END { print(@,1); clear(@); }'Attaching 3 probes...@[    cpuidle_enter_state+202    cpuidle_enter+46    call_cpuidle+35    do_idle+487    cpu_startup_entry+32    start_secondary+345    secondary_startup_64+182]: 14455

Здесь мы запускаем profile на каждом ядре 99 раз в секунду, через десять секунд выстрелит интервал и вызовет exit(), а секция END напечатает только верхний элемент словаря @ самый часто встречающийся ядерный стек (по которому мы видим, что моя система, в основном, бездействует).


Bpftrace: tutorial


Базовые навыки


Начнем с простого, запустим bpftrace без аргументов:


# bpftrace

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


Посмотрим внимательнее на аргумент -l. Он позволяет найти события по шаблону. (Если что-то дальше не ясно, то читайте про события в предыдущем разделе, который вы пропустили.) Вот как можно искать события среди всех возможных:


# bpftrace -l '*kill_all*'kprobe:rfkill_allockprobe:kill_allkprobe:btrfs_kill_all_delayed_nodes

А здесь мы ищем события только среди tracepoints:


# bpftrace -l 't:*kill*'tracepoint:cfg80211:rdev_rfkill_polltracepoint:syscalls:sys_enter_killtracepoint:syscalls:sys_exit_killtracepoint:syscalls:sys_enter_tgkilltracepoint:syscalls:sys_exit_tgkilltracepoint:syscalls:sys_enter_tkilltracepoint:syscalls:sys_exit_tkill

Подмножество tracepoint:syscalls, на которое мы только что наткнулись, можно использовать для самостоятельных экспериментов по изучению bpftrace. Для каждого системного вызова X определены две точки останова:


tracepoint:syscalls:sys_enter_Xtracepoint:syscalls:sys_exit_X

Поиграемся с каким-нибудь системным вызовом, например, execve(2). Для того, чтобы посмотреть на то, как использовать какой-либо tracepoint можно использовать дополнительный аргумент -v, например:


# bpftrace -lv 't:s*:sys_*_execve'tracepoint:syscalls:sys_enter_execve    int __syscall_nr;    const char * filename;    const char *const * argv;    const char *const * envp;tracepoint:syscalls:sys_exit_execve    int __syscall_nr;    long ret;

(заметьте как ловко мы дважды использовали *, чтобы не писать syscalls полностью и чтобы не перечислять sys_enter_execve и sys_exit_execve по отдельности). Параметры, перечисленные в выводе -lv доступны через встроенную переменную args:


# bpftrace -e '    t:s*:sys_enter_execve { printf("ENTER: %s\n", str(uptr(args->filename))); }    t:s*:sys_exit_execve { printf("EXIT: %s: %d\n", comm, args->ret); }'Attaching 2 probes...ENTER: /bin/lsEXIT: ls: 0ENTER: /bin/lssEXIT: bash: -2

Этот короткий листинг позволяет разглядеть несколько интересных вещей.


В первом обработчике мы печатаем аргумент args->filename. Так как передается он нам как указатель, нам нужно вычитать строку при помощи встроенной функции str, но просто так ее использовать нельзя: указатель этот указывает в пространство пользователя, а значит мы должны об этом специально сказать при помощи функции uptr. Сам bpftrace постарается угадать принадлежность указателя, но он не гарантирует результат. Также, к сожалению, вызов bpftrace -lv не расскажет нам о семантике указателя, для этого придется изучать исходники, в данном случае, мы посмотрим на определение системного вызова execve (обратите внимание на квалификатор типа __user).


Во втором обработчике мы используем встроенную переменную comm, которая возвращает текущее имя потока. Код возврата системного вызова доступен через переменную args->ret. Как известно, этот системный вызов "не возвращается" в текущую программу, так как его работа заключается собственно в замене кода программы новым. Однако, в случае ошибки он-таки вернется, как мы и можем видеть в выводе выше: в первом случае я запустил /bin/ls из баша и exec запустился нормально и вернул 0 (внутри процесса ls прямо перед запуском кода /bin/ls), а во втором случае я запустил несуществующую программу /bin/lss и exec вернул -2 внутри процесса bash.


Упражнение. Возьмите ваш любимый системный вызов и напишите несколько программ, аналогичных приведенной выше. Попробуйте напечатать все аргументы системного вызова и значение, которое он возвращает. Не забудьте использовать uptr, если нужно.


Структура программ `bpftrace`


Язык bpftrace создавался по аналогии с языком awk (см. также главу 6 в книжке The AWK Programming Language) и имеет очень похожую структуру. Программы состоят из списка блоков вида


<probe> <filter> { <action> }

Например,


# bpftrace -e 'BEGIN { printf("Hello\n") } END { printf("World\n") }'

Здесь <probe> это BEGIN и END, а <action> это printf. Поле
<filter> является опциональным и используется для фильтрации событий,
например, программа


# bpftrace -e 'p:s:1 /cpu == 0/ { printf("Привет с CPU%d\n", cpu); }'Attaching 1 probe...Привет с CPU0Привет с CPU0^C

будет передавать привет, только если запускается на CPU 0.


Упражнение. Что выведет эта команда на вашей машине, если убрать фильтр /cpu == 0/?


На практике <filter> удобно использовать для синхронизации между двумя событиями. Например, вы хотите подсчитать время выполнения системного вызова write на вашей системе. Для этого мы можем использовать пару трейспоинтов sys_enter_write и sys_exit_write и считать время выполнения по тредам:


# cat /tmp/write-times.btt:syscalls:sys_enter_write {  @[tid] = nsecs}t:syscalls:sys_exit_write /@[tid]/ {  @write[comm] = sum((nsecs - @[tid]) / 1000);  delete(@[tid]);}END {  print(@write, 10);  clear(@write);  clear(@);}

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


# bpftrace /tmp/write-times.bt

В первом событии, sys_enter_write, мы запоминаем время запуска системного вызова write в наносекундах в словаре @, ключом к которому служит tid.


Во втором событии, sys_exit_write, мы при помощи фильтра /@[tid]/ проверяем, запускался ли обработчик первого события для данного потока. Нам нужно это делать, ведь мы могли запустить программу в тот момент, когда какой-то поток был внутри системного вызова write. Дальше мы записываем потраченное время (в микросекундах) в отдельный словарь @write и удаляем элемент @[tid].


Наконец, после того как мы нажимаем ^C, запускается секция END, в которой мы печатаем десять самых прожорливых процессов и чистим словари @write и @, чтобы bpftrace не выводил их содержимое.


Упражнение. Так что же именно может пойти не так, если убрать фильтр /@[tid]/?


Храним состояние: переменные и мапы


Внутри программ bpftrace вы можете использовать обычные для языка C конструкции, например, :?, ++, --. Вы можете использовать блоки if {} else {}. Можно составлять циклы при помощи while и экзотического unroll (который появился в то время, когда в архитектуре BPF циклы были запрещены). Содержание же во все эти конструкции добавляют переменные и структуры ядра, доступные из контекста.


Переменные бывают двух типов: локальные и глобальные. Локальные переменные начинаются со знака доллара $ и доступны в пределах заданного события, оба следующих варианта сработают:


# bpftrace -e 'BEGIN { $x = 1; printf("%d\n", ++$x); exit(); }'# bpftrace -e 'BEGIN { if (1) { $x = 1; } printf("%d\n", ++$x); exit(); }'

а следующее нет:


# bpftrace -e 'BEGIN { $x = 1; exit(); } END { printf("%d\n", $x); }'

Глобальные переменные, с которыми мы уже встречались выше, начинаются со знака @ и доступны между событиями. Вы можете использовать "безымянную" глобальную переменную @, как мы делали выше для хранения начала вызова write (@[tid]). (Глобальные переменные в bpftrace хранятся в мапах специальных размеченных областях памяти. Они, между прочим, глобальные в более общем смысле: любая программа с рутовым доступом на системе может их читать и писать. Но для данной статьи это не так важно, смотрите предыдущие серии, если вам интересны подробности.)


И теперь, мы переходим к самому главному: зачем нам нужны переменные и что мы в них будем записывать? Каждое событие bpftrace запускается с определенным контекстом. Для kprobes нам доступны аргументы вызываемой функции, для tracepoints аргументы, передаваемые в tracepoint, а для событий Perf, как и для других программ, глобальные переменные. Мы уже видели как мы можем работать с tracepoints, в этой и следующих секциях мы посмотрим на kprobes, а в секции Веселые Картинки мы посмотрим на события Perf.


Аргументы kprobes доступны внутри программы как arg0, arg1, и т.д. Аргументы передаются без типа, так что вам придется к нужному типу их приводить вручную. Пример:


#include <linux/skbuff.h>#include <linux/ip.h>k:icmp_echo {  $skb = (struct sk_buff *) arg0;  $iphdr = (struct iphdr *) ($skb->head + $skb->network_header);  @pingstats[ntop($iphdr->saddr), ntop($iphdr->daddr)]++;}

Эта программа строит статистику о том, кто пингует данный хост. Мы цепляемся к kprobe на входе в функцию icmp_echo, которая вызывается на приход ICMPv4 пакета типа echo request. Ее первый аргумент, arg0 в нашей программе, это указатель на структуру типа sk_buff, описывающую пакет. Из этой структуры мы достаем IP адреса и увеличиваем соответствующий счетчик в глобальной переменной @pingstats. Все, теперь у нас есть полная статистика о том, кто и как часто пинговал наши IP адреса! Раньше для написания такой программы вам пришлось бы писать модуль ядра, регистрировать в нем обработчик kprobe, а также придумывать механизм взаимодействия с user space, чтобы хранить и читать статистику.


Посмотрим на нее еще раз. Вначале мы включили два хедера ядра, для этого нужно установить пакет с kernel headers. Эти хедеры нужны нам для определения структур sk_buff и iphdr, которые мы собираемся разыменовывать. (Если бы у нас был собран BTF, то нам не нужно было бы это делать ни устанавливать пакет, ни включать хедеры.) В первой строчке программы мы приводим единственный аргумент функции icmp_echo к указателю на sk_buff и сохраняем его в локальной переменной $skb. На второй строчке мы разыменовываем $skb и находим место в памяти, где хранится сетевой заголовок, который мы, в свою очередь, приводим к указателю на iphdr. На третьей строчке мы используем сетевой заголовок и встроенную функцию ntop языка bpftrace, которая преобразует бинарный IP адрес в строку.


Упражнение. Возьмите любую интересующую вас функцию ядра и попробуйте разыменовать ее аргументы. Не забывайте про uptr и kptr. Например: возьмите функцию vfs_write ядра, ее первый аргумент это указатель на структуру struct file, определенную в заголовке <linux/fs.h>. Попробуйте напечатать интересующие вас флаги файла до и после вызова vfs_write. (Hint: как вы можете передать указатель на struct file внутрь kretprobe?)


Упражнение. Напишите программу bpftrace, которая будет печатать имя и пароль пользователя, всякий раз, как он запускает sudo.


Считаем и агрегируем события


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


# bpftrace -e 'p:hz:5000 { @x++; @y = count(); } i:s:10 { exit(); }'Attaching 2 probes...@x: 760528@y: 799002

В течение 10 секунд по 5000 раз в секунду на каждом из 16 ядер моей системы мы увеличиваем значения двух счетчиков @x и @y. Операция ++ выполняется безо всяких блокировок и поэтому значение счетчика не совсем точное. Операция count() на самом деле выполняется тоже безо всяких блокировок, но использует CPU-локальные переменные: для каждого из CPU хранится свой счетчик, значения которых при печати суммируются.


Кроме подсчета событий в bpftrace есть несколько полезных функций, которые могут быстро показать какие-то баги в работающей системе. Главный инструмент тут это гистограммы. Посмотрим на простой пример.


# bpftrace -e 'kr:vfs_write { @ = hist(retval); } i:s:10 { exit() }'Attaching 2 probes...@:[1]             14966407 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|[2, 4)                 0 |                                                    |[4, 8)                 0 |                                                    |[8, 16)             1670 |                                                    |[16, 32)               0 |                                                    |[32, 64)             123 |                                                    |[64, 128)              0 |                                                    |[128, 256)             0 |                                                    |[256, 512)             0 |                                                    |[512, 1K)              0 |                                                    |[1K, 2K)               0 |                                                    |[2K, 4K)               0 |                                                    |[4K, 8K)               0 |                                                    |[8K, 16K)        7531982 |@@@@@@@@@@@@@@@@@@@@@@@@@@                          |

В течении десяти секунд мы строим гистограмму возвращаемых значений функции vfs_write, и мы можем заметить, что кто-то уверенно пытается писать по одному байту! Давайте чуть усовершенствуем программу (то заняло у меня около 20 секунд):


# bpftrace -e '    kr:vfs_write /retval == 1/ { @[pid, comm] = hist(retval); }    i:s:10 { exit() }    END { print(@, 1); clear(@); }'Attaching 3 probes...@[133835, dd]:[1]             14254511 |@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@|

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


# tr '\000' ' ' < /proc/133835/cmdlinedd if=/dev/zero of=/dev/null bs=1

Упражнение. Найдите ссылку на официальный туториал по bpftrace в конце статьи и выполните все шаги, в которых встречаются гистограммы.


Веселые Картинки: flame graphs


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


profile:hz:rateprofile:s:rateprofile:ms:rateprofile:us:rate

В первом случае мы задаем частоту события в HZ, а в следующих частоту события в секундах, миллисекундах и микросекундах, соответственно.


Одной из интересных глобальных переменных, которые мы можем сэмплировать, является стек ядра, выглядит он так:


bpftrace -e 'profile:hz:99 { print(kstack); exit() }'Attaching 1 probe...        cpuidle_enter_state+202        cpuidle_enter+46        call_cpuidle+35        do_idle+487        cpu_startup_entry+32        rest_init+174        arch_call_rest_init+14        start_kernel+1724        x86_64_start_reservations+36        x86_64_start_kernel+139        secondary_startup_64+182

здесь в момент сэмплирования мы попали на ядро-бездельника.


Для практического профилирования, однако, смотреть на миллионы стеков ядра не очень удобно. Поэтому БГ разработал механизм агрегации под названием flame graphs, превращающий текст в удобную кликабельную картинку.


Для начала, нам нужно запустить bpftrace следующим образом:


# bpftrace -e 'profile:hz:333 { @[kstack] = count(); } i:s:10 { exit() }' > /tmp/raw# wc -l /tmp/raw3374 /tmp/raw

Здесь мы по 333 раза в секунду сэмплируем стек ядра и считаем сколько раз мы увидели разные стеки (мы используем kstack как ключ в словаре @, ведь kstack это просто строка).


Далее нам нужно склонировать репозиторий FlameGraph и запустить пару скриптов:


$ git clone https://github.com/brendangregg/FlameGraph.git$ cd FlameGraph$ ./stackcollapse-bpftrace.pl /tmp/raw > /tmp/ready$ ./flamegraph.pl /tmp/ready > /tmp/kstack.svg

Первый скрипт приводит вывод bpftrace к каноническому виду, а второй строит по нему картинку (кликните на нее, чтобы открылся gist с SVG):



Здесь моя система наполовину бездействует, а на половине CPU крутится все тот же dd, копирующий /dev/zero в /dev/null по одному байту. Кликайте на картинку, чтобы посмотреть подробности.


Упражнение. Снимки стека можно делать не только при помощи bpftrace. Загляните в в репозиторий FlameGraph и сделайте снимок своей системы другим способом.


Пора закругляться


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


BCC: утилиты и фреймворк


Благодаря проекту BCC люди, роботы и не определившиеся могут использовать возможности BPF без необходимости утруждать себя программированием проект содержит почти 100 готовых утилит. Эти утилиты не случайные примеры, а рабочие инструменты, используемые повседневно в недрах Netflix, Facebook и других компаний добра. См. ссылки на книжки БГ в конце статьи, в которых подробно описано большинство утилит и подробно обсуждается почему и зачем они нужны.


Утилиты написаны на основе libbcc и libbpf и представляют из себя код на питоне, в который встроены куски псевдо-C кода, так что их легко редактировать и расширять даже на боевой машине. Также вы можете писать новые утилиты по аналогии с существующими, см. следующий подраздел.


Утилиты BCC должны быть более-менее опакечены в популярных дистрибутивах. Например, на убунте достаточно поставить пакет bpfcc-tools. После этого мы можем сразу ими пользоваться. Например, команда


# funccount-bpfcc 'vfs_*' -d 5Tracing 67 functions for "b'vfs_*'"... Hit Ctrl-C to end.FUNC                                    COUNTb'vfs_statfs'                               1b'vfs_unlink'                               1b'vfs_lock_file'                            2b'vfs_fallocate'                           31b'vfs_statx_fd'                            32b'vfs_getattr'                             80b'vfs_getattr_nosec'                       88b'vfs_open'                               108b'vfs_statx'                              174b'vfs_writev'                            2789b'vfs_write'                             6554b'vfs_read'                              7363Detaching...

посчитает сколько раз были вызваны функции ядра с префиксом vfs_ на интервале в пять секунд. Чуть интереснее подсунуть программе параметр -p, в котором передается PID процесса. Например, вот что делает мой mplayer, пока я это пишу:


# funccount-bpfcc 'vfs_*' -d 5 -p 29380Tracing 67 functions for "b'vfs_*'"... Hit Ctrl-C to end.FUNC                                    COUNTb'vfs_write'                              208b'vfs_read'                               629Detaching...

Пишем новую утилиту BCC


Давайте напишем простую утилиту BCC. Эта утилита будет считать сколько раз в секунду были вызваны функции ядра mutex_lock и mutex_unlock. Ее полный код приведен далее, также вы можете прочитать его здесь.


#! /usr/bin/python3from bcc import BPFfrom ctypes import c_intfrom time import sleep, strftimefrom sys import argvb = BPF(text="""BPF_PERCPU_ARRAY(mutex_stats, u64, 2);static inline void inc(int key){    u64 *x = mutex_stats.lookup(&key);    if (x)        *x += 1;}void do_lock(struct pt_regs *ctx) { inc(0); }void do_unlock(struct pt_regs *ctx) { inc(1); }""")b.attach_kprobe(event="mutex_lock", fn_name="do_lock")b.attach_kprobe(event="mutex_unlock", fn_name="do_unlock")print("%-8s%10s%10s" % ("TIME", "LOCKED", "UNLOCKED"))while 2 * 2 == 4:    try:        sleep(1)    except KeyboardInterrupt:        exit()    print("%-8s%10d%10d" % (          strftime("%H:%M:%S"),          b["mutex_stats"].sum(0).value,          b["mutex_stats"].sum(1).value))    b["mutex_stats"].clear()

Вначале мы подключаем нужные библиотеки. Понятно, что самая интересная часть тут это импорт класса BPF:


from bcc import BPF

Этот класс позволяет нам определить программы BPF, которые мы будем подключать к событиям. В качестве параметра класс BPF принимает текст программы на псевдо-C. В нашем случае это


BPF_PERCPU_ARRAY(mutex_stats, u64, 2);static inline void inc(int key){    u64 *x = mutex_stats.lookup(&key);    if (x)        *x += 1;}void do_lock(struct pt_regs *ctx)   { inc(0); }void do_unlock(struct pt_regs *ctx) { inc(1); }

Этот код написан на магическом C, вы не сможете скомпилировать его в таком виде, но при инициализации класса BPF некоторые части будут заменены реальным кодом на C.
Так или иначе, вначале мы определяем массив mutex_stats из двух элементов типа u64, наших счетчиков. Заметьте, что мы использовали PERCPU массив, это означает, что для каждого логического CPU будет создан свой массив. Далее мы определяем функцию inc, принимающую в качестве аргумента индекс в массиве mutex_stats. Эта функция увеличивает значение соответствующего счетчика.
Наконец, тривиальные функции do_lock и do_unlock увеличивают каждая свой счетчик.


На этом с ядерной частью почти покончено во время инициализации класс BPF обратится к библиотеке libllvm, чтобы скомпилировать код, и потом зальет его в ядро. Осталось только подключить программы к интересующим нас kprobes:


b.attach_kprobe(event="mutex_lock", fn_name="do_lock")b.attach_kprobe(event="mutex_unlock", fn_name="do_unlock")

Пользовательская часть кода занимается исключительно сбором информации:


print("%-8s%10s%10s" % ("TIME", "LOCKED", "UNLOCKED"))while 2 * 2 == 4:    try:        sleep(1)    except KeyboardInterrupt:        exit()    print("%-8s%10d%10d" % (          strftime("%H:%M:%S"),          b["mutex_stats"].sum(0).value,          b["mutex_stats"].sum(1).value))    b["mutex_stats"].clear()

После печати заголовка бесконечный цикл раз в секунду печатает значение счетчиков и обнуляет массив mutex_stats. Значение счетчиков мы получаем при помощи метода sum массива mutex_stats, который суммирует значения счетчиков для каждого CPU:


sum(index) {    result = 0    для каждого CPU {        result += cpu->mutex_stats[index]    }    return result}

Вот и все. Программа должна работать примерно так:


$ sudo ./bcc-tool-exampleTIME        LOCKED  UNLOCKED18:06:33     11382     1299318:06:34     11200     1278318:06:35     18597     2255318:06:36     20776     2551618:06:37     59453     6820118:06:38     49282     5806418:06:39     25541     2742818:06:40     22094     2528018:06:41      5539      725018:06:42      5662      735118:06:43      5205      690518:06:44      6733      8438

Где-то в 18:06:35 я переключился из терминала на вкладку с youtube в Firefox, поставил youtube на паузу и затем в 18:06:40 переключился назад в терминал. Итого, программа показала, что при просмотре youtube вы заставляете ядро локать примерно сорок тысяч мьютексов в секунду.


Напоследок хочется сказать, что если вы предпочитаете писать на C, то смотрите в сторону libbpf и CO-RE. Использование libbpf напрямую позволяет избавиться от тяжелых зависимостей времени запуска, типа libllvm, ускоряет время работы, а также экономит дисковое пространство. В частности, некоторые утилиты BCC уже переписаны на libbpf + CO-RE прямо внутри проекта BCC, см. libbpf-tools. За подробностями обращайтесь к статье BCC to libbpf conversion guide (или ждите следующую статью из этой серии).


Ply: bpftrace для бедных


Утилита ply, написанная шведом Tobias Waldekranz в доисторическом 2015 году, является в определенном смысле прямым предком bpftrace. Она поддерживает awk-подобный язык для создания и инструментации ядра программами BPF, например,


ply 'tracepoint:tcp/tcp_receive_reset {    printf("saddr:%v port:%v->%v\n", data->saddr, data->sport, data->dport);}'

Отличительной особенностью ply является минимизация зависимостей: ей нужна только libc (любая). Это удобно, если вы хотите с минимальными усилиями поиграться в BPF на встроенных системах. Для того, чтобы отрезать все зависимости, в ply встроен компилятор ply script language -> BPF.


Однако, не умаляя достоинств ply, стоит отметить, что разработка проекта к настоящему времени заглохла ply работает, поддерживается, но новые фичи не появляются. Вы все еще можете использовать ply, например, для того, чтобы потестировать сборку ядра на встроенной системе или для тестирования прототипов, но я бы советовал сразу писать программы на C с использованием libbpf для эмбедщиков это не составит труда, см., например, статью Building BPF applications with libbpf-bootstrap.


Ссылки


Предыдущие серии:



Ссылки на ресурсы по bpftrace, BCC и вообще отладке Linux:


  • The bpftrace One-Liner Tutorial. Это туториал по bpftrace, в котором перечисляются основные возможности. Представляет из себя список из двенадцати однострочных, или около того, программ.


  • bpftrace Reference Guide. Все, что вы хотели знать про использование bpftrace, но боялись спросить. Если вам этого документа мало, то идите читать про внутренности bpftrace.


  • BCC Tutorial. Если вы освоились с bpftrace и хотите копнуть глубже (но еще не готовы к освоению libbpf и настоящим приключениям), то смотрите на этот туториал по BCC, на BCC Reference Guide, а также на книжки, перечисленные ниже.


  • Brendan Gregg, BPF Performance Tools. БГ распознал потенциал BPF в деле трассировки Linux сразу после его появления и в данной книжке описывает результаты работы последних пяти лет сотню или больше отладочных утилит из проекта BCC. Книжка является отличным справочным дополнением по BPF к следующей.


  • Brendan Gregg, Systems Performance: Enterprise and the Cloud, 2nd Edition (2020). Это второе издание знаменитой Systems Performance. Главные изменения: добавлен материал по BPF, выкинут материал по Solaris, а сам БГ стал на пять лет опытнее. Если книжка BPF Performance Tools отвечает на вопрос как?, то эта книжка отвечает на вопрос почему?, а также рассказывает о других техниках (не BPF единым жив человек).


Подробнее..

Категории

Последние комментарии

  • Имя: Макс
    24.08.2022 | 11:28
    Я разраб в IT компании, работаю на арбитражную команду. Мы работаем с приламы и сайтами, при работе замечаются постоянные баны и лаги. Пацаны посоветовали сервис по анализу исходного кода,https://app Подробнее..
  • Имя: 9055410337
    20.08.2022 | 17:41
    поможем пишите в телеграм Подробнее..
  • Имя: sabbat
    17.08.2022 | 20:42
    Охренеть.. это просто шикарная статья, феноменально круто. Большое спасибо за разбор! Надеюсь как-нибудь с тобой связаться для обсуждений чего-либо) Подробнее..
  • Имя: Мария
    09.08.2022 | 14:44
    Добрый день. Если обладаете такой информацией, то подскажите, пожалуйста, где можно найти много-много материала по Yggdrasil и его уязвимостях для написания диплома? Благодарю. Подробнее..
© 2006-2024, personeltest.ru