Bağımlılık Yönetimi

Sorun bildirin Kaynağı göster

Önceki sayfalara bakarken bir tema tekrar tekrar tekrarlanıyor: Kendi kodunuzu yönetmek oldukça basit ancak bağımlılıklarını yönetmek çok daha zor. Her türlü bağımlılık vardır: Bazen bir göreve (örneğin, "bir sürümü tamamlandı olarak işaretlemeden önce belgeleri aktar") ve bazen bir yapıya (ör. "Kodumu derlemek için bilgisayar vizyon kitaplığının en son sürümüne sahip olmam gerekiyor") bağlı olabilir. Bazen bir üçüncü tarafa veya başka bir kod tabanınızın sahibi olan verilere bağımlı olursunuz. Ama her halükarda, “Bunu yapabilmek için öncelikle buna ihtiyacım var” fikri, derleme sistemlerinin tasarımında sürekli olan bir şeydir ve bağımlılıkları yönetmek, belki de bir derleme sisteminin en temel işidir.

Modüller ve Bağımlılıkları Ele Alma

Bazel gibi yapı tabanlı derleme sistemleri kullanan projeler bir dizi modüle ayrılmıştır. Modüller, BUILD dosyaları aracılığıyla birbirine bağımlılıkları ifade eder. Bu modüllerin ve bağımlılıkların doğru şekilde düzenlenmesi, hem derleme sisteminin performansı hem de bakımı için gereken çalışma miktarı üzerinde büyük bir etki yaratabilir.

Ayrıntılı Modülleri ve 1:1:1 Kuralını Kullanma

Yapı tabanlı bir yapı yapılandırırken ortaya çıkan ilk soru, bağımsız bir modülün ne kadar işlev içermesi gerektiğine karar vermektir. Bazel'de modül, java_library veya go_binary gibi derlenebilir bir birim belirten bir hedefle temsil edilir. Bir uçta, köke bir BUILD dosyası yerleştirilerek projenin tüm kaynak dosyaları yinelemeli bir şekilde bir araya getirilerek tüm proje tek bir modülde bulunabilir. Diğer uçta, neredeyse her kaynak dosya kendi modülünde yapılabilir. Bu da etkin bir şekilde, her bir dosyanın bağlı olduğu diğer tüm dosyaların BUILD dosyasında listelenmesini gerektirir.

Çoğu proje bu uç noktalar arasında bir yerdedir ve seçim, performans ile sürdürülebilirlik arasında bir denge kurmayı gerektirir. Projenin tamamı için tek bir modül kullanmak, harici bağımlılık ekleme dışında hiçbir zaman BUILD dosyasına dokunmanız gerekmeyebilir, ancak derleme sisteminin her zaman tüm projeyi tek seferde derlemesi gerektiği anlamına gelir. Bu, derlemenin parçalarını paralelleştiremeyeceği veya dağıtamayacağı ya da önceden derlenmiş parçaları önbelleğe alamayacağı anlamına gelir. Dosya başına bir modül ise bunun tam tersi: Derleme sistemi, derlemenin önbelleğe alma ve planlama adımlarında maksimum esnekliğe sahiptir ancak mühendisler, referans verilen dosyaları her değiştirdiklerinde bağımlılık listelerini tutmak için daha fazla çaba harcamalıdır.

Tam ayrıntı düzeyi dile göre (ve genellikle dil içinde bile) değişiklik gösterse de Google, normalde görev tabanlı bir derleme sisteminde yazabileceğinizden çok daha küçük modülleri tercih eder. Google'daki tipik bir üretim ikili programı genellikle on binlerce hedefe bağlıdır. Orta ölçekli bir ekip bile kod tabanında yüzlerce hedefe sahip olabilir. Güçlü bir yerleşik paketleme kavramına sahip Java gibi diller için her dizin genellikle tek bir paket, hedef ve BUILD dosyası içerir (Bazel'i temel alan başka bir derleme sistemi olan Pantolonlar için buna 1:1:1 kuralı denir). Daha zayıf paketleme kurallarına sahip diller, genellikle BUILD dosyası başına birden fazla hedef tanımlar.

