Pada artikel ini saya akan berbicara tentang satu teknologi yang kurang dikenal yang telah menemukan aplikasi utama dalam game online kami untuk para programmer. Agar tidak menarik karet untuk waktu yang lama, ada spoiler segera: tampaknya tidak ada yang melakukan perdukunan seperti itu dalam kode Node.js asli yang kami alami setelah beberapa tahun pembangunan. Mesin mesin virtual terisolasi (open source), yang berjalan di bawah kap proyek, ditulis khusus untuk kebutuhannya, dan saat ini digunakan dalam produksi oleh kami dan startup lain. Dan kemampuan isolasi yang ia berikan unik dan layak untuk diberitahu tentang mereka.
Tapi mari kita bicara tentang semuanya secara berurutan.
Latar belakang
Apakah Anda suka pemrograman? Bukan perusahaan rutin yang melakukan pengkodean yang banyak dari kita dipaksa untuk melakukan 40 jam seminggu, berjuang dengan penundaan, menuangkan dalam liter kopi dan secara profesional terbakar; dan pemrograman adalah proses ajaib yang tak tertandingi untuk mengubah pemikiran menjadi program kerja, mendapatkan kesenangan dari fakta bahwa kode yang baru saja Anda tulis diwujudkan di layar dan mulai menjalani kehidupan yang diminta oleh pencipta. Pada saat-saat seperti itu, saya ingin menulis kata "Pencipta" dengan huruf besar - perasaan yang muncul dalam proses itu kadang-kadang dekat dengan penghormatan.

Sangat disayangkan bahwa sangat sedikit proyek nyata yang terkait dengan pendapatan sehari-hari dapat menawarkan perasaan seperti itu kepada pengembang mereka. Paling sering, agar tidak kehilangan gairah untuk pemrograman, penggemar harus memulai perselingkuhan di sisi: hobi pemrograman, proyek hewan peliharaan, sumber terbuka modis, hanya skrip python untuk mengotomatisasi rumah pintar mereka ... atau perilaku karakter di beberapa online populer permainan.
Ya, itu adalah game online yang sering menyediakan sumber inspirasi yang tidak ada habisnya untuk programmer. Bahkan gim pertama dalam genre ini (Ultima Online, Everquest, belum lagi semua jenis MUD) menarik banyak pengrajin yang tidak begitu tertarik dalam memainkan peran dan menikmati fantasi dunia, tetapi dalam penerapan bakat mereka untuk mengotomatisasi segala sesuatu ruang game virtual. Dan sampai hari ini, itu tetap menjadi disiplin khusus dari Olimpiade Game MMO online: sempurnakan pikiran Anda untuk menulis bot Anda agar tidak diperhatikan oleh administrasi dan dapatkan keuntungan maksimum dibandingkan dengan pemain lain. Atau bot lain - seperti, misalnya, di EVE Online, di mana perdagangan di pasar padat penduduk sedikit kurang dari sepenuhnya dikendalikan oleh skrip perdagangan, seperti pada pertukaran nyata.
Gagasan tentang game online, yang awalnya dan sepenuhnya berorientasi pada programmer, melayang di udara. Seperti permainan di mana menulis bot bukanlah tindakan yang bisa dihukum, tetapi inti dari gameplay. Di mana tugasnya bukan untuk melakukan tindakan yang sama "Bunuh monster X dan temukan item Y" dari waktu ke waktu, tetapi untuk menulis skrip yang dapat melakukan tindakan ini dengan benar atas nama Anda. Dan karena itu menyiratkan permainan online dalam genre MMO, persaingan terjadi dengan skrip pemain lain secara real time dalam satu dunia game umum.
Jadi pada tahun 2014, permainan Screeps (dari kata "Scripts" dan "creeps") muncul - kotak pasir MMO strategis real-time dengan satu dunia persisten besar di mana pemain tidak memiliki pengaruh pada apa yang terjadi kecuali dengan menulis skrip AI untuk unit permainan mereka . Semua mekanisme permainan strategis biasa - ekstraksi sumber daya, unit bangunan, membangun basis, merebut wilayah, manufaktur, dan perdagangan - Anda perlu memprogram pemain sendiri melalui JavaScript API, yang disediakan oleh dunia game. Perbedaan dari kompetisi yang berbeda dalam penulisan AI adalah bahwa dunia game, sebagaimana seharusnya dalam dunia game online, terus bekerja dan menjalani kehidupannya secara real time 24/7 selama 4 tahun terakhir, meluncurkan AI setiap pemain setiap siklus permainan.
Jadi, cukup tentang permainan itu sendiri - ini harus cukup untuk lebih memahami esensi dari masalah teknis yang kami temui selama pengembangan. Anda bisa mendapatkan lebih banyak penayangan dari video ini, tetapi ini opsional:
Masalah teknis
Inti dari mekanisme dunia game adalah sebagai berikut: seluruh dunia dibagi menjadi kamar - kamar , yang saling terhubung oleh pintu keluar pada empat titik mata angin. Satu ruangan adalah unit atom dari proses memproses keadaan dunia game. Ruangan itu mungkin memiliki benda-benda tertentu (misalnya, unit) yang memiliki keadaan sendiri, dan pada setiap langkah permainan mereka menerima perintah dari para pemain. Server handler mengambil satu ruangan pada satu waktu, mengeksekusi perintah-perintah ini, mengubah keadaan objek, dan melakukan keadaan baru ruangan ke database. Sistem ini berskala baik secara horizontal: Anda dapat menambahkan lebih banyak penangan ke kluster, dan karena kamar-kamar secara arsitektur terisolasi satu sama lain, karena banyak kamar dapat diproses secara paralel karena ada penangan yang beroperasi.

