Ceph Latentzia Handitik Kernel Patchera eBPF/BCC erabiliz

Ceph Latentzia Handitik Kernel Patchera eBPF/BCC erabiliz

Linuxek nukleoa eta aplikazioak arazketarako tresna ugari ditu. Gehienek eragin negatiboa dute aplikazioaren errendimenduan eta ezin dira produkzioan erabili.

Duela urte pare bat zegoen beste tresna bat garatu da - eBPF. Kernelaren eta erabiltzaileen aplikazioen trazatzea ahalbidetzen du gainkostu txikiarekin eta programak berreraiki eta hirugarrenen moduluak nukleoan kargatu beharrik gabe.

Dagoeneko badaude eBPF erabiltzen duten aplikazio-utilitate asko, eta artikulu honetan liburutegian oinarrituta zure profil-erabilgarritasuna nola idatzi aztertuko dugu. PythonBCC. Artikulua benetako gertakarietan oinarritzen da. Arazo batetik konpontzera joango gara lehendik dauden utilitateak egoera zehatzetan nola erabil daitezkeen erakusteko.

Ceph Motela Da

Ostalari berri bat gehitu da Ceph clusterra. Datu batzuk horretara migratu ondoren, ohartu gara haren idazketa-eskaerak prozesatzeko abiadura beste zerbitzarietan baino askoz txikiagoa zela.

Ceph Latentzia Handitik Kernel Patchera eBPF/BCC erabiliz
Beste plataforma batzuek ez bezala, ostalari honek bcache eta linux 4.15 kernel berria erabiltzen zituen. Konfigurazio honen ostalari bat hemen erabiltzen zen lehen aldia izan zen. Eta momentu horretan argi zegoen arazoaren erroa teorikoki edozer izan zitekeela.

Ostalaria ikertzen

Has gaitezen zer gertatzen den ceph-osd prozesuaren barruan. Horretarako erabiliko dugu perfektua ΠΈ flameskopioa (Hori buruz gehiago irakur dezakezu Hemen):

Ceph Latentzia Handitik Kernel Patchera eBPF/BCC erabiliz
Irudiak funtzioa esaten digu fdatasync() denbora asko eman zuen eskaera bat funtzioetara bidaltzen generic_make_request(). Horrek esan nahi du ziurrenik arazoen kausa osd deabrutik kanpo dagoela nonbait. Hau nukleoa edo diskoak izan daitezke. Iostat irteerak latentzia handia erakutsi zuen bcache diskoen eskaerak prozesatzeko.

Ostalaria egiaztatzean, systemd-udevd deabruak CPU denbora asko kontsumitzen duela ikusi dugu -% 20 inguru hainbat nukleotan. Jokaera arraroa da, beraz, zergatik jakin behar duzu. Systemd-udevd-ek uevents-ekin lan egiten duenez, haiek aztertzea erabaki dugu udevadm monitorea. Bihurtzen da sistemako bloke-gailu bakoitzeko aldaketa-gertaera ugari sortu zirela. Hau nahiko ezohikoa da, beraz, gertaera horiek guztiak zerk sortzen dituen aztertu beharko dugu.

BCC Toolkit erabiliz

Dagoeneko jakin dugunez, nukleoak (eta sistema-deian dagoen ceph daemonak) denbora asko ematen du. generic_make_request(). Saia gaitezen funtzio honen abiadura neurtzen. IN BCC Dagoeneko erabilgarritasun zoragarri bat dago - funtzionalitate. Daemonaren PIDaren arabera trazatuko dugu irteeren artean 1 segundoko tartearekin eta emaitza milisegundotan aterako dugu.

Ceph Latentzia Handitik Kernel Patchera eBPF/BCC erabiliz
Ezaugarri honek normalean azkar funtzionatzen du. Egiten duen bakarra eskaera gailuaren kontrolatzaile ilarara pasatzea da.

Bcache gailu konplexu bat da, benetan hiru diskoz osatuta:

  • babeskopia gailua (cachean gordetako diskoa), kasu honetan HDD motela da;
  • caching gailua (caching diskoa), hemen hau NVMe gailuaren partizio bat da;
  • aplikazioa exekutatzen duen bcache gailu birtuala.

