DevOps

【sk_buff란?】 부제: Linux와 K8S에서의 Packet흐름 feat. eBPF

흑당망고 2026. 6. 4. 01:41

개요

지난번 AL 2023업그레이드할때 Node.js에서 겪을 수 있는 이슈를 설명하며 Cgroup이라는걸 알아보았다.

Linux 커널에서 동작하는 재밌는 친구였다.

그럼 이번엔 Network 쪽을 알아보자

sk_buff란?

그게 머에요..?

https://elixir.bootlin.com/linux/v7.1-rc6/source/include/linux/skbuff.h

 

skbuff.h - include/linux/skbuff.h - Linux source code v7.1-rc6 - Bootlin Elixir Cross Referencer

 

elixir.bootlin.com

Linux 커널에서 네트워크 쪽을 컨트롤링할때 일반적으로 우리는 '패킷'이라는 단위를 사용한다.

여기서 이 패킷이라는 것을 표현할때 가장 중요하게 사용되는 요소중 하나인 구조체가 존재하는데 이 친구가 바로 sk_buff다.

 

이 sk_buff라는 친구가 어디서, 왜 사용될까?

→ 데이터(네트워크 포함)의 송/수신을 하게 되었을때부터 어플리케이션에 전달할때까지 모두 사용되는 구조체이다.

우리가 알고 있는 OSI 7Layer로 보면 Layer2 ~ Layer4까지 모두 이 구조체를 통해서 전달을 한다는 의미다.

여기서 2~4까지 개입한다.

상식적으로 L1부터 L7까지의 기술이 연결되는건 모두가 알겠지만, 이 sk_buff라는 친구가 유독 특별한데는 이유가 있다.

바로 네트워크 스택들과 강결합이 되어있고, 단일 구조체에서 포인터만 딸깍 거리며 움직인다는 점 이다.

 

DevOps Engineering 적인 측면에서 접근해보면 eBPF와 같은 고-급 기술이 이러한 sk_buff와 연관이 깊다는 점이다.

eBPF라고 적어두면 긴가민가 할 수 있다. 하지만 이 친구를 예시로 들면 바로 아~ 싶을 것이다.

바로 eBPF 기반으로 동작하는 친구중 가장 유명한 친구인 Hot한 CNI Cilium이다.

왜 이 Cilium이 Hot하고 OverHead를 줄여주는지는 이번 블로그를 모두 보고나면 알 수 있을 것이다.

sk_buff 구조 뜯어보기

주의

내용이 전반적으로 깊다. (정확히는 복잡하다)

한번 집중해서 읽으면 생각보다 흐름에 대해서 이해가 잘 될 수도 있다.

struct sk_buff

여기에는 메타데이터가 적재되어있다.

여기에 적혀있는 포인터 주소가 Head Romm, Data, Tail Room등을 가르킨다.

이건 sk_buff와는 다른 존재로, 메모리에서도 별도로 관리가 되고 있다.

하지만 중요한 내용이므로 여기에 함께 적어두었다.

Head Room

수신할때는 쓰지 않고 송신(TX)할때 사용되는 빈 공간이다.

패킷는 송신(TX)시 커널이 skb_push를 호출해 포인터를 뒤로(왼쪽으로) 후진시키며 TCP, IP, MAC 헤더를 차곡차곡 조립시킨다.

이때 해당 데이터들의 내용들이 들어갈 자리가 바로 Head Room이다.

애플리케이션(L7)이 "이 데이터 좀 보내줘"라고 커널에 던질 때, 커널은 밑단에서 헤더가 얼마나 붙을지 미리 계산해서 앞쪽에 빈 헤드룸을 넉넉하게 할당해둔다.

 

아래가 바로 skb_push 함수다.

아래 skb_push함수를 보면 현재 기준으로 뒤에 Head Room 방향으로 칸을 만들어서 Head Room의 일부 공간을 확보한다.

