Жоғары цеф кідірісінен eBPF/BCC көмегімен ядро ​​патчына дейін

Жоғары цеф кідірісінен eBPF/BCC көмегімен ядро ​​патчына дейін

Linux-та ядро ​​мен қолданбаларды жөндеуге арналған көптеген құралдар бар. Олардың көпшілігі қолданбаның өнімділігіне теріс әсер етеді және өндірісте қолданыла алмайды.

Бір-екі жыл бұрын болған басқа құрал әзірленді - eBPF. Бұл ядроға және үшінші тарап модульдерін ядроға жүктеуге және бағдарламаларды қайта құруды қажет етпей, төмен шығындармен және пайдаланушы қолданбаларын қадағалауға мүмкіндік береді.

eBPF пайдаланатын көптеген қолданбалы утилиталар бар және осы мақалада біз кітапхана негізінде өзіңіздің профильдеу утилитаңызды қалай жазу керектігін қарастырамыз. PythonBCC. Мақала нақты оқиғаларға негізделген. Бар утилиталарды нақты жағдайларда қалай пайдалануға болатынын көрсету үшін мәселеден түзетуге көшеміз.

Цеф баяу

Ceph кластеріне жаңа хост қосылды. Деректердің кейбірін оған көшіргеннен кейін біз оның жазу сұрауларын өңдеу жылдамдығы басқа серверлерге қарағанда әлдеқайда төмен екенін байқадық.

Жоғары цеф кідірісінен eBPF/BCC көмегімен ядро ​​патчына дейін
Басқа платформалардан айырмашылығы, бұл хост bcache және жаңа linux 4.15 ядросын пайдаланды. Бұл конфигурацияның хосты мұнда алғаш рет пайдаланылды. Міне, сол сәтте мәселенің түбірі теориялық тұрғыдан кез келген нәрсе болуы мүмкін екені белгілі болды.

Хостты зерттеу

Ceph-osd процесінде не болатынын қарастырудан бастайық. Ол үшін біз қолданамыз Perf и фламаскоп (ол туралы толығырақ оқуға болады осында):

Жоғары цеф кідірісінен eBPF/BCC көмегімен ядро ​​патчына дейін
Сурет бізге функцияны айтады fdatasync() функцияларға сұрау жіберуге көп уақыт жұмсады generic_make_request(). Бұл, ең алдымен, проблемалардың себебі osd демонының өзінен тыс жерде болуы мүмкін дегенді білдіреді. Бұл ядро ​​немесе дискілер болуы мүмкін. Iostat шығысы bcache дискілері арқылы сұрауларды өңдеуде жоғары кідіріс көрсетті.

Хостты тексеру кезінде біз systemd-udevd демоны процессордың көп уақытын жұмсайтынын анықтадық - бірнеше ядроларда шамамен 20%. Бұл біртүрлі мінез-құлық, сондықтан оның себебін анықтау керек. Systemd-udevd оқиғалармен жұмыс істейтіндіктен, біз оларды қарап шығуды жөн көрдік udevadm мониторы. Жүйедегі әрбір блок құрылғысы үшін өзгерту оқиғаларының үлкен саны жасалғаны белгілі болды. Бұл өте ерекше, сондықтан біз осы оқиғалардың барлығын не тудыратынын қарастыруымыз керек.

BCC құралдар жинағын пайдалану

Біз бұрыннан белгілі болғандай, ядро ​​(және жүйелік қоңыраудағы ceph демоны) көп уақытты generic_make_request(). Осы функцияның жылдамдығын өлшеуге тырысайық. IN BCC Қазірдің өзінде тамаша утилита бар - функциялық. Біз демонды оның PID арқылы қадағалаймыз және нәтижені миллисекундпен шығарамыз.

Жоғары цеф кідірісінен eBPF/BCC көмегімен ядро ​​патчына дейін
Бұл функция әдетте жылдам жұмыс істейді. Мұның бәрі сұрауды құрылғы драйверінің кезегіне беру болып табылады.

Bcache шын мәнінде үш дискіден тұратын күрделі құрылғы:

  • қосалқы құрылғы (кэштелген диск), бұл жағдайда бұл баяу HDD;
  • кэштеу құрылғысы (кэштеу дискісі), мұнда бұл NVMe құрылғысының бір бөлімі;
  • қолданба жұмыс істейтін bcache виртуалды құрылғысы.

