eBPF 에 대해 알아보자

김세환·2024년 1월 20일
0

eBPF-cilium

목록 보기
1/1
post-thumbnail
post-custom-banner

최근 Kubernetes / Cloud Native 와 관련해서 가장 핫(?)한 기술 중 하나는 아마도 eBPF 일 것이다. 특히 Kubernetes 의 CNI 중 하나인 cilium 은 기술의 근간이 eBPF 이다. 최근 cilium 을 도입하기 위해서 개인적으로 알아보고, 공부하는 차원에서 작성하는 글이며 틀린 점이나 피드백을 줄 것이 있다면 댓글 환영합니다.

eBPF 란?

eBPF 는 extended Berkeley Packet Filter 의 약자로, 확장된 버클리 패킷 필터라고 번역 할 수 있다. eBPF 는 개발자가 커널에 동적으로 자신이 개발한 코드를 로드하여 커널의 동작을 변경 할 수 있는 기술인데, 자신이 개발한 코드가 네트워크와 관련된것일수도, 가시성(Observability)를 위한 것일 수도 있습니다.

eBPF 를 통해 고성능 네트워킹, 통합된 가시성, 그리고 보안 도구까지도 사용 가능합니다.

eBPF는 Linux 커널에 통합되어있으므로 최신 Linux 배포판에서 동작합니다.

또한 eBPF는 안전과 효율성을 위해서 Linux 커널 내의 자체 가상머신에서 실행됩니다.

BPF 자체는 버클리 패킷 필터의 약자로 1997년 커널 2.1.75 버전에서 Liunx에 처음 도입되었고, tcpdump 툴에서 패킷을 효율적으로 캡쳐하기 위해 도입되었습니다.

이후 커널 버전 3.18 부터 eBPF 라고 불리는 것으로 발전하였는데, eBPF maps 라는 것이 소개되었고, 이것은 user space 애플리케이션과 BPF 프로그램이 액세스 할 수 있는, 그리고 공유 할 수 있는 데이터 구조입니다.

또한 user space 애플리케이션과 상호작용 할 수 있도록 bpf() 시스템 콜이 추가되었고, BPF Helper function 들이 추가되었습니다.

여기서는 깊게 다루지 않고 eBPF 가 처음에는 BPF로 시작했고, 뭔가 좀 더 확장되어 갔구나 정도만 이해하면 될 것 같습니다.

Liunx 는 기본적으로 User SpaceKernel 로 분리되어있고, 우리가 만든 Application 은 기본적으로 User Space에서 실행되며, Kernel과 상호작용 하기 위해서는 system call 을 사용해서 처리하게 됩니다.

이것을 단적으로 쉽게 보는 방법은 strace 명령어를 사용하면 알 수 있습니다.

$ strace -c ls 와 같이 strace -c {command} 형태로 명령을 수행하면, 해당 명령/애플리케이션이 호출하는 시스템 콜이 무엇인지 알 수 있습니다.

이렇게 시스템콜을 호출해서 애플리케이션을 구현하는 것이 아니라, 정말 커널 영역 내에서 뭔가 내가 만든 코드가 돌아가게 하고싶다면 아마 커널 모듈을 직접 만들어서 컴파일하고 ,빌드하고, 운영체제에 로드하면 되겠지만. 그게 단순하거나 쉬운 작업이 아니기도 하고, 커널 전체의 안정성을 떨어트릴 확률이 큽니다.

eBPF는 커널 모듈을 작성하고 빌드하고 로드하지 않고도, 동적으로, 그리고 안전하게 커널 영역에서의 작업을 할 수 있습니다.

eBPF 는 네트워킹 영역에서도 매우 큰 성능 향상을 이끌어 내는데, eBPF 프로그램을 통해 패킷을 필터링(drop , redirect , pass) 하고 그 패킷들이 커널의 네트워크 스택을 타지 않고 바로 다른 NIC에 넘겨주거나, drop 할 수 있어서 오버헤드가 줄고 성능향상에 도움이 된다고 합니다.