그러면 헤더가 들어갈 자리가 만들어지게 되고 그때 헤더값을 넣는다.

void *skb_push(struct sk_buff *skb, unsigned int len)
{
	skb->data -= len;
	skb->len  += len;
	if (unlikely(skb->data < skb->head))
		skb_under_panic(skb, len, __builtin_return_address(0));
	return skb->data;
}

일반적인 통신에서는 커널이 기본적으로 넉넉하게 잡아두는 표준 사이즈의 Head Room만으로도 포장(TCP/IP/MAC)을 치는 데 아무런 문제가 없지만, K8s CNI나 IPsec VPN처럼 패킷을 겹겹이 이중 삼중으로 포장(Encapsulation)해야 하는 환경에서는 기본 Head Room 사이즈를 초과해버릴 수 있으므로 추가적인 아키텍처 고려가 필수적이다.

Data

네트워크 인터페이스 Ring Buffer에 수신된 패킷은 DMA를 통해 커널의 sk_buff 구조체가 가리키는 '선형 Data 영역'에 적재된다.

이때 적재되는 것은 MAC, IP, TCP 헤더와 실제 데이터(Payload)가 모두 합쳐진 Raw 데이터(Raw Packet) 전체다.

  • 커널이 이 거대한 통뼈 데이터에서 헤더를 구분해 내는 원리의 경우 각 프로토콜 헤더의 크기와 특정 필드(N번째 Byte)에 어떤 값이 들어있는지 이미 국제 표준(RFC)으로 엄격하게 협약되어 있어 그리 어려운일은 아니다.

커널은 데이터를 일일이 복사하거나 파싱하는 대신, 이 약속된 규격에 따라 skb_pull 같은 함수로 포인터만 뒤로 이동시키며 포장지(MAC, IP, TCP 헤더)를 차례대로 벗겨낸다.

이 모든 릴레이 과정이 끝나고 나면, 최종적으로 순수한 Payload만이 남아 어플리케이션으로 전달되게 된다.

  • 단, 거대한 페이로드의 경우 메모리 최적화를 위해 Data 영역에 모두 담기지 않고 skb_shared_info를 통해 비선형(Non-linear) 메모리 페이지로 흩어져 관리된다.

Ring Buffer와 NIC, 그리고 AWS ENA나 K8s CNI 환경에서의 네트워크 인터페이스 동작 과정은 기회가 된다면 다른 포스팅에서 더 깊게 다루어볼 예정이다.

 

Tail Room

사내망 방화벽과 AWS 클라우드를 VPN(IPsec)으로 연결할 때, 데이터 유출과 변조를 막기 위해 패킷을 암호화하는것은 누구나 아는 사실이다. 하지만 이때 커널은 앞쪽(Head Room)에 ESP 헤더를 붙이는 것뿐만 아니라, 패킷의 맨 뒤(Tail Room)에도 'ESP 트레일러'와 '인증 데이터(ICV)'라는 꼬리표를 반드시 붙여야 한다.

이때 커널은 Tail Room 공간을 사용할 때 skb_put 함수를 호출하여 포인터를 뒤(오른쪽)로 밀어 공간을 확보한다. 이때 미리 넉넉하게 비워두었던 Tail Room 공간을 파먹으며(사용하며) 꼬리표가 들어갈 자리를 확보하는 것이다.

void *skb_put(struct sk_buff *skb, unsigned int len)
{
	void *tmp = skb_tail_pointer(skb);
	SKB_LINEAR_ASSERT(skb);
	skb->tail += len;
	skb->len  += len;
	if (unlikely(skb->tail > skb->end))
		skb_over_panic(skb, len, __builtin_return_address(0));
	return tmp;
}

 

