Setiap orang memiliki buku favorit mereka tentang sihir. Seseorang memiliki Tolkien, seseorang memiliki Pratchett, seseorang, seperti saya, memiliki Max Fry. Hari ini saya akan memberi tahu Anda tentang keajaiban TI favorit saya - tentang BPF dan infrastruktur modern di sekitarnya.
BPF berada di puncaknya sekarang. Teknologi ini berkembang pesat, menembus ke tempat-tempat yang paling tidak terduga dan menjadi semakin mudah diakses oleh pengguna rata-rata. Di hampir setiap konferensi populer hari ini, Anda dapat mendengar laporan tentang topik ini, dan GopherCon Rusia tidak terkecuali: Saya menyajikan versi teks laporan saya .
Tidak akan ada penemuan unik dalam artikel ini. Saya hanya akan mencoba menunjukkan kepada Anda apa itu BPF, apa yang dapat dilakukannya, dan bagaimana BPF dapat membantu Anda secara pribadi. Kami juga akan melihat fitur terkait Go.
Setelah membaca artikel saya, saya ingin agar mata Anda bersinar dengan cara yang sama seperti mata seorang anak yang membaca buku Harry Potter untuk pertama kalinya menyala, sehingga Anda pulang atau bekerja dan mencoba βmainanβ baru dalam aksi.
Apa itu eBPF?
Jadi, sihir macam apa yang akan diceritakan oleh pria berjanggut berusia 34 tahun dengan mata terbakar?
Kami tinggal bersamamu di tahun 2020. Jika Anda membuka Twitter, Anda akan membaca tweet dari para pria pemarah yang mengklaim bahwa perangkat lunak tersebut sekarang ditulis dengan kualitas yang sangat buruk sehingga lebih mudah untuk membuang semuanya dan memulai kembali. Beberapa bahkan mengancam untuk meninggalkan profesinya, karena mereka tidak tahan lagi: semuanya terus-menerus rusak, tidak nyaman, lambat.
Mungkin mereka benar: tanpa seribu komentar, kami tidak akan tahu. Tapi yang pasti saya setuju adalah bahwa tumpukan perangkat lunak modern lebih kompleks dari sebelumnya.
BIOS, EFI, sistem operasi, driver, modul, perpustakaan, jaringan, database, cache, orkestrator seperti K8s, kontainer seperti Docker, akhirnya, perangkat lunak kami dengan runtime dan pengumpul sampah. Seorang profesional sejati dapat menjawab pertanyaan tentang apa yang terjadi setelah Anda mengetik ya.ru di browser Anda selama beberapa hari.
Sangat sulit untuk memahami apa yang terjadi di sistem Anda, terutama jika ada yang tidak beres saat ini dan Anda kehilangan uang. Masalah ini telah menyebabkan munculnya lini bisnis yang dirancang untuk membantu Anda memahami apa yang terjadi di dalam sistem Anda. Perusahaan besar memiliki seluruh departemen Sherlock yang tahu di mana harus memalu dan mur mana yang harus dikencangkan untuk menghemat jutaan dolar.
Dalam wawancara saya sering bertanya kepada orang-orang bagaimana mereka akan men-debug masalah jika mereka bangun jam empat pagi.
Salah satu pendekatannya adalah menganalisis log . Tetapi masalahnya adalah hanya mereka yang dimasukkan oleh pengembang ke dalam sistemnya yang tersedia. Mereka tidak fleksibel.
Pendekatan populer kedua adalah mempelajari metrik . Tiga sistem paling populer untuk bekerja dengan metrik ditulis dalam Go. Metrik sangat berguna, tetapi tidak selalu membantu Anda memahami penyebabnya dengan memungkinkan Anda melihat gejalanya.
Pendekatan ketiga yang mendapatkan popularitas adalah apa yang disebut observabilitas: kemampuan untuk mengajukan pertanyaan kompleks yang sewenang-wenang tentang perilaku sistem dan mendapatkan jawabannya. Karena pertanyaan bisa sangat kompleks, jawabannya mungkin memerlukan banyak variasi informasi, dan sampai pertanyaan itu diajukan, kita tidak tahu apa. Ini berarti fleksibilitas sangat penting untuk observasi.
Berikan kemampuan untuk mengubah tingkat penebangan dengan cepat? Terhubung dengan debugger ke program yang sedang berjalan dan melakukan sesuatu di sana tanpa mengganggu pekerjaannya? Pahami permintaan apa yang masuk ke sistem, visualisasikan sumber permintaan lambat, lihat memori apa yang dihabiskan melalui pprof, dan dapatkan grafik perubahannya dari waktu ke waktu? Ukur latensi satu fungsi dan ketergantungan latensi pada argumen? Semua pendekatan ini saya akan mengacu pada observabilitas. Ini adalah seperangkat utilitas, pendekatan, pengetahuan, pengalaman, yang bersama-sama akan memberi Anda kesempatan untuk melakukan, jika bukan segalanya, maka banyak "keuntungan", tepat di sistem kerja. Pisau IT Swiss modern.
Tapi bagaimana ini bisa dilakukan? Ada dan banyak instrumen di pasaran: sederhana, kompleks, berbahaya, lambat. Tapi topik artikel hari ini adalah BPF.
Kernel Linux digerakkan oleh peristiwa. Hampir semua yang terjadi di kernel, dan di sistem secara keseluruhan, dapat direpresentasikan sebagai sekumpulan peristiwa. Interupsi adalah peristiwa, menerima paket melalui jaringan adalah peristiwa, transfer prosesor ke proses lain adalah peristiwa, peluncuran fungsi adalah peristiwa.
Jadi, BPF adalah subsistem kernel Linux yang memungkinkan untuk menulis program kecil yang akan diluncurkan oleh kernel sebagai respons terhadap peristiwa. Program-program ini dapat menjelaskan apa yang terjadi di sistem Anda dan mengontrolnya.
Itu adalah perkenalan yang sangat panjang. Mari kita lebih dekat dengan kenyataan.
1994 melihat versi pertama dari BPF, yang mungkin pernah Anda temui saat menulis aturan sederhana untuk utilitas tcpdump untuk melihat atau mengendus paket jaringan. tcpdump dapat menyetel "filter" untuk melihat tidak semua, tetapi hanya paket yang Anda minati. Misalnya, "hanya protokol tcp dan hanya port 80". Untuk setiap paket yang lewat, sebuah fungsi dijalankan untuk memutuskan apakah akan menyimpan paket tertentu atau tidak. Mungkin ada banyak paket, yang berarti fungsi kita harus sangat cepat. Filter tcpdump kami baru saja diubah menjadi fungsi BPF, contohnya ditunjukkan pada gambar di bawah.
Filter sederhana untuk tcpdump disajikan sebagai program BPF
BPF asli adalah mesin virtual yang sangat sederhana dengan beberapa register. Namun, bagaimanapun, BPF secara signifikan mempercepat pemfilteran paket jaringan. Itu adalah langkah maju yang besar pada masanya.
Pada 2014, Alexey Starovoitov memperluas fungsionalitas BPF. Dia meningkatkan jumlah register dan ukuran program yang diizinkan, menambahkan kompilasi JIT dan membuat pemverifikasi yang memeriksa keamanan program. Tetapi hal yang paling mengesankan adalah bahwa program BPF baru dapat diluncurkan tidak hanya saat memproses paket, tetapi juga sebagai respons terhadap banyak kejadian kernel, dan meneruskan informasi bolak-balik antara kernel dan ruang pengguna.
Perubahan ini membuka jalan bagi kasus penggunaan baru untuk BPF. Beberapa hal yang sebelumnya dilakukan dengan menulis modul kernel yang kompleks dan berbahaya kini relatif mudah dilakukan melalui BPF. Kenapa ini keren? Karena kesalahan apapun saat menulis modul seringkali menimbulkan kepanikan. Bukan untuk kepanikan Go-shnoy yang lembut, tetapi untuk kepanikan kernel, setelah itu - hanya reboot.
Rata-rata pengguna Linux sekarang memiliki kekuatan super untuk dilihat, sebelumnya hanya tersedia untuk pengembang kernel hardcore atau siapa pun. Opsi ini sebanding dengan kemampuan untuk menulis program dengan mudah untuk iOS atau Android: pada ponsel lama itu tidak mungkin atau jauh lebih sulit.
Versi baru BPF dari Alexey disebut eBPF (dari kata diperpanjang - diperpanjang). Tetapi sekarang ini telah menggantikan semua versi lama BPF dan telah menjadi sangat populer sehingga semua orang menyebutnya BPF untuk kesederhanaan.
Dimana BPF digunakan?
Jadi apa peristiwa, atau pemicu ini, yang mana program BPF dapat dilampirkan, dan bagaimana orang mulai memanfaatkan kekuatan yang baru ditemukan ini?
Saat ini ada dua kelompok besar pemicu.
Kelompok pertama digunakan untuk memproses paket jaringan dan untuk mengatur lalu lintas jaringan. Ini adalah XDP, acara kontrol lalu lintas dan beberapa lainnya.
Acara ini diperlukan untuk:
- , . Cloudflare Facebook BPF- DDoS-. ( BPF- ), . .
- , , β , , . . Facebook, , , .
- Bangun penyeimbang yang cerdas. Contoh paling menonjol adalah proyek Cilium , yang paling sering digunakan di cluster K8s sebagai jaringan mesh. Cilium mengelola lalu lintas: menyeimbangkan, mengalihkan, dan menganalisisnya. Dan semua ini dilakukan dengan bantuan program BPF kecil yang diluncurkan oleh kernel sebagai respons terhadap satu atau beberapa peristiwa yang terkait dengan paket atau soket jaringan.
Ini adalah kelompok pemicu pertama yang terkait dengan masalah jaringan dengan kemampuan untuk memengaruhi perilaku. Kelompok kedua terkait dengan observabilitas yang lebih umum; Program dari kelompok ini paling sering tidak memiliki kemampuan untuk mempengaruhi sesuatu, tetapi hanya dapat "mengamati". Dia lebih menarik minat saya.
Grup ini berisi pemicu seperti:
- perf events β , Linux- perf: , , minor/major- . . , , , - . , , , , .
- tracepoints β ( ) , (, ). , β , , , , . - , tracepoints :
- ;
- , ;
- API, , , , , API.
, , , , , pprof .
- ;
- USDT β , tracepoints, user space-. . : MySQL, , PHP, Python. enable-dtrace . , Go . -, , DTrace . , , Solaris: , , GC -, .
Nah, kemudian tingkat sihir lain dimulai:
- ftrace trigger memberi kita kemampuan untuk menjalankan program BPF di awal hampir semua fungsi kernel. Sangat dinamis. Ini berarti kernel akan memanggil fungsi BPF Anda sebelum menjalankan fungsi kernel apa pun yang Anda pilih. Atau semua fungsi kernel - apa pun. Anda dapat melampirkan ke semua fungsi kernel dan mendapatkan visualisasi yang bagus dari semua panggilan di keluaran.
- kprobes / uprobes memberikan hal yang hampir sama dengan ftrace, hanya kami yang memiliki kemampuan untuk memasang ke sembarang tempat saat menjalankan suatu fungsi, baik di kernel maupun di ruang pengguna. Di tengah fungsi ada semacam if pada variabel dan Anda perlu memplot histogram dari nilai variabel ini? Bukan masalah.
- kretprobes/uretprobes β , user space. , , . , , PID fork.
Hal yang paling luar biasa tentang semua ini, saya ulangi, adalah bahwa, dipanggil pada salah satu pemicu ini, program BPF kami dapat melihat sekeliling: baca argumen fungsi, waktunya, baca variabel, variabel global, ambil jejak tumpukan, simpan itu kemudian untuk nanti, transfer data ke ruang pengguna untuk diproses, dapatkan data dari ruang pengguna untuk pemfilteran atau beberapa perintah kontrol. Kecantikan!
Saya tidak tahu tentang Anda, tapi bagi saya infrastruktur baru itu seperti mainan yang sudah lama saya nantikan dengan cemas.
API, atau Cara menggunakannya
Oke, Marco, Anda membujuk kami untuk melihat ke BPF. Tapi bagaimana cara mendekatinya?
Mari kita lihat apa saja program BPF dan bagaimana cara berinteraksi dengannya.
Pertama, kami memiliki program BPF yang, jika diverifikasi, akan dimuat ke kernel. Di sana JIT akan dikompilasi menjadi kode mesin dan dijalankan dalam mode kernel ketika pemicunya dipasang ke aktif.
Program BPF memiliki kemampuan untuk berinteraksi dengan bagian kedua - program ruang pengguna. Ada dua cara untuk melakukannya. Kita dapat menulis ke buffer melingkar, dan bagian ruang pengguna dapat membacanya. Kita juga dapat menulis dan membaca dalam penyimpanan-nilai-kunci, yang disebut peta BPF, dan bagian ruang pengguna, masing-masing, dapat melakukan hal yang sama, dan, karenanya, mereka dapat mentransfer beberapa informasi satu sama lain.
Jalan yang lurus
Cara termudah untuk bekerja dengan BPF, yang tidak perlu Anda mulai, adalah menulis program BPF yang mirip dengan bahasa C dan mengkompilasi kode ini menggunakan kompiler Clang ke dalam kode mesin virtual. Kami kemudian memuat kode ini menggunakan panggilan sistem BPF secara langsung dan berinteraksi dengan program BPF kami juga menggunakan panggilan sistem BPF.
Penyederhanaan pertama yang tersedia adalah menggunakan pustaka libbpf, yang disertakan dengan sumber kernel dan memungkinkan Anda untuk tidak bekerja secara langsung dengan panggilan sistem BPF. Bahkan, ini menyediakan pembungkus yang nyaman untuk memuat kode, bekerja dengan apa yang disebut peta untuk mentransfer data dari kernel ke ruang pengguna dan sebaliknya.
bcc
Jelas bahwa penggunaan semacam itu jauh dari ramah manusia. Untungnya, di bawah merek iovizor, proyek BCC muncul, yang sangat menyederhanakan hidup kita.
Faktanya, ini mempersiapkan seluruh lingkungan perakitan dan memberi kita kesempatan untuk menulis program BPF tunggal, di mana bagian C akan dirakit dan dimuat ke dalam kernel secara otomatis, dan bagian ruang pengguna dapat dilakukan dengan Python yang sederhana dan mudah dipahami.
bpftrace.dll
Tetapi BCC terlihat rumit untuk banyak hal. Untuk beberapa alasan, orang khususnya tidak suka menulis bagian dalam C.
Orang yang sama dari iovizor memperkenalkan alat bpftrace, yang memungkinkan Anda untuk menulis skrip BPF dalam bahasa skrip sederhana ala AWK (atau umumnya satu baris).
Pakar kinerja dan observasi terkenal Brendan Gregg menyiapkan visualisasi berikut tentang cara yang tersedia untuk bekerja dengan BPF: Secara
vertikal, kami memiliki kesederhanaan alat, dan secara horizontal, kekuatannya. Dapat dilihat bahwa BCC adalah alat yang sangat kuat, tetapi tidak super sederhana. bpftrace jauh lebih sederhana, tetapi kurang kuat.
Contoh penggunaan BPF
Tapi mari kita lihat kemampuan magis yang telah tersedia bagi kita, dengan contoh spesifik.
Baik BCC dan bpftrace berisi folder Alat, yang berisi sejumlah besar skrip menarik dan berguna yang siap pakai. Mereka juga merupakan Stack Overflow lokal tempat Anda dapat menyalin potongan kode untuk skrip Anda.
Misalnya, berikut ini skrip yang menunjukkan latensi untuk kueri DNS:
ββmarko@marko-home ~
β°β$ sudo gethostlatency-bpfcc
TIME PID COMM LATms HOST
16:27:32 21417 DNS Res~ver #93 3.97 live.github.com
16:27:33 22055 cupsd 7.28 NPI86DDEE.local
16:27:33 15580 DNS Res~ver #87 0.40 github.githubassets.com
16:27:33 15777 DNS Res~ver #89 0.54 github.githubassets.com
16:27:33 21417 DNS Res~ver #93 0.35 live.github.com
16:27:42 15580 DNS Res~ver #87 5.61 ac.duckduckgo.com
16:27:42 15777 DNS Res~ver #89 3.81 www.facebook.com
16:27:42 15777 DNS Res~ver #89 3.76 tech.badoo.com :-)
16:27:43 21417 DNS Res~ver #93 3.89 static.xx.fbcdn.net
16:27:43 15580 DNS Res~ver #87 3.76 scontent-frt3-2.xx.fbcdn.net
16:27:43 15777 DNS Res~ver #89 3.50 scontent-frx5-1.xx.fbcdn.net
16:27:43 21417 DNS Res~ver #93 4.98 scontent-frt3-1.xx.fbcdn.net
16:27:44 15580 DNS Res~ver #87 5.53 edge-chat.facebook.com
16:27:44 15777 DNS Res~ver #89 0.24 edge-chat.facebook.com
16:27:44 22099 cupsd 7.28 NPI86DDEE.local
16:27:45 15580 DNS Res~ver #87 3.85 safebrowsing.googleapis.com
^C%
Utilitas ini menunjukkan waktu eksekusi kueri DNS secara real time, sehingga Anda dapat menangkap, misalnya, beberapa pencilan yang tidak terduga.
Dan ini adalah skrip yang "memata-matai" apa yang diketik orang lain di terminal mereka:
ββmarko@marko-home ~
β°β$ sudo bashreadline-bpfcc
TIME PID COMMAND
16:51:42 24309 uname -a
16:52:03 24309 rm -rf src/badoo
Jenis skrip ini dapat digunakan untuk menangkap tetangga yang buruk atau mengaudit keamanan server perusahaan.
Skrip untuk melihat panggilan aliran bahasa tingkat tinggi:
ββmarko@marko-home ~/tmp
β°β$ sudo /usr/sbin/lib/uflow -l python 20590
Tracing method calls in python process 20590... Ctrl-C to quit.
CPU PID TID TIME(us) METHOD
5 20590 20590 0.173 -> helloworld.py.hello
5 20590 20590 0.173 -> helloworld.py.world
5 20590 20590 0.173 <- helloworld.py.world
5 20590 20590 0.173 <- helloworld.py.hello
5 20590 20590 1.174 -> helloworld.py.hello
5 20590 20590 1.174 -> helloworld.py.world
5 20590 20590 1.174 <- helloworld.py.world
5 20590 20590 1.174 <- helloworld.py.hello
5 20590 20590 2.175 -> helloworld.py.hello
5 20590 20590 2.176 -> helloworld.py.world
5 20590 20590 2.176 <- helloworld.py.world
5 20590 20590 2.176 <- helloworld.py.hello
6 20590 20590 3.176 -> helloworld.py.hello
6 20590 20590 3.176 -> helloworld.py.world
6 20590 20590 3.176 <- helloworld.py.world
6 20590 20590 3.176 <- helloworld.py.hello
6 20590 20590 4.177 -> helloworld.py.hello
6 20590 20590 4.177 -> helloworld.py.world
6 20590 20590 4.177 <- helloworld.py.world
6 20590 20590 4.177 <- helloworld.py.hello
^C%
Contoh ini menunjukkan tumpukan panggilan program Python.
Brendan Gregg yang sama membuat gambar di mana dia mengumpulkan semua skrip yang ada dengan panah yang menunjukkan subsistem yang memungkinkan setiap utilitas untuk "mengamati". Seperti yang Anda lihat, kami sudah memiliki sejumlah besar utilitas siap pakai yang tersedia - untuk hampir semua kesempatan.
Jangan coba-coba melihat sesuatu di sini. Gambar tersebut digunakan sebagai referensi
Bagaimana dengan kita dengan Go?
Sekarang mari kita bicara tentang Go. Kami memiliki dua pertanyaan utama:
- Bisakah Anda menulis program BPF di Go?
- Apakah mungkin untuk mem-parsing program yang ditulis dalam Go?
Ayo pergi secara berurutan.
Sampai saat ini, satu-satunya kompiler yang dapat dikompilasi ke dalam format yang dimengerti oleh mesin BPF adalah Clang. Kompiler populer lainnya, GCC, belum memiliki backend BPF. Dan satu-satunya bahasa pemrograman yang dapat dikompilasi ke BPF adalah versi C yang sangat terbatas.
Namun, program BPF memiliki bagian kedua, yaitu di ruang pengguna. Dan itu bisa ditulis dalam Go.
Seperti yang saya sebutkan di atas, BCC memungkinkan Anda untuk menulis bagian ini dengan Python, yang merupakan bahasa utama alat tersebut. Pada saat yang sama, di repositori utama, BCC juga mendukung Lua dan C ++, dan di repositori pihak ketiga, BCC juga mendukung Go .
Program semacam itu terlihat persis sama dengan program Python. Pada awalnya ada garis dimana program BPF di C, kemudian kita beri tahu di mana harus melampirkan program ini, dan entah bagaimana berinteraksi dengannya, misalnya kita mendapatkan data dari peta EPF.
Sebenarnya itu saja. Anda dapat melihat contohnya lebih detail di Github .
Mungkin kelemahan utamanya adalah perpustakaan C libbcc atau libbpf digunakan untuk bekerja, dan membangun program Go dengan perpustakaan seperti itu sama sekali tidak terlihat seperti jalan yang bagus di taman.
Selain iovisor / gobpf, saya menemukan tiga proyek terkini yang memungkinkan Anda menulis bagian userland di Go.
- https://github.com/dropbox/goebpf
- https://github.com/cilium/ebpf
- https://github.com/andrewkroh/go-ebpf
Versi Dropbox tidak memerlukan pustaka C apa pun, tetapi Anda harus membuat sendiri bagian kernel dari program BPF menggunakan Clang dan kemudian memuatnya ke dalam kernel dengan program Go.
Versi Cilium memiliki fitur yang sama dengan versi Dropbox. Tapi patut disebutkan, jika hanya karena dilakukan oleh orang-orang dari proyek Cilium, yang berarti pasti akan sukses.
Saya membawa proyek ketiga untuk kelengkapan gambar. Seperti dua sebelumnya, ia tidak memiliki ketergantungan C eksternal, membutuhkan perakitan manual program BPF C, tetapi tampaknya tidak menjanjikan banyak.
Sebenarnya, ada pertanyaan lain: mengapa harus menulis program BPF di Go? Lagi pula, jika Anda melihat BCC atau bpftrace, program BPF biasanya mengambil kurang dari 500 baris kode. Bukankah lebih mudah untuk menulis skrip dalam bahasa-bpftrace atau menemukan sedikit Python? Saya melihat dua alasan di sini.
Pertama, Anda sangat menyukai Go dan lebih suka melakukan semua hal di dalamnya. Selain itu, program Go yang berpotensi lebih mudah dipindahkan dari mesin ke mesin: tautan statis, biner sederhana, dan seterusnya. Tapi semuanya masih jauh dari jelas, karena kita terikat pada inti tertentu. Saya akan berhenti di sini, jika tidak, artikel saya akan mencapai 50 halaman lagi.
Opsi kedua: Anda tidak menulis skrip sederhana, tetapi sistem berskala besar yang juga menggunakan BPF secara internal. Saya bahkan memiliki contoh sistem seperti itu di Go :
Proyek Scope terlihat seperti satu biner yang, ketika diluncurkan di infrastruktur K8s atau cloud lainnya, menganalisis semua yang terjadi di sekitar, dan menunjukkan apa itu container, layanan, bagaimana mereka berinteraksi, dll. Dan banyak dari ini dilakukan dengan menggunakan BPF. Proyek yang menarik.
Menganalisis program Go
Jika Anda ingat, kami memiliki satu pertanyaan lagi: dapatkah kami menganalisis program yang ditulis di Go menggunakan BPF? Pikiran pertama - tentu saja! Apa perbedaannya dalam bahasa apa program itu ditulis? Bagaimanapun, ini hanyalah kode yang dikompilasi, seperti semua program lain, menghitung sesuatu pada prosesor, memakan memori seolah-olah tidak menjadi dirinya sendiri, berinteraksi dengan perangkat keras melalui kernel, dan dengan kernel melalui panggilan sistem. Pada prinsipnya, ini benar, tetapi ada fitur dengan tingkat kesulitan yang berbeda.
Mengoper argumen
Salah satu fiturnya adalah Go tidak menggunakan ABI seperti kebanyakan bahasa lain. Kebetulan para founding fathers memutuskan untuk mengambil ABI dari sistem Plan 9 , yang mereka kenal dengan baik.
ABI seperti API, perjanjian interoperabilitas, hanya pada tingkat bit, byte, dan kode mesin.
Elemen ABI utama yang menarik minat kami adalah bagaimana argumennya diteruskan ke fungsi dan bagaimana responsnya dikembalikan dari fungsi. Sedangkan ABI x86-64 standar menggunakan register prosesor untuk meneruskan argumen dan respons, ABI Plan 9 menggunakan tumpukan untuk ini.
Rob Pike dan timnya tidak berencana membuat standar lain: mereka sudah memiliki kompiler C yang hampir siap pakai untuk sistem Plan 9, sesederhana dua-dua, yang dengan cepat mereka ubah menjadi kompiler untuk Go. Pendekatan teknik dalam tindakan.
Namun sebenarnya ini bukanlah masalah yang sangat kritis. Pertama, kita akan segera melihat di Go melewati argumen melalui register , dan kedua, mendapatkan argumen dari stack dari BPF tidaklah sulit: alias sargX telah ditambahkan ke bpftrace , dan hal yang sama akan muncul di BCC , kemungkinan besar dalam waktu dekat ...
Pembaruan : dari saat saya membuat laporan, bahkan proposal resmi terperinci untuk transisi ke penggunaan register di ABI muncul.
Pengenal utas unik
Fitur kedua berkaitan dengan fitur favorit Go, goroutine. Salah satu cara untuk mengukur latensi suatu fungsi adalah dengan menghemat waktu yang diperlukan untuk memanggil fungsi, waktu untuk keluar dari fungsi, dan menghitung perbedaannya; dan simpan waktu mulai dengan kunci yang berisi nama fungsi dan TID (nomor utas). Nomor utas diperlukan, karena fungsi yang sama dapat dipanggil secara bersamaan oleh program berbeda atau utas berbeda dari program yang sama.
Namun di Go, goroutine berjalan di antara utas sistem: sekarang goroutine dijalankan pada satu utas, dan kemudian di utas lainnya. Dan dalam kasus Go, kami tidak akan memasukkan TID di kunci, tetapi GID, yaitu ID dari goroutine, tetapi kami tidak bisa mendapatkannya. Secara teknis, ID ini ada. Anda bahkan dapat menariknya keluar dengan peretasan kotor, karena ini ada di suatu tempat di tumpukan, tetapi melakukan ini sangat dilarang oleh rekomendasi dari grup pengembangan Go utama. Mereka merasa bahwa kami tidak akan pernah membutuhkan informasi seperti itu. Serta penyimpanan lokal Goroutine, tapi saya ngelantur.
Memperluas tumpukan
Masalah ketiga adalah yang paling serius. Begitu serius bahkan jika kita entah bagaimana memecahkan masalah kedua, itu tidak akan membantu kita dengan cara apa pun untuk mengukur latensi fungsi Go.
Mungkin sebagian besar pembaca memahami dengan baik apa itu stack. Tumpukan yang sama, di mana, berbeda dengan heap atau heap, Anda dapat mengalokasikan memori untuk variabel dan tidak berpikir untuk membebaskannya.
Jika kita berbicara tentang C, maka tumpukan di sana memiliki ukuran tetap. Jika kita melampaui ukuran tetap ini, stack overflow yang terkenal akan terjadi .
Di Go, tumpukan bersifat dinamis. Dalam versi yang lebih lama, itu adalah potongan memori yang digabungkan. Sekarang potongan berukuran dinamis terus menerus. Ini berarti bahwa jika potongan yang dipilih tidak cukup bagi kami, kami akan memperluas yang sekarang. Dan jika kami tidak dapat memperluas, maka kami memilih yang lain yang lebih besar dan memindahkan semua data dari tempat lama ke tempat baru. Ini adalah cerita yang sangat menarik yang menyentuh tentang jaminan keamanan, cgo, pengumpul sampah, tapi itu topik untuk artikel lain.
Penting untuk diketahui bahwa agar Go memindahkan stack, ia perlu menjalankan stack panggilan program, semua penunjuk pada stack.
Di sinilah letak masalah utamanya: uretprobes, yang digunakan untuk memasang fungsi BPF, secara dinamis mengubah tumpukan di akhir eksekusi fungsi untuk menyebariskan panggilan ke penangannya, yang disebut trampolin. Dan perubahan seperti itu dalam tumpukannya, tidak terduga untuk Go, dalam banyak kasus diakhiri dengan crash program. Ups!
Namun, kisah ini tidaklah unik. Pembuka "stack" C ++ juga mengalami error satu kali pada saat penanganan pengecualian.
Tidak ada solusi untuk masalah ini. Seperti biasa dalam kasus seperti itu, para pihak saling bertukar argumen yang sangat masuk akal tentang kesalahan masing-masing.
Namun jika Anda benar-benar perlu memasang uretprobe, maka masalah tersebut bisa dielakkan. Bagaimana? Jangan menaruh uretprobe. Kita bisa memasang uprobe di semua tempat di mana kita keluar dari fungsi. Mungkin ada satu tempat seperti itu, atau mungkin 50.
Dan di sini keunikan Go bermain di tangan kami.
Biasanya, trik seperti itu tidak akan berhasil. Kompilator yang cukup cerdas dapat melakukan apa yang disebut pengoptimalan panggilan ekor , ketika alih-alih kembali dari suatu fungsi dan kembali sepanjang tumpukan panggilan, kita cukup melompat ke awal fungsi berikutnya. Pengoptimalan semacam ini sangat penting untuk bahasa fungsional seperti Haskell . Tanpa itu, mereka tidak bisa mengambil langkah tanpa stack overflow. Tetapi dengan pengoptimalan seperti itu, kami tidak dapat menemukan semua tempat di mana kami kembali dari fungsi tersebut.
Keunikannya adalah kompilator Go versi 1.14 belum dapat melakukan optimasi tail call. Ini berarti trik melampirkan ke semua keluar eksplisit dari suatu fungsi berfungsi, meskipun sangat membosankan.
Contoh dari
Jangan berpikir BPF tidak berguna untuk Go. Ini jauh dari kasus: kita dapat melakukan segala sesuatu yang tidak mempengaruhi nuansa di atas. Dan kita akan.
Mari kita lihat beberapa contoh.
Mari kita ambil program sederhana untuk persiapan. Pada dasarnya ini adalah server web yang mendengarkan pada port 8080 dan memiliki penangan permintaan HTTP. Penangan akan mendapatkan parameter nama, parameter Go dari URL dan melakukan semacam pemeriksaan "situs", dan kemudian mengirim ketiga variabel (nama, tahun dan status pemeriksaan) ke fungsi preparedAnswer (), yang akan menyiapkan tanggapan sebagai string.
Validasi situs adalah permintaan HTTP yang memeriksa apakah situs konferensi sudah aktif dan berjalan menggunakan pipa dan goroutine. Dan fungsi menyiapkan respons hanya mengubah semuanya menjadi string yang dapat dibaca.
Kami akan memicu program kami dengan permintaan curl sederhana:
Sebagai contoh pertama, kami akan menggunakan bpftrace untuk mencetak semua panggilan fungsi dari program kami. Kami melampirkan di sini untuk semua fungsi yang termasuk dalam main. Di Go, semua fungsi Anda memiliki simbol yang terlihat seperti nama paket-titik-nama fungsi. Paket kita adalah main, dan runtime fungsinya adalah runtime.
Ketika saya melakukan curl, handler, fungsi validasi situs, dan subfungsi goroutine diluncurkan, dan kemudian fungsi persiapan respons. Kelas!
Selanjutnya, saya ingin tidak hanya menampilkan fungsi apa yang sedang dijalankan, tetapi juga argumennya. Mari kita ambil fungsi preparedAnswer (). Dia memiliki tiga argumen. Mari kita coba mencetak dua int.
Kami mengambil bpftrace, sekarang bukan hanya satu baris, tetapi sebuah skrip. Kami melampirkan ke fungsi kami dan menggunakan alias untuk argumen tumpukan yang saya sebutkan.
Dalam output, kita melihat apa yang kita lewati pada tahun 2020, mendapat status 200, dan melewati 2021 satu kali.
Tetapi fungsinya memiliki tiga argumen. Yang pertama adalah string. Bagaimana dengan dia?
Mari kita cetak semua argumen tumpukan dari 0 sampai 4. Dan apa yang kita lihat? Beberapa angka besar, beberapa lebih kecil, dan tahun 2021 dan 200 kami yang lama. Apa angka-angka aneh ini pada awalnya?
Di sinilah berguna untuk mengetahui perangkat Go. Jika di C sebuah string hanyalah sebuah array karakter yang diakhiri dengan null, maka di Go sebuah string sebenarnya adalah struktur yang terdiri dari penunjuk ke sebuah array karakter (omong-omong, tidak diakhiri null) dan panjang.
Tetapi kompiler Go, ketika melewatkan sebuah string sebagai argumen, memperluas struktur ini dan meneruskannya sebagai dua argumen. Dan ternyata digit aneh pertama hanyalah penunjuk ke array kita, dan yang kedua adalah panjangnya.
Dan kebenarannya: panjang string yang diharapkan adalah 22.
Oleh karena itu, kami memperbaiki skrip kami sedikit untuk mendapatkan dua nilai ini melalui tumpukan register pointer dan offset yang benar, dan menggunakan fungsi bawaan str (), kami menampilkannya sebagai string. Semuanya berfungsi:
Baiklah, mari kita lihat runtime. Misalnya, saya ingin tahu goroutine apa yang diluncurkan program kami. Saya tahu bahwa goroutine dipicu oleh fungsi newproc () dan newproc1 (). Mari terhubung dengan mereka. Argumen pertama ke fungsi newproc1 () adalah penunjuk ke struktur funcval, yang hanya memiliki satu bidang - penunjuk fungsi:
Dalam kasus ini, kami akan menggunakan kesempatan untuk mendefinisikan struktur secara langsung di skrip. Ini sedikit lebih mudah daripada bermain dengan set offset. Di sini kami telah mengeluarkan semua goroutine yang diluncurkan saat penangan kami dipanggil. Dan jika setelah itu kita mendapatkan nama-nama simbol untuk offset kita, maka di antara mereka kita akan melihat fungsi checkSite kita. Hore!
Contoh-contoh ini adalah setetes lautan kemampuan BPF, BCC, dan bpftrace. Dengan pengetahuan yang tepat tentang internal dan pengalaman, Anda bisa mendapatkan hampir semua informasi dari program yang sedang berjalan tanpa menghentikan atau mengubahnya.
Kesimpulan
Hanya itu yang ingin saya ceritakan. Saya harap saya bisa menginspirasi Anda.
BPF adalah salah satu tren paling trendi dan paling menjanjikan di Linux. Dan saya yakin di tahun-tahun mendatang kita akan melihat lebih banyak hal menarik tidak hanya dalam teknologi itu sendiri, tetapi juga pada alat dan distribusinya.
Sebelum terlambat dan tidak semua orang tahu tentang BPF, bermainlah dengannya, jadilah pesulap, selesaikan masalah, dan bantu kolega Anda. Mereka mengatakan bahwa trik sulap hanya bekerja sekali.
Adapun Go, seperti biasa, kami cukup unik. Kami selalu memiliki beberapa nuansa: entah kompilernya berbeda, lalu ABI, kami memerlukan semacam GOPATH, nama yang tidak boleh Google. Tapi kita telah menjadi kekuatan yang harus diperhitungkan, dan saya percaya bahwa hidup hanya akan menjadi lebih baik.