Manajemen Ketergantungan

Laporkan masalah Lihat sumber Per Malam · 7,4 kami. 7.3 · 7.2 · 7.1 · 7.0 · 6.5

Saat melihat halaman sebelumnya, satu tema berulang: mengelola kode Anda sendiri cukup mudah, tetapi mengelola dependensinya akan lebih sulit. Ada berbagai macam ketergantungan: terkadang ada dependensi pada tugas (seperti “dorong dokumentasi sebelum saya menandai rilis sebagai lengkap”), dan terkadang ada ketergantungan pada suatu artefak (seperti “Saya perlu memiliki library computer vision versi terbaru untuk membuat kode saya”). Terkadang, Anda memiliki dependensi internal pada bagian lain codebase Anda, dan terkadang Anda memiliki ketergantungan eksternal pada kode atau data yang dimiliki oleh tim lain (baik di organisasi Anda maupun pihak ketiga). Tapi bagaimanapun juga, gagasan tentang “Saya memerlukan itu sebelum saya dapat memiliki ini” adalah sesuatu yang berulang kali dalam sistem build, dan mengelola dependensi mungkin yang paling dasar sistem build.

Menangani Modul dan Dependensi

Project yang menggunakan sistem build berbasis artefak seperti Bazel dipecah menjadi satu set modul, dengan modul yang mengekspresikan dependensi satu sama lain melalui BUILD . Pengaturan modul dan dependensi ini yang tepat dapat memberikan dampak besar pada performa sistem build dan jumlah pekerjaan yang diperlukan untuk mempertahankannya.

Menggunakan Modul yang Lebih Mendetail dan Aturan 1:1:1

Pertanyaan pertama yang muncul saat menyusun build berbasis artefak adalah menentukan jumlah fungsi yang harus dicakup oleh setiap modul. Di Bazel, modul direpresentasikan oleh target yang menentukan unit yang dapat di-build seperti java_library atau go_binary. Pada satu ekstrem, seluruh proyek bisa yang ada dalam satu modul dengan meletakkan satu file BUILD di root dan secara rekursif menggabungkan semua file sumber proyek itu. Di sisi lain, hampir setiap file sumber dapat dibuat menjadi modulnya sendiri, yang secara efektif memerlukan setiap file untuk mencantumkan setiap file lain yang menjadi dependensinya dalam file BUILD.

Sebagian besar project berada di antara kedua ekstrem ini, dan pilihannya melibatkan kompromi antara performa dan kemampuan pemeliharaan. Menggunakan satu modul untuk keseluruhan project mungkin berarti Anda tidak perlu menyentuh file BUILD kecuali saat menambahkan dependensi eksternal, tetapi itu berarti bahwa sistem build harus selalu membangun seluruh proyek sekaligus. Artinya, build tidak akan dapat melakukan paralelisasi atau mendistribusikan bagian build, dan tidak akan dapat meng-cache bagian yang sudah di-build. Satu modul-per-file adalah kebalikannya: sistem build memiliki fleksibilitas maksimum dalam langkah-langkah caching dan penjadwalan build, tetapi insinyur perlu berupaya lebih keras dalam mengelola daftar dependensi setiap kali mereka mengubah {i>file<i} yang merujuk ke mana.

Meskipun tingkat perincian yang tepat bervariasi menurut bahasa (dan sering kali bahkan dalam bahasa), Google cenderung lebih menyukai modul yang jauh lebih kecil daripada yang biasanya ditulis dalam sistem build berbasis tugas. Biner produksi standar di Google sering kali bergantung pada puluhan ribu target, dan bahkan tim berukuran sedang dapat memiliki beberapa ratus target dalam codebase-nya. Untuk bahasa seperti Java yang memiliki konsep pengemasan bawaan yang kuat, setiap direktori biasanya berisi satu paket, target, dan file BUILD (Celana, sistem build lain menurut Bazel, menyebutnya aturan 1:1:1). Bahasa dengan konvensi pengemasan yang lebih lemah sering kali menentukan beberapa target per file BUILD.