또한 패킷이 목적지에 도착하기 전, 선을 타고 오는 동안 노이즈 때문에 데이터가 깨지지 않았는지 확인하기 위해 패킷의 맨 끝에Checksum 데이터를 덧붙이는데, 최근에는 네트워크 카드(NIC) 하드웨어가 이 작업을 대신해 주는 경우(Hardware Offloading)가 많지만, 커널 소프트웨어에서 이를 직접 계산해서 붙여야 할 때는 바로 이 테일룸 공간이 사용된다.

 

여기에 더해 한가지 중요한 기능이 존재한다.

이더넷 통신에는 "아무리 작은 패킷이라도 전체 크기가 최소 64바이트는 되어야 전송할 수 있다"는 규칙(CSMA/CD 제약)이 있다. 만약 애플리케이션이 보낸 데이터가 너무 작아서 전체 패킷 크기가 64바이트에 미달한다면? 패킷이 Drop되지는 않는다.

그랬다면 우리는 '.'같은 단일 문자열을 비인증 프로토콜 위에서 보내지도 못했을테니 말이다.

이런 경우를 대비해 커널은 Tail Room 공간을 파먹으며 남는 공간을 의미 없는 '0'으로 마구 채워 넣는다. 택배 상자가 너무 헐렁할 때 빈 공간에 뽁뽁이를 채워 넣는 것과 같은 원리라고 보면 된다.

skb_shared_info

만약 페이로드 자체가 수십 KB 단위로 매우 크다면 어떨까?

커널은 이 거대한 데이터를 제한된 크기의 메인 선형 버퍼(Data 영역)에 꾸역꾸역 밀어 넣는 바보 같은 짓을 하지 않는다.

 

덩치가 큰 데이터를 담기 위해 커널 메모리에서 '거대하고 연속된 빈 공간'을 찾는 것은 메모리 단편화를 유발하고 심각한 성능 저하를 가져오기 때문이다.

 

대신 커널은 'Scatter-Gather'라는 고도의 전략을 사용한다. 거대한 페이로드는 커널의 남는 메모리 페이지(Page) 공간에 조각조각 흩어두고(Scatter), 메인 버퍼 꼬리에 위치한 skb_shared_info라는 구조체 명부에 "이 데이터 조각들은 저기 메모리 주소들에 흩어져 있어"라고 포인터만 기록해 두는 것이다.

 

이후 패킷이 네트워크 카드(NIC)로 전송될 때, 랜카드가 이 명부만 보고 흩어진 조각들을 직접 긁어모아(Gather) 선으로 쏴버린다. 데이터 복사를 시도조차 하지 않고 처리를 할 수 있는 효율적인 방법이라 생각되는 포인트이다.

 

 

L1 ~ L2

https://elixir.bootlin.com/linux/v7.1-rc6/source/net/core/dev.c#L6454

 

dev.c - net/core/dev.c - Linux source code v7.1-rc6 - Bootlin Elixir Cross Referencer

 

elixir.bootlin.com

 

L1에서 L2까지의 흐름은 다음과 같다.

RX (수신)일때

