Kuidas kÔrget Ceph latentsust leevendada kernelipatch'i abil eBPF/BCC-ga.

Kuidas kÔrget Ceph latentsust leevendada kernelipatch'i abil eBPF/BCC-ga.

Linuxis on palju tööriistu tuuma ja rakenduste silumiseks. Enamik neist mÔjutab rakenduste jÔudlust negatiivselt ja neid ei saa kasutada tootmises.

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

Praeguseks on juba mitmeid rakenduslikke utiliite, mis kasutavad eBPF-d, ja selles artiklis vaatame, kuidas kirjutada oma profilleerimise utiliit, kasutades teeki PythonBCC. Artikkel pĂ”hineb tĂ”eliselt juhtunud sĂŒndmustel. Me lĂ€bime teekonna probleemide ilmumisest kuni nende lahendamiseni, et nĂ€idata, kuidas olemasolevaid utiliite konkreetses olukorras kasutada.

Ceph on aeglane

Klusterisse Ceph lisati uus host. PÀrast osa andmete selle peale migreerimist mÀrkasime, et kirjutamiselt pÀringute töötlemise kiirus on neil oluliselt madalam kui teistel serveritel.

Kuidas kÔrget Ceph latentsust leevendada kernelipatch'i abil eBPF/BCC-ga.
Erinevalt teistest platvormidest kasutati selle hosti puhul bcache'i ja uut linux 4.15 kerneli. Sellist konfiguratsiooni kasutati siin esmakordselt. Ja tol ajal oli selge, et probleemi juurteks vÔis teoreetiliselt olla mis iganes.

Uurime hosti

Alustame uurimist, vaadates, mis toimub ceph-osd protsessi sees. Selleks kasutame perf ja flamescope (millega saab rohkem tutvuda siit):

Kuidas kÔrget Ceph latentsust leevendada kernelipatch'i abil eBPF/BCC-ga.
Pilt nÀitab meile, et funktsioon fdatasync() kulutas palju aega pÀringu saatmisele funktsioonis generic_make_request(). See tÀhendab, et tÔenÀoliselt on probleemide pÔhjus kuskil vÀljaspool osd teenust. See vÔib olla kas kernel vÔi kettad. Iostat'i vÀljund nÀitas bcache-kettaste kÔrgeid pÀringute töötlemise viivitusi.

Hosti kontrollimisel avastasime, et systemd-udevd demon tarbib suurt hulka CPU aega — umbes 20% mitmel tuumal. See on kummaline kĂ€itumine, seega on vaja vĂ€lja selgitada selle pĂ”hjus. Kuna Systemd-udevd tegeleb uevent'idega, otsustasime vaadata neid lĂ€bi udevadm monitor. Selgub, et sĂŒsteemis genereeriti iga plokiseadmise jaoks suur hulk muudatus-sĂŒndmusi. See on ĂŒsna ebatavaline, seega tuleb vaadata, mis kĂ”ik need sĂŒndmused genereerib.

BCC tööriistakomplekti kasutamine

Nagu me juba selgitasime, veedab kernel (ja demon ceph sĂŒsteemitĂ”kses) palju aega generic_make_request(). Proovime selle funktsiooni töökiirus Ă€ra mÔÔta. Selles BCC on juba suurepĂ€rane utiliit — funclatency. Me jĂ€lgime deemonit selle PID jĂ€rgi, tehes teabe vĂ€ljundite vahel 1-sekundilise intervalli ning vĂ€ljastame tulemuse millisekundites.

Kuidas kÔrget Ceph latentsust leevendada kernelipatch'i abil eBPF/BCC-ga.
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:

  • backing device (sisemine ketas), antud juhul aeglane HDD;
  • caching device (vahemĂ€lu ketas), siin on see ĂŒks NVMe seadme partitsioon;
  • virtuaalne seade bcache, millega rakendus töötab.

Teame, et pÀringu edastamine on aeglane, aga millise nende seadmete puhul? Selle selgitame vÀlja hiljem.

Praegu teame, et uevent'id vĂ”ivad pĂ”hjustada probleeme. Leida, mis tĂ€pselt nende genereerimist pĂ”hjustab, pole lihtne. Eeldame, et see on mingi perioodiliselt kĂ€ivitatav tarkvara. Vaadakem, mis tarkvara töötab sĂŒsteemis skripti abil execsnoop samast BCC utiliitide komplektist. KĂ€ivitame selle ja suuname vĂ€ljundi faili.

NĂ€iteks niimoodi:

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

Ei too siinkohal vĂ€lja execsnoop'i tĂ€ielikku 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 protsessi PPID (vanem PID). Protsess, millel on PID 5802, osutus ĂŒheks meie sĂŒsteemi jĂ€lgimisvoogudest. JĂ€lgimisseadme konfiguratsiooni kontrollimisel leiti vale seadistusega parameetrid. HBA-adapteri temperatuuri loeti iga 30 sekundi tagant, mis on palju sagedamini, kui vajalik. PĂ€rast kontrollintervalli pikendamist avastasime, et pĂ€ringute töötlemise viivitus sellel hostil ei eristu enam teiste hostide seas.

Kuid siiani ei ole selge, miks bcache-seade nii aeglaselt töötas. Valmistame ette testplatvormi identse konfiguratsiooniga ja proovime probleemi kopeerida, kĂ€ivitades fio bcache'il, perioodiliselt kĂ€ivitades udevadm trigger'i ĂŒrituste genereerimiseks.

BCC-pÔhiste tööriistade kirjutamine

Proovime kirjutada lihtsat utiliiti, et jÀlgida ja ekraanile vÀlja printida kÔige aeglasemaid vÀljakutseid generic_make_request(). Meid huvitab ka ketta nimi, mille jaoks see funktsioon kutsuti.

Plaani eesmÀrk:

  • Registreerime kprobe jĂ€rgnevaga generic_make_request():
    • Salvestame mĂ€llu ketta nime, mis on saadaval funktsiooni argumendi kaudu;
    • Salvestame ajatempli.

  • Registreerime kretprobe tagasipöördumine generic_make_request():
    • Saame praeguse ajatempli;
    • Otsime salvestatud ajatempli ja vĂ”rdleme praegusega;
    • Kui tulemus on suurem mÀÀratud, leiame salvestatud ketta nime ja vĂ€ljastame terminalile.

Kprobid ja kretprobid kasutavad peatumispunktide mehhanismi funktsioonide koodi reaalajas muutmiseks. VÔite lugeda dokumentatsiooni ja head artiklit selle teema kohta. Kui vaadata erinevate utiliitide koodi BCC, siis vÔib mÀrgata, et neil on identne struktuur. Seega jÀtame kÀesolevas artiklis skripti argumentide parsimise vahele ja liigume otse BPF programmi juurde.

eBPF tekst Python-skripti sees nÀeb vÀlja jÀrgmiselt:

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

Andmete vahetamiseks funktsioonide vahel kasutavad eBPF programmid hÔlbustus. Nii teeme meiegi. VÔtame vÔtmena 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 hĂŒvitaabeli, mille nimi on p, vĂ”tme tĂŒĂŒbiga u64 ja vÀÀrtuse tĂŒĂŒbiga struct data_t. Tabel on kergesti kasutatav meie BPF-programmi kontekstis. Makro BPF_PERF_OUTPUT registreerib teise tabeli, mida nimetatakse events, mida kasutatakse andmete edastamiseks kasutajaruumi.

Kuidas mÔÔta viivitusi funktsiooni kutsumise ja selle tagasiviimise vahel vĂ”i erinevate funktsioonide kutsumiste vahel, tuleb arvesse vĂ”tta, et saadud andmed peavad kuuluma samasse konteksti. TeisisĂ”nu, tuleb meeles pidada vĂ”imalikke funktsioonide ĂŒheaegseid kĂ€ivitusi. Meil on vĂ”imalus mÔÔta viivitust funktsiooni kutsumise vahel ĂŒhe protsessi kontekstis ja selle funktsiooni tagasiviimist teise protsessi kontekstis, kuid see on tĂ”enĂ€oliselt kasutu. Hea nĂ€ide siinkohal on biolatency utiliit, kus vĂ”tmena kasutatakse hash-tabeli puhul viidet struct request, mis kajastab ĂŒhe kettasaidi pĂ€ringut.

Edasi peame kirjutama koodi, mis kÀivitatakse uuritava funktsiooni kutsumise korral:

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 pannakse teise argumendina paika kutsutud funktsiooni esimene argument 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. KÔvakettanimi saadakse struktuurist bio, mis edastatakse kutse ajal generic_make_request(), ja salvestatakse samasse struktuuri data. Viimase sammuna lisame eelnevalt mainitud hash-tabelisse kirje.

JÀrgmine funktsioon kutsutakse vÀlja tagastamisel 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 sarnaneb eelnevale: saame protsessi PID ja ajamÀrgi, kuid ei eralda mÀlu uue data struktuuri jaoks. Selle asemel otsime hash-tabelist juba olemasolevat struktuuri vÔtme jÀrgi, mis vastab praegusele PID-le. Kui struktuur leiti, siis saame teada kÀimasoleva protsessi nime ja lisame selle sinna.

Siin kasutatav binaarne nihutamine on vajalik, et saada thread GID, st peamise protsessi PID, mis kĂ€ivitas niidi, milles me töötame. Meie kutse bpf_get_current_pid_tgid() tagastab nii niidi GID kui ka tema PID ĂŒhes 64-bitises vÀÀrtuses.

VÀljunditerminali puhul ei huvita meid praegu voog, vaid peamine protsess. PÀrast saadud viivituse vÔrdlemist mÀÀratud lÀvega edastame meie struktuuri data kasutaja ruumi kaudu tabelisse events, seejÀrel eemaldame kirje p.

Python skriptis, mis laadib antud koodi, peame asendama MIN_US ja FACTOR viivituse lÀvenditega ja ajainetega, mille me edastame argumentide kaudu:

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 ette valmistama BPF programmi lĂ€bi BPF makro ja registreerima proovide:

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

Samuti peame 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Ă€ljund terminali:

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

Kood on saadaval GItHubis. Proovime kÀivitada selle testplatvormil, kus fio kirjutab bcache'ile ja kutsume esile udevadm monitor:

Kuidas kÔrget Ceph latentsust leevendada kernelipatch'i abil eBPF/BCC-ga.
LĂ”finally! NĂŒĂŒd nĂ€eme, et see, mis nĂ€is peatavat bcache-seadet, on tegelikult peatumine generic_make_request() vahemĂ€lustatud ketta jaoks.

Kaevake sĂŒdamikku

Mis tĂ€pselt peatab pĂ€ringu esitamist? NĂ€eme, et viivitus tekib isegi enne, kui pĂ€ringu arvestamine algab, st konkreetse pĂ€ringu arvestamine edasise statistika kuvamiseks (/proc/diskstats vĂ”i iostat) ei ole veel alanud. Seda on lihtne kontrollida, kĂ€ivitades iostat probleemide kordamisel, vĂ”i BCC skript biolatency, mis pĂ”hineb pĂ€ringute arvestamise alguses ja lĂ”pus. Ükski neist utiliidist ei nĂ€ita vahemĂ€lustatud kettale tehtud pĂ€ringute probleeme.

Kui vaatame funktsiooni generic_make_request(), siis nĂ€eme, et enne pĂ€ringu konto loomist kutsutakse vĂ€lja veel kaks funktsiooni. Esimene — generic_make_request_checks(), teeb pĂ€ringu legitiimsuse kontrolle seoses ketta seadistustega. Teine — blk_queue_enter(), kus on huvitav kĂ”ne 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));

Selles tuum ootab jÀrjekorra sulgemist. MÔÔdame viivituse blk_queue_enter():

