Tantangan Menulis Aturan

7/0.9/2.5. Lihat sumber Nightly {/3/}

Halaman ini memberikan ringkasan umum tentang masalah dan tantangan spesifik dalam menulis aturan Bazel yang efisien.

Persyaratan Ringkasan

  • Asumsi: Bertujuan untuk Ketepatan, Throughput, Kemudahan Penggunaan, dan Latensi
  • Asumsi: Repositori Skala Besar
  • Asumsi: Bahasa Deskripsi mirip BANGUN
  • Historis: Pemisahan Sulit antara Pemuatan, Analisis, dan Eksekusi Usang, tetapi masih memengaruhi API
  • Intrinsik: Eksekusi Jarak Jauh dan Cache Sulit
  • Intrinsic: Penggunaan Informasi Perubahan untuk Build Inkremental yang Benar dan Cepat memerlukan Pola Coding yang Tidak Biasa
  • Intrinsik: Menghindari Waktu Kuadrat dan Konsumsi Memori itu Sulit

Asumsi

Berikut beberapa asumsi yang dibuat tentang sistem build, seperti kebutuhan akan ketepatan, kemudahan penggunaan, throughput, dan repositori skala besar. Bagian berikut membahas asumsi tersebut dan menawarkan panduan untuk memastikan aturan ditulis secara efektif.

Targetkan ketepatan, throughput, kemudahan penggunaan, dan latensi

Kami berasumsi bahwa sistem build harus diutamakan dan paling tepat dalam kaitannya dengan build inkremental. Untuk hierarki sumber tertentu, output dari build yang sama harus selalu sama, terlepas dari tampilan hierarki output-nya. Pada perkiraan pertama, ini berarti Bazel perlu mengetahui setiap input yang masuk ke langkah build tertentu, sehingga ia dapat menjalankan kembali langkah tersebut jika ada perubahan input. Ada batasan seberapa benar Bazel bisa mendapatkannya, karena Bazel membocorkan beberapa informasi seperti tanggal / waktu build, dan mengabaikan jenis perubahan tertentu seperti perubahan pada atribut file. Sandboxing membantu memastikan ketepatan dengan mencegah pembacaan ke file input yang tidak dideklarasikan. Selain batas intrinsik sistem, ada beberapa masalah ketepatan yang diketahui, yang sebagian besar terkait dengan Fileset atau aturan C++, yang keduanya merupakan masalah yang sulit. Kami memiliki upaya jangka panjang untuk memperbaikinya.

Tujuan kedua dari sistem build ini adalah memiliki throughput yang tinggi; kami secara permanen mendorong batas-batas apa 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.

Yang berikutnya adalah kemudahan penggunaan. Dari beberapa pendekatan yang tepat dengan footprint yang sama (atau serupa) pada layanan eksekusi jarak jauh, kami memilih cara 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, maupun pesan error bahwa file BUILD memiliki kesalahan ketik.

Perlu diperhatikan bahwa kedua sasaran ini sering kali tumpang tindih; latensi sama pentingnya dengan throughput layanan eksekusi jarak jauh, sama halnya dengan ketepatan yang relevan untuk kemudahan penggunaan.

Repositori skala besar

Sistem build harus beroperasi pada skala repositori besar dengan skala besar yang berarti tidak muat di satu hard drive, sehingga tidak mungkin melakukan checkout penuh pada hampir semua mesin developer. Build berukuran sedang harus membaca dan mengurai puluhan ribu file BUILD, serta mengevaluasi ratusan ribu glob. Meskipun secara teori memungkinkan untuk membaca semua file BUILD di satu komputer, kita belum dapat melakukannya dalam waktu dan memori yang cukup. Oleh karena itu, file BUILD harus dapat dimuat dan diuraikan secara terpisah.

Bahasa deskripsi yang mirip BANGUN

Dalam konteks ini, kami mengasumsikan bahasa konfigurasi yang kira-kira mirip dengan file BUILD dalam deklarasi library dan aturan biner serta interdependensinya. File BUILD dapat dibaca dan diuraikan secara terpisah, dan kita bahkan tidak perlu melihat file sumber kapan pun kita bisa (kecuali jika ada).

Bersejarah

Ada perbedaan antara versi Bazel yang menyebabkan tantangan dan beberapa di antaranya diuraikan di bagian berikut.

