Intoduction
Limitations of Existing Host Network Stacks
오늘날의 Linux network stack엔 자주 언급되는 한계들이 있다.
- inefficient packet processing pipeline
기존의 Linux network stack은 packet이 느리게 도착하고, interrupt나 system call이 bottleneck이 디지 않는다는 가정 하에 설계되었다. 이러한 전제를 바탕으로 설계된 packet processing pipeline은 오늘날처럼 초당 수백만 개의 작은 packet이 오가는 환경에서 비효율을 초래한다.
- inability to isolate latency-sensitive and throughput-bound applications
latency에 민감한 app과 높은 throughput을 요구하는 app을 서로 분리해서 처리하지 못한다.
- rigid and complex implemention
복잡한 구현 구조로 인해 성능 최적화나 새로운 프로토콜 도입이 어렵다.
이러한 한계점들은 network stack에 대한 많은 논의가 이루어지게 했다. 예를 들어, application interface는 어떤 형태여야 하는지(app이 network와 통신할 때 stream 기반 API를 사용해야 하는지, RPC 기반 API를 사용해야 하는지), application에 어떤 semantics을 제공할 것인지(app이 네트워크 요청을 기다리는 synchronous 방식을 사용할 것인지, 기다리지 않고 다음 작업을 처리하도록 하는 asynchronous 방식을 사용할 것인지), 네트워크 스택 구현 placement(커널 안에 구현할 것이지, userspace에 구현할 것인지)에 대한 논의들이 이루어졌다. 그러나 본 연구에서는, network stack의 한계는 앞서 언급한 interface, semantic, 위치에 기인하는 것이 아니라 network stack 그 자체의 architecture에 기인함을 밝힌다.
Architecture of Existing Host Network Stacks
더 자세히 살펴보기 위해 오늘날 network stack의 architecture를 살펴보자. 오늘날의 network stack들은 기본적으로 고전적인 pipe abstraction에 기인하고 있다. 즉, applicatoin이 data를 하나의 송신 socket에 넣으면, 이 데이터는 network stack을 따라 흐르다가, 반대편의 수신 socket에서 나오는 pipe 구조를 따르는 것처럼 보이게 돼 있다.
이러한 pipe abstraction에는 다음과 같은 세 가지의 특징을 가진다.
-
Dedicated pipes
각 application은 독립적은 파이프를 갖는다. 즉 application 간에는 이 파이프라인을 공유하지 않고, 각자 독립된 전송 경로를 통해 데이터를 송수신하는 구조이다.
-
Tightly-integrated packet processing pipeline
각 pipe에서 수행되는 transport layer operation(congestion control, flow control, etc...)은 모두 해당 pipe에서 독립적으로 수행된다. 또한 이러한 packet processing 과정은 해당 app이 실행 중인 core 중심으로 수행된다.
-
Static pipes
각 pipe 할당되는 host resource(cpu, memory, etc..)는 pipe가 만들어질 때 static 하게 결정되고, 이후 다른 pipe의 존재나 host resource에 변화가 생기더라도 변하지 않는다.
network core가 주요한 performance bottleneck이었을 때는 이러한 pipe property가 문제 되지 않았다. 그러나 link bandwidth가 급격히 증가하면서, 본 연구에서는 이런 dedicated, tightly integrated, static pipeline이 바로 host network stack architecutre이 modern hardward의 support(μs-scale latency, hundreds of gigabits of bandwidth)를 충분히 이용하지 못하게 하는 주요 원인이라고 지적한다.
Motivation
Prerequisite
추후 나오는 measurement를 이해하기 위해 알고 있어야 할 선수 지식이다.
- aRFS
- 특정 flow의 packet을 해당 app이 실행 중인 cpu core로 steer 하는 메커니즘
- benefits: read/write data from local cache/memory
- downside: poor scalability when # apps in the same core increases
- 여러 app이 동일한 core에서 실행 중이면 오히려 해당 core로 steer 되는 packet이 몰리게 되면서 오히려 네트워크 성능이 떨어짐
- io_uring
- an asynchronous I/O interface for the Linux kernel
- synchronous: program이 I/O 요청을 보내고, 그 작업이 끝날 때까지 기다리는 것
- asynchronous: program이 kernel에 I/O 작업을 맡기고, cpu는 다른 일 수행 (syscall 즉시 return)
(I/O 작업은 백그라운드에서 처리되며, 작업이 끝나기를 기다리지 않아도 됨)
- userspace와 kernel space 사이에 shared ring buffer 존재
- submission queue(SQ): user space process가 asynchronous I/O request를 kernel에 전달하기 위해 사용하는 queue
- completion queue(CQ): kernel이 asynchronous I/O request의 결과를 user space에게 전달하기 위해 사용하는 queue
- the syscall overhead and number of context switches can be significantly reduced
- 기존에는 파일을 1000번 읽으려면 read() syscall를 1000번 호출
- 이때마다 user-kernel space를 오가며 context switch 반복
- io_uring은 1000번의 요청을 SQ에 적어두고 io_uring_enter() syscall 1번으로 read 요청을 보낼 수 있음.
- MPTCP (Multipath TCP)
- 기존 TCP의 확장으로 하나의 logical TCP connection을 여러 개의 subflow(path)로 구성
- logical connection: 하나의 TCP 연결 (e.g., socket()을 통해 맺은 하나의 연결)
- WiFi와 LTE를 동시에 사용할 수 있는 구조로 WiFi가 끊겨도 나머지 경로로 자동 전환
measurement setup
주요 원인이 dedicated, tightly integrated, static pipeline임을 증명하기 위해 저자들은 T-apps와 L-apps의 performance bottleneck을 살펴보는 측정을 진행했다. 먼저 측정 환경은 다음과 같다.
- two servers directly connected by a 100Gbps link
- each server has 4 NUMA nodes with 8 CPU cores per-NUMA node
- enabled DCA
Static and dedicated pipeline ➝ lack of scalability for long flows

