Ders 12 / 14
Analitik ve Çökme Raporlama
Analitik olayları ve çökme raporları bir gönderim kuyruğundan sunucuya gider; yanıt kaybolursa istemci aynı olayı yeniden dener. Ders gönderilen olayın sunucuya kaç kopya ulaştığını, sınırlı kuyruğun en eski olayları düşürmesini ve tekilleştirmenin teslimi neden değiştirmediğini ölçer.
Yayın Güncelleme
Bu derste
- Analitik olayı, çökme raporu ve çökmesiz oturum kavramlarını ayırır
- Yanıt kaybolunca yeniden denenen olayın sunucuya birden çok kopya ulaştığını ölçer
- Sınırlı kuyruğun dolunca en eski olayları düşürdüğünü kayıp olarak gösterir
- Tekilleştiren sunucunun sonucu koruduğunu, teslimin yine yinelenen kaldığını ayırır
İçindekiler
Önceki derste sürüm aygıta kuruldu. Çalışan uygulamanın ne yaptığını bilmenin yolu, olay göndermektir: bir ekran açıldı, bir sepete ürün eklendi, uygulama çöktü. Bu olaylar üretildikleri anda sunucuya gitmez; bir kuyrukta birikir ve ağ uygun olunca gönderilir. Soru kursun sorusudur: gönderilen bu olay alıcıya kaç kopya olarak ulaştı? Olay raporlanan olaydır, alıcı sunucudur.
// Arrival.kt — gonderilen bir olayin aliciya kac kopya olarak ulastigini siniflandirir
class Signal(val key: String, val seq: Int)
class Arrival(val label: String, val sent: Signal, received: List<Signal>) {
val copies: Int = received.count { it.seq == sent.seq }
val verdict: String = when {
copies > 1 -> "yinelenen"
copies == 1 -> "birebir"
received.any { it.key == sent.key && it.seq > sent.seq } -> "ezilen"
else -> "kayip"
}
fun line() {
println("$label: anahtar=${sent.key} | kopya=$copies -> $verdict")
}
}
fun countArrivals(arrivals: List<Arrival>) {
for (name in listOf("birebir", "yinelenen", "ezilen", "kayip")) {
println("$name: ${arrivals.count { it.verdict == name }}")
}
}
Olay, Rapor ve Oturum
Analitik olayı, kullanıcının uygulamada yaptığı bir adımın kaydıdır: ekran açıldı, düğmeye basıldı. Çökme raporu, uygulama beklenmedik bir hatayla durduğunda üretilen kayıttır; hata türünü ve yığını taşır. Çökmesiz oturum (crash-free session) ise bir oturumun çökmeden bitmesidir ve çökme oranı hesabının paydasını oluşturur. Üçü de aynı yoldan gider: bir kuyruğa girer ve sunucuya gönderilir.
Çökme raporunu üreten kanca platformun çökme raporlama çerçevesinde durur. Uygulama çöktüğünde süreç kapanmadan bir işleyici çağrılır ve rapor kuyruğa eklenir.
// Platform kodu: Thread.UncaughtExceptionHandler
val previous = Thread.getDefaultUncaughtExceptionHandler()
Thread.setDefaultUncaughtExceptionHandler { thread, error ->
reportQueue.enqueue(CrashReport(thread.name, error))
previous?.uncaughtException(thread, error)
}
Kuyruğun boşaltılması arka planda, ağ bağlıyken yapılır.
// Platform kodu: WorkManager
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
val request = OneTimeWorkRequestBuilder<UploadWorker>()
.setConstraints(constraints)
.build()
WorkManager.getInstance(context).enqueue(request)
İki parça platform kodudur ve burada çıktı, süre ya da sayı iddia etmiyoruz. Model, platformun bu kuralını kurar; model platformun kendisi değildir. Yeniden deneme çoğu istemcide üstel geri çekilmeyle yapılır: her başarısız denemeden sonra bekleme uzar. Bu bekleme denemelerin aralığını değiştirir, sayısını değiştirmez; modelde bekleme yoktur.
Yanıt Kaybolursa
Gönderim, iki yarıdan oluşur: istek sunucuya ulaşır, sunucu bir onay yanıtı döner. Yanıt yolda kaybolursa istemci isteğin ulaşıp ulaşmadığını bilemez ve onay gelene kadar aynı olayı yeniden gönderir. Modelde her olay, onayın kaç kez kaybolacağını taşır; her gönderim sunucuya ulaşır, yalnız onay kaybolur.
// Report.kt — onay kaybolan olay, onay gelene kadar yeniden gonderilir
class Report(val signal: Signal, val lostAcks: Int = 0)
fun retryUntilAck(reports: List<Report>): List<Signal> =
reports.flatMap { r -> List(r.lostAcks + 1) { r.signal } }
fun boundedQueue(capacity: Int, queued: List<Signal>): List<Signal> = queued.takeLast(capacity)
fun dedupeById(received: List<Signal>): List<Signal> =
received.distinctBy { it.key to it.seq }
// Probe.kt — gonderilen olaylar icin sunucuya ulasan listeyi alici sayar
fun probe(sent: List<Signal>, received: List<Signal>): List<Arrival> =
sent.mapIndexed { i, s -> Arrival("olay-${i + 1}", s, received) }
Onay Her Seferinde Gelirse
Dört olay gönderilir: iki analitik olayı, bir çökme raporu ve bir çökmesiz oturum kaydı. Hiçbirinin onayı kaybolmaz.
// Main.kt — onay gelirse her olay bir kez gonderilir
fun main() {
val open = Signal("olay.acilis", 1)
val cart = Signal("olay.sepet", 2)
val crash = Signal("cokme.oturum", 3)
val clean = Signal("oturum.saglam", 4)
val reports = listOf(Report(open), Report(cart), Report(crash), Report(clean))
val received = retryUntilAck(reports)
val arrivals = probe(listOf(open, cart, crash, clean), received)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=olay.acilis | kopya=1 -> birebir olay-2: anahtar=olay.sepet | kopya=1 -> birebir olay-3: anahtar=cokme.oturum | kopya=1 -> birebir olay-4: anahtar=oturum.saglam | kopya=1 -> birebir birebir: 4 yinelenen: 0 ezilen: 0 kayip: 0
Dört olayın dördü birer kopya ulaştı. Yeniden denemeye gerek kalmadı.
Aynı Olay Yeniden Denenirse
Şimdi sepet olayının onayı bir kez, çökme raporunun onayı iki kez kaybolur. İstemci onay gelene kadar gönderir ve sunucu her gönderimi ayrı bir kopya olarak alır.
// Main.kt — onayi kaybolan olay tekillestirmeden yeniden denenir
fun main() {
val open = Signal("olay.acilis", 1)
val cart = Signal("olay.sepet", 2)
val crash = Signal("cokme.oturum", 3)
val clean = Signal("oturum.saglam", 4)
val reports = listOf(Report(open), Report(cart, 1), Report(crash, 2), Report(clean))
val received = retryUntilAck(reports)
val arrivals = probe(listOf(open, cart, crash, clean), received)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=olay.acilis | kopya=1 -> birebir olay-2: anahtar=olay.sepet | kopya=2 -> yinelenen olay-3: anahtar=cokme.oturum | kopya=3 -> yinelenen olay-4: anahtar=oturum.saglam | kopya=1 -> birebir birebir: 2 yinelenen: 2 ezilen: 0 kayip: 0
Bu dersin teslimi: yinelenen. Yanıtı kaybolan olay sunucuya ikinci ve üçüncü kez ulaştı. İstemci bu davranışta bir hata yapmadı: onay gelmeyen bir gönderimi yeniden denemek, hiçbir şey göndermemekten güvenlidir. Bedeli sunucuya fazladan kopyadır. Çökme oranı gibi bir sayı sunucuda kopyalarla hesaplanırsa, çöken oturumlar olduğundan fazla sayılır.
Burada bir ayrım yapmak gerekir. Aynı kopyayı iki kez işleyen alıcı olayı yinelemiş olmaz. Yinelenen, bir kopyanın alıcıya ikinci kez ulaşmasıdır; alıcının onu iki kez işlemesi, kopyaların sayısını değiştirmez. Bu derste kopyalar sunucuya ulaşırken sayılır, sunucunun içinde ne olduğuna bakılmaz.
GD1. Model, her gönderimin sunucuya ulaştığını, yalnız onayın kaybolduğunu varsayar. Platformda istek de yolda kaybolabilir; o durumda yeniden deneme olayı yinelemez, ilk kez ulaştırır.
GD2. Model, yeniden denemeyi onay gelene kadar sürdürür ve bir üst sınır koymaz. Platformda deneme sayısı ve bekleme çalışma ilkesinden gelir.
Kuyruk Neden Sınırlıdır
Kuyruğun sınırsız olmaması bir tasarım zorunluluğudur. Aygıtın belleği ve depolaması sınırlıdır; ağ günlerce yoksa ve uygulama her ekran açılışını kuyruğa yazıyorsa kuyruk uygulamanın kendisinden büyük olabilir. Bu yüzden istemciler bir üst sınır koyar ve sınır dolunca iki yoldan birini seçer: en eskiyi düşürmek ya da en yeniyi kabul etmemek.
İki seçimin sonucu aynı değildir. En eskiyi düşüren kuyruk, ağ döndüğünde yakın geçmişi gönderir ve uzak geçmişi kaybeder; en yeniyi reddeden kuyruk uzak geçmişi korur ama ağ döndüğünde güncel durumu bilmez. Çökme raporları için genellikle ikinci seçim daha az zararlıdır, çünkü en eski çökme çoğu kez ilk ve en açıklayıcı olandır. Model ilk seçimi kurar: takeLast(capacity) en yeni olayları tutar.
Kayıp, bu tasarımın bedeli olarak ortaya çıkar ve istemci bunu kendi başına göremez. Düşürülen olay için gönderim hiç denenmediğinden onay beklenmez, hata üretilmez, hiçbir şey kaydedilmez. Alıcı da bir şey bilmez; ona yalnız ulaşanlar görünür. Sayıdaki eksiklik, ancak üretilen olay sayısıyla ulaşan olay sayısı karşılaştırılırsa fark edilir.
Yeniden deneme yukarıda ölçüldü, kuyruk sınırı aşağıda ölçülecek; gerçek bir istemcide ikisi aynı olay akışında çalışır. Kuyruğa giren olayların bir kısmı düşer, kalanlar gönderilir ve gönderilenlerin bir kısmının onayı kaybolur. Aşağıdaki ölçümde beş olay üç yerlik kuyruğa girer; kuyrukta kalan ödeme olayının onayı bir kez kaybolur.
Sınırlayıcı Ölçüm
Yeniden denemenin tek riski kopya değildir. Ağ uzun süre yoksa olaylar kuyrukta birikir ve kuyruğun boyu sınırlıdır. Kuyruk dolunca yenisine yer açmak için en eski olaylar düşer. Beş olay birikir ve kuyruk üç olay alır.
// Main.kt — sinirli kuyruk dolunca en eski olaylar dusurulur
fun main() {
val sent = listOf(
Signal("olay.acilis", 1),
Signal("olay.sepet", 2),
Signal("cokme.oturum", 3),
Signal("olay.odeme", 4),
Signal("oturum.saglam", 5),
)
val received = boundedQueue(3, sent)
val arrivals = probe(sent, received)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=olay.acilis | kopya=0 -> kayip olay-2: anahtar=olay.sepet | kopya=0 -> kayip olay-3: anahtar=cokme.oturum | kopya=1 -> birebir olay-4: anahtar=olay.odeme | kopya=1 -> birebir olay-5: anahtar=oturum.saglam | kopya=1 -> birebir birebir: 3 yinelenen: 0 ezilen: 0 kayip: 2
En eski iki olay düştü ve sunucuya hiç ulaşmadı. Anahtarları farklı olduğu için bunları yerinden eden başka bir kopya yoktur; kural onları kayıp sayar. Kuyruk en eskiyi attığı için ilk açılış olayları kaybolur, en yeni olaylar kalır. Bu seçim bir politikadır: çökme raporu yeterince eskiyse o da düşebilir.
Şimdi sunucunun olay kimliğiyle tekilleştirdiğini düşünelim. Önceki yeniden deneme senaryosu aynen yaşanır, ama sunucu aynı anahtar ve sırayla gelen kopyaları tek kayda indirir.
// Main.kt — tekillestiren sunucu sonucu korur ama teslim yinelenen kalir
fun main() {
val open = Signal("olay.acilis", 1)
val cart = Signal("olay.sepet", 2)
val crash = Signal("cokme.oturum", 3)
val clean = Signal("oturum.saglam", 4)
val reports = listOf(Report(open), Report(cart, 1), Report(crash, 2), Report(clean))
val received = retryUntilAck(reports)
val arrivals = probe(listOf(open, cart, crash, clean), received)
arrivals.forEach { it.line() }
countArrivals(arrivals)
println("ulasan: ${received.size}")
println("saklanan: ${dedupeById(received).size}")
}
olay-1: anahtar=olay.acilis | kopya=1 -> birebir olay-2: anahtar=olay.sepet | kopya=2 -> yinelenen olay-3: anahtar=cokme.oturum | kopya=3 -> yinelenen olay-4: anahtar=oturum.saglam | kopya=1 -> birebir birebir: 2 yinelenen: 2 ezilen: 0 kayip: 0 ulasan: 7 saklanan: 4
Sunucu yedi kopyayı dört kayda indirdi, yani sonuç doğrudur: çökme oranı kopyalarla şişmez. Ama teslim yine yinelenendir: sunucuya yedi kopya ulaştı ve kural ulaşan kopyaları sayar. Tekilleştirme alıcının içindeki bir iştir; teslim ise kopyanın alıcıya ulaşmasıdır. İkisi birbirini değiştirmez.
Yeniden deneme ve kuyruk birlikte çalışınca iki etki aynı akışta görünür.
// Main.kt — dusen olaylar kayip, onayi kaybolan olay yinelenen olur
fun main() {
val sent = listOf(
Signal("olay.acilis", 1),
Signal("olay.sepet", 2),
Signal("cokme.oturum", 3),
Signal("olay.odeme", 4),
Signal("oturum.saglam", 5),
)
val queued = boundedQueue(3, sent)
val reports = queued.map { Report(it, if (it.seq == 4) 1 else 0) }
val received = retryUntilAck(reports)
val arrivals = probe(sent, received)
arrivals.forEach { it.line() }
countArrivals(arrivals)
}
olay-1: anahtar=olay.acilis | kopya=0 -> kayip olay-2: anahtar=olay.sepet | kopya=0 -> kayip olay-3: anahtar=cokme.oturum | kopya=1 -> birebir olay-4: anahtar=olay.odeme | kopya=2 -> yinelenen olay-5: anahtar=oturum.saglam | kopya=1 -> birebir birebir: 2 yinelenen: 1 ezilen: 0 kayip: 2
Aynı akışta üç farklı teslim görüldü. Kayıp kuyruğun sınırından, yinelenen kaybolan onaydan gelir; ikisi birbirinden bağımsızdır ve biri diğerini düzeltmez. Kuyruğa girmeyen olay yeniden denenmediği için hiçbir zaman yinelenen olmaz; yeniden denenen olay da kuyruktan düşmüş olamaz, çünkü gönderim kuyrukta kalan olaylar için yapılır.
GD3. Model, tekilleştirmeyi anahtar ve sıra çiftiyle yapar. Platformda sunucu bir olay kimliği, bir zaman damgası ya da ikisini birlikte kullanabilir; model yalnız birincisini kurar.
Sunucunun Yapacakları
Bu dersin alıcısı sunucudur ve kopyaları ondan başkası düzeltemez. Üç iş sıralanır.
- Her olaya bir kimlik vermek ve kimliğe göre tekilleştirmek. Kimliği istemci üretir ve yeniden denemede aynı kimlik gönderilir; sunucu aynı kimliği ikinci kez gördüğünde olayı saymaz. Kimlik her denemede yeni üretilirse tekilleştirme çalışmaz, çünkü sunucu iki kopyayı iki ayrı olay olarak görür.
- Tekilleştirme penceresini bilmek. Sunucu gördüğü kimlikleri sonsuza dek tutamaz. Pencere kısa tutulursa geç gelen kopya yeni olay sayılır; uzun tutulursa bellek şişer. Pencerenin uzunluğu, istemcinin yeniden deneme süresinden büyük olmalıdır.
- Kopya sayısını ayrıca saymak. Tekilleştirme sonucu korur, ama kaç kopya geldiğini gizler. Yeniden deneme oranı sağlıklı bir ağın göstergesidir; kopya sayısı yükselmişse onaylar kayboluyordur ve bu, sonucu değiştirmese de bilinmesi gereken bir şeydir.
Kopya sayısını sağlıklı ağ ile karşılaştırmak için sunucunun elinde bir taban olmalıdır: onayı kaybolmayan olayların oranı bilinmeden kopya artışı yorumlanamaz.
İstemci tarafında yapılacak iş bir olayı bir kez üretip aynı kimlikle göndermektir. Kimliğin üretildiği an, yeniden denemeden önce olmalıdır.
Yaygın Yanılgılar
Geri çekilme sayıyı değil aralığı değiştirir. Üstel geri çekilme, istemcinin ağı yormadan denemesini sağlar; ama onay gelene kadar gönderen istemci için kopya sayısı yine kaybolan onay sayısının bir fazlasıdır. Geri çekilme, sunucunun aynı anda gelen kopyalardan korunmasına yardım eder, kopyaları ortadan kaldırmaz.
Kuyruğa yazılan olay sunucuya ulaşmış sayılmaz. Üretim ile ulaşım arasındaki aralıkta olay kuyruktadır ve sürecin kapanması bu aralıkta olayı yok edebilir. Ölçüm kuyruğun sürekli bellekte kaldığını varsayar; kalıcı olmayan bir kuyruk uygulama kapanınca bütün bekleyenleri kaybeder.
Tekilleştiren sunucuda yinelenen yoktur. Vardır. Ölçümde yedi kopya dört kayda indi ve teslim yine yinelenen 2 kaldı. Tekilleştirme sonucu düzeltir, ulaşan kopyaların sayısını değiştirmez.
Yeniden deneme olayın kaybolmasını önler. Yalnız onayı kaybolan olayı önler. Kuyrukta düşen olay hiç gönderilmediği için yeniden deneme ona ulaşmaz; ölçümde düşen iki olay yine kayıp kaldı.
Kuyruk büyüdükçe daha güvenlidir. Büyük kuyruk daha az olay düşürür, ama aygıtın belleğini ve ağ döndüğünde gönderilecek yükü artırır. Sınır bir güvenlik değil, bir dengedir.
Bir olayın kimliği, sunucunun ona bakmasına bağlıdır. Kimliği yalnız istemci taşıyorsa sunucu kopyayı ayıramaz. Tekilleştirme iki tarafın birlikte uyguladığı bir sözleşmedir: istemci aynı kimliği gönderir, sunucu aynı kimliği tek kez sayar.
Çökme raporu da diğer olaylar gibi sayılır. Sayılır, ve bu yüzden kopyalı sayılan çökme raporu çökme oranını şişirir. Ölçümde çökme raporu üç kopya olarak ulaştı; tekilleştirme yoksa o oturum üç çökme sayılırdı.
Özet
- Bu dersin teslimi: yinelenen. Onayı kaybolan olay, tekilleştirmeden yeniden denenince sunucuya birden çok kopya ulaşır.
- Analitik olayı bir adımı, çökme raporu bir çöküşü, çökmesiz oturum bir sağlam bitişi kaydeder; üçü aynı kuyruktan gider.
- Onay her seferinde gelirse dört olayın dördü birebir ulaşır. Onayı kaybolan iki olayda yinelenen 2, birebir 2 olur.
- Beş olay üç yerlik kuyruğa girince en eski ikisi düşer: kayıp 2, birebir 3.
- Yeniden deneme ve kuyruk birlikte çalışınca kayıp 2, birebir 2, yinelenen 1 görülür; ikisi birbirini düzeltmez.
- Olay kimliğiyle tekilleştiren sunucu yedi kopyayı dört kayda indirir, ama teslim yine yinelenen 2’dir. Aynı kopyayı iki kez işleyen alıcı olayı yinelemiş olmaz.
Sonraki Adım
Olaylar sunucuya ulaşıyor ve uygulamanın üretimde nasıl davrandığı görünüyor. Davranışı değiştirmek için her seferinde yeni sürüm yayımlamak gerekmez. Sonraki derste uygulama sunucudan bir yapılandırma getirir; etkinleştirmeden önce iki kez getirilen yapılandırmada aynı anahtarın son değeri kazanır.
İlerlemeni kaydetmek ve not almak için Giriş yap
Notlarım
Not almak için giriş yapmalısın.