62 poin oleh xguru 2023-11-09 | 9 komentar | Bagikan ke WhatsApp

Elixir sebagai sistem fanout

  • Setiap kali sesuatu terjadi di Discord, seperti pesan dikirim atau seseorang bergabung ke channel suara, UI di klien semua pengguna yang sedang online dalam server yang sama (juga disebut "guild") harus diperbarui
  • Mereka menggunakan satu proses Elixir per guild sebagai titik perutean pusat untuk semua hal yang terjadi di server tersebut, dan proses lain ("session") untuk klien tiap pengguna yang terhubung
  • Proses guild bertugas melacak session milik pengguna yang merupakan anggota guild tersebut dan menyebarkan pekerjaan ke session-session itu
  • Ketika session menerima pembaruan, pembaruan itu diteruskan ke klien melalui koneksi WebSocket
  • Sebagian pekerjaan berlaku untuk semua orang di server, sementara sebagian lain perlu memeriksa izin, sehingga harus mengetahui informasi tentang role dan channel server tersebut, serta role milik pengguna
  • Volume aktivitas guild sebanding dengan jumlah orang di server itu, dan jumlah kerja yang dibutuhkan untuk melakukan fanout satu pesan juga sebanding dengan jumlah pengguna online di server tersebut
    • Artinya, jumlah kerja yang dibutuhkan untuk menangani server Discord meningkat secara kuadrat terhadap skala server
    • Jika ada 1.000 orang online di satu server dan semuanya masing-masing berkata "Saya suka jeli" satu kali, berarti ada 1 juta notifikasi yang harus diproses
    • Jika 10.000 orang, ada 100 juta notifikasi, dan jika 100.000 orang, 10 miliar notifikasi harus dikirimkan
  • Selain masalah throughput secara keseluruhan, ada juga kasus di mana beberapa pekerjaan menjadi lebih lambat saat server membesar
  • Agar server terasa sangat responsif—misalnya orang lain harus segera bisa melihat pesan yang baru dikirim, atau seseorang harus bisa langsung mulai bergabung ketika ada yang masuk ke channel suara—hampir semua pekerjaan harus diproses dengan cepat
  • Jika butuh beberapa detik untuk menyelesaikan pekerjaan yang mahal, pengalaman pengguna akan menurun
  • Meski ada masalah-masalah ini, bagaimana mereka bisa tetap mendukung server Midjourney yang memiliki lebih dari 10 juta anggota, dengan 1 juta di antaranya selalu online?
    • Pertama, penting untuk memahami performa sistem
    • Setelah datanya tersedia, mereka mencari peluang untuk meningkatkan throughput sekaligus responsivitas

Memahami performa sistem

  • Wall time analysis:
    • Stack tracing menggunakan Process.info(pid, :current_stacktrace)
    • Mengukur loop pemrosesan event untuk mencatat jumlah pesan yang diterima per tipe serta waktu maksimum/minimum/rata-rata/total yang dibutuhkan untuk menanganinya
    • Pekerjaan yang mengambil kurang dari 1% dari total waktu diabaikan semua kecuali bila benar-benar meledak ekstrem
    • Menyingkirkan pekerjaan murah dan menyoroti pekerjaan yang paling mahal
  • Process Heap Memory Analysis
    • Memahami cara penggunaan memori juga penting
    • Alih-alih melihat tiap elemen satu per satu, mereka menulis helper library yang mengambil sampel map dan list besar (bukan struct) untuk menghasilkan perkiraan penggunaan memori
    • Library ini membantu memahami performa GC, sekaligus berguna untuk menemukan field yang layak difokuskan untuk optimasi dan field yang pada akhirnya tidak relevan
  • Setelah memahami ke mana proses guild menghabiskan waktu, mereka bisa menyusun strategi agar proses guild tidak sibuk 100% sepanjang waktu
    • Dalam beberapa kasus, cukup dengan menulis ulang implementasi yang tidak efisien menjadi lebih efisien
    • Namun cara ini hanya bisa membawa sampai titik tertentu. Diperlukan perubahan yang lebih mendasar

Session pasif - menghindari pekerjaan yang tidak perlu

  • Salah satu cara terbaik untuk mengatasi bottleneck throughput adalah mengurangi jumlah pekerjaan
  • Salah satu cara untuk melakukannya adalah dengan mempertimbangkan kebutuhan aplikasi klien
  • Dalam topologi awal, semua pengguna menerima semua aksi yang bisa mereka lihat di semua guild tempat mereka berada
  • Tetapi beberapa pengguna berada di banyak guild, dan mungkin bahkan tidak mengeklik untuk melihat apa yang sedang terjadi di sebagian guild itu
  • Bagaimana jika semua hal tidak dikirim sampai pengguna mengekliknya? Mereka tidak perlu memeriksa izin untuk tiap pesan satu per satu, dan akibatnya jumlah data yang dikirim ke klien juga jauh berkurang
  • Mereka menyebut ini koneksi 'Passive', dan menyimpannya dalam daftar terpisah dari koneksi 'Active' yang harus menerima semua data
  • Hasilnya, di server besar sekitar 90% koneksi pengguna-guild adalah koneksi pasif, sehingga biaya pekerjaan fanout berkurang 90%
  • Ini memberi ruang bernapas sampai taraf tertentu, tetapi seiring komunitas terus tumbuh, tentu ini saja tidak cukup
    (ketika jumlah kerja turun 10x, pada skala komunitas maksimum bisa diperoleh keuntungan sekitar 3x)

