
Ada banyak alat untuk debugging kernel dan program userspace di Linux. Kebanyakan dari mereka memiliki dampak kinerja dan tidak dapat dengan mudah dijalankan di lingkungan produksi. Beberapa tahun yang lalu, eBPF
dikembangkan , yang menyediakan kemampuan untuk melacak kernel dan ruang pengguna dengan overhead rendah, tanpa perlu mengkompilasi ulang program atau memuat modul kernel.
Sekarang ada banyak alat yang menggunakan eBPF dan dalam artikel ini, kami akan menjelaskan cara menulis alat profil Anda sendiri menggunakan
pustaka PythonBCC . Artikel ini didasarkan pada masalah nyata dari lingkungan produksi. Kami akan memandu Anda menyelesaikan masalah dan menunjukkan bagaimana alat bcc yang ada dapat digunakan dalam beberapa kasus.
Ceph lambat
Platform baru ditambahkan ke ceph cluster. Setelah memigrasi beberapa data ke platform, latensi permintaan tulis lebih tinggi daripada di server lain.
Platform ini memiliki perangkat virtual caching baru - bcache, yang belum pernah kami gunakan di cluster ini sebelumnya - dan kernel baru - 4,15, yang masih tidak digunakan di tempat lain di cluster ini. Akar masalahnya bisa di mana saja, jadi mari kita lihat lebih dalam.
Investigasi tuan rumah
Mari kita lihat apa yang terjadi di dalam proses ceph-osd. Kami menggunakan alat tracing
perf dan
flamescope untuk membuat flamegraph:
Seperti yang dapat kita lihat dari flamegraph,
fdatasync () menghabiskan banyak waktu mengirimkan bio dalam fungsi
generic_make_request () . Jadi, akar masalah kita adalah di suatu tempat di luar daemon ceph. Mungkin masalah kernel, bcache, atau disk. Output iostat menunjukkan latensi tinggi untuk perangkat bcache.
Temuan mencurigakan lainnya adalah bahwa daemon systemd-udevd mengkonsumsi CPU; sekitar 20% pada banyak CPU. Ini perilaku aneh, jadi kita harus mencari tahu apa yang terjadi. Karena systemd-udevd bekerja dengan uevents, kita harus menggunakan
monitor udevadm untuk mengetahui apakah ada uevents di sistem. Setelah memeriksa, kami melihat bahwa banyak "perubahan" yang dihasilkan untuk setiap perangkat blok dalam sistem.
Ini tidak biasa, jadi kami akan mencari tahu apa yang menyebabkan semua ini dikirim.
Menggunakan BCC Toolkit
Seperti yang sudah kita ketahui, kernel (dan ceph daemon) menghabiskan banyak waktu untuk menjalankan fungsi
generic_make_requst () . Mari kita mengukur latensi menggunakan
funclatency dari
BCC toolkit , hanya untuk memastikan bahwa kita berada di jalur yang benar. Kami akan melacak PID daemon ceph (argumen -p) dalam interval 1 detik (-i) dan mencetak latensi dalam milidetik (-m).
Fungsi ini biasanya bekerja sangat cepat. Yang dilakukannya hanyalah mengirimkan struktur bio ke antrian driver perangkat.
Bcache adalah perangkat yang kompleks; sebenarnya, ini terdiri dari 3 perangkat: perangkat pendukung, yang merupakan HDD lambat dalam kasus kami; perangkat caching, yang merupakan partisi drive NVMe; dan perangkat virtual bcache, yang digunakan oleh aplikasi. Kami tahu pengiriman lambat, tetapi untuk perangkat apa? Ini adalah sesuatu yang akan kita lihat nanti.
Untuk saat ini, kita tahu bahwa uevents menyebabkan masalah pada daemon ceph dan kita harus menemukan perangkat lunak yang memicu uevents. Tidak mudah untuk menemukan apa yang menyebabkan uevents dihasilkan. Kami menganggap itu perangkat lunak yang hanya berjalan secara berkala. Untuk melihat apa yang sedang dieksekusi pada sistem, kami menggunakan
execsnoop dari BCC toolkit. Kita dapat menjalankannya dan mengarahkan
stdout ke file.
Sebagai contoh:
/usr/share/bcc/tools/execsnoop | tee ./execdump
Kami tidak akan memberikan output execsnoop penuh di sini, tetapi satu string menarik yang kami temukan di sana adalah:
sh 1764905 5802 0 sudo arcconf getconfig 1 AD | grep Temperature | awk -F '[:/]' '{print $2}' | sed 's/^ \([0-9]*\) C.*/\1/'
Kolom ketiga adalah PPID proses. Kami memeriksa apa itu 5802 dan melihat bahwa itu adalah salah satu utas daemon pemantauan kami. Melihat lebih jauh pada konfigurasi sistem pemantauan, kami menemukan parameter yang salah. Suhu HBA sedang diambil setiap 30 detik, yang terlalu sering. Setelah mengubah interval pemeriksaan ke nilai yang lebih tepat, kami melihat bahwa latensi kami cocok dengan platform lain.
Tetapi kita masih tidak tahu mengapa latensi bcache tinggi. Kami menyiapkan platform pengujian dengan konfigurasi yang sama dan mencoba mereproduksi masalah dengan fio pada perangkat bcache sementara secara bersamaan memicu udev dengan perintah memicu udevadm.
Menulis Alat Berbasis BCC
Apa yang akan kita lakukan di sini adalah menulis alat sederhana yang melacak panggilan generic_make_request () paling lambat dan mencetak nama disk yang dipanggil untuk fungsi tersebut.
Rencananya sederhana:
- Daftarkan kprobe di generic_make_request () :
- Simpan nama disk yang tersedia dari argumen fungsi
- Simpan cap waktu saat ini
- Daftarkan kretprobe di pernyataan pengembalian generic_make_request () :
- Ambil stempel waktu saat ini
- Cari stempel waktu yang disimpan sebelumnya dan bandingkan dengan yang sekarang
- Jika hasilnya lebih tinggi dari ambang, cari nama disk yang disimpan sebelumnya dan cetak ke terminal dengan informasi tambahan
Kprobes dan
kretprobes menggunakan breakpoint untuk mengubah kode fungsi saat runtime. Anda dapat menemukan
dokumentasi serta
artikel bagus tentang ini. Jika Anda melihat kode untuk
alat BCC yang berbeda, Anda akan melihat bahwa mereka semua memiliki struktur yang sama. Kami akan melewatkan argumen penguraian dan fokus pada program BPF itu sendiri.
Teks program kami akan didefinisikan dalam python sebagai berikut:
bpf_text = βββ
Program BPF menggunakan
hashmaps untuk berbagi data antara berbagai fungsi. Kami akan menggunakan PID sebagai struktur kunci dan yang didefinisikan sendiri sebagai nilai.
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);
Di sini kita mendaftarkan hashmap yang disebut
p dengan
tipe kunci
u64 dan tipe nilai
struct data_t . Peta ini dapat diakses dari konteks program BPF kami. Makro
BPF_PERF_OUTPUT mendaftarkan peta lain yang disebut
peristiwa , yang digunakan untuk
mendorong data ke ruang pengguna.
Saat mengukur latensi antara panggilan fungsi dan pengembaliannya atau antara satu panggilan fungsi dan yang lain, Anda harus yakin bahwa data yang Anda simpan dan akses nanti terkait dengan konteks yang sama. Dengan kata lain, Anda harus mengetahui eksekusi paralel lain dari fungsi yang sama. Dimungkinkan untuk melacak latensi antara pemanggilan fungsi dari satu proses dan pengembalian fungsi yang sama dari proses lain, tetapi ini tidak membantu kami. Contoh yang baik adalah
alat biolatensi di mana pointer ke
permintaan struct digunakan sebagai kunci hashmap.
Selanjutnya, kita harus menulis kode yang akan dieksekusi pada panggilan fungsi melalui mekanisme kprobe:
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); }
Di sini kita memiliki
argumen generic_make_request () pertama sebagai argumen kedua fungsi kita. Kemudian kita mendapatkan PID dan cap waktu saat ini dalam nanodetik dan menulisnya ke
data data_t struct yang baru dialokasikan. Kami mendapatkan nama disk dari struktur bio, yang diteruskan ke
generic_make_request () , dan menyimpannya ke
data kami. Langkah terakhir adalah menambahkan entri ke hashmap yang kami jelaskan sebelumnya.
Fungsi ini akan dieksekusi pada
generic_make_request () mengembalikan:
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); } }
Kami mendapatkan PID dan cap waktu dari output sebelumnya dan mencari peta hash untuk nilai di mana
kunci == PID saat ini . Jika ditemukan, kami mendapatkan nama proses yang sedang berjalan dan menambahkannya ke struktur
data . Apa yang kita lakukan dengan
data-> pid di sini memberi kita id grup utas. Fungsi
bpf_get_current_pid_tgid () yang sebelumnya disebut mengembalikan thread GID dan PID dari proses dalam nilai 64-bit yang sama.
Kami tidak tertarik pada setiap ID utas, tetapi kami ingin mengetahui PID utas utama. Setelah memeriksa bahwa latensi berada di atas ambang batas, kami mengirimkan struktur
data kami ke userspace melalui peta
peristiwa , lalu menghapus entri hashmap di bagian akhir.
Dalam skrip python kami, kami harus mengganti
MIN_US dan
FACTOR sesuai dengan ambang yang kami inginkan dan satuan waktu yang ingin kami lihat dalam hasil kami:
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"
Maka kita harus menyiapkan program BPF dengan
makro BPF () dan mendaftarkan 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")
Kita juga perlu mendefinisikan struktur yang sama dengan
struct data_t dalam skrip kami untuk membaca data dari program BPF:
TASK_COMM_LEN = 16
Langkah terakhir adalah mencetak data yang kita inginkan:
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)
Script lengkap tersedia di
GitHub . Mari kita jalankan skrip dan memicu peristiwa udev sementara fio menulis ke perangkat bcache:
Sukses! Sekarang kita melihat bahwa apa yang tampak seperti latensi tinggi untuk bcache adalah
generic_make_request () latensi untuk perangkat pendukungnya.
Gali ke dalam kernel
Apa yang diseret saat mengirimkan permintaan? Kita melihat bahwa lonjakan latensi terjadi sebelum permintaan akuntansi dimulai. Ini dapat dengan mudah diperiksa dengan menjalankan iostat selama masalah atau
skrip BCC biolatensi , yang didasarkan pada permulaan permintaan akuntansi, sehingga tidak ada alat yang akan menunjukkan masalah disk.
Jika kita melihat
generic_make_request () , kita melihat bahwa ada dua fungsi yang berjalan sebelum akuntansi dimulai. Yang pertama adalah
generic_make_request_checks () , yang ringan dan memeriksa bio sesuai dengan pengaturan perangkat, dll. Yang kedua adalah
blk_queue_enter () , yang memiliki panggilan
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));
Di sini kernel menunggu sampai antrian tidak beku. Mari kita mengukur latensi blk_queue_enter ():
~
Sepertinya kita sudah dekat. Fungsi yang digunakan untuk membekukan / mencairkan antrian adalah
blk_mq_freeze_queue dan
blk_mq_unfreeze_queue . Mereka digunakan untuk mengubah pengaturan antrian, yang dapat memengaruhi permintaan io yang saat ini dalam penerbangan. Ketika
blk_mq_freeze_queue () dipanggil,
q-> mq_freeze_depth ditambahkan di
blk_freeze_queue_start () . Setelah itu, kernel menunggu antrian menjadi kosong di
blk_mq_freeze_queue_wait () .
Waktu tunggu ini sama dengan latensi disk, karena kernel harus menunggu semua operasi io selesai. Ketika antrian kosong, perubahan dapat dilakukan. Langkah terakhir adalah memanggil
blk_mq_unfreeze_queue () , yang menurunkan penghitung
freeze_depth .
Sekarang kita cukup tahu untuk memperbaiki masalah ini. Perintah memicu udevadm mengubah pengaturan untuk perangkat blok. Pengaturan tersebut dijelaskan dalam aturan udev. Kita dapat mengetahui pengaturan apa yang membekukan antrian dengan mengubahnya melalui sysfs atau dengan melihat kode sumber kernel. Atau, kita dapat
memanggil jejak dari BCC toolkit untuk mencetak kernel dan tumpukan pengguna untuk setiap panggilan
blk_freeze_queue :
~
Aturan Udev tidak sering berubah, bahkan menetapkan nilai yang sudah ada ke parameter tertentu menyebabkan lonjakan pengiriman latensi untuk aplikasi. Tentu saja menghasilkan peristiwa udev ketika tidak ada perubahan dalam konfigurasi perangkat (tidak ada perangkat yang terpasang atau terlepas) bukanlah praktik yang baik. Namun, kita dapat mencegah kernel membekukan antrian jika tidak ada alasan untuk melakukan ini.
Tiga komitmen kecil memperbaiki masalah ini.
Kesimpulan
eBPF adalah instrumen yang sangat fleksibel dan kuat. Dalam artikel ini, kami hanya melihat satu kasus dan menunjukkan sedikit kemampuannya. Jika Anda tertarik untuk mengembangkan alat berbasis BCC, Anda harus melihat pada
tutorial resmi , yang menjelaskan konsep dasarnya.
Ada juga alat berbasis eBPF menarik lainnya yang tersedia untuk profil dan debugging. Salah satunya adalah
bpftrace , yang memungkinkan Anda menulis oneliner yang kuat dan program-program kecil dalam bahasa yang mirip bahasa awk. Yang lainnya adalah
ebpf_exporter , yang dapat mengumpulkan metrik resolusi tinggi tingkat rendah ke server prometheus Anda dengan visualisasi yang hebat dan kemampuan mengingatkan.