golang

Создание XDP eBPF программы с использованием C и Golang: пошаговое руководство

  • пятница, 22 ноября 2024 г. в 00:00:05
https://habr.com/ru/companies/otus/articles/860104/

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

XDP позволяет перехватывать пакеты на уровне драйвера сетевого интерфейса, а eBPF предоставляет гибкую и эффективную среду для выполнения пользовательской логики обработки пакетов. В совокупности эти технологии обеспечивают беспрецедентный уровень контроля и производительности в сетевых приложениях. Наш проект, названный «dilih» (drop it like it’s hot), демонстрирует создание простого инструмента хаос-инжиниринга, который произвольно отбрасывает пакеты на указанном сетевом интерфейсе, что может быть полезно разработчикам для понимания поведения их приложений при проблемах с сетью. С помощью этого руководства вы получите базовое представление о XDP, eBPF и их практическом применении для работы с сетью.

Примечание: весь исходный код данной статьи доступен на github.  

Обзор проекта

Цель проекта — создать программу XDP на основе eBPF, написанную на C и Golang. Программа, получившая название «dilih», представляет собой простой инструмент хаос-инжиниринга, который случайным образом сбрасывает около 50% пакетов на заданном сетевом интерфейсе. Этот проект демонстрирует мощь и гибкость XDP и eBPF в управлении обработкой пакетов на высокой скорости, что делает его отличной отправной точкой для изучения этих технологий.

XDP eBPF программа, реализованная на C, подключается к сетевому стеку ядра Linux на раннем этапе, чтобы перехватывать пакеты и решать их дальнейшую судьбу. Используя простой механизм случайного отбора, программа избирательно сбрасывает пакеты, создавая управляемый хаос в сетевом трафике. Кроме того, программа применяет механизм событий perf eBPF для сбора статистики и измерения времени обработки как сброшенных, так и пропущенных пакетов.

Сопутствующее приложение на Golang взаимодействует с XDP eBPF программой, предоставляя удобный интерфейс для мониторинга поведения сброса пакетов и визуализации статистики производительности. Оно использует eBPF-карты для извлечения и агрегирования собранных данных из пространства ядра, что позволяет пользователям получить представление о влиянии сброшенных пакетов и эффективности обработки пакетов.

Настройка среды разработки

Чтобы приступить к созданию XDP eBPF программы на C и Golang, необходимо подготовить среду разработки. Следуйте этим шагам, чтобы установить все необходимые инструменты и зависимости:

  1. Установка инструментов разработки

Для начала убедитесь, что на вашей системе установлены необходимые инструменты разработки, такие как clang, llvm и bpftool. Вы можете установить эти инструменты с помощью менеджера пакетов, доступного в вашей дистрибуции Linux. Однако я рекомендую выделить время на сборку этих инструментов из исходных кодов, так как это даст вам больший контроль над флагами и функциями, встроенными в инструменты.

Если вам интересно, как именно настроена моя среда LLVM/Clang, посмотрите следующие ansible-задачи:

  1. Установка Golang

Далее вам нужно установить Golang — язык программирования, на котором написано сопровождающее приложение. Посетите официальный сайт Golang по адресу https://golang.org и следуйте инструкциям по установке для вашей операционной системы. После установки убедитесь, что команда go доступна из командной строки, добавив соответствующий каталог с бинарными файлами в переменную PATH вашей системы.

Если вам интересно, как именно настроена моя среда для Golang, вы можете посмотреть следующую ansible-задачу. 

  1. Установка зависимостей проекта

Зависимости Go

Перейдите в корневую директорию проекта и установите необходимые зависимости для Golang, выполнив следующую команду:

go mod download

Эта команда скачает и установит необходимые пакеты Golang, указанные в файле go.mod проекта.

libbpf

В нашем коде на C мы будем использовать библиотеку libbpf. В репозитории dilih она добавлена как подмодуль Git, но вы можете управлять ею в другом месте, если пожелаете. При сборке нашей программы на C мы включим libbpf с помощью флага -I../libbpf/src.

  1. Настройка IDE (необязательно)

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

