Bağımlılık Yönetimi

Önceki sayfalara bakarken, bir tema tekrar tekrar ortaya çıkar: 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 (örneğin “bir sürümü tamamlandı olarak işaretlemeden önce dokümanı gönderin) ve bazen bir yapıya bağımlılık söz konusu olur (örneğin, “Kodumu oluşturmak için bilgisayar görüşü kitaplığının en son sürümüne sahip olmam gerekir”). Bazen kod tabanınızın başka bir bölümüne veya veri tabanınızın başka bir bölümüne dahili bağımlılıklar vardır. Ancak her durumda, "Bunu yapabilmem için önce buna ihtiyacım var" fikri, derleme sistemlerinin tasarımında tekrar tekrar tekrarlanan bir şeydir ve bağımlılıkları yönetmek, bir derleme sisteminin belki de en temel işidir.

Modülleri ve Bağımlılıkları Yönetme

Bazel gibi yapı tabanlı derleme sistemleri kullanan projeler bir dizi modüle ayrılı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 üzerinde büyük bir etkiye sahip olabilir.

Ayrıntılı Modülleri 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şlev içermesi 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ştirip projenin tüm kaynak dosyalarını yinelemeli olarak globing yaparak tek bir modülden oluşabilir. Diğer uçta, neredeyse her kaynak dosya kendi modülünde oluşturulabiliyordu. Böylece, her dosyanın etkili bir şekilde, bağımlı olduğu tüm dosyaların bir BUILD dosyasında listelenmesi mümkün oluyordu.

Çoğu proje bu uç noktalar arasında bir yerde bulunur ve seçim, performans ile sürdürülebilirlik arasında bir denge gerektirir. Projenin tamamı için tek bir modül kullanmak, dış bağımlılık ekleme dışında hiçbir zaman BUILD dosyasına dokunmanız gerekmediği anlamına gelebilir. Ancak bu, derleme sisteminin her zaman tüm projeyi aynı anda derlemesi gerektiği anlamına gelir. Bu, yapının yapının parçalarını paralelleştiremeyeceği veya dağıtamayacağı ya da önceden oluşturulmuş parçaları önbelleğe alamayacağı anlamına gelir. Dosya başına tek modül seçeneğinin tersi geçerlidir: Derleme sistemi, derlemenin önbelleğe alma ve planlama adımlarında maksimum esnekliğe sahiptir ancak mühendislerin hangi dosyaya referans verdiklerini 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 çoğu zaman dil içinde) değişse de Google, genelde göreve dayalı bir derleme sisteminde yazılabilecek bir modülden çok daha küçük modülleri tercih eder. Google'daki tipik bir üretim ikili programı genellikle on binlerce hedefe dayanır ve orta ölçekli bir ekip bile kod tabanında birkaç yüz 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 tabanlı başka bir derleme sistemi olan Pantolonlar buna 1:1:1 kuralı adını verir). 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ığından ve hedefleri yeniden oluşturma ihtiyacının daha az olmasını sağladığından geniş ölçekte gösterilmeye başlar. Testler resmedildikten sonra avantajlar daha da cazip 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ştiricilere yük bindirilmesini önlemek amacıyla bu dezavantajları azaltma konusunda bazı adımlar attık.

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 izin verir. Bu özellik, hangi diğer hedeflerin buna bağlı olabileceğini belirleyen bir özelliktir. Gizli hedeflere yalnızca kendi BUILD dosyası içinde referans verilebilir. Hedef, açıkça tanımlanmış bir BUILD dosya listesinin 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 en aza indirmek genellikle en iyisidir. Genel olarak Google'daki ekipler, hedefleri yalnızca bu hedefler Google'daki herhangi bir ekibin kullanımına sunulan yaygın olarak kullanılan kitaplıkları temsil ettiğinde herkese açık hale getirir. Kodunu kullanmadan önce başkalarının kendileriyle koordine olmasını gerektiren ekipler, hedef görünürlüğü olarak müşteri hedeflerini içeren bir izin verilenler listesi bulunduracaktır. Her ekibin dahili uygulama hedefleri yalnızca ekibin sahip olduğu dizinlerle sınırlandırılır ve çoğu BUILD dosyası, gizli olmayan tek bir hedefe sahip olur.

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