static __latent_entropy void net_rx_action(void)
{
	struct softnet_data *sd = this_cpu_ptr(&softnet_data);
	unsigned long time_limit = jiffies +
		usecs_to_jiffies(READ_ONCE(net_hotdata.netdev_budget_usecs));
	struct bpf_net_context __bpf_net_ctx, *bpf_net_ctx;
	int budget = READ_ONCE(net_hotdata.netdev_budget);
	LIST_HEAD(list);
	LIST_HEAD(repoll);

	bpf_net_ctx = bpf_net_ctx_set(&__bpf_net_ctx);
start:
	sd->in_net_rx_action = true;
	local_irq_disable();
	list_splice_init(&sd->poll_list, &list);
	local_irq_enable();

	for (;;) {
		struct napi_struct *n;

		skb_defer_free_flush();

		if (list_empty(&list)) {
			if (list_empty(&repoll)) {
				sd->in_net_rx_action = false;
				barrier();
				/* We need to check if ____napi_schedule()
				 * had refilled poll_list while
				 * sd->in_net_rx_action was true.
				 */
				if (!list_empty(&sd->poll_list))
					goto start;
				if (!sd_has_rps_ipi_waiting(sd))
					goto end;
			}
			break;
		}

		n = list_first_entry(&list, struct napi_struct, poll_list);
		budget -= napi_poll(n, &repoll);

		/* If softirq window is exhausted then punt.
		 * Allow this to run for 2 jiffies since which will allow
		 * an average latency of 1.5/HZ.
		 */
		if (unlikely(budget <= 0 ||
			     time_after_eq(jiffies, time_limit))) {
			/* Pairs with READ_ONCE() in softnet_seq_show() */
			WRITE_ONCE(sd->time_squeeze, sd->time_squeeze + 1);
			break;
		}
	}

	local_irq_disable();

	list_splice_tail_init(&sd->poll_list, &list);
	list_splice_tail(&repoll, &list);
	list_splice(&list, &sd->poll_list);
	if (!list_empty(&sd->poll_list))
		__raise_softirq_irqoff(NET_RX_SOFTIRQ);
	else
		sd->in_net_rx_action = false;

	net_rps_action_and_irq_enable(sd);
end:
	bpf_net_ctx_clear(bpf_net_ctx);
}

https://elixir.bootlin.com/linux/v7.1-rc6/source/net/core/dev.c#L7914

 

dev.c - net/core/dev.c - Linux source code v7.1-rc6 - Bootlin Elixir Cross Referencer

 

elixir.bootlin.com

[초기]

처음 유저의 요청이 패킷 형태로 NIC(네트워크 카드)에 수신된다.

NIC에는 일반적으로 Ring Buffer라는 링 형태의 디스크립터 구조가 존재하며, NIC는 이를 통해 리눅스 커널과 상호작용한다.

 

[NIC 전송 이후]

NIC는 수신한 raw 패킷을 DMA(메모리에 직접 쓰는)방식으로 미리 할당된 RX 버퍼(rx_buffer/page)에 직접 적재한다.

이때 그 위치는 rx descriptor가 가리킨다. 적재가 끝나면 NIC가 HW 인터럽트를 발생시켜 커널에 "처리할 패킷이 있다"고 알린다.

 

[추가 동작]

이때 매 패킷마다 인터럽트로 처리하면 트래픽이 몰릴 때 인터럽트 폭주가 발생하므로, 커널은 NAPI라는 방식을 쓴다.

최초 인터럽트를 받은 뒤 추가 인터럽트는 끄고, softirq(net_rx_action)가 폴링으로 RX 버퍼를 비우며 sk_buff를 구성한다.

이때 폴링 Budget은 기본 300이며, 이는 패킷 단위이다. NAPI는 이렇게 정해진 예산 한도 내에서 바라보며 폴링한다.

[DevOps/SRE Engineer를 위한 TIP]
이 budget을 다 쓰거나 time_limit을 초과하면 time_squeeze 를 올리고 양보한다.
이 값은 /proc/net/softnet_stat에서 확인 가능한 지표로, 계속 증가하면 budget이 부족하다는 신호이므로 튜닝의 근거가 된다.

 

[예외]

처리 못 한 작업이 남으면 __raise_softirq_irqoff로 softirq를 다시 예약해 다음 기회에 마저 비운다.

한편 코드를 보면 함수 진입/종료에서 bpf_net_ctx를 설정·해제하는데, 이는 RX 경로 자체에 eBPF가 개입할 여지가 있음을 의미하며, 뒤에서 다룰 Cilium의 동작과 직결된다.

TX (송신) 일때