Если вам интересно, как именно настроена моя среда, посмотрите этот репозиторий, где я устанавливаю neovim, настраиваю LSP и конфигурирую всё необходимое для разработки.

Написание программы XDP eBPF на C

Программа XDP реализована с использованием фреймворка eBPF на языке C и библиотеки libbpf. Это позволяет нам перехватывать пакеты на раннем этапе в сетевом стеке ядра Linux и выполнять пользовательскую логику обработки пакетов. В этом разделе мы пошагово рассмотрим процесс написания программы XDP eBPF на языке C.

  1. Логика программы

Перед тем как перейти к коду, давайте разберёмся с логикой нашей XDP программы. Цель состоит в том, чтобы случайным образом сбрасывать около 50% пакетов на заданном сетевом интерфейсе. Мы будем использовать механизм рандомизации для определения, сбросить или пропустить каждый пакет («является ли случайное число чётным?»). Программа также будет собирать статистику и измерять время обработки сброшенных и пропущенных пакетов, используя механизм событий perf в eBPF. Наша программа BPF выполняется в пространстве ядра, но для передачи данных в пространство пользователя мы будем использовать BPF-карты, чтобы передавать данные в наше приложение на Go.

  1. Создание исходного файла программы

Начните с создания нового файла с именем dilih_kern.c в каталоге ./bpf/ вашего проекта. Этот файл будет содержать логику XDP eBPF программы. Откройте файл в своем любимом текстовом редакторе.

  1. Определение необходимых заголовков и структур

Для начала подключите необходимые заголовочные файлы и определите нужные структуры для нашей XDP программы. Нам потребуются bpf.h и bpf_helpers.h, которые содержат полезные структуры и вспомогательные функции.

#include <linux/bpf.h>
#include <bpf_helpers.h>
  1. Определение структур данных и карт

Далее определите необходимые структуры данных и карты, которые будет использовать наша программа XDP. Мы создадим структуру для представления данных perf-события и карту BPF_MAP_TYPE_PERF_EVENT_ARRAY для хранения этих событий. Определите следующие структуры и карты:

struct perf_trace_event {
    __u64 timestamp;
    __u32 processing_time_ns;
    __u8 type;
};

#define TYPE_ENTER 1
#define TYPE_DROP 2
#define TYPE_PASS 3

struct {
    __uint(type, BPF_MAP_TYPE_PERF_EVENT_ARRAY);
    __uint(key_size, sizeof(int));
    __uint(value_size, sizeof(struct perf_trace_event));
    __uint(max_entries, 1024);
} output_map SEC(".maps");

Карта output_map будет использоваться для хранения perf-событий, которые генерирует наша программа XDP. Определения TYPE_* помогут сделать код более читаемым в дальнейшем.

  1. Реализация функции программы XDP

Теперь приступим к реализации функции самой программы XDP. Начнём с объявления функции XDP с соответствующей сигнатурой:

SEC("xdp")
int xdp_dilih(struct xdp_md *ctx)
{
    // Добавьте логику программы здесь ... подробно описано на следующем этапе
}

Функция xdp_dilih будет точкой входа для нашей XDP eBPF программы и вызываться для каждого входящего пакета.

  1. Обработка perf-событий и сбор данных

Внутри функции xdp_dilih мы можем обрабатывать perf-события для сбора данных и измерения времени обработки. Мы уже определили output_map для хранения этих событий. Используйте вспомогательную функцию bpf_perf_event_output, чтобы записывать perf-события в карту.

struct perf_trace_event e = {};

// Событие perf для входа в программу xdp
e.timestamp = bpf_ktime_get_ns();
e.type = TYPE_ENTER;
e.processing_time_ns = 0;
bpf_perf_event_output(ctx, &output_map, BPF_F_CURRENT_CPU, &e, sizeof(e));

// Логика отбрасывания пакетов
if (bpf_get_prandom_u32() % 2 == 0) {
    // Событие perf для отбрасывания пакета
    e.type = TYPE_DROP;
    __u64 ts = bpf_ktime_get_ns();
    e.processing_time_ns = ts - e.timestamp;
    e.timestamp = ts;
    bpf_perf_event_output(ctx, &output_map, BPF_F_CURRENT_CPU, &e, sizeof(e));
    return XDP_DROP;
}

