Nuo didelio Ceph delsos iki branduolio pataisos naudojant eBPF / BCC

Nuo didelio Ceph delsos iki branduolio pataisos naudojant eBPF / BCC

„Linux“ turi daugybę įrankių, skirtų branduoliui ir programoms derinti. Dauguma jų turi neigiamą poveikį programos veikimui ir negali būti naudojami gamyboje.

Prieš porą metų buvo buvo sukurtas kitas įrankis - eBPF. Tai leidžia atsekti branduolį ir vartotojo programas su mažomis sąnaudomis ir nereikia perdaryti programų ir įkelti į branduolį trečiųjų šalių modulių.

Jau yra daug taikomųjų programų, naudojančių eBPF, ir šiame straipsnyje apžvelgsime, kaip sukurti savo profiliavimo įrankį pagal biblioteką. PythonBCC. Straipsnis paremtas tikrais įvykiais. Pereisime nuo problemos prie sprendimo, kad parodytume, kaip esamos komunalinės paslaugos gali būti naudojamos konkrečiose situacijose.

Cefas yra lėtas

Prie Ceph klasterio buvo pridėtas naujas pagrindinis kompiuteris. Perkėlus į jį kai kuriuos duomenis, pastebėjome, kad jo rašymo užklausų apdorojimo greitis buvo daug mažesnis nei kituose serveriuose.

Nuo didelio Ceph delsos iki branduolio pataisos naudojant eBPF / BCC
Skirtingai nuo kitų platformų, šis kompiuteris naudojo „bcache“ ir naują „Linux 4.15“ branduolį. Tai buvo pirmas kartas, kai čia buvo naudojamas šios konfigūracijos kompiuteris. Ir tuo metu buvo aišku, kad problemos šaknis teoriškai gali būti bet kas.

Tiriantis šeimininką

Pradėkime nuo to, kas vyksta ceph-osd procese. Tam naudosime puikus и flameskopas (daugiau apie tai galite perskaityti čia):

Nuo didelio Ceph delsos iki branduolio pataisos naudojant eBPF / BCC
Paveikslėlis mums rodo, kad funkcija fdatasync () praleido daug laiko siųsdamas užklausą funkcijoms generic_make_request(). Tai reiškia, kad greičiausiai problemų priežastis yra kažkur už paties osd demono. Tai gali būti branduolys arba diskai. Iostato išvestis parodė didelį užklausų apdorojimo bcache diskuose delsą.

Tikrindami pagrindinį kompiuterį nustatėme, kad systemd-udevd demonas sunaudoja daug procesoriaus laiko - apie 20% keliuose branduoliuose. Tai keistas elgesys, todėl reikia išsiaiškinti, kodėl. Kadangi „Systemd-udevd“ veikia su įvykiais, nusprendėme juos peržiūrėti udevadm monitorius. Paaiškėjo, kad kiekvienam sistemos blokiniam įrenginiui buvo sugeneruota daug pokyčių įvykių. Tai gana neįprasta, todėl turėsime pažvelgti į tai, kas generuoja visus šiuos įvykius.

Naudojant BCC įrankių rinkinį

Kaip jau išsiaiškinome, branduolys (ir ceph demonas sistemos iškvietime) praleidžia daug laiko generic_make_request(). Pabandykime išmatuoti šios funkcijos greitį. IN BCC Jau yra nuostabi priemonė - funkcionalumas. Mes suseksime demoną pagal jo PID su 1 sekundės intervalu tarp išėjimų ir išvesime rezultatą milisekundėmis.

Nuo didelio Ceph delsos iki branduolio pataisos naudojant eBPF / BCC
Ši funkcija paprastai veikia greitai. Viskas, ką ji daro, yra perduoti užklausą įrenginio tvarkyklės eilei.

Bcache yra sudėtingas įrenginys, kurį iš tikrųjų sudaro trys diskai:

  • atsarginis įrenginys (talpyklos diskas), šiuo atveju tai lėtas HDD;
  • talpyklos įrenginys (talpyklos diskas), čia yra vienas NVMe įrenginio skaidinys;
  • bcache virtualus įrenginys, su kuriuo veikia programa.

