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

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 ja (rohkema teabe saamiseks lugege ):

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. Seda juba olemasolevat suurepĂ€rast tööriista â funclatency. Me tracime demonit tema PID ja infosalongide vaheajaga 1 sekund ja kuvame tulemuse millisekundites.

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 . 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 ja artikkel selle teema kohta. Kui vaadata erinevate utiliitide koodi , 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 . 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 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 , 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 . 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 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 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 . Proovime seda testplatvormil kÀivitada, kus fio kirjutab bcache'i ja kutsume esile udevadm monitor:

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 , 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 â , milles on huvitav kutse :
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 ja . 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 suurendab arvestit q->mq_freeze_depth. PĂ€rast seda ootab tuum, kuni jĂ€rjekord tĂŒhjeneb .
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 , 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 , 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. 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 , mis selgitab hĂ€sti töö aluseid.
On veel muid huvitavaid eBPF-pĂ”hiseid silumis- ja profileerimistööriistu. Ăks neist on , mis vĂ”imaldab kirjutada vĂ”imsaid ĂŒhesirgelisi skripte ja vĂ€ikeseid programme awk-taolises keeles. Teine on , 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
