No High Ceph latentuma līdz kodola ielāpim, izmantojot eBPF/BCC

No High Ceph latentuma līdz kodola ielāpim, izmantojot eBPF/BCC

Linux ir liels skaits rÄ«ku kodola un lietojumprogrammu atkļūdoÅ”anai. Lielākajai daļai no tiem ir negatÄ«va ietekme uz lietojumprogrammu veiktspēju, un tos nevar izmantot ražoÅ”anā.

Pirms pāris gadiem bija ir izstrādāts cits rÄ«ks - eBPF. Tas ļauj izsekot kodolam un lietotāja lietojumprogrammām ar zemām izmaksām un bez nepiecieÅ”amÄ«bas pārbÅ«vēt programmas un ielādēt kodolā treÅ”o puÅ”u moduļus.

Jau ir daudz lietojumprogrammu utilÄ«tu, kas izmanto eBPF, un Å”ajā rakstā mēs apskatÄ«sim, kā izveidot savu profilÄ“Å”anas utilÄ«tu, pamatojoties uz bibliotēku. PythonBCC. Raksts ir balstÄ«ts uz patiesiem notikumiem. Mēs pāriesim no problēmas uz labojumu, lai parādÄ«tu, kā esoŔās utilÄ«tas var izmantot konkrētās situācijās.

Kefs ir lēns

Ceph klasterim ir pievienots jauns saimniekdators. Pēc dažu datu migrÄ“Å”anas uz to mēs pamanÄ«jām, ka rakstÄ«Å”anas pieprasÄ«jumu apstrādes ātrums ar to bija daudz mazāks nekā citos serveros.

No High Ceph latentuma līdz kodola ielāpim, izmantojot eBPF/BCC
AtŔķirÄ«bā no citām platformām Å”is resursdators izmantoja bcache un jauno Linux 4.15 kodolu. Å Ä« bija pirmā reize, kad Å”eit tika izmantots Ŕīs konfigurācijas resursdators. Un tajā brÄ«dÄ« bija skaidrs, ka problēmas sakne teorētiski varētu bÅ«t jebkas.

Saimnieka izmeklēŔana

Sāksim ar to, kas notiek ceph-osd procesā. Å im nolÅ«kam mēs izmantosim ideāls Šø flaskops (vairāk par to varat lasÄ«t Å”eit):

No High Ceph latentuma līdz kodola ielāpim, izmantojot eBPF/BCC
Attēls parāda, ka funkcija fdatasync() pavadÄ«ja daudz laika, nosÅ«tot pieprasÄ«jumu funkcijām generic_make_request(). Tas nozÄ«mē, ka, visticamāk, problēmu cēlonis ir kaut kur ārpus paÅ”a osd dēmona. Tas var bÅ«t kodols vai diski. Iostata izvade uzrādÄ«ja lielu latentumu, apstrādājot pieprasÄ«jumus ar bcache diskiem.

Pārbaudot resursdatoru, mēs atklājām, ka systemd-udevd dēmons patērē daudz CPU laika - aptuveni 20% no vairākiem kodoliem. Tā ir dÄ«vaina uzvedÄ«ba, tāpēc jums ir jānoskaidro, kāpēc. Tā kā Systemd-udevd darbojas ar notikumiem, mēs nolēmām tos apskatÄ«t udevadm monitors. Izrādās, ka katrai sistēmas blokierÄ«cei tika Ä£enerēts liels skaits izmaiņu notikumu. Tas ir diezgan neparasti, tāpēc mums bÅ«s jāskatās, kas rada visus Å”os notikumus.

Izmantojot BCC rīku komplektu

Kā mēs jau esam noskaidrojuÅ”i, kodols (un ceph dēmons sistēmas izsaukumā) pavada daudz laika generic_make_request(). Mēģināsim izmērÄ«t Ŕīs funkcijas ātrumu. IN BCC Jau ir brÄ«niŔķīga lietderÄ«ba - funkcionalitāte. Mēs izsekosim dēmonu pēc tā PID ar 1 sekundes intervālu starp izvadēm un izvadÄ«sim rezultātu milisekundēs.

No High Ceph latentuma līdz kodola ielāpim, izmantojot eBPF/BCC
Šī funkcija parasti darbojas ātri. Viss, ko tas dara, ir nosūtīt pieprasījumu ierīces draivera rindai.

Bcache ir sarežģīta ierīce, kas faktiski sastāv no trim diskiem:

  • atbalsta ierÄ«ce (keÅ”atmiņā saglabāts disks), Å”ajā gadÄ«jumā tas ir lēns HDD;
  • keÅ”atmiņas ierÄ«ce (keÅ”atmiņas disks), Å”eit Å”is ir viens NVMe ierÄ«ces nodalÄ«jums;
  • bcache virtuālā ierÄ«ce, ar kuru darbojas lietojumprogramma.

