İçeriğe geç
academia.sh

Ders 15 / 19

Sürekli Tümleştirme ve Teslimat

Bir değişiklik geldiğinde hattın kaç iş koşturduğunu ölçer. Değişiklik süzgeci yalnız etkilenen modüllerin işlerini çalıştırır; ortak modüle dokunan değişiklikler bütçeyi aşar, süzgeçsiz hat her değişiklikte bütün işleri koşar.

Yayın Güncelleme

Bu derste

  • Sürekli tümleştirme ile sürekli teslimatın aynı hattın iki ucu olduğunu ayırt eder
  • Hattı iş, aşama ve değişiklik süzgeci olarak modeller
  • Bir değişiklik için koşan iş sayısını ölçer ve bütçeyle karşılaştırır
  • Ortak modüle dokunan değişikliklerin neden bütçeyi aştığını açıklar
İçindekiler

Önceki ders sürüm kimliğini elle verdi: bir derleme betiğinde bir tamsayı. Bu sayıyı her değişiklikte birinin artırması, her derlemeyi birinin üretip sınaması gerekir. İş elle yapılırsa atlanır; atlanan iş de ancak bir kullanıcı sorunla karşılaşınca fark edilir. Sürekli tümleştirme (continuous integration) bu işi bir hatta bağlar: her değişiklik geldiğinde derleme ve sınama kendiliğinden koşar. Sürekli teslimat (continuous delivery) aynı hattın ucunu uzatır: sınamadan geçen derleme dağıtıma hazır bir çıktı olarak bekler.

Hat iyi çalıştığında bir değişiklik yeşil ya da kırmızı döner ve kimse neyin koştuğunu sormaz. Soru, hat büyüyüp her değişiklikte uzun süre beklenmeye başlayınca ortaya çıkar. Bu yüzden kursun sorusunu bu derste hatta uyarlıyoruz: bu ölçüm bütçesini kaç örnekte aşıyor? Ölçüm bir değişiklik için koşan iş sayısıdır, bütçe ekibin bir değişiklik başına kabul ettiği iş sayısıdır, her değişiklik bir örnektir.

// Overrun.kt — bir olcumun butcesini kac ornekte astigini siniflandirir
class Budget(val name: String, val limit: Int)

class Overrun(val label: String, val budget: Budget, samples: List<Int>) {
    val total: Int = samples.size
    val over: Int = samples.count { it > budget.limit }
    val verdict: String = when {
        over == 0 -> "icinde"
        over == total -> "hepsinde"
        over * 2 < total -> "azinda"
        else -> "cogunda"
    }

    fun line() {
        println("$label: butce=${budget.name}<=${budget.limit} | asan=$over/$total -> $verdict")
    }
}

fun countOverruns(overruns: List<Overrun>) {
    for (name in listOf("icinde", "azinda", "cogunda", "hepsinde")) {
        println("$name: ${overruns.count { it.verdict == name }}")
    }
}

Overrun sınırı kesin aşan örnekleri sayar ve yaygınlığı dört adla verir: içinde, azında, çoğunda, hepsinde. Kural kaç değişikliğin aştığını görür; bir değişikliğin altı mı yoksa yedi iş mi koşturduğu aşan her örnek için aynıdır.

Hat, İş ve Süzgeç

Hat (pipeline) işlerden kurulur. İş (job), tek bir amacı olan bir adımdır: statik denetim, bir modülün birim sınaması, uygulamanın derlenmesi, aygıt üzerinde koşan sınama. Her iş hangi modüllere bağlı olduğunu söyler. Değişiklik süzgeci (change filter) bu bağı kullanır: bir değişiklik yalnız data modülüne dokunduysa, data ile ilgisi olmayan bir işin koşması gerekmez.

# Platform kodu: iş akışı tanımı
on:
  pull_request:
    paths:
      - "feature/**"
      - "core/**"
jobs:
  unit-feature:
    steps:
      - run: ./derle.sh :feature:test

Bu parça bir iş akışı tanımıdır; çalıştırılmaz ve burada çıktı iddia etmiyoruz. Model, bu ölçümün kuralını kurar; model aracın kendisi değildir. Modelde süzgeç bir küme kesişimidir: iş koşar eğer bağlı olduğu modüllerden biri değişmişse. Hiçbir modüle bağlı olmayan iş, yani bağımlılık listesi boş olan iş, her değişiklikte koşar.

