Saya sangat berminat dengan minat saya dalam pembangunan perisian, khususnya teka-teki mencipta sistem perisian secara ergonomik yang menyelesaikan set masalah yang paling luas sambil membuat sedikit kompromi yang mungkin. Saya juga suka menganggap diri saya sebagai pembangun sistem, yang, menurut definisi Andrew Kelley, bermaksud pembangun yang berminat untuk memahami sepenuhnya sistem yang mereka gunakan. Dalam blog ini saya berkongsi dengan anda idea saya untuk menyelesaikan masalah berikut: Membina aplikasi perusahaan tindanan penuh yang boleh dipercayai dan berprestasi. Agak mencabar, bukan? Dalam blog saya menumpukan pada bahagian "pelayan web berprestasi" - di situlah saya rasa saya boleh menawarkan perspektif yang segar, kerana selebihnya sama ada terpengaruh, atau saya tiada apa-apa untuk ditambah.
Kaveat utama - akan ada tiada sampel kod, saya sebenarnya belum menguji ini. Ya, ini adalah kelemahan utama, tetapi sebenarnya melaksanakan ini akan mengambil banyak masa, yang saya tidak ada, dan antara menerbitkan blog yang cacat dan tidak menerbitkannya sama sekali, saya tetap dengan yang pertama. Anda telah diberi amaran.
Dan bahagian apakah yang akan kami kumpulkan aplikasi kami?
Dengan alat kami diputuskan, mari mulakan!
Zig tidak mempunyai sokongan tahap bahasa untuk coroutine :( Dan coroutine ialah apa yang digunakan oleh setiap pelayan web yang berprestasi. Jadi, tiada gunanya mencuba?
Tunggu, mari kita pakai topi pengaturcara sistem kita dahulu. Coroutine bukan peluru perak, tiada apa-apa. Apakah faedah dan kelemahan sebenar yang terlibat?
Adalah pengetahuan umum bahawa coroutine (benang ruang pengguna) lebih ringan dan lebih pantas. Tetapi dengan cara apa sebenarnya? (jawapan di sini sebahagian besarnya adalah spekulasi, ambil dengan sebutir garam dan uji sendiri)
Waktu jalan Go, contohnya, memultiplekskan goroutin ke urutan OS. Benang berkongsi jadual halaman, serta sumber lain yang dimiliki oleh proses. Jika kami memperkenalkan pengasingan dan pertalian CPU kepada campuran - benang akan terus berjalan pada teras CPU masing-masing, semua struktur data OS akan kekal dalam ingatan tanpa perlu ditukar, penjadual ruang pengguna akan memperuntukkan masa CPU untuk goroutin dengan ketepatan, kerana ia menggunakan model multitasking koperasi. Adakah persaingan boleh dilakukan?
Kemenangan prestasi dicapai dengan mengenepikan abstraksi peringkat OS bagi benang, dan menggantikannya dengan goroutine. Tetapi adakah tiada apa-apa yang hilang dalam terjemahan?
Saya akan berhujah bahawa abstraksi peringkat OS "sebenar" untuk unit pelaksanaan bebas bukanlah satu utas pun - ia sebenarnya proses OS. Sebenarnya, perbezaan di sini tidak begitu jelas - semua yang membezakan benang dan proses ialah nilai PID dan TID yang berbeza. Bagi deskriptor fail, memori maya, pengendali isyarat, sumber yang dijejaki - sama ada ini berasingan untuk kanak-kanak dinyatakan dalam hujah kepada syscall "klon". Oleh itu, saya akan menggunakan istilah "proses" untuk bermaksud rangkaian pelaksanaan yang memiliki sumber sistemnya sendiri - terutamanya masa cpu, memori, deskriptor fail terbuka.
Sekarang mengapa ini penting? Setiap unit pelaksanaan mempunyai permintaan sendiri untuk sumber sistem. Setiap tugas yang kompleks boleh dipecahkan kepada unit, di mana setiap satu boleh membuat permintaan sendiri, boleh diramal, untuk sumber - memori dan masa CPU. Dan semakin jauh ke atas pokok subtugasan anda pergi, ke arah tugas yang lebih umum - graf sumber sistem membentuk lengkung loceng dengan ekor panjang. Dan menjadi tanggungjawab anda untuk memastikan bahawa ekor tidak melebihi had sumber sistem. Tetapi bagaimanakah perkara itu dilakukan, dan apakah yang berlaku jika had itu sebenarnya melebihi?
Jika kita menggunakan model satu proses dan banyak coroutine untuk tugasan bebas, apabila satu coroutine melebihi had ingatan - kerana penggunaan memori dijejaki pada tahap proses, keseluruhan proses dimatikan. Itulah dalam kes terbaik - jika anda menggunakan cgroup (yang secara automatik berlaku untuk pod dalam Kubernetes, yang mempunyai cgroup setiap pod) - seluruh cgroup terbunuh. Membuat sistem yang boleh dipercayai memerlukan ini untuk diambil kira. Dan bagaimana pula dengan masa CPU? Jika perkhidmatan kami mendapat banyak permintaan intensif pengiraan pada masa yang sama, ia akan menjadi tidak bertindak balas. Kemudian tarikh akhir, pembatalan, percubaan semula, dimulakan semula mengikuti.
Satu-satunya cara yang realistik untuk menangani senario ini untuk kebanyakan susunan perisian arus perdana ialah meninggalkan "gemuk" dalam sistem - beberapa sumber yang tidak digunakan untuk hujung lengkung loceng - dan mengehadkan bilangan permintaan serentak - yang, sekali lagi, mendahului kepada sumber yang tidak digunakan. Dan walaupun dengan itu, kami akan menyebabkan OOM terbunuh atau tidak bertindak balas sekali-sekala - termasuk untuk permintaan "tidak bersalah" yang berlaku dalam proses yang sama dengan outlier. Kompromi ini boleh diterima oleh ramai orang, dan menyediakan sistem perisian dalam amalan dengan cukup baik. Tetapi bolehkah kita berbuat lebih baik?
Memandangkan penggunaan sumber dijejaki setiap proses, idealnya kami akan menghasilkan proses baharu untuk setiap unit pelaksanaan yang kecil dan boleh diramal. Kemudian kami menetapkan ulimit untuk masa dan memori cpu - dan kami bersedia untuk pergi! ulimit mempunyai had lembut dan keras, yang akan membolehkan proses ditamatkan dengan anggun apabila mencapai had lembut, dan jika itu tidak berlaku, mungkin disebabkan oleh pepijat - ditamatkan secara paksa apabila mencapai had keras. Malangnya, pembiakan proses baharu pada Linux adalah perlahan, proses baharu bagi setiap permintaan tidak disokong untuk banyak rangka kerja web, serta sistem lain seperti Temporal. Selain itu, penukaran proses adalah lebih mahal - yang dikurangkan oleh penyematan CoW dan cpu, tetapi masih tidak sesuai. Malangnya, proses yang berjalan lama adalah realiti yang tidak dapat dielakkan.
Semakin jauh kita pergi dari abstraksi bersih proses jangka pendek, semakin banyak kerja peringkat OS yang kita perlukan untuk menjaga diri kita sendiri. Tetapi terdapat juga faedah yang boleh diperoleh - seperti menggunakan io_uring untuk menyusun IO antara banyak utas pelaksanaan. Sebenarnya, jika tugas besar terdiri daripada sub-tugas - adakah kita benar-benar mengambil berat tentang penggunaan sumber individu mereka? Hanya untuk profiling. Tetapi jika untuk tugas besar kita boleh menguruskan (memotong) ekor keluk loceng sumber, itu sudah cukup baik. Oleh itu, kami boleh menghasilkan seberapa banyak proses seperti permintaan yang ingin kami kendalikan secara serentak, memastikannya bertahan lama dan hanya menyesuaikan semula ulimit untuk setiap permintaan baharu. Jadi apabila permintaan mengatasi kekangan sumbernya, ia mendapat isyarat OS dan dapat ditamatkan dengan baik, tidak menjejaskan permintaan lain. Atau, jika penggunaan sumber yang tinggi itu disengajakan, kami boleh memberitahu pelanggan untuk membayar kuota sumber yang lebih tinggi. Bunyi agak bagus untuk saya.
Tetapi prestasi masih akan terjejas, berbanding pendekatan coroutine-per-permintaan. Pertama, menyalin sekitar jadual memori proses adalah mahal. Oleh kerana jadual mengandungi rujukan kepada halaman ingatan, kami boleh menggunakan halaman besar, dengan itu mengehadkan saiz data untuk disalin. Ini hanya boleh dilakukan secara langsung dengan bahasa peringkat rendah, seperti Zig. Selain itu, multitasking peringkat OS adalah preemptive dan tidak kooperatif, yang akan sentiasa kurang cekap. Atau adakah ia?
Terdapat syscall sched_yield, yang membenarkan utas untuk melepaskan CPU apabila ia telah menyelesaikan bahagian kerjanya. Nampak agak kerjasama. Bolehkah terdapat cara untuk meminta kepingan masa saiz tertentu juga? Sebenarnya, ada - dengan dasar penjadualan SCHED_DEADLINE. Ini ialah dasar masa nyata, yang bermaksud bahawa untuk potongan masa CPU yang diminta, urutan berjalan tanpa gangguan. Tetapi jika hirisan telah ditakluki - preemption bermula, dan benang anda ditukar dan tidak diberi keutamaan. Dan jika kepingan itu kurang dijalankan - utas boleh memanggil sched_yield untuk menandakan penamat awal, membenarkan utas lain dijalankan. Itu kelihatan seperti yang terbaik dari kedua-dua dunia - model koperasi dan preemtif.
Penghadan ialah hakikat bahawa utas SCHED_DEADLINE tidak boleh bercabang. Ini memberikan kita dua model untuk konkurensi - sama ada proses setiap permintaan, yang menetapkan tarikh akhir untuk dirinya sendiri, dan menjalankan gelung peristiwa untuk IO yang cekap, atau proses yang dari mula menghasilkan benang untuk setiap tugasan mikro, yang setiap satunya menetapkan tarikh akhir sendiri, dan menggunakan baris gilir untuk komunikasi antara satu sama lain. Yang pertama adalah lebih mudah, tetapi memerlukan gelung peristiwa dalam ruang pengguna, yang kedua lebih banyak menggunakan kernel.
Kedua-dua strategi mencapai matlamat yang sama seperti model coroutine - dengan bekerjasama dengan kernel, tugas aplikasi boleh dijalankan dengan gangguan yang minimum.
Ini semua untuk bahagian berprestasi tinggi, kependaman rendah, tahap rendah, tempat Zig bersinar. Tetapi apabila ia datang kepada perniagaan sebenar aplikasi, fleksibiliti jauh lebih berharga daripada kependaman. Jika proses melibatkan orang sebenar yang menandatangani dokumen - kependaman komputer adalah diabaikan. Selain itu, walaupun mengalami penderitaan dalam prestasi, bahasa berorientasikan objek memberikan pembangun primitif yang lebih baik untuk memodelkan domain perniagaan dengannya. Dan pada hujung yang paling jauh ini, sistem seperti Flowable dan Camunda membenarkan kakitangan pengurusan dan operasi memprogramkan logik perniagaan dengan lebih fleksibiliti dan halangan kemasukan yang lebih rendah. Bahasa seperti Zig tidak akan membantu dalam hal ini, dan hanya menghalang anda.
Python, sebaliknya, adalah salah satu bahasa paling dinamik yang ada. Kelas, objek - semuanya adalah kamus di bawah hud, dan boleh dimanipulasi pada masa jalan mengikut apa yang anda suka. Ini mempunyai penalti prestasi, tetapi menjadikan pemodelan perniagaan dengan kelas dan objek serta banyak helah pintar praktikal. Zig adalah kebalikan daripada itu - terdapat beberapa helah bijak secara sengaja dalam Zig, memberikan anda kawalan maksimum. Bolehkah kita menggabungkan kuasa mereka dengan meminta mereka saling beroperasi?
Memang kita boleh, kerana kedua-duanya menyokong C ABI. Kami boleh menjalankan penterjemah Python dari dalam proses Zig, dan bukan sebagai proses yang berasingan, mengurangkan overhed dalam kos masa jalan dan kod gam. Ini seterusnya membolehkan kami menggunakan pengagih tersuai Zig dalam Python - menetapkan arena untuk memproses permintaan individu, dengan itu mengurangkan jika tidak menghapuskan overhed pemungut sampah, dan menetapkan had memori. Had utama ialah benang pemijahan masa jalan CPython untuk pengumpulan sampah dan IO, tetapi saya tidak menemui bukti bahawa ia berlaku. Kami boleh menghubungkan Python ke dalam gelung acara tersuai dalam Zig, dengan penjejakan memori per-coroutine, dengan menggunakan medan "konteks" dalam AbstractMemoryLoop. Kemungkinannya tidak terhad.
Kami membincangkan kebaikan konkurensi, selari, dan pelbagai bentuk penyepaduan dengan kernel OS. Penerokaan tidak mempunyai tanda aras dan kod, yang saya harap ia dapat menampung kualiti idea yang ditawarkan. Pernahkah anda mencuba sesuatu yang serupa? Apakah pendapat anda? Maklum balas dialu-alukan :)
Atas ialah kandungan terperinci Pelayan Web yang berprestasi dan boleh dikembangkan dengan Zig dan Python. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!