Mēs zinām, ka pieprasÄ«jumu pārraide ir lēna, bet kurai no Ŕīm ierÄ«cēm? Mēs ar to nodarbosimies nedaudz vēlāk.

Tagad mēs zinām, ka notikumi var radÄ«t problēmas. Atrast, kas tieÅ”i izraisa viņu paaudzi, nav tik vienkārÅ”i. Pieņemsim, ka Ŕī ir sava veida programmatÅ«ra, kas tiek palaista periodiski. ApskatÄ«sim, kāda veida programmatÅ«ra darbojas sistēmā, izmantojot skriptu execsnoop no tā paÅ”a BCC palÄ«gkomplekts. PalaidÄ«sim to un nosÅ«tÄ«sim izvadi uz failu.

Piemēram, Ŕādi:

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

Å eit mēs neparādÄ«sim pilnu execsnoop izvadi, taču viena mÅ«s interesējoŔā rinda izskatÄ«jās Ŕādi:

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

TreŔā kolonna ir procesa PPID (parent PID). Process ar PID 5802 izrādÄ«jās viens no mÅ«su uzraudzÄ«bas sistēmas pavedieniem. Pārbaudot monitoringa sistēmas konfigurāciju, tika konstatēti kļūdaini parametri. HBA adaptera temperatÅ«ra tika mērÄ«ta ik pēc 30 sekundēm, kas ir daudz biežāk nekā nepiecieÅ”ams. Pēc pārbaudes intervāla nomaiņas uz garāku, mēs atklājām, ka pieprasÄ«juma apstrādes latentums Å”ajā resursdatorā vairs neizceļas salÄ«dzinājumā ar citiem saimniekdatoriem.

Bet joprojām nav skaidrs, kāpēc bcache ierīce bija tik lēna. Mēs sagatavojām testa platformu ar identisku konfigurāciju un mēģinājām reproducēt problēmu, palaižot fio uz bcache, periodiski palaižot udevadm trigeri, lai ģenerētu uevents.

Uz BCC balstītu rīku rakstīŔana

Mēģināsim uzrakstÄ«t vienkārÅ”u utilÄ«tu, lai izsekotu un parādÄ«tu lēnākos zvanus generic_make_request(). MÅ«s interesē arÄ« tā diska nosaukums, kuram Ŕī funkcija tika izsaukta.

Plāns ir vienkārŔs:

  • ReÄ£istrēties kprobe par generic_make_request():
    • Mēs saglabājam diska nosaukumu atmiņā, kas pieejams, izmantojot funkcijas argumentu;
    • Mēs saglabājam laika zÄ«mogu.

  • ReÄ£istrēties kretprobe par atgrieÅ”anos no generic_make_request():
    • Mēs iegÅ«stam paÅ”reizējo laika zÄ«mogu;
    • Mēs meklējam saglabāto laikspiedolu un salÄ«dzinām to ar paÅ”reizējo;
    • Ja rezultāts ir lielāks par norādÄ«to, mēs atrodam saglabātā diska nosaukumu un parādām to terminālÄ«.

Kprobes Šø kretzondes izmantojiet pārtraukuma punkta mehānismu, lai lidojuma laikā mainÄ«tu funkcijas kodu. JÅ«s varat lasÄ«t dokumentācija Šø labs rakstu par Å”o tēmu. Ja paskatās uz dažādu utilÄ«tu kodu BCC, tad var redzēt, ka tiem ir identiska struktÅ«ra. Tāpēc Å”ajā rakstā mēs izlaidÄ«sim skripta argumentu parsÄ“Å”anu un pāriesim pie paÅ”as BPF programmas.

eBPF teksts python skriptā izskatās Ŕādi:

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

Lai apmainītos ar datiem starp funkcijām, eBPF programmas izmanto hash tabulas. Mēs darīsim tāpat. Mēs izmantosim procesa PID kā atslēgu un definēsim struktūru kā vērtību:

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

Šeit mēs reģistrējam hash tabulu ar nosaukumu p, ar atslēgas veidu u64 un tipa vērtību struct data_t. Tabula būs pieejama mūsu BPF programmas kontekstā. Makro BPF_PERF_OUTPUT reģistrē citu tabulu ar nosaukumu notikumi, kas tiek izmantots datu pārraide lietotāja telpā.

