Od visoke latencije Ceph do kernel patch-a putem eBPF/BCC

Od visoke latencije Ceph do kernel patch-a putem eBPF/BCC

U Linux-u postoji mnogo alata za debagovanje jezgra i aplikacija. Većina njih negativno utiče na performanse aplikacija i ne može se koristiti u produkciji.

Pre nekoliko godina je razvijen još jedan alat — eBPF. Omogućava praćenje jezgra i korisničkih aplikacija sa niskim preopterećenjem i bez potrebe za rekonstrukcijom programa i učitavanjem eksternih modula u jezgro. Trenutno već postoji mnogo aplikativnih alata koji koriste eBPF, a u ovom članku ćemo razmotriti kako da napišemo sopstveni alat za profilisanje zasnovan na biblioteci

PythonBCC . Članak je zasnovan na stvarnim događajima. Proći ćemo put od pojavljivanja problema do njegovog rešavanja, kako bismo pokazali kako se postojeći alati mogu koristiti u konkretnim situacijama.Ceph je spor

U Ceph klaster je dodat novi host. Nakon migracije dela podataka na njega, primetili smo da je brzina obrade zahteva za zapis puno niža nego na ostalim serverima.

Za razliku od drugih platformi, na ovom hostu je korišćen bcache i novo jezgro linux 4.15. Ova konfiguracija hosta se ovde koristila po prvi put. I tada je bilo jasno da je koren problema teoretski mogao biti bilo šta.

Od visoke latencije Ceph do kernel patch-a putem eBPF/BCC
Istraživanje hosta

Počećemo sa tim da pogledamo šta se dešava unutar procesa ceph-osd. Za to ćemo koristiti

perf flamescope in (o kojima možete pročitati više Slika nam govori da je funkcija ovde):

Od visoke latencije Ceph do kernel patch-a putem eBPF/BCC
potrošila mnogo vremena pri slanju zahteva u funkciji fdatasync() generic_make_request() . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova.U proveri hosta otkrili smo da demon systemd-udevd troši veliku količinu CPU vremena — oko 20% na nekoliko jezgara. Ovo bizarno ponašanje zahteva istraživanje. Pošto systemd-udevd radi sa uevent-ima, odlučili smo da ih pratimo putem

udevadm monitor . Iskristalizovalo se da se generiše velika količina change događaja za svaki blok uređaja u sistemu. To je prilično neobično, pa ćemo morati da istražimo šta generiše sve te događaje.Korišćenje BCC alata

Uporaba BCC orodja

Kot smo že ugotovili, jedro (in demon ceph v sistemskem klicu) porabi veliko časa pri . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova.. Poskusimo izmeriti hitrost dela te funkcije. V BCC že obstaja osupljiva utilita — funclatency. Sledili bomo demon po njegovem PID z intervalom med izhodi informacij 1 sekundo in izpisovali rezultate v milisekundah.

Od visoke latencije Ceph do kernel patch-a putem eBPF/BCC
Običajno ta funkcija deluje hitro. Vse, kar počne — pošilja zahtevo v čakalno vrsto naprave.

Bcache je kompleksen sistem, ki v resnici sestavlja tri diske:

  • backing device (disk, ki ga je mogoče predpomniti), v tem primeru je to počasen HDD;
  • caching device (predpomnilni disk), tukaj je to ena particija na NVMe napravi;
  • virtualna naprava bcache, s katero deluje aplikacija.

Vemo, da prenos zahteve upočasnjuje, toda za katero izmed teh naprav? O tem se bomo pogovorili malo kasneje.

Zdaj vemo, da ueventi verjetno povzročajo težave. Ugotoviti, kaj natančno povzroča njihovo generacijo, ni tako preprosto. Predpostavimo, da je to kakšen program, ki se izvaja občasno. Poglejmo, kateri programi se zaženejo v sistemu, s pomočjo skripta execsnoop iz istega niz utilit BCC. Zaženimo ga in preusmerimo izhod v datoteko.

Na primer tako:

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

Tukaj ne bomo priložili celotnega izhoda execsnoop, vendar je ena zanimiva vrstica izgledala takole:

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

Tretja kolona je PPID (parent PID) procesa. Proces z PID 5802 se je izkazal za eno od niti našega nadzornega sistema. Pri preverjanju konfiguracije nadzornega sistema so bile ugotovljene napačne nastavitve. Temperatura HBA-adapterja se je merila vsakih 30 sekund, kar je bistveno pogosteje, kot je potrebno. Po spremembi intervala preverjanja na daljši, smo ugotovili, da zamik obdelave zahtevkov na tej gostiteljski napravi ni več izstopal v primerjavi z drugimi gostiteljskimi napravami.