Daha küçük derleme hedeflerinin avantajları geniş ölçekte görülmeye başlar. Çünkü bu hedefler, daha hızlı dağıtılan derlemelerin yapılmasını sağlar ve hedeflerin yeniden oluşturulma sıklığının daha az olmasını sağlar. Test süreci başladıktan sonra bu avantajlar daha da cazip hale gelir. Çünkü daha ayrıntılı hedefler, derleme sisteminin herhangi bir değişiklikten etkilenebilecek sınırlı bir test alt kümesini çalıştırma konusunda çok daha akıllı olabileceği anlamına gelir. Google, daha küçük hedefler kullanmanın sistemsel faydalarına inandığı için geliştiricilerin yükünü azaltmak amacıyla BUILD dosyalarını otomatik olarak yönetecek araçlara yatırım yaparak olumsuz tarafları azaltma konusunda bazı adımlar attık.

buildifier ve buildozer gibi bu araçlardan bazıları, buildtools dizininde Bazel ile birlikte kullanılabilir.

Modül Görünürlüğünü En Aza İndirme

Bazel ve diğer derleme sistemleri, her bir hedefin bir görünürlük belirtmesine olanak tanır. Bu görünürlük, başka hangi hedeflerin buna bağlı olabileceğini belirleyen bir özelliktir. Gizli bir hedefe yalnızca kendi BUILD dosyası içinde referans verilebilir. Hedef, açıkça tanımlanmış BUILD dosyaları listesinin hedeflerine veya herkese açık görünürlük durumunda çalışma alanındaki her hedefe daha kapsamlı görünürlük sağlayabilir.

Çoğu programlama dilinde olduğu gibi, görünürlüğü mümkün olduğunca azaltmak genellikle en iyisidir. Google'daki ekipler genellikle yalnızca hedeflerin Google'daki herhangi bir ekibin kullanımına sunulan yaygın olarak kullanılan kitaplıkları temsil etmesi durumunda hedefleri herkese açık hale getirir. Kodlarını kullanmadan önce başka kişilerin iş birliği yapmasını gerektiren ekipler, hedeflerinin görünürlüğü olarak müşteri hedeflerinin izin verilenler listesini tutar. Her ekibin dahili uygulama hedefleri yalnızca ekibe ait dizinlerle sınırlandırılacak ve çoğu BUILD dosyası gizli olmayan yalnızca tek bir hedefe sahip olacaktır.

Bağımlılıkları Yönetme

Modüllerin birbirlerine referans verebilmesi gerekir. Bir kod tabanını ayrıntılı modüllere ayırmanın dezavantajı, bu modüller arasındaki bağımlılıkları yönetmeniz gerekmesidir (ancak araçlar bunu otomatikleştirmeye yardımcı olabilir). Bu bağımlılıkları ifade etmek genellikle BUILD dosyasındaki içeriğin çoğunluğunu oluşturur.

İç bağımlılıklar

Ayrıntılı modüllere ayrılmış büyük bir projede, çoğu bağımlılığın iç kaynaklı olma ihtimali yüksektir. Diğer bir deyişle, aynı kaynak deposunda tanımlanmış ve derlenmiş başka bir hedefte bağımlılığın çoğu söz konusudur. İç bağımlılıklar, derlemeyi çalıştırırken önceden oluşturulmuş bir eser olarak indirilmeleri yerine kaynaktan oluşturuldukları için dış bağımlılıklardan farklıdır. Bu aynı zamanda iç bağımlılıklar için "sürüm" kavramı olmadığı anlamına gelir. Bir hedef ve tüm iç bağımlılıkları depoda her zaman aynı kayıt/düzeltmede oluşturulur. İç bağımlılıklar konusunda dikkatli bir şekilde ele alınması gereken bir konu da geçişli bağımlılıkların nasıl yönetileceğidir (Şekil 1). A hedefinin, ortak kitaplık hedefi C'ye bağlı olan hedef B'ye bağlı olduğunu varsayalım. A hedefi, C hedefinde tanımlanan sınıfları kullanabilmeli mi?

