- Today
- Total
Byeo
The eXpress Data Path (XDP): Fast Programmable Packet Processing in the Operating System Kernel 2 본문
The eXpress Data Path (XDP): Fast Programmable Packet Processing in the Operating System Kernel 2
BKlee 2023. 8. 24. 00:14이 글은 Conext '18에 공개된 The eXpress Data Path (XDP): Fast Programmable Packet Processing in the Operating System Kernel 를 번역해 정리한 글입니다.
이전 게시글
XDP Design
XDP는 패킷 처리 성능을 위하여 OS에 safety를 보장하면서 통합되었다. 지금도 꾸준히 Linux community로부터 의견을 받아가며 발전시켜 나가고 있다. 이 구조에는 몇 가지 한계와 교훈이 있으나, 논문에서는 다루지 않았다. 여기에서는 그보다 XDP의 구성 요소와 동작 원리, 그리고 다른 시스템과 어떻게 동작하는지 설명한다.
Figure 1은 XDP와 Linux kernel이 어떻게 통합되었는지, Figure 2는 XDP를 이용해 작성된 프로그램이 어떻게 동작하는지 나타낸다. Figure 1의 캡션에 설명되어 있는 것 처럼 packet이 시스템에 도착하면 packet이 건드려지기 전에 device driver의 eBPF 프로그램이 먼저 drop을 할 지 결정할 수 있다. 또는 들어온 interface로 그대로 다시 내보낼지 (redirect), 다른 interface로 보낼지 (forward), 혹은 userspace로 보낼지 (AF_XDP), TC BPF hook을 사용하여 kernel stack 내부를 흐를지 등등을 다양하게 결정할 수 있다. 여러 eBPF 프로그램 및 userspace와도 통신도 가능하다.
XDP는 크게 4가지 구성요소로 이루어져있다:
- 1. XDP driver hook: XDP의 main entry point이며, hardware에서 packet이 도착했을 때 실행된다.
- 2. The eBPF virtual machine: XDP program의 byte code를 실행한다. 성능을 향상시키기 위해 just-in-time-compile 형태로 동작한다.
- 3. BPF maps: 다른 시스템과 정보를 교환하기 위한 KV store이다.
- 4. The eBPF verifier: 프로그램이 로드되기 전에 해당 프로그램이 kernel 내에서 crash나 시스템을 망칠 우려가 없는지 정적으로 분석한다.
1. XDP driver hook
XDP 프로그램은 packet이 도착할 때마다 device driver layer에서 hook에 의해 실행된다. 따라서, kernel 내에서는 해당 코드를 device driver가 실행할 수 있으며, userspace로의 context switching overhead가 발생하지 않는다. 이 프로그램은 kernel이 per-packet 자료구조인 sk_buff를 할당하기 전에, 그리고 networking stack이 packet을 분석하기 전에 실행된다.
Figure2는 XDP 프로그램의 일반적인 실행 동작 단계를 나타낸다.
(1) Reading: 프로그램은 일반적으로 context object에 접근하면서 시작된다. 이 object가 raw packet data의 포인터를 갖고 있고, Rx-ed interface나 packet이 들어온 Rx-queue 등의 메타데이터도 함께 포함하고 있다. 다음으로 프로그램은 packet data를 분석할 것이다. 해당 분석을 완료하면, 프로토콜에 따라서 어떤 XDP 프로그램을 실행할지 결정할 수 있다.
(2) Metadata Processing: Packet data 분석이 끝난 뒤에 XDP는 packet과 관련된 메타데이터를 확인하기 위해 context object를 사용하여 필요한 데이터를 얻어올 수 있다. 추가로 context object에서는 메모리 공간을 제공하는데, 여기에 사용자가 필요에 따라 packet processing을 위한 데이터를 적어 놓을 수 있다.
나아가, XDP는 per-packet 자료구조 뿐만 아니라 시스템 전체에 필요한 자료구조도 BPF maps를 통해 제공한다. 이 자료구조를 이용해 다른 프로그램/시스템과 정보를 교환할 수 있다. 또한 XDP는 kernel이 제공하는 다양한 함수들을 사용할 수 있다. 이 helper function을 이용해 기존의 kernel 기능 (e.g., routing table)들을 사용할 수 있다. 이 helper function들은 kernel community에 의해 지속적으로 추가되고 있으며, 따라서 XDP의 기능은 더욱 풍부해질 것으로 기대하고 있다.
(3) Writing: 프로그램은 packet buffer를 추가/제거와 함께 쓰기 기능들을 사용할 수 있다. 이는 encap / decap, address rewriting (forwarding) 등을 가능케 한다. 특히, packet header의 데이터가 바뀌면 header checksum의 계산도 다시 해야 하는데, 이 역시 kernel의 helper function을 이용해 쉽게 처리할 수 있다 (근데 hardware checksum offloading이 해결해줄 수 있는 영역이 아닌가..?).
이 세 가지 스텝은 figure2의 회색 박스 각각에 해당한다. 다만, XDP는 자유롭게 작성할 수 있기 때문에 반드시 이 3개의 스텝을 순서대로 따라갈 필요는 없다. 순서를 바꾸거나 몇몇 절차를 반복할 수는 있지만 고성능을 달성하기 위해서는 위 절차를 따르는걸 권장한다.
Packet processing 끝나면 XDP program은 packet에 대해 최종적인 결정 (verdict라고 표현했다)을 내린다. Figure 2의 오른쪽 부분에 나타나있는 것 처럼 4가지 종류가 존재하며, return code에 값이 적힌다. Drop, Xmit out (동일 interface tx), kernel networking stack은 parameter없는 return code를 반환한다. 이들과는 다르게 redirect는 (1) 다른 interface tx, (2) 다른 CPU로 전송, (3) userspace로 전송을 구분하기 위해서 위하여 추가 return parameter를 필요로 한다. 이렇게 redirect 결정에만 target type에 따라 추가 parameter를 넣은 이유는, 추후에 더 많은 target type을 쉽게 통합하여 확장시키기 위함이라고 한다. 현재 target type과 관련된 정보는 BPF map에 들어있다고 한다.
2. eBPF Virtual Machine
XDP 프로그램은 BPF에서 eBPF VM 내에서 실행된다. BPF는 레지스터 기반의 VM에서 필터 역할을 하기 위해 수십년간 발전해온 기능이다. 기존의 BPF는 2개의 32-bit 레지스터와 22개의 명령어를 이해하는 VM이었다. 하지만 eBPF는 이 수를 크게 늘려서 11개의 64-bit 레지스터로 확장했다. 64-bit 레지스터들은 커널의 64-bit 아키텍처 레지스터들과 1:1 매핑되어 있어 JIT 컴파일을 효율적으로 수행할 수 있다고 한다. 현재 C 코드를 eBPF 코드로 변환하는 작업도 LLVM 컴파일러로 지원하고 있다.
또한 eBPF 명령어 세트는 여러 종류가 추가되었다. 64-bit register의 산술/논리 연산(Arithmetic-logic) 명령어, C의 함수 호출 흐름과 동일한 call 명령 등을 추가했다. 이러한 eBPF 명령어 셋을 통해서 일반적인 목적의 연산들을 작성하고 처리할 수 있음과 동시에, eBPF verifier가 유저가 작성한 프로그램이 kernel을 망가뜨리지 않도록 검증 및 제약을 거는 역할을 수행한다. 이를 통해 코드를 kernel address space에서 실행하는 위험성에 대한 안전을 보장한다 (이는 XDP의 특징이 아니라 eBPF의 특징).
eBPF는 BPF-map을 kernel 내의 다른 eBPF 프로그램과 공유하고 있고, 따라서 이들간에 상호작용이 가능하다. 예를들어 CPU의 부하를 측정하는 eBPF 프로그램이 kernel 내에서 별도로 돌고 있는 경우, 해당 프로그램이 XDP 프로그램한테 packet을 drop하라는 지시를 내릴 수 있다.
eBPF VM은 eBPF 프로그램을 동적으로 load할 수 있으며, 커널이 모든 eBPF 프로그램의 life cycle을 관리한다. 이는 동적으로 kernel이 eBPF 프로그램들을 자유롭게 load하여 임의의 eBPF 프로그램을 얼만큼 실행할 것인지 결정할 수 있도록 한다.
3. BFP Maps
eBPF 프로그램은 kernel의 이벤트에 대한 응답으로 실행된다 (packet arrival event in XDP). 매 실행마다 동일한 초기 상태를 지니며, persistent memory에 대한 접근 권한은 없다. 대신, BPF map이라는 자료구조에 kernel helper function을 통해 접근할 수 있다.
이 BPF map은 eBPF 프로그램이 실행될 때 정의되는, 그리고 eBPF code에서 접근할 수 있는 key/value-store이다. 이 MAP은 per-CPU 및 globally 존재한다. 이는 다른 eBPF 프로그램에 의해, 그리고 userspace에서도 접근 및 공유가 가능하다. 이 map은 hash maps, 배열, radix tree, eBPF program pointer, redirect targets, 심지어는 다른 eBPF map pointer 포함할 수 있다.
MAP은 몇 가지 목적을 갖고 사용이 가능하다.: 동일한 eBPF 프로그램의 persistent memory로서 (나름의 storage 처럼), 데이터 공유를 통한 여러 eBPF 프로그램들간의 상호작용, userspace 프로그램과 kernel eBPF 사이의 상호작용 (control plane은 userspace에서, data plane은 kernel에서)과 같은 사용 예제가 있다.
4. The eBPF Verifier
소개한 것처럼 eBPF는 kernel 내부에서 직접 실행된다. 따라서 임의의 kernel memory를 접근할 수 있고, 이는 시스템을 망가뜨릴 수 있는 수단이 될 수도 있다. 이러한 사고를 막기 위해서 kernel은 모든 eBPF 프로그램들을 load하기 위해 하나의 진입점( bpf() 시스템콜)을 사용하도록 강제한다. eBPF 프로그램이 load되면 가장 먼저 in-kernel verifier에 의해 분석된다. 이 프로그램은 정적으로 프로그램의 byte code를 분석하며, 시스템을 망가뜨릴 수 있는 (임의의 메모리 접근 등) 요소가 없는지, 그리고 프로그램이 정상적으로 종료될 수 있는지 까지도 분석한다. eBPF 프로그램이 정상종료 되는 것은 중요하다. 이는 loop를 불허하고 program의 size를 제한함으로써 정상 종료를 강제한다.
Verifier는 eBPF 프로그램에 대한 control flow의 directed acycling graph (DAG)를 만들어서 동작한다. 가장 먼저 DFS를 활용해 해당 control flow가 실제로 DAG인지 확인한다. 그 다음으로 verifier는 모든 DAG 흐름을 따라가면서 unsafe memory access가 없는지, helper function의 인자가 올바른 지 확인한다. 이 인자 확인은 실제로 모든 레지스터와 stack 변수를 확인하면서 이뤄진다. 이는 verifier가 사전에 메모리 경계(bound)가 어딘지 모르는 상황에서 eBPF가 혹시나 out of bound memory 접근을 발생시킬 수 없도록 하기 위함이다. 또한, 사전에 완전히 알 수 없기에, verifier는 eBPF 프로그램이 pointer dereferencing을 하기 전에 적절한 체크를 하는지 검증한다.
Data 접근을 추적하기 위해, verifier는 data type, pointer offset, 그리고 모든 register의 가능한 수의 범위를 추적한다. 예를 들어 1byte를 레지스터 R1에 로드하는 경우, 가능성한 범위는 0-255다. eBPF 프로그램이 코드 흐름 중에 R1 > 10이라는 if 문이 존재한다면, if문은 R1의 10~255를, else문은 R1의 0~10를 상태 변수를 활용해 계속 추적해 나가는 방식이다. 이 범위 정보를 갖고 있는 상태변수를 사용해서 verifier는 load 명령어의 memory access 범위를 예상할 수 있고, 오로지 안전한 memory access만 가능토록 강제하는 구조이다. 추가로, pointer 연산은 제한되며, pointer를 integer로 type casting하는 것 역시 허용되지 않는다. 그 외에 verifier가 안전하다고 확신할 수 없는 연산이 존재하는 경우, eBPF 프로그램은 load되지 않는다.
이 verifier는 kernel 내에서 악의적이거나 버그를 방지하기 위함이지 실행 시 성능을 위한 목적이 아니다. 만약 XDP 프로그래밍 중에 과도한 프로세싱이 있다면 시스템 전반의 속도를 낮춰버릴 수도 있고, 잘못된 packet data write가 있다면 (logical error) 네트워크 스택을 망가뜨릴 수도 있다. 이를 방지하는 것은 프로그래머의 책임이며, 이러한 시스템 영향도 때문에 XDP 실행은 root 권한이 있어야 한다.
5. XDP 프로그램 예제
위 코드는 논문에서 예제로 제시한 코드다. Packet header를 분석하고, UDP packet을 source와 mac 주소만 뒤집어서 들어온 interface로 내보내는 작업을 수행한다. 간단한 예제이지만 실 사례에서 많이 사용하는 특징들을 담은 코드이다.
- 처리된 packet 통계를 저장하기 위한 eBPF map이 1~7번 줄에 정의되어 있다. 이 map은 IP protocol 번호를 key로 하고, value로 packet count로서 활용한다. 60~62번 줄에서 해당 value 값을 증가시키는 것을 확인할 수 있다. Userspace 프로그램은 XDP프로그램이 실행되는 중에 이 map의 통계를 가져가기 위해 poll을 사용할 수 있다.
- Packet의 data에 접근하기 위해서, 30~31번 줄을 보면 object context를 활용해 packet의 시작 점과 끝 점의 pointer를 얻는 것을 확인할 수 있다.
- 22번, 36번, 47번 줄의 조건 문을 보면 packet data 접근이 packet end를 넘지 않는지 한 번 확인한다.
- 41~50번 줄은 VLAN header를 분석하는 코드다.
- 14~16번 줄이 packet 데이터에 실제로 접근하는 코드다. MAC 주소 6 bytes를 스왑하는 것을 확인할 수 있다.
- 60번 줄은 bpf map을 읽기 위해서 kernel helper function을 사용하는 코드다. 이 프로그램에서 유일하게 사용된 함수이며, htons()를 포함한 다른 함수는 컴파일때 inline처리된다.
- 최종 packet 판결 (verdict)는 69번 줄처럼 return code를 이용해 전달된다. (37번, 48번 줄도 return XDP_DROP을 보내는 코드가 있다.)
이 프로그램이 interface에 설치될 때, 가장 먼저 eBPF byte code로 컴파일 된 후에 verifier에 의해 검증된다. 주요 검증 요소로서는 (a) loop가 없는지와 program 최종 크기, (b) packet data의 모든 접근이 경계 검증을 제대로 수행하는지, (c) map lookup function으로 넘어가는 인자들이 bpf map 정의와 부합하는지, (d) map lookup의 반환 값이 NULL 체크를 했는지 등을 확인한다.
6. 요약
XDP 프로그램은 (1) XDP device driver hook, (2) eBPF virtual machine, (3) BPF map, (4) eBPF verifier 로 구성되어 있다. 이 요소들이 결합하여 입맛에 맞는 packet processing application을 작성하고 실행할 수 있으며, 가속화 된 성능과 기존 kernel의 기능들을 유지할 수 있는 강력한 환경을 제공한다.
성능 실험은 다음 포스트에서..