Nga Latenca e Lartë e Ceph te Patch-i i Kernel-it me ndihmën e eBPF/BCC.

Nga Latenca e Lartë e Ceph te Patch-i i Kernel-it me ndihmën e eBPF/BCC.

Në Linux ka një numër të madh mjetesh për debugsimin e kernel-it dhe aplikacioneve. Shumica e tyre ndikojnë negativisht në performancën e aplikacioneve dhe nuk mund të përdoren në prodhim.

Para disa vjetĂ«sh u zhvillua njĂ« tjetĂ«r mjet — eBPF. Ai ofron mundĂ«sinĂ« pĂ«r tĂ« vijuar gjurmĂ«t e bĂ«rthamĂ«s dhe aplikacioneve pĂ«rdoruesi me njĂ« overhead tĂ« ulĂ«t dhe pa nevojĂ«n pĂ«r tĂ« rikompiluar programet dhe pĂ«r tĂ« ngarkuar module tĂ« jashtme nĂ« bĂ«rthamĂ«. Tani ekzistojnĂ« shumĂ« utilitare praktike qĂ« pĂ«rdorin eBPF, dhe nĂ« kĂ«tĂ« artikull do tĂ« shqyrtojmĂ« se si tĂ« shkruajmĂ« utilitarin tonĂ« pĂ«r profilimin bazuar nĂ« bibliotekĂ«n

PythonBCC . Artikulli është i bazuar në ngjarje reale. Ne do të kalojmë nëpër procesin nga shfaqja e problemit deri te zgjidhja e tij, për të ilustruar se si mund të përdoren utilitarët ekzistues në situata konkrete.. Artikulli bazohet në ngjarje të vërteta. Ne do të kalojmë nga shfaqja e problemit deri te zgjidhja e tij, për të treguar se si mund të përdoren utilitarët ekzistues në situata specifike.

Ceph është ngadalë

Në klustërin Ceph u shtua një host i ri. Pas migrimit të një pjese të të dhënave te ai, vumë re se shpejtësia e përpunimit të kërkesave për shkarkim ishte shumë më e ulët se në serverat e tjerë.

Nga Latenca e Lartë e Ceph te Patch-i i Kernel-it me ndihmën e eBPF/BCC.
Ndryshe nga platformat e tjera, në këtë host u përdor bcache dhe një bërthamë e re linux 4.15. Ky host me këtë konfigurim ishte përdorur këtu për herë të parë. Dhe në atë moment ishte e qartë se shkaku i problemit mund të ishte teorikisht çdo gjë.

Kërkimi i Hostit

Le të fillojmë me atë që ndodh brenda procesit ceph-osd. Për këtë do të përdorim perf dhe flamescope (mund të lexoni më shumë rreth të cilave këtu):

Nga Latenca e Lartë e Ceph te Patch-i i Kernel-it me ndihmën e eBPF/BCC.
Imazhi na tregon se funksioni fdatasync() ka kaluar shumë kohë gjatë dërgimit të një kërkese në funksionin generic_make_request(). Kjo do të thotë se, shumë shpejt, problemi ndodhet diku jashtë vetë demonit osd. Kjo mund të jetë ose bërthama, ose diskët. Shfaqja e iostat tregonte vonesa të larta në përpunimin e kërkesave nga diskët bcache.

GjatĂ« kontrollit tĂ« hostit, ne zbuluam se demoni systemd-udevd konsumonte njĂ« sasi tĂ« madhe kohe CPU — rreth 20% nĂ« disa bĂ«rthama. Ky Ă«shtĂ« njĂ« sjellje e çuditshme, kĂ«shtu qĂ« duhet tĂ« kuptojmĂ« shkakun e saj. Duke qenĂ« se Systemd-udevd punon me ueventĂ«t, ne vendosĂ«m tĂ« shikojmĂ« pĂ«r to nĂ«pĂ«rmjet udevadm monitor. Doli se po gjeneroheshin shumĂ« ngjarje ndryshimi pĂ«r çdo pajisje blloku nĂ« sistem. Kjo Ă«shtĂ« mjaft e pazakontĂ«, prandaj do tĂ« jetĂ« e nevojshme tĂ« shikojmĂ« se çfarĂ« po gjeneron kĂ«to ngjarje.

