За последние несколько десятилетий вычислительные системы стали не проще — наоборот, сложнее. Рассуждения о том, как программное обеспечение само себя обеспечивает, привели к созданию нескольких бизнес-категорий, и все они пытаются решить проблемы, связанные с необходимостью понять сложные системы. Один из подходов — анализ журналов данных, генерируемых приложениями, работающими в вычислительной системе. Журналы являются отличным источником информации. Они предоставляют точные данные о том, как себя ведет приложение. Тем не менее они ограничивают вас, потому что вы получаете только ту информацию, на сбор которой инженеры, создавшие приложение, запрограммировали эти журналы. Сбор любой дополнительной информации в формате журнала из любой системы может быть такой же сложной задачей, как декомпиляция программы и наблюдение за ходом выполнения. Другой популярный подход заключается в использовании метрик, позволяющих понять, почему программа ведет себя именно так и как она это делает. Метрики отличаются от журналов форматом данных: журналы предоставляют явные данные, а метрики объединяют данные, чтобы выяснить, как программа ведет себя в определенный момент времени.
Наблюдаемость — это новая практика, которая позволяет подойти к данной проблеме с другой стороны. Мы определяем наблюдаемость как способность задавать произвольные вопросы и получать сложные ответы от любой системы. Основное различие между наблюдаемостью, ведением журналов и группированием метрик — это данные, которые вы собираете. Учитывая то, что, практикуя наблюдаемость, вы должны отвечать на любой произвольный вопрос в любой момент, единственный способ рассуждать о данных — собрать все данные, которые ваша система может сгенерировать, и скомпоновать их только тогда, когда необходимо ответить на ваши вопросы.
Нассим Николас Талеб, автор такого бестселлера, как Antifragile: Things That Gain From Disorder, популяризировал термин «черный лебедь», назвав так неожиданные события, имеющие серьезные последствия, которых можно было ожидать, если бы их наблюдали до того, как они случились. В своей книге «Черный лебедь» он объясняет, как наличие соответствующих данных может помочь снизить риск возникновения этих редких событий. При разработке программного обеспечения «черные лебеди» встречаются чаще, чем мы думаем, и они неизбежны. Поскольку можно предположить, что предотвратить такого рода события нельзя, то получить как можно больше информации о них — единственная возможность решить проблему так, чтобы она не оказала критического воздействия на бизнес-системы. Наблюдаемость помогает создавать надежные системы и предотвращать будущие «черные лебеди», поскольку она основана на той предпосылке, что вы собираете любые данные, которые помогут ответить на любой заданный в дальнейшем вопрос. Изучение «черного лебедя» и практика наблюдаемости сходятся в центральной точке — данных, которые вы собираете из своих систем.
Контейнеры Linux — это абстракция в дополнение к набору функций ядра Linux для изоляции компьютерных процессов и управления ими. Ядро, традиционно отвечающее за управление ресурсами, обеспечивает также изоляцию задач и безопасность. В Linux основными функциями, на которых основаны контейнеры, являются пространства имен и контрольные группы. Пространства имен — это компоненты, которые изолируют задачи друг от друга. В некотором смысле, когда вы находитесь внутри пространства имен, вам кажется, что операционная система не выполняет никаких других задач на компьютере. Контрольные группы — это компоненты, которые обеспечивают управление ресурсами. С точки зрения эксплуатации они дают вам детальный контроль над любым использованием ресурсов, таких как ЦП, дисковый ввод-вывод, сеть и т.д. В последнее десятилетие с ростом популярности контейнеров Linux произошли изменения в том, как разработчики программного обеспечения проектируют большие распределенные системы и вычислительные платформы. Многоклиентские вычисления полностью зависят от этих функций в ядре.
Мы настолько полагаемся на низкоуровневые возможности ядра Linux, что у нас появляются новые источники информации, которые необходимо учитывать при разработке наблюдаемых систем. Ядро представляет собой систему, в которой вся работа описывается и реализуется на основе событий. Открытие файла — это своего рода событие, выполнение некоторой инструкции процессором — событие, получение сетевого пакета — событие и т.д. Berkeley Packet Filter (BPF) — это подсистема ядра, которая может проверять новые источники информации. BPF позволяет писать программы, которые выполняются безопасно, когда ядро реагирует на какое-либо событие. BPF обеспечивает безопасность, чтобы предотвратить системные сбои и вредоносное поведение каких-либо программ. BPF предоставляет новое поколение инструментов, которые помогают разработчикам систем наблюдать за новыми платформами и работать с ними.
В этой книге мы продемонстрируем возможности BPF, позволяющие сделать любую вычислительную систему более наблюдаемой. А также покажем, как писать программы BPF с использованием нескольких языков программирования. Мы поместили код для ваших программ в GitHub, поэтому не нужно копировать и вставлять его — вы найдете его в Git-репозитории к этой книге (github.com/bpftools/linux-observability-with-bpf).
Но прежде, чем начать разбирать технические аспекты BPF, посмотрим, как все начиналось.
В 1992 году Стивен Маккейн и Ван Якобсон опубликовали статью The BSD Packet Filter: A New Architecture for User-Level Packet Capture («Пакетный фильтр BSD: новая архитектура для захвата пакетов на уровне пользователя»). В ней они описали способ реализации фильтра сетевых пакетов для ядра Unix, который работал в 20 раз быстрее, чем все остальные, имеющиеся на то время в области фильтрации пакетов. Пакетные фильтры имеют конкретную цель: предоставлять приложениям, которые отслеживают сетевую активность, прямую информацию из ядра. Обладая этой информацией, приложения могут решить, что делать с пакетами. BPF представил два серьезных нововведения в области фильтрации пакетов:
• новую виртуальную машину (ВМ), предназначенную для эффективной работы с ЦП на основе регистров;
• возможность использования буферов для каждого приложения, способных фильтровать пакеты без копирования всей информации о них. Это минимизировало количество данных BPF, необходимых для принятия решений.
Такие радикальные улучшения заставили все системы Unix принять BPF в качестве технологии выбора для фильтрации сетевых пакетов, отказавшись от прежних реализаций, которые потребляли больше памяти и были менее производительными. Хотя они все еще присутствуют во многих производных ядра Unix, включая ядро Linux.
В начале 2014 года Алексей Старовойтов разработал расширенную реализацию BPF. Новый подход был оптимизирован для современного оборудования, благодаря чему его результирующий набор команд работает быстрее, чем машинный код, сгенерированный старым интерпретатором BPF. Расширенная версия также увеличила число регистров в виртуальной машине BPF с двух 32-битных регистров до десяти 64-битных. Увеличение количества регистров и их глубины позволило писать более сложные программы, поскольку разработчики могли свободно обмениваться дополнительной информацией, используя параметры функций. Эти изменения наряду с прочими улучшениями привели к тому, что расширенная версия BPF стала в четыре раза быстрее оригинальной реализации BPF.
Первоначальная цель создания новой реализации состояла в том, чтобы оптимизировать внутренний набор команд BPF, обрабатывающих сетевые фильтры. На этом этапе BPF все еще был ограничен пространством ядра, и только несколько программ в пользовательском пространстве могли задавать фильтры BPF для обработки ядром, такие как Tcpdump и Seccomp, о которых мы поговорим в следующих главах. Сегодня эти программы все еще генерируют байт-код для старого интерпретатора BPF, но ядро преобразует данные инструкции в значительно улучшенное внутреннее представление.
В июне 2014 года расширенная версия BPF стала доступной для пользователей. Это был переломный момент для будущего BPF. Как написал Алексей в патче с изменениями, «новый набор патчей демонстрирует потенциал eBPF».
BPF стал подсистемой ядра верхнего уровня и перестал ограничиваться только сетевым стеком. BPF-программы стали больше походить на модули ядра с сильным акцентом на безопасности и стабильности. В отличие от обычных модулей ядра, BPF-программы не требуют его перекомпиляции и гарантированно завершаются без сбоев.
Верификатор BPF, о котором мы поговорим в следующей главе, добавил необходимые гарантии безопасности. Здесь подразумевается, что любая BPF-программа завершится без сбоев и программы не будут пытаться получить доступ к памяти вне области их деятельности. Но эти преимущества сопровождаются определенными ограничениями: программы имеют максимально допустимый размер, и циклы должны быть ограничены, чтобы гарантировать, что память системы никогда не будет исчерпана неправильно написанной программой BPF.
Одновременно с изменениями, сделавшими BPF доступным из пространства пользователя, разработчики ядра добавили новый системный вызов (syscall) — bpf. Он станет центральным элементом связи между пользовательским пространством и ядром. Мы обсудим, как применять этот системный вызов для работы с программами и картами BPF, в главах 2 и 3.
Карты BPF станут основным механизмом обмена данными между ядром и пользовательским пространством. В главе 2 говорится о том, как применять эти специализированные структуры для сбора информации из ядра, а также отправки информации в программы BPF, которые уже работают в нем.
В книге в первую очередь рассматривается расширенная версия BPF. За пять лет, прошедших с момента появления расширенной версии, BPF получил значительное развитие, и мы подробно обсудим эволюцию программ BPF, карт BPF и подсистем ядра.
Архитектура BPF в ядре впечатляет. Далее мы еще рассмотрим конкретные детали, а сейчас, в этой главе, хотим дать краткий обзор того, как все работает.
Как мы говорили ранее, BPF — это высокоразвитая виртуальная машина, выполняющая инструкции кода в изолированной среде. В определенном смысле вы можете думать о BPF как о виртуальной машине Java (JVM) — специализированной программе, которая выполняет машинный код, скомпилированный из исходного кода на языке программирования высокого уровня. Компиляторы, такие как LLVM и GNU Compiler Collection (GCC), в ближайшем будущем обеспечат поддержку BPF, что позволит вам компилировать код C в инструкции BPF. После того как код скомпилирован, BPF использует верификатор, чтобы убедиться, что программа безопасна для запуска в пространстве ядра. Он не позволяет запускать код, который может представлять угрозу для вашей системы из-за сбоя ядра. Если код безопасен, программа BPF будет загружена в ядро. Ядро Linux также включает в себя JIT-компилятор для команд BPF. JIT преобразует байт-код BPF в машинный код непосредственно после проверки программы, убирая все ненужное во время выполнения. Одним из интересных аспектов этой архитектуры является то, что вам не нужно перезагружать систему для загрузки BPF-программ — вы можете загрузить их по мере надобности, а также написать собственные сценарии инициализации, которые загружают программы BPF при запуске вашей системы.
Прежде чем запустить какую-либо BPF-программу, ядро должно знать, к какой точке выполнения она прикреплена. В ядре несколько точек подключения, и их список постоянно растет. Точки выполнения определяются типами программ BPF, мы обсудим их в следующей главе. Когда вы выбираете точку выполнения, ядро предоставляет также специальные помощники функций: их можно использовать для работы с данными, которые получает ваша программа, что связывает точки выполнения и программы BPF.
Последний компонент в архитектуре BPF отвечает за обмен данными между ядром и пользовательским пространством. Он называется картой BPF (см. главу 3). Карты BPF представляют собой двунаправленные структуры для обмена данными. Это означает, что вы можете записывать в них и читать из них с обеих сторон — из ядра и из пользовательского пространства. Существует несколько типов структур: от простых массивов и хеш-карт до специализированных карт, в которых можно сохранять целые BPF-программы.
Далее в книге мы более подробно рассмотрим каждый компонент архитектуры BPF. Вы также научитесь пользоваться преимуществами расширяемости и обмена данными BPF, изучая конкретные примеры, охватывающие различные темы: от анализа трассировки стека до сетевой фильтрации и изоляции во время выполнения.
Мы написали эту книгу, чтобы познакомить вас с основными концепциями BPF, которые вам понадобятся в повседневной работе с данной подсистемой Linux. BPF все еще находится в процессе разработки, и каждый день совершенствуются новые концепции и парадигмы. В идеале эта книга поможет вам расширить свои знания, обеспечив прочную базу для понимания основных компонентов BPF.
В следующей главе будет подробно рассказано о структуре программ BPF и о том, как их запускает ядро. Мы также рассмотрим точки в ядре, в которых вы можете прикрепить указанные программы. Это поможет вам в деталях узнать, какие данные могут понадобиться вашим программам и как их использовать.
Taleb N.N. Antifragile: Things That Gain from Disorder. — N.Y.: Random House, 2012.
Талеб Н.Н. Черный лебедь. Под знаком предсказуемости. 2-е изд., доп. — М.: КоЛибри, 2020.