Relay - membagi fanout ke beberapa mesin

  • Salah satu teknik standar untuk memperluas batas throughput satu inti adalah membagi pekerjaan ke beberapa thread (atau dalam istilah Elixir, proses)
  • Berdasarkan ide ini, mereka membangun sistem bernama 'relay' di antara guild dan session pengguna
  • Alih-alih semua pekerjaan untuk menangani session diproses dalam satu proses, pekerjaan itu dibagi ke beberapa relay sehingga satu guild bisa memakai lebih banyak resource untuk melayani komunitas besar
  • Sebagian pekerjaan tetap harus dilakukan di proses guild utama, tetapi pendekatan ini memungkinkan mereka menangani komunitas dengan ratusan ribu anggota
  • Untuk menerapkannya, mereka harus mengidentifikasi pekerjaan penting yang perlu dilakukan di relay, pekerjaan yang harus dilakukan di guild, dan pekerjaan yang bisa dilakukan di kedua sistem
  • Setelah memahami apa yang dibutuhkan, mereka mulai melakukan refactoring untuk mengekstrak logic yang bisa dibagikan antar sistem
    • Misalnya, sebagian besar logic tentang cara melakukan fanout direfaktor menjadi library yang dipakai oleh guild dan relay
    • Sebagian logic yang tidak bisa dibagikan memerlukan solusi lain; pengelolaan status suara pada dasarnya diimplementasikan dengan relay mem-proxy semua pesan ke guild dengan perubahan seminimal mungkin
  • Salah satu keputusan desain menarik saat pertama kali merilis relay adalah memasukkan seluruh daftar anggota ke dalam state tiap relay
    • Ini keputusan yang baik dari sisi kesederhanaan karena semua informasi anggota yang diperlukan selalu tersedia
    • Namun pada skala Midjourney dengan jutaan anggota, desain ini makin lama makin tidak masuk akal
  • Bukan hanya informasi puluhan juta anggota disimpan dalam RAM dalam puluhan salinan, tetapi untuk membuat relay baru mereka juga harus men-serialisasi semua informasi anggota dan mengirimkannya ke relay baru, yang menyebabkan guild tertunda selama puluhan detik
  • Untuk mengatasi masalah ini, mereka menambahkan logic untuk mengidentifikasi anggota yang benar-benar dibutuhkan relay untuk bekerja, yang ternyata hanya sebagian sangat kecil dari keseluruhan anggota