Përdorimi i BCC Toolkit

Siç e konstatuam, bĂ«rthama (dhe demoni ceph nĂ« thirrjen e sistemit) kalon shumĂ« kohĂ« nĂ« generic_make_request(). Le tĂ« pĂ«rpiqemi tĂ« matim shpejtĂ«sinĂ« e punĂ«s sĂ« kĂ«tij funksioni. NĂ« BCC ka njĂ« utilitar tĂ« shkĂ«lqyer — funclatency. Ne do tĂ« vijojmĂ« demonin sipas PID tĂ« tij me njĂ« interval midis info-sh pĂ«r çdo 1 sekondĂ« dhe do tĂ« nxjerrim rezultatin nĂ« milisekonda.

Nga Latenca e Lartë e Ceph te Patch-i i Kernel-it me ndihmën e eBPF/BCC.
Zakonisht ky funksion punon shpejt. E gjithë ajo që bën është të dërgojë një kërkesë në radhën e drejtuesit të pajisjes.

Bcache — Ă«shtĂ« njĂ« pajisje e komplikuar qĂ« nĂ« tĂ« vĂ«rtetĂ« pĂ«rbĂ«het nga tre disqe:

  • disqet mbĂ«shtetĂ«se (disku qĂ« ndihmon), nĂ« kĂ«tĂ« rast Ă«shtĂ« njĂ« HDD i ngadalshĂ«m;
  • disku qĂ« ndihmon (disku i caching), kĂ«tu Ă«shtĂ« njĂ« ndarje NVMe e pajisjes;
  • pajisja virtuale bcache, me tĂ« cilĂ«n punon aplikacioni.

E dimë që dërgimi i kërkesës është ngadalësuar, por për cilin nga këto disqe? Do ta zbulojmë këtë më vonë.

Tani e dimë se ueventët, ndoshta, po shkaktojnë probleme. Të gjejmë se çfarë pikërisht shkakton gjenerimin e tyre, nuk është kaq e thjeshtë. Le të supozojmë se është ndonjë softuer që ekzekutohet periodikisht. Le të shohim se çfarë software po ekzekutohet në sistem, me ndihmën e skenarit execsnoop nga të njëjtën grup utilitarësh BCC. Le të eci dhe të drejtojmë daljen në një skedar.

Për shembull, ashtu:

/usr/share/bcc/tools/execsnoop  | tee ./execdump

Nuk do të japim këtu daljen e plotë të execsnoop, por një rresht që na intereson 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 i prindit) të procesit. Procesi me PID 5802 rezultoi të ishte një nga fijet e sistemit tonë të monitorimit. Gjatë kontrollit të konfigurimit të sistemit të monitorimit u gjetën parametra të caktuar gabimisht. Temperatura e adaptorit HBA u mat një herë në 30 sekonda, që ishte shumë më shpesh se nevojitej. Pas ndryshimit të intervalit të kontrollit në një më të gjatë, vërejti se vonesa në të përpunuarit e kërkesave në këtë host kishte ndaluar të dalë në pah në krahasim me hostet e tjerë.

Por akoma nuk është e qartë pse pajisja bcache ka qenë kaq e ngadaltë. Ne përgatitëm një platformë testuese me konfigurim identik dhe përpiqemi të riprodhojmë problemin duke ekzekutuar fio në bcache, duke ekzekutuar periodikisht udevadm trigger për të gjeneruar uevents.

Shkrimi i Veglave të BCC

Le të përpiqemi të shkruajmë një utilitar të thjeshtë për të gjurmë dhe shfaqur thirrjet më të ngadalta generic_make_request(). Ne gjithashtu na intereson emri i diskut për të cilin u thirr ky funksion.