Manfaat target versi yang lebih kecil benar-benar mulai terlihat dalam skala besar karena menghasilkan build yang terdistribusi lebih cepat dan mengurangi kebutuhan untuk membangun ulang target. Keuntungannya menjadi lebih menarik setelah pengujian masuk ke dalam gambar, karena target yang lebih terperinci berarti sistem build dapat menjadi jauh lebih cerdas dalam hanya menjalankan subset pengujian terbatas yang dapat terpengaruh oleh perubahan tertentu. Karena Google percaya pada manfaat sistemis dari penggunaan target yang lebih kecil, kami telah melakukan beberapa langkah untuk memitigasi kelemahan dengan berinvestasi dalam alat untuk mengelola file BUILD secara otomatis agar tidak membebani developer.

Beberapa alat ini, seperti buildifier dan buildozer, tersedia dengan Bazel di Direktori buildtools.

Meminimalkan Visibilitas Modul

Bazel dan sistem build lainnya memungkinkan setiap target menentukan visibilitas — properti yang menentukan target lain yang mungkin bergantung padanya. Target pribadi hanya dapat direferensikan dalam file BUILD-nya sendiri. Target dapat memberikan visibilitas ke target daftar file BUILD yang ditentukan secara eksplisit, atau, di visibilitas publik, ke setiap target di ruang kerja.

Seperti sebagian besar bahasa pemrograman, sebaiknya minimalkan visibilitas sebanyak mungkin. Umumnya, tim di Google akan membuat target bersifat publik hanya jika target tersebut mewakili perpustakaan yang banyak digunakan, yang tersedia untuk tim mana pun di Google. Tim yang mengharuskan orang lain berkoordinasi dengan mereka sebelum menggunakan kode mereka akan mempertahankan daftar target pelanggan yang diizinkan sebagai visibilitas target mereka. Target implementasi internal setiap tim akan dibatasi hanya untuk direktori yang dimiliki oleh tim, dan sebagian besar file BUILD hanya akan memiliki satu target yang tidak pribadi.

Mengelola dependensi

Modul harus dapat saling merujuk. Kelemahan membagi codebase menjadi modul terperinci adalah Anda perlu mengelola dependensi di antara modul tersebut (meskipun alat dapat membantu mengotomatiskan hal ini). Dengan menyatakan dependensi biasanya menjadi bagian terbesar dari konten dalam file BUILD.

Dependensi internal

Dalam sebuah proyek besar yang dibagi menjadi modul-modul halus, sebagian besar dependensi mungkin bersifat internal; yaitu, pada target lain yang ditentukan dan dibuat dengan ke repositori sumber. Dependensi internal berbeda dari dependensi eksternal di bahwa library tersebut dibuat dari sumber, bukan didownload sebagai artefak bawaan saat menjalankan build. Ini juga berarti bahwa tidak ada istilah “versi” untuk dependensi internal—target dan semua dependensi internalnya selalu dibangun pada commit/revisi yang sama dalam repositori. Satu masalah yang harus diperhatikan ditangani dengan hati-hati sehubungan dengan dependensi internal, adalah bagaimana memperlakukan dependensi transitif (Gambar 1). Misalkan target A bergantung pada target B, yang bergantung pada target library umum C. Harus menargetkan A agar dapat menggunakan class yang didefinisikan dalam target C?

Dependensi transitif

Gambar 1. Dependensi transitif

Selama alat yang mendasarinya, tidak ada masalah dengan hal ini; keduanya B dan C akan ditautkan ke target A ketika dibangun, jadi setiap simbol yang ditentukan dalam C dikenal oleh A. Bazel mengizinkan hal ini selama bertahun-tahun, tetapi seiring berkembangnya Google, kami mulai melihat masalah. Misalkan B difaktorkan ulang sedemikian rupa sehingga tidak lagi yang diperlukan untuk bergantung pada C. Jika dependensi B pada C kemudian dihapus, A dan target lain yang menggunakan C melalui dependensi pada B akan rusak. Secara efektif, dependensi target menjadi bagian dari kontrak publiknya dan tidak dapat diubah dengan aman. Artinya, dependensi terakumulasi dari waktu ke waktu dan build di Google mulai melambat.