Сұранысты жіберу баяу екенін білеміз, бірақ осы құрылғылардың қайсысы үшін? Бұл мәселені сәл кейінірек қарастырамыз.

Біз қазір оқиғалардың қиындықтар тудыруы мүмкін екенін білеміз. Олардың ұрпақтарының нақты себебін табу оңай емес. Бұл мерзімді түрде іске қосылатын бағдарламалық жасақтаманың бір түрі деп есептейік. Сценарий арқылы жүйеде қандай бағдарламалық құрал жұмыс істейтінін көрейік execsnoop сол жерден BCC утилиталар жинағы. Оны іске қосып, нәтижені файлға жіберейік.

Мысалы, келесідей:

/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 құрылғысының неге соншалықты баяу болғаны әлі белгісіз. Біз бірдей конфигурациясы бар сынақ платформасын дайындадық және оқиғаларды жасау үшін udevadm триггерін мерзімді түрде іске қосып, bcache жүйесінде fio іске қосу арқылы мәселені қайта шығаруға тырыстық.

BCC негізіндегі құралдарды жазу

Ең баяу қоңырауларды қадағалау және көрсету үшін қарапайым қызметтік бағдарламаны жазуға тырысайық generic_make_request(). Бізді бұл функция шақырылған дискінің аты да қызықтырады.

Жоспар қарапайым:

  • Тіркелу kprobe туралы generic_make_request():
    • Функция аргументі арқылы қол жеткізуге болатын диск атауын жадқа сақтаймыз;
    • Уақыт белгісін сақтаймыз.

  • Тіркелу кретпроб қайтып оралу үшін generic_make_request():
    • Біз ағымдағы уақыт белгісін аламыз;
    • Біз сақталған уақыт белгісін іздейміз және оны ағымдағымен салыстырамыз;
    • Егер нәтиже көрсетілгеннен үлкен болса, біз сақталған диск атауын тауып, оны терминалда көрсетеміз.

Kprobes и кретпробтар функция кодын жылдам өзгерту үшін тоқтау нүктесі механизмін пайдаланыңыз. Сіз оқи аласыз құжаттама и жақсы осы тақырып бойынша мақала. Түрлі утилиталардың кодын қарасаңыз BCC, сонда олардың құрылымы бірдей екенін көруге болады. Сондықтан бұл мақалада біз сценарий дәлелдерін талдауды өткізіп, BPF бағдарламасының өзіне көшеміз.

Python сценарийіндегі eBPF мәтіні келесідей көрінеді:

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

Функциялар арасында деректер алмасу үшін eBPF бағдарламалары пайдаланылады хэш кестелері. Біз де солай істейміз. Біз процестің PID мәнін кілт ретінде қолданамыз және құрылымды мән ретінде анықтаймыз:

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(). Осыдан кейін біз жұмыс істеп жатқан процестің PID кодын және наносекундтағы ағымдағы уақыт белгісін аламыз. Біз мұның бәрін жаңадан таңдалған етіп жазамыз struct data_t деректері. Құрылымнан диск атауын аламыз био, ол қоңырау шалу кезінде беріледі generic_make_request(), және оны сол құрылымда сақтаңыз мәліметтер. Соңғы қадам - ​​бұрын айтылған хэш кестесіне жазба қосу.

Келесі функция қайтып келгенде шақырылады 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. Біз шақыратын функция bpf_get_current_pid_tgid() ағынның GID және оның PID бір 64-бит мәнінде қайтарады.

Терминалға шығарған кезде біз қазіргі уақытта ағынға қызығушылық танытпаймыз, бірақ бізді негізгі процесс қызықтырады. Алынған кідірісті берілген шекпен салыстырғаннан кейін біз құрылымымызды өтеміз мәліметтер кесте арқылы пайдаланушы кеңістігіне оқиғалар, содан кейін біз жазбаны жоямыз 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 бағдарламасын арқылы дайындауымыз керек 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()

Сценарийдің өзі мына жерден қол жетімді GItHub. Оны fio іске қосылған, bcache-ге жазып, udevadm мониторын шақыратын сынақ платформасында іске қосып көрейік:

Жоғары цеф кідірісінен eBPF/BCC көмегімен ядро ​​патчына дейін
Ақырында! Енді біз тоқтап тұрған bcache құрылғысы сияқты көрінетін нәрсе шынымен тоқтап тұрған қоңырау екенін көреміз generic_make_request() кэштелген диск үшін.

Ядроны қазыңыз