Plani është i thjeshtë:

  • gitlab-runner. kprobe nĂ« generic_make_request():
    • RuajmĂ« nĂ« memorie emrin e diskut, i cili Ă«shtĂ« i disponueshĂ«m pĂ«rmes argumentit tĂ« funksionit;
    • RuajmĂ« shenjĂ«n e kohĂ«s.

  • gitlab-runner. kretprobe nĂ« kthim nga generic_make_request():
    • Marrim shenjĂ«n aktuale tĂ« kohĂ«s;
    • KĂ«rkojmĂ« shenjĂ«n e ruajtur tĂ« kohĂ«s dhe e krahasojmĂ« me tĂ« tanishmen;
    • NĂ«se rezultati Ă«shtĂ« mĂ« i madh se i caktuari, atĂ«herĂ« gjejmĂ« emrin e ruajtur tĂ« diskut dhe e shfaqim nĂ« terminal.

Kprobes dhe kretprobes përdorin mekanizmat e pikave të bllokimit për të modifikuar kodin e funksioneve në fluks. Mund të lexoni dokumentacioni dhe një artikul të mirë mbi këtë temë. Nëse shikoni kodin e disa utiliteteve në BCC, mund të vëreni se ato kanë një strukturë identike. Kështu që në këtë artikull do të kalojmë përpara duke lënë mënjanë analizimin e argumenteve të skriptit dhe do të kalojmë direkt në programin BPF.

Teksti eBPF brenda skriptit python duket si më poshtë:

bpf_text = """ # Këtu do të jetë kodi i programit bpf """

Për shkëmbimin e të dhënave midis funksioneve, programet e eBPF përdorin hash-tabelës. Kështu do të veprojmë edhe ne. Si çelës do të përdorim PID-në e procesit dhe si vlerë do të përcaktojmë strukturën:

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, e cila quhet p, me çelës të tipit u64 dhe vlerë të tipit struct data_t. Tabela do të jetë e aksesueshme në kontekstin e programit tonë BPF. Makrosi BPF_PERF_OUTPUT regjistron një tabelë tjetër, të quajtur events., e cila përdoret për shkëmbimin e të dhënave në hapësirën e përdoruesit.

Kur matim vonesat midis thirrjes së funksionit dhe kthimit prej tij, ose midis thirrjeve të funksioneve të ndryshme, duhet të kemi parasysh se të dhënat e marra duhet të përkasin të njëjtit kontekst. me fjalë të tjera, duhet të kujdesemi për mundësinë e ekzekutimit paralel të funksioneve. Ne kemi mundësinë të masim vonesën midis thirrjes së funksionit në kontekstin e një procesi dhe kthimit nga ky funksion në kontekstin e një procesi tjetër, por kjo, me siguri, do të ishte e padobishme. Një shembull i mirë këtu mund të jetë utilita biolatency, ku si çelësi i tabelës hash caktohet një tregues në struct request, që reflekton një kërkesë të vetme në disk.

Më pas na nevojitet të shkruajmë kodin, i cili do të ekzekutohet kur thirret funksioni i hulumtuar:

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 si argument të dytë do të vendoset argumenti i parë i thirrjes së funksionit generic_make_request(). Pas kësaj ne marrim PID-në e procesit, në kontekstin e të cilit punojmë, dhe vulën aktuale të kohës në nanosekonda. E gjithë kjo e shkruajmë në një struct data_t data. Emrin e diskut e marrim nga struktura bio, që kalojnë gjatë thirrjes generic_make_request(), dhe e ruajmë atë në të njëjtën strukturë data. Hapi përfundimtar është të shtojmë një shënim në tabelën hash, të cilën e përmendëm më parë.

Funksioni i ardhshëm do të thirret në kthimin 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 të kaluarin: ne mësojmë PID-në e procesit dhe vulën e kohës, por nuk alokojmë memorie për një strukturë të re data. Në vend të kësaj, ne kërkojmë në tabelën hash strukturën ekzistuese me çelës = PID-në aktuale. Nëse strukura gjendet, ne mësojmë emrin e procesit të ekzekutuar dhe e shtojmë atë brenda.