Yukarıdaki parçada paths alanı süzgeçtir: yalnız feature ya da core altındaki dosyalara dokunan bir değişiklik bu iş akışını başlatır. Süzgecin iki yüzü vardır. Yararı, ilgisiz değişikliklerde işi hiç koşturmamasıdır. Riski, yolların elle yazılmasıdır: yeni bir modül eklenip yola yazılmazsa o modüldeki değişiklik hiçbir işi tetiklemez. Modelde bu riski Job sınıfındaki modules kümesi taşır; küme eksik yazılırsa iş koşmaz ve sayı düşer.

// PipelineModel.kt — isleri, degisiklikleri ve degisiklik suzgecini modeller
class Job(val name: String, val modules: Set<String>)

class Change(val label: String, val modules: Set<String>)

fun runs(job: Job, change: Change): Boolean =
    job.modules.isEmpty() || job.modules.any { it in change.modules }

fun jobsForChange(jobs: List<Job>, change: Change): List<String> =
    jobs.filter { runs(it, change) }.map { it.name }

fun pipelineJobs(): List<Job> = listOf(
    Job("denetim", emptySet()),
    Job("birim-cekirdek", setOf("core")),
    Job("birim-veri", setOf("data")),
    Job("birim-ozellik", setOf("feature", "core")),
    Job("birim-uygulama", setOf("app", "core")),
    Job("aygit-testi", setOf("app", "feature")),
    Job("derleme", emptySet()),
)

fun sampleChanges(): List<Change> = listOf(
    Change("yorum", emptySet()),
    Change("veri", setOf("data")),
    Change("ozellik", setOf("feature")),
    Change("uygulama", setOf("app")),
    Change("veri-ozellik", setOf("data", "feature")),
    Change("uygulama-veri", setOf("app", "data")),
    Change("cekirdek", setOf("core")),
    Change("yazim-duzeltme", emptySet()),
)

Yedi iş ve sekiz değişiklik var. denetim ve derleme modül listesi boş olduğu için her değişiklikte koşar; öteki beş iş yalnız bağlı olduğu modül değişince koşar. Sekiz değişikliğin ikisi yalnız yorum ve yazım düzeltmesidir, hiçbir modüle dokunmaz.

Değişiklik Başına İş

Bütçeyi dört iş olarak koyalım: ekip bir değişikliğin dört işten fazlasını beklemesini istemiyor. Ölçüm, her değişiklik için jobsForChange sonucunun boyutudur.

// Main.kt — her degisiklik icin kosan is sayisi olculur, butce dort is
fun main() {
    val jobs = pipelineJobs()
    val changes = sampleChanges()
    for (change in changes) {
        println("${change.label}: ${jobsForChange(jobs, change)}")
    }
    val samples = changes.map { jobsForChange(jobs, it).size }
    println("kosan is: $samples")
    Overrun("degisiklik-basina-is", Budget("is", 4), samples).line()
}
yorum: [denetim, derleme]
veri: [denetim, birim-veri, derleme]
ozellik: [denetim, birim-ozellik, aygit-testi, derleme]
uygulama: [denetim, birim-uygulama, aygit-testi, derleme]
veri-ozellik: [denetim, birim-veri, birim-ozellik, aygit-testi, derleme]
uygulama-veri: [denetim, birim-veri, birim-uygulama, aygit-testi, derleme]
cekirdek: [denetim, birim-cekirdek, birim-ozellik, birim-uygulama, derleme]
yazim-duzeltme: [denetim, derleme]
kosan is: [2, 3, 4, 4, 5, 5, 5, 2]
degisiklik-basina-is: butce=is<=4 | asan=3/8 -> azinda

Üç değişiklik dört işi aşıyor: iki modüle dokunan iki değişiklik ve ortak modüle dokunan bir değişiklik. Sekizin yarısından azı aştığı için yanıt azında. Bu, dersin aşımıdır. Süzgeç çoğu değişikliği bütçenin içinde tutar; aşan örnekler hattın zayıf yeridir.