Google akhirnya menyelesaikan masalah ini dengan memperkenalkan model mode dependensi” di Bazel. Dalam mode ini, Bazel mendeteksi apakah target mencoba mereferensikan simbol tanpa bergantung padanya secara langsung dan, jika demikian, akan gagal dengan dan perintah {i>shell <i}yang dapat digunakan untuk memasukkan dependensi. Meluncurkan perubahan ini di seluruh codebase Google dan memfaktorkan ulang setiap jutaan target build untuk mencantumkan secara eksplisit dependensi adalah upaya yang berlangsung selama bertahun-tahun, tapi hal itu sangat bermanfaat. Build kami kini jauh lebih cepat karena target memiliki lebih sedikit dependensi yang tidak perlu, dan engineer diberi kemampuan untuk menghapus dependensi yang tidak mereka perlukan tanpa khawatir akan merusak target yang bergantung padanya.

Seperti biasa, menerapkan dependensi transitif yang ketat melibatkan kompromi. Hal ini membuat file build lebih panjang, karena library yang sering digunakan kini perlu dicantumkan secara eksplisit di banyak tempat, bukan ditarik secara insidental, dan engineer perlu menghabiskan lebih banyak upaya untuk menambahkan dependensi ke file BUILD. Sejak saat itu mengembangkan alat yang mengurangi toil ini dengan secara otomatis mendeteksi banyak dependensi dan menambahkannya ke file BUILD tanpa developer apa pun intervensi. Namun, meskipun tanpa alat tersebut, kami mendapati bahwa kompromi tersebut sangat bernilai saat codebase diskalakan: menambahkan dependensi secara eksplisit ke file BUILD adalah biaya satu kali, tetapi menangani dependensi transitif implisit dapat menyebabkan masalah berkelanjutan selama target build ada. Roti Bazel menerapkan dependensi transitif yang ketat kode Java secara default.

Dependensi eksternal

Jika bukan internal, dependensi harus eksternal. Dependensi eksternal adalah dependensi pada artefak yang di-build dan disimpan di luar sistem build. Tujuan dependensi diimpor langsung dari repositori artefak (biasanya diakses melalui internet) dan digunakan apa adanya dan bukan dibuat dari sumber. Salah satu perbedaan terbesar antara dependensi eksternal dan internal adalah dependensi eksternal memiliki versi, dan versi tersebut ada secara independen dari kode sumber project.

Manajemen dependensi otomatis versus manual

Sistem build dapat mengizinkan versi dependensi eksternal dikelola secara manual atau otomatis. Jika dikelola secara manual, buildfile secara eksplisit mencantumkan versi yang ingin diunduh dari repositori artefak, sering kali menggunakan string versi semantik seperti sebagai 1.1.4. Jika dikelola secara otomatis, file sumber akan menentukan rentang versi yang dapat diterima, dan sistem build selalu mendownload versi terbaru. Misalnya, Gradle memungkinkan versi dependensi dideklarasikan sebagai “1.+” untuk menentukan bahwa versi minor atau patch dependensi dapat diterima selama versi utamanya adalah 1.

Dependensi yang dikelola secara otomatis mungkin nyaman untuk proyek kecil, tetapi Mereka biasanya menjadi resep bencana pada proyek dengan ukuran yang besar atau sedang dikerjakan oleh lebih dari satu insinyur. Masalahnya dengan sistem dependensi terkelola adalah Anda tidak memiliki kendali atas kapan versi diperbarui. Tidak ada cara untuk menjamin bahwa pihak eksternal tidak akan melakukan pelanggaran update terbaru (bahkan saat mereka mengklaim menggunakan pembuatan versi semantik), jadi build bekerja di suatu hari mungkin rusak di hari berikutnya tanpa cara mudah untuk mendeteksi apa yang berubah atau untuk melakukan roll back ke keadaan kerja. Meskipun build tidak rusak, mungkin ada perubahan perilaku atau performa halus yang tidak dapat dilacak.

