본문 바로가기

Research Log/Tracing

BTF, CO-RE

Brendan Greeg's Blog [link]

BTF: BPF Type Format, which provides struct information to avoid needing Clang and kernel headers.

CO-RE: BPF Compile-Once Run-Everywhere, which allows compiled BPF bytecode to be relocatable, avoiding the need for recompilation by LLVM.

PingCAP Article [link]

BCC 단점

BCC(BPF Compiler Collection) toolkilt은 효과적인 kernel tracing을 지원하기 위해 만들어졌지만 여러 단점이 있다.

BCC는 LLVM 이나 Clang을 사용하여 BPF 프로그램은 재작성, 컴파일, 로드 한다.

Clang은 front-end에서 사용자가 작성한 BPF program을 컴파일 하는데 사용된다. 그러므로 문제가 발생하면 어떠한 지점에서 발생했는지 찾기가 어렵다.

또한 사용자는 naming convention과 자동적으로 생성되는 tracepoint struct를 기억하고 있어야한다.

libbcc 라이브러리는 LLVM 이나 Clang 라이브러리를 포함하고 있기 때문에 여러 문제들을 겪을 수 있다.

tool이 시작할 때 BPF를 컴파일하는데에 CPU 와 메모리 리소스를 사용한다. 그러므로 시스템 리소스가 부족한 서버에서는 문제가 발생할 수도 있다.

BCC는 hsot에 알맞은 kernel header package를 설치하여 사용해야한다.

BPF 프로그램이 runtime 도중에 컴파일 되기 때문에, 간단한 컴파일 에러도 runtime에 나타나게 된다.

libbpf + CO-RE 장점

반면 libbpf + BPF CO-RE를 사용하면 BPF 프로그램을 개발하기 위해 커널 개발자가 제공한 libbpf 라이브러리를 직접적으로 사용할 수 있다. 이 방식은 일반적인 C 프로그램을 작성하는 것과 동일하므로 하나의 작은 바이너리 파일로 컴파일할 수 있다.
libbpf는 BPF program loader처럼 동작하고 BPF 프로그램의 재배치, 로드, 확인을 담당한다. BPF 개발자는 BPF 프로그램의 오류와 성능에만 집중할 수 있다.

또한 이 방식은 기존의  LLVM이나 Clang과 같은 큰 dependency를 없애기 때문에 storage space와 runtime overhead를 줄일 수 있다.

성능 차이

Brandan Gregg의 말(link)에 따르면 CO-RE를 사용하였을때 BCC를 사용하는 것보다 메모리를 대략 9배 정도 적게 사용한다고 한다.

As my colleague Jason pointed out, the memory footprint of opensnoop as CO-RE is much lower than opensnoop.py. 9 Mbytes for CO-RE vs 80 Mbytes for Python.

PingCAP에서의 libbpf-tools 사용

PingCAP에서는 특정 워크로드의 I/O 성능을 분석할 때 block layer에서 성능을 관측하기 위해 bpf를 사용하는 여러 성능 분석 도구를 사용하고 있다고 한다.

Task Performance analysis tool
Check I/O requests' latency distribution ./biolatency -d nvme0n1
Analyze I/O mode ./biopattern -T 1 -d 259:0
Check the request size distribution diagram when the task sent physical I/O requests ./bitesize -c fio -T
Analyze each physical I/O ./biosnoop -d nvme0n1

분석 결과를 통해 I/O 성능을 tuning 할 수 있었고 추가적으로 scheduler 관련 libbpf-tool을 사용하여 데이터베이스를 tuning 할 계획이라고 한다.

BPF CO-RE (Compile Once – Run Everywhere) [link]

BPF: state of the art

BPF 어플리케이션을 직관적이고 user friendly 하게 만드는 것은 중요한 과제였으므로 개발을 쉽게 할 수 있도록 많은 발전이 있었다. 하지만 이러한 개선에도 불구하고 BPF 어플리케이션의 이식성(portability)는 기술적인 한계들로 인해 개선이 되지 못하고 있었다. BPF portability를 달성하기 위해서는 BPF 코드가 서로 다른 특정 커널에 대해 다시 컴파일 및 verifier를 통과할 필요없이 다른 커널 버전에서 올바르게 동작할 수 있어야한다. 이를 해결하기 위해 BPF CO-RE solution이 제시되었다.

The problem of BPF portability

BPF 프로그램은 커널에 직접 주입되는 사용자가 작성한 코드이다. 프로그램이 로드되고 verify되면 BPF 프로그램은 커널 영역에서 실행되게 된다. 이러한 프로그램은 커널의 모든 내부적인 상태에 접근이 가능하므로 매우 강력한 기능을 할 수 있게 된다. 그러나 이러한 강력한 기능들을 올바르게 구현하기 위해서 특정 커널에 정확히 일치하도록 환경을 갖추고 사용해야만 했기 때문에 BPF portability가 낮아지는 문제가 발생되었다.