Modüllerin birbirlerine başvuruda bulunabilmesi gerekir. Kod tabanını ayrıntılı modüllere bölmenin olumsuz tarafı, bu modüller arasındaki bağımlılıkları yönetmeniz gerekmesidir (bununla birlikte, araçlar bunu otomatikleştirmeye yardımcı olabilir). Bu bağımlılıkların ifade edilmesi, 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ımlanmış ve oluşturulmuş başka bir hedef üzerindedir. İç bağımlılıklar, derleme çalıştırılırken önceden oluşturulmuş bir yapı olarak indirilmek yerine kaynaktan derlenmiş olmaları nedeniyle dış bağımlılıklardan farklıdır. Aynı zamanda bu, iç bağımlılıklar için bir "sürüm" kavramı olmadığı anlamına gelir. Bir hedef ve tüm iç bağımlılıkları her zaman depoda aynı kaydetme/düzeltmede oluşturulur. İç bağımlılıklar konusunda 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 bir kitaplık hedefine bağlı olan B hedefine 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

Temel araçlar söz konusu olduğunda bununla ilgili herhangi bir sorun yoktur. Hem B hem de C, oluşturulduğunda A hedefine bağlanacağı için C'de tanımlanan tüm semboller A tarafından bilinir. Bazel buna yıllarca izin verdi ancak Google büyüdükçe sorunlar görmeye başladık. B'nin, artık C'ye bağlı olması gerekmeyecek şekilde yeniden düzenlendiğini varsayalım. 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 açık sözleşmenin bir parçası haline geldi ve asla güvenli bir şekilde değiştirilemez. Bu da, zamanla bağımlılıkların birikmesi ve Google'da geliştirmelerin yavaşlamaya başlaması anlamına geliyordu.

Google nihayetinde 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 referansta bulunmaya ç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 tüm kod tabanına yaymak ve bağımlılıklarını açıkça listelemek için milyonlarca derleme hedefimizden her birini yeniden düzenlemek yıllarca süren bir çabaydı ama buna değdi. Hedeflerin gereksiz bağımlılıklarının daha az olması ve mühendisler, bunlara bağımlı olan 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ı.

Her zamanki gibi katı geçişli bağımlılıkların uygulanması için ödün verilmesi gerekiyordu. Bu durum dosyaların oluşturulmasını daha ayrıntılı hale getirdi. Çünkü sık kullanılan kitaplıkların artık tesadüfi olarak çekilmesi yerine birçok yerde açıkça listelenmesi gerekiyordu ve mühendislerin BUILD dosyalarına bağımlılık eklemek için daha fazla çaba sarf etmeleri gerekiyordu. O zamandan beri, birçok eksik bağımlılığı otomatik olarak tespit ederek ve bunları geliştirici müdahalesi olmadan BUILD dosyalarına ekleyerek bu zahmeti azaltan araçlar geliştirdik. Ancak bu tür araçlar olmasa bile, kod tabanı ölçeklendirildiğinden ödün vermeye değer 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 başa çıkmak, derleme hedefi var 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ış olması gerekir. Dış bağımlılıklar, derleme sisteminin dışında derlenip depolanan yapılar üzerindeki 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

Derleme sistemleri, harici bağımlılık sürümlerinin manuel veya otomatik olarak yönetilmesine olanak tanır. Derleme dosyası, manuel olarak yönetildiğinde yapı deposundan indirmek istediği sürümü açıkça listeler ve genellikle 1.1.4 gibi anlamsal bir sürüm dizesi kullanır. Kaynak dosya otomatik olarak yönetildiğinde, 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ümünün kabul edilebilir olduğunu belirtmek için bağımlılık sürümünün "1.+" olarak bildirilmesine izin verir.

Otomatik olarak yönetilen bağımlılıklar küçük projeler için kullanışlı olsa da genelde önemsiz boyutta 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ın (anlamsal sürüm oluşturmayı kullandığını iddia etseler bile) garanti etmenin bir yolu yoktur. Bu nedenle, bir gün işe yarayan bir derleme, sonraki gün bozulabilir ve neyin değiştiğini algılamanın veya tekrar çalışır hale getirmenin kolay bir yolu olmayabilir. Derleme bozulmasa bile takip edilmesi imkansız oldukça zor davranışlar veya performans değişiklikleri olabilir.

Bunun aksine, 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üzeyde bile olsa manuel sürüm yönetiminin getirdiği ek yük, sağladığı kararlılık açısından oldukça değerlidir.

Tek Sürüm Kuralı

