Мишка — символ языка P4, который был впервые описан в 2014 году

Эта первая часть обзорной статьи, в которой мы разбираемся с молодым языком программирования 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

Программный коммутатор 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

CONTAINER ID 

IMAGE

COMMAND

CREATED

STATUS

PORTS NAMES

db924306352f

ubuntu:14.04

"/bin/bash"

2 minutes ago

Up 2 minutes

host2

ae5e987dee8c 

ubuntu:14.04

"/bin/bash"

2 minutes ago

Up 2 minutes

host1

aed19d76cd3a

docker-sonic-p4:latest

"/bin/bash"

2 minutes ago

Up 2 minutes

switch2

680f95a83512

docker-sonic-p4:latest

"/bin/bash"

2 minutes ago

Up 2 minutes

switch1

  • Ждем минуту до загрузки и запускаем ./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$ docker exec -it switch1 bash
root@switch1:/# vtysh -c "show ip bgp sum"
BGP router identifier 192.168.1.1, local AS number 10001
RIB entries 3, using 336 bytes of memory
Peers 1, using 4568 bytes of memory
  • Запускаем ./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. Во второй части – разберем сетевую архитектуру и поэкспериментируем. Так что подписывайтесь и оставайтесь с нами!