Pemisahan yang ketat antara pemuatan, analisis, dan eksekusi sudah usang, tetapi masih memengaruhi API

Secara teknis, aturan cukup mengetahui file input dan output suatu tindakan sebelum tindakan tersebut dikirim ke eksekusi jarak jauh. Namun, code base Bazel asli memiliki pemisahan paket pemuatan yang ketat, lalu menganalisis aturan menggunakan konfigurasi (pada dasarnya flag command line), dan baru setelah itu menjalankan tindakan apa pun. Perbedaan ini masih menjadi bagian dari aturan API saat ini, meskipun inti dari Bazel tidak lagi memerlukannya (detail selengkapnya di bawah).

Artinya, API aturan memerlukan deskripsi deklaratif dari antarmuka aturan (atribut yang dimilikinya, jenis atribut). Ada beberapa pengecualian saat API memungkinkan kode kustom dijalankan 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 bisa direferensikan dari aturan lain dalam grafik build.

Selain itu, analisis aturan tidak dapat membaca file sumber apa pun atau memeriksa output tindakan; sebagai gantinya, analisis aturan perlu membuat grafik bipartit terarah sebagian yang berisi langkah-langkah build dan nama file output yang hanya ditentukan dari aturan itu sendiri dan dependensinya.

Intrinsik

Ada beberapa properti intrinsik yang membuat aturan penulisan menjadi sulit dan beberapa yang paling umum dijelaskan di bagian berikut.

Eksekusi jarak jauh dan caching sulit

Eksekusi jarak jauh dan penyimpanan cache meningkatkan waktu build dalam repositori besar sekitar dua kali lipat dibandingkan dengan menjalankan build pada satu mesin. Akan tetapi, skala yang perlu dilakukannya sangat mengejutkan: Layanan eksekusi jarak jauh Google dirancang untuk menangani banyak permintaan per detik, dan protokol ini dengan hati-hati menghindari perjalanan bolak-balik yang tidak perlu serta pekerjaan yang tidak perlu di sisi layanan.

Saat ini, protokol mengharuskan sistem build mengetahui semua input untuk tindakan tertentu sebelumnya; sistem build kemudian menghitung sidik jari tindakan unik, dan meminta penjadwal untuk mendapatkan cache. Jika ditemukan cache ditemukan, penjadwal akan membalas dengan ringkasan file output; file itu sendiri akan ditangani oleh digest nanti. Namun, hal ini memberlakukan pembatasan pada aturan Bazel, yang harus mendeklarasikan semua file input terlebih dahulu.

Penggunaan informasi perubahan untuk build inkremental yang benar dan cepat memerlukan pola coding yang tidak biasa

Di atas, kami berpendapat bahwa agar benar, Bazel perlu mengetahui semua file input yang disertakan dalam langkah build untuk mendeteksi apakah langkah build tersebut masih yang terbaru atau tidak. Hal yang sama berlaku untuk pemuatan paket serta analisis aturan, dan kami telah mendesain Skyframe untuk menangani hal ini secara umum. Skyframe adalah library grafik dan framework evaluasi yang mengambil node sasaran (seperti 'build //foo with these options'), dan dipecah menjadi beberapa bagian konstituennya, yang kemudian dievaluasi dan digabungkan untuk memberikan hasil ini. Sebagai bagian dari proses ini, Skyframe membaca paket, menganalisis aturan, dan mengeksekusi tindakan.

Pada setiap node, Skyframe melacak dengan tepat node mana yang digunakan oleh node tertentu untuk menghitung outputnya sendiri, mulai dari node sasaran hingga file input (yang juga merupakan node Skyframe). Memiliki grafik ini secara eksplisit dalam memori memungkinkan sistem build mengidentifikasi dengan tepat node mana yang terpengaruh oleh perubahan yang diberikan pada file input (termasuk pembuatan atau penghapusan file input), sehingga melakukan pekerjaan minimal untuk memulihkan hierarki output ke status yang diinginkan.

Sebagai bagian dari 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 kita terikat pada penggunaan Java, jadi tidak ada thread ringan dan tidak ada kelanjutan).

