eBPF/BCC istifadə edərək Yüksək Ceph Latency-dən Kernel Patch-ə qədər

eBPF/BCC istifadə edərək Yüksək Ceph Latency-dən Kernel Patch-ə qədər

Linux-da kernel və proqramların sazlanması üçün çoxlu alətlər var. Onların əksəriyyəti tətbiq performansına mənfi təsir göstərir və istehsalda istifadə edilə bilməz.

Bir neçə il əvvəl var idi başqa bir alət hazırlanmışdır - eBPF. Bu, az yüklə və proqramları yenidən qurmağa və üçüncü tərəf modullarını nüvəyə yükləməyə ehtiyac olmadan nüvəni və istifadəçi proqramlarını izləməyə imkan verir.

Artıq eBPF-dən istifadə edən bir çox proqram yardımçıları var və bu məqalədə kitabxanaya əsaslanaraq öz profilləşdirmə yardım proqramını necə yazacağınıza baxacağıq. PythonBCC. Məqalə real hadisələrə əsaslanır. Mövcud kommunal proqramların konkret vəziyyətlərdə necə istifadə oluna biləcəyini göstərmək üçün problemdən həllə keçəcəyik.

Ceph Yavaşdır

Ceph klasterinə yeni host əlavə edildi. Məlumatların bir hissəsini ona köçürdükdən sonra onun yazma sorğularını emal sürətinin digər serverlərə nisbətən xeyli aşağı olduğunu gördük.

eBPF/BCC istifadə edərək Yüksək Ceph Latency-dən Kernel Patch-ə qədər
Digər platformalardan fərqli olaraq, bu host bcache və yeni linux 4.15 nüvəsindən istifadə edirdi. Bu, ilk dəfə idi ki, burada bu konfiqurasiyadan istifadə edilirdi. Və o an aydın oldu ki, problemin kökü nəzəri olaraq hər şey ola bilər.

Ev sahibinin araşdırılması

Ceph-osd prosesində baş verənlərə baxaraq başlayaq. Bunun üçün istifadə edəcəyik perf и alovoskop (haqqında daha çox oxuya bilərsiniz burada):

eBPF/BCC istifadə edərək Yüksək Ceph Latency-dən Kernel Patch-ə qədər
Şəkil bizə funksiyanı bildirir fdatasync() funksiyalara sorğu göndərmək üçün çox vaxt sərf etdi generic_make_request(). Bu o deməkdir ki, çox güman ki, problemlərin səbəbi osd demonunun özündən kənardadır. Bu, nüvə və ya disklər ola bilər. İostat çıxışı bcache diskləri tərəfindən sorğuların emalında yüksək gecikmə göstərdi.

Hostu yoxlayarkən biz müəyyən etdik ki, systemd-udevd demonu çoxlu CPU vaxtını sərf edir - bir neçə nüvədə təxminən 20%. Bu qəribə davranışdır, ona görə də bunun səbəbini tapmaq lazımdır. Systemd-udevd hadisələrlə işlədiyi üçün biz onları nəzərdən keçirmək qərarına gəldik udevadm monitor. Belə çıxır ki, sistemdəki hər bir blok cihazı üçün çoxlu sayda dəyişiklik hadisəsi yaradılıb. Bu olduqca qeyri-adidir, ona görə də bütün bu hadisələri nəyin yaratdığına baxmaq məcburiyyətində qalacağıq.

BCC Alət dəstindən istifadə

Artıq aşkar etdiyimiz kimi, nüvə (və sistem çağırışında olan ceph demonu) çox vaxt sərf edir. generic_make_request(). Bu funksiyanın sürətini ölçməyə çalışaq. IN BCC Artıq gözəl bir yardım proqramı var - funklatlıq. Demonu çıxışlar arasında 1 saniyəlik fasilə ilə PID-i ilə izləyəcəyik və nəticəni millisaniyələrlə çıxaracağıq.

eBPF/BCC istifadə edərək Yüksək Ceph Latency-dən Kernel Patch-ə qədər
Bu xüsusiyyət adətən tez işləyir. Bunun etdiyi hər şey sorğunu cihaz sürücüsü növbəsinə ötürməkdir.