게다가 커널 종류와 커널 데이터의 구조는 끊임없이 변화한다. 구조체의 필드의 이름이 바뀌거나 구조체끼리 서로 합쳐질 수도 있고 데이터 타입이 변경되어 호환이 되지 않을 수 있다. 같은 종류 및 같은 버전의 커널이라도 커널 컴파일 시 configuration에 따라도 변할 수 있다.

즉, 커널이 어떻게 배포되었는지에 따라 항상 상황이 변경되는 환경에 대처해야 하므로 BPF 프로그램 개발자가 BPF portability를 충족하기는 매우 어려운 문제였다. 이를 해결하기 위해 수행된 몇가지 방안이 있다.

먼저 모든 BPF 프로그램이 내부 커널 데이터 구조를 사용하지는 않는다. 그 예시 중 하나는 kprobes/tracepoints를 통해 몇가지 system call의 인자를 확인하고 어떤 프로세스가 어떤 파일을 여는지 추적하는 opensnoop tool이다. System call 파라미터는 안정적인 ABI를 제공하므로 커널 버전간에 변경되지 않기 때문에 portability의 문제가 없다. 하지만 이러한 프로그램이 수행할 수 있는 작업이 매우 제한적이므로 그 수도 매우 적다.

따라서 커널 내부의 BPF machinery는 BPF 프로그램이 어떤 커널에서든 사용할 수 있는 stable interface를 제공하여 이를 해결한다. 실제로 커널의 기본 구조와 메커니즘은 변경되지만 BPF-provided stable interace는 이러한 변경을 때에 맞게 적용할 수 있도록 사용자 프로그램의 정보를 추상화한다.

예를 들어 task_struct의 offset 16에 있다고 생각한 필드이지만 어떠한 커널에서는 필드를 추가하여 offset 24에서 읽어야 제대로된 데이터를 얻을 수 있다면 이는 어떻게 해결해야 할까? 또 thread-local storage에 접근하는데 유용한 thread_struct의 fs 필드가 커널 4.6~4.7에서 fsbase로 이름이 변경된 경우는? 커널의 서로 다른 configuration에서 실행하는데 그중 하나는 특정 기능이 거져있는 상황이라면? 이러한 kernel memory layout이 서로 다른 문제로 인해 개발자가 사용중인 서버의 kernel header를 사용하여 로컬에서 컴파일하고 그 컴파일된 형태를 다른 시스템에 배포할 수 없었다.

지금까지 사람들은 BCC에 의존하여 이러한 문제를 처리했다. BCC를 사용하면 BPF 프로그램 C 코드를 유저 스페이스의 control application에 포함시킬 수 있다. 그러므로 control appliction이 배포되어 실행될 때 BCC를 통해 내장된 Clang/LLVM을 호출하고 local kernel header(알맞은 kernel-devel package가 설치되어 있어야함)를 가지고 와서 호스트에서 BPF 프로그램을 즉석으로 컴파일시켜서 사용하였다. 이렇게 하면 BPF 프로그램이 예상하는 메모리 레이아웃이 해당 호스트에서 실행중인 커널과 동일하게 일치하게 되고, 만약 환경에 따라 선택적으로 일부 변경이 필요한 경우 소스 코드에서 #ifdef/#else 를 사용하여 해결할 수 있었다. 그러면 Embedded Clang은 코드에서 관련이 없는 부분을 제거하고 BPF 프로그램 코드를 특정 코드에 맞게 조정하여 컴파일 하였다.

이는 좋은 것처럼 보이지만, 이러한 과정에는 여러 단점이 있다.

- Clang/LLVM은 매우 큰 라이브러리이므로 어플리케이션을 배포할 때 대용량의 바이너리가 생긴다.

- Clang/LLVM은 리소스를 많이 사용하므로 BPF 코드를 컴파일 할 때 상당한 양의 리소스를 사용하게 되어 본래의 프로그램에 영향을 끼칠 수 있으며, busy 호스트에서는 작은 BPF 프로그램을 컴파일 하는데도 오랜 시간이 걸릴 수 있다.

- 시스템에 커널 헤더가 있어야만 동작할 수 있다. 개발 프로세스의 일부로 커스텀된 일회용 커널을 빌드하고 배포해야 하는 경우가 많기 때문에 커널 개발자가 알맞은 커널 헤더 패키지를 계속해서 만들어야 한다.

- User space의 control application을 다시 컴파일하고 재시작해야 BPF 코드가 컴파일 되므로 테스트 및 개발을 반복하는 것이 매우 힘들다.