Mērot aizkavi starp funkcijas izsaukÅ”anu un atgrieÅ”anos no tās vai starp dažādu funkciju izsaukumiem, jāņem vērā, ka saņemtajiem datiem ir jāpieder vienam un tam paÅ”am kontekstam. Citiem vārdiem sakot, jums jāatceras par iespējamu funkciju paralēlu palaiÅ”anu. Mums ir iespēja izmērÄ«t latentumu starp funkcijas izsaukÅ”anu viena procesa kontekstā un atgrieÅ”anos no Ŕīs funkcijas cita procesa kontekstā, taču tas, visticamāk, ir bezjēdzÄ«gi. Å eit bÅ«tu labs piemērs biolatences lietderÄ«ba, kur hash tabulas atslēga ir iestatÄ«ta kā rādÄ«tājs uz struktÅ«ras pieprasÄ«jums, kas atspoguļo vienu diska pieprasÄ«jumu.

Tālāk mums jāraksta kods, kas darbosies, kad tiks izsaukta pētāmā 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);
}

Å eit pirmais izsauktās funkcijas arguments tiks aizstāts kā otrais arguments generic_make_request(). Pēc tam mēs iegÅ«stam procesa PID, kura kontekstā mēs strādājam, un paÅ”reizējo laikspiedolu nanosekundēs. Mēs to visu pierakstām tikko izvēlētā veidā struct data_t dati. Mēs iegÅ«stam diska nosaukumu no struktÅ«ras bio, kas tiek nodota zvanot generic_make_request()un saglabājiet to tādā paŔā struktÅ«rā dati. Pēdējais solis ir pievienot ierakstu jaucēj tabulai, kas tika minēts iepriekÅ”.

Atgriežoties no, tiks izsaukta Ŕāda funkcija 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);
    }
}

Å Ä« funkcija ir lÄ«dzÄ«ga iepriekŔējai: mēs noskaidrojam procesa PID un laikspiedolu, bet nepieŔķiram atmiņu jaunajai datu struktÅ«rai. Tā vietā mēs meklējam jaucēj tabulā jau esoÅ”u struktÅ«ru, izmantojot atslēgu == paÅ”reizējais PID. Ja struktÅ«ra ir atrasta, tad noskaidrojam palaiÅ”anas procesa nosaukumu un pievienojam tam.

Binārā maiņa, ko mēs Å”eit izmantojam, ir nepiecieÅ”ama, lai iegÅ«tu pavedienu GID. tie. Galvenā procesa PID, kas aizsāka pavedienu, kura kontekstā mēs strādājam. Funkcija, ko mēs izsaucam bpf_get_current_pid_tgid() atgriež gan pavediena GID, gan tā PID vienā 64 bitu vērtÄ«bā.

Izvadot uz termināli, mÅ«s Å”obrÄ«d neinteresē pavediens, bet gan galvenais process. SalÄ«dzinot iegÅ«to aizkavi ar doto slieksni, mēs izturam savu struktÅ«ru dati lietotāja telpā, izmantojot tabulu notikumi, pēc kura mēs izdzÄ“Å”am ierakstu no p.

Python skriptā, kas ielādēs Å”o kodu, mums jāaizstāj MIN_US un FACTOR ar aizkaves sliekŔņiem un laika vienÄ«bām, kuras mēs nodosim, izmantojot 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"

Tagad mums ir jāsagatavo BPF programma, izmantojot BPF makro un reģistrēt paraugus:

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

Mums arī būs jānosaka struct data_t mūsu skriptā, pretējā gadījumā mēs nevarēsim neko izlasīt:

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

Pēdējais solis ir datu izvadÄ«Å”ana terminālÄ«:

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 skripts ir pieejams vietnē GItHub. Mēģināsim palaist to testa platformā, kurā darbojas fio, rakstot uz bcache un izsaucam udevadm monitoru:

No High Ceph latentuma līdz kodola ielāpim, izmantojot eBPF/BCC
Beidzot! Tagad mēs redzam, ka tas, kas izskatÄ«jās kā apstājusies bcache ierÄ«ce, patiesÄ«bā ir apstājies zvans generic_make_request() keÅ”atmiņā saglabātam diskam.

Iedziļināties kodolā

Kas tieÅ”i palēninās pieprasÄ«juma pārsÅ«tÄ«Å”anas laikā? Redzam, ka kavÄ“Å”anās notiek pat pirms pieprasÄ«juma uzskaites sākuma, t.i. vēl nav sākusies konkrēta pieprasÄ«juma uzskaite tālākai statistikas izvadÄ«Å”anai par to (/proc/diskstats vai iostat). To var viegli pārbaudÄ«t, palaižot iostatu, vienlaikus atkārtojot problēmu, vai BCC skripta biolatence, kuras pamatā ir pieprasÄ«juma uzskaites sākums un beigas. Neviena no Ŕīm utilÄ«tprogrammām neuzrādÄ«s problēmas saistÄ«bā ar pieprasÄ«jumiem keÅ”atmiņā saglabātajam diskam.