Сұранысты жіберу кезінде нақты не баяулайды? Біз кідіріс сұрауды есепке алу басталғанға дейін орын алатынын көреміз, яғни. ол бойынша статистиканы одан әрі шығаруға арналған нақты сұрауды есепке алу (/proc/diskstats немесе iostat) әлі басталған жоқ. Мұны мәселені шешу кезінде iostat іске қосу арқылы оңай тексеруге болады немесе BCC сценарийінің биологиялық кешігуі, ол сұраныс есебінің басталуы мен аяқталуына негізделген. Бұл утилиталардың ешқайсысы кэштелген дискіге сұрауларға қатысты мәселелерді көрсетпейді.

функциясына қарасақ generic_make_request(), содан кейін сұрауды есепке алу басталғанға дейін тағы екі функция шақырылатынын көреміз. Бірінші - generic_make_request_checks(), диск параметрлеріне қатысты сұраудың заңдылығын тексеруді жүзеге асырады. Екінші - blk_queue_enter(), онда қызықты сынақ бар 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));

Онда ядро ​​кезекті босатуды күтеді. Кешіктіруді өлшейік 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    	|                                    	|

Шешімге жақындап қалған сияқтымыз. Кезекті мұздату/босату үшін пайдаланылатын функциялар blk_mq_freeze_queue и blk_mq_unfreeze_queue. Олар осы кезектегі сұраулар үшін ықтимал қауіпті болып табылатын сұрау кезегі параметрлерін өзгерту қажет болғанда пайдаланылады. Қоңырау шалғанда blk_mq_freeze_queue() функциясы blk_freeze_queue_start() санауыш ұлғаяды q->mq_мұздату_тереңдігі. Осыдан кейін ядро ​​​​кезекті босатуды күтеді blk_mq_freeze_queue_wait().

Бұл кезекті жоюға кететін уақыт дискінің кешігуіне тең, өйткені ядро ​​барлық кезектегі әрекеттердің аяқталуын күтеді. Кезек бос болғаннан кейін параметрлер өзгертулері қолданылады. Осыдан кейін ол шақырылады blk_mq_unfreeze_queue(), есептегішті азайту қату_тереңдігі.

Енді біз жағдайды түзету үшін жеткілікті білеміз. Udevadm триггер пәрмені блоктау құрылғысының параметрлерін қолдануды тудырады. Бұл параметрлер udev ережелерінде сипатталған. Қай параметрлер кезекті тоқтатып тұрғанын sysfs арқылы өзгертуге тырысу немесе ядроның бастапқы кодын қарау арқылы таба аламыз. Сондай-ақ BCC утилитасын қолданып көруге болады із, ол терминалға әрбір қоңырау үшін ядро ​​мен пайдаланушылар кеңістігі стек іздерін шығарады blk_freeze_queue, мысалы:

~# /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 ережелері өте сирек өзгереді және әдетте бұл бақыланатын түрде болады. Сонымен, біз қазірдің өзінде орнатылған мәндерді қолданудың өзі сұрауды қолданбадан дискіге тасымалдаудың кешігуіне әкелетінін көреміз. Әрине, диск конфигурациясында өзгерістер болмаған кезде (мысалы, құрылғы орнатылмаған/ажыратылған) udev оқиғаларын жасау жақсы тәжірибе емес. Дегенмен, біз ядроға қажетсіз жұмыстарды жасамауға көмектесе аламыз және қажет болмаса, сұрау кезегін тоқтата аламыз. Үш кішкентай міндеттеу жағдайды түзетіңіз.

қорытынды

eBPF - өте икемді және қуатты құрал. Мақалада біз бір практикалық жағдайды қарастырдық және не істеуге болатынын аз ғана бөлігін көрсеттік. Егер сіз BCC утилиталарын дамытуға қызығушылық танытсаңыз, оны қарастырған жөн ресми оқу құралы, ол негіздерді жақсы сипаттайды.

eBPF негізінде басқа да қызықты жөндеу және профильдеу құралдары бар. Олардың біреуі - bpftrace, ол awk тіліндегі қуатты бір-лайнерлер мен шағын бағдарламаларды жазуға мүмкіндік береді. Басқа - ebpf_exporter, төмен деңгейлі, жоғары ажыратымдылықтағы көрсеткіштерді кейінірек әдемі визуализациялар және тіпті ескертулер алу мүмкіндігімен тікелей prometheus серверіне жинауға мүмкіндік береді.

Ақпарат көзі: www.habr.com

пікір қалдыру