Geçişli bağımlılıklar

Şekil 1. Geçişli bağımlılıklar

Kullanılan temel araçlar söz konusu olduğunda bununla ilgili bir sorun yoktur. Hem B hem de C, oluşturulduğunda hedef A'ya bağlanır, bu nedenle C'de tanımlanan tüm simgeler A tarafından bilinir. Bazel yıllarca buna izin verdi ancak Google büyüdükçe sorunlar görmeye başladık. B'nin, artık C'ye bağlı kalmayacak şekilde yeniden düzenlendiğini varsayalım. B'nin C'ye bağımlılığı kaldırılırsa A ve B'ye bağımlılık aracılığıyla C kullanan diğer hedefler bozulur. Bir hedefin bağımlılıkları, kamu sözleşmesinin bir parçası haline geldi ve hiçbir zaman güvenli bir şekilde değiştirilemeyecekti. Bu durum, bağımlılıkların zaman içinde biriken ve Google'daki derlemelerin yavaşlamaya başlaması anlamına geliyordu.

Google, sonunda Bazel'de "katı geçişli bağımlılık modu" sunarak bu sorunu çözdü. Bu modda Bazel, bir hedefin bir sembole doğrudan bağlı olmadan referansta bulunmaya çalışıp çalışmadığını algılar. Bu durumda, bir hata mesajı ve bağımlılığı otomatik olarak eklemek için kullanılabilecek bir kabuk komutuyla başarısız olur. Bu değişikliği Google'ın tüm kod tabanına uygulamak ve bağımlılıklarını açıkça listelemek amacıyla milyonlarca derleme hedefimizin her birini yeniden düzenlemek çok yıllık bir çalışmaydı ancak buna değdi. Hedeflerin daha az sayıda gereksiz bağımlılığı olduğu ve mühendislerin, kendilerine bağımlı hedefleri kırma konusunda endişelenmeden ihtiyaç duymadıkları bağımlılıkları kaldırma gücüne sahip olduğu için derlemelerimiz artık çok daha hızlı.

Katı geçişli bağımlılıkların uygulanması her zaman olduğu gibi, iki tarafın da çaba göstermesini gerektiriyordu. Sık kullanılan kitaplıkların artık tesadüfi olarak çekilmesi yerine birçok yerde açık bir şekilde listelenmesi gerektiğinden, mühendislerin de BUILD dosyalarına bağımlılık eklemek için daha fazla çaba sarf etmeleri gerekiyor. O zamandan beri, eksik olan birçok bağımlılığı otomatik olarak tespit edip bunları geliştiricinin herhangi bir müdahalesi olmadan BUILD dosyasına ekleyerek bu iş yükünü azaltan araçlar geliştirdik. Ancak bu tür araçlar olmadan bile, kod tabanı ölçeklendirildikçe bunun iyi bir çaba olduğunu gördük: BUILD dosyasına açıkça bir bağımlılık eklemek tek seferlik bir maliyettir, ancak örtülü geçişli bağımlılıklarla başa çıkmak, derleme hedefi mevcut olduğu sürece devam eden sorunlara neden olabilir. Bazel varsayılan olarak Java kodunda katı geçişli bağımlılıkları zorunlu kılar.

Harici bağımlılıklar

Bağımlılık dahili değilse harici olması gerekir. Dış bağımlılıklar, derleme sisteminin dışında derlenen ve depolanan yapılar üzerindedir. Bağımlılık, doğrudan bir yapı deposundan (genellikle internet üzerinden erişilebilir) içe aktarılır ve kaynaktan derleme yerine olduğu gibi kullanılır. Dış ve iç bağımlılıklar arasındaki en büyük farklardan biri, dış bağımlılıkların sürümlerinin olması ve bu sürümlerin projenin kaynak kodundan bağımsız olarak var olmasıdır.