Žinome, kad užklausos perdavimas yra lėtas, bet kuriam iš šių įrenginių? Tai nagrinėsime šiek tiek vėliau.

Dabar žinome, kad įvykiai gali sukelti problemų. Rasti, kas tiksliai sukelia jų atsiradimą, nėra taip paprasta. Tarkime, kad tai tam tikra programinė įranga, kuri periodiškai paleidžiama. Pažiūrėkime, kokia programinė įranga veikia sistemoje naudojant scenarijų execsnoop iš to paties BCC paslaugų rinkinys. Paleiskite jį ir nusiųskite išvestį į failą.

Pavyzdžiui taip:

/usr/share/bcc/tools/execsnoop  | tee ./execdump

Čia nerodysime visos „execsnoop“ išvesties, tačiau viena mus dominanti eilutė atrodė taip:

sh 1764905 5802 0 sudo arcconf getconfig 1 AD | grep Temperature | awk -F '[:/]' '{print $2}' | sed 's/^ ([0-9]*) C.*/1/'

Trečias stulpelis yra proceso PPID (parent PID). Procesas su PID 5802 pasirodė esąs viena iš mūsų stebėjimo sistemos gijų. Tikrinant stebėjimo sistemos konfigūraciją, buvo rasti klaidingi parametrai. HBA adapterio temperatūra buvo matuojama kas 30 sekundžių, tai yra daug dažniau nei būtina. Pakeitę tikrinimo intervalą į ilgesnį, nustatėme, kad užklausų apdorojimo delsa šiame priegloboje nebeišsiskyrė, palyginti su kitais pagrindiniais kompiuteriais.

Tačiau vis dar neaišku, kodėl bcache įrenginys buvo toks lėtas. Parengėme bandomąją platformą su identiška konfigūracija ir bandėme atkurti problemą paleisdami fio bcache, periodiškai paleisdami udevadm trigerį, kad generuotume uevents.

BCC pagrįstų įrankių rašymas

Pabandykime parašyti paprastą įrankį, skirtą atsekti ir rodyti lėčiausius skambučius generic_make_request(). Mus taip pat domina disko, kuriam buvo iškviesta ši funkcija, pavadinimas.

Planas paprastas:

  • Registruotis kprobe apie generic_make_request():
    • Išsaugome disko pavadinimą į atmintį, pasiekiamą per funkcijos argumentą;
    • Išsaugome laiko žymą.

  • Registruotis kretprobe grįžimui iš generic_make_request():
    • Gauname esamą laiko žymą;
    • Ieškome išsaugotos laiko žymos ir lyginame ją su esama;
    • Jei rezultatas yra didesnis nei nurodytas, tada mes surandame išsaugoto disko pavadinimą ir parodome jį terminale.

Kprobes и kretzondai naudokite pertraukos taško mechanizmą, kad pakeistumėte funkcijos kodą. Galite skaityti dokumentacija и geras straipsnis šia tema. Jei pažvelgsite į įvairių paslaugų kodą BCC, tada pamatysite, kad jų struktūra yra identiška. Taigi šiame straipsnyje praleisime scenarijaus argumentų analizę ir pereisime prie pačios BPF programos.

eBPF tekstas python scenarijaus viduje atrodo taip:

bpf_text = “”” # Here will be the bpf program code “””

Duomenims keistis tarp funkcijų naudoja eBPF programos maišos lentelės. Mes darysime tą patį. Mes naudosime proceso PID kaip raktą ir apibrėžsime struktūrą kaip reikšmę:

struct data_t {
	u64 pid;
	u64 ts;
	char comm[TASK_COMM_LEN];
	u64 lat;
	char disk[DISK_NAME_LEN];
};

BPF_HASH(p, u64, struct data_t);
BPF_PERF_OUTPUT(events);

Čia registruojame maišos lentelę, vadinamą p, su rakto tipu u64 ir tipo reikšmė struct data_t. Lentelė bus prieinama mūsų BPF programos kontekste. BPF_PERF_OUTPUT makrokomanda registruoja kitą lentelę, vadinamą renginiai, kuris naudojamas duomenų perdavimas į vartotojo erdvę.