실제로 클라우드플레어나 Meta 의 경우 ddos 공격에 대한 방어책으로 eBPF 프로그램을 쓴다고 알려져있고, 초당 천만개 이상의 패킷을 단일 CPU가 drop 하는 수준의 성능이라고 합니다.

결론적으로 매우 쉽게 이야기하자면, eBPF커널을 수정하거나, 커널 모듈을 만들어서 로드하거나 할 필요 없이 커널 영역에서 우리가 만든 프로그램을 동작시켜서 네트워크 패킷을 조작하거나, 가시성을 얻거나, 보안을 강화 하는 등 다양한 작업을 할 수 있게 하는 프로그램, 기능이라고 보면 됩니다.

Kubernetes 와는 무슨 관계가 있을까요

쿠버네티스에서 CNI(Container Network Interface) 는 https://github.com/containernetworking/cni/blob/main/SPEC.md 에 지정된 스펙에 따라서 구현하면 되는데, 기존 CNI, 그리고 kube-proxyiptables 기반으로 구현되어있는 경우가 대부분이였습니다. iptables는 정말 훌륭하게 각종 라우팅처리, 패킷 드롭, 패스 잘 해주고 있지만, iptables 에서 관리하는 테이블(규칙들)이 커지면 커질수록 새로운 규칙을 반영하는데도 오래걸리고, iptables 에서 들어온 패킷에 맞는 규칙을 찾는데 걸리는 시간이 늘어나서 전반적인 네트워크 성능이 저하되는 경우가 있다고 합니다.

eBPF는 근본적으로 iptables와 달리 커널의 네트워크 스택을 타지 않고 drop / redirect 등이 가능하기 때문에, 그리고 iptables 처럼 규칙을 찾는데 시간이 걸리는 것이 아닌 이미 로드된 eBPF 프로그램이 패킷을 처리하기때문에 iptables에 비해 성능이 비약적으로 향상된다고 알려져있습니다.

이와 관련해 자세한 내용은 다음 포스팅에서 작성해보도록 하겠습니다.

그래서 eBPF 코드 어떻게 짜는데?

아래 예제는 macOS 에서는 별도의 작업을 해야 동작할 수 있습니다. Linux (가상)머신에서 수행하는게 좋습니다. 테스트 코드는 https://github.com/lizrice/learning-ebpf 레포지토리를 참고하였습니다.

eBPF 애플리케이션을 작성하기 위한 여러 라이브러리와 프레임워크가 있지만 우선 BCC python 을 통해 간단히 테스트 해보도록 하겠습니다.

우선 여러분의 환경에 맞게 bcc 툴을 설치하면 되는데.
https://github.com/iovisor/bcc/blob/master/INSTALL.md
위 문서를 참조하고, ubuntu 기준으로는

$ sudo apt-get install bpfcc-tools linux-headers-$(uname -r) 로 설치 가능합니다.

그렇게 한 이후 hello.py 라는 이름의 파일을 아래와 같이 만들어 봅니다.

#!/usr/bin/python3  
from bcc import BPF

program = r"""
int hello(void *ctx) {
    bpf_trace_printk("Hello World!");
    return 0;
}
"""

b = BPF(text=program)
syscall = b.get_syscall_fnname("execve")
b.attach_kprobe(event=syscall, fn_name="hello")

b.trace_print()

아직 각 부분이 무엇을 하는지 알지 못하더라도, 동작시켜보면서 하나하나 확인해봅시다. 위 코드를 실행시켜보면 ($ python3 hello.py)

위와 같이 무언가 콘솔에 출력으로 찍히는것을 볼 수 있습니다. 우리는 Hello World! 를 출력해내는 우리의 첫번째 eBPF 프로그램을 작성하고 실행시켰습니다.

