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

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 и (daugiau apie tai galite perskaityti ):

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

Š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 . 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 и straipsnis šia tema. Jei pažvelgsite į įvairių paslaugų kodą , 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 . 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 į 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 , 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 . 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 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 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 . Pabandykime paleisti jį bandomojoje platformoje, kurioje veikia fio, rašo į bcache ir iškviečiame udevadm monitorių:

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 , 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 - , kuri turi įdomų iššūkį :
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 и . 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 skaitiklis padidinamas q->mq_užšalimo_gylis. Po to branduolys laukia, kol eilė ištuštės .
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 , 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į , 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. 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 , kuriame gerai aprašomi pagrindai.
Yra ir kitų įdomių derinimo ir profiliavimo įrankių, pagrįstų eBPF. Vienas iš jų - , kuri leidžia rašyti galingas vieno eilutes ir mažas programas awk panašia kalba. Kitas - , 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
