Byeo

Revisiting the Open vSwitch Dataplane Ten Years Later 2 본문

프로그래밍 (Programming)/논문 (Paper)

Revisiting the Open vSwitch Dataplane Ten Years Later 2

BKlee 2024. 5. 8. 23:14
반응형

본 포스트는 sigcomm '21의 Revisiting the Open vSwitch Dataplane Ten Years Later를 요약하였습니다.

 

이전 포스트:https://byeo.tistory.com/entry/Revisiting-the-Open-vSwitch-Dataplane-Ten-Years-Later-1

 

Revisiting the Open vSwitch Dataplane Ten Years Later 1

본 포스트는 sigcomm '21의 Revisiting the Open vSwitch Dataplane Ten Years Later를 요약하였습니다. Abstarct 해당 논문은 수천 명의 VMware 고객이 사용하는 data center 가상화를 위한 NSX 상품의 일부로서, Open vSwitch

byeo.tistory.com

 

 

3. OVS With AF_XDP

3.1 AF_XDP background

 OVS with AF_XDP의 구현에서 직면했던 challenge들을 나열한다. 주요 목표는 최적화를 통해서 kernel-bypass solution과 견줄 수 있는 성능을 얻어냄에 있다. 

 

 

 

  Figure 4는 AF_XDP 개발 과정을 보여준다. 그림의 왼쪽은 eBPF 프로그램을 XDP hook point에 설치하는 일반적인 과정을 보여준다. Clang과 LLVM은 엄격하게 검증된 C program을 input으로 하여, eBPF 명령어를 포함한 ELF object file을 내뱉는다. 그다음, iproute와 같은 eBPF loader는 프로그램을 검증하고 로드하기 위해서 BPF system call을 호출한다. 이 과정을 통과하면, hook point이 붙게 된다. 연이어서 XDP ingress hook에 도달한 packet은 eBPF 프로그램을 실행하게 된다. 그램의 XDP program은 packet을 수신하면 모두 AF_XDP socket (a.k.a. XSK)을 통해 userspace로 전달한다. 각 XSK은 userspace에 Rx와 Tx 자료구조를 사용하며, 이들은 umem이라 칭해진다. 

 umem은 두 개의 ring으로 구성되어 있다. "fill ring", "completion ring".

①: packet 수신 준비를 위해서 userspace prgroam은 빈 descriptor를 Rx fill ring에 부착한다. 

②: packet이 실제로 도착했을 때, kernel은 ring에서 descriptor를 pop한다.

③: packet을 fill ring과 연결된 memory에 실제로 작성한다.

④: 작성이 끝났다면 completion ring을 push 한다.

⑤: userspace program은 Rx completion ring을 확인한다.

⑥: 그리고 descriptor의 정보를 확인해 실제로 메모리에 가서 packet data를 읽는다.

 

이 작업이 모두 끝나면, 다시 이 descriptor들을 Rx fill ring에 넣어서 packet 수신을 대기한다.

반대로 packet을 전송하는 것도 이와 비슷한 로직으로 이뤄진다.

 

 이어지는 subsection에서는 physical devices, virtual devices, container들 각각에 어떻게 구현했는지 다룬다. 실험 환경은 다음과 같다:

  • CPU: Xeon E5 2620 v3 12-core (2.4 GHz) servers (back-to-back)
  • Mellanox ConnectX-6 25 GbE NIC
  • Ubuntu kernel 5.3
  • 한 서버에서는 64 bytes single UDP flow를 Tx 하고 반대편 서버에서는 OVS AF_XDP를 통해서 packet을 처리한다. throughput은 drop이 발생하기 전의 최고 성능을 표현하였다.

3.2 Physical Devices

 

Table 2는 아래에서 설명할 최적화들을 하나씩 적용해 나갔을 때의 성능을 나타낸다.

Optimization 1: Dedicated thread per queue

 기존 코드는 OpenFlow와 OVSDB 처리를 userspace datapath의 packet processing과 동일한 프로세스에서 실행하였다. strace로 분석해 보니 polling에 많은 CPU를 사용하는 문제를 찾았고, PMD (poll-mode-driver) 전용으로 별도의 thread를 분리하였다. PMD thread는 AF_XDP packet을 하나씩 수신할 때마다 처리한다. 이를 통해 0.8 Mpps에서 4.8 Mpps로 6배 향상시켰.

 

Optimization 2: Spinlock instead of mutex

 OVS는 별도의 umempool이라는 메모리 영역을 할당받고, 네트워크 장비가 이를 곧바로 접근할 수 있도록 허용한다. 하지만 receive 과정에서 하나의 thread만 umem region 작업을 처리하더라도 send는 임의의 thread가 보낼 수 있다. 따라서 synchronization 과정을 요하고, 처음에는 mutex_lock으로 접근했었다. 

 하지만 perf 분석 결과, pthread_mutex_lock 함수에만 약 5%의 CPU를 사용하는 문제가 있었다. 심지어는 1개의 queue에 1개의 thread임에도 불구하고 말이다. 이를 spinlock으로 변경한 결과 4.8 Mpps에서 6.0 Mpps로 1.25배 상승하였다.

 

Optimization 3: Spinlock batching

AF_XDP의 디자인은 userspace rx ring으로 packet이 batch로 도달함을 가정한다. 그러나, 각 batch receive operation은 사실상 추가적인 보조 작업을 필요로 한다. 예를 들어, batch receive가 성공하면 연이어서 packet을 받을 수 있도록 XSK fill ring을 refill 해야 하는 등의 작업이다. 이러한 작업들은 contention을 막기 위하여 lock을 요구한다. 

 umempool에 있는 packet들에게 각각 spinlock을 주었던 우리의 초기 구현은 spinlock의 단위가 너무 잘게 쪼개져있었다. 이를 packet단위가 아닌 batch 단위로 spinlock을 걸면서 성능을 6.0 Mpps에서 6.3 Mpps로 1.05배 상승시켰다.

 

Optimization 4: Metadata pre-allocation

OVS는 각 packet들의 metadata를 추적하기 위해서 dp_packet이라는 구조체를 사용한다. 이는 input port, L3 header offset, NIC hash 등을 관리한다. strace 분석 결과, dp_packet 구조체를 할당하게 위해서 사용하는 mmap이 상당한 overhead가 있는 것으로 확인되었다. 이를 피하기 위해서 우리는 pre-allocated packet metadata를 연속적인 하나의 배열로 미리 할당하였다. 이 단일 배열은 cache locality를 끌어올리는 장점도 있어서 최종적으로 6.3 Mpps에서 6.6 Mpps로 1.04배 성능이 향상되었다.

 

Optimization 5: Checksum offload

최근 NIC은 CPU 비용을 줄이기 위해서 checksum 계산, VLAN tag push/pop, TCP segmentation을 지원한다. Linux kernel과 DPDK는 이러한 hardware NIC 기능을 사용할 수 있다. 하지만, AF_XDP는 아직 이를 사용할 수 없다 (BTF와 XDP hint가 존재함에도 불구하고). 이를 해결하기 위해서 vendor사의 device driver에 checksum offload support를 사용할 수 있도록 협력을 요청하고 있다.

 offloading이 가능할 때의 성능을 추정해 보기 위해서 packet checksum에 고정된 값만을 보내고, receiver에서는 이 checksum을 문제가 없다고 판단하도록 코드를 약간 수정했다. 그 결과 6.6 Mpps에서 7.1 Mpps로 1.07배 성능을 향상시켰다. 

 checksum calculation은 payload size가 클수록 비용이 증가함에 따라, 오프로딩이 정상적으로 수행된다면 그만큼 성능 향상도 위에서 언급된 1.07배보다 더 좋은 결과를 얻을 것이라고 기대한다. 뿐만 아니라 TSO도 TCP workload에 성능 향상을 가져올 것이라고 기대한다 (단, 우리가 실험에서 사용하는 < MTU packet은 예외)

 

3.3 Virtual Devices

지금까지의 내용은 NIC과 OVS userspace 사이에서의 흐름을 최적화했지만, 동일 호스트의 OVS userspace와 VM 사이는 이야기된 내용이 없다.

 Packet을 local VM으로 보내기 위해서, OVS with AF_XDP는 sendto system call을 사용하여 Linux Kernel "tap" device로 패킷을 전송한다. (Figure 5. path A) 이 과정의 시스템 콜 비용은 평균 2 us로 측정되었다. 이 값은 보통의 OVS user/kernel model보다 상당히 높은데, 그 이유로는 기존의 OVS user/kernel model의 경우 kernel memory에 거주하던 데이터들을 단순히 inter-kernel call로 tap device에게 넘겨주는 구조였기 때문이다. 이 부분은 7.1 Mpps에서 1.3 Mpps로 성능을 감소시켰다.

 이를 극복하기 위해서 VM을 kernel의 tap device가 사용하는 vhost protocol과 동일한 프로토콜을 바로 사용하도록 설정하였다. 이 vhostuser 구현을 통해서 패킷은 (Figure 5. path B)를 통해 통신할 수 있게 되었고, 결과적으로 kernel hop을 건너뛰게 되었다.  이는 다시 성능을 6.0 Mpps로 끌어올렸다.

 

3.4 Containers

 대부분의 non-NFV application은 network 접근을 위해서 kernel socket에 의존한다. 이는 packet이 얼마나 들어오든지 상관없이 kernel TCP/IP stack이 초기에 수신하고 처리하는 로직이 필요하다는 의미이다. 

 container의 경우 packet은 동일한 host kernel에서 처리된다. (data copy 없이 "veth"를 통해서 쉽게 컨테이너로 넘길 수 있다.) 결국, 이 상황은 kernel -> container가 가능했던 기존 구조에서 kernel -> ovs userspace -> kernel로 들어가는 구조가 되고, (data copy와 kernel/user switch에 의한) 성능 역시 하락시키는 문제점을 야기하는 것이다. 

 

 이 문제는 현재 탐구 중에 있다. veth에서 AF_XDP의 zero-copy를 사용하는 방법은 packet copy 비용을 제거할 수 있으리라 예상한다. 또 다른 선택지로는 OVS XDP program을 수정해서 OVS userspace를 bypass 하는 것이다. 즉, (Figure 5. path C)처럼 XDP program이 곧바로 veth XDP program으로 전송하고, container로 packet을 쏘는 방법이 가능할 것이다. 이는 Section 5.4에서 장단점을 다룬다. 

 

3.5 eBPF를 활용하여 OVS 확장

 container에서 이야기 한 것과 동일. XDP program이 처리하고, 매칭되는 내용이 없다면 OVS datapath로 올리는 구조가 가능할 것이다. (device driver XDP에서 ovs kernel datapth를 구현하고, flow cache miss가 발생하면 userspace로 올려서 처리하는 구조가 만약 문제가 없다면 내 생각에 최적일 것 같은데...) 또한, eBPF가 다른 3rd-party 패킷 처리 소프트웨어들과 잘 통합돼서 함께 OpenFlow를 프로토콜을 사용할 수 있도록 하는데 유용할 수 있다. 

 

 그러나 device driver eBPF는 아직 지원이 완벽하지 않다. 또한, 제조사마다 XDP program을 network device driver에 붙이는데 각자 다른 상황이다.

 

 Figure 6 (a)가  Intel NIC이 사용하는 모델로, 모든 유입 packet은 XDP 프로그램을 거치도록 되어있다. 만약 트래픽을 구분하고 싶다면, XDP 프로그램에 이러한 별도의 처리를 구현해야 한다.

 Figure 6(b)는 Mellanox NIC이다. XDP 프로그램이 device receive queue마다 부착될 수 있다. 이 경우, 사용자는 NIC hardware 기능을 이용해 hardware 단에서 flow classification rule을 추가하고, 실제로 올라오는 트래픽을 미리 구분지을 수 있을 것이다. 이 경우에는 XDP 프로그램이 원하는 트래픽 유형에 대해서만 처리하는 로직을 포함하면 된다.

 

4. Implementation

 NSX에서 사용중인 OVS 설명은 생략하였습니다.

 

 

 Figure 7(a)는 NSX가 전통적인 OVS kernel DP 방식을 사용하는 그림을 나타낸다. kernel dp를 거친 packet은 TCP/IP stack, conntrack, device driver를 모두 거치는 구조이다.

 Figure 7(b)는 NSX가 OVS w/ AF_XDP를 사용하는 구조이다. 여기서 packet 처리는 kernel 밖에있는 ovs-vswitchd에서 이뤄진다. OVS는 XDP 프로그램을 관리한다 (사용 가능한 XDP feature를 확인, eBPF 컴파일을 위해서 LLVM/Clang 버젼 확인, 그리고 bridge에 포트가 추가 혹은 삭제될 때마다 XDP 프로그램을 load/unload)

 packet 처리를 userspace에서 함에 따라 Linux networking subsystem을 모두 우회한다. 즉, Kernel이 제공하는 많은 기능들을 상실한다. conntrack, netfilter ,GRE, ERSPAN, VXLAN, Geneve, QoS via tc등을 모두 활용할 수 없게된다. OVS 개발자는 수년간 이를 userspace에 별도로 구현해오고 있는 중이다.

 단, NSX agent나 다른 상품의 경우에서는 kernel 구현을 곧바로 사용하기 때문에 OVS는 Netlink를 통해 kernel table의 복사본을 userspace에서 들고있는 상태이다. (성능에 악영향은 없다. 자주 발생하는 이벤트가 아니기 때문)

 

반응형
Comments