Bağımlılık Yönetimi

Önceki sayfalara bakarken bir tema tekrar tekrar tekrar etmektedir: Kendi kodunuzu yönetmek oldukça basit bir işlemdir ancak bağımlılıklarını yönetmek çok daha zordur. Her türlü bağımlılık vardır: Bazen bir göreve bağımlılık olur (ör. “bir sürümü tamamlandı olarak işaretlemeden önce belgeleri gönderme) ve bazen bir yapıya bağımlılık söz konusu olur (örneğin, "Kodumu oluşturmak için bilgisayarın görüntü kitaplığının en son sürümüne sahip olmam gerekiyor"). Bazen verilerin kodları sizin başka bir tarafın veya bir üçüncü tarafın dış bağımlılıkları içerir. Ancak her koşulda, “Bunu yapabilmek için buna ihtiyacım var” fikri derleme sistemlerinin tasarımında tekrar tekrar ortaya çıkan bir düşüncedir ve bağımlılıkları yönetmek, belki de bir derleme sisteminin en temel işidir.

Modüller ve Bağımlılıklarla Başa Çıkma

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

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

Yapı tabanlı bir derleme yapılandırırken ortaya çıkan ilk soru, her bir modülün ne kadar işlevi kapsaması gerektiğine karar vermektir. Bazel'de modül, java_library veya go_binary gibi derlenebilir bir birimi belirten bir hedefle temsil edilir. Bir uç noktada, köke bir BUILD dosyası yerleştirilerek ve bu projenin tüm kaynak dosyalarını yinelemeli olarak toplu hale getirerek projenin tamamı tek bir modülde yer alabilir. Diğer uçta, neredeyse her kaynak dosya kendi modülü haline getirilebilir ve bu durumda, her dosyanın bağımlı olduğu diğer tüm dosyaların bir BUILD dosyasında listelenmesi gerekir.

Çoğu proje bu uç noktaların arasında bir yerdedir ve seçim yaparken performans ile sürdürülebilirlik arasında bir değer gerekir. Projenin tamamı için tek bir modül kullanmak, harici bir bağımlılık eklemek dışında hiçbir zaman BUILD dosyasına dokunmanız gerekmediği anlamına gelebilir ancak derleme sisteminin her zaman tüm projeyi tek seferde derlemesi gerektiği anlamına gelir. Bu, derlemenin parçalarını paralellayamayacağı veya dağıtamayacağı ya da önceden derlenmiş parçaları önbelleğe alamayacağı anlamına gelir. Dosya başına tek modüllü bu durumun tam tersi vardır: Derleme sistemi, derlemenin önbelleğe alma ve planlama adımlarında maksimum esnekliğe sahiptir ancak mühendislerin hangi dosyaların hangi dosyaya referans verdiğini her değiştirdiklerinde bağımlılık listelerini korumak için daha fazla çaba sarf etmeleri gerekir.

Tam ayrıntı düzeyi dile göre (ve genellikle dil içinde bile) değişiklik gösterse de Google, göreve dayalı bir derleme sisteminde genellikle yazılabilecek modüllerden çok daha küçük modülleri tercih eder. Google'daki tipik bir üretim ikili programı genellikle on binlerce hedefe dayanır ve orta büyüklükte bir ekip bile kod tabanında birkaç yüz hedefe sahip olabilir. Güçlü bir yerleşik paketleme kavramına sahip olan Java gibi diller için her dizin genellikle tek bir paket, hedef ve BUILD dosyası içerir (Bazel tabanlı başka bir derleme sistemi olan Pantolonlar'a 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ı, daha hızlı dağıtılan derlemelere yol açtığı ve hedefleri yeniden oluşturma ihtiyacının daha az olmasını sağladığı için, gerçekten geniş ölçekte gösterilmeye başlar. Test durumu resmedildiğinde, avantajlar daha da çekici hale gelir. Çünkü daha ayrıntılı hedefler, derleme sisteminin belirli bir değişiklikten etkilenebilecek yalnızca 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 sistematik faydalarına inandığından, BUILD dosyalarını otomatik olarak yönetecek araçlara yatırım yaparak geliştiricilerin zarar görmesini önlemek için bazı ilerlemeler kaydettik.