Sebaliknya, karena dependensi yang dikelola secara manual memerlukan perubahan pada tetap mudah ditemukan dan di-roll back, serta memungkinkan periksa repositori versi lama untuk membangun dengan dependensi yang lebih lama. Bazel mewajibkan versi semua dependensi ditentukan secara manual. Bahkan pada skala sedang, overhead pengelolaan versi manual sangat sebanding dengan stabilitas yang diberikannya.

Aturan Satu Versi

Versi library yang berbeda biasanya diwakili oleh artefak yang berbeda, jadi secara teori tidak ada alasan mengapa versi yang berbeda dari dependensi eksternal yang sama tidak dapat dideklarasikan dalam sistem build dengan nama yang berbeda. Dengan begitu, setiap target dapat memilih versi dependensi mana yang ingin gunakan. Hal ini menyebabkan banyak masalah dalam praktik, jadi Google memberlakukan Aturan Satu Versi untuk semua dependensi pihak ketiga di codebase kami.

Masalah terbesar dengan mengizinkan beberapa versi adalah masalah dependensi berlian. Misalkan target A bergantung pada target B dan pada v1 eksternal library. Jika target B kemudian difaktorkan ulang untuk menambahkan dependensi pada v2 library eksternal yang sama, target A akan rusak karena sekarang secara implisit bergantung pada dua versi berbeda dari library yang sama. Sebenarnya, tidak pernah aman untuk menambahkan dependensi baru dari target ke library pihak ketiga mana pun dengan beberapa versi, karena salah satu pengguna target tersebut bisa bergantung pada . Dengan mengikuti Aturan Satu Versi, konflik ini tidak akan terjadi—jika target menambahkan dependensi pada library pihak ketiga, dependensi yang ada akan sudah berada pada versi yang sama, sehingga keduanya dapat berdampingan dengan baik.

Dependensi eksternal transitif

Menangani dependensi transitif dari dependensi eksternal dapat sangat sulit. Banyak repositori artefak seperti Maven Central, memungkinkan artefak menentukan dependensi pada versi artefak lain tertentu di repositori. Alat build seperti Maven atau Gradle sering mendownload masing-masing secara rekursif dependensi transitif secara default, artinya menambahkan satu dependensi proyek Anda berpotensi menyebabkan lusinan artefak diunduh di total.

Hal ini sangat praktis: saat menambahkan dependensi pada library baru, akan sangat merepotkan jika harus melacak setiap dependensi transitif library tersebut dan menambahkan semuanya secara manual. Tapi ada juga kerugian besar: karena perbedaan bisa bergantung pada versi yang berbeda dari pustaka pihak ketiga yang sama, maka strategi selalu melanggar Aturan Satu Versi dan menyebabkan dependensi. Jika target Anda bergantung pada dua library eksternal yang menggunakan versi berbeda dari dependensi yang sama, tidak ada yang dapat dapatkan. Ini juga berarti bahwa memperbarui dependensi eksternal dapat menyebabkan kegagalan yang tidak terkait di seluruh codebase jika versi baru mulai digunakan versi yang bertentangan dari beberapa dependensinya.

Karena alasan ini, Bazel tidak secara otomatis mengunduh dependensi transitif. Sayangnya, tidak ada solusi praktis—alternatif Bazel adalah mewajibkan file global yang mencantumkan setiap dependensi eksternal repositori dan versi eksplisit yang digunakan untuk dependensi tersebut di seluruh repositori. Untungnya, Bazel menyediakan alat yang mampu secara otomatis membuat file seperti itu yang berisi dependensi transitif dari satu set Maven artefak. Alat ini dapat dijalankan sekali untuk membuat file WORKSPACE awal untuk sebuah proyek, dan file tersebut kemudian dapat diperbarui secara manual untuk menyesuaikan dari setiap dependensi.

Sekali lagi, pilihan di sini adalah antara kemudahan dan skalabilitas. Kecil proyek mungkin lebih memilih untuk tidak perlu mengkhawatirkan pengelolaan dependensi transitif sendiri dan mungkin bisa menggunakan penggunaan transitif otomatis dependensi. Strategi ini menjadi semakin tidak menarik seiring berkembangnya organisasi dan codebase, serta konflik dan hasil yang tidak terduga menjadi semakin sering. Pada skala yang lebih besar, biaya pengelolaan dependensi secara manual jauh lebih kecil daripada biaya untuk menangani masalah yang disebabkan oleh pengelolaan dependensi otomatis.