cekirdek satırı ayrıca bir şey gösterir: yalnız core değişti, ama core üç işin bağımlılığındadır, bu yüzden bu değişiklik tek bir modüle dokunmasına rağmen en geniş koşuyu tetikledi. Ortak modül, hattın içinde bir kavşak gibi davranır: ne kadar çok modülün bağımlı olduğu, o modüldeki bir değişikliğin o kadar çok iş çalıştırması demektir.

DA3. Model iş sayısını sayar ve her işi eşit görür. Gerçek bir hatta bir birim sınaması çok kısa, bir aygıt testi çok uzun sürebilir; model bu farkı kurmaz. Ayrıca modül bağları bir girdidir: gerçekte bağlar derleme betiğindeki bağımlılıklardan çıkar, burada elle yazıldı.

Geçişli Bağımlılık

Şimdiye dek her işin bağlı olduğu modülleri elle yazdık. Gerçekte bir modülün değişmesi, ona bağlı modülleri de etkiler: core değişince core’a dayanan data ve feature, onlara dayanan app da değişmiş sayılır. Bağlar bir grafikte durur ve değişikliğin etkilediği küme grafikten türetilir. Aşağıdaki ölçüm aynı sekiz değişikliği iki yoldan sayar. Birincisi yalnız doğrudan değişen modülleri kullanır. İkincisi değişikliği bağımlı modüllere yayar. İşler bu kez her modül için ayrı bir birim sınamasıdır; denetim ve derleme yine her değişiklikte koşar.

// Main.kt — gecisli bagimlilik hesaba katilinca ayni degisiklikler daha fazla is kosar
val dependsOn = mapOf(
    "data" to setOf("core"),
    "feature" to setOf("core"),
    "app" to setOf("feature", "data"),
)

fun affected(changed: Set<String>): Set<String> {
    var result = changed
    while (true) {
        val next = result + dependsOn.filter { (_, deps) -> deps.any { it in result } }.keys
        if (next == result) return result
        result = next
    }
}

fun main() {
    val jobs = listOf(
        Job("denetim", emptySet()),
        Job("birim-cekirdek", setOf("core")),
        Job("birim-veri", setOf("data")),
        Job("birim-ozellik", setOf("feature")),
        Job("birim-uygulama", setOf("app")),
        Job("derleme", emptySet()),
    )
    val changes = sampleChanges()
    val direct = changes.map { jobsForChange(jobs, it).size }
    val spread = changes.map { jobsForChange(jobs, Change(it.label, affected(it.modules))).size }
    println("dogrudan is: $direct")
    println("gecisli is: $spread")
    val budget = Budget("is", 4)
    Overrun("dogrudan", budget, direct).line()
    Overrun("gecisli", budget, spread).line()
}
dogrudan is: [2, 3, 3, 3, 4, 4, 3, 2]
gecisli is: [2, 4, 4, 3, 5, 4, 6, 2]
dogrudan: butce=is<=4 | asan=0/8 -> icinde
gecisli: butce=is<=4 | asan=2/8 -> azinda

Aynı değişiklikler aynı bütçede doğrudan sayımda içinde, geçişli sayımda azında çıktı. Fark cekirdek satırında en büyüktür: doğrudan sayımda üç iş, geçişli sayımda altı iş koştu, çünkü çekirdek değişince dört modülün hepsi etkilendi. Ortak modülün kavşak olduğunu söylemiştik; bu ölçüm onun maliyetini grafikten türetir. Doğrudan sayım hattı gerçekte olduğundan ucuz gösterir.

DA5. Model bağımlılık grafiğini elle yazılmış bir tablo olarak alır ve döngü içermediğini varsayar. Gerçek bir derleme sistemi grafiği betiklerden çıkarır; burada çıkarma yoktur. Ayrıca model her etkilenen modülün sınamasını koşturur; gerçek bir hat yalnız arayüzü değişen modüllerde koşmayı da seçebilir ve model bunu kurmaz.

Bütçe Bir Karardır

Aynı sekiz değişikliği dört ayrı bütçeyle ölçelim. Bütçe değişince aşım da değişir; bu çelişki değildir, ölçünün kendisidir.

