BEAM Binaries'in Gizemini Çözmek: DNS Kütüphanemizi Nasıl Işık Hızında Yaptık (Hiç NIF Kullanmadan)
NameOcean'ta yaptığımız gibi alan adı kaydı, DNS barındırma ve bulut altyapısı gibi süreçleri yöneten bir platform çalıştırıyorsanız, performans sadece "olsa iyi olur" dediğimiz bir özellik değil, güvenilirliğin temelidir. Bu yüzden Erlang ekosisteminden gelen zekice optimizasyon hikayelerini gördüğümüzde heyecanlanıyoruz ve favorilerimizden birini sizin için detaylandırmak istiyoruz.
Geçenlerde, DNSimple ekibinin açık kaynaklı DNS kütüphaneleri olan dns_erlang'ı optimize etme çalışmaları hakkında hazırladıkları büyüleyici bir derinlemesine incelemeye rastladık. Sonuçları çarpıcıydı: Hiçbir NIF (Native Implemented Function) gerektirmeden, sadece saf BEAM optimizasyonlarıyla sorgu başına %12 daha az CPU kullanımı ve saniyede %14 daha fazla sorgu karşılama oranı elde ettiler. Elixir veya Erlang ile yüksek throughput'lu (işlem hacimli) sistemler inşa eden herkes için bu, mutlaka okunması gereken bir metin.
Bunu neyin mümkün kıldığını inceleyelim.
BEAM Binary Zihinsel Modeli
Bu, birçok Elixir geliştiricisini şaşırtan bir durumdur: Sıkı bir döngü içinde binary'lere ekleme yapmak (append), sandığınız kadar maliyetli değildir.
Şöyle bir kod yazdığınızda:
def build_response(acc, []) do
acc
end
def build_response(acc, [h | t]) do
build_response(<<acc::binary, h>>, t)
end
Bunun O(n²) olmasını bekleyebilirsiniz; yani her yinelemede yeni bir binary oluşturulur ve her şey kopyalanır. Ancak BEAM bundan çok daha zekidir.
Binary'ler değişmez (immutable) olduğu için derleyici, eğer acc az önce bir önceki ekleme işlemiyle oluşturulduysa ve ona referans veren başka hiçbir şey yoksa, doğrudan mevcut belleğe yazmanın güvenli olduğunu kanıtlayabilir. İlk ekleme işlemi biraz boş kapasite ayırır ve sonraki eklemeler bunun içine yazılır. Sonuç mu? Ekleme başına amortize edilmiş O(1) maliyeti; yani tıpkı C++'taki std::vector yapısının çalıştığı gibi.
Peki ya püf noktası? Eğer binary'niz "kaçarsa" (yani bir process sözlüğüne kaydedilirse, bir çağırana geri döndürülürse veya başka bir şeyin üzerine eklenirse), eğlence biter. Tekrar kopyalama aşamasına geri dönersiniz.
Bu desen, binary protokol işleme süreçlerinin her yerinde karşımıza çıkar. DNS yanıtlarını yinelemeli olarak oluştururken, o biriktirici (accumulator) akışını sıkı tutmak hayati önem taşır.
Sub-binaries: Ücretsiz Dilimleme (Çoğunlukla)
Binary'ler üzerinde eşleştirme (matching) yapmak, BEAM'in "sub-binary" dediği şeyi oluşturur; bunlar, kopyalama yapmadan orijinal verinin içine işaret eden minik terimlerdir. Şöyle yazdığınızda:
<<Header:12/binary, Rest/binary>> = Packet
Hiçbir veri kopyalanmaz. Rest sadece orijinal pakete bakan bir penceredir.
Ancak insanları yanıltan ince detay şudur: Rekürsif olarak eşleştirme yaptığınızda, BEAM dahili olarak bir eşleştirme bağlamı (match context) tutar; bu, esasen baytlar arasında yürüyen bir imleç gibidir. Siz onu gerçekten "kaçırmadığınız" sürece (yani geri döndürmediğiniz, saklamadığınız veya bir yerde asılı kalabileceği bir yere aktarmadığınız sürece) bir sub-binary oluşturmaz.
Bu, DNS ayrıştırma (parsing) işlemleri için çok önemlidir. Her sorgu bir binary olarak gelir, protokol yapısıyla eşleştirilir ve bir yanıt üretmesi gerekir. Ne zaman yeni bir iş yarattığınız ile ne zaman sadece veriye baktığınız arasındaki fark, yük altında tıkanan bir ayrıştırıcı ile ölçeklenebilen bir ayrıştırıcı arasındaki farktır.
Tam Sayı (Integer) Temsili Neden Sandığınızdan Daha Önemlidir?
64-bit bir BEAM üzerinde, tam sayılar tip etiketleri için birkaç bit kaplar. Bu da gerçek değer için 60 bit bırakır. Bu alana sığan tam sayılar "küçük tam sayılar"dır (small integers); bunlar doğrudan process register'larında yaşayan ve heap'e (yığın belleğe) hiç dokunmayan anlık değerlerdir.
Daha büyük olan her şey bir "bignum" haline gelir; bu da heap tahsisi ve çöp toplama (garbage collection) düşüncesini gerektirir. Bu kulağa teorik gelebilir ancak binary protokol işlerinin gerektirdiği o bit düzeyindeki işlemler (bit-twiddling) yapıldığında kritik hale gelir.
Bu tekniğe SWAR (SIMD Within A Register) denir; birden fazla değeri tek bir tam sayıya paketleyerek CPU seviyesinde paralel olarak işleyebilmenizi sağlar. DNS için bu, her işlemde birçok baytlık verinin ayrıştırılması anlamına gelir. Ancak bu, sadece paketlediğiniz değerler "küçük tam sayı" aralığına sığarsa işe yarar.
Sığmadıklarında, sadece bellek tahsis maliyetine katlanmazsınız; aynı zamanda normalde var olmayacak çöp toplama döngülerini tetikleme potansiyeline sahip olursunuz. Saniyede milyonlarca sorgu alındığında, bu döngüler birikir.
Bu Kodunuz İçin Ne Anlama Geliyor?
İşte pratik çıkarım: Ciddi performans kazanımları elde etmek için NIF'ler yazmanıza veya C diline başvurmanıza gerek yok. BEAM'in veri hakkında nasıl düşündüğünü anlamanız gerekiyor.
İçselleştirilmesi gereken birkaç desen:
1. Binary biriktiricilerini sıkı tutun. Sadece ekleme yapılan (append-only) döngüler dostunuzdur. Bir binary kaçtığı anda maliyet modeli dramatik bir şekilde değişir.
2. Mümkün olduğunda yerinde eşleştirme yapın. Veriyi kopyalamak yerine, veriye bakmak için pattern matching kullanın. BEAM bunun için optimize edilmiştir.
3. Tam sayı boyutlarınıza dikkat edin. Aritmetik veya bit manipülasyonu yaparken, değerlerinizin "küçük tam sayı" aralığında kalıp kalmayacağını düşünün.
4. Redüksiyonları (reductions) göz önünde bulundurarak profil çıkarın. BEAM, işleri redüksiyonlar (kabaca fonksiyon çağrıları) halinde planlar. Daha az fonksiyon çağrısıyla daha fazla eşleştirme yapan "sıcak" bir döngü, scheduler (zamanlayıcı) açısından daha az iş yapar.
İster DNS, ister WebSocket çerçeveleri veya özel protokol uygulamaları olsun, binary protokolleri işleyen sistemler inşa eden herkes için bu desenler doğrudan geçerlidir. BEAM size hızlı kod yazmanız için inanılmaz araçlar sunar, ancak bunlar sadece modeli anlarsanız kullanışlıdır.
BEAM semantiğini derinlemesine anlamanın, karmaşıklık yaratmadan %90 kazanç sağlayabileceği durumlarda, ekiplerin NIF veya Rust NIF'lere başvurduğunu gördük. Çalışma zamanı (runtime), ona verdiğimiz değerden çok daha zekidir; bizim işimiz, onun yeteneklerini sergilemesine izin veren kodları yazmaktır.
NameOcean'da, ölçeklenebilir performans üzerine sürekli düşünüyoruz. İster bir sonraki projeniz için Vibe Hosting kuruyor olun, ister yüksek trafikli bir uygulama için DNS yönetiyor olun; bu temelleri anlamak, işler yoğunlaştığında hızlı kalmaya devam eden sistemler inşa etmenize yardımcı olur.
Keyifli kodlamalar, binary'leriniz her zaman verimli bir şekilde eklensin.