Bcache əslində üç diskdən ibarət mürəkkəb bir cihazdır:

  • dəstək cihazı (keşlənmiş disk), bu halda yavaş HDD-dir;
  • önbelleğe alma cihazı (keşləmə diski), burada bu NVMe cihazının bir hissəsidir;
  • proqramın işlədiyi bcache virtual cihazı.

Sorğunun ötürülməsinin yavaş olduğunu bilirik, lakin bu cihazlardan hansı üçün? Bununla bir az sonra məşğul olacağıq.

İndi hadisələrin problem yarada biləcəyini bilirik. Onların nəslinə tam olaraq nəyin səbəb olduğunu tapmaq o qədər də asan deyil. Tutaq ki, bu, vaxtaşırı işə salınan bir növ proqramdır. Skriptdən istifadə edərək sistemdə hansı proqram təminatının işlədiyini görək execsnoop eynidən BCC yardım dəsti. Onu işə salaq və çıxışı fayla göndərək.

Məsələn, bu kimi:

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

Burada execsnoop-un tam çıxışını göstərməyəcəyik, lakin bizi maraqlandıran bir xətt belə görünürdü:

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

Üçüncü sütun prosesin PPID-idir (ana PID). PID 5802 ilə proses monitorinq sistemimizin mövzularından biri oldu. Monitorinq sisteminin konfiqurasiyası yoxlanılarkən səhv parametrlər aşkar edilmişdir. HBA adapterinin temperaturu hər 30 saniyədə götürülürdü ki, bu da lazım olduğundan daha tez-tez olur. Yoxlama intervalını daha uzun müddətə dəyişdikdən sonra biz aşkar etdik ki, bu hostda sorğunun emal gecikməsi artıq digər hostlarla müqayisədə fərqlənmir.

Ancaq bcache cihazının niyə bu qədər yavaş olduğu hələ də aydın deyil. Biz eyni konfiqurasiyaya malik test platforması hazırladıq və hadisələri yaratmaq üçün vaxtaşırı udevadm trigger-i işə salaraq, bcache-də fio işlətməklə problemi təkrar yaratmağa çalışdıq.

BCC əsaslı alətlərin yazılması

Ən yavaş zəngləri izləmək və göstərmək üçün sadə bir yardım proqramı yazmağa çalışaq generic_make_request(). Bu funksiyanın çağırıldığı sürücünün adı ilə də maraqlanırıq.

Plan sadədir:

  • Qeydiyyatdan keçin kprobe haqqında generic_make_request():
    • Disk adını yaddaşda saxlayırıq, funksiya arqumenti vasitəsilə əldə edilə bilər;
    • Vaxt möhürünü saxlayırıq.

  • Qeydiyyatdan keçin kretprobe -dən qayıtmaq üçün generic_make_request():
    • Cari vaxt damğasını alırıq;
    • Saxlanmış vaxt damğasını axtarırıq və onu cari ilə müqayisə edirik;
    • Nəticə göstəriləndən böyükdürsə, biz saxlanan diskin adını tapırıq və onu terminalda göstəririk.

Kprobes и kretproblar funksiya kodunu tez dəyişmək üçün kəsilmə nöqtəsi mexanizmindən istifadə edin. Oxuya bilərsiniz sənədlər и yaxşıdır bu mövzuda məqalə. Müxtəlif kommunalların koduna baxsanız BCC, onda onların eyni quruluşa malik olduğunu görə bilərsiniz. Beləliklə, bu məqalədə skript arqumentlərinin təhlilini atlayacağıq və BPF proqramının özünə keçəcəyik.

Python skriptindəki eBPF mətni belə görünür:

bpf_text = “”” # Here will be the bpf program code “””

Funksiyalar arasında məlumat mübadiləsi üçün eBPF proqramları istifadə edir hash cədvəlləri. Biz də eyni şeyi edəcəyik. Proses PID-ni açar kimi istifadə edəcəyik və strukturu dəyər kimi təyin edəcəyik:

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

Burada adlanan hash cədvəlini qeyd edirik p, açar növü ilə u64 və növün dəyəri struktur data_t. Cədvəl bizim BPF proqramımızın kontekstində mövcud olacaq. BPF_PERF_OUTPUT makro adlanan başqa bir cədvəli qeyd edir hadisələrüçün istifadə olunur məlumatların ötürülməsi istifadəçi məkanına.

