Aylık ortalama etkinlik sayısının 10 milyarı aştığı bir ortamda, veriyi kısa sürede analiz edip kullanıcı davranışı analizi özelliği (Cohort) sunmak gereken bir durum ortaya çıkıyor.
(örn. son 6 ayda uygulamamızda ayda 100 bin won'dan fazla harcama yapan 30'lu yaşlardaki kadınlar → bunların yeniden ziyaret oranı)
Bu, geliştiricinin şimdiye kadar sadece kullandığı datastore'u bizzat nasıl hayata geçirdiğinin hikayesi.
Kullanıcı davranışı analiz sorgularını gerçekleştirebilmek için…
-
Önceden hesaplanmamış metrikler sorgulanabilmeli (+ yeni analiz türleri de yeniden indeksleme olmadan mümkün olmalı)
-
Etkinlik verisini kullanıcı bazında
Group Byyaparken yüksek kardinaliteli shuffle darboğazı az olmalı
Mevcut bir çözüm mü kullanalım, yoksa kendi çözümümüzü mü geliştirelim diye düşündük
-
Druid başka bir yerde kullanılıyordu ama Pre-Aggregation (yalnızca hesaplanmış değerleri okuma yöntemi) sınırlaması nedeniyle bu özelliği geliştirmek için uygun değildi
-
Snowflake veya Redshift gibi veri ambarları büyük ölçekte işletilebilir, ancak genel amaçlı yapıları nedeniyle hedefe kıyasla gereğinden büyük bir küme çalıştırmak gerekiyor ve bu da pahalıya mal oluyor
-
Funnel, ID eşleştirme gibi çeşitli ihtiyaçları kapsamak için SQL tabanlı DB'lerin sınırları var
Sonunda datastore'u kendimiz yaptık
-
Luft = en baştan kullanıcı ID'si temel alınarak
Group Byedilmiş kullanıcı davranışı analiz sorgularını hızlı çalıştırmaya optimize edilmiş bir datastore -
Golang tabanlı geliştirildi
-
Onlarca TB ölçeğindeki kullanıcı verisini 5'ten az node ile ortalama 3 saniye, en fazla 10 saniye arasında analiz ediyor
-
Genel RDBMS'lerden farklı olarak değişmezlik özelliğine sahip (gerekirse aynı dönemin verisi üzerine yazılıyor) → basit küme tasarımı, karmaşık bir page manager uygulamadan yüksek performans, istenen veri saklama formatını tasarlayabilme
Teknik temeli inceleyelim
- TrailDB (depolama motoru) - kullanıcı ID'si bölümlemeye optimize edilmiş, zaman serisi etkinliklerini saklayan bir rowstore
→ Değerleri sözlükleştirip yalnızca o ID'leri saklıyor
→ Kullanıcı etkinliklerini zamana göre sıralayıp önceki etkinliğe göre artan zaman değeri ile yalnızca değişen sütunları saklıyor (çünkü çoğu kullanıcı özelliği değişmiyor)
→ İndeks yok. Tam tarama şart.
→ Ama şaşırtıcı derecede yüksek sıkıştırma oranı sunuyor (CSV 13GB → ~TrailDB 300mb)
→ Zaman karmaşıklığı O(n) olduğundan, uzay karmaşıklığını azaltmanın yeterli olacağı düşünülmüş
- LLVM (sorgu motoru)
→ Ancak TrailDB yalnızca OR-AND biçimindeki equals işlemlerini sağlıyor; Go'da parse edilen sorguların C, C++ tarafına aktarılması gerekiyor
→ PostgreSQL'in sorguları LLVM JiT ile derlediği fark edilmiş
→ Sorgularda işlev genişletmeleri sık olduğundan, bunları C, C++ ile yazmanın geliştirme maliyetini artırma sorunu böylece önlenebiliyor (Golang tarafında yalnızca LLVM IR üretilip aktarılırsa, C, C++ tarafında JiT derleyip çalıştırmak yeterli)
- İşlem katmanını da kendimiz yaptık
→ MapReduce yaygın kullanılıyor ama Golang kullandığımız için kullanamadık
→ Spark/Hadoop uzun süreli işler için optimize edildiğinden, bağlansa bile performans iyi çıkmıyor
→ Bunu da kendimiz yaptık → https://github.com/ab180/lrmr
→ gRPC + Protobuf + etcd kombinasyonu, tanıdık Spark tasarımından çok şey ödünç alındı
→ Dayanıklılıktan vazgeçelim → performansı uç noktaya taşırsak, arıza olsa bile baştan yeniden çalıştırmak 10 saniyenin altında kalır
→ Büyük ölçekli veri işlemeden kaynaklanan sık buffer overflow (Backpressure) sorununu pull-based event stream yapısına çevirerek çözdük (Kafka, Armeria vb. de bunu kullanıyor)
- Sharding'i de kendimiz uyguladık
→ Shard = historical node
→ Bölümün tarih aralığını sharding anahtarı olarak kullanırsak?
→ Tüm sorgularda zaman var → filtreleme kolay
→ Aynı zaman aralığında benzer hacimde veri var → veri dağıtımı kolay
→ Dağıtık ortam güzel değil…
→ Node düşerse ya da yeni node eklenirse?
→ Depolama alanı dolarsa?
→ Arıza nedeniyle yük tek bir node'a yığılırsa?
→ Druid'in Cost Function'ını özelleştirerek, bölüm tarih aralıkları birbirine ne kadar yakın ve örtüşüyorsa cost'un o kadar yüksek olmasını sağladık
→ Shard erişilebilirliği için aşağıdakileri yaptık
→ Shard bilgisine TTL verip düzenli olarak yeniledik (etcd)
→ Bölümleri S3'e kaydedip, bölüm listesini DynamoDB ile yönettik
Şu anki production durumu
- Yalnızca 4 adet
c5.2xlargeinstance ile 500GB veriyi 15 saniye içinde tarıyor
Gelecek hedefleri (veya yapılması gerekenler)
-
Gerçek zamanlı Funnel analizini 10'dan az node'lu bir kümede yapmak istiyoruz
-
Spark desteği ekleyip ML entegrasyonu gibi kullanım senaryolarını desteklemek istiyoruz
-
TrailDB'nin yerine geçecek kendi column store'umuzu (Ziegel) geliştiriyoruz
→ SIMD ve çok çekirdek optimizasyonu
→ Bitmap Index ile kullanıcı özelliği tabanlı ön filtreleme
2 yorum
traildb eğlenceli. https://www.youtube.com/watch?v=-oPFxSwn0lM İlginçtir. Eski bir video ama traildb tarafında bu süre içinde değişen bir şey olmamıştır.
Şimdi bakınca geliştiricinin blog yazısı da varmış,
https://engineering.ab180.co/stories/introducing-luft
TrailDB’yi ilk kez duydum, şöyle bir şeymiş...
https://github.com/traildb/traildb