static __latent_entropy void net_tx_action(void)
{
	struct softnet_data *sd = this_cpu_ptr(&softnet_data);

	if (sd->completion_queue) {
		struct sk_buff *clist;

		local_irq_disable();
		clist = sd->completion_queue;
		sd->completion_queue = NULL;
		local_irq_enable();

		while (clist) {
			struct sk_buff *skb = clist;

			clist = clist->next;

			WARN_ON(refcount_read(&skb->users));
			if (likely(get_kfree_skb_cb(skb)->reason == SKB_CONSUMED))
				trace_consume_skb(skb, net_tx_action);
			else
				trace_kfree_skb(skb, net_tx_action,
						get_kfree_skb_cb(skb)->reason, NULL);

			if (skb->fclone != SKB_FCLONE_UNAVAILABLE)
				__kfree_skb(skb);
			else
				__napi_kfree_skb(skb,
						 get_kfree_skb_cb(skb)->reason);
		}
	}

	if (sd->output_queue) {
		struct Qdisc *head;

		local_irq_disable();
		head = sd->output_queue;
		sd->output_queue = NULL;
		sd->output_queue_tailp = &sd->output_queue;
		local_irq_enable();

		rcu_read_lock();

		while (head) {
			spinlock_t *root_lock = NULL;
			struct sk_buff *to_free;
			struct Qdisc *q = head;

			head = head->next_sched;

			/* We need to make sure head->next_sched is read
			 * before clearing __QDISC_STATE_SCHED
			 */
			smp_mb__before_atomic();

			if (!(q->flags & TCQ_F_NOLOCK)) {
				root_lock = qdisc_lock(q);
				spin_lock(root_lock);
			} else if (unlikely(test_bit(__QDISC_STATE_DEACTIVATED,
						     &q->state))) {
				/* There is a synchronize_net() between
				 * STATE_DEACTIVATED flag being set and
				 * qdisc_reset()/some_qdisc_is_busy() in
				 * dev_deactivate(), so we can safely bail out
				 * early here to avoid data race between
				 * qdisc_deactivate() and some_qdisc_is_busy()
				 * for lockless qdisc.
				 */
				clear_bit(__QDISC_STATE_SCHED, &q->state);
				continue;
			}

			clear_bit(__QDISC_STATE_SCHED, &q->state);
			to_free = qdisc_run(q);
			if (root_lock)
				spin_unlock(root_lock);
			tcf_kfree_skb_list(to_free, q, NULL, qdisc_dev(q));
		}

		rcu_read_unlock();
	}

	xfrm_dev_backlog(sd);
}

https://elixir.bootlin.com/linux/v7.1-rc6/source/net/core/dev.c#L5780

 

dev.c - net/core/dev.c - Linux source code v7.1-rc6 - Bootlin Elixir Cross Referencer

 

elixir.bootlin.com

[초기]

이번엔 송신을 하는 입장이다보니 직접 애플리케이션이 send()/write()를 호출한다.

소켓을 거쳐 커널이 sk_buff 생성한다. RX는 NIC가 만들어서 위로 올렸지만, TX는 위(앱)에서 만들어서 아래로 내리는 형태이다.

 

[헤더 붙이기]

위 Layer에서 부터 내려오면서 L4(TCP)→L3(IP)→L2(MAC) 헤더를 skb_push로 앞쪽(Head Room)에 차곡차곡 붙인다.

여기서 앞에서 말한 Head Room쪽에서 설명한 "RX에서 pull로 벗기던 걸, TX에선 push로 붙인다"는 내용이 기억이 날 것 이다. 

 

[qdisc]

TX 동작에서 조금 중요한 비중을 차지하는 부분인데, TX할때 사실 중간에 패킷을 바로 NIC로 안 보내고 qdisc 라는 큐를 거친다.

여기서 트래픽 셰이핑/우선순위/pfifo_fast 같은 게 동작하는데, 이게 RX의 NAPI budget에 대응하는 TX쪽 핵심 메커니즘이다.