Matuojant delsą tarp funkcijos iškvietimo ir grįžimo iš jos arba tarp iškvietimų į skirtingas funkcijas, reikia atsižvelgti į tai, kad gauti duomenys turi priklausyti tam pačiam kontekstui. Kitaip tariant, turite atsiminti apie galimą lygiagretų funkcijų paleidimą. Turime galimybę išmatuoti delsą tarp funkcijos iškvietimo vieno proceso kontekste ir grįžimo iš tos funkcijos kito proceso kontekste, tačiau tai greičiausiai nenaudinga. Čia būtų geras pavyzdys biolatencijos naudingumas, kur maišos lentelės raktas nustatytas kaip žymeklis į struktūros užklausa, kuris atspindi vieną disko užklausą.

Tada turime parašyti kodą, kuris bus paleistas, kai bus iškviesta tiriama funkcija:

void start(struct pt_regs *ctx, struct bio *bio) {
	u64 pid = bpf_get_current_pid_tgid();
	struct data_t data = {};
	u64 ts = bpf_ktime_get_ns();
	data.pid = pid;
	data.ts = ts;
	bpf_probe_read_str(&data.disk, sizeof(data.disk), (void*)bio->bi_disk->disk_name);
	p.update(&pid, &data);
}

Čia pirmasis iškviestos funkcijos argumentas bus pakeistas antruoju argumentu generic_make_request(). Po to gauname proceso, kurio kontekste dirbame, PID ir esamą laiko žymą nanosekundėmis. Viską užrašome į ką tik pasirinktą struct data_t data. Disko pavadinimą gauname iš struktūros biologinės, kuris perduodamas skambinant generic_make_request(), ir išsaugokite jį toje pačioje struktūroje duomenys. Paskutinis žingsnis yra pridėti įrašą į maišos lentelę, kuri buvo minėta anksčiau.

Ši funkcija bus iškviesta grįžus iš generic_make_request():

void stop(struct pt_regs *ctx) {
    u64 pid = bpf_get_current_pid_tgid();
    u64 ts = bpf_ktime_get_ns();
    struct data_t* data = p.lookup(&pid);
    if (data != 0 && data->ts > 0) {
        bpf_get_current_comm(&data->comm, sizeof(data->comm));
        data->lat = (ts - data->ts)/1000;
        if (data->lat > MIN_US) {
            FACTOR
            data->pid >>= 32;
            events.perf_submit(ctx, data, sizeof(struct data_t));
        }
        p.delete(&pid);
    }
}

Ši funkcija panaši į ankstesnę: sužinome proceso PID ir laiko žymą, bet neskiriame atminties naujai duomenų struktūrai. Vietoj to, maišos lentelėje ieškome jau esamos struktūros, naudodami raktą == dabartinis PID. Jei struktūra randama, mes sužinome vykdomo proceso pavadinimą ir pridedame jį prie jo.

Dvejetainis poslinkis, kurį naudojame čia, reikalingas gijos GID gauti. tie. Pagrindinio proceso, kuris pradėjo giją, kurios kontekste mes dirbame, PID. Funkcija, kurią mes vadiname bpf_get_current_pid_tgid() grąžina gijos GID ir jos PID viena 64 bitų verte.

Išvesdami į terminalą šiuo metu nesidomime sriegiu, bet domimės pagrindiniu procesu. Palyginę gautą uždelsimą su tam tikra riba, pereiname į savo struktūrą duomenys į vartotojo erdvę per lentelę renginiai, po to ištriname įrašą iš p.

Python scenarijuje, kuris įkels šį kodą, turime pakeisti MIN_US ir FACTOR delsos slenksčiais ir laiko vienetais, kuriuos pateiksime per argumentus:

bpf_text = bpf_text.replace('MIN_US',str(min_usec))
if args.milliseconds:
	bpf_text = bpf_text.replace('FACTOR','data->lat /= 1000;')
	label = "msec"
else:
	bpf_text = bpf_text.replace('FACTOR','')
	label = "usec"

Dabar turime paruošti BPF programą per BPF makrokomandas ir registruoti pavyzdžius:

b = BPF(text=bpf_text)
b.attach_kprobe(event="generic_make_request",fn_name="start")
b.attach_kretprobe(event="generic_make_request",fn_name="stop")

Taip pat turėsime nustatyti struct data_t mūsų scenarijuje, kitaip nieko negalėsime perskaityti:

TASK_COMM_LEN = 16	# linux/sched.h
DISK_NAME_LEN = 32	# linux/genhd.h
class Data(ct.Structure):
	_fields_ = [("pid", ct.c_ulonglong),
            	("ts", ct.c_ulonglong),
            	("comm", ct.c_char * TASK_COMM_LEN),
            	("lat", ct.c_ulonglong),
            	("disk",ct.c_char * DISK_NAME_LEN)]

Paskutinis žingsnis yra išvesti duomenis į terminalą:

def print_event(cpu, data, size):
    global start
    event = ct.cast(data, ct.POINTER(Data)).contents
    if start == 0:
        start = event.ts
    time_s = (float(event.ts - start)) / 1000000000
    print("%-18.9f %-16s %-6d   %-1s %s   %s" % (time_s, event.comm, event.pid, event.lat, label, event.disk))

b["events"].open_perf_buffer(print_event)
# format output
start = 0
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

Pats scenarijus pasiekiamas adresu GItHub. Pabandykime paleisti jį bandomojoje platformoje, kurioje veikia fio, rašo į bcache ir iškviečiame udevadm monitorių:

Nuo didelio Ceph delsos iki branduolio pataisos naudojant eBPF / BCC
Pagaliau! Dabar matome, kad tai, kas atrodė kaip stringantis bcache įrenginys, iš tikrųjų yra stringantis skambutis generic_make_request() talpykloje išsaugotam diskui.

Pasinerkite į branduolį

Kas tiksliai sulėtėja perduodant užklausą? Matome, kad vėlavimas atsiranda dar neprasidėjus užklausų apskaitai, t.y. konkretaus prašymo dėl tolesnės statistikos apie jį (/proc/diskstats arba iostat) apskaita dar nepradėta. Tai galima lengvai patikrinti paleidus iostat atkuriant problemą arba BCC scenarijaus biolatencija, kuri pagrįsta užklausų apskaitos pradžia ir pabaiga. Nė viena iš šių paslaugų nerodys problemų dėl užklausų talpykloje esančio disko.

Jei žiūrėsime į funkciją generic_make_request(), tada pamatysime, kad prieš pradedant skaičiuoti užklausą, iškviečiamos dar dvi funkcijos. Pirmas - generic_make_request_checks(), atlieka prašymo dėl disko nustatymų teisėtumo patikras. Antra - blk_queue_enter(), kuri turi įdomų iššūkį laukti_įvykis_pertraukiamas():

ret = wait_event_interruptible(q->mq_freeze_wq,
	(atomic_read(&q->mq_freeze_depth) == 0 &&
	(preempt || !blk_queue_preempt_only(q))) ||
	blk_queue_dying(q));

Jame branduolys laukia, kol eilė išsijungs. Išmatuokime vėlavimą blk_queue_enter():

~# /usr/share/bcc/tools/funclatency  blk_queue_enter -i 1 -m               	 
Tracing 1 functions for "blk_queue_enter"... Hit Ctrl-C to end.

 	msecs           	: count 	distribution
     	0 -> 1      	: 341  	|****************************************|

 	msecs           	: count 	distribution
     	0 -> 1      	: 316  	|****************************************|

 	msecs           	: count 	distribution
     	0 -> 1      	: 255  	|****************************************|
     	2 -> 3      	: 0    	|                                    	|
     	4 -> 7      	: 0    	|                                    	|
     	8 -> 15     	: 1    	|                                    	|

Atrodo, kad esame arti sprendimo. Funkcijos, naudojamos eilei sustabdyti / atšaukti blk_mq_freeze_queue и blk_mq_unfreeze_queue. Jie naudojami, kai reikia pakeisti užklausų eilės nustatymus, kurie gali būti pavojingi užklausoms šioje eilėje. Kai skambina blk_mq_freeze_queue() funkcija blk_freeze_queue_start() skaitiklis padidinamas q->mq_užšalimo_gylis. Po to branduolys laukia, kol eilė ištuštės blk_mq_freeze_queue_wait().