Otomatik ve manuel bağımlılık yönetimi

Derleme sistemleri, harici bağımlılıkların sürümlerinin manuel veya otomatik olarak yönetilmesini sağlayabilir. Manuel olarak yönetildiğinde derleme dosyası, yapı deposundan indirmek istediği sürümü açıkça listeler. Bunun için genellikle 1.1.4 gibi bir anlamsal sürüm dizesi kullanılır. Otomatik olarak yönetildiğinde kaynak dosya, kabul edilebilir bir dizi sürüm belirtir ve derleme sistemi her zaman en yeni sürümü indirir. Örneğin Gradle, ana sürüm 1 olduğu sürece bir bağımlılığın herhangi bir alt veya yama sürümünün kabul edilebilir olduğunu belirtmek için bağımlılık sürümünün "1.+" olarak tanımlanmasına izin verir.

Otomatik olarak yönetilen bağımlılıklar küçük projeler için kullanışlı olabilir ancak bu bağımlılıklar genellikle önemsiz büyüklükteki ya da birden fazla mühendisin üzerinde çalıştığı projelerde felaketlere yol açabilir. Otomatik olarak yönetilen bağımlılıklardaki sorun, sürümün ne zaman güncelleneceği üzerinde herhangi bir kontrolünüzün olmamasıdır. Harici tarafların hatalı güncellemeler yapmayacağını (anlamsal sürüm oluşturmayı kullandıklarını iddia etseler bile) garanti etmenin bir yolu yoktur. Bu nedenle, bir gün başarılı olan bir derleme, nelerin değiştiğini tespit etmenin veya çalışma durumuna geri döndürmenin kolay bir yolu olmadan ertesi gün bozulabilir. Derleme bozulmasa bile tespit edilmesi imkansız incelikli davranış veya performans değişiklikleri olabilir.

Buna karşılık manuel olarak yönetilen bağımlılıklar kaynak kontrolünde değişiklik gerektirdiğinden kolayca keşfedilip geri alınabilir. Ayrıca eski bağımlılıklarla derleme yapmak için deponun eski bir sürümünü kullanmak mümkündür. Bazel, tüm bağımlılıkların sürümlerinin manuel olarak belirtilmesini gerektirir. Orta düzeyli ölçeklerde bile manuel sürüm yönetiminin ek yükü, sağladığı kararlılık açısından buna değer.

Tek Sürüm Kuralı

Bir kitaplığın farklı sürümleri genellikle farklı yapılarla temsil edilir. Bu nedenle, teoride derleme sisteminde aynı dış bağımlılığın farklı sürümlerinin her ikisinin de farklı adlarla tanımlanamamasının bir nedeni yoktur. Bu şekilde her hedef, kullanmak istediği bağımlılık sürümünü seçebilir. Bu durum pratikte pek çok soruna yol açtığından Google, kod tabanımızdaki tüm üçüncü taraf bağımlılıkları için katı bir Tek Sürüm Kuralı uygulamaktadır.

Birden fazla sürüme izin verme konusundaki en büyük sorun, elmas bağımlılık sorunudur. Hedef A'nın hedef B'ye ve harici bir kitaplığın v1'ine bağlı olduğunu varsayalım. Hedef B daha sonra aynı harici kitaplığın v2'sine bağımlılık eklemek için yeniden düzenlenirse hedef A, artık dolaylı olarak aynı kitaplığın iki farklı sürümüne bağlı olduğundan bozulur. Bir hedeften, birden fazla sürümü olan herhangi bir üçüncü taraf kitaplığına yeni bir bağımlılık eklemek hiçbir zaman güvenli değildir, çünkü söz konusu hedefin kullanıcıları zaten farklı bir sürüme bağlı olabilir. Tek Sürüm Kuralını uygulamak bu çakışmayı imkansız kılar. Bir hedef üçüncü taraf kitaplığına bağımlılık eklerse mevcut bağımlılıklar da aynı sürümde olur. Böylece birlikte kullanılabilirler.