~# /usr/share/bcc/tools/funclatency blk_queue_enter -i 1 -m               	 
Funktsioonide jÀlgimine "blk_queue_enter"... Vajutage Ctrl-C, et lÔpetada.

 	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 lĂ€hedal lahendusele. Funktsioonid, mida kasutatakse jĂ€rjekorra "kĂŒlmutamiseks/sulatamiseks" — need on blk_mq_freeze_queue ja blk_mq_unfreeze_queue. Neid kasutatakse, kui on vajalik muuta jĂ€rjekorra seadistusi, mis vĂ”ivad olla potentsiaalselt ohtlikud sellele jĂ€rjekorras olevatele pĂ€ringutele. Kui kutsutakse blk_mq_freeze_queue() funktsiooni blk_freeze_queue_start() kalkulaatori vÀÀrtus suureneb q->mq_freeze_depth. PĂ€rast seda ootab tuum, kuni jĂ€rjekord on tĂŒhi blk_mq_freeze_queue_wait().

Selle jĂ€rjekorra tĂŒhjendamise ooteaeg on vĂ”rdne kettaseadmest tingitud viivitusega, kuna tuum ootab, kuni kĂ”ik jĂ€rjekorda pandud toimingud on lĂ”petatud. Kui jĂ€rjekord on tĂŒhi, rakendatakse seade muudatused. SeejĂ€rel kutsutakse vĂ€lja blk_mq_unfreeze_queue(), vĂ€hendades freeze_depth.

NĂŒĂŒd teame piisavalt, et olukorda parandada. KĂ€sk udevadm trigger viib lĂ”puks seadete rakendamiseni plokkseadmest. Need seaded on kirjeldatud udev reeglites. Saame vĂ€lja selgitada, millised seaded „kĂŒlmutavad” jĂ€rjekorra, proovides neid muuta lĂ€bi sysfs vĂ”i vaadates tuuma allika koodi. Samuti saame proovida BCC trace, mis kuvab terminalis tuuma ja kasutaja ruumi kuhjade jĂ€lgimise iga kĂ”ne jaoks 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 harva ja tavaliselt toimub see kontrollitud viisil. Nii nĂ€eme, et isegi mÀÀratud vÀÀrtuste rakendamine pĂ”hjustab hilinemise suurenemist rakenduse ja ketta vaheliste pĂ€ringute edastamisel. Loomulikult ei ole hea praktika genereerida udev-sĂŒndmusi, kui ketaste konfiguratsioonis pole mingeid muutusi (nĂ€iteks seade ei ĂŒhenda ega lahku). Siiski saame aidata tuumal mitte teha jĂ”hkrat tööd ega "kĂŒlmutada" pĂ€ringute jĂ€rjekorda, kui selleks pole mingit vajadust. Kolm vĂ€ikesed commit lahendab olukorra.

KokkuvÔte

eBPF on vĂ€ga paindlik ja vĂ”imas tööriist. Artiklis vaatasime ĂŒhte praktilist juhtumit ja nĂ€itasime vĂ€ikeses osas, mida on vĂ”imalik teha. Kui teid huvitab BCC-utility arendamine, siis tasub tutvuda ametliku Ă”petuse, mis kirjeldab hĂ€sti töö aluseid.

On ka teisi huvitavaid tööriistu, mis pĂ”hinevad eBPF-il, ĂŒks neist on bpftrace, mis vĂ”imaldab kirjutada vĂ”imsaid ĂŒhelauseprogramme ja vĂ€ikeseid skripte awk-sarnases keeles. Teine on ebpf_exporter, mis vĂ”imaldab koguda madala taseme kĂ”rge eraldusvĂ”imega metrikat otse teie prometheus serverisse, vĂ”imaldades hiljem saada kaunist visualiseerimist ja isegi hoiatusi.

Allikas: habr.com

Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid đŸ”„ Osta usaldusvÀÀrne veebihosting DDoS kaitsega, VPS VDS serverid | ProHoster