위 코드는 eBPF 프로그램과, eBPF 프로그램을 로드하고, 출력을 읽어오는 유저 스페이스의 코드 두 부분으로 구성되는데. program = ... 으로 시작하는 부분이 eBPF 프로그램이고 그 외 나머지가 유저스페이스의 우리의 애플리케이션입니다.

int hello(void *ctx) {
    bpf_trace_printk("Hello World!");
    return 0;
}

위에 해당하는 부분은 실제 커널에서 실행될 eBPF 프로그램입니다.

eBPF 프로그램은 Helper Function 들이 존재하고 bpf_trace_printk() 는 그 중 하나입니다. 이러한 Helper Function 들에 대해서는 나중 포스팅에서 다루도록 하겠습니다.

eBPF 프로그램은 실행하기전에 컴파일 해야 하지만 BCC가 이번 예제에서는 대신 처리하고 있습니다.

eBPF 프로그램은 eventattach 되어야 하는데 이 예시에서는 프로그램을 실행할때 호출되는 시스템 콜인 execveattach 되었습니다. 따라서 이 코드를 실행하는 머신에서 새로운 프로그램이 실행(시스템 콜 호출)될 때 마다 이 eBPF 프로그램이 호출되는 것 이라고 생각 할 수 있습니다.

더 자세한 eBPF 프로그램에 대한 내용은 나중 포스트에서 다루도록 하겠습니다.

cilium 도 eBPF 프로그램이 있는건가요?

cilium 을 Kubernetes 클러스터에 설치하게 되면 cilium agent 라는 데몬셋이 노드마다 설치되게 됩니다. 이 cilium agenteBPF 프로그램을 로드하고 eBPF 프로그램에 따라 네트워크 패킷이 처리되어서 CNI 규격에 맞는 동작을 하게 됩니다.

실제로 cilium 이 사용하는 eBPF 프로그램의 코드는
https://github.com/cilium/cilium/tree/main/bpf
위에서 볼 수 있습니다.

cilium 을 제 로컬 k8s 클러스터에서 테스트 할 수 있을까요?

minikube 환경에서 cilium 을 설치하는 방법을 소개합니다.
minikube 버전 v1.31.1 을 기준으로 합니다.
macOS 의 경우 도커 버전 4.25.2 이하 버전에서만 cilium 이 정상 동작합니다. (https://github.com/kubernetes/minikube/issues/17780)

$ minikube start \
  --driver=docker \
  --insecure-registry="quay.io" \
  --insecure-registry="registry.k8s.io" \
  --network=minikube \
  --network-plugin=cni \
  --cni=false

위와 같이 minikube 를 실행하면 cni 를 설치하지 않은 형태의 쿠버네티스 클러스터를 생성 할 수 있습니다.

이후

$ helm repo add cilium https://helm.cilium.io/
$ helm install cilium cilium/cilium --version 1.14.5 --namespace kube-system

형태로 cilium 을 헬름을 이용해서 설치 하실 수 있고.

https://github.com/kimsehwan96/istio-example/tree/velog-example/cilium 에서와 같은 구조로 kustomization.yaml 을 생성 후 $ kubectl kustomize --enable-helm . | kubectl apply -f - --server-side --force-conflicts 형태로 설치도 가능합니다.

정상적으로 설치되었다면 위에 보는 것 처럼 cilium 데몬셋이 정상적으로 떠있어야 하고. 단일 워커노드 클러스터의 경우 cilium operator 하나가 Pending 상태로 떠있는것은 정상입니다.
(https://guide.ncloud-docs.com/docs/k8s-k8strouble)
같은 사례로 nks 이용시 단일 워커노드 사용중일 경우 cilium operatorPending 으로 유지되는 현상이 있습니다.

참고 :

profile
DevOps 엔지니어로 핀테크 회사에서 일하고있습니다. 아직 많이 부족합니다.
post-custom-banner

0개의 댓글