Bu araçlardan bazıları (ör. buildifier ve buildozer), buildtools dizininde Bazel ile kullanılabilir.

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

Bazel ve diğer derleme sistemleri, her hedefin bir görünürlük belirtmesine olanak tanır. Bu, hangi diğer hedeflerin buna bağlı olabileceğini belirleyen bir özelliktir. Özel bir hedefe yalnızca kendi BUILD dosyası içinde referans verilebilir. Bir hedef, açık bir şekilde tanımlanmış BUILD dosyalarından oluşan bir listenin hedeflerine veya herkese açık görünürlük durumunda çalışma alanındaki her hedefe daha geniş 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. Genel olarak Google'daki ekipler, hedefleri ancak bu hedefler, Google'daki tüm ekiplerin kullanımına sunulan yaygın olarak kullanılan kitaplıkları temsil ediyorsa herkese açık hale getirir. Kodlarını kullanmadan önce başkalarının kendileriyle koordine olmasını gerektiren ekipler, hedeflerinin görünürlüğü olarak müşteri hedeflerini içeren bir izin verilenler listesini elinde bulundurur. Her ekibin dahili uygulama hedefleri yalnızca ekibe ait dizinlerle sınırlandırılır ve çoğu BUILD dosyası, gizli olmayan yalnızca bir hedefe sahip olur.

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

Modüllerin birbirine başvuruda bulunabilmesi gerekir. Kod tabanını ayrıntılı modüllere ayırmanın olumsuz tarafı, bu modüller arasındaki bağımlılıkları yönetmenizin gerekmesidir (ancak araçlar bunu otomatikleştirmeye yardımcı olabilir). Bu bağımlılıkları ifade etmek, genellikle bir BUILD dosyasındaki içeriğin büyük bir kısmını oluşturur.

İç bağımlılıklar

Ayrıntılı modüllere ayrılmış büyük bir projede, çoğu bağımlılık büyük olasılıkla dahilidir, yani aynı kaynak deposunda tanımlanan ve oluşturulan başka bir hedeftedir. İç bağımlılıklar, derleme çalıştırırken önceden oluşturulmuş bir yapı olarak indirilmek yerine kaynaktan derlenmeleri nedeniyle dış bağımlılıklardan farklıdır. Bu aynı zamanda dahili bağımlılıklar için bir "sürüm" kavramının olmadığı anlamına gelir. Bir hedef ve tüm iç bağımlılıkları her zaman depoda aynı kaydetme/revizyon ile oluşturulur. İç bağımlılıklarla ilgili olarak dikkatli bir şekilde ele alınması gereken sorunlardan biri geçişli bağımlılıkların nasıl ele alınacağıdır (Şekil 1). A hedefinin, ortak kitaplık hedefine bağlı olan B hedefine bağlı olduğunu varsayalım. Hedef A, hedef C'de tanımlanan sınıfları kullanabilmeli mi?

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

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

Temel araçların kullanılmasında herhangi bir sorun yoktur. B ve C, oluşturulduğunda hedef A'ya bağlanır. Bu nedenle, C'de tanımlanan semboller A tarafından bilinir. Bazel yıllarca buna izin verdi ama Google büyüdükçe sorunlarla karşılaşmaya başladık. B'nin, artık C'ye bağımlı olması gerekmeyecek şekilde yeniden düzenlendiğini varsayalım. Bu durumda B'nin C'ye olan bağımlılığı kaldırılırsa A ve B'ye olan bağımlılık aracılığıyla C'yi kullanan diğer hedefler bozulur. Etkili bir şekilde, bir hedefin bağımlılıkları kamuya yönelik sözleşmenin bir parçası haline geldi ve asla güvenli bir şekilde değiştirilemez. Bu, zamanla biriken bağımlılıkların ve Google'daki derlemelerin yavaşlamaya başladığı anlamına geliyordu.