Saat ini kami memiliki 42.060 kamar dalam permainan. Sekelompok server dari 36 mesin fisik quad-core berisi 144 prosesor. Kami menggunakan Redis untuk membuat antrian, seluruh backend ditulis dalam Node.js.
Ini adalah satu tahap dari kebijaksanaan permainan. Tapi dari mana tim pemain berasal? Kekhasan gim ini adalah tidak ada antarmuka tempat Anda dapat mengeklik unit dan menyuruhnya pergi ke titik tertentu atau membangun struktur tertentu. Maksimum yang dapat dilakukan dalam antarmuka adalah menempatkan bendera tidak berwujud di tempat yang tepat di ruangan. Agar unit datang ke tempat ini dan mengambil tindakan yang diperlukan, skrip Anda perlu melakukan sesuatu seperti yang berikut untuk beberapa kutu game:
module.exports.loop = function() { let creep = Game.creeps['Creep1']; let flag = Game.flags['Flag1']; if(!creep.pos.isEqualTo(flag.pos)) { creep.moveTo(flag.pos); } }
Ternyata pada setiap langkah gim Anda perlu mengambil fungsi loop
pemain, menjalankannya di lingkungan JavaScript penuh dari pemain tertentu itu (di mana objek Game
dibentuk untuknya ada), dapatkan satu set pesanan untuk unit, dan memberikannya ke tahap pemrosesan berikutnya. Segalanya tampak sangat sederhana.

Permasalahan dimulai ketika sampai pada nuansa implementasi. Saat ini kami memiliki 1.600 pemain aktif di dunia. Skrip masing-masing pemain sudah tidak dapat disebut "skrip" - beberapa di antaranya berisi hingga 25 ribu baris kode , dikompilasi dari TypeScript atau bahkan dari C / C ++ / Rust melalui WebAssembly (ya, kami mendukung wasm!), Dan menerapkan konsep OS miniatur nyata, di mana para pemain telah mengembangkan kumpulan tugas-tugas permainan mereka sendiri dan manajemen mereka melalui inti, yang mengambil banyak tugas ternyata bekerja berdasarkan kebijaksanaan permainan, mengeksekusi mereka, dan menempatkan mereka kembali dalam antrian sampai langkah berikutnya. Karena CPU dan memori pemain terbatas pada setiap siklus clock, model ini berfungsi dengan baik. Meskipun tidak wajib - untuk memulai permainan, cukup bagi pemula untuk mengambil skrip 15 baris, yang juga sudah ditulis sebagai bagian dari tutorial.
Tapi sekarang mari kita ingat bahwa skrip pemain harus bekerja di mesin JavaScript nyata. Dan gim ini bekerja dalam waktu nyata - yaitu, mesin JavaScript setiap pemain harus selalu ada, bekerja dengan kecepatan tertentu, agar tidak memperlambat gim secara keseluruhan. Tahap mengeksekusi skrip game dan membentuk pesanan untuk unit bekerja kira-kira berdasarkan prinsip yang sama dengan ruang pemrosesan - skrip masing-masing pemain adalah tugas yang dilakukan oleh satu pawang dari pool, banyak pawang paralel yang bekerja dalam klaster. Tapi tidak seperti tahap ruang pemrosesan, sudah ada banyak kesulitan.
Pertama, Anda tidak bisa hanya membagikan tugas oleh penangan secara acak pada setiap siklus jam, karena hal itu mungkin dilakukan dalam kasus kamar. Mesin JavaScript pemain harus bekerja tanpa henti, setiap tindakan selanjutnya hanyalah panggilan fungsi loop
baru, tetapi konteks global harus tetap sama. Secara kasar, gim ini memungkinkan Anda melakukan sesuatu seperti ini:
let counter = 0; let song = ['EX-', 'TER-', 'MI-', 'NATE!']; module.exports.loop = function () { Game.creeps['DalekSinger'].say(song[counter]); counter++; if(counter == song.length) { counter = 0; } }

Merayap seperti itu akan bernyanyi pada satu baris lagu setiap permainan mengalahkan. Nomor baris counter
lagu disimpan dalam konteks global yang disimpan di antara langkah-langkah. Jika setiap kali skrip pemain ini dieksekusi dalam proses handler baru, konteksnya akan hilang. Ini berarti bahwa semua pemain harus dialokasikan ke penangan tertentu, dan mereka harus diubah sesedikit mungkin. Tapi lalu bagaimana dengan load balancing? Satu pemain dapat menghabiskan 500ms eksekusi pada node ini, dan pemain lain dapat menghabiskan 10ms, dan sangat sulit untuk memprediksi ini sebelumnya. Jika 20 pemain masing-masing 500 ms jatuh pada satu simpul, maka operasi simpul semacam itu akan memakan waktu 10 detik, di mana semua yang lain akan menunggu sampai selesai dan berdiri diam. Dan untuk menyeimbangkan kembali para pemain ini dan melemparkan mereka ke node lain, Anda harus kehilangan konteksnya.
Kedua, lingkungan pemain harus diisolasi dengan baik dari pemain lain dan dari lingkungan server. Dan ini tidak hanya menyangkut keamanan, tetapi juga kenyamanan bagi pengguna itu sendiri. Jika seorang pemain tetangga berjalan pada node yang sama di cluster seperti yang saya lakukan dengan mengerikan, menghasilkan banyak sampah, dan umumnya berperilaku tidak patut, maka saya seharusnya tidak merasakannya. Karena sumber daya CPU dalam permainan adalah waktu pelaksanaan skrip (dihitung dari awal hingga akhir metode loop
), pemborosan sumber daya pada tugas-tugas asing selama pelaksanaan skrip saya bisa sangat sensitif, karena dihabiskan dari anggaran sumber daya CPU saya.
Dalam mencoba menangani masalah-masalah ini, kami menemukan beberapa solusi.
Versi pertama
Versi pertama dari mesin permainan didasarkan pada dua hal dasar:
- modul
vm
penuh waktu dalam pengiriman Node.js, - garpu proses runtime.
Terlihat seperti ini. Pada setiap mesin di cluster ada 4 (sesuai dengan jumlah core) proses penangan script game. Ketika tugas baru diterima dari antrian skrip permainan, pawang meminta data yang diperlukan dari database dan mentransfernya ke proses anak, yang dipertahankan dalam keadaan terus berjalan, dimulai kembali jika terjadi kegagalan, dan digunakan kembali oleh pemain yang berbeda. Proses anak, yang diisolasi dari induknya (yang berisi logika bisnis klaster), hanya dapat melakukan satu hal: membuat objek Game
dari data yang diterima dan memulai mesin virtual pemain. Untuk memulai, kami menggunakan modul vm
di Node.js.
Mengapa keputusan ini tidak sempurna? Sebenarnya, dua masalah di atas tidak diselesaikan di sini.
vm
bekerja dalam mode single-threaded yang sama dengan Node.js. Oleh karena itu, untuk memiliki empat prosesor paralel pada setiap inti pada mesin 4-inti, Anda harus memiliki 4 proses. Memindahkan pemain "hidup" dalam satu proses ke proses lain mengarah pada penciptaan kembali konteks global yang lengkap, bahkan jika ini terjadi dalam mesin yang sama.

