Halaman ini memberikan ringkasan tingkat tinggi tentang masalah dan tantangan khusus dalam menulis aturan Bazel yang efisien.
Ringkasan Persyaratan
- Asumsi: Bertujuan untuk Akurasi, Throughput, Kemudahan Penggunaan &Latensi
- Asumsi: Repositori Skala Besar
- Asumsi: Bahasa Deskripsi seperti BUILD
- Historis: Pemisahan yang Sulit antara Pemuatan, Analisis, dan Eksekusi Sudah Ketinggalan Zaman, tetapi masih memengaruhi API
- Intrinsik: Eksekusi dan Caching Jarak Jauh Sulit
- Intrinsik: Menggunakan Informasi Perubahan untuk Build Inkremental yang Benar dan Cepat memerlukan Pola Pengkodean yang Tidak Biasa
- Intrinsik: Menghindari Konsumsi Waktu dan Memori Kuadrat Sulit
Asumsi
Berikut beberapa asumsi yang dibuat tentang sistem build, seperti kebutuhan akan akurasi, kemudahan penggunaan, throughput, dan repositori skala besar. Bagian berikut membahas asumsi ini dan menawarkan panduan untuk memastikan aturan ditulis secara efektif.
Bertujuan untuk akurasi, throughput, kemudahan penggunaan &latensi
Kami berasumsi bahwa sistem build harus benar terlebih dahulu dan terutama terkait build inkremental. Untuk hierarki sumber tertentu, output build yang sama harus selalu sama, terlepas dari tampilan hierarki output. Dalam perkiraan pertama, ini berarti Bazel harus mengetahui setiap input yang masuk ke langkah build tertentu, sehingga dapat menjalankan kembali langkah tersebut jika ada input yang berubah. Ada batasan seberapa akurat Bazel, karena Bazel membocorkan beberapa informasi seperti tanggal / waktu build, dan mengabaikan jenis perubahan tertentu seperti perubahan pada atribut file. Sandboxing membantu memastikan akurasi dengan mencegah pembacaan ke file input yang tidak dideklarasikan. Selain batasan intrinsik sistem, ada beberapa masalah akurasi yang diketahui, yang sebagian besar terkait dengan Fileset atau aturan C++, yang keduanya merupakan masalah yang sulit. Kami memiliki upaya jangka panjang untuk memperbaiki masalah ini.
Tujuan kedua dari sistem build adalah memiliki throughput yang tinggi; kami terus mendorong batas kemampuan yang dapat dilakukan dalam alokasi mesin saat ini untuk layanan eksekusi jarak jauh. Jika layanan eksekusi jarak jauh kelebihan beban, tidak ada yang dapat menyelesaikan pekerjaan.
Kemudahan penggunaan menjadi tujuan berikutnya. Dari beberapa pendekatan yang benar dengan jejak layanan eksekusi jarak jauh yang sama (atau serupa), kami memilih pendekatan yang lebih mudah digunakan.
Latensi menunjukkan waktu yang diperlukan dari memulai build hingga mendapatkan hasil yang diinginkan, baik itu log pengujian dari pengujian yang lulus atau gagal, atau pesan error bahwa file BUILD memiliki kesalahan ketik.
Perhatikan bahwa tujuan ini sering kali tumpang-tindih; latensi adalah fungsi throughput layanan eksekusi jarak jauh, seperti halnya akurasi yang relevan untuk kemudahan penggunaan.
Repositori skala besar
Sistem build harus beroperasi pada skala repositori besar yang berarti tidak muat di satu hard drive, sehingga tidak mungkin melakukan checkout penuh di hampir semua mesin developer. Build berukuran sedang perlu membaca dan mengurai puluhan ribu file BUILD, dan mengevaluasi ratusan ribu glob. Meskipun secara teoretis memungkinkan untuk membaca semua file BUILD di satu mesin, kami belum dapat melakukannya dalam jumlah waktu dan memori yang wajar. Oleh karena itu, file BUILD harus dapat dimuat dan diurai secara independen.
Bahasa deskripsi seperti BUILD
Dalam konteks ini, kami mengasumsikan bahasa konfigurasi yang kira-kira mirip dengan file BUILD dalam deklarasi aturan library dan biner serta interdependensinya. File BUILD dapat dibaca dan diurai secara independen, dan kami menghindari melihat file sumber jika memungkinkan (kecuali untuk keberadaan).
Historis
Ada perbedaan antara versi Bazel yang menyebabkan tantangan dan beberapa di antaranya diuraikan di bagian berikut.
Pemisahan yang sulit antara pemuatan, analisis, dan eksekusi sudah ketinggalan zaman, tetapi masih memengaruhi API
Secara teknis, aturan cukup mengetahui file input dan output tindakan tepat sebelum tindakan dikirim ke eksekusi jarak jauh. Namun, codebase Bazel asli memiliki pemisahan paket pemuatan yang ketat, lalu menganalisis aturan menggunakan konfigurasi (pada dasarnya, flag command line), dan baru kemudian menjalankan tindakan apa pun. Perbedaan ini masih menjadi bagian dari API aturan saat ini, meskipun inti Bazel tidak lagi memerlukannya (detail selengkapnya di bawah).
Artinya, API aturan memerlukan deskripsi deklaratif antarmuka aturan (atribut yang dimilikinya, jenis atribut). Ada beberapa pengecualian saat API memungkinkan kode kustom berjalan selama fase pemuatan untuk menghitung nama implisit file output dan nilai implisit atribut. Misalnya, aturan java_library bernama 'foo' secara implisit menghasilkan output bernama 'libfoo.jar', yang dapat direferensikan dari aturan lain dalam grafik build.
Selain itu, analisis aturan tidak dapat membaca file sumber atau memeriksa output tindakan; sebagai gantinya, analisis aturan perlu menghasilkan grafik bipartit terarah parsial dari langkah-langkah build dan nama file output yang hanya ditentukan dari aturan itu sendiri dan dependensinya.
Intrinsik
Ada beberapa properti intrinsik yang membuat penulisan aturan menjadi sulit dan beberapa properti yang paling umum dijelaskan di bagian berikut.
Eksekusi dan caching jarak jauh sulit
Eksekusi dan caching jarak jauh meningkatkan waktu build di repositori besar sekitar dua kali lipat dibandingkan dengan menjalankan build di satu mesin. Namun, skala yang harus dilakukan sangat mencengangkan: Layanan eksekusi jarak jauh Google dirancang untuk menangani sejumlah besar permintaan per detik, dan protokol dengan cermat menghindari round trip yang tidak perlu serta pekerjaan yang tidak perlu di sisi layanan.
Saat ini, protokol mengharuskan sistem build mengetahui semua input ke tindakan tertentu sebelumnya; sistem build kemudian menghitung sidik jari tindakan unik, dan meminta hit cache penjadwal. Jika hit cache ditemukan, penjadwal akan membalas dengan ringkasan file output; file itu sendiri akan ditangani berdasarkan ringkasan nanti. Namun, hal ini memberlakukan batasan pada aturan Bazel, yang perlu mendeklarasikan semua file input sebelumnya.
Menggunakan informasi perubahan untuk build inkremental yang benar dan cepat memerlukan pola pengkodean yang tidak biasa
Di atas, kami berpendapat bahwa agar benar, Bazel harus mengetahui semua file input yang masuk ke langkah build untuk mendeteksi apakah langkah build tersebut masih terbaru. Hal yang sama berlaku untuk pemuatan paket dan analisis aturan, dan kami telah mendesain Skyframe untuk menangani hal ini secara umum. Skyframe adalah library grafik dan framework evaluasi yang mengambil node tujuan (seperti 'build //foo dengan opsi ini'), dan membaginya menjadi bagian-bagian konstituennya, yang kemudian dievaluasi dan digabungkan untuk menghasilkan hasil ini. Sebagai bagian dari proses ini, Skyframe membaca paket, menganalisis aturan, dan menjalankan tindakan.
Di setiap node, Skyframe melacak dengan tepat node mana yang digunakan node tertentu untuk menghitung outputnya sendiri, mulai dari node tujuan hingga file input (yang juga merupakan node Skyframe). Dengan grafik ini yang direpresentasikan secara eksplisit dalam memori, sistem build dapat mengidentifikasi dengan tepat node mana yang terpengaruh oleh perubahan tertentu pada file input (termasuk pembuatan atau penghapusan file input), sehingga melakukan jumlah pekerjaan minimal untuk memulihkan hierarki output ke status yang diinginkan.
Sebagai bagian dari hal ini, setiap node melakukan proses penemuan dependensi. Setiap node dapat mendeklarasikan dependensi, lalu menggunakan konten dependensi tersebut untuk mendeklarasikan dependensi lebih lanjut. Pada prinsipnya, hal ini dipetakan dengan baik ke model thread per node. Namun, build berukuran sedang berisi ratusan ribu node Skyframe, yang tidak mudah dilakukan dengan teknologi Java saat ini (dan karena alasan historis, saat ini kami terikat untuk menggunakan Java, sehingga tidak ada thread ringan dan tidak ada kelanjutan).
Sebagai gantinya, Bazel menggunakan kumpulan thread berukuran tetap. Namun, hal itu berarti bahwa jika node mendeklarasikan dependensi yang belum tersedia, kita mungkin harus membatalkan evaluasi tersebut dan memulai ulang (mungkin di thread lain), saat dependensi tersedia. Hal ini, pada gilirannya, berarti bahwa node tidak boleh melakukan hal ini secara berlebihan; node yang mendeklarasikan N dependensi secara serial berpotensi dimulai ulang N kali, sehingga memerlukan waktu O(N^2). Sebagai gantinya, kami bertujuan untuk deklarasi dependensi massal di awal, yang terkadang memerlukan penataan ulang kode, atau bahkan membagi node menjadi beberapa node untuk membatasi jumlah memulai ulang.
Perhatikan bahwa teknologi ini saat ini tidak tersedia di API aturan; sebagai gantinya, API aturan masih ditentukan menggunakan konsep lama dari fase pemuatan, analisis, dan eksekusi. Namun, batasan mendasar adalah semua akses ke node lain harus melalui framework sehingga dapat melacak dependensi yang sesuai. Terlepas dari bahasa yang digunakan untuk menerapkan sistem build atau bahasa yang digunakan untuk menulis aturan (tidak harus sama), penulis aturan tidak boleh menggunakan library atau pola standar yang melewati Skyframe. Untuk Java, hal itu berarti menghindari java.io.File serta segala bentuk refleksi, dan library apa pun yang melakukan salah satunya. Library yang mendukung injeksi dependensi antarmuka tingkat rendah ini masih perlu disiapkan dengan benar untuk Skyframe.
Hal ini sangat menyarankan untuk menghindari mengekspos penulis aturan ke runtime bahasa lengkap sejak awal. Bahaya penggunaan API tersebut secara tidak sengaja terlalu besar - beberapa bug Bazel di masa lalu disebabkan oleh aturan yang menggunakan API yang tidak aman, meskipun aturan tersebut ditulis oleh tim Bazel atau pakar domain lainnya.
Menghindari konsumsi waktu dan memori kuadrat sulit
Lebih buruk lagi, selain persyaratan yang diberlakukan oleh Skyframe, batasan historis penggunaan Java, dan aturan API yang sudah ketinggalan zaman, pengenalan konsumsi waktu atau memori kuadrat secara tidak sengaja adalah masalah mendasar dalam sistem build apa pun yang didasarkan pada aturan library dan biner. Ada dua pola yang sangat umum yang memperkenalkan konsumsi memori kuadrat (dan oleh karena itu konsumsi waktu kuadrat).
Rantai Aturan Library - Pertimbangkan kasus rantai aturan library A bergantung pada B, bergantung pada C, dan seterusnya. Kemudian, kita ingin menghitung beberapa properti di atas penutupan transitif aturan ini, seperti classpath runtime Java, atau perintah linker C++ untuk setiap library. Secara naif, kita mungkin menggunakan implementasi daftar standar; namun, hal ini sudah memperkenalkan konsumsi memori kuadrat: library pertama berisi satu entri di classpath, yang kedua dua, yang ketiga tiga, dan seterusnya, dengan total 1+2+3+...+N = O(N^2) entri.
Aturan Biner yang Bergantung pada Aturan Library yang Sama - Pertimbangkan kasus saat sekumpulan biner yang bergantung pada aturan library yang sama — seperti jika Anda memiliki sejumlah aturan pengujian yang menguji kode library yang sama. Katakanlah dari N aturan, setengah aturan adalah aturan biner, dan setengah lainnya adalah aturan library. Sekarang pertimbangkan bahwa setiap biner membuat salinan beberapa properti yang dihitung di atas penutupan transitif aturan library, seperti classpath runtime Java, atau command line linker C++. Misalnya, hal ini dapat memperluas representasi string command line dari tindakan link C++. N/2 salinan dari N/2 elemen adalah memori O(N^2).
Class koleksi kustom untuk menghindari kompleksitas kuadrat
Bazel sangat terpengaruh oleh kedua skenario ini, jadi kami memperkenalkan sekumpulan class koleksi kustom yang secara efektif mengompresi informasi dalam memori dengan menghindari salinan di setiap langkah. Hampir semua struktur data ini memiliki semantik yang ditetapkan, jadi kami menyebutnya
depset
(juga dikenal sebagai NestedSet dalam implementasi internal). Sebagian besar perubahan untuk mengurangi konsumsi memori Bazel selama beberapa tahun terakhir adalah perubahan untuk menggunakan depset, bukan apa pun yang sebelumnya digunakan.
Sayangnya, penggunaan depset tidak secara otomatis menyelesaikan semua masalah; khususnya, bahkan hanya melakukan iterasi atas depset di setiap aturan akan memperkenalkan kembali konsumsi waktu kuadrat. Secara internal, NestedSets juga memiliki beberapa metode helper untuk memfasilitasi interoperabilitas dengan class koleksi normal; sayangnya, meneruskan NestedSet secara tidak sengaja ke salah satu metode ini akan menyebabkan perilaku penyalinan, dan memperkenalkan kembali konsumsi memori kuadrat.