62 puan yazan xguru 2023-11-09 | 9 yorum | WhatsApp'ta paylaş

Elixir bir fanout sistemi olarak

  • Discord'da bir mesaj gönderildiğinde ya da biri bir ses kanalına katıldığında olduğu gibi bir olay gerçekleştiğinde, aynı sunucudaki ("guild" olarak da adlandırılır) çevrimiçi durumdaki tüm kullanıcıların istemcilerinde UI'ın güncellenmesi gerekir
  • O sunucuda gerçekleşen her şey için merkezi yönlendirme noktası olarak guild başına bir Elixir prosesi, bağlı her kullanıcının istemcisi için ise ayrı bir proses ("session") kullanılıyor
  • Guild prosesi, o guild'in üyesi olan kullanıcıların session'larını takip etmekten ve ilgili işleri bu session'lara yaymaktan sorumlu
  • Session bir güncelleme aldığında bunu WebSocket bağlantısı üzerinden istemciye iletir
  • Bazı işlemler sunucudaki herkes için geçerliyken, bazıları izin kontrolü gerektirir; bu yüzden kullanıcının rollerinin yanı sıra o sunucudaki roller ve kanallar hakkında da bilgi sahibi olmak gerekir
  • Bir guild'in etkinlik hacmi o sunucudaki kişi sayısıyla orantılıdır ve tek bir mesajı fanout etmek için gereken iş miktarı da o sunucudaki çevrimiçi kullanıcı sayısıyla orantılıdır
    • Yani Discord sunucusunu işlemek için gereken iş yükü, sunucunun büyüklüğüne bağlı olarak dördüncü kuvvetle artar
    • Bir sunucuda 1.000 kişi çevrimiçiyse ve hepsi bir kez "jöleyi seviyorum" dediyse bu, 1 milyon bildirimin işlenmesi gerektiği anlamına gelir
    • 10.000 kişi varsa 100 milyon bildirim oluşur, 100.000 kişi varsa 10 milyar bildirimin iletilmesi gerekir
  • Genel throughput sorununun yanı sıra, sunucu büyüdükçe bazı işlemler yavaşlayabiliyordu
  • Bir mesaj gönderildiğinde başkalarının bunu hemen görebilmesi, biri ses kanalına katıldığında anında katılmaya başlayabilmesi gibi, sunucunun yüksek derecede tepkisel olduğu hissini vermek için neredeyse tüm işlemlerin hızlı olması gerekiyordu
  • Maliyetli işlemlerin tamamlanmasının birkaç saniye sürmesi kullanıcı deneyimini kötüleştiriyordu
  • Peki tüm bu sorunlara rağmen, 10 milyondan fazla üyesi olan ve bunların 1 milyondan fazlası her zaman çevrimiçi olan Midjourney sunucusunu nasıl destekleyebildiler?
    • Önce sistemin performansını anlamak önemliydi
    • Veriler toplandıktan sonra hem throughput'u hem de tepkiselliği artırabilecek fırsatlar bulundu

Sistem performansını anlamak

  • Wall time analizi:
    • Process.info(pid, :current_stacktrace) ile stack trace çıkarma
    • Olay işleme döngüsü ölçülerek her tür mesajın kaç kez alındığı ve bunları işlemenin en yüksek/en düşük/ortalama/toplam ne kadar sürdüğü kaydedildi
    • Toplam sürenin %1'inden azını alan işler, aşırı patlayan durumlar dışında tamamen göz ardı edildi
    • Ucuz işler elendi, en maliyetli işler öne çıkarıldı
  • Process Heap Memory Analysis
    • Belleğin nasıl kullanıldığını anlamak da önemliydi
    • Her öğeye tek tek bakmak yerine, büyük map ve listelerden (struct olmayan) örnekleme yaparak tahmini bellek kullanımını üreten bir yardımcı kütüphane yazıldı
    • Bu kütüphane yalnızca GC performansını anlamaya yardımcı olmakla kalmadı, aynı zamanda optimizasyon için odaklanmaya değer alanları ve nihayetinde alakasız olan alanları bulmakta da işe yaradı
  • Guild prosesinin zamanını nerede harcadığı anlaşıldıktan sonra, guild prosesinin %100 meşgul kalmamasını sağlayacak stratejiler geliştirilebildi
    • Bazı durumlarda verimsiz bir implementasyonu daha verimli olacak şekilde yeniden yazmak yeterliydi
    • Ancak bu yöntemle gidilebilecek yer sınırlıydı. Daha köklü değişiklikler gerekiyordu

Pasif session'lar - gereksiz işlerden kaçınmak

  • Throughput darboğazını çözmenin en iyi yollarından biri, yapılacak işi azaltmaktır
  • Bunun bir yolu da istemci uygulamasının gereksinimlerini dikkate almaktı
  • Orijinal topolojide tüm kullanıcılar, üyesi oldukları tüm guild'lerde görebilecekleri her eylemi alıyordu
  • Ancak bazı kullanıcılar birden fazla guild'e üyeydi ve bazı guild'lerde neler olduğuna bakmak için hiç tıklamıyor olabiliyordu
  • Kullanıcı tıklayana kadar hiçbir şeyi göndermesek nasıl olurdu? Böylece her mesaj için tek tek izin kontrolü yapmaya gerek kalmayacak, sonuç olarak istemciye gönderilen veri miktarı da çok azalacaktı
  • Buna "Passive" bağlantı adı verildi ve tüm verileri almak zorunda olan "Active" bağlantılardan ayrı bir listede tutuldu
  • Sonuç olarak, büyük sunucularda kullanıcı-guild bağlantılarının yaklaşık %90'ı pasif bağlantıydı; bu da fanout işinin maliyetini %90 azalttı
  • Bu bir miktar rahatlama sağladı ama topluluk büyümeye devam ettikçe doğal olarak tek başına yeterli olmadı
    (iş yükü 10 kat azalırsa, en büyük topluluk ölçeğinde yaklaşık 3 kat kazanç elde edilebiliyor)

Relay'ler - fanout'u birden fazla makineye bölmek

  • Tek çekirdekli throughput sınırını büyütmek için standart tekniklerden biri, işi birden fazla thread'e (ya da Elixir terminolojisiyle proseslere) bölmektir
  • Bu fikirden yola çıkılarak guild ile kullanıcı session'ları arasına "relay" adlı bir sistem kuruldu
  • Session'ları işleme görevini tek bir proseste yapmak yerine birden fazla relay'e bölerek, tek bir guild'in büyük topluluklara hizmet vermek için daha fazla kaynak kullanabilmesi sağlandı
  • Bazı işlemler hâlâ ana guild prosesinde yapılmak zorundaydı ama bu sayede yüz binlerce üyeye sahip topluluklar işlenebilir hâle geldi
  • Bunu uygulamak için relay'de yapılması gereken önemli işleri, guild'de yapılması gereken işleri ve her iki sistemde de yapılabilecek işleri belirlemek gerekti
  • Gerekenler netleştikten sonra, sistemler arasında paylaşılabilecek mantığı ayıklamak için refactoring çalışmaları başladı
    • Örneğin fanout'un nasıl yapılacağına dair mantığın büyük bölümü, hem guild hem relay tarafından kullanılan bir kütüphaneye refactor edildi
    • Böyle paylaşılamayan bazı mantıklar için başka çözümler gerekti; voice state yönetimi ise relay'in tüm mesajları minimum değişiklikle guild'e proxy etmesi şeklinde uygulandı
  • Relay'ler ilk kez devreye alınırken verilen ilginç tasarım kararlarından biri, her relay'in durumuna tam üye listesinin dahil edilmesiydi
    • Gerekli tüm üye bilgilerinin el altında olması açısından bu, sadelik bakımından iyi bir karardı
    • Ancak üye sayısı milyonlara ulaşan Midjourney ölçeğinde bu tasarım giderek anlamını yitirmeye başladı
  • On milyonlarca üyeye ait bilgiler RAM'de onlarca kopya hâlinde tutulmakla kalmıyor, yeni bir relay oluşturmak için tüm üye bilgilerinin serialize edilip yeni relay'e gönderilmesi gerektiğinden guild'in onlarca saniye gecikmesine yol açıyordu
  • Bu sorunu çözmek için, relay'in gerçekten çalışabilmesi için gerekli üyeleri belirleyen bir mantık eklendi; bunlar toplam üyelerin yalnızca çok küçük bir kısmıydı