Selain itu, vm
tidak benar-benar membuat mesin virtual yang sepenuhnya terisolasi. Apa yang dilakukannya hanyalah membuat konteks yang terisolasi, atau ruang lingkup, tetapi jalankan kode dalam contoh yang sama dari mesin virtual JavaScript, di mana panggilan vm.runInContext
. Dan itu berarti - dalam contoh yang sama di mana pemain lain diluncurkan. Meskipun para pemain dipisahkan oleh konteks global yang terisolasi, tetapi, sebagai bagian dari mesin virtual yang sama, mereka memiliki memori tumpukan yang sama, pengumpul sampah yang sama, dan menghasilkan sampah bersama. Jika pemain "A" menghasilkan banyak sampah selama pelaksanaan skrip permainannya, pekerjaan yang selesai, dan kontrol diteruskan ke pemain "B", maka pada saat itu semua sampah dari proses tersebut dapat dikumpulkan, dan pemain "B" akan membayar waktu CPU untuk mengumpulkan sampah orang lain. Belum lagi fakta bahwa semua konteks bekerja dalam loop peristiwa yang sama, dan secara teoritis dimungkinkan untuk mengeksekusi janji orang lain kapan saja, meskipun kami mencoba mencegahnya. Juga, vm
tidak memungkinkan Anda untuk mengontrol berapa banyak memori-tumpukan dialokasikan untuk eksekusi skrip, semua memori proses tersedia.
terisolasi-vm
Hiduplah orang yang luar biasa bernama Marcel Laverde. Bagi sebagian orang, ia pernah menjadi luar biasa karena menulis perpustakaan simpul-serat , bagi yang lain, karena meretas Facebook dan dipekerjakan untuk bekerja di sana . Dan bagi kami dia luar biasa karena dia dengan murah hati berpartisipasi dalam kampanye crowdfunding pertama kami dan sampai hari ini adalah penggemar berat Screeps.
Proyek kami telah open source selama beberapa tahun sekarang - server game diterbitkan di GitHub. Meskipun klien resmi dijual dengan biaya melalui Steam, ada versi alternatifnya, dan server itu sendiri tersedia untuk studi dan modifikasi pada skala apa pun, yang sangat kami dorong.
Dan begitu Marcel menulis kepada kami: “Guys, saya punya pengalaman bagus dalam pengembangan C / C ++ asli untuk Node.js, dan saya suka game Anda, tetapi tidak semua orang suka cara kerjanya - mari kita menulis yang baru teknologi peluncuran mesin virtual untuk Node.js khusus untuk Screeps? ”
Karena Marcel tidak meminta uang, kami tidak bisa menolak. Setelah beberapa bulan kerja sama kami, perpustakaan terisolasi-vm lahir. Dan itu benar-benar mengubah segalanya.
isolated-vm
berbeda dari vm
dalam hal itu tidak mengisolasi konteks , tetapi mengisolasi dalam hal V8 . Tanpa merinci, ini berarti bahwa instance terpisah penuh dari mesin JavaScript dibuat, yang tidak hanya memiliki konteks globalnya sendiri, tetapi juga memori tumpukannya, pengumpul sampah, dan bekerja sebagai bagian dari loop peristiwa terpisah. Dari minus: untuk setiap mesin yang berjalan, diperlukan overhead RAM yang kecil (sekitar 20 MB), dan juga tidak mungkin untuk mentransfer objek atau memanggil fungsi langsung ke mesin, seluruh pertukaran harus serial. Ini mengakhiri kontra, sisanya - itu hanya obat mujarab!

Sekarang sangat mungkin untuk menjalankan skrip masing-masing pemain di ruang mereka yang sepenuhnya terisolasi. Pemain memiliki pinggul 500 MB sendiri, jika berakhir, itu berarti pinggul Anda sendiri yang telah berakhir, dan bukan pinggul dari keseluruhan proses. Jika Anda menghasilkan sampah - maka ini adalah sampah Anda sendiri, Anda harus mengumpulkannya. Janji yang menggantung akan dieksekusi hanya ketika isolat Anda mengambil alih waktu berikutnya, dan tidak lebih awal. Baik dan keamanan - dalam keadaan apa pun tidak mungkin untuk mengakses suatu tempat di luar isolat, hanya jika Anda menemukan kerentanan di suatu tempat di tingkat V8.
Tapi bagaimana dengan keseimbangan? Kelebihan lain dari terisolasi-vm adalah bahwa ia memulai mesin dari proses yang sama, tetapi dalam utas yang terpisah (pengalaman Marcel dengan serat-serat berguna di sini). Jika kita memiliki mesin 4-core, kita dapat membuat kumpulan 4 thread, dan memulai 4 mesin paralel pada satu waktu. Pada saat yang sama, berada dalam proses yang sama, yang berarti memiliki memori bersama, kami dapat mentransfer pemain mana pun dari satu utas ke yang lain di dalam kumpulan ini. Meskipun setiap pemain tetap terikat pada satu proses spesifik pada satu mesin tertentu (agar tidak kehilangan konteks global), menyeimbangkan antara 4 utas sudah cukup untuk menyelesaikan masalah pendistribusian pemain "berat" dan "ringan" antar node sehingga semua prosesor selesai bekerja pada waktu dan waktu yang bersamaan.
Setelah menjalankan fungsi ini dalam mode eksperimental, kami menerima sejumlah besar umpan balik positif dari pemain yang skripnya mulai bekerja jauh lebih baik, lebih stabil dan lebih dapat diprediksi. Dan sekarang ini adalah mesin default kami, meskipun pemain masih dapat memilih warisan runtime murni untuk kompatibilitas dengan skrip lama (beberapa pemain secara sadar berfokus pada spesifik lingkungan bersama dalam permainan).
Tentu saja, masih ada ruang untuk optimasi lebih lanjut, dan juga ada bidang menarik lainnya dari proyek di mana kami memecahkan berbagai masalah teknis. Tetapi lebih banyak tentang itu lain kali.