Sebagai gantinya, Bazel menggunakan kumpulan thread berukuran tetap. Namun, hal ini berarti jika sebuah node mendeklarasikan dependensi yang belum tersedia, kita mungkin harus membatalkan evaluasi tersebut dan memulainya kembali (mungkin dalam thread lain), saat dependensi tersedia. Oleh karena itu, hal ini berarti node tidak boleh melakukan hal ini secara berlebihan; node yang mendeklarasikan dependensi N secara berurutan dapat dimulai ulang N kali, dengan biaya O(N^2). Sebagai gantinya, kami menargetkan deklarasi dependensi massal di awal, yang terkadang memerlukan pengaturan ulang kode, atau bahkan membagi node menjadi beberapa node untuk membatasi jumlah mulai ulang.

Perlu diperhatikan bahwa teknologi ini saat ini tidak tersedia di rules API; sebagai gantinya, API aturan masih ditentukan menggunakan konsep lama, yaitu fase pemuatan, analisis, dan eksekusi. Namun, batasan mendasarnya adalah semua akses ke node lain harus melalui framework agar dapat melacak dependensi yang terkait. Terlepas dari bahasa yang digunakan sistem build atau aturan yang ditulis (tidak harus sama ), penulis aturan tidak boleh menggunakan library standar atau pola yang mengabaikan Skyframe. Untuk Java, hal itu berarti menghindari java.io.File serta segala bentuk refleksi, dan library apa pun yang melakukan keduanya. Library yang mendukung injeksi dependensi antarmuka level rendah ini masih harus disiapkan dengan benar untuk Skyframe.

Hal ini sangat menyarankan untuk menghindari mengekspos penulis aturan ke runtime bahasa lengkap sejak awal. Risiko 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 waktu kuadrat dan konsumsi memori adalah hal yang sulit

Lebih buruk lagi, terlepas dari persyaratan yang diberlakukan oleh Skyframe, batasan historis penggunaan Java, dan sudah tidak berlakunya API aturan, yang secara tidak sengaja memasukkan waktu kuadrat atau konsumsi memori adalah masalah mendasar dalam sistem build apa pun berdasarkan library dan aturan biner. Ada dua pola yang sangat umum yang menyebabkan konsumsi memori kuadrat (dan oleh karena itu konsumsi waktu kuadrat).

  1. Rantai Aturan Library - Pertimbangkan kasus rantai aturan library A yang bergantung pada B, bergantung pada C, dan seterusnya. Selanjutnya, kita ingin menghitung beberapa properti selama penutupan transitif aturan ini, seperti classpath runtime Java, atau perintah penaut C++ untuk setiap library. Namun, kita mungkin mengambil implementasi daftar standar; namun, library ini sudah memperkenalkan konsumsi memori kuadrat: library pertama berisi satu entri di classpath, dua kedua, tiga entri, dan seterusnya, dengan total 1+2+3+...+N = O(N^2).

  2. Aturan Biner yang Bergantung pada Aturan Library yang Sama - Pertimbangkan kasus ketika kumpulan biner yang bergantung pada aturan library yang sama, misalnya jika Anda memiliki sejumlah aturan pengujian yang menguji kode library yang sama. Misalnya dari aturan N, setengah aturan tersebut adalah aturan biner, dan separuh aturan lainnya adalah aturan library. Sekarang, pertimbangkan bahwa setiap biner membuat salinan beberapa properti yang dihitung selama penutupan transitif aturan library, seperti classpath runtime Java, atau command line penaut C++. Misalnya, fungsi ini dapat memperluas representasi string command line dari tindakan link C++. N/2 salinan elemen N/2 adalah memori O(N^2).

Class koleksi kustom untuk menghindari kompleksitas kuadrat

Bazel sangat terpengaruh oleh kedua skenario tersebut, jadi kami memperkenalkan serangkaian class pengumpulan kustom yang secara efektif mengompresi informasi dalam memori dengan menghindari salinan di setiap langkah. Hampir semua struktur data ini telah menetapkan semantik, 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 terkait penggunaan depset, bukan yang digunakan sebelumnya.

Sayangnya, penggunaan depset tidak otomatis menyelesaikan semua masalah; secara khusus, bahkan hanya melakukan iterasi melalui depset di setiap aturan memperkenalkan kembali konsumsi waktu kuadrat. Secara internal, NestedSets juga memiliki beberapa metode bantuan untuk memfasilitasi interoperabilitas dengan class koleksi normal. Sayangnya, meneruskan NestedSets secara tidak sengaja ke salah satu metode ini menyebabkan perilaku penyalinan, dan memperkenalkan kembali konsumsi memori kuadrat.