
"Öğrenci bu videonun gerçekte ne kadarını izledi?" sorusunun tek satırlık bir cevabı varmış gibi görünür. Oynatıcı oynatma imlecinin nerede olduğunu bilir, saniyeleri toplarsınız, olur biter. Sonra ham olayları açarsınız ve o tek satır dağılır: hiç kaydedilmemiş duraklatmalar, dersin ortasında kapatılan sekmeler, saati dakikalarca kaymış telefonlar, 2x hızda izleyenler, bir ispatı üçüncü kez görmek için kaydırıcıyı geri çekenler.
Büyük, Farsça bir çevrim içi eğitim platformu için bu telemetriyi insanların güvenebileceği olgulara dönüştüren pipeline'ı yeniden kurdum. Pipeline her öğrenci ve her video için şu soruları yanıtlıyor: kaç ayrı saniye izlendi, hangi bölümler tekrar izlendi ya da atlandı, video gerçekten bitirildi mi ve izleme günlere nasıl dağıldı. Çalışma, başka iki geliştiricinin 2024'te yazdığı bir prototipten yola çıktı; 2026'da ingest servisini, analiz motorunu, depolama modelini ve testleri yeniden ele aldım. Bu not, önemli olduğu ortaya çıkan kararları adım adım anlatıyor.
Oynatıcı ne gönderiyor
Web oynatıcısı bir şey olduğunda küçük bir JSON olayı gönderir: oynatma, duraklatma, atlama (seek), hız ya da kalite değişikliği ve periyodik bir "hâlâ oynatılıyor" heartbeat'i. Her olay videodaki konumu, bir istemci zaman damgasını ve öğrenen ile videonun kimliklerini taşır. Video dosyasının kendisi hiçbir zaman analiz edilmez. Aşağıdaki her şey bu olaylardan çıkarılır.
Tasarımın tamamını iki özellik belirliyor. Olayları göndermek ucuzdur ve olaylar büyük hacimlerde gelir; bu yüzden veri alımı oynatıcıyı asla yavaşlatmamalıdır. Ayrıca olaylar, istisnai değil olağan sayılacak biçimlerde güvenilmezdir; bu yüzden analiz her olaya tek tek güvenmek yerine iki olay arasında muhtemelen ne olduğu üzerine akıl yürütmek zorundadır.
Ingest: hızlı kabul et, sonra yaz
Ingest servisi, .NET 10 üzerinde çalışan bir ASP.NET Core minimal API'dir. İstek yolu neredeyse hiçbir şey yapmaz: olayı kapasitesi sınırlı, bellek içi bir kanala koyar ve 202 Accepted döner. Kanal dolduğunda problem details gövdesiyle 503 yanıtı verir; böylece sunucu çökmek yerine istemciler geri çekilir. Endpoint'in basitleştirilmiş hali:
app.MapPost("/events", (PlayerEvent e, EventBuffer buffer) =>
buffer.TryEnqueue(e)
? Results.Accepted()
: Results.Problem(title: "The event buffer is full.",
statusCode: StatusCodes.Status503ServiceUnavailable));
Bir arka plan worker'ı kanalı boşaltır ve bir batch dolduğunda ya da bir zamanlayıcı tetiklendiğinde, hangisi önce olursa, birikenleri yazar (flush). Her flush batch'i geçici tablolara yükler ve tek bir transaction içinde küme tabanlı tek bir upsert uygular; böylece SQL Server binlerce küçük işlem yerine birkaç büyük işlem yapar.
Benzer her sistemde tekrarlayacağım ayrıntı, başarısız bir batch'in nasıl ele alındığı. SQL Server bir batch'i bir veri hatasıyla (bir kısıt ihlali, fazla uzun bir değer, başarısız bir dönüşüm) reddederse, worker o batch'i satır satır yeniden dener, yine başarısız olan satırları bir hata tablosuna yazar ve yoluna devam eder. Bozuk tek bir olay, arkasında sırada bekleyen binlerce sağlam olayı engelleyemez. Altyapı hataları farklı ele alınır: batch tamponda kalır ve daha sonra yeniden denenir, çünkü veri sağlamdı, sorun veritabanındaydı.
Buradaki ödünleşim bilinçli ve yazılı olarak kayda geçmiş durumda: süreç çökerse bellek içi tampon, içinde ne varsa kaybeder. Davranışsal telemetri için bu, milisaniyelerle ölçülen bir istek yolu karşılığında kabul edilebilir bir bedel. Ödemeler için olmazdı. Ayrı bir watchdog, beklenenden uzun süre hiç olay gelmediğinde ekibi uyarır; bu da bozuk bir oynatıcı sürümünü herhangi bir dashboard'dan daha hızlı yakalar.
12 çekirdekli bir iş istasyonunda, 50 eşzamanlı kullanıcıyla 10.000 olayın yeniden oynatıldığı bir Locust benchmark'ında her istek hatasız kabul edildi: saniyede yaklaşık 2.450 istek, 13 ms medyan gecikme ve 21 ms 95. yüzdelik değer. Servisin bellek kullanımı en fazla yaklaşık 108 MB'a çıktı.
Olaylardan izlenen saniyelere
Analiz motoru hiçbir bağımlılığı olmayan sade bir C# kodu; bu da onu kapsamlı biçimde test etmeyi kolaylaştırdı. Her (öğrenen, video) çifti için olay geçmişinin tamamını zaman sırasıyla okur ve oynatma oturumlarını yeniden oluşturur. İşin büyük kısmını dört kural yapar.
1. İlerleme fiziksel olarak mümkün olmalı
Ardışık iki olay arasında oynatma imleci belirli sayıda video saniyesi ilerlerken gerçek zamanda da belirli sayıda saniye geçmiştir. Video, izin verilen en yüksek oynatma hızının (2x) açıklayabileceğinden daha fazla ilerlemişse o segment izlenmiş sayılmaz; başka bir şey olmuştur, genellikle geç kaydedilmiş ya da hiç kaydedilmemiş bir atlama. İleriye doğru hiç ilerleme olmayan ya da videonun kendisinden uzun olan segmentler de aynı nedenle reddedilir.
static bool IsPlausible(double videoSeconds, TimeSpan wallClock,
double videoLength, double maxSpeed = 2.0) =>
videoSeconds > 0
&& videoSeconds <= videoLength
&& videoSeconds <= maxSpeed * wallClock.TotalSeconds;
2. Saniyeleri değil, saniyenin onda birlerini sayın
Kapsama 0,1 saniyelik çözünürlükle tutulur; yarı açık aralıklar kullanıldığı için bitişik iki segment ortak sınırlarını hiçbir zaman iki kez saymaz. Bir video saniyesi, ancak onda birlik on diliminin en az yarısı kapsanmışsa izlenmiş sayılır. Bu, birbirine zıt iki hatayı önler: 12,9'a düşen bir atlamaya bütün bir saniye yazmak ve iki kısa parça halinde izlenmiş bir saniyeyi kaybetmek.
3. Kimsenin kaydetmediği duraklatmaları çıkarsayın
Video oynarken oynatıcı düzenli aralıklarla heartbeat gönderir. Bu da sessizliği bilgi taşıyan bir şeye dönüştürür. Son olaydan bu yana geçen süre, izin verilen en düşük hızda oynatmanın açıklayabileceği süreden (artı bir tolerans) çok daha uzunsa, öğrenen neredeyse kesinlikle sunucuya bir duraklatma olayı ulaşmadan izlemeyi bırakmıştır: kapatılmış bir sekme, kopan bir bağlantı, kapağı kapatılmış bir dizüstü bilgisayar. Motor bütün boşluğu izlenmiş saymak yerine, güvenebileceği son konuma sanal bir duraklatma ekler.
Aynı yeniden oluşturma mantığı diğer gündelik durumları da kapsar. Bir atlama, oynatmayı hedef konumdan sürdürür. Bir ayar değişikliği, zaten kapanmış bir oturumu yeniden açamaz. Geçmişin sonunda hâlâ açık olan bir oturum, bilinen son konumunda kapatılır.
4. Tekrar izleme, tekrar izleme gibi görünmeli
Birden fazla kez izlenen saniyeler öğretmenlerin ilgisini çeker, çünkü dersin zor kısımlarına işaret ederler. Ama bir kelimeyi yakalamak için iki saniye geri saran bir öğrenen hiçbir şeyi tekrar izlemiş olmaz. Motor tekrarlanan bir aralığı ancak içindeki her saniye en az iki kez izlenmişse ve kesintisiz dizi en az dört saniye sürüyorsa raporlar; dizinin içindeki bir saniyelik boşluğa tolerans gösterir. Atlanan aralıklar aynı zaman çizelgesinden, bu kez öbür yönden okunarak elde edilir.
Zaman göründüğünden daha zordur
Daha küçük iki karar, tuhaf sonuç türlerini topluca ortadan kaldırdı. Birincisi istemci saatleriyle ilgili: bir olayın kendi zaman damgası ancak sunucunun olayı aldığı andan en fazla 30 dakika farklıysa kullanılır; aksi halde sunucu zamanı geçerli olur. Yanlış güne ayarlanmış bir telefon artık birinin çalışmasını bir sonraki aya itemez.
İkincisi gece yarısı. Öğrenme analitiği için gün bazında kapsama önemlidir ("her gün biraz çalıştı mı?"); bu yüzden gece yarısını geçen bir segment bölünür. Segment içinde ilerlemenin düzgün olduğu varsayılır ve her video saniyesi, orta noktasını içeren güne atanır. Tarih boyutu İran takvimi alanlarını da taşır, çünkü raporları okuyanlar İran takvimindeki aylarla düşünür.
Sonuçların saklanması
Sonuçlar SQL Server'daki küçük bir yıldız şemaya yazılır: tarih ve video boyutları, ayrıca çift bazında izleme, günlük kapsama, saniye bazında eylemler, atlanan aralıklar ve tekrarlanan saniyeler için fact tabloları. Batch'ler SqlBulkCopy ile, Multiple Active Result Sets özelliği bilerek kapalı tutulan bir bağlantı üzerinden yazılır; büyük asenkron bulk copy işlemleriyle MARS'ı aynı bağlantıda bir arada kullanmaktan kaçınmak en iyisidir.
Yeniden hesaplama tasarım gereği idempotent. Bir çift için yeni olaylar geldiğinde motor o çiftin tüm geçmişini yeniden hesaplar ve onu üreten görevle etiketlenmiş taze bir snapshot yazar, ardından aynı çiftlerin eski snapshot'larını siler. Bir görevi yeniden çalıştırmak ya da çakışan iki görev çalıştırmak, hiçbir şeyi iki kez saymadan aynı duruma yakınsar. Görevlerin kendisi, olay log'u üzerinde bir watermark tutan bir kuyruk tablosundaki satırlardır; böylece yeni olaylar artımlı olarak işlenir ve yarıda kalan bir çalışma kaldığı yerden devam edebilir.
Cevaplarını bilmediğiniz bir motoru test etmek
Bu tür bir motoru test etmenin zor kısmı test oracle'ıdır: karmaşık bir geçmiş için doğru cevap nedir? Sonunda her test geçmişi için beklenen değerleri, motoru üzerinde çalıştırmadan önce bağımsız olarak hesaplamaya ve ancak ondan sonra karşılaştırmaya karar verdim. Test paketi 282 başarılı teste ulaştı: belirli örüntüleri (boşluklar, atlamalar, heartbeat'ler, gece yarısı, hız değişiklikleri) yakalamak için seçilmiş geçmişler, rastgele örneklenmiş geçmişler ve yukarıdaki her kural için elle kurulmuş küçük senaryolar.
Birim testlerinin üstünde bir denetim katmanı var. Aynı kuralların Python ile yazılmış ayrı bir implementasyonu, rastgele seçilmiş bir çift örneklemi için ham olayları okur, beş fact tablosunun hepsini yeniden hesaplar ve sonuçları C# pipeline'ının sakladıklarıyla karşılaştırır. Son denetimde örneklenen her çift her tabloda eşleşti. Farklı dillerde yazılmış iki implementasyonun karmaşık veri üzerinde aynı sonucu vermesi, ikisinden birinin kendi testlerini geçmesinden çok daha güçlü bir işarettir.
Sırada neyi değiştirirdim
- Analiz, çiftleri tek bir döngüde birbiri ardına işliyor. Bu basit ve doğru, ama iş tamamen paralelleştirilebilir (embarrassingly parallel) ve çiftleri worker'lar arasında bölmek bariz bir sonraki adım.
- Sonuç nesneleri
SqlBulkCopyiçin reflection ileDataTable'lara dönüştürülüyor. Tipli birIDataReader, büyük batch'lerde bellek ayırmalarını azaltır. - Ingest tamponu ve hareketsizlik watchdog'u tasarım gereği tek instance olarak çalışıyor. Mevcut ölçekte bu sorun değil; servis bir gün birden fazla node üzerinde çalışırsa ilk ele alınacak şey bu olur.
Oynatıcı telemetrisinden etkileşimi ölçüyor ve kurallara ya da pipeline'a ikinci bir gözün bakmasını istiyorsanız, bu üstlendiğim türden bir backend ve veri işi: Benimle çalışın sayfasına göz atın.
Bu not İngilizce ve Farsça olarak da yayımlandı: İngilizce · Farsça.
