„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
Jau yra daug taikomųjų programų, naudojančių eBPF, ir šiame straipsnyje apžvelgsime, kaip sukurti savo profiliavimo įrankį pagal biblioteką.
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
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
Š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
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
eBPF tekstas python scenarijaus viduje atrodo taip:
bpf_text = “”” # Here will be the bpf program code “””
Duomenims keistis tarp funkcijų naudoja eBPF programos
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
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
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
Š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
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
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
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
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 -
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
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
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į
~# /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š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
Yra ir kitų įdomių derinimo ir profiliavimo įrankių, pagrįstų eBPF. Vienas iš jų -
Šaltinis: www.habr.com