Intro
P4는 Programmable Switch, 즉 Hardware에 묶여있는 Software를 통해 DataPlane을 사용하는 것이 아닌 필요에 따라 Switch의 DataPlane을 직접 Programming하여 사용하는 새로운 방법론을 제시한다.
History
예전 Posting에서 🔗 OpenFlow와 🔗 SDN에 대해서 다룬 적이 있다. 이때, P4에 대해서 간략히 알아보고 지나갔었는데, 해당 Posting에서는 이를 자세히 다룰 것이다. 만약, 이에 대한 개념이 잡혀있지 않다면, 해당 Posting을 이해하기 어렵다. 따라서, SDN과 OpenFlow 관련 Posting을 먼저 읽고 다시 돌아오기를 바란다.
먼저, P4의 초기 시작은 OpenFlow의 성장과 연관이 있다. OpenFlow는 기존의 Switch를 Data Plane과 Control Plane으로 나누어 Control Plane을 외부 Server(Controller)로 옮기고 이들은 OpenFlow Protocol을 통해서 Switch의 Data Palen을 제어하고자 했다. OpenFlow의 사용이 가속화되면서 계속해서 새로운 version이 update되었고, 1.0에서 1.5까지 도달하게 되었다. 그런데, 여러 Usecase와 Protocol을 지원하기 위해서 점점 비대해지는 Header를 마주치게 된다. 이는 통신 속도의 저하를 야기할 뿐만 아니라 기존의 유연한 시스템을 만들고자한 OpenFlow의 탄생 배경과도 거리가 있게 된다. 따라서, Data Plane을 필요에 따라 Protocol, Header, Algorithm을 포함하여 Programming으로 구현하여 사용하면 좋지 않을까라는 발상에서 시작된 것이 P4이다. 그렇기에 P4의 full name이 Programming Protocol-independent Packet processors인 것이다.
이렇게 시작된 Project는 점점 비대해지며, 원래는 Programmable Switch를 위한 언어를 목적으로 했다면 현재에는 여러 목적의 장비(target)을 지원하는 언어로 확장되었다. 따라서, P4를 한 문장으로 정의하라고 하면, Programmable Target의 Data Plane을 Programming하는 언어라고 할 수 있다.
동작원리
P4를 통한 Programming을 통해서 우리가 최종적으로 만드는 것은 두 가지이다. 첫번째로, DataPlane runtime
은 Table과 Action 그리고 적절한 Alogrithm을 통해 실제로 Packet을 처리 및 Forwarding할 Software를 제작하고, Control Plane에서 Data Plane을 제어 및 설정을 하기 위한 API를 제작한다.
이를 위해서 Target을 제작하는 Vender는 다음과 같은 3가지를 제작한다.
- P4 Architecture : 해당 Target에서 작동이 가능한 Interface와 extern object를 정의해놓은 명세서이다. 후에 P4 programmer는 이를 참고하여 해당 Interface에 해당하는 Package를 작성하면 된다.
- P4 Compiler : Programmer가 작성한 Code를 Compile하여 API와 DataPlane Runtime을 생성한다.
- Target(Hardware) : 실제로 동작하는 Hardware이다.
이제 P4 개발자(Programmer)는 P4 Architecture를 기반으로 하여 원하는 동작을 구현한다. 이를 Compiler를 통해서 실행시켜 정상동작 여부를 확인하여 최종적으로 동작하는 P4 Target을 완성할 수 있다.
이렇게 보았듯이 P4 Programming 과정은 Target 디자이너(Vender)에 의해서 만들어진 객체 지향 Architecture에 기반하여 Implementation을 수행하는 것이 핵심이다. 아마 객체 지향에 익숙하다면 굉장히 친숙한 개념일 것이다.
구성요소
P4 programming을 통해서 실제로 작성하거나 사용하는 구성요소들은 아래와 같다.
- Architecture : P4를 지원하는 지원하는 Switch의 구성요소들의 interface(객체지향에서의 interface)를 작성한 명세서로 이는 Programmer가 아닌 해당 Target을 제작한 Vender가 작성한다. 이를 기반으로 P4 Programmer는 Implementation을 수행한다.
- Header Type : 해당 Data Plane에서 사용할 각각의 Packet의 Header의 형태를 의미한다. 즉, Packet의 Header 부분을 정의한 것이다.
- Parser : 연속으로 들어오는 Packet에서 Header를 식별하고 추출하는 역할을 한다.
- Table : Programmer에 의해서 정의된 key와 이에 대응하는 Action이 저장된다. 이를 통해서, Routing Table, Flow Lookup Table, ACL 등과 같은 일반 Switch의 Table도 구성이 가능하며, 더 복잡한 형태의 새로운 Table을 구성하는 것도 가능하다.
- Action : Header와 Metadata에 특정한 조작을 가할 수 있다.
- Match-action unit : 실제로 Table을 조회하여, Action을 수행하는 Unit으로 동작순서는 다음과 같다.
- Packet의 field와 metadata를 활용하여 key를 생성한다.
- 생성한 key로 Table에서 조회(Lookup)한다.
- 조회된 key에 대응하는 action이 존재한다면, 이를 수행한다.
- User-defined metadata : Programmer(user)에 의해서 정의된 data 구조로 각 packet에서 추출이 가능하다.
- Intrinsic metadata : 기본적으로 정의된 data 구조로 각 packet에서 추출이 가능하다.
- Extern object : 일반적인 Programming Language에서 Library와 같은 역할을 하며, P4가 실행될 Hardware에서 제공하는 기능들이 여기에 포함된다. P4에서 이를 이용하여 Programming이 가능하지만, 이는 P4를 지원하는 Hardware Switch 제작자가 지정하는 것이기에 P4를 통한 구현은 불가능하다.
- Deparser : Packet의 Header와 Payload를 다시 결합하여 output port로 내보낼 Packet을 생성한다.
- Control flow : target의 packet 처리를 기술하는 필수적인 program 요소이다. Deparsing과 Match-action 등을 이를 통해서 기술할 수 있다.
Example
가장 기본적인 Siwtch(Very Simple Switch, VSS)를 구현하며 P4의 programming 절차를 익혀보자. 아래 그림은 VSS를 분석한 그림이다.
Flow
총 3가지의 Flow가 존재한다. 하나는 일반적인 데이터 Packet을 의미하며 Target의 Physical Ethernet을 통해서 들어온 packet이 이에 해당한다. 또 다른 하나의 Control Flow로 대게 SDN의 Controller를 통해서 들어온 Packet이다. 대게 이는 CPU를 통해서 전달되기 때문에 From CPU라고 표기한다. 마지막은 Recirculate인데 이는 동일 Target 내부에서 재처리를 위해서 다시 Arbiter로 전달된 Packet을 의미한다. 이는 한 번의 순환으로는 제대로 된 처리가 어려운 경우에 사용한다.
Component
주황색 박스의 요소는 Hardware로 정의된 요소(Arbiter, Parser Runtime, Demux/Queue)를 의미하고, 검은 박스의 요소(Parser, Match-action Pipeline, Deparser)는 Software로 구현되는 요소를 의미한다. 각 요소를 먼저 알아보도록 하자.
- Arbiter
한국어로는 중재자라는 뜻을 가지며, Input을 일차적으로 가공하는 Block(장치)으로 아래와 같은 기능을 수행한다.- 총 16개의 Port를 가지며, 3가지 종류의 Input을 입력받는다. (1)Physical Ethernet Input, (2)Control Flow(from CPU), (3)Recirculate Input을 받는다.
- Ethernet Input인 경우, checksum을 추출하고 검증한다. 만약, 올바르지 않은 checksum이라면 packet이 버려지고, 올바르다면 packet의 payload에서 checksum을 추출하여 다음 단계로 전달한다.
- 여러 packet을 동시에 수신한 경우에는 이를 scheduling하는 Algorithm을 실행시킨다.
- Arbiter가 Busy 상태이고, 대기 queue가 꽉 찬 경우에는 도착한 packet을 Drop한다.
- packet을 받은 port를
inCtrl.inputPort
에 저장하여, Match-action Pipeline으로 전달하여 packet이 어디서부터 왔는지를 marking한다. 여기서는 Physical Ethernet port는 0 ~ 7번까지를 의미하고, 13은 recirculation port, 14는 CPU port를 의미한다.
- Parser Packet을 가공하여 Input Header를 추출하고, user defined metadata를 생성하여 이를 Match-action Pipeline으로 전달한다.
- Parser Runtime Parser와 협력을 하며 실행되는 장치이다. Parser에 정의된 action에 기반하여 Match-action Pipeline으로 error code를 Match-action Pipelien, packet payload에 대한 정보(payload 길이 등)를 Demux로 전달한다.
- Match-action Pipeline
Parser로 부터 전달받은 Input Header와 Parser Runtime으로 부터 받은 error, Arbiter를 통해 받은
inCtrl.inputPort
를 기반으로 하여 key를 구성하고, 이를 통해서 Table을 조회하여 적절한 Action을 조회하여 실행한다. 이를 통해서 결론적으로 Output Header와outCtrl.outputPort
를 생성한다. - Deparser Deparser의 역할은 output Header를 다시 재조립하는 장치이다. 온전한 header 형태를 완성해서 Demux에서 Packet을 최종으로 생성할 수 있도록 돕는다.
- Demux/Queue
밖으로 전달할 packet의 header는 Deparser로부터, payload는 Parser로 부터 전달받아서 이를 재결합하여 새로운 packet을 생성하여 올바른 output port로 내보내는 역할을 하는 장치이다. output port는 Matc-action Pipeline의
outCtrl.outputPort
를 통해서 전달된다. 세부적인 동작은 아래와 같다.- packet 삭제를 원하는 경우 drop port로 packet을 내보낸다.
- Physical Ethernet으로 나가는 packet인 경우 해당하는 output interface로 전달한다. 만약, 해당 interface가 Busy 상태라면 Queue에 저장된다. output interface에서는 packet의 checksum을 계산하여 packet의 끝에 붙여서 내보낸다.
- CPU를 통해서 전달하는 Control plane packet의 경우에는 Deparser에서 생성된 Header를 사용하지 않고, original packet 그대로만 전송할 수 있다.
outCtrl.outputPort
가 올바르지 않다면, 해당 packet은 drop된다.- 만약, Demux가 Busy 상태이고, queue에 빈 공간이 존재하지 않는다면,
outCtrl.outputPort
를 무시하고 packet을 drop한다.
Architecture.p4
아래는 위의 내용을 기반으로 Vender가 작성한 Architecture의 내용이다.
1// File: "very_simple_switch_model.p4" 2// Core Library로 packet_in과 packet_out을 사용하기 위해 필요하다. 3#include <core.p4> 4 5// Port는 4bit로 표현 가능하다. 6typedef bit<4> PortId; 7 8// 4 width(bit)로 표현하는 8, 생략해서 8만 써도 무방 9const PortId REAL_PORT_COUNT = 4w8; 10 11// Input packet에 동반되는 metadata로 Match-action Pipeline에서 사용된다. 12struct InControl { 13 PortId inputPort; 14} 15 16// Special Input Port 17const PortId RECIRCULATE_IN_PORT = 0xD; 18const PortId CPU_IN_PORT = 0xE; 19 20// Match-action Pipeline에서 생성되는 Output packet에 동반되는 metadata 21struct OutControl { 22 PortId outputPort; 23} 24 25// Special Output Port 26const PortId DROP_PORT = 0xF; 27const PortId CPU_OUT_PORT = 0xE; 28const PortId RECIRCULATE_OUT_PORT = 0xD; 29 30// 공통으로 사용되는 H는 header로 programmer에 의해서 정의된다. 31 32/** 33 * Parse 34 * @param b input_packet 35 * @param parsedHeaders headers contructed by parser 36 */ 37parser Parser<H>(packet_in b, out H parsedHeaders); 38 39 40/** 41 * Match-action Pipeline 42 * @param headers Parser로 부터 받고, Deparser에게 보낸다. 43 * @param parseError parsing 도중에 생성된 error 44 * @param inCtrl packet을 받은 input port를 포함한 정보 45 * @param outCtrl packet을 보낼 output port를 포함한 정보 46 */ 47control Pipe<H>(inout H headers, 48 in error parseError, 49 in InControl inCtrl, 50 out OutControl outCtrl); 51 52/** 53 * Deparser 54 * @param outputHeaders programmer에 의해서 정의된 output header 55 * @param b 밖으로 내보낼 packet 56 */ 57control Deparser<H>(inout H outputHeaders, packet_out b); 58 59/** 60 * Top level Packet 61 */ 62packege VSS<H>(parse<H> p, Pipe<H> map, Deparser<H> d); 63 64/** 65 * Extern block 66 * 이는 Target vender가 내부적으로 구현한 block이다. 67 * programmer는 아래 선언된 block을 자유롭게 사용가능하다. 68 */ 69extern Checksum16 { 70 Checksum16(); 71 void clear(); 72 void update<T>(in T data); 73 void remove<T>(in T data); 74 bit<16> get(); 75}
이렇게 작성된 architecture를 기반으로 하여 programmer는 코드 작성이 가능하다.
Programming.p4
먼저 어떤 동작을 수행하게 할지를 정의하자.
- Ethernet/IPv4 header를 활용하여 Forwarding을 수행할 것이다.
- CheckSum을 확인하여 정당성 여부를 확인한다.
- TTL 값을 확인하여 정당성 여부를 확인한다.
- Destination IPv4를 활용하여 Next Hop의 IPv4를 찾는다.
- 추출했던 Packet의 Header를 다시 Ethernet/IPv4로 재구성한다.
이를 코드로써 구현하면 아래와 같다.
1// File: "very_simple_switch_impl.p4" 2# include <core.p4> 3# include "very_simple_switch_model.p4" 4 5// 해당 program은 packet에서 IPv4와 6// ethernet header를 가져와 7// destination IP에 기반하여 packet을 8// forwarding하는 것을 목적으로 한다. 9 10typedef bit<48> EthernetAddress; 11typedef bit<32> IPv4Address; 12 13// Standard Ethernet header 14header Ethernet_h { 15 EthernetAddress dstAddr; 16 EthernetAddress srcAddr; 17 bit<16> etherType; 18} 19 20// IPv4 header (without option) 21header IPv4_h { 22 bit<4> version; 23 bit<4> ihl; 24 bit<8> diffserv; 25 bit<16> totalLen; 26 bit<16> identification; 27 bit<3> flags; 28 bit<13> fragOffset; 29 bit<8> ttl; 30 bit<8> protocol; 31 bit<16> hdrChecksum; 32 IPv4Address srcAddr; 33 IPv4Address dstAddr; 34} 35 36// packet에서 추출한 header형태의 구조체 37struct Parsed_packet { 38 Ethernet_h ethernet; 39 IPv4_h ip; 40} 41 42/* Parser Section */ 43 44// Programmer에 의해서 정의된 error로 45// parsing 도중에 이를 발생시킬 수 있다. 46error { 47 IPv4OptionsNotSupported, 48 IPv4IncorrectVersion, 49 IPv4ChecksumError 50} 51 52/** 53 * @param b 들어오는 packet 54 * @param p parsing해서 나갈 packet Header 55 */ 56parser TopParser(packet_in b, out Parsed_packet p) { 57 Checksum16() ck; // checksum block을 instantiating 58 59 state start { 60 b.extract(p.ethernet); // p의 ethernet 부분을 b에서 추출 61 transition select(p.ethernet.etherType) { 62 0x800: parse_ipv4; // forward packet to parse_ipv4 63 // default rule이 없기 때문에 0x800(ethernet)을 64 // 제외한 packet을 모두 rejected 된다. 65 } 66 } 67 68 state parse_ipv4 { 69 b.extract(p.ip); 70 verify(p.ip.version == 4w4, error.IPv4IncorrectVersion); 71 verify(p.ip.ihl == 4w5, error.IPv4OptionsNotSupported); 72 ck.clear(); 73 ck.update(p.ip); 74 verify(ck.get() == 16w0, error.IPv4ChecksumError); 75 transition accept; // 다음 block으로 out을 forwarding 76 } 77} 78 79/* Match-action Pipeline Section */ 80 81control TopPipe(inout Parsed_packet headers, 82 in error parseError, 83 in InControl inCtrl, 84 out OutControl outCtrl) { 85 IPv4Address nextHop; 86 87 action Drop_action() { outCtrl.outputPort = DROP_PORT; } 88 89 action Set_nhop(IPv4Address ipv4_dest, PortId port) { 90 nextHop = ipv4_dest; 91 headers.ip.ttl = headers.ip.ttl - 1; 92 outCtrl.outputPort = port; 93 } 94 95 table ipv4_match { 96 // lpm : longest-prefix match 97 key = { headers.ip.dstAddr: lpm; } 98 // 해당 테이블에서 key에 대응할 수 있는 action의 list이다. 99 // 해당 table은 후에 controller에 의해서 채워진다. 100 // 즉, 테이블은 p4 prgramming을 통해서 채우는 것은 아니다. 101 actions = { 102 Drop_action; 103 Set_nhop; 104 } 105 size = 1024; 106 default_action = Drop_action; 107 } 108 109 action Send_to_cpu() { 110 outCtrl.outputPort = CPU_OUT_PORT; 111 } 112 113 table check_ttl { 114 // exact : 정확히 일치하는지 여부를 확인 115 key = { headers.ip.ttl: exact; } 116 actions = { 117 Send_to_cpu; 118 NoAction; 119 } 120 const default_action = NoAction; 121 } 122 123 action Set_dmac(EthernetAddress dmac) { 124 headers.ethernet.dstAddr = dmac; 125 } 126 127 table dmac { 128 key = { NextHop: exact; } 129 actions = { 130 Drop_action; 131 Set_dmac; 132 } 133 size = 1024; 134 default_action = Drop_action; 135 } 136 137 action Set_smac(EthernetAddress smac) { 138 headers.ethernet.srcAddr = smac; 139 } 140 141 table smac { 142 key = { outCtrl.outputPort: exact; } 143 actions = { 144 Drop_action; 145 Set_smac; 146 } 147 size = 16; 148 default_action = Drop_action; 149 } 150 151 apply { 152 if (parseError != error.NoError) { 153 Drop_actrion(); 154 return; 155 } 156 157 ipv4_match.apply(); 158 if (outCtrl.outputPort == DROP_PORT) return; 159 160 check_ttl.apply(); 161 if (outCtrl.outputPort == CPU_OUT_PORT) return; 162 163 dmac.apply(); 164 if (outCtrl.outputPort == DROP_PORT) return; 165 166 smac.apply(); 167 } 168} 169 170/* Deparser Section */ 171 172control TopDeparser(inout Parsed_packet p, packet_out b) { 173 Checksum16() ck; 174 apply { 175 b.emit(p.ethernet); 176 if (p.ip.isValid()) { 177 ck.clear(); 178 p.ip.hdrChecksum = 16w0; 179 ck.update(p.ip); 180 p.ip.hdrChecksum = ck.get(); 181 } 182 b.emit(p.ip); 183 } 184} 185 186// VSS packet를 Instantiate 187VSS(TopParser(), TopPipe(), TopDeparser()) main;
여기까지가 기본적인 P4에 대한 설명이다. 후에 전반적인 문법과 작성법에 대한 가이드를 작성하도록 하겠다.
Reference
- Thumbnail: 🔗 P4 공식홈페이지
- P4 specification
Comments