Google sonunda bu sorunu Bazel'de "katı geçişli bağımlılık modunu" kullanıma sunarak çözdü. Bu modda Bazel, bir hedefin doğrudan bağlı olmadan bir sembole referans vermeye çalışıp çalışmayacağını tespit eder; başarısız olursa bir hata 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 kod tabanının tamamı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ı ama buna değdi. Hedeflerin daha az gereksiz bağımlılıklara sahip olduğu göz önünde bulundurulduğunda derlemelerimiz artık çok daha hızlı. Mühendisler de ihtiyaç duymadıkları bağımlılıkları, bunlara bağımlı olan hedefleri kırma konusunda endişelenmeden ortadan kaldırabilecek güçte.

Her zaman olduğu gibi, katı geçişli bağımlılıkları uygulamak bir değiş tokuş yapmayı gerektiriyordu. Sık kullanılan kitaplıkların artık tesadüfi olarak alınmak yerine birçok yerde açıkça listelenmesi gerektiğinden ve mühendislerin BUILD dosyalarına bağımlılık eklemek için daha fazla çaba sarf etmeleri gerektiğinden bu dosyalar daha ayrıntılı bir hale geldi. O günden beri birçok eksik bağımlılığı otomatik olarak algılayıp geliştirici müdahalesi olmadan BUILD dosyalarına ekleyerek bu iş yükünü azaltan araçlar geliştirdik. Ancak bu tür araçlar kullanılmadığında bile, kod tabanı ölçeklendiğinde bu dengenin oldukça değerli olduğunu gördük: BUILD dosyasına açık bir şekilde bağımlılık eklemek tek seferlik bir maliyettir ancak örtülü geçişli bağımlılıklarla ilgilenmek, 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 iç değilse dış olmalıdır. Dış bağımlılıklar, derleme sisteminin dışında derlenip depolanan yapılardaki bağımlılıklardır. Bağımlılık, doğrudan bir yapı deposundan içe aktarılır (genellikle internet üzerinden erişilebilir) ve kaynaktan oluşturulmak 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ümleri 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 karşılaştırması

Derleme sistemleri, dış bağımlılık sürümlerinin manuel veya otomatik olarak yönetilmesine izin verebilir. Derleme dosyası, manuel olarak yönetildiğinde yapı deposundan indirmek istediği sürümü, genellikle 1.1.4 gibi bir anlamsal sürüm dizesi kullanarak açıkça listeler. Otomatik olarak yönetildiğinde, kaynak dosya kabul edilebilir bir sürüm aralığı 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 küçük veya yama sürümlerinin kabul edilebilir olduğunu belirtmek için bağımlılık sürümünün "1.+" olarak beyan edilmesine izin verir.

Otomatik olarak yönetilen bağımlılıklar küçük projeler için elverişli olabilir ancak genellikle önemsiz büyüklükteki veya birden fazla mühendisin üzerinde çalıştığı projelerde felaketin formülüdür. Otomatik olarak yönetilen bağımlılıklardaki sorun, sürümün ne zaman güncelleneceği üzerinde hiçbir kontrolünüzün olmamasıdır. Harici tarafların önemli güncellemeler yapmayacağını (anlamsal sürüm oluşturmayı kullandıklarını iddia ettiklerinde bile) garanti etmenin bir yolu yoktur. Bu nedenle, bir gün işe yarayan bir derleme, nelerin değiştiğini tespit etmenin veya tekrar çalışır duruma getirmenin kolay bir yolu olmadan bozulabilir. Derleme bozulmasa bile takip edilmesi imkânsız dikkatsiz 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 ve eski bağımlılıklarla derlemek için deponun eski bir sürümüne göz atmak mümkündür. Bazel, tüm bağımlılıkların sürümlerinin manuel olarak belirtilmesini gerektirir. Orta düzeylerde bile, sağladığı kararlılık açısından manuel sürüm yönetiminin ek yüküne değer.