qdisc 내부의 스케줄링 알고리즘(셰이핑/우선순위 등)은 주제를 벗어나므로 깊게는 다루지 않는다.
다만 코드상 net_tx_action의 동작만 짚고 넘어가자면 아래 두가지 동작만 기억하면 좋다.
① completion_queue로 전송 완료된 skb를 해제한다.
② output_queue를 돌며 qdisc_run으로 큐에 쌓인 패킷을 송신 처리한다는 흐름만 짚고 넘어간다.

 

[드라이버 → DMA → NIC 전송]

qdisc에서 나온 skb를 드라이버가 받아서, DMA로 NIC에 전달 → NIC가 회선으로 송출.

RX에서는 DMA로 받았지만 이번엔 오히려 보내는 입장이기 때문에 TX에선 DMA로 보낸다.

 

[전송 완료 후 정리]

NIC가 "다 보냈다"라는 내용으로 인터럽트를 발생 → 앞서 본 net_tx_action이 skb를 해제한다

 

L3 ~ L4

netif_receive_skb로 sk_buff가 스택에 진입한 직후부터의 동작이다.

L3

[진입점]

netif_receive_skb → ip_rcv() (IPv4). 여기가 L2→L3의 경계다.

 

[ip_rcv에서 하는 검증]

- IP 헤더 체크섬, 버전, 길이 검증

- 여기단계에서 netfilter 훅이 박힘 → NF_INET_PRE_ROUTING.

- iptables의 PREROUTING 체인이 여기 단계다. (DNAT 걸리는 지점)

 

[라우팅 결정 (ip_rcv_finish → ip_route_input)]

- "이 패킷 내 거야(로컬)? 아니면 포워딩?" 갈림길이다.

- 로컬 → ip_local_deliver → L4로 올림

- 포워딩이면 → ip_forward → 다시 나감 (여기서 NF_INET_FORWARD 훅)

 

[로컬 배달 직전 또 훅]

- NF_INET_LOCAL_IN (iptables INPUT 체인)

 

무슨 권투선수도 아니고 계속 훅만 날리나 싶겠지만, 이 동작들이 L3에서 가장 중요한 단계이다.

이 훅이라는 것은 패킷을 이동시키는 게 아니라, 라우팅으로 정해진 경로의 길목마다 앉아서 패킷을 검사하거나 변형하거나 걸러낸다.

iptables/Cilium이 개입하는 게 전부 이 훅 지점이다.

L4

[ip_local_deliver → 프로토콜 핸들러 호출]

- 헤더의 protocol 필드 보고 → TCP면 tcp_v4_rcv, UDP면 udp_rcv

- sk_buff 글에서 "헤더 N번째 바이트 보고 구분"한다던 그 원리가 여기서 동작한다.

 

[소켓 찾기 (demux)]

- 4-tuple(출발IP/포트, 목적IP/포트)로 어느 소켓에 속하는지 해시 테이블에서 찾는다.

- 소켓 못 찾으면? TCP는 RST, UDP는 ICMP port unreachable를 뱉는다.

- "연결이 안 돼요"라고 울부짖는 트러블슈팅의 커널 레벨 근거가 여기라고 보면 된다.

 

[TCP 상태 처리 + 리시브 큐 적재]

- tcp_v4_rcv → 시퀀스 검증, 상태머신(ESTABLISHED 등) 처리를 한다.

- 정상이면 소켓의 receive queue에 sk_buff 적재

- 앱이 recv() 호출하면 거기서 꺼내간다. 그러면 비로소 유저스페이스까지 도달하게 된다.

그럼 TX는?

TX는 역순이다. 절대 쓰기 귀찮아서 요약하는게 아니라 진짜 역순이다.

앱 send() → tcp_sendmsg → ip_queue_xmit(L3, 라우팅+헤더) →
NF_INET_LOCAL_OUT/POSTROUTING(SNAT) → L2(아까 한 qdisc)

그래서 Cilium은?

iptables는 이 훅마다 규칙을 순차 검사하는데, 규칙이 수천 개로 늘면(K8S Service가 많아질수록) 선형 탐색 비용이 커진다.

Cilium은 이 netfilter 훅 체인 자체를 eBPF로 대체해 이런 탐색 비용을 줄이는데, 자세한 건 진짜 바로 뒤에서 다룬다.

Cilium?

우선 CNI라는 것에 대한 이해도가 있는 사람을 대상으로 설명을 진행한다.

[왜 기존 CNI방식이 느린가]

먼저 kube-proxy의 iptables 모드 문제를 깔고 들어가야한다.

 

기본적인 동작에서 쿠버는 K8S Service(ClusterIP) → Pod IP 변환을 iptables DNAT 규칙으로 한다.

(PRE_ROUTING에 들어가있다.)

 

Service/Pod가 늘면 규칙이 선형(O(n))으로 증가하는 구조가 된다.

즉 가볍게 이야기하면 패킷마다 규칙 체인을 위에서부터 쭉 검사하는데 양이 많아지면 이를 읽는데 시간이 오래걸린다는 이야기이다.

 

Service 수천 개면 규칙은 수만 개이고 이는 매칭 비용 폭증시키는데 영향을 주고 룰 업데이트를 위해 전체 테이블을 다시 쓰기 때문 룰 업데이트도 느려진다.

 

[eBPF가 어디서 개입하나]

eBPF는 커널 여러 훅 포인트에 프로그램을 붙일 수 있다. 여기서 핵심이 XDP랑 tc(traffic control) ingress이다.

XDP: 스택 진입 전이다. 거의 NIC 드라이버 레벨에서 패킷 가로채는데 이는 sk_buff 만들어지기도 전이다. 그래서 제일 빠른 지점이다.

tc/eBPF: sk_buff 생긴 직후이며, 여기는 아까 설명 중 L2~L3 사이. Cilium이 주로 여기랑 XDP 부분을 자주 사용한다.

 

즉 Cilium은 netfilter 훅 체인(PRE_ROUTING 등)에 도달하기 전에 eBPF로 가로채서 처리해버리기 때문에 K8S기존 동작보다 빠른 속도를 제공한다.

 

[왜 빠른지 요약]

 

  • 선형 탐색 제거: iptables가 규칙 순차 검사하던 걸, eBPF는 해시맵(eBPF map)으로 O(1) 조회한다. 즉 Service가 수천 개여도 한 방에 찾을 수 있다는 말이다.
  • 경로 단축: XDP는 네트워크 스택 대부분을 건너뛴다. Pod 간 통신에서 불필요한 iptables/netfilter/라우팅 전부 우회하게 되고 아까 우리가 본 sk_buff 글에서 '그 긴거' 라고 불려도 될 정도의 양인 릴레이들을 스킵한다.
  • 커널 진입 비용 절감: 위 내용에 의거하면 컨텍스트 스위칭/스택 통과 오버헤드 감소는 당연히 얻어간다.

 

마무리 정리

펀하고 쿨하고 섹시한 Kernel과 Network

 

  • sk_buff = 패킷이 스택 타고 올라가며 겪는 그 모든 릴레이
  • Cilium/eBPF = 그 릴레이 자체를 줄이거나 건너뛰는 것

우리가 위에서 복잡하고 머리아픈 sk_buff의 여정을 알았기에, 그걸 단축하는 eBPF의 가치를 이해할 수 있었다고 볼 수 있다.

 

Cilium 뿐만 아니라 이런 네트워크 스택에서의 간결함과 인터럽트를 통한 오버헤드 절감 등은 실제로 우리가 쓰고 있는 쿠버 내에서 다양한 곳에 존재한다.

 

복잡하게만 느껴지는 이런 시스템을 모두 이해하려고 하기 보다. 왜 이 스택이 만들어졌는지에 집중하면 파고들때 몰입이 잘 될 것 이다.