// Событие perf для пропуска пакета
e.type = TYPE_PASS;
__u64 ts = bpf_ktime_get_ns();
e.processing_time_ns = ts - e.timestamp;
e.timestamp = ts;
bpf_perf_event_output(ctx, &output_map, BPF_F_CURRENT_CPU, &e, sizeof(e));

return XDP_PASS;

В этом участке кода мы обрабатываем perf-события для сбора данных и измерения времени обработки сброшенных и пропущенных пакетов. Мы сначала создаём perf-событие при входе в программу XDP (тип 1). Затем используем механизм рандомизации, чтобы решить, сбросить или пропустить пакет. Если пакет сбрасывается, создаём perf-событие с типом 2 и возвращаем XDP_DROP. Если пакет пропускается, создаём perf-событие с типом 3 и возвращаем XDP_PASS.

Функция bpf_ktime_get_ns() используется для измерения временной метки (в наносекундах с момента загрузки системы, без учёта времени приостановки) и измерения времени обработки пакета. Функция bpf_get_prandom_u32() генерирует случайное значение, которое помогает решить, сбросить или пропустить пакет — то самое «является ли случайное число чётным».

Дополнительно мы используем bpf_printk() для вывода отладочных сообщений, которые можно просматривать в буфере трассировки ядра.

Таким образом, реализация XDP eBPF программы на C завершена. Эта программа будет избирательно сбрасывать пакеты на основе механизма рандомизации и генерировать perf-события для сбора данных и измерения времени обработки.

Компиляция и загрузка XDP eBPF программы 

После написания программы на языке C следующий шаг — её компиляция и загрузка в ядро. В этом разделе мы разберём данный процесс.

Компиляция программы XDP

Для компиляции программы XDP мы будем использовать компилятор LLVM Clang с соответствующими флагами. Откройте терминал и перейдите в каталог bpf, где находится файл dilih_kern.c. Затем выполните следующую команду:

clang -S \
    -g \
    -target bpf \
    -I../libbpf/src\
    -Wall \
    -Werror \
    -O2 -emit-llvm -c -o dilih_kern.ll dilih_kern.c

Разберём флаги:

  • -S: Создаёт файл с промежуточным представлением (IR) вместо объектного кода. Этот шаг используется для генерации кода LLVM IR.

  • -g: Включает информацию о типах BTF (BPF Type Format).

  • -target bpf: Указывает архитектуру цели как "bpf" (Berkeley Packet Filter), что означает, что код компилируется для выполнения на eBPF.

  • -I../libbpf/src: Добавляет путь ../libbpf/src к путям поиска include-файлов, позволяя компилятору найти необходимые заголовочные файлы (вспомогательные файлы bpf) из библиотеки libbpf.

  • -Wall: Включает все предупреждения компилятора.

  • -Werror: Обрабатывает все предупреждения как ошибки, из-за чего процесс компиляции прерывается при возникновении любых предупреждений.

  • -O2: Применяет второй уровень оптимизации, что повышает производительность без увеличения размера кода. Этот уровень оптимизации обязателен для некоторых случаев использования BPF.

  • -emit-llvm: Указывает компилятору выводить код LLVM IR.

  • -c: Компилирует входной исходный файл без линковки, создавая объектный файл.

  • -o dilih_kern.ll: Указывает имя выходного файла для сгенерированного кода LLVM IR как dilih_kern.ll.

Теперь используем команду llc, чтобы дополнительно обработать LLVM IR код и сгенерировать итоговый объектный файл:

llc -march=bpf -filetype=obj -O2 -o dilih_kern.o dilih_kern.ll

Разберём флаги:

  • -march=bpf: Указывает архитектуру цели как "bpf" для стадии генерации кода.

  • -filetype=obj: Указывает требуемый тип выходного файла как объектный файл.

  • -O2: Применяет второй уровень оптимизации к сгенерированному коду на стадии генерации.

  • -o dilih_kern.o: Указывает имя выходного файла для сгенерированного объектного кода как dilih_kern.o.