single TCP socket로 stream of data를 전송했을 때의 실험 결과이다. figure (a)를 보면 알 수 있든 어떠한 interface를 사용하든 100Gbps link를 saturate 시키지 못했음을 알 수있다 또한 (b)를 보면 receiver 측에서 data copy가 주요 overhead로 작용하고 있음을 알 수 있다.
-
TCP read/write syscalls
- aRFS와 Manual IRQ steering을 사용한 각각의 경우 모두 100Gbps link를 saturate 시키지 못했다.
- aRFS를 사용한 경우보다 aRFS를 disable시키고, interrupt를 application이 존재하는 NUMA node의 다른 core로 steer시켰을 경우의 throughput이 더 높다.
- TCP/IP 처리나 netdev processing이 app core가 아닌 다른 core에서 이루어지므로, app core는 data copy를 하는 데에 cpu cycle을 더 많이 소모할 수 있다. 여기서 비롯되는 차이이다.
(앞서 언급한 aRFS의 단점을 떠올려 보자. 여러 app이 하나의 core에서 돌아가고 있을 때 오히려 이 core로 steer 되는 작업들이 많아서 성능이 안 좋아졌다. 이 점을 확장시켜서 생각해 보면, 지금은 하나의 app만 돌아가고 있는 상황이지만 100Gbps 환경이므로 app이 도는 core에서 엄청나게 많은 packet에 대한 IRQ 처리부터 data copy 작업이 이루어진다. 즉 해당 core에 부하가 집중된다고 생각된다. 내 생각이다.)
-
TCP io_uring
- total throughput이 TCP read/write syscalls 경우보다 감소했다.
- kernel이 I/O 처리를 위해 생성한 background thread가 TCP/IP 처리, data copy 등을 진행하기 위해 socket queue에 접근해야 한다. application thread도 같은 socket에 대해 상태 확인 등을 위해 접근할 수 있다. 즉, lock으로 보호되고 있는 socket에 두 thread가 동시에 접근할 수 있다. 이로 인해 lock contention이 발생하고, throughput이 감소된 것이다.
(그래프의 lock은 lock을 획득한 뒤, lock이 걸려 있던 resource에 대해 수행한 실제 작업의 cpu 소모량임. 이 lock을 잡기 위해 기다릴 땐 보통 스케줄러에 의해 sleep/block 되므로 cpu usage 크지 않음.)
-
MPTCP
- subflow 수가 증가할수록 throughput이 감소했다.
- 각 subflow가 독립적인 TCP/IP 처리와 독립적인 netdev layer 처리를 요구하기 때문이다.
정리해 보자. 앞선 long flow에 대한 실험에서는 idle core가 있음에도 interface-level processing(cost of buffering + copying data between user space and kernel)이 app core에만 집중되어 수행되기 때문에 성능이 제한된 것을 보여준다. 이는 이상적인 network stack에선 host의 현재 상황에 따라 interface-level processing에 사용되는 core 수를 동적으로 확장할 수 있어야 함을 시사한다.
Static and dedicated pipeline ➝ lack of scalability for short flows