Sunucu tepkiselliğini korumak

  • Throughput sınırları içinde kalmanın yanı sıra sunucunun tepkiselliğini de korumak gerekiyordu
  • Burada da zamanlama verilerine bakmak faydalı oldu
  • Toplam süre yerine, çağrı başına süresi uzun olan işlere odaklanmak daha etkiliydi
  • Worker process + ETS
    • Tepkisizliğin en büyük nedenlerinden biri, guild üzerinde çalışan ve tüm üyeleri dolaşması gereken işlerdi
    • Bu tür durumlar çok nadirdi ama yine de oluyordu. Örneğin biri @everyone ping'i yaptığında, o mesajı görebilecek sunucudaki herkesin belirlenmesi gerekiyordu
    • Ancak bu kontrol işlemleri birkaç saniye sürebiliyordu. Bunu nasıl ele almak gerekiyordu?
    • En ideali, guild başka işleri işlerken bu mantığın ayrı çalışmasıydı; fakat Elixir prosesleri belleği iyi paylaşmaz. Bu nedenle başka bir çözüm gerekiyordu
    • Proseslerin veriyi paylaşılabilir bellekte tutmak için kullanabileceği Erlang/Elixir araçlarından biri ETS'tir
    • Bu, birden fazla Elixir prosesinin güvenli şekilde erişebildiği özelliklere sahip bir in-memory veritabanıdır
    • Proses heap'indeki veriye erişmek kadar verimli değildir ama yine de çok hızlıdır. Ayrıca proses heap'inin boyutunu küçülterek garbage collection gecikmelerini azaltma avantajı da sağlar
    • Üye listesini tutmak için hibrit bir yapı oluşturmaya karar verildi:
      • Üye listesi, diğer prosesler tarafından da okunabilmesi için ETS'te saklanıyor; ancak son değişiklikler (ekleme, güncelleme, silme) de proses heap'inde tutuluyor
      • Üyelerin büyük kısmı her zaman güncellenmediğinden, son değişiklikler kümesi tüm üye kümesinin çok küçük bir bölümünü oluşturuyor
    • Artık ETS'teki üyeleri kullanarak worker process'ler oluşturmak ve maliyetli işler olduğunda bunlara üzerinde çalışacakları ETS tablo tanımlayıcısını vermek mümkün
    • Worker process'ler, guild diğer işlere devam ederken maliyetli kısmı işleyebiliyor. Bunun nasıl yapıldığına dair basit bir yöntemden de bahsediliyor (orijinal metinde kod parçası var)
    • Bunun kullanıldığı örneklerden biri, guild prosesinin bir makineden diğerine taşınması gerektiğinde (genelde bakım veya deploy için)
    • Bu süreçte yeni makinede guild'i işleyecek yeni bir proses oluşturuluyor, ardından eski guild prosesinin durumu yeni prosese kopyalanıyor, bağlı tüm session'lar yeni guild prosesine yeniden bağlanıyor ve bu işlem sırasında biriken backlog işleniyor
    • Worker process'ler sayesinde mevcut guild prosesi çalışmaya devam ederken üyelerin büyük kısmı (GB'larca veri olabilir) aktarılabildiğinden, her handoff sırasında yaşanan dakikalarca gecikme azaltılabildi
  • Manifold offload
    • Tepkiselliği iyileştirmek ve throughput sınırlarını aşmak için bir başka fikir de manifold'u genişleterek (fanout'u guild prosesi içinde yapmak yerine) alıcı node'lara fanout yapmak üzere ayrı bir "sender" prosesi kullanmaktı
    • Bu, yalnızca guild prosesinin iş yükünü azaltmakla kalmaz; aynı zamanda guild ile relay arasındaki ağ bağlantılarından biri geçici olarak tıkanırsa BEAM backpressure'ına karşı da koruma sağlar (BEAM, Elixir kodunun çalıştığı sanal makinedir)
    • Teoride kolay bir çözüm gibi görünüyordu ama ne yazık ki bu özellik (manifold offload olarak adlandırılıyor) kullanıldığında performansın ciddi biçimde düştüğü görüldü
    • Bu nasıl olabilirdi? Teoride iş yükü azalırken proses neden daha meşgul görünüyordu?
    • Yakından bakıldığında, ek işin büyük bölümünün garbage collection ile ilgili olduğu anlaşıldı
    • Bu noktada erlang.trace fonksiyonu adeta kurtarıcı oldu
    • Bu fonksiyon sayesinde, guild prosesi her garbage collection yaptığında veri toplanabildi; böylece yalnızca bunun ne kadar sık olduğu değil, garbage collection'ı neyin tetiklediği konusunda da içgörü elde edildi
    • Bu izleme bilgileri ışığında BEAM'in garbage collection kodu incelendiğinde, manifold offload etkin olduğunda büyük (full) garbage collection'ların tetikleyicisinin sanal binary heap olduğu anlaşıldı
    • Sanal binary heap, proses heap'i içinde saklanmayan string'lerin kullandığı belleğin, prosesin normalde garbage collection yapmasına gerek olmasa bile serbest bırakılabilmesini sağlamak için tasarlanmış bir özelliktir
    • Ne yazık ki kullanım kalıbı, birkaç yüz KB belleği geri kazanmak için GB boyutunda heap'leri kopyalama pahasına sürekli garbage collection tetiklenmesi anlamına geliyordu; bu da açıkça değmeyecek bir takastı
    • Neyse ki BEAM'de bu davranış, min_bin_vheap_size proses bayrağı kullanılarak ayarlanabiliyor
    • Bu değer birkaç MB'a yükseltildiğinde patolojik garbage collection davranışı ortadan kalktı ve manifold offload açıldığında performansın ciddi biçimde arttığı görüldü