Toda še vedno ni jasno, zakaj je bcache naprava tako upočasnila. Pripravili smo testno platformo z identično konfiguracijo in poskusili reproducirati težavo, tako da smo zagnali fio na bcache, občasno zagnali udevadm trigger za generacijo uevents.

Pisanje orodij na osnovi BCC

Poskusimo napisati preprosto utilito za sledenje in izpis na ekran najpočasnejših klicev . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova.. Zanimivo nam je tudi ime diska, za katerega je bila ta funkcija poklicana.

Načrt je preprost:

  • Registriramo kprobe na . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova.:
    • Shranjujemo v spomin ime diska, ki je dostopno prek argumenta funkcije;
    • Shranjujemo časovni žig.

  • Registriramo kretprobe na povratku iz . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova.:
    • Pridobimo trenutni časovni žig;
    • Iščemo shranjen časovni žig in ga primerjamo s trenutnim;
    • Če je rezultat večji od nastavljene vrednosti, poiščemo shranjeno ime diska in ga prikažemo na terminalu.

Kprobes in kretprobes uporabljajo mehanizem točk za zaustavitev za dinamično spreminjanje kode funkcij. Preberete lahko dokumentacijo in dobro članek na to temo. Če pogledamo kodo različnih orodij v BCC, opazimo, da imajo identično strukturo. V tem članku bomo preskočili razčlenjevanje argumentov skripte in prešli k sami BPF programu.

Besedilo eBPF znotraj python skripte izgleda takole:

bpf_text = """ # Tu bo koda bpf programa """

Za izmenjavo podatkov med funkcijami, eBPF programi uporabljajo hash-tabele. Tudi mi bomo to storili. Kot ključ bomo uporabili PID procesa, vrednost pa bomo definirali kot strukturo:

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

Tukaj registriramo hash-tabelo, ki se imenuje p, s ključem tipa u64 in vrednostjo tipa struct data_t. Tabela bo dostopna v kontekstu naše BPF-programa. Makro BPF_PERF_OUTPUT registrira drugo tabelo, ki se imenuje events, ki se uporablja za prenos podatkov v uporabniški prostor.

Pri merjenju zamud med klicem funkcije in povratkom iz nje, ali med klici različnih funkcij, je treba upoštevati, da morajo pridobljeni podatki pripadati enemu kontekstu. Drugimi besedami, treba je paziti na morebiten vzporedni zagon funkcij. Imamo možnost izmeriti zamudo med klicem funkcije v okviru enega procesa in povratkom iz te funkcije v okviru drugega procesa, vendar to verjetno ni zelo koristno. Dober primer tu je orodje biolatency, kjer je kot ključ hash-tabele določen kazalec na struct request, ki odraža en zahtevek za disk.

Nato moramo napisati kodo, ki se bo izvajala ob klicu preučevane funkcije:

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

Tukaj bomo kot drugi argument vstavili prvi argument poklicane funkcije . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova.. Po tem dobimo PID procesa, v okviru katerega delujemo, in trenutno časovno oznako v nanosekundah. Vse to zapišemo v sveže dodeljeno struct data_t data. Ime diska dobimo iz strukture bio, ki se prenese ob klicu . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova., in ga shranimo v isto strukturo data. Zadnji korak je dodajanje zapisa v hash tabelo, o kateri smo govorili prej.

Naslednja funkcija se bo klicala ob vrnitvi iz . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova.:

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

Ta funkcija je podobna prejšnji: izvemo PID procesa in časovno oznako, vendar ne dodelimo pomnilnika za novo strukturo data. Namesto tega poiščemo v hash tabeli že obstoječo strukturo po ključu == trenutni PID. Če struktura najdemo, izvemo ime zagnanega procesa in ga dodamo vanjo.

Binarno premikanje, ki ga tu uporabljamo, je potrebno, da pridobimo thread GID, tj. PID glavnega procesa, ki je zagnal nit, v okviru katere delamo. Kličemo funkcijo bpf_get_current_pid_tgid() vrne tako GID niti kot tudi njen PID v eni 64-bitni vrednosti.

Ob izpisu v terminal nas sedaj ne zanima nit, ampak glavni proces. Po primerjavi pridobljene zamude z določenim pragom prenesemo našo strukturo data v prostor uporabnika preko tabele events, nato pa odstranimo zapis iz p.

V python skripti, ki bo naložila to kodo, moramo zamenjati MIN_US in FACTOR z pragovi zamude in enotami časa, ki jih bomo posredovali preko argumentov:

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"

Zdaj moramo pripraviti BPF program preko makra BPF in registrirati probe:

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

Prav tako bomo morali definirati struct data_t v naši skripti, sicer ne bomo mogli nič prebrati:

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

Poslednji korak — izlaz podataka na 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()

Sam skript je dostupan na GItHub. Pokušajmo ga pokrenuti na testnoj platformi gde se pokreće fio, koji piše na bcache, i pozvati udevadm monitor:

Od visoke latencije Ceph do kernel patch-a putem eBPF/BCC
Napokon! Sada vidimo da ono što se činilo kao usporavajuće bcache uređaj, zapravo je usporavajući poziv . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova. za keširani disk.

Istraži kernel

Šta tačno usporava tokom prenosa zahteva? Vidimo da kašnjenje nastaje čak i pre početka obračuna zahteva, tj. praćenje konkretnog zahteva za dalju statistiku ( /proc/diskstats ili iostat) još nije počelo. To se lako može proveriti pokretanjem iostat-a tokom reprodukcije problema, ili BCC skript biolatency, koji se oslanja na početak i kraj obračuna zahteva. Nijedna od ovih alatki neće pokazati probleme za zahteve ka keširanom disku.

Ako pogledamo funkciju . To znači da su verovatno problemi negde van samog osd demona. To može biti ili jezgro ili diskovi. iostat izlaz je pokazivao visoku latenciju obrade zahteva od strane bcache diskova., videćemo da pre nego što počne obračun zahteva, pozivaju se još dve funkcije. Prva — generic_make_request_checks(), vrši provere legitimnosti zahteva u odnosu na postavke diska. Druga — blk_queue_enter(), koja ima interesantan poziv 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));

U njemu kernel čeka odmrzavanje reda. Izmerimo kašnjenje blk_queue_enter():

~# /usr/share/bcc/tools/funclatency  blk_queue_enter -i 1 -m               	 
Praćenje 1 funkcija za "blk_queue_enter"... Pritisnite Ctrl-C za kraj.

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

Čini se da smo blizu razjašnjenja. Funkcije koje se koriste za „zamrzavanje/odmrzavanje“ reda su blk_mq_freeze_queue in blk_mq_unfreeze_queue. Se uporabljajo, ko je potrebno spremeniti nastavitve čakalne vrste zahtevkov, kar lahko predstavlja potencialno nevarnost za zahtevke v tej čakalni vrsti. Ob klicu blk_mq_freeze_queue() funkcija blk_freeze_queue_start() se poveča števec q->mq_freeze_depth. Po tem jedro čaka na praznjenje čakalne vrste v blk_mq_freeze_queue_wait().

Čas čakanja za praznjenje te čakalne vrste je enak zakasnitvi diska, saj jedro čaka na dokončanje vseh operacij, ki so bile postavljene v čakalno vrsto. Ko je čakalna vrsta prazna, se uporabijo spremenjene nastavitve. Nato se pokliče blk_mq_unfreeze_queue(), ki zmanjša števec freeze_depth.

Zdaj vemo dovolj, da popravimo situacijo. Ukaz udevadm trigger vodi do uporabe nastavitev za blok napravo. Te nastavitve so opisane v pravilih udev. Lahko ugotovimo, katere natančne nastavitve „zamrznejo“ čakalno vrsto, če jih poskusimo spremeniti prek sysfs ali si ogledamo izvorno kodo jedra. Poleg tega lahko poskusimo z orodjem BCC trace, ki bo na terminal izpisalo sledi jedrskega in uporabniškega prostora za vsak klic blk_freeze_queue, na primer:

~# /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 pravila se redko spreminjajo in običajno potekajo pod nadzorom. Tako vidimo, da tudi uporaba že določenih vrednosti povzroči skok v zakasnitvi prenosa zahtevka iz aplikacije na disk. Seveda generiranje udev dogodkov, ko ni nobenih sprememb v konfiguraciji diskov (na primer, če naprava ni priključena/odklopljena), ni najboljša praksa. Kljub temu lahko pomagamo jedru, da ne dela nepotrebnega dela in ne „zamrzne“ čakalne vrste zahtevkov, če to ni potrebno. Tri majhne zaveze odpravljajo situacijo.

Zaključek

eBPF je izjemno prilagodljivo in zmogljivo orodje. V članku smo obravnavali en praktičen primer in pokazali majhen delček tistega, kar je mogoče doseči. Če vas zanima razvoj BCC orodij, si oglejte uradni vodič, ki dobro opisuje osnove delovanja.

Obstajajo tudi druga zanimiva orodja za odpravljanje napak in profiliranje, ki temeljijo na eBPF. Eden izmed njih je bpftrace, ki omogoča pisanje zmogljivih enovrstičnih in majhnih programov v jeziku, podobnem awku. Drugo je ebpf_exporter, ki omogoča zbiranje nizko nivojskih metrik visoke ločljivosti neposredno na vašem prometheus strežniku, z možnostjo poznejše pridobitve lepe vizualizacije in celo opozoril.

Izvor: habr.com

Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki 🔥 Kupite zanesljivo gostovanje za spletne strani z zaščito pred DDoS, VPS VDS strežniki | ProHoster