전반적으로 BCC는 특히 빠른 프로토타이핑, 실험 및 소형 도구에 훌륭한 도구이지만 널리 배포된 프로덕션 BPF 응용 프로그램에 사용할 경우 확실히 많은 단점이 있다. 그러나 BPF CO-RE는 portability를 강화하고 있으며 위의 단점을 해소하기 위해 많은 개선을 해왔다.

High-level BPF CO-RE mechanics

BPF CO-RE는 kernel, user-space BPF loader library(libbpf) 그리고 컴파일러(Clang)와 같은 소프트웨어 스택의 모든 수준에서 필요한 기능과 데이터를 제공하여 pre-compiled BPF 프로그램에서 서로 다른 커널 간의 불일치를 처리하여 이식 가능하능한 BPF 프로그램을 쉽게 작성할 수 있도록 한다.

BPT CO-RE를 구현하기 위해서는 아래의 구성요소들이 필요하다.

- BTF type information은 커널 및 BPF 프로그램의 자료형 및 코드에 대한 중요한 정보를 가지고 있다.
- 컴파일러(Clang)를 통해 BPF 프로그램 C 코드가 의미를 표현하고 relocation information을 기록할 수 있다.
- BPF 로더(libbpf)는 대상 호스트의 특정 커널에 컴파일된 BPF 코드를 조정하기 위해 커널과 BPF 프로그램의 BTF를 연결시킨다.
- Kernel은 BPF CO-RE의 불가지론을 지키면서 고급 BPF 기능을 제공하여 일부 고급 시나리오를 가능하게 한다.

BTF(BPF Type Format)

BPF CO-RE 접근법을 가능하게 하는 중요한 요소 중 하나는 BTF이다. BTF는 보다 일반적이고 verbose한 DWARF debug 정보의 대안으로 만들어졌다. BTF는 space-efficient, compact 하며 C 프로그램의 모든 type information을 충분히 표현할 수 있는 데이터 형태이다. BTF duplication algorithm과 BTF 자체의 simplicity한 특정으로 인해 BTF는 DWARF와 비교하여 크기를 최대 100개 줄일 수 있다. 커널 빌드 과정에서 CONFIG_DEBUG_INFO_BTF=y 옵션을 적용하기만 하면 이제 런타임에 항상 BTF type information이 포함되어 있는 Linux 커널을 빌드할 수 있으며 이것이 더욱 실용적이다.

커널의 BTF는 커널 동작 과정에서 직접 사용되기도 하며 BPF 검증기의 기능을 향상시키는데 사용된다. (예: bpf_probe_read() 없이 직접 커널 메모리 읽기가 가능해짐) BPF CO-RE와 관련한 더욱 중요한 점은 커널이 /sys/kernel/btf/vmlinux의 sysfs를 통해 권한이 필요했었던(authoritative) BTF information를 자체적으로 노출할 수 있다는 것이다. 즉 "bpftool btf dump file /sys/kernel/btf/vmlinux format c" 명령어를 통해서 모든 kernel type을 표현하는 컴파일 가능한 C 헤더 파일인 "vmlinux.h"  얻을 수 있다. 그리고 이 파일은 kernel-devel 패키지를 통해 제공되지 않았던 정보들 또한 포함되어 있다.

Compiler support

BPF CO-RE를 활성화하고 BPF loader(libbpf)가 BPF 프로그램을 대상 호스트에서 실행가능한 커널로 조정하도록 하기 위해 Clang은 몇가지 내장 기능(built-in)들을 추가하였다. 그 기능들은 BPF 프로그램 코드가 읽으려는 정보에 대한 high-level description인 BTF relocation을 알려주는(emit) 역할을 한다.

이를 통해 코드에서 task_struct->pid field에 접근하려는 경우 Clang이 "struct task_struct" 에 있는 "pid_t" 유형의 "pid"라는 이름의 field라는 것을 명확히 기록해둘 수 있게되고, 대상 커널에 task_struct 구조 내에서 "pid" 필드가 다른 offset으로 이동된 경우 혹은 다른 nested anonymous struct 나 union으로 변경되었더라도 field의 이름과 type 정보만으로 여전히 같은 값에 접근할 수 있도록 한다. 이를 field offset relocation이라고 한다.

또한 field offset 뿐만 아니라 field 존재 여부나 size와 같은 다른 속성들이 변경된 경우에도 이를 감지(capture) 후 재배치(relocate)하는 것이 가능하다. 심지어 bitfield들의 경우에도 BPF 프로그램 개발자에게 투명한 방법으로 relocation을 할 수 있는 충분한 정보를 제공한다.

BPF loader(libbpf)

