Linux-ն ունի միջուկի և հավելվածների վրիպազերծման մեծ թվով գործիքներ: Դրանց մեծ մասը բացասաբար է անդրադառնում հավելվածի աշխատանքի վրա և չի կարող օգտագործվել արտադրության մեջ:
Մի երկու տարի առաջ կար
Արդեն կան բազմաթիվ հավելվածներ, որոնք օգտագործում են eBPF, և այս հոդվածում մենք կանդրադառնանք, թե ինչպես գրել ձեր սեփական պրոֆիլավորման օգտակար ծրագիրը՝ հիմնվելով գրադարանի վրա:
Սեֆը դանդաղ է
Ceph կլաստերին ավելացվել է նոր հաղորդավար: Տվյալների մի մասը դրան տեղափոխելուց հետո մենք նկատեցինք, որ դրա կողմից գրելու հարցումների մշակման արագությունը շատ ավելի ցածր էր, քան մյուս սերվերներում:
Ի տարբերություն այլ հարթակների, այս հոսթն օգտագործում էր bcache և նոր Linux 4.15 միջուկ: Սա առաջին անգամն էր, որ այս կոնֆիգուրացիայի հոսթն օգտագործվում էր այստեղ: Եվ այդ պահին պարզ էր, որ խնդրի արմատը տեսականորեն կարող է լինել ամեն ինչ։
Հյուրընկալողի հետաքննություն
Եկեք սկսենք նայելով, թե ինչ է տեղի ունենում ceph-osd գործընթացի ներսում: Դրա համար մենք կօգտագործենք
Նկարը մեզ ասում է, որ ֆունկցիան fdatasync () շատ ժամանակ է ծախսել գործառույթներին հարցում ուղարկելու համար generic_make_request (). Սա նշանակում է, որ, ամենայն հավանականությամբ, խնդիրների պատճառը գտնվում է բուն osd daemon-ից դուրս: Սա կարող է լինել կամ միջուկը կամ սկավառակը: Իոստատի ելքը ցույց տվեց bcache սկավառակների կողմից հարցումների մշակման բարձր ուշացում:
Հոսթին ստուգելիս մենք պարզեցինք, որ systemd-udevd daemon-ը մեծ քանակությամբ պրոցեսորի ժամանակ է ծախսում՝ մոտ 20% մի քանի միջուկների վրա: Սա տարօրինակ պահվածք է, ուստի պետք է պարզել, թե ինչու: Քանի որ Systemd-udevd-ն աշխատում է uevents-ի հետ, մենք որոշեցինք դրանք դիտարկել udevadm մոնիտոր. Պարզվում է, որ համակարգի յուրաքանչյուր բլոկ սարքի համար մեծ թվով փոփոխության իրադարձություններ են ստեղծվել։ Սա բավականին անսովոր է, ուստի մենք պետք է նայենք, թե ինչն է առաջացնում այս բոլոր իրադարձությունները:
Օգտագործելով BCC Toolkit-ը
Ինչպես արդեն պարզել ենք, միջուկը (և համակարգային զանգի ceph daemon-ը) շատ ժամանակ է ծախսում generic_make_request (). Փորձենք չափել այս ֆունկցիայի արագությունը։ IN
Այս հատկությունը սովորաբար արագ է աշխատում: Այն ամենը, ինչ անում է, այն է, որ հարցումը փոխանցի սարքի վարորդի հերթին:
Bcache բարդ սարք է, որն իրականում բաղկացած է երեք սկավառակից.
- օժանդակ սարք (քեշավորված սկավառակ), այս դեպքում դա դանդաղ HDD է;
- քեշավորման սարք (քեշավորման սկավառակ), այստեղ սա NVMe սարքի մեկ բաժանումն է.
- bcache վիրտուալ սարքը, որով աշխատում է հավելվածը:
Մենք գիտենք, որ հարցումների փոխանցումը դանդաղ է, բայց այս սարքերից ո՞րի համար: Սրանով կզբաղվենք մի փոքր ուշ:
Այժմ մենք գիտենք, որ իրադարձությունները կարող են խնդիրներ առաջացնել: Գտնել, թե կոնկրետ ինչն է առաջացնում նրանց սերունդը, այնքան էլ հեշտ չէ: Ենթադրենք, որ սա ինչ-որ ծրագրաշար է, որը պարբերաբար գործարկվում է: Տեսնենք, թե ինչպիսի ծրագրակազմ է աշխատում համակարգում՝ օգտագործելով սկրիպտը execsnoop նույնից
Օրինակ այսպես.
/usr/share/bcc/tools/execsnoop | tee ./execdump
Մենք այստեղ չենք ցուցադրի execsnoop-ի ամբողջական արդյունքը, բայց մեզ հետաքրքրող մի տող այսպիսի տեսք ուներ.
sh 1764905 5802 0 sudo arcconf getconfig 1 AD | grep Temperature | awk -F '[:/]' '{print $2}' | sed 's/^ ([0-9]*) C.*/1/'
Երրորդ սյունակը գործընթացի PPID-ն է (ծնող PID): PID 5802-ով գործընթացը պարզվեց, որ մեր մոնիտորինգի համակարգի թելերից մեկն է։ Մոնիտորինգի համակարգի կոնֆիգուրացիան ստուգելիս հայտնաբերվել են սխալ պարամետրեր: HBA ադապտերի ջերմաստիճանը չափվել է 30 վայրկյանը մեկ, ինչը շատ ավելի հաճախ է, քան անհրաժեշտ է։ Ստուգման միջակայքն ավելի երկարի փոխելուց հետո մենք պարզեցինք, որ հարցումների մշակման հետաձգումը այս հոսթինգում այլևս աչքի չի ընկնում այլ հոսթորդների համեմատ:
Բայց դեռ պարզ չէ, թե ինչու էր bcache սարքն այդքան դանդաղ: Մենք պատրաստեցինք նույնական կոնֆիգուրացիայով փորձնական հարթակ և փորձեցինք վերարտադրել խնդիրը՝ գործարկելով fio-ն bcache-ում, պարբերաբար գործարկելով udevadm գործարկիչը՝ uevents ստեղծելու համար:
BCC-ի վրա հիմնված գործիքներ գրելը
Փորձենք գրել մի պարզ ծրագիր՝ ամենադանդաղ զանգերը հետագծելու և ցուցադրելու համար generic_make_request (). Մեզ հետաքրքրում է նաև այն դրայվի անվանումը, որի համար կանչվել է այս ֆունկցիան։
Պլանը պարզ է.
- Գրանցվել kprobe մասին generic_make_request ():
- Մենք պահում ենք սկավառակի անունը հիշողության մեջ, որը հասանելի է ֆունկցիայի փաստարկի միջոցով.
- Մենք պահպանում ենք ժամանակի կնիքը:
- Գրանցվել kretprobe -ից վերադարձի համար generic_make_request ():
- Մենք ստանում ենք ընթացիկ ժամանակացույցը;
- Մենք փնտրում ենք պահպանված ժամանակի դրոշմակնիքը և համեմատում այն ընթացիկի հետ.
- Եթե արդյունքը ավելի մեծ է, քան նշվածը, ապա մենք գտնում ենք պահված սկավառակի անունը և ցուցադրում այն տերմինալում:
Kprobes и kretprobes օգտագործեք բեկման կետի մեխանիզմ՝ ֆունկցիոնալ կոդը անմիջապես փոխելու համար: Դուք կարող եք կարդալ
python սցենարի ներսում eBPF տեքստը նման է հետևյալին.
bpf_text = “”” # Here will be the bpf program code “””
Ֆունկցիաների միջև տվյալների փոխանակման համար eBPF ծրագրերն օգտագործում են
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);
Այստեղ մենք գրանցում ենք հեշ աղյուսակ, որը կոչվում է p, բանալի տեսակով u64 և տիպի արժեքը struct data_t. Աղյուսակը հասանելի կլինի մեր BPF ծրագրի համատեքստում: BPF_PERF_OUTPUT մակրոն գրանցում է մեկ այլ աղյուսակ, որը կոչվում է իրադարձությունները, որն օգտագործվում է
Ֆունկցիա կանչելու և դրանից վերադառնալու կամ տարբեր գործառույթների կանչերի միջև ուշացումները չափելիս պետք է հաշվի առնել, որ ստացված տվյալները պետք է պատկանեն նույն համատեքստին: Այլ կերպ ասած, դուք պետք է հիշեք գործառույթների հնարավոր զուգահեռ գործարկման մասին: Մենք հնարավորություն ունենք չափելու ուշացումը մի գործընթացի համատեքստում ֆունկցիա կանչելու և մեկ այլ գործընթացի համատեքստում այդ ֆունկցիայից վերադառնալու միջև, բայց դա, հավանաբար, անօգուտ է: Այստեղ լավ օրինակ կլինի
Հաջորդը, մենք պետք է գրենք կոդը, որը կաշխատի, երբ ուսումնասիրվող ֆունկցիան կանչվի.
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);
}
Այստեղ կանչված ֆունկցիայի առաջին արգումենտը կփոխարինվի որպես երկրորդ արգումենտ
Հետևյալ ֆունկցիան կկանչվի վերադառնալուց հետո 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);
}
}
Այս ֆունկցիան նման է նախորդին. մենք պարզում ենք գործընթացի PID-ն ու ժամանակի դրոշմակնիքը, բայց տվյալների նոր կառուցվածքի համար հիշողություն չենք հատկացնում: Փոխարենը, մենք որոնում ենք հեշ աղյուսակում արդեն գոյություն ունեցող կառուցվածք՝ օգտագործելով == ընթացիկ PID բանալին: Եթե կառույցը գտնվի, ապա մենք պարզում ենք ընթացիկ գործընթացի անվանումը և ավելացնում այն:
Երկուական հերթափոխը, որը մենք օգտագործում ենք այստեղ, անհրաժեշտ է թեմայի GID-ը ստանալու համար: դրանք. Հիմնական գործընթացի PID, որը սկսել է շարանը, որի համատեքստում մենք աշխատում ենք: Գործառույթը, որը մենք կոչում ենք
Տերմինալ ելք ուղարկելիս մենք ներկայումս հետաքրքրված չենք հոսքով, բայց մեզ հետաքրքրում է հիմնական գործընթացը: Ստացված ուշացումը տվյալ շեմի հետ համեմատելուց հետո մենք անցնում ենք մեր կառուցվածքը տվյալներ աղյուսակի միջոցով օգտագործողի տարածություն իրադարձությունները, որից հետո մենք ջնջում ենք մուտքը p.
Python սկրիպտում, որը կբեռնի այս կոդը, մենք պետք է փոխարինենք MIN_US-ը և FACTOR-ը հետաձգման շեմերով և ժամանակի միավորներով, որոնք մենք կանցնենք արգումենտների միջով.
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"
Այժմ մենք պետք է պատրաստենք BPF ծրագիրը միջոցով
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")
Մենք նույնպես պետք է որոշենք struct data_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)]
Վերջին քայլը տվյալների տերմինալ դուրս բերելն է.
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()
Սցենարն ինքնին հասանելի է
Վերջապես! Այժմ մենք տեսնում ենք, որ այն, ինչ նման էր փակվող bcache սարքի, իրականում դադարեցված զանգ է generic_make_request () քեշավորված սկավառակի համար:
Փորեք միջուկը
Ի՞նչն է կոնկրետ դանդաղում հարցումների փոխանցման ժամանակ: Մենք տեսնում ենք, որ ուշացումը տեղի է ունենում նույնիսկ պահանջի հաշվառման մեկնարկից առաջ, այսինքն. Դրա վերաբերյալ վիճակագրության հետագա ելքի համար կոնկրետ հարցումի հաշվառումը (/proc/diskstats կամ iostat) դեռ չի սկսվել: Սա հեշտությամբ կարելի է հաստատել՝ խնդիրը վերարտադրելիս գործարկելով iostat, կամ
Եթե նայենք ֆունկցիային generic_make_request (), ապա կտեսնենք, որ մինչ հարցումը կսկսվի հաշվառումը, կանչվում է ևս երկու գործառույթ։ Առաջին - generic_make_request_checks(), ստուգում է սկավառակի կարգավորումների վերաբերյալ հարցումի օրինականությունը։ Երկրորդ -
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));
Դրանում միջուկը սպասում է հերթի ապասառեցմանը: Եկեք չափենք ուշացումը 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 | |
Կարծես թե մենք մոտ ենք լուծմանը: Հերթի սառեցման/ապասառեցման համար օգտագործվող գործառույթներն են
Այս հերթը մաքրելու համար պահանջվող ժամանակը համարժեք է սկավառակի հետաձգմանը, քանի որ միջուկը սպասում է հերթագրված բոլոր գործողությունների ավարտին: Երբ հերթը դատարկ է, կարգավորումների փոփոխությունները կիրառվում են: Որից հետո կոչվում է
Այժմ մենք բավականաչափ գիտենք իրավիճակը շտկելու համար։ Udevadm ձգան հրամանը հանգեցնում է արգելափակման սարքի կարգավորումների կիրառմանը: Այս կարգավորումները նկարագրված են udev կանոններում: Մենք կարող ենք գտնել, թե որ կարգավորումներն են սառեցնում հերթը` փորձելով դրանք փոխել sysfs-ի միջոցով կամ նայելով միջուկի սկզբնական կոդը: Մենք կարող ենք նաև փորձել BCC կոմունալ ծրագիրը
~# /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-ի իրադարձությունների ստեղծումը, երբ սկավառակի կազմաձևում փոփոխություններ չկան (օրինակ՝ սարքը միացված/անջատված չէ) լավ պրակտիկա չէ: Այնուամենայնիվ, մենք կարող ենք օգնել միջուկին չանել ավելորդ աշխատանք և սառեցնել հարցումների հերթը, եթե դա անհրաժեշտ չէ:
Եզրափակում
eBPF-ն շատ ճկուն և հզոր գործիք է: Հոդվածում մենք դիտարկեցինք մեկ գործնական դեպք և ցույց տվեցինք, թե ինչ կարելի է անել: Եթե դուք հետաքրքրված եք BCC կոմունալ ծառայությունների մշակմամբ, արժե դիտել
EBPF-ի վրա հիմնված վրիպազերծման և պրոֆիլավորման այլ հետաքրքիր գործիքներ կան: Նրանցից մեկը -
Source: www.habr.com