High Ceph Latency'le kerneliparanduse juurde eBPF/BCC abil

High Ceph Latency'le kerneliparanduse juurde eBPF/BCC abil

Linuxis on palju tööriistu kerneli ja rakenduste silumiseks. Enamik neist mĂ”jutab negatiivselt rakenduste jĂ”udlust ning ei saa tootmisreĆŸiimis kasutada.

MĂ”ni aasta tagasi loodi veel ĂŒks tööriist — eBPF. See vĂ”imaldab jĂ€lgida sĂŒdamikku ja kasutajarakendusi madala ĂŒlekattega ning ilma vajaduseta programme uuesti kompileerida ja laadida kolmandate osapoolte mooduleid sĂŒdamikku.

Praegu on juba mitmeid rakenduslikke tööriistu, mis kasutavad eBPF-d, ja selles artiklis uurime, kuidas kirjutada oma töötlemistööriista, mis pĂ”hineb teegil PythonBCC. Artikkel pĂ”hineb tĂ”elistel sĂŒndmustel. Me lĂ€bime tee probleemist lahenduseni, et nĂ€idata, kuidas olemasolevaid tööriistu saab konkreetses olukorras kasutada.

Ceph on aeglane

Lisati uus host Ceph klastrisse. PÀrast andmete osalist migratsiooni sellele avastasime, et kirjutamisvÔime kiirus on palju madalam kui teistel serveritel.

High Ceph Latency'le kerneliparanduse juurde eBPF/BCC abil
Erinevalt teistest platvormidest kasutati sellel hostil bcache'i ja uut linuxi 4.15 kernelit. Sellise konfiguratsiooniga hosti kasutatakse siin esmakordselt. Ja sel hetkel oli selge, et probleemide juur vÔib teoreetiliselt olla mis tahes.

Hosti uurimine

Alustame sellest, et vaatame, mis toimub ceph-osd protsessi sees. Selleks kasutame perf ja flamescope'i (rohkema teabe saamiseks lugege siin):

High Ceph Latency'le kerneliparanduse juurde eBPF/BCC abil
Pilt ĂŒtleb meile, et funktsioon fdatasync() veetis palju aega pĂ€ringu saatmisel funktsiooni generic_make_request(). See tĂ€hendab, et probleemide pĂ”hjus on tĂ”enĂ€oliselt kusagil vĂ€ljaspool ise osd deemonit. See vĂ”ib olla kas sĂŒda vĂ”i kettad. iostat-i vĂ€ljund nĂ€itas kĂ”rgeid viivitusi bcache kettadega seotud pĂ€ringute töötlemisel.

Hosti kontrollimise ajal avastasime, et systemd-udevd deemon kasutab palju CPU aega — umbes 20% mitmel tuumal. See kummaline kĂ€itumine vajab uurimist. Kuna Systemd-udevd töötab ueventidega, otsustasime neid vaadata lĂ€bi udevadm monitori. Selgus, et sĂŒsteemis genereeritakse iga plokiseadmest palju change-sĂŒndmusi. See on ĂŒsna ebatavaline, seega tuleb uurida, mis genereerib kĂ”ik need sĂŒndmused.

BCC tööriistakomplekti kasutamine

Nagu me juba tuvastasime, kulutab sĂŒda (ja ceph deemon sĂŒsteemi kutses) palju aega generic_make_request(). Proovime mÔÔta selle funktsiooni töötamise kiirus. BCC Seda juba olemasolevat suurepĂ€rast tööriista — funclatency. Me tracime demonit tema PID ja infosalongide vaheajaga 1 sekund ja kuvame tulemuse millisekundites.

High Ceph Latency'le kerneliparanduse juurde eBPF/BCC abil
Tavaliselt töötab see funktsioon kiiresti. KĂ”ik, mida ta teeb – edastab pĂ€ringu seadme draiveri jĂ€rjekorda.

Bcache on keeruline seade, mis tegelikult koosneb kolmest kettast:

  • toetav seade (vahemĂ€lu ketas), sel juhul aeglane HDD;
  • vahemĂ€lu seade (vahemĂ€lu ketas), siin on see ĂŒhe NVMe seadme partitsioon;
  • virtuaalne bcache seade, millega rakendus töötab.

Teame, et pÀringu edastamine on aeglane, kuid millise nende seadmete puhul? Uurime seda natuke hiljem.