Эта команда компилирует файл dilih_kern.c в объектный файл BPF с именем dilih_kern.o. Флаг -target bpf задаёт архитектуру цели как BPF, а флаг -O2 включает оптимизацию.

Загрузка программы XDP

Чтобы загрузить программу XDP в ядро, используем командную утилиту bpftool. Убедитесь, что утилита bpftool установлена на вашей системе. Если она ещё не установлена, то сделать это можно через менеджер пакетов вашего дистрибутива.

В терминале выполните следующую команду для загрузки программы XDP:

sudo bpftool prog load dilih_kern.o /sys/fs/bpf/dilih

Эта команда загружает объектный файл dilih_kern.o и закрепляет его в директории /sys/fs/bpf/dilih. При необходимости скорректируйте путь в зависимости от конфигурации вашей системы. Утилита bpftool выполнит процесс загрузки и проверит корректность программы.

Подключение программы XDP

После загрузки программы XDP нужно подключить её к сетевому интерфейсу для перехвата пакетов. Для этого выполните следующую команду:

sudo bpftool net attach xdp pinned /sys/fs/bpf/dilih dev <interface>
# you can get <interface> by running `ip link'

Замените <interface_name> на имя сетевого интерфейса, к которому хотите подключить программу XDP, например, eth0. Эта команда подключает программу XDP к указанному интерфейсу, позволяя ей перехватывать входящие пакеты.

Makefile

Для удобства давайте добавим описанные выше команды в файл Makefile в директорию ./bpf/Makefile. В этой статье мы не будем углубляться в принцип работы Makefile, но кратко опишем функциональность после примера кода:

TARGET = dilih
BPF_TARGET = ${TARGET:=_kern}
BPF_C = ${BPF_TARGET:=.c}
BPF_OBJ = ${BPF_C:.c=.o}

BPF_PINNED_PATH := /sys/fs/bpf/$(TARGET)
XDP_NAME := dilih
DEV := ens160

xdp: $(BPF_OBJ)
        -bpftool net detach xdpgeneric dev $(DEV)
        rm -f $(BPF_PINNED_PATH)
        bpftool prog load $(BPF_OBJ) $(BPF_PINNED_PATH)
        bpftool net attach xdpgeneric pinned $(BPF_PINNED_PATH) dev $(DEV)

$(BPF_OBJ): %.o: %.c
        clang -S \
                -g \
                -target bpf \
          -I../libbpf/src\
                -Wall \
                -Werror \
                -O2 -emit-llvm -c -o ${@:.o=.ll} $<
        llc -march=bpf -filetype=obj -O2 -o $@ ${@:.o=.ll}

clean:
        -bpftool net detach xdpgeneric dev $(DEV)
        sudo rm -f $(BPF_PINNED_PATH)
        rm -f $(BPF_OBJ)
        rm -f ${BPF_OBJ:.o=.ll}

С этим Makefile и установленным make можно запустить команду DEV=eth0 make, чтобы скомпилировать и загрузить eBPF-программу, а DEV=eth0 make clean — чтобы удалить файлы и выгрузить eBPF-программу.

Поздравляем! Вы успешно скомпилировали и загрузили XDP eBPF программу в ядро и подключили её к сетевому интерфейсу. Программа теперь готова перехватывать и обрабатывать пакеты в соответствии с вашей логикой.

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

Написание приложения на Golang

В этом разделе мы создадим приложение на Golang, которое будет взаимодействовать с программой XDP eBPF и собирать метрики. Приложение будет считывать perf-события из загруженной программы XDP и отображать статистику на основе собранных данных.

Написание кода для приложения на Golang

Создайте новый файл с именем main.go и откройте его в текстовом редакторе. Этот файл будет содержать код нашего приложения. Скопируйте и вставьте следующий код в main.go:

package main

import (
        "encoding/binary"
        "fmt"
        "net"
        "os"
        "os/signal"
        "syscall"

        "github.com/cilium/ebpf"
        "github.com/cilium/ebpf/link"
        "github.com/cilium/ebpf/perf"
)

const (
        TYPE_ENTER = 1
        TYPE_DROP  = 2
        TYPE_PASS  = 3
)

type event struct {
        TimeSinceBoot  uint64
        ProcessingTime uint32
        Type           uint8
}

const ringBufferSize = 128 // размер кольцевого буфера, используемого для расчета среднего времени обработки
type ringBuffer struct {
        data   [ringBufferSize]uint32
        start  int
        pointer int
        filled bool
}

func (rb *ringBuffer) add(val uint32) {
        if rb.pointer < ringBufferSize {
                rb.pointer++
        } else {
                rb.filled = true
                rb.pointer= 1
        }
        rb.data[rb.pointer-1] = val
}

func (rb *ringBuffer) avg() float32 {
        if rb.pointer == 0 {
                return 0
        }
        sum := uint32(0)
        for _, val := range rb.data {
                sum += uint32(val)
        }
        if rb.filled {
                return float32(sum) / float32(ringBufferSize)
        }
        return float32(sum) / float32(rb.pointer)
}

func main() {
        spec, err := ebpf.LoadCollectionSpec("bpf/dilih_kern.o")
        if err != nil {
                panic(err)
        }

        coll, err := ebpf.NewCollection(spec)
        if err != nil {
                panic(fmt.Sprintf("Failed to create new collection: %v\n", err))
        }
        defer coll.Close()

        prog := coll.Programs["xdp_dilih"]
        if prog == nil {
                panic("No program named 'xdp_dilih' found in collection")
        }

        iface := os.Getenv("INTERFACE")
        if iface == "" {
                panic("No interface specified. Please set the INTERFACE environment variable to the name of the interface to be use")
        }
        iface_idx, err := net.InterfaceByName(iface)
        if err != nil {
                panic(fmt.Sprintf("Failed to get interface %s: %v\n", iface, err))
        }
        opts := link.XDPOptions{
                Program:   prog,
                Interface: iface_idx.Index,
                // Flags — одно из значений XDPAttachFlags (необязательно).
        }
        lnk, err := link.AttachXDP(opts)
        if err != nil {
                panic(err)
        }
        defer lnk.Close()

        fmt.Println("Successfully loaded and attached BPF program.")

        // обработка perf-событий
        outputMap, ok := coll.Maps["output_map"]
        if !ok {
                panic("No map named 'output_map' found in collection")
        }
        perfEvent, err := perf.NewReader(outputMap, 4096)
        if err != nil {
                panic(fmt.Sprintf("Failed to create perf event reader: %v\n", err))
        }
        defer perfEvent.Close()
        buckets := map[uint8]uint32{
                TYPE_ENTER: 0, // вход в программу BPF
                TYPE_DROP: 0, // программа BPF отбрасывает пакет
                TYPE_PASS: 0, // программа BPF пропускает пакет
        }

        processingTimePassed := &ringBuffer{}
        processingTimeDropped := &ringBuffer{}

        go func() {
                // переменная event типа event
                for {
                        record, err := perfEvent.Read()
                        if err != nil {
                                fmt.Println(err)
                                continue
                        }

                        var e event
                        if len(record.RawSample) < 12 {
                                fmt.Println("Invalid sample size")
                                continue
                        }
                        // время с момента запуска системы в первых 8 байтах
                        e.TimeSinceBoot = binary.LittleEndian.Uint64(record.RawSample[:8])
                        // время обработки в следующих 4 байтах
                        e.ProcessingTime = binary.LittleEndian.Uint32(record.RawSample[8:12])
                        // тип в последнем байте
                        e.Type = uint8(record.RawSample[12])
                        buckets[e.Type]++

                        if e.Type == TYPE_ENTER {
                                continue
                        }
                        if e.Type == TYPE_DROP {
                                processingTimeDropped.add(e.ProcessingTime)
                        } else if e.Type == TYPE_PASS {
                                processingTimePassed.add(e.ProcessingTime)
                        }

                        fmt.Print("\033[H\033[2J")
                        fmt.Printf("total: %d. passed: %d. dropped: %d. passed processing time avg (ns): %f. dropped processing time avg (ns): %f\n", buckets[TYPE_ENTER], buckets[TYPE_PASS], buckets[TYPE_DROP], processingTimePassed.avg(), processingTimeDropped.avg())
                }
        }()

        c := make(chan os.Signal, 1)
        signal.Notify(c, os.Interrupt, syscall.SIGTERM)
        <-c
}

Вам может понадобиться выполнить go mod init && go mod tidy, если вы еще этого не сделали.

Код настраивает необходимые компоненты для приложения на Golang: загружает программу BPF, подключает её к указанному сетевому интерфейсу и инициализирует reader для обработки событий perf. Однако код для чтения и обработки событий perf ещё не реализован.

На этом текущий раздел завершается. Содержимое можно изменять и настраивать в соответствии со своими требованиями.

Сборка и запуск проекта

Теперь, когда мы реализовали XDP eBPF программу на C и приложение на Golang, давайте соберём и запустим проект.

Сборка XDP eBPF программы 

Этот шаг можно пропустить, если вы уже скомпилировали dilih_kern.o, выполняя предыдущие инструкции.

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

Для сборки программы перейдите в каталог bpf и выполните следующую команду:

make

Эта команда скомпилирует код на C и создаст объектный файл dilih_kern.o.

Сборка приложения на Golang

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

CGO_ENABLED=0 go build

Эта команда скомпилирует код на Golang и создаст исполняемый бинарный файл. Обратите внимание, что для этого приложения CGO не требуется. Хотя его можно оставить включённым, я предпочитаю использовать CGO_ENABLED=0, так как это позволяет создать статически скомпилированный бинарный файл, который легко загружать в контейнеры.

Запуск проекта на Go

sudo ./dilih

Вы увидете сводку об обработанных пакетах на указанном интерфейсе:

Запускайте приложение с повышенными привилегиями (через sudo), чтобы получить доступ к необходимым ресурсам.

Приложение на Golang начнёт собирать данные из программы XDP и отображать статистику на основе полученных событий perf.

Чтобы очистить проект и удалить программу XDP с сетевого интерфейса, выполните следующую команду:

sudo make clean

Эта команда отключит программу XDP от сетевого интерфейса и удалит все связанные артефакты.

Готово! Вы успешно собрали и запустили проект. Попробуйте подключить разные сетевые интерфейсы и наблюдайте статистику сброса пакетов, отображаемую приложением.

Вы можете также исследовать дополнительные функции и модификации, чтобы улучшить проект.

Тестирование и проверка XDP eBPF программы

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

Настройка тестовой среды

Для создания подходящей тестовой среды мы будем использовать виртуальные сетевые интерфейсы (veth-устройства) для имитации сетевого трафика и наблюдения за поведением программы XDP.

Установите пакет iproute2, если он ещё не установлен на вашей системе. Этот пакет предоставляет необходимые инструменты для управления сетевыми интерфейсами.

Создайте пару veth-устройств, используя следующие команды:

sudo ip link add veth0 type veth peer name veth1

Эта команда создаст два виртуальных сетевых интерфейса (veth0 и veth1), соединеных друг с другом.

Настройте интерфейсы и назначьте им IP-адреса:

sudo ip link set veth0 up
sudo ip link set veth1 up
sudo ip addr add 10.0.0.1/24 dev veth0
sudo ip addr add 10.0.0.2/24 dev veth1

Эти команды активируют интерфейсы и назначат им IP-адреса (10.0.0.1 и 10.0.0.2).

С настроенными устройствами veth мы можем приступить к тестированию и проверке функциональности XDP eBPF программы.

Проверка сброса пакетов

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

Откройте два окна терминала и перейдите в каталог проекта в обоих окнах.

В первом терминале выполните следующую команду для прослушивания ICMP-запросов эхо (ping):

sudo tcpdump -i veth1 icmp

Во втором терминале отправьте ICMP-запросы эхо (ping) с veth0 на veth1, используя следующую команду:

sudo ip netns exec veth0 ping 10.0.0.2