Laikas, kurio reikia šiai eilei išvalyti, prilygsta disko delsai, nes branduolys laukia, kol bus baigtos visos eilės operacijos. Kai eilė tuščia, taikomi nustatymų pakeitimai. Po to jis vadinamas blk_mq_unfreeze_queue(), mažinant skaitiklį užšalimo_gylis.

Dabar žinome pakankamai, kad ištaisytume situaciją. Naudojant udevadm trigerio komandą, taikomi blokavimo įrenginio parametrai. Šie nustatymai aprašyti udev taisyklėse. Galime sužinoti, kurie nustatymai užšaldo eilę, bandydami juos pakeisti per sysfs arba žiūrėdami į branduolio šaltinio kodą. Taip pat galime išbandyti BCC įrankį sekti, kuri išves branduolio ir vartotojo erdvės kamino pėdsakus kiekvienam iškvietimui į terminalą blk_freeze_queue, pavyzdžiui:

~# /usr/share/bcc/tools/trace blk_freeze_queue -K -U
PID 	TID 	COMM        	FUNC        	 
3809642 3809642 systemd-udevd   blk_freeze_queue
    	blk_freeze_queue+0x1 [kernel]
    	elevator_switch+0x29 [kernel]
    	elv_iosched_store+0x197 [kernel]
    	queue_attr_store+0x5c [kernel]
    	sysfs_kf_write+0x3c [kernel]
    	kernfs_fop_write+0x125 [kernel]
    	__vfs_write+0x1b [kernel]
    	vfs_write+0xb8 [kernel]
    	sys_write+0x55 [kernel]
    	do_syscall_64+0x73 [kernel]
    	entry_SYSCALL_64_after_hwframe+0x3d [kernel]
    	__write_nocancel+0x7 [libc-2.23.so]
    	[unknown]

3809631 3809631 systemd-udevd   blk_freeze_queue
    	blk_freeze_queue+0x1 [kernel]
    	queue_requests_store+0xb6 [kernel]
    	queue_attr_store+0x5c [kernel]
    	sysfs_kf_write+0x3c [kernel]
    	kernfs_fop_write+0x125 [kernel]
    	__vfs_write+0x1b [kernel]
    	vfs_write+0xb8 [kernel]
    	sys_write+0x55 [kernel]
    	do_syscall_64+0x73 [kernel]
    	entry_SYSCALL_64_after_hwframe+0x3d [kernel]
    	__write_nocancel+0x7 [libc-2.23.so]
    	[unknown]

Udev taisyklės keičiasi gana retai ir dažniausiai tai vyksta kontroliuojamai. Taigi matome, kad net pritaikius jau nustatytas reikšmes uždelsta užklausos perkėlimas iš programos į diską. Žinoma, generuoti udev įvykius, kai nėra disko konfigūracijos pakeitimų (pavyzdžiui, įrenginys nėra sumontuotas/atjungtas) nėra gera praktika. Tačiau galime padėti branduoliui neatlikti nereikalingų darbų ir užšaldyti užklausų eilę, jei tai nėra būtina. trys maža įsipareigoti ištaisyti situaciją.

Išvada

eBPF yra labai lankstus ir galingas įrankis. Straipsnyje pažvelgėme į vieną praktinį atvejį ir parodėme nedidelę dalį to, ką galima padaryti. Jei jus domina BCC paslaugų kūrimas, verta pasidomėti oficiali pamoka, kuriame gerai aprašomi pagrindai.

Yra ir kitų įdomių derinimo ir profiliavimo įrankių, pagrįstų eBPF. Vienas iš jų - bpftrace, kuri leidžia rašyti galingas vieno eilutes ir mažas programas awk panašia kalba. Kitas - ebpf_exporter, leidžia rinkti žemo lygio didelės raiškos metriką tiesiai į savo „Prometheus“ serverį ir vėliau gauti gražių vizualizacijų ir net įspėjimų.

Šaltinis: www.habr.com

Добавить комментарий