Menyimpan cache hasil build menggunakan dependensi eksternal

Dependensi eksternal paling sering disediakan oleh pihak ketiga yang merilis library versi stabil, mungkin tanpa menyediakan kode sumber. Beberapa organisasi mungkin juga memilih untuk menyediakan beberapa kode mereka sendiri sebagai artefak, sehingga memungkinkan bagian kode lain bergantung padanya sebagai pihak ketiga, bukan dependensi internal. Secara teoritis, hal ini dapat mempercepat build jika artefak lambat di-build, tetapi cepat didownload.

Namun, hal ini juga menyebabkan banyak overhead dan kompleksitas: seseorang harus bertanggung jawab untuk mem-build setiap artefak tersebut dan menguploadnya ke repositori artefak, dan klien harus memastikan bahwa mereka selalu menggunakan versi terbaru. Proses debug juga menjadi jauh lebih sulit karena berbagai bagian sistem akan di-build dari berbagai titik di repositori, dan tidak ada lagi tampilan hierarki sumber yang konsisten.

Cara yang lebih baik untuk memecahkan masalah artefak yang membutuhkan waktu lama untuk dibangun adalah dengan menggunakan sistem pembangunan yang mendukung {i> cache<i} jarak jauh, seperti dijelaskan sebelumnya. Sungguh sistem build menyimpan artefak yang dihasilkan dari setiap build ke lokasi yang dibagikan kepada seluruh engineer, jadi jika developer bergantung pada artefak yang baru-baru ini dibuat oleh orang lain, sistem build akan otomatis mendownload alih-alih membangunnya. Hal ini memberikan semua manfaat performa yang bergantung langsung pada artefak sekaligus memastikan bahwa build konsisten seolah-olah selalu dibuat dari sumber yang sama. Ini adalah yang digunakan secara internal oleh Google, dan Bazel dapat dikonfigurasi untuk menggunakan {i>cache<i}.

Keamanan dan keandalan dependensi eksternal

Bergantung pada artefak dari sumber pihak ketiga, berisiko secara inheren. Terdapat risiko ketersediaan jika sumber pihak ketiga (seperti repositori artefak) karena seluruh build mungkin akan berhenti jika tidak dapat didownload dependensi eksternal. Ada juga risiko keamanan: jika sistem pihak ketiga disusupi oleh penyerang, penyerang dapat mengganti artefak yang dirujuk dengan salah satu desainnya sendiri, sehingga memungkinkan mereka memasukkan kode arbitrer ke dalam build Anda. Kedua masalah tersebut dapat dimitigasi dengan mencerminkan artefak apa pun yang Anda butuhkan ke server yang Anda kontrol dan memblokir sistem build agar tidak mengakses repositori artefak pihak ketiga seperti Maven Central. Konsekuensinya adalah cermin ini membutuhkan usaha dan sumber daya untuk dipelihara, jadi pilihan apakah akan menggunakannya sering tergantung pada skala proyek. Masalah keamanan juga dapat sepenuhnya dicegah dengan {i>overhead<i} yang membutuhkan {i>hash <i}dari setiap artefak pihak ketiga ditentukan dalam repositori sumber, sehingga menyebabkan gagal jika artefak telah dirusak. Alternatif lain yang sepenuhnya menghindari masalah ini adalah dengan membeli dependensi project Anda. Saat project menjual dependensinya, project akan memeriksanya ke dalam kontrol sumber bersama kode sumber project, baik sebagai sumber maupun sebagai biner. Hal ini secara efektif berarti bahwa semua dependensi eksternal project dikonversi menjadi dependensi internal. Google menggunakan pendekatan ini secara internal, memeriksa setiap pihak ketiga library yang dirujuk di seluruh Google ke dalam direktori third_party di root pohon sumber Google. Namun, ini berfungsi di Google hanya karena yang dibuat khusus untuk menangani monorepo yang sangat besar, jadi vendor mungkin tidak menjadi pilihan untuk semua organisasi.