Praegu teame, et uevent'id pĂ”hjustavad tĂ”enĂ€oliselt probleeme. Leida, mis tĂ€pselt nende genereerimist pĂ”hjustab, pole nii lihtne. Eeldame, et see on mingi tarkvara, mis kĂ€ivitatakse perioodiliselt. Vaatame, milline tarkvara sĂŒsteemis kĂ€ivitatakse, kasutades skripti execsnoop samast BCC utiliitide komplektist. KĂ€ivitame selle ja suuname vĂ€ljundi faili.

NĂ€iteks nii:

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

Ei hakka siin tooma tĂ€ishulgaga execsnoop'i vĂ€ljundit, kuid ĂŒks meid huvitav rida nĂ€gi vĂ€lja nii:

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

Kolmas veerg on PPID (vanema PID) protsessist. Protsess, millel on PID 5802, oli ĂŒks meie jĂ€lgimis sĂŒsteemi lĂ”ime. JĂ€lgimis sĂŒsteemi konfiguratsiooni kontrollimisel leiti valesti seadistatud parameetreid. HBA-adapteri temperatuur mÔÔdeti iga 30 sekundi jĂ€rel, mis oli palju oftĂŒkk aegade jooksul. PĂ€rast kontrollimise vahemaa pikendamist avastasime, et pĂ€ringute töötlemise viivitus ei olnud enam teiste hostide seas eristatav.

Kuid endiselt ei ole selge, miks bcache seade nii aeglaselt töötas. Valmistame ette testplatvormi identse konfiguratsiooniga ja proovime probleemi uuesti esile kutsuda, kÀivitades fio bcache'l, aeg-ajalt kÀivitades udevadm trigger'i, et genereerida uevents.

BCC-pÔhiste tööriistade kirjutamine

Proovime kirjutada lihtsa utiliidi jÀlgimiseks ja ekraanile kuvamiseks kÔige aeglasemaid kutseid generic_make_request(). Meid huvitab ka ketta nimi, mille jaoks see funktsioon kutsuti.

Plaani on lihtne:

  • Registreerime kprobe . Tundub, et generic_make_request():
    • Kasutame mĂ€lus ketta nime, mis on saadaval funktsiooni argumendina;
    • Salvesta ajatempos.

  • Registreerime kretprobe tagasi pöördumise korral generic_make_request():
    • Saame praeguse ajatempo;
    • Otsime salvestatud ajatempo ja vĂ”rreldame seda praegusega;
    • Kui tulemus on suurem kui mÀÀratud, siis leiame salvestatud ketta nime ja kuvame selle terminalis.

Kprobes ja kretprobes kasutavad funktsioonide koodi muutmiseks peatuspunkte. Saate lugeda dokumentatsioon ja hea artikkel selle teema kohta. Kui vaadata erinevate utiliitide koodi BCC, siis on nende struktuur ĂŒhesugune. Seega jĂ€tame selles artiklis skripti argumentide analĂŒĂŒsi vahele ja liikume edasi BPF-programmi juurde.

eBPF teksti python-skripti sees nÀeb vÀlja jÀrgmine:

bpf_text = """ # Siia tuleb bpf programmi kood """

Andmete vahetamiseks funktsioonide vahel kasutavad eBPF programmid hash-tabelina. Teeme ka nii. VÔtame vÔtmeks protsessi PID ja vÀÀrtuseks mÀÀrame struktuuri:

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);

Siin registreerime hash-tabelle, mida nimetatakse p, vĂ”tme tĂŒĂŒbiga u64 ja vÀÀrtuse tĂŒĂŒbiga struct data_t. Tabel on meie BPF-programmi kontekstis kergesti ligipÀÀsetav. Makros BPF_PERF_OUTPUT registreerib teise tabeli nimega events, mida kasutatakse andmete edastamiseks kasutajaruumi.

Kutsumisviisi ja tagastamise vaheliste viiviste mÔÔtmisel, kas erinevate funktsioonide vahel, tuleb arvestada, et saadud andmed peavad kuuluma samasse konteksti. TeisisĂ”nu, tuleb meeles pidada vĂ”imalike funktsioonide paralleelset tĂ€itmist. Meil on vĂ”imalus mÔÔta viivitust funktsiooni kutse ja tagasipöörde vahel, koos ĂŒhes protsessis, aga see on tĂ”enĂ€oliselt kasutu. Hea nĂ€itena vĂ”ib tuua utiliidi biolatency, kus hash-tabeli vĂ”tmena kasutatakse nĂ€idikut struct request, mis kajastab ĂŒhte kettakutsungit.