Geçişli dış bağımlılıklar

Dış bağımlılığın geçişli bağımlılıklarıyla başa çıkmak özellikle zor olabilir. Maven Central gibi birçok yapı deposu, yapıların depodaki diğer yapıların belirli sürümlerine bağımlılık belirtmesine olanak tanır. Maven veya Gradle gibi araçlar genellikle varsayılan olarak her geçişli bağımlılığı tekrarlı bir şekilde indirir. Yani projenize tek bir bağımlılık eklemek onlarca yapının toplamda indirilmesine neden olabilir.

Bu çok kullanışlıdır: Yeni bir kitaplığa bağımlılık eklerken, söz konusu kitaplığın geçişli bağımlılıklarının her birini takip etmek ve hepsini manuel olarak eklemek işin en zor kısmıdır. Ancak aynı zamanda büyük bir dezavantaj vardır: Farklı kitaplıklar aynı üçüncü taraf kitaplığının farklı sürümlerine bağlı olabileceğinden, bu strateji Tek Sürüm Kuralını mutlaka ihlal eder ve elmas bağımlılık sorununa yol açar. Hedefiniz aynı bağımlılığın farklı sürümlerini kullanan iki harici kitaplığa bağlıysa hangisini alacağınızı bilmek imkansızdır. Bu aynı zamanda, yeni sürüm bazı bağımlılıklarının çakışan sürümlerini çekmeye başlarsa harici bir bağımlılığı güncellemenin kod tabanı genelinde ilişkisiz gibi görünen hatalara neden olabileceği anlamına da gelir.

Bu nedenle Bazel, geçişli bağımlılıkları otomatik olarak indirmez. Maalesef sihirli bir değnek yoktur. Bazel'in alternatifi, deponun harici bağımlılıklarının her birini listeleyen global bir dosyanın ve depo genelinde bu bağımlılık için kullanılan açık bir sürümün kullanılmasını zorunlu kılmaktır. Neyse ki Bazel, bir dizi Maven yapısının geçişli bağımlılıklarını içeren böyle bir dosyayı otomatik olarak oluşturabilen araçlar sunar. Bu araç, bir proje için ilk WORKSPACE dosyasını oluşturmak için bir kez çalıştırılabilir. Ardından bu dosya, her bağımlılığın sürümlerini ayarlamak için manuel olarak güncellenebilir.

Burada da, kolaylık ve ölçeklenebilirlik arasında bir tercih olması gerekir. Küçük projeler geçişli bağımlılıkları tek başına yönetmeyi tercih etmeyebilir ve otomatik geçişli bağımlılıkları kullanarak bu durumdan kurtulabilir. Organizasyon ve kod tabanı büyüdükçe, çakışmalar ve beklenmedik sonuçlar gittikçe daha sık ortaya çıktıkça bu stratejinin çekiciliği azalır. Büyük ölçeklerde bağımlılıkları manuel olarak yönetmenin maliyeti, otomatik bağımlılık yönetiminin neden olduğu sorunlarla ilgilenmenin maliyetinden çok daha düşüktür.

Harici bağımlılıkları kullanarak derleme sonuçlarını önbelleğe alma

Dış bağımlılıklar çoğu zaman, belki kaynak kodu sağlamadan kitaplıkların kararlı sürümlerini yayınlayan üçüncü taraflarca sağlanır. Bazı kuruluşlar kendi kodlarından bazılarını yapı olarak kullanıma sunmayı da tercih edebilir. Böylece, diğer kod parçaları iç bağımlılıklar yerine üçüncü taraf olarak bunlara bağımlı olabilir. Yapıların derlemesi yavaş ancak indirme işlemi hızlıysa derlemeleri teorik olarak hızlandırabilir.