single TCP socket으로 4KB RPC request를 전송했을 때의 실험 결과이다. 그래프를 보면 sender-side의 TCP/IP processing과 netdevice subsystem이 dominant overhead임을 알 수 있다. 이는 전송 단위가 작더라도, 이 작은 전송을 수행하기 위해서 TCP/IP stack은 여전히 socket queue 진입, segment 분할, tcp 헤더 생성 등 protocol-level 고정 비용이 거의 비슷하게 들어가기 때문이다.
- TCP io_uring
- io_uring을 사용하여도 throughput이 향상되지 않았다.
- io_uring는 system call cost나 context-switch가 dominant overhead일 때 효과적인데, 이 시나리오에서는 두 요소가 주요 overhead가 아니기 때문이다.
- MPTCP
- 여러 subflow를 생성할 수 있더라도, 이 subflows의 network layer processing을 처리하는 건 결국 application core에서 발생하기 때문에 이 시나리오에선 도움이 안 된다.
- CPU load에 따라 동적으로 runtime에 subflow 수를 조절할 수 없으므로 이 시나리오에선 도움이 안 된다.
마찬가지로, 이 short flow에 대한 실험에서는 idle core가 있음에도 tcp/ip processing과 netdevice subsystem processing이 app core에만 집중되어 수행되기 때문에 성능이 제한된 것을 보여준다. 이는 network stack이 static한 pipeline을 사용하기 때문에 발생하는 제약이며, 이상적인 network stack이라면 상황에 따라 application이 정확히 인지하지 못하더라도 network layer processing에 할당되는 resource을 동적으로 조정할 수 있어야 함을 시사한다.
Tightly-integrated pipeline ➝ lack of scalability for short flows
L-apps와 T-apps 간의 performance interference를 이해하기 위해, 1개의 L-app과 8개의 T-apps을 같은 NUMA node에 위치시켰다. 앞서 실험 환경에서 언급한 NUMA node당 존재하는 core 수 8개보다 app의 개수가 많은 상황이고, 어떤 app은 어떤 core에 실행되게끔 지정하지 않았기 때문에 CPU scheduler가 apps을 동적으로 scheduling 한다. aRFS를 포함한 모든 optimization을 적용한 결과이다.