SeejÀrel peame kirjutama koodi, mis kÀivitatakse uuritava funktsiooni kutse tulemuseks:

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);
}

Siin on teise argumendina esimese argumendi kutse generic_make_request(). PÀrast seda saame protsessi PID, mille kontekstis töötame, ja praeguse ajatempli nanosekundites. Salvestame kÔik vÀrskelt eraldatud struct data_t data. Ketaste nime saame struktuurist bio, mis edastatakse kutse puhul generic_make_request(), ja salvestame selle samasse struktuuri data. Viimasena lisame kirje varem mainitud hash-tabelisse.

JĂ€rgmine funktsioon kutsub end tagasi 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);
    }
}

See funktsioon on sarnane eelnevale: me saame protsessi PId ja ajatempli, kuid ei reserveeri uut mÀlu data jaoks. Selle asemel otsime hash-tabelist juba olemasolevat struktuuri, mille vÔti on praegune PID. Kui struktuur leidub, teame kÀivitatud protsessi nime ja lisame selle sellele.

Siin kasutatav binaarset nihutust kasutatakse, et saada niisugune thread GID, st. pĂ”hiprotsessi PID, mis kĂ€ivitab niidi, mille kontekstis me töötame. Meie kutsutav funktsioon bpf_get_current_pid_tgid() tagastab nii thread GID kui ka selle PID ĂŒhes 64-bitises vÀÀrtuses.

Kuna me ei huvita voogu terminali vÀljundis, huvita meid pÔhiprotsess. Kui saadud viivitus vÔrreldakse seatud lÀvega, edastame oma struktuuri data kasutajaruumis tabeli kaudu events, pÀrast mida kustutame kirje p.

Python-skriptis, mis seda koodi laadib, peame asendama MIN_US ja FACTOR viivituse lÀve ja ajayksuse vÀÀrtustega, mille anname argumendina:

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"

NĂŒĂŒd peame valmistama ette BPF programmi kasutades BPF makrot ja registreerima katsepunktid:

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")

Peame ka mÀÀratlema struct data_t meie skriptis, vastasel juhul ei saa me midagi lugeda:

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)]

Viimane samm – andmete vĂ€ljastamine terminalis:

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)
# Ń„ĐŸŃ€ĐŒĐ°Ń‚Đ° ĐČыĐČĐŸĐŽĐ°
start = 0
while 1:
    try:
        b.perf_buffer_poll()
    except KeyboardInterrupt:
        exit()

Skripti on saadaval GItHub. Proovime seda testplatvormil kÀivitada, kus fio kirjutab bcache'i ja kutsume esile udevadm monitor:

High Ceph Latency'le kerneliparanduse juurde eBPF/BCC abil
LĂ”puks! NĂŒĂŒd nĂ€eme, et see, mis nĂ€is olevat bcache-seadmest tulenev viivitus, oli tegelikult viivitus ĐČŃ‹Đ·ĐŸĐČа generic_make_request() kaheks kassatava kettale.

Kaevume sĂŒvitsi Kernelisse

Mis tĂ€pselt pĂ”hjustab pĂ€ringu edastamisel viivituse? NĂ€eme, et viivitus tekib isegi enne, kui pĂ€ringu arvestamine algab, st konkreetse pĂ€ringu arvestamine edasiseks statistika esitamiseks (\/proc\/diskstats vĂ”i iostat) ei ole veel alanud. Seda on lihtne kontrollida, kĂ€ivitades iostat probleemide reprodutseerimise ajal, vĂ”i BCC skripti biolatency, mis pĂ”hineb pĂ€ringute arvestamise algusel ja lĂ”pule. Ükski neist utiliitidest ei nĂ€ita probleeme kassatava ketta pĂ€ringutega.

Kui vaatame funktsiooni generic_make_request(), siis nĂ€eme, et enne pĂ€ringu arvestamise algust kutsutakse ĂŒles veel kaks funktsiooni. Esimene — generic_make_request_checks(), kontrollib pĂ€ringu legitiimsust ketta seadetega. Teine — blk_queue_enter(), milles on huvitav kutse wait_event_interruptible():

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));

Sellega ootab kernel jÀrjekorra sulgemist. MÔÔdame viivituse blk_queue_enter():

~# \/usr\/share\/bcc\/tools\/funclatency  blk_queue_enter -i 1 -m               	 
JÀlgimine 1 funktsiooni jaoks "blk_queue_enter"... Vajutage Ctrl-C lÔpetamiseks.

 	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    	|                                    	|

Tundub, et oleme lahenduse lĂ€hedal. Funktsioonid, mida kasutatakse jĂ€rjekorra "kĂŒlmutamiseks\/sulgemiseks", on blk_mq_freeze_queue ja blk_mq_unfreeze_queue. Need on kasutusel, kui on vaja muuta pĂ€ringute jĂ€rjekorra seadeid, mis vĂ”ivad olla ohtlikud selle jĂ€rjekorra pĂ€ringutele. Kui kutsub blk_mq_freeze_queue() funktsioon blk_freeze_queue_start() suurendab arvestit q->mq_freeze_depth. PĂ€rast seda ootab tuum, kuni jĂ€rjekord tĂŒhjeneb blk_mq_freeze_queue_wait().

Ootamisaja pikkus, et see jĂ€rjekord tĂŒhjendataks, on vĂ”rreldav ketta latentsusega, kuna tuum ootab kĂ”igi jĂ€rjekorda seatud toimingute lĂ”puleviimist. Kui jĂ€rjekord on tĂŒhi, rakendatakse seade muutused. PĂ€rast seda kutsutakse ĂŒles blk_mq_unfreeze_queue(), vĂ€hendades arvestit freeze_depth.

NĂŒĂŒd me teame piisavalt, et olukorda parandada. KĂ€sk udevadm trigger toob kaasa seadete rakendamise plokiseadmest. Need seaded on kirjeldatud udev reeglites. Saame teada, millised seaded „kĂŒlmutavad“ jĂ€rjekorda, proovides neid muuta sysfs kaudu vĂ”i vaadates tuumakoodi. Samuti saame proovida BCC utiliiti trace, mis vĂ€ljastab terminalile tuuma ja kasutajaruumi steki jĂ€ljed iga blk_freeze_queue, nĂ€iteks:

~# /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 reeglid muutuvad ĂŒsna harva ja tavaliselt toimub see kontrollitud viisil. Nii et me nĂ€eme, et isegi juba seadistatud vÀÀrtuste rakendamine pĂ”hjustab rakenduse ja ketta vahelise pĂ€ringu edastamise viivituse tĂ”usu. Loomulikult ei ole udev sĂŒndmuste genereerimine, kui ketta konfiguratsioonis ei toimu mingeid muutusi (nĂ€iteks seade ei tĂ”mmata vĂ€lja ega ĂŒhendata), eriti hea praktika. Siiski saame aidata tuumale mitte teha mĂ”ttetut tööd ja mitte „kĂŒlmutada“ pĂ€ringute jĂ€rjekorda, kui see pole vajalik. Kolm vĂ€ikesed LLVM projekti arendajad on vĂ€ljendanud soovi jĂ€rgida teiste projektide nĂ€idet parandavad olukorda.

KokkuvÔte

eBPF on vĂ€ga paindlik ja vĂ”imas tööriist. Artiklis vaatasime ĂŒhte praktilist juhtumit ja demonstreerisime vaid vĂ€ikest osa sellest, mida on vĂ”imalik teha. Kui olete huvitatud BCC utiliitide arendamisest, tasub vaadata ametlikku Ă”petust, mis selgitab hĂ€sti töö aluseid.

On veel muid huvitavaid eBPF-pĂ”hiseid silumis- ja profileerimistööriistu. Üks neist on bpftrace, mis vĂ”imaldab kirjutada vĂ”imsaid ĂŒhesirgelisi skripte ja vĂ€ikeseid programme awk-taolises keeles. Teine on ebpf_exporter, mis vĂ”imaldab koguda madala taseme kĂ”rge eraldusvĂ”imega mÔÔdikuid otse teie prometheus serverisse, vĂ”imaldades hiljem saada ilusat visualiseerimist ja isegi hĂ€ireid.

Allikas: habr.com

Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne hostimine veebilehtede jaoks DDoS-i kaitsega, VPS VDS serverid | ProHoster