Libbpf는 BPF program loader의 역할로 이전에 언근합 모든 데이터들(kernel BTF와 Clang relocation)이 합쳐서 처리한다. 그 데이터들은 BPF ELF object file로 컴파일되고, 컴파일된 BPF ELF object file을 가지고 와서 필요에 따라 후처리하며, 다양한 커널 개체(maps, programs, etc)를 설정하고 BPF 프로그램 load 및 verification하는 작업을 시작한다.

Libbpf는 BPF 프로그램에 기록된 BTF type과 relocation information을 보고 호스트에서 실행중인 커널의 BTF information에 맞춰 BPF 프로그램 코드를 수정하는 역할을 한다: BPF 프로그램의 logic이 호스트의 커널에서 올바르게 동작하는지 확인하기 위해 모든 types과 fields를 확인하여 일치시키고, 필요에 따라 offset을 업데이트하거나 다른 데이터들을 relocation 시킴.

모든 확인 작업이 끝나면 개발자는 대상 호스트의 커널을 위해 컴파일된 것과 같은 "맞춤형"인 BPF 프로그램을 얻을 수 있다. 또한 이 모든 작업은 어플리케이션과 함께 Clang을 배포하지 않고 호스트의 런타임 도중에 컴파일을 하는 오버헤드가 없이 이루어진다.

Kernel

놀랍게도 BPF CO-RE를 지원하기 위해 커널의 많은 변경이 필요하지는 않다. libbpf가 BPF 프로그램 코드를 처리한 후에는 커널의 입장에서는 이미 valid한 BPF 프로그램으로 보이기 때문이다. 이로 인해 최신 커널 헤더가 있는 호스트에서 직접 컴파일한 BPF 프로그램과 차이를 구별할 수 없다. 이것은 BPF CO-RE가 많은 기능을 지원하기 위해 최신의 커널 기능은 필요로 하지 않는다는 것을 의미하므로, 기술이 훨씬 더 광범위하게 빨리 배포될 수 있다는 것을 의미한다.

최신 커널이 필요한 일부 고급 기능이 있지만, 매우 드문 경우이다. 이러한 경우는 다음 장에서 BPF CO-RE API를 설명할 때 자세히 설명한다.

BPF CO-RE: user-facing experience

실제 BPF 응용 프로그램이 처리해야 하는 일반적인 시나리오를 살펴보고 BPF CO-RE로 해결하는 방법을 살펴본다. 아래에서 볼 수 있듯이 일부 이식성 문제(예: 구조체 레이아웃 차이)는 매우 투명하고 자연스럽게 처리되는 반면 다른 문제들은 보다 명시적으로 처리된다. 예를 들어 if/else 조건부(BCC 프로그램에서 compile time에 적용되는 #ifdef/#else 방식과는 다름) 그리고 BPF CO-RE에서 추가적으로 제공하는 메커니즘으로 해결한다.

Getting rid of kernel header dependency

Reading kernel structure’s fields

Dealing with kernel version and configuration differences

Altering behavior based on user-provided configuration

RECAP

BPF CO-RE의 목표는 개발자가 간단한 방식으로 이식성 문제를 해결하는 것이다.

- vmlinux.h는 커널 헤더에 대한 종속성을 제거함
- field relocation(field offset, existence, size 등)은 커널에서 데이터 추출을 하여 portability 높임
- libbpf에서 제공하는 Kconfig extern 변수를 사용하면 BPF 프로그램이 다양한 커널 버전 및 configuration 변경 사항에 대처할 수 있음
- 다른 모든 것이 실패하더라도 app-provided read-only configuration과 struct 특징은 어플리케이션이 처리해야 하는 복잡한 시나리오를 처리하는 궁극적인 큰 도움을 줄 수 있음

CO-RE의 모든 기능이 portable BPF 프로그램을 작성, 배포, 유지 관리하는데 필요한 것은 아니지만 portability와 관련되지 않은 다른 기능들이 필요할 때에 가능한 가장 간단한 방법으로 문제를 해결하는 데 도움을 줄 수 있다. CO-RE를 사용함으로서 더 이상 무거운 컴파일러 라이브러리를 가지고 가서 런타임 컴파일을 위해 귀중한 런타임 리소스를 지불할 필요가 없고 런타임에 사소한 컴파일 오류를 잡을 필요도 없어진다.

BPF CO-RE as of 2021

'Research Log > Tracing' 카테고리의 다른 글

The PMCs of EC2: Measuring IPC  (0) 2022.03.06
Perf Events  (0) 2022.03.06
CPU cycle에 대한 고찰  (0) 2022.03.06
PCI  (0) 2022.03.02
[17 SOSP] Canopy: An End-to-End Performance Tracing And Analysis System  (0) 2022.03.02