Badakigu eskaeraren transmisioa motela dela, baina gailu hauetako zeinentzat? Geroxeago jorratuko dugu honi.

Orain badakigu gertakariek arazoak sor ditzaketela. Haien belaunaldia zerk eragiten duen aurkitzea ez da hain erraza. Demagun hau aldizka abiarazten den software mota bat dela. Ikus dezagun zer nolako softwarea exekutatzen den sisteman script bat erabiliz execsnoop beretik BCC erabilgarritasun-kit. Exekutatu dezagun eta bidal dezagun irteera fitxategi batera.

Adibidez, honela:

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

Ez dugu hemen execsnoop-en irteera osoa erakutsiko, baina guretzako interes-lerro bat honelakoa zen:

sh 1764905 5802 0 sudo arcconf getconfig 1 AD | grep Temperature | awk -F '[:/]' '{print $2}' | sed 's/^ ([0-9]*) C.*/1/'

Hirugarren zutabea prozesuaren PPID (guraso PID) da. PID 5802-rekin egindako prozesua gure monitorizazio sistemaren harietako bat izan zen. Monitorizazio sistemaren konfigurazioa egiaztatzean, parametro okerrak aurkitu dira. HBA egokigailuaren tenperatura 30 segundoz behin hartu zen, hau da, beharrezkoa baino askoz maizago. Egiaztatzeko tartea luzeago batera aldatu ondoren, ikusi dugu ostalari honetan eskaerak prozesatzeko latentzia ez zela nabarmentzen beste ostalariekin alderatuta.

Baina oraindik ez dago argi zergatik bcache gailua hain motela izan zen. Konfigurazio berdineko proba-plataforma bat prestatu genuen eta arazoa erreproduzitzen saiatu ginen bcache-n fio exekutatuz, aldian-aldian udevadm trigger exekutatuz uevents sortzeko.

BCCn oinarritutako tresnak idaztea

Saia gaitezen dei motelenak trazatzeko eta bistaratzeko utilitate sinple bat idazten generic_make_request(). Funtzio hori deitu zitzaion unitatearen izena ere interesatzen zaigu.

Plana erraza da:

  • Izena eman kzunda on generic_make_request():
    • Diskoaren izena memorian gordetzen dugu, funtzioaren argumentuaren bidez eskuragarria;
    • Denbora-zigilua gordetzen dugu.

  • Izena eman kretsonda itzultzeko generic_make_request():
    • Uneko denbora-zigilua lortzen dugu;
    • Gordetako denbora-zigilua bilatzen dugu eta unekoarekin alderatzen dugu;
    • Emaitza zehaztutakoa baino handiagoa bada, gordetako diskoaren izena aurkituko dugu eta terminalean bistaratuko dugu.

Kprobes ΠΈ kretzundak erabili eten-puntuaren mekanismo bat funtzio-kodea aldatzeko. Irakur dezakezu dokumentazioa ΠΈ ona gai honi buruzko artikulua. Hainbat utilitateren kodea ikusten baduzu BCC, orduan ikus dezakezu egitura berdina dutela. Beraz, artikulu honetan script argumentuak analizatzea saltatuko dugu eta BPF programara joango gara.

Python script-en barruan dagoen eBPF testua honelakoa da:

bpf_text = β€œβ€β€ # Here will be the bpf program code β€œβ€β€

Funtzioen artean datuak trukatzeko, eBPF programek erabiltzen dute hash taulak. Gauza bera egingo dugu. Prozesuaren PID erabiliko dugu gako gisa, eta egitura balio gisa definituko dugu:

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

Hemen izeneko hash taula bat erregistratzen dugu p, giltza-motarekin u64 eta motaren balio bat struct data_t. Taula gure BPF programaren testuinguruan egongo da eskuragarri. BPF_PERF_OUTPUT makroak izeneko beste taula bat erregistratzen du gertaerak, horretarako erabiltzen dena datuen transmisioa erabiltzaileen espaziora.

Funtzio bati deitzearen eta bertatik itzultzearen arteko atzerapenak neurtzean, edo funtzio ezberdinetarako deien artean, kontuan izan behar duzu jasotako datuak testuinguru berekoak izan behar direla. Beste era batera esanda, funtzioen abiarazte paralelo posibleari buruz gogoratu behar duzu. Funtzio bat prozesu baten testuinguruan deitzearen eta beste prozesu baten testuinguruan funtzio horretatik itzultzearen arteko latentzia neurtzeko gaitasuna dugu, baina hori alferrikakoa izango da ziurrenik. Hemen adibide ona litzateke biolatentzia erabilgarritasuna, non hash taularen gakoa erakusle gisa ezarrita dagoen egitura eskaera, disko eskaera bat islatzen duena.

Ondoren, aztergai dugun funtzioa deitzen denean exekutatzen den kodea idatzi behar dugu:

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);
}

Hemen deitutako funtzioaren lehen argumentua bigarren argumentu gisa ordezkatuko da generic_make_request(). Horren ostean, lan egiten ari garen testuinguruko prozesuaren PID-a eta uneko denbora-zigilua nanosegundotan jasoko ditugu. Aukeratutako berri batean idazten dugu dena struct data_t data. Diskoaren izena egituratik lortzen dugu bio, deitzean pasatzen dena generic_make_request(), eta gorde egitura berean datuak. Azken urratsa lehen aipatu zen hash taulan sarrera bat gehitzea da.

Hurrengo funtzioari dei egingo zaio bueltan 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);
    }
}

Funtzio hau aurrekoaren antzekoa da: prozesuaren PID eta denbora-zigilua aurkitzen ditugu, baina ez dugu memoriarik esleitzen datu-egitura berrirako. Horren ordez, hash taulan lehendik dagoen egitura bat bilatzen dugu == uneko PID gakoa erabiliz. Egitura aurkitzen bada, orduan martxan dagoen prozesuaren izena aurkituko dugu eta horri gehituko diogu.

Hemen erabiltzen dugun aldaketa bitarra GID haria lortzeko beharrezkoa da. horiek. Lantzen ari garen testuinguruan haria hasi duen prozesu nagusiaren PID. Deitzen dugun funtzioa bpf_get_current_pid_tgid() hariaren GID eta bere PID 64 biteko balio bakarrean itzultzen ditu.

Terminalera irtetean, momentuz ez zaigu haria interesatzen, baina prozesu nagusia interesatzen zaigu. Sortutako atzerapena atalase jakin batekin alderatu ondoren, gure egitura pasatzen dugu datuak erabiltzailearen espaziora taula bidez gertaerak, ondoren, sarrera ezabatuko dugu p.

Kode hau kargatuko duen python script-ean, MIN_US eta FACTOR ordeztu behar ditugu atzerapen atalaseak eta denbora-unitateekin, argumentuetatik pasatuko ditugunak:

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"

Orain BPF programaren bidez prestatu behar dugu BPF makroa eta erregistratu laginak:

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

Guk ere zehaztu beharko dugu struct data_t gure gidoian, bestela ezingo dugu ezer irakurri:

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

Azken urratsa datuak terminalera ateratzea da:

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

Gidoia bera hemen dago eskuragarri GItHub. Saia gaitezen fio exekutatzen ari den proba-plataforma batean exekutatzen, bcache-n idazten eta udevadm monitor deitzen:

Ceph Latentzia Handitik Kernel Patchera eBPF/BCC erabiliz
Azkenean! Orain ikusten dugu gelditzen den bcache gailu bat zirudiena benetan gelditzen den dei bat dela generic_make_request() cachean gordetako disko baterako.

Zulatu Kernelean

Zer da zehazki moteltzen eskaeraren transmisioan? Atzerapena eskaeraren kontabilitatea hasi baino lehen ere gertatzen dela ikusten dugu, hau da. Estatistikak gehiago ateratzeko eskaera zehatz baten kontabilitatea (/proc/diskstats edo iostat) oraindik ez da hasi. Hau erraz egiazta daiteke iostat exekutatuz arazoa erreproduzitzen duzun bitartean, edo BCC gidoiaren biolatentzia, eskaeraren kontabilitatearen hasiera eta amaieran oinarritzen dena. Utilitate hauetako batek ere ez du arazorik agertuko cachean gordetako diskoan egindako eskaerak egiteko.