// Main.kt — ayni sekiz degisiklik dort ayri butceyle dort ayri asim verir
fun main() {
    val jobs = pipelineJobs()
    val samples = sampleChanges().map { jobsForChange(jobs, it).size }
    val roomy = Overrun("bes-is", Budget("is", 5), samples)
    val normal = Overrun("dort-is", Budget("is", 4), samples)
    val tight = Overrun("uc-is", Budget("is", 3), samples)
    val minimal = Overrun("bir-is", Budget("is", 1), samples)
    listOf(roomy, normal, tight, minimal).forEach { it.line() }
    countOverruns(listOf(roomy, normal, tight, minimal))
}
bes-is: butce=is<=5 | asan=0/8 -> icinde
dort-is: butce=is<=4 | asan=3/8 -> azinda
uc-is: butce=is<=3 | asan=5/8 -> cogunda
bir-is: butce=is<=1 | asan=8/8 -> hepsinde
icinde: 1
azinda: 1
cogunda: 1
hepsinde: 1

Dört bütçe dört farklı kategori verdi. Beş işlik bütçede hiçbir değişiklik aşmadı, çünkü en geniş koşu beş işti. Bir işlik bütçede ise denetim ve derleme her değişiklikte koştuğu için sekiz değişikliğin hepsi aştı. Burada bir ders çıkar: her değişiklikte koşan işlerin sayısı, süzgecin hiçbir zaman altına inemeyeceği tabandır. Bütçe o tabanın altına konursa, hangi süzgeç kurulursa kurulsun yanıt hepsinde olur.

Teslimat Ucu

Hattın teslimat ucu tümleştirmeden farklı bir soru sorar: bu derleme dağıtıma hazır mı? Burada imza ve paketleme devreye girer. İmzalı bir derleme, yayın anahtarıyla imzalanmış çıktıdır; imza anahtarı hattın gizli girdisidir ve sıradan değişikliklerde koşan işlerden ayrı tutulur. Teslimat adımı bu yüzden her değişiklikte değil, yalnız bir yayın etiketi geldiğinde koşar.

// Platform kodu: imzali derleme
android {
    signingConfigs {
        create("release") {
            storeFile = file(providers.gradleProperty("keystorePath").get())
            keyAlias = providers.gradleProperty("keyAlias").get()
        }
    }
}

Anahtar yolu ve takma adı derleme betiğine gömülmez, hat dışarıdan verir. Bu parçanın çıktısı yoktur; yalnız imzanın yayın girdisi olduğunu gösterir. Teslimat ucundaki iş, bütçeye girmez: onu sayan bir model değişiklik başına değil yayın başına işler ve ders onu ölçmez.

Sınırlayıcı Ölçüm

Bu bölüm süzgecin değerini, süzgeci tümden kaldırarak ölçer; böylece bütçenin ulaşılabilir olup olmadığını görürüz. Süzgeci kaldıralım. Hattın bütün işleri bütün modülleri kapsayacak şekilde yazılır: her iş her değişiklikte koşar. Bu, kurmak için en kolay hattır ve çoğu ekibin ilk hâlidir.

// Main.kt — suzgecsiz hat her degisiklikte butun isleri kosar
fun main() {
    val everyModule = setOf("app", "core", "data", "feature")
    val unfiltered = pipelineJobs().map { Job(it.name, emptySet()) }
    val filtered = pipelineJobs()
    val changes = sampleChanges()
    val budget = Budget("is", 4)

    val withFilter = Overrun("suzgecli", budget, changes.map { jobsForChange(filtered, it).size })
    val noFilter = Overrun("suzgecsiz", budget, changes.map { jobsForChange(unfiltered, it).size })
    listOf(withFilter, noFilter).forEach { it.line() }
    println("modul sayisi: ${everyModule.size}, is sayisi: ${filtered.size}")
}
suzgecli: butce=is<=4 | asan=3/8 -> azinda
suzgecsiz: butce=is<=4 | asan=8/8 -> hepsinde
modul sayisi: 4, is sayisi: 7

Süzgeçli hat aynı bütçede azında, süzgeçsiz hat hepsinde çıktı. Süzgeçsiz hatta bütün işler boş bağımlılık listesiyle yazıldığı için yedi iş her değişiklikte koşar ve sekiz değişikliğin sekizi dört işi aşar. Yorum düzeltmesi bile yedi iş çalıştırır.