fig (a)는 L-app의 tail latency와 total throughput을 나타낸다. 이를 통해 두 app이 co-located일 때의 tail latency가 isolated 상황에서의 tail latency에 비해 37배 큰 것을 알 수 있다. (b) 그래프도 같이 보자. 그냥 37배 큰 게 아니라 per-core cpu usage는 비슷한데, tail latency에선 엄청 큰 차이가 나는 것이다.
이는 network layer processing이 app과 동일한 cpu core에서 함께 실행되는 tight-coupled pipeline 구조에서 비롯된 문제이다.
현재 시나리오에서는 전체 core 수보다 app의 수가 더 많기 때문에 어느 시점엔 L-app이 하나 이상의 T-app과 같은 cpu core를 공유하게 된다. 이때, 같은 core에서 수행되는 T-app의 network processing 작업이 L-app의 network processing 작업을 지연시켜 결과적으로 L-app의 tail latency를 증가시키게 된다.
이 문제를 해결하기 위한 방법으로는, L-app이 보내는 packet의 priority를 높여서 qdisc queue에 저장하거나 L-app process 자체의 priority를 높이는 방식이 있다. (qdisc는 queuing discipline으로 , NIC의 Tx queue에 packet을 스케줄링해서 보내는 역할을 한다.)
이러한 방식이 그래프에 표시된 "Prioritization" 실험이다.
하지만 이러한 접근은 크게 효과가 없는데, 그 이유는 두 가지다. 첫째, 시스템 구조상 qdisc에는 많은 패킷이 쌓이지 않는다. 즉, 큐의 깊이가 얕아, priority를 부여해도 실질적인 차이를 만들기 어렵다. 둘째, 대부분의 network layer processing을 처리하는 IRQ thread의 priority는 그대로이기 때문에, L app thread의 priority를 높여도 처리 순서에 영향을 주기 어렵다.
이 실험에서도 역시, app이 실행되는 core에서 network processing도 같이 처리되다 보니 L-app의 latency가 같은 core를 공유하는 T-app에 영향을 받아 증가했음을 보여준다. 이러한 결과는, 이상적인 network stack이라면 L-app과 T-app이 같은 core에 있는 경우더라도, 각 app의 network layer processing을 분리해 독립적으로 처리할 수 있어야 함을 시사한다.
NetChannel
NetChannel design
저자들이 이를 해결하기 위해 제안한 host network stack architecture는 바로 NetChannel이다. NetChannel은 다음과 아이디어를 기반으로 설계되었다.
핵심 아이디어: disaggregate the packet processing pipeline into separate, loosely coupled layers
-
instead of having dedicated resources for each pipeline, different pipelines should be able to share resources
더 이상 pieline마다 resource를 dedicate 하게 제공하는 것이 아니라, 여러 pipeline이 resource를 유연하게 공유하도록 하자.
-
instead of a tightly integrated pipeline, different parts of the pipeline should be decoupled
기존처럼 모든 과정이 하나의 pipeline에서 밀접하게 진행되는 구조가 아니라 pipeline의 간 단계가 서로 느슨하게 연결되어서 독립적으로 동작해야 한다.
-
instead of static assignments, it should be possible to independently scale the resources allocated to each stage of the pipeline dynamically
기존처럼 각 pipeline에 resource를 정적으로 할당하는 것이 아니라, pipeline 각 단계가 필요에 따라 유연하게 resource를 조절할 수 있어야 한다.

그림을 보면 알 수 있듯 NetChannel은 기존 packet processing pipelined을 VNS, NetDriver, NetScheduler 이렇게 총 3개의 layer로 분리하였다.
Virtual Network System (VNS)
VNS layer는 두 가지 주요 역할을 수행한다.
- interface가 달라짐에 따라, application을 수정할 필요 없이 app의 동작이 가능하도로 하는 virtual interface를 제공
예를 들어, NetChannel은 standard POSIX socket interface를 virtual socket을 통해 제공한다. application의 관점에서 이 virtual socket은 일반 socket과 동일한 semantics를 갖는 것처럼 보인다. 즉 packet은 순서가 맞춰진 상태로 도착하며, app은 해당 socket에 standard system call을 통해 접근할 수 있다. 그러나 이 virtual interface는 말 그대로 virtual하다. 즉, 겉으로는 같아 보일 수 있으나 내부적으로는 새로운 방식으로 동작한다.
- userspace와 kernel space 간 data copy를 위한 per-core worker thread 관리
이로 인해 data copy 작업이 더 이상 applicatio이 실행 중인 core에서만 처리되는 것이 아니라 여러 core에 존재하는 worker thread가 이를 분할하여 병렬적으로 수행할 수 있게 된다.
NetDriver
- network를 여러 개의 channel로 구성된 하나의 device처럼 abstraction
즉, application은 복잡한 전송 경로를 몰라도, queue에 packet을 넣는 것만으로도 전송이 이루어지는 것처럼 보인다. 이때 각 channel은 서로 간섭 없이 데이터를 주고받을 수 있는 독립적인 통신 경로로 동작한다. 예를 들어, 우리가 보통 TCP socket 하나를 열면 하나의 서버와 통신 경로가 형성되는데, 여러 개의 channel이 있다면 이는 여러 개의 TCP 연결을 동시에 사용하는 것과 유사한 구조가 된다.
- 새로운 transport protocol을 사용하는 것이 용이해짐
- network layer processing을 app이 사용하는 socket이나 cpu core와 분리해서 수행
다시 말해, 실행 중인 app이 몇 개이든, 각각이 어떠한 socket이나 core를 사용하든 관계 없이 두 서버 간에 하나 이상의 channel을 구성할 수 있도록 해준다.
NetScheduler
NetScheduler의 위치를 잘 보면 channel에 data가 얼마나 쌓였는지, cpu core의 utilization은 어떠한지 등 돌아가는 상황을 보기 좋은 위치이다. 이 위치에서 여러 정보를 조합해서 scheduling 한다고 보면 된다.
- multiplex/demultiplex
app이 발생시킨 data를 어떤 channel로 전송할지, 수신된 data는 어떤 virtual socket으로 전달할지 결정한다.
- per-core worker thread sheduling
여러 개의 per-core worker thread를 활용하여 userspace와 kernel space 간의 data copy 요청을 schduling하는 역할을 수행한다. 이를 통해 throughput이 중요한 app의 data copy 요청을 여러 core 에 걸쳐 분산시킬 수 있다는 장점이 있다. (VNS는 per-core worker 자체의 생명 주기를 관리하는 것이지 scheduling까지 하는 것은 아님)
- 동적으로 channel 수와 channel-core mapping 관계 조정
app의 overhead가 증가하면 기존에 2개였던 channel을 8개로 동적으로 조정할 수 있다. 또한 core와 channel 간 mapping 관계를 조정함으로써 L-app과 T-app의 처리를 분리할 수 있다.
Evaluation
Setup
Hardware setup.
- 2 servers connected directly via a 100Gbps link
- each server has a 4 NUMA nodes with 8 cores per NUMA node
- enable TSO, GRO, Jumbo Frames, aRFS, Dynamically-Tuned Interrupt Moderation
- disable hyperthreading, IOMMU
Performance metrics.
- throughput-per-core = total throughput / CPU utilization
- CPU utilization = max(client-side utilization, server-side utilization)
Result

Scalability for long flows
- figure (a), figure (b)
- NetChannel enables Linux to saturate the 100Gbps link by making use of multiple cores
- it allows independent scaling of data copy processing at VNS
Scalability for short messages
- figure (c)
- disable TSO/GRO, Jumbo Frames
- throughput increases near-linearly with the addition of channels
- it allows dynamically scaling network layer processing by allowing data from a single virtual socket to be multiplexed across multiple channels
Enabling performance isolation
- figure (d)
- 1 L-app, 8 T-apps > 8 cores
- 17.5x lower tail latency for L-apps
- it allows isolating network layer processing for L-apps from T-apps
Overheads

Overheads of emulating the Linux network stack
- figure (a)
- single application thread, single channel thread
- ~7% reduction in throughput-per-core
Overheads of scaling data copy processing
- figure (b)
- For Netchannel, 2 data copy threads and 1 channel thread
- For Linux, 3 long-lived TCP connections over 3 cores
- 12% reductoin in throughput-per-core
- L1 cache misses during copy (data copy core != app core)
Overheads of scaling network layer processing
- figure (c)
- throughput-per-core remains the same independent of the number of channels
- wake up channel threads more frequently as each channel thread goes to sleep after processing each short message (4KB)
- 패킷이 도착하면 io_work를 만들어서 워크큐에 등록하고 이 워크큐를 담당하는 kworker를 깨워 처리하는 식으로 코드가 되어 있는데 이 kworker가 channel thread 같다. 좀 더 봐야 할 듯하다.
Overheads of achieving performance isolation
- figure (d)
- T-app 입장에서 throughput-per-core 손해가 12% 존재
기존 Linux network stack의 dedicated, static and tightly integrated packet processing pipeline으로 인해 modern hw의 성능을 충분히 이용할 수 없는 상황이 오자 이 pipeline을 disaggregate해서 만든 architecture, NetChannel에 대해 살펴보았다.