Funksiyaya zəng etmək və ondan qayıtmaq və ya müxtəlif funksiyalara zənglər arasında gecikmələri ölçərkən nəzərə almaq lazımdır ki, alınan məlumat eyni kontekstə aid olmalıdır. Başqa sözlə, funksiyaların mümkün paralel işə salınmasını xatırlamaq lazımdır. Bir proses kontekstində funksiyanın çağırılması ilə digər proses kontekstində həmin funksiyadan geri qayıtması arasındakı gecikməni ölçmək imkanımız var, lakin bu, çox güman ki, faydasızdır. Burada yaxşı bir nümunə olardı biolatentlik proqramı, burada hash cədvəli açarı göstəriciyə təyin edilir struktur tələbi, bir disk sorğusunu əks etdirir.

Sonra, öyrənilən funksiya çağırıldıqda işləyəcək kodu yazmalıyıq:

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

Burada çağırılan funksiyanın birinci arqumenti ikinci arqument kimi əvəz olunacaq generic_make_request(). Bundan sonra biz işlədiyimiz kontekstdə prosesin PID-ini və nanosaniyələrdə cari vaxt damğasını alırıq. Hamısını təzə seçilmiş şəkildə yazırıq struct data_t data. Strukturdan diskin adını alırıq bio, zəng edərkən ötürülür generic_make_request(), və eyni strukturda qeyd edin məlumat. Son addım əvvəllər qeyd olunan hash cədvəlinə giriş əlavə etməkdir.

Aşağıdakı funksiya geri qayıtdıqda çağırılacaq 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);
    }
}

Bu funksiya əvvəlkinə bənzəyir: biz prosesin PID-sini və vaxt damğasını tapırıq, lakin yeni məlumat strukturu üçün yaddaş ayırmırıq. Bunun əvəzinə biz == cari PID açarından istifadə edərək artıq mövcud strukturu hash cədvəlində axtarırıq. Əgər struktur tapılarsa, o zaman işləyən prosesin adını tapıb ona əlavə edirik.

Burada istifadə etdiyimiz binar shift GID ipini əldə etmək üçün lazımdır. olanlar. İşlədiyimiz kontekstdə mövzunu başlatan əsas prosesin PID-i. Zəng etdiyimiz funksiya bpf_get_current_pid_tgid() həm mövzunun GID-sini, həm də PID-ni tək 64 bit dəyərində qaytarır.

Terminalda çıxış edərkən, biz hazırda mövzu ilə maraqlanmırıq, lakin əsas proseslə maraqlanırıq. Yaranan gecikməni müəyyən bir həddi ilə müqayisə etdikdən sonra strukturumuzu keçirik məlumat cədvəl vasitəsilə istifadəçi sahəsinə daxil edilir hadisələr, bundan sonra girişi silirik p.

Bu kodu yükləyən python skriptində MIN_US və FACTOR-u arqumentlərdən keçəcəyimiz gecikmə hədləri və vaxt vahidləri ilə əvəz etməliyik:

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"

İndi vasitəsilə BPF proqramını hazırlamalıyıq BPF makro və qeydiyyat nümunələri:

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

Biz də müəyyən etməli olacağıq struktur data_t skriptimizdə, əks halda heç nə oxuya bilməyəcəyik:

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

Son addım məlumatları terminala çıxarmaqdır:

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

Skriptin özü burada mövcuddur GItHub. Gəlin onu fio-nun işlədiyi, bcache-ə yazdığı və udevadm monitoru çağırdığı sınaq platformasında işə salmağa çalışaq:

eBPF/BCC istifadə edərək Yüksək Ceph Latency-dən Kernel Patch-ə qədər
Nəhayət! İndi görürük ki, dayanan bcache cihazı kimi görünən şey əslində dayanan bir zəngdir generic_make_request() keşlənmiş disk üçün.

Kernelə qazın

Sorğunun ötürülməsi zamanı tam olaraq nə yavaşlayır? Biz gecikmənin sorğunun uçotu başlamazdan əvvəl baş verdiyini görürük, yəni. ona dair statistik məlumatların əlavə çıxarılması üçün xüsusi sorğunun (/proc/diskstats və ya iostat) uçotu hələ başlamamışdır. Bu problemi təkrarlayarkən iostat işlətməklə asanlıqla yoxlanıla bilər və ya BCC skriptinin biolatentliyi, bu sorğunun mühasibat uçotunun başlanğıcı və sonuna əsaslanır. Bu yardım proqramlarının heç biri keşlənmiş diskə sorğular üçün problemlər göstərməyəcək.

