21 puan yazan hiddenest 2020-12-24 | 2 yorum | WhatsApp'ta paylaş

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 By yaparken 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 By edilmiş 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.2xlarge instance 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

 
gera1d 2020-12-24

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.

 
hiddenest 2020-12-24

Ş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