Burada kuralın sınırı görünür. Kural, süzgecin bir iş koşmasını engellediğini bilmez; yalnız sayıyı görür. Süzgeç yanlış kurulmuşsa, yani gereken bir iş bağımlılığı eksik yazıldığı için koşmuyorsa, iş sayısı düşer ve hat daha iyi görünür, oysa sınanması gereken bir değişiklik sınanmamıştır. Aşım sayısı hattın doğruluğunu ölçmez; yalnız maliyetini ölçer.

DA4. Model süzgecin doğru kurulduğunu varsayar. Süzgecin yanlış kurulması, yani bir bağımlılığın unutulması modelde yoktur; bunu görmek için her iş için hangi değişikliklerde koşması gerektiğini ayrıca doğrulamak gerekir.

Yaygın Yanılgılar

“Sürekli tümleştirme ve sürekli teslimat aynı şeydir.” Tümleştirme her değişikliği derler ve sınar. Teslimat sınamadan geçen derlemeyi dağıtıma hazır tutar. Bu derste ikincisi ölçülmedi, çünkü teslimat adımı değişiklik başına değil yayın başına koşar.

“Süzgeç kurulduysa hat ucuzlamıştır.” Süzgeç yalnız koşan iş sayısını düşürür. Doğrudan sayım ile geçişli sayım arasındaki fark, yanlış kurulmuş bir süzgecin hattı olduğundan ucuz gösterebileceğini gösterdi. Süzgecin doğruluğu ayrıca denetlenir.

“Daha az iş her zaman daha iyi bir hattır.” Kural iş sayısını sayar; hangi işin atlandığını görmez. Gereken bir iş atlandıysa sayı düşer ama hat kötüleşir. “Hat ne kadar hızlıysa o kadar iyidir.” Bu ders süreyi ölçmedi; yalnız bir değişiklikte koşan iş sayısını saydı. İş sayısı süreyi vermez, çünkü işler paralel koşabilir ve bir işin süresi ötekinden çok farklı olabilir. Süre iddiası ayrı bir ölçüm ister.

“Bağımlılık grafiği bir kez çıkarılır ve değişmez.” Modüller arası bağımlılık her değişiklikte yeniden doğar. Grafik elle tutulursa eskir ve geçişli sayım sessizce yanlış olur; bu dersin modeli grafiği elle yazılmış bir girdi olarak aldı ve gerçek bir hatta grafiğin yapılandırmadan okunması gerekir.

“Her değişiklik her modülü etkiler.” Etkilemez. Yalnız bir modülü değiştiren bir düzenleme, o modüle bağımlı olmayanları koşturmak zorunda bırakmaz; süzgecin işi bu ayrımı yapmaktır.

Özet

  • Bu dersin aşımı: azında. Sekiz değişikliğin üçü dört işlik bütçeyi aştı; yarısından azı aştığı için yanıt azında.
  • Sürekli tümleştirme her değişiklikte derleme ve sınamayı koşturur, sürekli teslimat aynı hattı dağıtıma hazır bir çıktıya uzatır.
  • Süzgeç yalnız bağlı modülü değişen işleri koşturur; ortak modüle dokunan değişiklik üç işi birden tetikleyerek en geniş koşuyu verdi.
  • Aynı değişiklikler beş, dört, üç ve bir işlik bütçelerde sırayla içinde, azında, çoğunda ve hepsinde çıktı; her değişiklikte koşan iki iş tabanı belirler.
  • Bağımlılık grafiği hesaba katılınca aynı değişiklikler daha fazla iş koşturdu: doğrudan sayım içinde, geçişli sayım azında çıktı; doğrudan sayım hattı ucuz gösterir.
  • Süzgeçsiz hat dört işlik bütçeyi sekiz değişikliğin sekizinde aştı: hepsinde. Kural maliyeti sayar, süzgecin doğruluğunu sayamaz.

Sonraki Adım

Hat bir derlemeyi sınadı ve dağıtıma hazır bıraktı. Derlemenin gerçek kullanıcılara ulaşmadan önce geçeceği yollar vardır: iç sınama, kapalı sınama, açık sınama. Sıradaki ders, Test Kanalları, bir derlemenin bu yollardan kaç adımda geçtiğini ölçer.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat