如果覺得文章對你有所啟發,可以考慮用 🌟 支持 Gthulhu 專案,短期目標是集齊 300 個 🌟 藉此被 CNCF Landscape 採納 [ref]。
克服了前面兩個巨大的挑戰後,基本上 Gthulhu 就已經有一定的穩定性了(至少在我的主力開發機器上熬過了漫長的 7x24 Hrs)。但是,還是有一個問題非常困擾我。
基本上,eBPF program 的全域變數會根據不同的宣告方式被歸類在不同的 segment:
struct {
struct bpf_map *cpu_ctx_stor;
struct bpf_map *task_ctx_stor;
struct bpf_map *queued;
struct bpf_map *dispatched;
struct bpf_map *priority_tasks;
struct bpf_map *running_task;
struct bpf_map *usersched_timer;
struct bpf_map *rodata;
struct bpf_map *data_uei_dump;
struct bpf_map *data;
struct bpf_map *bss;
struct bpf_map *goland;
} maps;
上方程式碼是由 bpftool 產生的 skeleton file 的一部分,可以發現 Gthulhu 使用的 eBPF program 就至少會有:
其中,bss 存放的資料非常重要:
struct main_bpf__bss {
u64 usersched_last_run_at;
u64 nr_queued;
u64 nr_scheduled;
u64 nr_running;
u64 nr_online_cpus;
u64 nr_user_dispatches;
u64 nr_kernel_dispatches;
u64 nr_cancel_dispatches;
u64 nr_bounce_dispatches;
u64 nr_failed_dispatches;
u64 nr_sched_congested;
} *bss;
這裡面的 nr_scheduled
以及 nr_queued
會影響 user space scheduler 為一個任務分配 time slice 的大小(呼應前面說的,scx_rustland 會根據待排程任務的數量決定 time slice)。
然而,libbpfgo 的 API 會將一個 bss section 視為一個 eBPF MAP,如果我今天先讀取後更新這份 MAP,在這個期間 eBPF program 只要對這個 MAP 裡面的資料增減,就會造成 DATA RACE 的問題。這類的問題如果出現在 DATABASE,就有點像是買超賣超的問題,不過在 DATABASE 的場景中使用 transaction 或是 DB Lock 就能解決這個問題了。
scx_rustland 本身會直接呼叫 skeleton API,所以能夠指定更新 bss map 的某一個欄位,為了克服這個惱人的問題,我的解法就是利用 eBPF skeleton!
// wrapper.c
#include "wrapper.h"
struct main_bpf *global_obj;
void *open_skel() {
struct main_bpf *obj = NULL;
obj = main_bpf__open();
main_bpf__create_skeleton(obj);
global_obj = obj;
return obj->obj;
}
u32 get_usersched_pid() {
return global_obj->rodata->usersched_pid;
}
void set_usersched_pid(u32 id) {
global_obj->rodata->usersched_pid = id;
}
void set_kugepagepid(u32 id) {
global_obj->rodata->khugepaged_pid = id;
}
void set_early_processing(bool enabled) {
global_obj->rodata->early_processing = enabled;
}
void set_default_slice(u64 t) {
global_obj->rodata->default_slice = t;
}
void set_debug(bool enabled) {
global_obj->rodata->debug = enabled;
}
void set_builtin_idle(bool enabled) {
global_obj->rodata->builtin_idle = enabled;
}
u64 get_nr_scheduled() {
return global_obj->bss->nr_scheduled;
}
u64 get_nr_queued() {
return global_obj->bss->nr_queued;
}
void notify_complete(u64 nr_pending) {
global_obj->bss->nr_scheduled = nr_pending;
}
void sub_nr_queued() {
if (global_obj->bss->nr_queued){
global_obj->bss->nr_queued--;
}
}
void destroy_skel(void*skel) {
main_bpf__destroy(skel);
}
golang 雖然無法像 rust 一樣直接使用 skeleton API,但我可以將這些 API 進行封裝,再利用 cgo 呼叫這些函式。
wrapper:
bpftool gen skeleton main.bpf.o > main.skeleton.h
clang -g -O2 -Wall -fPIC -I scx/build/libbpf/src/usr/include -I scx/build/libbpf/include/uapi -I scx/scheds/include -I scx/scheds/include/arch/x86 -I scx/scheds/include/bpf-compat -I scx/scheds/include/lib -c wrapper.c -o wrapper.o
ar rcs libwrapper.a wrapper.o
透過上面的方式,將 wrapper 變成靜態鏈結函式庫,供 Gthulhu 使用:
CGOFLAG = CC=clang CGO_CFLAGS="-I$(BASEDIR) -I$(BASEDIR)/$(OUTPUT)" CGO_LDFLAGS="-lelf -lz $(LIBBPF_OBJ) -lzstd $(BASEDIR)/libwrapper.a"
如此一來,就能夠在 golang 呼叫這些封裝過的 API 了:
func (s *Sched) AssignUserSchedPid(pid int) error {
C.set_kugepagepid(C.u32(KhugepagePid()))
C.set_usersched_pid(C.u32(pid))
return nil
}
func (s *Sched) SetDebug(enabled bool) {
C.set_debug(C.bool(enabled))
}
func (s *Sched) SetBuiltinIdle(enabled bool) {
C.set_builtin_idle(C.bool(enabled))
}
func (s *Sched) SetEarlyProcessing(enabled bool) {
C.set_early_processing(C.bool(enabled))
}
func (s *Sched) SetDefaultSlice(t uint64) {
C.set_default_slice(C.u64(t))
}
截至目前為止,我們已經探討了將 scx_rustland 重新以 golang 實作時會遇到的“大問題”,接下來就可以將我多年泡在 free5GC 學到的奇怪知識與想法結合 Gthulhu,打造一款面向雲原生應用的排程器方案了👍