Funksiyaya baxsaq generic_make_request(), onda biz sorğunun mühasibat uçotuna başlamazdan əvvəl daha iki funksiyanın çağırıldığını görəcəyik. Birinci - generic_make_request_checks(), disk parametrləri ilə bağlı sorğunun qanuniliyini yoxlayır. İkinci - blk_queue_enter(), maraqlı bir çağırış var 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));

Orada nüvə növbənin donmasını gözləyir. Gecikməni ölçək 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    	|                                    	|

Deyəsən, həll yoluna yaxınlaşırıq. Növbənin dondurulması/açılması üçün istifadə olunan funksiyalar bunlardır blk_mq_freeze_queue и blk_mq_unfreeze_queue. Onlar bu növbədəki sorğular üçün potensial təhlükəli olan sorğu növbəsi parametrlərini dəyişdirmək lazım olduqda istifadə olunur. Zəng edəndə blk_mq_freeze_queue() funksiyası blk_freeze_queue_start() sayğac artır q->mq_dondurma_dərinliyi. Bundan sonra kernel növbənin boşalmasını gözləyir blk_mq_freeze_queue_wait().

Bu növbəni təmizləmək üçün lazım olan vaxt diskin gecikməsinə bərabərdir, çünki nüvə bütün növbəli əməliyyatların tamamlanmasını gözləyir. Növbə boş olduqdan sonra parametr dəyişiklikləri tətbiq edilir. Bundan sonra çağırılır blk_mq_unfreeze_queue(), sayğacın azaldılması donma_dərinliyi.

İndi vəziyyəti düzəltmək üçün kifayət qədər məlumatımız var. Udevadm trigger əmri blok cihazı üçün parametrlərin tətbiq edilməsinə səbəb olur. Bu parametrlər udev qaydalarında təsvir edilmişdir. Hansı parametrlərin növbəni dondurduğunu sysfs vasitəsilə dəyişdirməyə cəhd etməklə və ya nüvənin mənbə koduna baxaraq tapa bilərik. BCC yardım proqramını da sınaya bilərik iz, terminala hər zəng üçün kernel və istifadəçi sahəsi yığın izlərini çıxaracaq blk_freeze_queue, məsələn:

~# /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 qaydaları olduqca nadir hallarda dəyişir və adətən bu, idarə olunan şəkildə baş verir. Beləliklə, görürük ki, hətta artıq təyin edilmiş dəyərlərin tətbiqi sorğunun tətbiqdən diskə ötürülməsində gecikmənin artmasına səbəb olur. Əlbəttə ki, disk konfiqurasiyasında heç bir dəyişiklik olmadıqda (məsələn, cihaz quraşdırılmayıb/sökülməyib) udev hadisələrinin yaradılması yaxşı təcrübə deyil. Bununla belə, nüvəyə lazımsız iş görməməyə kömək edə bilərik və lazım deyilsə, sorğu növbəsini dondura bilərik. Üç nəfər kiçik törətmək vəziyyəti düzəltmək.

Nəticə

eBPF çox çevik və güclü vasitədir. Məqalədə biz bir praktik işə baxdıq və nə edilə biləcəyinin kiçik bir hissəsini nümayiş etdirdik. BCC utilitlərini inkişaf etdirməklə maraqlanırsınızsa, ona nəzər salmağa dəyər rəsmi dərslik, əsasları yaxşı təsvir edir.

eBPF-ə əsaslanan digər maraqlı sazlama və profilləşdirmə vasitələri var. Onlardan biri - bpftrace, awk-a bənzər dildə güclü bir-laynerlər və kiçik proqramlar yazmağa imkan verir. Başqa bir - ebpf_exporter, daha sonra gözəl vizuallaşdırmalar və hətta xəbərdarlıqlar əldə etmək imkanı ilə aşağı səviyyəli, yüksək ayırdetmə göstəricilərini birbaşa prometheus serverinizə toplamağa imkan verir.

Mənbə: www.habr.com

Добавить комментарий