Ancak bu, çok fazla ek yük ve karmaşıklığı da beraberinde getirir: Bu eserlerin her birini oluşturmaktan ve bunları yapı deposuna yüklemekten birinin sorumlu olması, müşterilerin ise en son sürümle güncel kalmasını sağlamak zorunda olması gerekir. Sistemin farklı bölümleri depodaki farklı noktalardan derleneceği ve kaynak ağacının artık tutarlı bir görünümünün olmadığı için hata ayıklama da çok daha zor hale gelir.

Derlemesi uzun süren yapılar sorununu çözmenin daha iyi bir yolu, daha önce açıklandığı gibi uzaktan önbelleğe almayı destekleyen bir derleme sistemi kullanmaktır. Böyle bir derleme sistemi, her derlemeden ortaya çıkan yapıları mühendisler arasında paylaşılan bir konuma kaydeder. Böylece, bir geliştirici, yakın zamanda başkası tarafından oluşturulmuş bir yapıya bağımlıysa derleme sistemi onu oluşturmak yerine otomatik olarak indirir. Bu, doğrudan yapılara bağlı olarak elde edebileceğiniz tüm performans avantajlarını sağlarken derlemelerin her zaman aynı kaynaktan derlenmiş gibi tutarlı olmasını da sağlar. Bu, Google tarafından şirket içinde kullanılan stratejidir. Bazel, uzak önbelleği kullanacak şekilde yapılandırılabilir.

Dış bağımlılıkların güvenliği ve güvenilirliği

Üçüncü taraf kaynaklardan gelen yapılara bağlı olarak doğası gereği risklidir. Üçüncü taraf kaynak (ör. yapı deposu) çökerse kullanılabilirlik riski ortaya çıkar. Bunun nedeni, bir harici bağımlılığı indirememesi durumunda derlemenizin tamamı durabilir. Ayrıca bir güvenlik riski de söz konusudur: Bir saldırgan, üçüncü taraf sisteminin güvenliğini ihlal ederse söz konusu yapıyı kendi tasarımıyla değiştirebilir. Böylece, derlemenize rastgele kod eklenebilir. Bağımlı olduğunuz yapıları sizin kontrol ettiğiniz sunuculara yansıtarak ve derleme sisteminizin Maven Central gibi üçüncü taraf yapı depolarına erişmesini engelleyerek her iki sorun da azaltılabilir. Bunun karşılığında, bu aynaların bakımı için çaba ve kaynak gerekir. Bu nedenle, genellikle bu aynaların kullanılıp kullanılmayacağına karar verilmesi projenin ölçeğine bağlıdır. Ayrıca güvenlik sorunu, her üçüncü taraf yapısının karmasının kaynak depoda belirtilmesini gerektirerek küçük bir ek yük ile tamamen önlenebilir ve yapı üzerinde oynandığında derleme başarısız olur. Sorunu tamamen ortadan kaldıran bir diğer alternatif yol da projenizin bağımlılıklarını tedarik etmektir. Bir proje bağımlılıklarını sağladığında, bunları projenin kaynak koduyla birlikte kaynak veya ikili program olarak kaynak kontrolüne dahil eder. Bu, projenin tüm dış bağımlılıklarının iç bağımlılıklara dönüştürülmesi anlamına gelir. Google bu yaklaşımı kendi içinde kullanır ve Google genelinde başvurulan her üçüncü taraf kitaplığını Google'ın kaynak ağacının kökündeki bir third_party dizininde kontrol eder. Ancak bu yalnızca Google'da işe yarar. Google'ın kaynak kontrol sistemi çok büyük bir monorepoyu işleyecek şekilde özel olarak tasarlandığından, tedarikçi firma tüm kuruluşlar için bir seçenek olmayabilir.