Menjaga server tetap responsif

  • Selain tetap berada dalam batas throughput, mereka juga harus menjaga responsivitas server
  • Di sini juga, melihat data timing sangat membantu
  • Lebih efektif fokus pada pekerjaan dengan durasi per panggilan yang panjang daripada total durasi keseluruhan
  • Proses worker + ETS
    • Salah satu penyebab terbesar server tidak responsif adalah pekerjaan yang dijalankan di guild dan harus mengiterasi semua anggota
    • Kasus seperti ini sangat jarang, tetapi memang terjadi. Misalnya, ketika seseorang melakukan ping ke semua orang, sistem harus mengetahui semua orang di server yang bisa melihat pesan tersebut
    • Namun pekerjaan pengecekan seperti ini bisa memakan beberapa detik. Bagaimana cara menanganinya?
    • Yang paling ideal adalah menjalankan logic ini sambil guild tetap menangani pekerjaan lain, tetapi proses Elixir tidak berbagi memori dengan baik. Jadi diperlukan solusi lain
    • Salah satu alat di Erlang/Elixir yang bisa digunakan untuk menyimpan data ke memori yang dapat dibagikan antarproses adalah ETS
    • Ini adalah database in-memory yang mendukung akses aman oleh banyak proses Elixir
    • Efisiensinya memang lebih rendah dibanding mengakses data di process heap, tetapi masih sangat cepat. Selain itu, ada keuntungan tambahan berupa mengecilkan ukuran process heap sehingga latensi garbage collection juga berkurang
    • Mereka memutuskan membuat struktur hibrida untuk menyimpan daftar anggota:
      • Menyimpan daftar anggota di ETS agar bisa dibaca proses lain, sambil juga menyimpan perubahan terbaru (insert, update, delete) di process heap
      • Karena sebagian besar anggota tidak terus-menerus diperbarui, kumpulan perubahan terbaru ini hanya bagian yang sangat kecil dari keseluruhan set anggota
    • Kini mereka bisa membuat proses worker menggunakan anggota di ETS dan meneruskan identifier tabel ETS untuk dikerjakan ketika ada pekerjaan berbiaya tinggi
    • Proses worker dapat menangani bagian yang mahal sementara guild tetap melanjutkan pekerjaan lain. Juga ada referensi pada cara sederhana untuk melakukan ini (snippet kode ada di artikel asli)
    • Salah satu contoh penggunaan metode ini adalah ketika proses guild harus dipindahkan dari satu mesin ke mesin lain (biasanya untuk maintenance atau deployment)
    • Dalam proses ini, mereka membuat proses baru di mesin baru untuk menangani guild, lalu menyalin state proses guild lama ke proses baru, menyambungkan kembali semua session yang terhubung ke proses guild baru, lalu memproses backlog yang menumpuk selama pekerjaan itu berlangsung
    • Dengan proses worker, mereka bisa mentransfer sebagian besar anggota yang ada—yang bisa mencapai data berukuran beberapa GB—sementara proses guild lama tetap terus bekerja, sehingga mengurangi jeda yang sebelumnya bisa mencapai beberapa menit setiap kali handoff dilakukan
  • Manifold offload
    • Ide lain untuk meningkatkan responsivitas dan melampaui batas throughput adalah memperluas manifold agar menggunakan proses "sender" terpisah untuk melakukan fanout ke node penerima (alih-alih proses guild yang melakukan fanout)
    • Ini tidak hanya mengurangi beban kerja proses guild, tetapi juga melindungi dari backpressure BEAM jika salah satu koneksi jaringan antara guild dan relay untuk sementara tersendat (BEAM adalah mesin virtual tempat kode Elixir berjalan)
    • Secara teori tampaknya mudah dipecahkan, tetapi sayangnya ketika fitur ini—disebut manifold offload—dicoba, mereka menemukan bahwa performanya justru turun drastis
    • Bagaimana itu bisa terjadi? Secara teori jumlah kerja berkurang, jadi mengapa proses malah menjadi lebih sibuk?
    • Setelah ditelusuri lebih jauh, mereka menemukan bahwa sebagian besar pekerjaan tambahan terkait dengan garbage collection
    • Pada titik ini, fungsi erlang.trace muncul sebagai penyelamat
    • Fungsi ini memungkinkan mereka mengumpulkan data setiap kali proses guild menjalankan garbage collection, sehingga mereka mendapat insight bukan hanya seberapa sering garbage collection terjadi, tetapi juga apa yang memicunya
    • Berdasarkan informasi tracing ini, mereka menelusuri kode garbage collection di BEAM dan menemukan bahwa ketika manifold offload aktif, kondisi pemicu utama untuk major (full) garbage collection adalah virtual binary heap
    • Virtual binary heap adalah fitur yang dirancang agar memori yang dipakai string yang tidak disimpan di dalam process heap tetap bisa dibebaskan meski proses tidak perlu menjalankan garbage collection
    • Sayangnya, pola penggunaan mereka berarti garbage collection terus dipicu demi merebut kembali memori beberapa ratus kilobyte, dengan biaya menyalin heap berukuran gigabyte—jelas sebuah trade-off yang tidak sepadan
    • Untungnya, di BEAM perilaku ini bisa disetel melalui process flag min_bin_vheap_size
    • Setelah nilai ini dinaikkan ke beberapa megabyte, perilaku garbage collection patologis itu menghilang, dan mereka bisa menyalakan manifold offload sambil melihat peningkatan performa yang signifikan

9 komentar

 
roxie 2023-11-18

Elixir mantap

 
arfwene 2023-11-10

Sesi pasif secara teknis bukan hal yang istimewa, tetapi ini tampaknya ide yang bagus.
Ini jelas bisa mengurangi beban dengan signifikan.

Bukan hanya Discord, kemungkinan tempat lain juga sudah menerapkan fitur seperti ini, dan saya penasaran perbedaan seperti apa yang ada di tiap layanan.

 
mhj5730 2023-11-10

Keren banget banget

 
abhidhamma 2023-11-09

Belakangan ini, tampaknya tujuan akhir dari streaming SSR milik Next.js yang terkenal adalah framework Phoenix dari Elixir. Dalam banyak hal, Elixir sepertinya berada di garis depan bahasa pemrograman modern.

 
papillon 2023-11-09

Semangat, Elixir!

 
n1ghtc4t 2023-11-09

Beberapa tahun lalu, setelah merujuk ke blog teknis Discord, kami akhirnya mengadopsi Elixir untuk layanan real-time. Kami meluncurkan layanan dengan tingkat kepuasan yang sangat tinggi, termasuk dari saya dan para eksekutif yang bertanggung jawab, baik dari sisi kecepatan pengembangan maupun stabilitas, jadi saya punya banyak kenangan baik tentang itu.

 
kotlinc 2023-11-09

Saya berharap Elixir menjadi lebih populer.

 
[Komentar ini disembunyikan.]
 
damtet 2023-11-10

Belakangan ini rasanya perusahaan besar seperti Naver-Kakao-Line tidak terlalu seperti itu, malah startup kecil dan menengah yang tampaknya didominasi Spring. Startup-startup itu juga kebanyakan dikelola oleh manajer yang memang spesialis Spring, jadi mau bagaimana lagi.

Semua inefisiensi bisa diselesaikan dengan uang dan skala. Toh perusahaan pada akhirnya memang tidak terlalu paham.