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

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 ja (millega saab rohkem tutvuda ):

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

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

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 , 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 â , kus on huvitav kĂ”ne :
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 ja . 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 kalkulaatori vÀÀrtus suureneb q->mq_freeze_depth. PĂ€rast seda ootab tuum, kuni jĂ€rjekord on tĂŒhi .
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 , 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 , 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. 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 , mis kirjeldab hĂ€sti töö aluseid.
On ka teisi huvitavaid tööriistu, mis pĂ”hinevad eBPF-il, ĂŒks neist on , mis vĂ”imaldab kirjutada vĂ”imsaid ĂŒhelauseprogramme ja vĂ€ikeseid skripte awk-sarnases keeles. Teine on , 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