Lëvizja binare që ne po përdorim këtu është e nevojshme për të marrë GID-in e thread-it. Pra, PID-i i procesit kryesor që ka nisur thread-in, në kontekstin e të cilit ne po punojmë. Funksioni ynë i thirrur bpf_get_current_pid_tgid() kthen si GID të threadit, ashtu edhe PID-in e tij në një vlerë 64-bit.

Kur e drejtojmë në terminal aktualisht nuk na intereson threadi, por procesi kryesor. Pas krahasimit të vonesës së marrë me pragun e caktuar, ne e kalojmë strukturën tonë data në hapësirën e përdoruesit përmes tabelës events., pas së cilës ne e fshijmë shënimin nga p.

Në skriptin python, i cili do të ngarkohet ky kod, ne duhet të zëvendësojmë MIN_US dhe FACTOR me pragjet e vonesës dhe njësitë e kohës që ne do të 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"
elif:
	bpf_text = bpf_text.replace('FACTOR','')
	label = "usec"

Tani na nevojitet të përgatitim programin BPF përmes makros BPF dhe të regjistrojmë provat:

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")

Gjithashtu do të duhet të përcaktojmë struct data_t në skriptin tonë, përndryshe nuk do të jetë e mundur 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ë të printoni të dhënat 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()

Skripti është në dispozicion në GItHub. Le të përpiqemi ta ekzekutojmë në një platformë provuese, ku është aktivizuar fio që shkruan në bcache, dhe të therrasim udevadm monitor:

Nga Latenca e Lartë e Ceph te Patch-i i Kernel-it me ndihmën e eBPF/BCC.
Në fund! Tani e shohim se ajo që dukej si një ndalesë e bcache-it, në të vërtetë është një ndalesë e thirrjes generic_make_request() për diskun e ruajtur.

Hulumtoni thellë në Kernel

ÇfarĂ« saktĂ«sisht po ngadalĂ«son gjatĂ« kalimit tĂ« kĂ«rkesĂ«s? Ne shohim se vonesa ndodh edhe para fillimit tĂ« logjikĂ«s sĂ« kĂ«rkesĂ«s, dmth. regjistrimi i kĂ«rkesĂ«s specifike pĂ«r dalje statistike pĂ«r tĂ« (/proc/diskstats ose iostat) ende nuk ka filluar. Kjo mund tĂ« verifikohet lehtĂ«sisht duke ekzekutuar iostat gjatĂ« riprodhimit tĂ« problemit, ose BCC skripti biolatency, i cili bazohet nĂ« fillimin dhe pĂ«rfundimin e logjikĂ«s sĂ« kĂ«rkesave. AsnjĂ«ra nga kĂ«to mjete nuk do tĂ« tregojĂ« probleme pĂ«r kĂ«rkesat pĂ«r diskun e ruajtur.

Nëse shikojmë funksionin generic_make_request(), do të shohim se para fillimit të logjikës së kërkesës thirren edhe dy funksione të tjera. I pari është generic_make_request_checks(), i cili kryen kontrollet e legjitimitetit të kërkesës në lidhje me konfigurimet e diskut. I dyti është blk_queue_enter(), ku ka një thirrje interesante wait_event_interruptible():

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ë, kernel-i pret për të zbuluar qendrën. Le të matim vonesën blk_queue_enter():

~# /usr/share/bcc/tools/funclatency  blk_queue_enter -i 1 -m               	 
Duke gjurmuar 1 funksione për "blk_queue_enter"... Shtypni Ctrl-C për të përfunduar.

 	msecs           	: count 	distribucioni
     	0 -> 1      	: 341  	|****************************************|

 	msecs           	: count 	distribucioni
     	0 -> 1      	: 316  	|****************************************|

 	msecs           	: count 	distribucioni
     	0 -> 1      	: 255  	|****************************************|
     	2 -> 3      	: 0    	|                                    	|
     	4 -> 7      	: 0    	|                                    	|
     	8 -> 15     	: 1    	|                                    	|

Duket se jemi afër zgjidhjes. Funksionet e përdorura për "ngrirjen/shkrirjen" e qendrës janë blk_mq_freeze_queue dhe blk_mq_unfreeze_queue. Ato përdoren kur është e nevojshme të modifikoni konfigurimet e qendrës së kërkesave, potencialisht të rrezikshme për kërkesat që ndodhen në këtë radhë. Kur thirret blk_mq_freeze_queue() nga funksioni blk_freeze_queue_start() numri i q->mq_freeze_depth. Pasi, kernel-i pret deri në zbrazjen e qendrës në blk_mq_freeze_queue_wait().

Koha e pritjes për të zbrazur këtë qendër barazvlenë me vonesën e diskut, pasi kernel-i pret për përfundimin e të gjithë operacioneve të radhitura. Sapo qendra të zbrazet, aplikohen ndryshimet e konfigurimeve. Pas kësaj, thirret blk_mq_unfreeze_queue(), që dekrementon numrin freeze_depth.

Tani e dimĂ« mjaftueshĂ«m pĂ«r tĂ« rregulluar situatĂ«n. Komanda udevadm trigger rezulton nĂ« aplikimin e konfigurimeve pĂ«r pajisjen bllokuese. KĂ«to konfigurime pĂ«rshkruhen nĂ« rregullat e udev. Mund tĂ« gjejmĂ« se cilat konfigurime "ngrin" qendrĂ«n, duke provuar t’i ndryshojmĂ« ato pĂ«rmes sysfs ose duke parĂ« kodin burimor tĂ« kernel-it. Gjithashtu mund tĂ« provojmĂ« mjetin BCC trace, i cili do tĂ« nxjerrĂ« nĂ« terminal gjurmĂ«t e stack-Ă«s sĂ« kernel-it dhe hapĂ«sirĂ«s sĂ« pĂ«rdoruesit pĂ«r çdo thirrje blk_freeze_queue, pĂ«r shembull:

~# /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 ndodhin në mënyrë të kontrolluar. Kështu që shohim se madje edhe aplikimi i vlerave tashmë të caktuara shkakton një shpërthim të vonesës së kalimit të kërkesës nga aplikacioni në disk. Sigurisht, gjenerimi i ngjarjeve të udev kur nuk ka ndryshime në konfigurimin e disqeve (p.sh., pajisja nuk lidhet/çlirohet) nuk është një praktikë e mirë. Megjithatë, mund të ndihmojmë kernelin të mos bëjë punë të panevojshme dhe të mos "ngrihet" radhë kërkesash, nëse nuk ka një nevojë të tillë. Tre të vogla komita zgjidhin situatën.

Përfundimi

eBPF është një instrument shumë fleksibël dhe i fuqishëm. Në këtë artikull shqyrtuam një rast praktik dhe demonstruam një pjesë të vogël të asaj që është e mundur të bëhet. Nëse jeni të interesuar në zhvillimin e mjeteve BCC, ia vlen të shikoni tutorialin zyrtar, i cili përshkruan mirë bazat e funksionimit.

Ka edhe mjete të tjera interesante për debugging dhe profilizim të bazuara në eBPF. Një prej tyre është bpftrace, i cili lejon të shkruani njëlinjësh të fuqishëm dhe programe të vogla në një gjuhë të ngjashme me awk. Një tjetër është ebpf_exporter, lejonë mbledhjen e metrikave të nivelit të ulët me një saktësi të lartë direkt në serverin tuaj prometheus, me mundësinë për të marrë një vizualizim të bukur dhe madje alarme më vonë.

Burimi: habr.com

Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS đŸ”„ Bleni hostim tĂ« besueshĂ«m pĂ«r faqe me mbrojtje nga DDoS, serverĂ« VPS VDS | ProHoster