Ja skatāmies uz funkciju generic_make_request(), tad redzēsim, ka pirms pieprasÄ«juma uzskaites sākÅ”anas tiek izsauktas vēl divas funkcijas. Pirmkārt - generic_make_request_checks(), veic diska iestatÄ«jumu pieprasÄ«juma likumÄ«bas pārbaudes. Otrais - blk_queue_enter(), kam ir interesants izaicinājums gaidÄ«t_notikums_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));

Tajā kodols gaida, lÄ«dz rinda tiek atsaldēta. IzmērÄ«sim kavÄ“Å”anos 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    	|                                    	|

Å Ä·iet, ka esam tuvu risinājumam. Rindas iesaldÄ“Å”anai/atsaldÄ“Å”anai izmantotās funkcijas ir blk_mq_freeze_queue Šø blk_mq_unfreeze_queue. Tos izmanto, ja nepiecieÅ”ams mainÄ«t pieprasÄ«jumu rindas iestatÄ«jumus, kas ir potenciāli bÄ«stami pieprasÄ«jumiem Å”ajā rindā. Zvanot blk_mq_freeze_queue() funkcija blk_freeze_queue_start() skaitÄ«tājs tiek palielināts q->mq_freeze_depth. Pēc tam kodols gaida, lÄ«dz rinda tiks iztukÅ”ota blk_mq_freeze_queue_wait().

Laiks, kas nepiecieÅ”ams Ŕīs rindas notÄ«rÄ«Å”anai, ir lÄ«dzvērtÄ«gs diska latentumam, jo ā€‹ā€‹kodols gaida, lÄ«dz tiks pabeigtas visas rindā esoŔās darbÄ«bas. Kad rinda ir tukÅ”a, tiek piemērotas iestatÄ«jumu izmaiņas. Pēc kura to sauc blk_mq_unfreeze_queue(), samazinot skaitÄ«tāju iesaldÄ“Å”anas_dziļums.

Tagad mēs zinām pietiekami daudz, lai situāciju labotu. udevadm trigera komanda liek lietot blokierīces iestatījumus. Šie iestatījumi ir aprakstīti udev noteikumos. Mēs varam noskaidrot, kuri iestatījumi iesaldē rindu, mēģinot tos mainīt, izmantojot sysfs vai apskatot kodola avota kodu. Mēs varam arī izmēģināt BCC utilītu izsekot, kas izvadīs kodola un lietotāja telpas steka trases katram termināļa izsaukumam blk_freeze_queue, piemēram:

~# /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 noteikumi mainās diezgan reti, un parasti tas notiek kontrolētā veidā. Tātad mēs redzam, ka pat jau iestatÄ«to vērtÄ«bu piemēroÅ”ana izraisa aizkavÄ“Å”anos pieprasÄ«juma pārsÅ«tÄ«Å”anā no lietojumprogrammas uz disku. Protams, udev notikumu Ä£enerÄ“Å”ana, kad diska konfigurācijā nav izmaiņu (piemēram, ierÄ«ce nav uzstādÄ«ta/atvienota), nav laba prakse. Taču mēs varam palÄ«dzēt kodolam neveikt liekus darbus un iesaldēt pieprasÄ«jumu rindu, ja tas nav nepiecieÅ”ams. TrÄ«s mazs apņemties labot situāciju.

Secinājumi

eBPF ir ļoti elastīgs un spēcīgs rīks. Rakstā apskatījām vienu praktisku gadījumu un nodemonstrējām nelielu daļu no tā, ko var darīt. Ja jūs interesē BCC utilītu izstrāde, ir vērts to apskatīt oficiālā apmācība, kas labi apraksta pamatus.

Ir arÄ« citi interesanti atkļūdoÅ”anas un profilÄ“Å”anas rÄ«ki, kuru pamatā ir eBPF. Viens no viņiem - bpftrace, kas ļauj rakstÄ«t jaudÄ«gus vienas lÄ«nijas un nelielas programmas awk lÄ«dzÄ«gā valodā. Cits - ebpf_exporter, ļauj apkopot zema lÄ«meņa augstas izŔķirtspējas metriku tieÅ”i savā Prometheus serverÄ« ar iespēju vēlāk iegÅ«t skaistas vizualizācijas un pat brÄ«dinājumus.

Avots: www.habr.com

Pievieno komentāru