9 yorum

 
roxie 2023-11-18

Elixir savaşsın

 
arfwene 2023-11-10

Pasif oturumlar teknik olarak çok da özel bir şey değil ama iyi bir fikir gibi görünüyor.
Yükü ciddi şekilde azaltabilir gibi duruyor.

Bunu yalnızca Discord değil başka yerler de muhtemelen uygulamıştır; hizmete göre ne gibi farklar olduğunu merak ediyorum.

 
mhj5730 2023-11-10

İnanılmaz havalıymış, vay be

 
abhidhamma 2023-11-09

Bugünlerde popüler olan Next.js'in streaming SSR'ının vardığı son noktanın da Elixir'in Phoenix framework'ü olduğunu gördüm. Elixir, birçok açıdan modern programlama dillerinin en ön cephesinde yer alıyor gibi görünüyor.

 
papillon 2023-11-09

Elixir'e devam!

 
n1ghtc4t 2023-11-09

Birkaç yıl önce Discord’un teknik blogunu referans alarak gerçek zamanlı hizmete Elixir’i dahil etmiştik; geliştirme hızı ve güvenlik açısından ben de dahil olmak üzere sorumlu yöneticiler çok memnun kalmıştı ve hizmeti başarıyla yayına aldığımız için buna dair çok iyi anılarım var.

 
kotlinc 2023-11-09

Umarım Elixir daha popüler olur.

 
[Bu yorum gizlendi.]
 
damtet 2023-11-10

Bugünlerde Naver-Kakao-Line o kadar da öyle değil gibi; hatta daha çok küçük ve orta ölçekli startup'ların Spring tekelinde olduğunu düşünüyorum. Sonuçta bu startup yöneticilerinin çoğu Spring uzmanı, yapacak bir şey yok.

Tüm verimsizlikler para ve ölçekle çözülür. Nasıl olsa şirketler de pek anlamıyor.