Язык сетевого программирования P4.

Часть 1: обзор возможностей и настройка SONiC-P4
Эта первая часть обзорной статьи, в которой мы разбираемся с молодым языком программирования P4: что это такое, для чего он нужен и чем лучше прочих систем обработки пакетов. Конечно, будет и практика: примеры программирования и обзор железа с поддержкой P4. А на десерт — пошаговая настройка виртуального коммутатора Sonic-P4. Поехали!

Начнем с теории. Язык P4 или Programming Protocol-Independent Packet Processors — общедоступный сетевой предметно-ориентированный язык для описания плоскости данных в сети.

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

Язык P4 — протокольно-независимый: программист сам описывает заголовки с именами полей и выбирает любой нужный протокол.

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

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

Коммутаторы: стандартный и программируемый на P4
На схеме показана разница между стандартным и программируемым коммутатором на P4. В стандартном устройстве производитель определяет функциональность плоскости данных. Плоскость управления контролирует плоскость данных, а именно:

  • управляет записями в таблицах;
  • настраивает специализированные объекты, например, регистры;
  • обрабатывает управляющие пакеты.
Теперь взглянем на два основных отличия коммутатора P4:

  1. Функциональные возможности плоскости данных не задаются первоначально, т.е. программист может их описать. Настройка плоскости данных в соответствии с кодом происходит при инициализации устройства. Отметили это красной стрелкой на схеме.
  2. Взаимодействие плоскости управления и плоскости данных происходит по тем же каналам, что и в стандартном коммутаторе, но при этом набор таблиц и других объектов в плоскости данных уже не является фиксированным и определяется программой P4. Компилятор P4 генерирует API, который использует плоскость управления для связи с плоскостью данных.
Абстракции языка P4
Плавно переходим к абстракциям языка P4. Кратко опишем сущность каждой:

  • Заголовки используют для описания формата или набора полей и их размеров — для каждого заголовка в пакете.
  • Парсеры или синтаксические анализаторы описывают разрешенные последовательности заголовков в получаемых пакетах, правила идентификации заголовков и поля для извлечения из пакетов. По сути, основная задача синтаксических анализаторов — правильно идентифицировать и разобрать заголовок.
  • Таблицы P4 объединяют ключи, действия и связи между ними. Они обобщают традиционные таблицы коммутации и помогают реализовать таблицы маршрутизации, списки управления доступом и создание сложных пользовательских таблиц. Единственное, что ограничивает количество таблиц — потребности программиста. Вхождения в таблицы происходят последовательно, если не создана абстрактная таблица, которая состоит из нескольких пользовательских.
  • Действия — фрагменты кода, описывающие правила обработки полей заголовка для пакета и метаданных.
  • Модули Match-Action — составная часть таблиц для выполнения следующей последовательности:
  1. Создание ключа поиска из полей пакета или вычисленных метаданных.
  2. Выполнение поиска в таблице для сопоставления созданного ключа и нужного действия.
  3. Выполнение найденного действия.
  • Поток управления — императивная программа, которая описывает процесс обработки пакетов на устройстве, включая последовательность вызовов модулей Match-Action.
  • Внешние объекты — архитектурно-зависимые конструкции, которыми управляют программы P4 при помощи четко определенных API. Важно: внутреннее поведение внешних объектов скорректировать невозможно. Примеры таких объектов: контрольная сумма, счетчики, регистры и т.д.
  • Пользовательские метаданные — структуры данных, которые связаны с каждым пакетом. Их определяет пользователь.
  • Внутренние метаданные — структуры данных, которые связаны с каждым пакетом. Их определяет архитектура.
Пример программирования устройства на P4
Производители устройств предоставляют аппаратную или программную среду реализации, архитектуру и компилятор P4.

Пользователь определяет программу на P4 для конкретной архитектуры. Компилятор генерирует конфигурацию плоскости данных, которая реализует логику пересылки и API для управления состоянием объектов плоскости данных из плоскости управления.
На схеме — пример программирования устройства на P4
Предметно-ориентированный язык P4 реализуется на разных устройствах, включая программируемые сетевые интерфейсные карты, FPGA, программные коммутаторы и аппаратные ASIC. И разработчики программ на P4 ограничены конструкциями, которые можно эффективно реализовать на всех этих платформах.

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

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

Давайте посмотрим на P4 на живых примерах. Ниже – заголовок, парсер и таблица для модели Very Simple Switch.
#include <core.p4>

#include <v1model.p4>

 

const bit<16> TYPE_IPV4 = 0x800;

 

/*******************************************

********** H E A D E R S  ******************

*******************************************/

typedef bit<9>  egressSpec_t;

typedef bit<48> macAddr_t;

typedef bit<32> ip4Addr_t;

 

header ethernet_t {

   macAddr_t dstAddr;

   macAddr_t srcAddr;

   bit<16>   etherType;

}

header ipv4_t {

   bit<4>    version;

   bit<4>    ihl;

   bit<8>    diffserv;

   bit<16>   totalLen;

   bit<16>   identification;

   bit<3>    flags;

   bit<13>   fragOffset;

   bit<8>    ttl;

   bit<8>    protocol;

   bit<16>   hdrChecksum;

   ip4Addr_t srcAddr;

   ip4Addr_t dstAddr;

}
/***************************************************************

*********************** P A R S E R  ***************************

***************************************************************/

 

parser MyParser(packet_in packet,

               out headers hdr,

               inout metadata meta,

               inout standard_metadata_t standard_metadata) {

   state start {

       transition parse_ethernet;

   }

   state parse_ethernet {

       packet.extract(hdr.ethernet);

       transition select(hdr.ethernet.etherType) {

           TYPE_IPV4: parse_ipv4;

           default: accept;

       }

   }

   state parse_ipv4 {

       packet.extract(hdr.ipv4);

       transition accept;

   }

}
   action drop() {

       mark_to_drop(standard_metadata);

   }

  

   action ipv4_forward(macAddr_t dstAddr, egressSpec_t port) {

       standard_metadata.egress_spec = port;

       hdr.ethernet.srcAddr = hdr.ethernet.dstAddr;

       hdr.ethernet.dstAddr = dstAddr;

       hdr.ipv4.ttl = hdr.ipv4.ttl - 1;

   }

  

   table ipv4_lpm {

       key = {

           hdr.ipv4.dstAddr: lpm;

       }

       actions = {

           ipv4_forward;

           drop;

           NoAction;

       }

       size = 1024;

       default_action = drop();

   }

  

   apply {

 

       if (hdr.ipv4.isValid()) {

           ipv4_lpm.apply();

       }

   }
Полюбовавшись на заголовки и парсеры, попробуем понять, а чем же P4 так хорош и хорош ли?
Преимущества языка P4
P4 превосходит современные системы обработки пакетов в следующих моментах:

  1. Гибкость: P4 помогает представить политики пересылки пакетов в виде программ, в отличие от традиционных коммутаторов, которые дают пользователям механизмы пересылки с фиксированными функциями.
  2. Выразительность: с P4 легко представлять сложные аппаратно независимые алгоритмы обработки пакетов. При этом использовать нужно исключительно операции общего назначения и поиск по таблицам. Такие программы переносимы между целевыми аппаратными средствами, которые реализуют одни и те же архитектуры, если нам хватает ресурсов.
  3. Управление ресурсами: программы P4 абстрактно описывают ресурсы хранения, например, адрес источника IPv4; компиляторы сопоставляют поля с доступными аппаратными ресурсами, которые определяет пользователь, и управляют низкоуровневыми элементами.
  4. Преимущества при разработке ПО: проверка типов, скрытие информации и повторное использование кода (software reuse).
  5. Библиотеки: библиотеки компонентов производителей могут использоваться для обертывания аппаратно-зависимых функций в переносимые высокоуровневые конструкции P4.
  6. Разделение аппаратного и программного обеспечения: производители устройств могут использовать абстрактные архитектуры, чтобы четко отделить низкоуровневые архитектурные элементы от высокоуровневой обработки.
  7. Отладка: производители предоставляют программные модели архитектуры для простоты разработки и отладки программ P4.
Гибкость, простота, эффективность – вроде здорово, но много ли у нас железа с поддержкой P4? Разбираемся дальше.
Устройства с поддержкой P4
P4 – относительно новый язык программирования, поэтому аппаратная поддержка не так сильна, как хотелось бы. Ниже перечислим продукты, на которых можно реализовать полную спецификацию языка.

Чипы Barefoot Tofino 2

Инженеры компании Barefoot Networks создали язык P4 и чипсет типа ASIC, который не использует проприетарный SDK, такой как Broadcom или Cavium.

Скоростные каналы SerDes Barefoot Tofino 2 Chip и более высокий предел пропускной способности, по сравнению с ASIC предыдущего поколения, в разы масштабируют производительность. Архитектура Intel Tofino 2 также дает больше ресурсов для обработки жестких рабочих нагрузок в распределенных приложениях, масштабировании виртуальных машин, искусственном интеллекте и бессерверных развертываниях.

Платформа Barefoot Tofino 2 и сравнение чипов в линейке Tofino
Netronome Agilio SmartNIC

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

Netronome Agilio CX
Xilinx Alveo

Устройства Xilinx с поддержкой P4 представляют собой смарт-карты на базе FPGA из линейки продуктов Alveo. Более старые версии карт Alveo оснащены схемами Xilinx Zynq UltraScale + FPGA, а более новые версии включают специализированную FPGA Xilinx UltraScale+, которая установлена только в линейке Alveo.

Карта Alveo U25 доступна с двумя сетевыми интерфейсами 10/25 GbE, а все более новые версии имеют один или два интерфейса 100 GbE.

У Xilinx есть своя целевая платформа FPGA для определения обработки пакетов в плоскости данных — SDNet. С точки зрения P4, среда SDNet предлагает несколько базовых архитектурных моделей, которые могут использовать программисты P4. Эти архитектуры включают XilinxSwitch, XilinxStreamSwitch и XilinxEngineOnly.

Xilinx Alveo U25
Xilinx Alveo U25
Программный коммутатор SONiC-P4
Что такое SONiC-P4?

SONiC-P4 — программный коммутатор на базе ASIC, который эмулируется P4 и использует sai_bm.p4 для программирования ASIC-коммутатора. Также он запускает сетевой стек SONiC. Текущую версию SONiC-P4 выпустили в виде образа докера. SONiC-P4 запускается везде, где работает докер — на «голом железе» Linux / Windows-машины, внутри виртуальной машины или в облачной среде.

Как использовать SONiC-P4?

Продемонстрируем использование программного коммутатора SONiC-P4 на простом стенде:
Топология тестового стенда для коммутатора SONiC-P4
Switch1 и switch2 — два коммутатора SONiC-P4 в двух разных BGP AS, которые взаимодействуют друг с другом. Switch1 объявляет 192.168.1.0/24, switch2 — 192.168.2.0/24.

  • На сервере Ubuntu качаем необходимые файлы. Разархивируем файл и переходим в каталог sonic/.
  • Запускаем ./install_docker_ovs.sh, чтобы установить docker и open-vswitch.
  • Запускаем ./load_image.sh, чтобы загрузить образ SONiC-P4.
  • Запускаем ./start.sh, чтобы настроить стенд. Если все ок, появятся четыре докера.
lgh@acs-p4:~/sonic$ docker ps
  • Ждем минуту до загрузки и запускаем ./test.sh, который пингует host2 с host1.
lgh@acs-p4:~/sonic$ ./test.sh                 
PING 192.168.2.2 (192.168.2.2) 56(84) bytes of data.
64 bytes from 192.168.2.2: icmp_seq=1 ttl=62 time=9.81 ms
64 bytes from 192.168.2.2: icmp_seq=2 ttl=62 time=14.9 ms
64 bytes from 192.168.2.2: icmp_seq=3 ttl=62 time=8.42 ms
64 bytes from 192.168.2.2: icmp_seq=4 ttl=62 time=14.7 ms
  • Проверяем BGP на switch1
lgh@acs-p4:~/sonic$ ./test.sh                 
PING 192.168.2.2 (192.168.2.2) 56(84) bytes of data.
64 bytes from 192.168.2.2: icmp_seq=1 ttl=62 time=9.81 ms
64 bytes from 192.168.2.2: icmp_seq=2 ttl=62 time=14.9 ms
64 bytes from 192.168.2.2: icmp_seq=3 ttl=62 time=8.42 ms
64 bytes from 192.168.2.2: icmp_seq=4 ttl=62 time=14.7 ms
  • Запускаем ./stop.sh для очистки.
Настройка топологии в start.sh

Для настройки топологии в start.sh мы запускаем четыре контейнера докеров. В качестве примера возьмем команду для switch1:
sudo docker run --net=none --privileged --entrypoint /bin/bash 
--name switch1 -it -d -v $PWD/switch1:/sonic docker-sonic-p4:latest
Мы указываем --net = none, чтобы механизм Docker не добавил свой интерфейс docker0, который может помешать тестируемой топологии.

--privileged позволяет каждому контейнеру настраивать свои интерфейсы.

-v $ PWD / switch1: / sonic монтирует папку конфигурации в контейнеры коммутатора.

Затем создаем три связи. В качестве примера возьмем связь между switch1 и switch2. Следующие команды соединяют интерфейс eth1 switch1 с интерфейсом eth1 switch2:
sudo ovs-vsctl add-br switch1_switch2
sudo ovs-docker add-port switch1_switch2 eth1 switch1
sudo ovs-docker add-port switch1_switch2 eth1 switch2
Также настраиваем IP-адрес интерфейса и маршруты по умолчанию на host1 и host2. Пример для host1:
sudo docker exec -d host1 ifconfig eth1 192.168.1.2/24 mtu 1400
sudo docker exec -d host1 ip route replace default via 192.168.1.1
Наконец, вызываем сценарий запуска для switch1 и switch2:
sudo docker exec -d switch1 sh /sonic/scripts/startup.sh
sudo docker exec -d switch2 sh /sonic/scripts/startup.sh
Конфигурация SONiC-P4
В start.sh мы смонтировали папку конфигурации в контейнер коммутатора в /sonic. Наиболее важные конфигурации находятся в /sonic/scripts/startup.sh, /sonic/etc/config_db/vlan_config.json и /sonic/etc/quagga/bgpd.conf.

В /sonic/scripts/startup.sh запускаем все службы SONiC и сам программный коммутатор P4 следующей строкой:

simple_switch --log-console -i 1@eth1 -i 2@eth2 …

Это действие связывает интерфейс eth1 с портом 1 программного коммутатора P4, eth2 — с портом 2 и так далее. Эти интерфейсы ethX обычно называются интерфейсами передней панели и напрямую используются коммутаторами P4 для передачи пакетов плоскости данных.

Однако SONiC работает с интерфейсами другого типа, так называемыми хост-интерфейсами EthernetX. Они предназначены для плоскости управления SONiC и НЕ несут пакеты плоскости данных.

Мы настраиваем одноранговый IP и MTU на хост-интерфейсах. SONiC считывает конфигурации, такие как IP и MTU, с интерфейсов хоста, а затем настраивает эти значения на программном коммутаторе P4 с помощью SAI.

Сопоставление между интерфейсами хоста и портами коммутатора указано в /port_config.ini:
# alias         lanes
Ethernet0       1
Ethernet1       2
…
Вместе с командой simple_switch в /sonic/scripts/startup.sh мы настроили следующее отображение: Ethernet0 -> lane 1 -> eth1. По сути, это отображение между интерфейсами хоста и интерфейсами передней панели.

/sonic/etc/config_db/vlan_config.json настраивает интерфейсы коммутатора vlan, который мы используем в этом эксперименте, через интерфейс ConfigDB (подробности — в руководстве SONiC Configuration Database):
{
    "VLAN": {
        "Vlan15": {
            "members": [
                "Ethernet0"
            ], 
            "vlanid": "15"
        }, 
        "Vlan10": {
            "members": [
                "Ethernet1"
            ], 
            "vlanid": "10"
        }
    },
    "VLAN_MEMBER": {
        "Vlan15|Ethernet0": {
            "tagging_mode": "untagged"
        },
        "Vlan10|Ethernet1": {
            "tagging_mode": "untagged"
        }
    },
    "VLAN_INTERFACE": {
        "Vlan15|10.0.0.0/31": {},
        "Vlan10|192.168.1.1/24": {}
    }
}
/sonic/etc/quagga/bgpd.conf настраивает сеанс BGP на коммутаторе. Вот конфигурация BGP для switch1, который взаимодействует с switch2, используя одноранговый IP-адрес 10.0.0.0/31, и объявляет 192.168.1.0/24:
router bgp 10001                        
  bgp router-id 192.168.1.1             
  network 192.168.1.0 mask 255.255.255.0
  neighbor 10.0.0.1 remote-as 10002     
  neighbor 10.0.0.1 timers 1 3          
  neighbor 10.0.0.1 send-community      
  neighbor 10.0.0.1 allowas-in          
  maximum-paths 64                      
!                                       
access-list all permit any
Итак, пока это все. В этой первой части мы познакомились с языком P4, подходящими для него hardware-платформами и настроили коммутатор SONiC-P4. Во второй части – разберем серверную архитектуру и поэкспериментируем.
Впервые эта статья была опубликована в нашем блоге на Хабре: «Язык сетевого программирования P4. Часть 1: обзор возможностей и настройка SONiC-P4»

Присоединяйтесь к нашей команде!

Приглашаем в нашу команду сетевого разработчика для развития направления Телеком