Linux ka një numër të madh mjetesh për korrigjimin e kernelit dhe aplikacioneve. Shumica e tyre kanë një ndikim negativ në performancën e aplikacionit dhe nuk mund të përdoren në prodhim.
Para nja dy vitesh kishte
Tashmë ka shumë shërbime aplikacioni që përdorin eBPF, dhe në këtë artikull do të shikojmë se si të shkruani programin tuaj të profilizimit bazuar në bibliotekë
Ceph është i ngadalshëm
Një host i ri është shtuar në grupin Ceph. Pas migrimit të disa të dhënave në të, vumë re se shpejtësia e përpunimit të kërkesave për shkrim nga ai ishte shumë më e ulët se në serverët e tjerë.
Ndryshe nga platformat e tjera, ky host përdori bcache dhe kernelin e ri linux 4.15. Kjo ishte hera e parë që një host i këtij konfigurimi u përdor këtu. Dhe në atë moment ishte e qartë se rrënja e problemit teorikisht mund të ishte çdo gjë.
Hetimi i hostit
Le të fillojmë duke parë se çfarë ndodh brenda procesit ceph-osd. Për këtë do të përdorim
Fotografia na tregon se funksioni fdatasync () shpenzoi shumë kohë duke dërguar një kërkesë për funksionet generic_make_request(). Kjo do të thotë që ka shumë të ngjarë që shkaku i problemeve të jetë diku jashtë vetë demonit osd. Ky mund të jetë ose kerneli ose disqet. Dalja e iostatit tregoi një vonesë të lartë në përpunimin e kërkesave nga disqet bcache.
Kur kontrolluam hostin, zbuluam se daemon systemd-udevd konsumon një sasi të madhe të kohës së CPU - rreth 20% në disa bërthama. Kjo është sjellje e çuditshme, kështu që ju duhet të zbuloni pse. Meqenëse Systemd-udevd punon me uevents, vendosëm t'i shikojmë ato monitor udevadm. Rezulton se një numër i madh ngjarjesh ndryshimi u krijuan për çdo pajisje bllok në sistem. Kjo është mjaft e pazakontë, kështu që ne do të duhet të shohim se çfarë gjeneron të gjitha këto ngjarje.
Duke përdorur paketën e veglave BCC
Siç e kemi zbuluar tashmë, kerneli (dhe daemon ceph në thirrjen e sistemit) shpenzon shumë kohë në generic_make_request(). Le të përpiqemi të matim shpejtësinë e këtij funksioni. NË
Kjo veçori zakonisht funksionon shpejt. Gjithçka që bën është ta kalojë kërkesën në radhën e drejtuesit të pajisjes.
Bcache është një pajisje komplekse që në fakt përbëhet nga tre disqe:
- pajisje mbështetëse (disk i memorizuar), në këtë rast është një HDD i ngadaltë;
- pajisja e memories (disku i memories), këtu kjo është një ndarje e pajisjes NVMe;
- pajisja virtuale bcache me të cilën funksionon aplikacioni.
Ne e dimë që transmetimi i kërkesës është i ngadaltë, por për cilën nga këto pajisje? Do të merremi me këtë pak më vonë.
Tani e dimë se ngjarjet ka të ngjarë të shkaktojnë probleme. Të gjesh se çfarë saktësisht shkakton gjenerimin e tyre nuk është aq e lehtë. Le të supozojmë se ky është një lloj softueri që lëshohet në mënyrë periodike. Le të shohim se çfarë lloj softueri funksionon në sistem duke përdorur një skript execsnoop nga e njëjta
Për shembull si kjo:
/usr/share/bcc/tools/execsnoop | tee ./execdump
Ne nuk do të tregojmë prodhimin e plotë të execsnoop këtu, por një linjë me interes për ne dukej kështu:
sh 1764905 5802 0 sudo arcconf getconfig 1 AD | grep Temperature | awk -F '[:/]' '{print $2}' | sed 's/^ ([0-9]*) C.*/1/'
Kolona e tretë është PPID (PID mëmë) i procesit. Procesi me PID 5802 doli të ishte një nga temat e sistemit tonë të monitorimit. Gjatë kontrollimit të konfigurimit të sistemit të monitorimit, u gjetën parametra të gabuar. Temperatura e përshtatësit HBA matej çdo 30 sekonda, gjë që është shumë më shpesh sesa duhet. Pas ndryshimit të intervalit të kontrollit në një më të gjatë, zbuluam se vonesa e përpunimit të kërkesës në këtë host nuk binte më në sy në krahasim me hostet e tjerë.
Por është ende e paqartë pse pajisja bcache ishte kaq e ngadaltë. Ne përgatitëm një platformë testimi me një konfigurim identik dhe u përpoqëm të riprodhonim problemin duke ekzekutuar fio në bcache, duke ekzekutuar në mënyrë periodike këmbëzën udevadm për të gjeneruar uevents.
Shkrimi i mjeteve të bazuara në BCC
Le të përpiqemi të shkruajmë një mjet të thjeshtë për të gjurmuar dhe shfaqur thirrjet më të ngadalta generic_make_request(). Ne jemi gjithashtu të interesuar për emrin e diskut për të cilin u thirr ky funksion.
Plani është i thjeshtë:
- Regjistrohu ksondë mbi generic_make_request():
- Ne ruajmë emrin e diskut në memorie, i arritshëm përmes argumentit të funksionit;
- Ne ruajmë vulën kohore.
- Regjistrohu kretsondë për kthim nga generic_make_request():
- Ne marrim vulën kohore aktuale;
- Ne kërkojmë vulën kohore të ruajtur dhe e krahasojmë atë me atë aktuale;
- Nëse rezultati është më i madh se ai i specifikuar, atëherë gjejmë emrin e diskut të ruajtur dhe e shfaqim atë në terminal.
Ksonda и kretsonda përdorni një mekanizëm të pikës së ndërprerjes për të ndryshuar kodin e funksionit në fluturim. Ju mund të lexoni
Teksti eBPF brenda skriptit python duket si ky:
bpf_text = “”” # Here will be the bpf program code “””
Për të shkëmbyer të dhëna ndërmjet funksioneve, programet eBPF përdorin
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);
Këtu regjistrojmë një tabelë hash të quajtur p, me tip çelësi u64 dhe një vlerë të llojit struct data_t. Tabela do të jetë e disponueshme në kontekstin e programit tonë BPF. Makroja BPF_PERF_OUTPUT regjistron një tabelë tjetër të quajtur Ngjarjet, e cila përdoret për
Kur matni vonesat midis thirrjes së një funksioni dhe kthimit prej tij, ose midis thirrjeve në funksione të ndryshme, duhet të keni parasysh që të dhënat e marra duhet t'i përkasin të njëjtit kontekst. Me fjalë të tjera, duhet të mbani mend për nisjen e mundshme paralele të funksioneve. Ne kemi aftësinë për të matur vonesën midis thirrjes së një funksioni në kontekstin e një procesi dhe kthimit nga ai funksion në kontekstin e një procesi tjetër, por kjo ka të ngjarë të jetë e padobishme. Një shembull i mirë këtu do të ishte
Më pas, duhet të shkruajmë kodin që do të ekzekutohet kur të thirret funksioni në studim:
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);
}
Këtu argumenti i parë i funksionit të thirrur do të zëvendësohet si argumenti i dytë
Funksioni i mëposhtëm do të thirret pas kthimit nga 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);
}
}
Ky funksion është i ngjashëm me atë të mëparshëm: ne zbulojmë PID-in e procesit dhe vulën kohore, por nuk ndajmë memorie për strukturën e re të të dhënave. Në vend të kësaj, ne kërkojmë në tabelën hash për një strukturë tashmë ekzistuese duke përdorur çelësin == PID aktual. Nëse gjendet struktura, atëherë zbulojmë emrin e procesit të ekzekutimit dhe e shtojmë atë në të.
Zhvendosja binare që përdorim këtu është e nevojshme për të marrë GID-in e fillit. ato. PID-i i procesit kryesor që nisi thread-in në kontekstin e të cilit po punojmë. Funksioni që ne quajmë
Kur nxjerrim në terminal, aktualisht nuk jemi të interesuar për thread-in, por jemi të interesuar për procesin kryesor. Pasi të krahasojmë vonesën që rezulton me një prag të caktuar, ne e kalojmë strukturën tonë të dhëna në hapësirën e përdoruesit nëpërmjet tabelës Ngjarjet, pas së cilës fshijmë hyrjen nga p.
Në skriptin python që do të ngarkojë këtë kod, duhet të zëvendësojmë MIN_US dhe FACTOR me pragjet e vonesës dhe njësitë kohore, të cilat do t'i kalojmë përmes argumenteve:
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"
Tani duhet të përgatisim programin BPF nëpërmjet
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")
Ne gjithashtu do të duhet të përcaktojmë struct data_t në skenarin tonë, përndryshe nuk do të mund të lexojmë asgjë:
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)]
Hapi i fundit është nxjerrja e të dhënave në 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()
Vetë skenari është i disponueshëm në
Më në fund! Tani shohim se ajo që dukej si një pajisje bcache e bllokuar është në të vërtetë një telefonatë e bllokuar generic_make_request() për një disk të memorizuar.
Gërmoni në Kernel
Çfarë saktësisht po ngadalësohet gjatë transmetimit të kërkesës? Shohim që vonesa ndodh edhe para fillimit të kontabilitetit të kërkesës, d.m.th. kontabilizimi i një kërkese specifike për dalje të mëtejshme të statistikave mbi të (/proc/diskstats ose iostat) nuk ka filluar ende. Kjo mund të verifikohet lehtësisht duke ekzekutuar iostat gjatë riprodhimit të problemit, ose
Nëse shikojmë funksionin generic_make_request(), atëherë do të shohim që para se kërkesa të fillojë kontabilizimin thirren edhe dy funksione të tjera. Së pari - generic_make_request_checks(), kryen kontrolle mbi ligjshmërinë e kërkesës në lidhje me cilësimet e diskut. E dyta -
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));
Në të, kerneli pret që radha të shkrihet. Le të matim vonesën 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 | |
Duket se jemi afër zgjidhjes. Funksionet e përdorura për të ngrirë / shkrirë një radhë janë
Koha që duhet për të pastruar këtë radhë është e barabartë me vonesën e diskut pasi kerneli pret që të gjitha operacionet në radhë të përfundojnë. Pasi radha të jetë bosh, ndryshimet e cilësimeve zbatohen. Pas së cilës quhet
Tani dimë mjaftueshëm për të korrigjuar situatën. Komanda e nxitjes udevadm bën që të aplikohen cilësimet për pajisjen e bllokut. Këto cilësime përshkruhen në rregullat e udev. Mund të gjejmë se cilat cilësime po ngrijnë radhën duke u përpjekur t'i ndryshojmë ato përmes sysfs ose duke parë kodin burimor të kernelit. Mund të provojmë gjithashtu programin BCC
~# /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]
Rregullat e Udev ndryshojnë mjaft rrallë dhe zakonisht kjo ndodh në mënyrë të kontrolluar. Pra, ne shohim që edhe aplikimi i vlerave tashmë të vendosura shkakton një rritje të vonesës në transferimin e kërkesës nga aplikacioni në disk. Sigurisht, krijimi i ngjarjeve udev kur nuk ka ndryshime në konfigurimin e diskut (për shembull, pajisja nuk është montuar/shkëputur) nuk është një praktikë e mirë. Megjithatë, ne mund ta ndihmojmë kernelin të mos bëjë punë të panevojshme dhe të ngrijë radhën e kërkesave nëse nuk është e nevojshme.
Përfundim
eBPF është një mjet shumë fleksibël dhe i fuqishëm. Në artikull ne shikuam një rast praktik dhe demonstruam një pjesë të vogël të asaj që mund të bëhet. Nëse jeni të interesuar në zhvillimin e shërbimeve të BCC, ia vlen t'i hidhni një sy
Ka mjete të tjera interesante për korrigjimin dhe profilizimin e bazuar në eBPF. Një prej tyre -
Burimi: www.habr.com