Tek Sürüm Kuralı

Bir kitaplığın farklı sürümleri genellikle farklı yapılar tarafından temsil edilir. Bu nedenle teoride, aynı dış bağımlılığın farklı sürümlerinin her ikisinin de derleme sisteminde farklı adlar altında tanımlanamayacağına dair bir neden yoktur. Bu şekilde her hedef, kullanmak istediği bağımlılığın sürümünü seçebilir. Bu durum pratikte birç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ı uygular.

Birden fazla sürüme izin vermeyle ilgili en büyük sorun, elmas bağımlılık sorunudur. A hedefinin, harici bir kitaplığın v1 ve B hedefine bağlı olduğunu varsayalım. B hedefi daha sonra aynı harici kitaplığın v2'sine bir bağımlılık eklemek üzere yeniden düzenlenirse, A hedefi artık dolaylı olarak aynı kitaplığın iki farklı sürümüne bağlı olduğundan hedef A bozulur. Birden fazla sürümü olan üçüncü taraf bir kitaplığa bir hedeften yeni bir bağımlılık eklemek hiçbir zaman güvenli değildir, çünkü söz konusu hedefin kullanıcılarından herhangi biri zaten farklı bir sürüme bağımlı olabilir. Tek Sürüm Kuralının uygulanması bu çakışmayı imkansız hale getirir. Hedef, üçüncü taraf bir kitaplığa bağımlılık eklerse mevcut bağımlılıklar aynı sürümde zaten yer alır ve böylece sorunsuz bir şekilde bir arada bulunabilirler.

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 belirtmesini sağlar. Maven veya Gradle gibi araçlar derleyin ve genellikle her geçişli bağımlılığı varsayılan olarak yinelemeli bir şekilde indirirsiniz. Diğer bir deyişle, projenize tek bir bağımlılık eklemek, toplamda onlarca yapının indirilmesine neden olabilir.

Bu çok kullanışlı bir yöntem: Yeni bir kitaplığa bağımlılık eklerken o kitaplığın geçişli bağımlılıklarını bulup hepsini manuel olarak eklemek çok zor olurdu. Ancak büyük bir dezavantajı da var: Farklı kitaplıklar aynı üçüncü taraf kitaplığının farklı sürümlerine bağlı olabileceğinden, bu strateji zorunlu olarak Tek Sürüm Kuralını 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ı belirleyemezsiniz. Bu aynı zamanda, yeni sürüm bazı bağımlılıklarının çakışan sürümlerini almaya başlarsa bir dış bağımlılığı güncellemenin, kod tabanında alakasız 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. Ne yazık ki sihirli bir değnek yok. Bazel'ın alternatifi, deponun her bir dış bağımlılıklarını listeleyen global bir dosya ve depo genelinde bu bağımlılık için kullanılan açık bir sürümü 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 sunuyor. Bu araç, bir projenin ilk WORKSPACE dosyasını oluşturmak için bir kez çalıştırılabilir ve dosya daha sonra her bağımlılığın sürümlerini ayarlamak için manuel olarak güncellenebilir.