Funtzioari erreparatzen badiogu generic_make_request(), orduan ikusiko dugu eskaera kontabilitateari ekin aurretik, beste bi funtzio deitzen direla. Lehenengoa - generic_make_request_checks(), diskoaren ezarpenei buruzko eskaeraren zilegitasuna egiaztatzen du. Bigarrena - blk_queue_enter(), erronka interesgarria duena 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));

Bertan, nukleoa ilara desizoztu arte itxarongo du. Neurtu dezagun atzerapena 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    	|                                    	|

Konponbidetik gertu gaudela dirudi. Ilara bat izozteko/desizozteko erabiltzen diren funtzioak hauek dira blk_mq_freeze_queue ΠΈ blk_mq_unfreeze_queue. Eskaeren ilararen ezarpenak aldatzea beharrezkoa denean erabiltzen dira, ilara honetako eskaeretarako arriskutsuak izan daitezkeenak. Deitzean blk_mq_freeze_queue() funtzioa blk_freeze_queue_start() kontagailua handitzen da q->mq_izozte_sakonera. Horren ondoren, nukleoa ilara hustu arte itxarongo du blk_mq_freeze_queue_wait().

Ilara hau garbitzeko behar den denbora diskoaren latentziaren baliokidea da nukleoak ilaran dauden eragiketa guztiak amaitu arte itxaroten baitu. Ilara hutsik dagoenean, ezarpenen aldaketak aplikatzen dira. Horren ostean deitzen da blk_mq_unfreeze_queue(), kontagailua gutxituz izoztu_sakonera.

Orain nahikoa dakigu egoera zuzentzeko. udevadm trigger komandoak bloke-gailuaren ezarpenak aplikatzea eragiten du. Ezarpen hauek udev arauetan deskribatzen dira. Ilara izozten duten ezarpenak aurki ditzakegu sysfs bidez aldatzen saiatuz edo nukleoaren iturburu-kodea begiratuz. BCC utilitatea ere proba dezakegu traza, zeinak kernel eta erabiltzaile-espazio pilaren arrastoak aterako ditu terminalera dei bakoitzeko blk_freeze_queue, adibidez:

~# /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 arauak oso gutxitan aldatzen dira eta normalean hori modu kontrolatuan gertatzen da. Beraz, ikusten dugu lehendik ezarritako balioak aplikatuta ere eskaera aplikaziotik diskora transferitzeko atzerapenaren gorakada bat eragiten duela. Jakina, diskoaren konfigurazioan aldaketarik ez dagoenean udev gertaerak sortzea (adibidez, gailua ez dago muntatuta/deskonektatuta) ez da praktika ona. Hala ere, nukleoari lagundu diezaiokegu alferrikako lana ez egiten eta eskaera-ilara izozten, beharrezkoa ez bada. hiru txikia konprometitu egoera zuzendu.

Ondorioa

eBPF tresna oso malgua eta indartsua da. Artikuluan kasu praktiko bat aztertu dugu eta egin daitekeenaren zati txiki bat erakutsi dugu. BCC utilitateak garatzea interesatzen bazaizu, merezi du begirada bat ematea tutoretza ofiziala, oinarriak ondo deskribatzen dituena.

Badira eBPFn oinarritutako arazketa eta profilak egiteko beste tresna interesgarri batzuk. Haietako bat - bpftrace, awk antzeko lengoaian indartsuak eta programa txikiak idazteko aukera ematen duena. Beste bat - ebpf_exporter, maila baxuko eta bereizmen handiko neurketak zuzenean zure prometheus zerbitzarian biltzeko aukera ematen dizu, gero bistaratze ederrak eta alertak ere jasotzeko aukerarekin.

Iturria: www.habr.com

Gehitu iruzkin berria