Наблюдайте за выводом в первом терминале. Вы должны увидеть захваченные ICMPАнализируйте захваченные пакеты, чтобы проверить процент сброса. Если программа XDP работает корректно, около 50% ICMP-запросов эхо должно быть сброшено, что приведёт к уменьшению количества захваченных пакетов.

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

Анализ производительности

Помимо функциональной проверки, важно проанализировать влияние XDP eBPF программы на производительность. Этот анализ помогает оценить эффективность и накладные расходы, вводимые программой.

  1. Используйте приложение на Golang для сбора метрик производительности и статистики из программы XDP. Обратитесь к разделу «Сборка и запуск проекта» для инструкций по запуску приложения на Golang.

  2. Мониторьте и наблюдайте за средним временем обработки как для пропущенных, так и для сброшенных пакетов. Приложение на Golang отображает среднее время обработки в наносекундах (нс) для каждого типа пакетов.

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

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

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

  4. Экспериментируйте с разными процентами сброса пакетов и наблюдайте их влияние на среднее время обработки. Изменяя процент сброса, вы сможете оценить баланс между потерей пакетов и эффективностью обработки.

  5. Проведение анализа производительности позволяет получить представление о влиянии XDP eBPF программы на производительность сети и принять обоснованные решения относительно её оптимизации и настройки.

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

Для обеспечения корректной интеграции XDP eBPF программы в общую систему важно провести интеграционное и системное тестирование. Это включает тестирование взаимодействия программы XDP с сетевым стеком и другими компонентами системы.

Создайте тестовый сценарий, максимально приближенный к продакшен-среде, в которой будет работать программа XDP. Учитывайте такие факторы, как структура сетевого трафика, системная нагрузка и наличие других сетевых компонентов.

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

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

Проверьте крайние случаи и граничные условия, чтобы оценить устойчивость и надёжность программы XDP. Это включает сценарии с высоким объёмом сетевого трафика, необычными структурами пакетов и непредвиденными сетевыми событиями.

Проводя интеграционное и системное тестирование, вы сможете убедиться, что XDP eBPF программа интегрируется в общую систему без сбоев и работает надёжно в различных условиях.

Заключение

Приводим список полезных ресурсов для дальнейшего изучения XDP, eBPF и сетевого программирования:

  • Cilium: проект с открытым исходным кодом, предоставляющий сетевые и защитные возможности на основе eBPF. Их документация и исходный код дают глубокое представление о применении eBPF. 

  • Iovisor: проект с открытым исходным кодом, который разрабатывает инструменты, библиотеки и инфраструктуру для трассировки, мониторинга и сетевой работы на основе eBPF. На сайте проекта можно найти обучающие материалы, документацию и примеры кода.

  • BCC (BPF Compiler Collection): набор мощных инструментов командной строки и библиотек, использующих eBPF для различных задач трассировки и анализа производительности. Репозиторий на GitHub предоставляет документацию и примеры для углублённого изучения eBPF.

  • eBPF.io: сайт сообщества, посвящённый ресурсам, руководствам и новостям о eBPF. Здесь вы найдёте статьи, примеры и подборку инструментов и библиотек, связанных с eBPF. 

  • Документация ядра Linux — включает обширный раздел по eBPF и XDP, охватывающий различные аспекты, включая справочные API, примеры использования и детали реализации. Доступ к документации: www.kernel.org/doc/html/latest/bpf 

Полный исходный код статьи можно найти на github.


Больше про языки программирования эксперты OTUS рассказывают в рамках практических онлайн-курсов. С полным каталогом курсов можно ознакомиться по ссылке. Приходите также на бесплатные открытые уроки:

  • 28 ноября: «Язык Cи и ООП: пошаговая разработка видеоплеера» — узнаем, как применить принципы ООП в языке С для создания сложных программ. Разберем практический пример разработки видеоплеера с использованием объектно-ориентированного подхода. Записаться

  • 2 декабря: «Взаимодействие с базой данных и миграции на Go» — научимся создавать таблицы и настраивать структуру БД, разрабатывать БД для веб-сервера на Go, понимать процесс миграции БД и работать с запросами на уровне ОРМ и чистого SQL. Записаться