Yine burada da tercih edilen seçenek, kolaylık ve ölçeklenebilirlik arasında. Küçük projeler, geçişli bağımlılıkların yönetimiyle ilgili endişe duymayı tercih etmeyebilir ve otomatik geçişli bağımlılıkları kullanarak bunu halledebilir. Kurum ve kod tabanı büyüdükçe bu stratejinin çekiciliği azalır, çatışmalar ve beklenmedik sonuçlar giderek daha sık ortaya çıkar. Daha büyük ölçekte bağımlılıkları manuel olarak yönetmenin maliyeti, otomatik bağımlılık yönetiminin neden olduğu sorunlarla ilgilenme 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ğunlukla, kaynak kodu sağlamadan kitaplıkların kararlı sürümlerini yayınlayan üçüncü taraflar tarafından sağlanır. Bazı kuruluşlar, kendi kodlarından bazılarını yapılar olarak kullanıma sunmayı tercih edebilir. Böylece, diğer kod parçalarının dahili bağımlılıklar yerine üçüncü taraf olarak bunlara güvenmesini sağlayabilirler. Yapılar yavaş ancak hızlı indiriliyorsa teorik olarak derlemeleri hızlandırabilir.

Ancak bu durum aynı zamanda birtakım ek yük ve karmaşıklık da beraberinde getirir: Bu eserlerin her birini oluşturmaktan ve yapı deposuna yüklemekten birinin sorumlu olması, müşterilerin ise süreçlerin en son sürümle güncel kalmasını sağlamaktan sorumlu olması gerekir. Sistemin farklı parçalarının depodaki farklı noktalardan derlenmiş olması ve kaynak ağacın artık tutarlı bir görünümü olmaması nedeniyle hata ayıklama işlemi de çok daha zor hale gelir.

Derlemesi uzun süren yapılar sorununu çözmenin daha iyi bir yolu, önceden 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 inşa edilen bir yapıya ihtiyaç duyarsa derleme sistemi onu oluşturmak yerine otomatik olarak indirir. Bu, doğrudan yapılara bağlı olmanın tüm performans avantajlarını sağlarken derlemelerin her zaman aynı kaynaktan derlenmiş gibi tutarlı olmasını sağlar. Bu, Google tarafından şirket içinde kullanılan stratejidir ve Bazel, uzak bir önbellek 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 çalışmak doğası gereği risk taşır. Üçüncü taraf kaynak (yapı deposu gibi) çökerse, harici bir bağımlılığı indiremezse derlemenizin tamamı durabileceği için kullanılabilirlik riski ortaya çıkar. Güvenlik riski de vardır: Bir saldırgan üçüncü taraf sistemin güvenliği ihlal edilirse saldırgan referans verilen yapıyı kendi tasarımlarından biriyle değiştirebilir. Böylece derlemenize rastgele kod yerleştirebilir. Güvendiğiniz yapıları kontrol ettiğiniz sunuculara yansıtıp derleme sisteminizin Maven Central gibi üçüncü taraf yapı depolarına erişmesini engelleyerek her iki sorun da azaltılabilir. Karşılığında, bu aynaların bakımı için çaba ve kaynak gerekir. Bu nedenle, aynaları kullanıp kullanmama seçimi genellikle projenin ölçeğine bağlıdır. Her üçüncü taraf yapısının karmasının kaynak depoda belirtilmesini gerektirerek güvenlik sorunu, çok az ek yük ile tamamen önlenebilir. Bu da yapı üzerinde izinsiz değişiklik yapılması halinde derlemenin başarısız olmasına neden olur. Bu sorunu tamamen ortadan kaldıran bir diğer alternatif projenizin bağımlılıklarını tedarik etmektir. Proje bağımlılıklarını tedarik ettiğinde, bunları projenin kaynak koduyla birlikte kaynak veya ikili program olarak kaynak kontrolüne kontrol eder. Bu da 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ı dahili olarak kullanarak Google genelinde referans verilen her üçüncü taraf kitaplığını Google'ın kaynak ağacının kökündeki bir third_party dizininde kontrol eder. Ancak bunun Google'da işe yaramasının nedeni Google'ın kaynak kontrol sisteminin son derece büyük bir monorepoyu işleyebilecek şekilde özel olarak derlenmiş olmasıdır. Bu nedenle, tedarik sağlamak tüm kuruluşlar için bir seçenek olmayabilir.