Bir kitaplığın farklı sürümleri genellikle farklı eserlerle temsil edilir. Bu nedenle teoride, aynı dış bağımlılığın farklı sürümlerinin her ikisinin de derleme sisteminde farklı adlarla bildirilememesine neden olmaz. 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ığı sorunudur. A hedefinin, harici bir kitaplığın v1 hedefine ve B hedefine bağımlı olduğunu varsayalım. Hedef B daha sonra aynı harici kitaplığın v2'sine bir bağımlılık eklemek üzere yeniden düzenlenirse, A hedefi artık aynı kitaplığın iki farklı sürümüne dolaylı olarak bağlı olduğundan hedef A bozulur. Birden fazla sürümü olan üçüncü taraf kitaplıklara bir hedeften yeni bağımlılık eklemek asla güvenli değildir. Çünkü bu hedefin kullanıcılarından herhangi biri zaten farklı bir sürüme bağlı olabilir. Tek Sürüm Kuralının uygulanması bu çakışmayı imkansız hale getirir. Bir hedef, üçüncü taraf kitaplığına bağımlılık eklerse mevcut bağımlılıklar zaten aynı sürümde yer alır ve böylece birlikte var olabilirler.

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ıkları belirtmesini sağlar. Maven veya Gradle gibi araçlar geliştirin her geçişli bağımlılığı genellikle varsayılan olarak tekrar tekrar indirir. Bu da projenize tek bir bağımlılık eklemenin 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ı izlemek ve hepsini manuel olarak eklemek çok büyük sıkıntı olurdu. Ancak çok büyük bir dezavantajı da vardır: Farklı kitaplıklar aynı üçüncü taraf kitaplığı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ı bilemezsiniz. Ayrıca yeni sürüm, bazı bağımlılıklarının çakışan sürümlerini almaya başlarsa harici bir bağımlılığın güncellenmesinin kod tabanı genelinde alakasız gibi görünen hatalara yol açabileceğ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 yoktur. Bazel'ın alternatifi, deponun her bir dış bağımlılığını listeleyen global bir dosyaya ve depo genelinde bu bağımlılık için kullanılan açık bir sürüme ihtiyaç duyulmasıdı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 bu dosya daha sonra her bağımlılığın sürümlerini ayarlamak için manuel olarak güncellenebilir.

Burada yine tercihimiz, rahatlık ile ölçeklenebilirlik arasında bir seçenek. Küçük projeler, geçişli bağımlılıkları kendilerinin yönetmeyle 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 ve çatışmalar ve beklenmedik sonuçlar giderek daha sık ortaya çıktıkça bu strateji giderek daha az ilgi çekici hale gelir. Büyük ölçeklerde bağımlılıkları manuel olarak yönetmenin maliyeti, otomatik bağımlılık yönetiminin neden olduğu sorunları ele alma maliyetinden çok daha düşüktür.

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

Dış bağımlılıklar genellikle, 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ının bunlara iç bağımlılıklar yerine üçüncü taraf olarak bağımlı olması mümkün olur. Yapıların derlenmesi yavaş ancak indirmesi hızlıysa bu yöntem, derlemeleri teorik olarak hızlandırabilir.

Ancak bu durum, bir çok ek yükü ve karmaşıklığı da beraberinde getirir: Birinin bu yapıların her birinin derlenmesinden ve bunları yapay havuzuna yüklenmesinden sorumlu olması, müşterilerin ise güncel sürümlerin en güncel sürümünü kullandığından emin olması gerekir. Sistemin farklı parçaları depodaki farklı noktalardan derleneceği ve artık kaynak ağacının tutarlı bir görünümü olmadığı için hata ayıklama ç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 elde edilen yapıları mühendisler arasında paylaşılan bir konuma kaydeder. Böylece, bir geliştirici yakın zamanda başka birinin inşa ettiği bir yapıya ihtiyaç duyarsa derleme sistemi onu derlemek yerine otomatik olarak indirir. Bu, doğrudan yapılara bağlı kalmanı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 risklidir. Üçüncü taraf kaynağın (ör. yapı deposu) çökmesi durumunda kullanılabilirlik riski ortaya çıkar. Bunun nedeni, dış bağımlılık indirememesi durumunda derlemenizin tamamı durma noktasına gelebilir. Bir de 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 ve böylece derlemenize rastgele kod yerleştirebilir. Bağımlı olduğunuz yapıları, 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ımları için çaba ve kaynak gerekir. Bu nedenle, bu 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, küçük bir ek yük ile tamamen önlenebilir. Bu da, yapıya müdahale edilmesi durumunda derlemenin başarısız olmasına neden olur. Bu sorunu tamamen ortadan kaldıran bir diğer alternatif de projenizin bağımlılıklarını temin etmektir. Bir proje bağımlılıklarını tedarik ettiğinde bunları projenin kaynak koduyla birlikte kaynak kontrolünde ya da ikili program şeklinde kontrol eder. Bu da projenin tüm dış bağımlılıklarının etkin bir şekilde iç bağımlılıklara dönüştürülmesi anlamına gelir. Google, bu yaklaşımı dahili olarak kullanır ve Google'da başvurulan her üçüncü taraf kitaplığını Google'ın kaynak ağacının kökündeki third_party dizinine bakarak kontrol eder. Ancak bu yalnızca Google'da işe yarar. Bunun 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çi firma tüm kuruluşlar için bir seçenek olmayabilir.