Arsitektur Shooter Meta Server Online Tacticool

Pembicaraan lain dengan Pixonic DevGAMM Talks - kali ini dari kolega kami di PanzerDog. Insinyur Perangkat Lunak Pimpinan perusahaan Pavel Platto membongkar meta-server permainan dengan arsitektur berorientasi layanan, memberi tahu solusi dan teknologi mana yang dipilih, apa dan bagaimana mereka diskalakan, dan kesulitan apa yang harus mereka hadapi. Teks laporan, slide dan tautan ke pidato lain dari mitap, seperti biasa, di bawah potongan.


Pertama, saya ingin menunjukkan trailer kecil untuk game kami:


Laporan akan terdiri dari 3 bagian. Pada bagian pertama saya akan berbicara tentang teknologi apa yang kami pilih dan mengapa, pada bagian kedua - bagaimana meta-server kami diatur, dan pada bagian ketiga saya akan berbicara tentang berbagai infrastruktur pendukung yang kami gunakan, dan bagaimana kami menerapkan pembaruan tanpa downtime. .


Tumpukan teknologi

Server meta di-host di Amazon dan ditulis dalam Elixir. Ini adalah bahasa pemrograman fungsional dengan model komputasi aktor. Karena kita tidak memiliki Ops, programmer terlibat dalam operasi, dan sebagian besar infrastruktur digambarkan sebagai kode menggunakan HashiCorp Terraform.

Tacticool saat ini dalam versi beta terbuka, server meta telah dalam pengembangan selama sedikit lebih dari setahun dan telah beroperasi selama hampir satu tahun. Mari kita lihat bagaimana semuanya dimulai.



Ketika saya bergabung dengan perusahaan, kami sudah memiliki fungsionalitas dasar yang diimplementasikan sebagai monolith pada campuran C / C ++ dan penyimpanan PostageSQL. Implementasi ini memiliki masalah tertentu.

Pertama, karena tingkat C yang rendah, ada beberapa bug yang sulit dipahami. Misalnya, untuk beberapa pemain, perjodohan hang ketat karena kesalahan pemberian array yang salah sebelum digunakan kembali. Tentu saja, menemukan hubungan antara kedua peristiwa ini cukup sulit. Dan karena keadaan beberapa utas secara universal dimodifikasi dalam kode, kondisi Ras bukan tanpa.

Pemrosesan paralel sejumlah besar tugas juga keluar dari pertanyaan, karena server dimulai pada awal sekitar 10 proses pekerja, yang diblokir oleh pertanyaan ke Amazon atau database. Dan bahkan jika kita lupa tentang permintaan pemblokiran ini, layanan mulai runtuh pada beberapa koneksi yang tidak melakukan operasi apa pun kecuali ping. Selain itu, layanan tidak dapat diskalakan secara horizontal.

Setelah beberapa minggu menghabiskan waktu menemukan dan memperbaiki bug yang paling kritis, kami memutuskan bahwa lebih mudah untuk menulis ulang semuanya dari awal daripada mencoba untuk memperbaiki semua kekurangan dari solusi saat ini.

Dan ketika Anda mulai dari awal, masuk akal untuk mencoba memilih bahasa yang akan membantu menghindari beberapa masalah sebelumnya. Kami memiliki tiga kandidat:

  • C #;
  • Pergi;
  • Elixir.



C # ada di daftar "kenalan", sebagai klien dan server gim ditulis dalam Unity, dan sebagian besar pengalaman dalam tim adalah dengan bahasa pemrograman ini. Go dan Elixir dianggap karena ini adalah bahasa modern dan cukup populer yang dibuat untuk mengembangkan aplikasi server.

Masalah dari iterasi sebelumnya membantu kami menentukan kriteria untuk mengevaluasi kandidat.

Kriteria pertama adalah kenyamanan bekerja dengan operasi asinkron. Di C #, pekerjaan mudah dengan operasi asinkron tidak muncul pada percobaan pertama. Ini mengarah pada fakta bahwa kita memiliki "kebun binatang" solusi yang, menurut pendapat saya, masih berdiri sedikit di samping. Di Go dan Elixir, masalah ini diperhitungkan saat merancang bahasa ini, mereka berdua menggunakan utas ringan (di Go mereka adalah goroutine, di Elixir mereka adalah proses). Aliran ini memiliki overhead yang jauh lebih kecil daripada utas sistem, dan karena kita dapat membuatnya dalam puluhan dan ratusan ribu, kami tidak menyesal memblokirnya.

Kriteria kedua adalah kemampuan untuk bekerja dengan proses kompetitif. C # out of the box tidak menawarkan apa pun selain kolam utas dan memori bersama, akses yang harus dilindungi menggunakan berbagai primitif sinkronisasi. Go memiliki model rawan kesalahan yang lebih sedikit dalam bentuk goroutine dan saluran. Elixir, di sisi lain, menawarkan model aktor tanpa memori bersama dengan olahpesan. Kurangnya memori bersama memungkinkan untuk menerapkan teknologi yang berguna untuk lingkungan eksekusi kompetitif dalam runtime, seperti multitasking take-out jujur ​​dan pengumpulan sampah tanpa gangguan di dunia.

Kriteria ketiga adalah ketersediaan alat untuk bekerja dengan tipe data yang tidak dapat diubah. Semua pengalaman pengembangan saya menunjukkan bahwa sebagian besar bug dikaitkan dengan perubahan data yang salah. Solusi untuk ini sudah ada sejak lama - tipe data yang tidak dapat diubah. Dalam C #, tipe data ini dapat dibuat, tetapi dengan biaya satu ton boilerplate. Di Go, ini tidak mungkin sama sekali. Dan di Elixir, semua tipe data tidak dapat diubah.

Dan kriteria terakhir adalah jumlah spesialis. Di sini hasilnya jelas. Pada akhirnya, kami memilih Elixir.

Dengan pilihan hosting, semuanya menjadi lebih sederhana. Kami telah meng-host server game di Amazon GameLift, di samping itu, Amazon menawarkan sejumlah besar layanan yang akan memungkinkan kami untuk mengurangi waktu pengembangan.



Kami sepenuhnya menyerah kepada cloud dan tidak menggunakan solusi pihak ketiga apa pun - database, antrian pesan - semua ini dikelola oleh Amazon untuk kami. Menurut pendapat saya, ini adalah satu-satunya solusi untuk tim kecil yang ingin mengembangkan game online, dan bukan infrastruktur untuk itu.

Kami menemukan pilihan teknologi, mari beralih ke cara kerja server meta.



Secara umum: klien terhubung ke penyeimbang beban Amazon melalui koneksi soket web; balancer mencerai-beraikan koneksi ini antara beberapa instance front-end, front-end mengirimkan permintaan klien ke backend. Tetapi front-end dan back-end berkomunikasi secara tidak langsung, melalui antrian pesan. Ada antrian terpisah untuk setiap jenis pesan, dan frontend, berdasarkan jenis pesan, menentukan tempat untuk menulisnya, dan backend mendengarkan antrian ini.

Agar backend dapat mengirim respons ke permintaan kepada klien, atau semacam acara, setiap frontend memiliki antrian terpisah (khusus dialokasikan untuk itu). Dan dalam setiap permintaan, backend menerima pengidentifikasi frontend untuk menentukan di mana antrian respons harus ditulis. Jika dia perlu mengirim suatu acara, dia memanggil basis data untuk mencari tahu contoh antarmuka mana yang terhubung dengan klien.

Dengan skema umum, mari beralih ke detail.



Pertama, saya akan berbicara tentang beberapa fitur interaksi client-server. Kami menggunakan protokol biner kami karena sangat efisien dan memungkinkan untuk menghemat lalu lintas. Kedua, untuk operasi apa pun dengan akun yang mengubahnya, server tidak mengirim perubahan ini kepada klien, tetapi versi lengkap (diperbarui) dari akun ini. Ini sedikit kurang efisien, tetapi tidak memakan banyak ruang dan menyederhanakan kehidupan kita baik pada klien maupun di server. Juga, frontend memastikan bahwa klien melakukan tidak lebih dari satu permintaan sekaligus. Ini memungkinkan Anda untuk menangkap bug pada klien, misalnya, ketika ia beralih ke layar lain sebelum pemain melihat hasil operasi sebelumnya.

Sekarang sedikit tentang bagaimana mengatur frontend.



Frontend pada dasarnya adalah server web yang mendengarkan koneksi soket web. Untuk setiap sesi, dua proses dibuat. Proses pertama melayani koneksi soket web itu sendiri, dan yang kedua adalah mesin negara yang menggambarkan keadaan klien saat ini. Berdasarkan keadaan ini, itu menentukan validitas permintaan dari klien. Misalnya, hampir semua permintaan tidak dapat diselesaikan sampai otorisasi selesai. Karena tidak ada keadaan di frontend selain sesi ini, sangat mudah untuk menambahkan instance frontend baru, tetapi sedikit lebih sulit untuk menghapus yang lama. Sebelum menghapus instalan, Anda harus membiarkan semua klien menyelesaikan permintaan mereka saat ini dan meminta mereka untuk terhubung kembali ke instance lain.

Sekarang tentang bagaimana tampilan backend. Saat ini, terdiri dari lima layanan.



Kesepakatan pertama dengan segala sesuatu yang berhubungan dengan akun - dari pembelian untuk mata uang dalam game hingga menyelesaikan pencarian. Yang kedua bekerja dengan semua yang berhubungan dengan pertandingan - berinteraksi langsung dengan GameLift dan server game. Layanan ketiga adalah belanja uang sungguhan. Yang keempat dan kelima bertanggung jawab untuk interaksi sosial - satu untuk teman, yang lain untuk permainan pesta.

Setiap layanan backend dari sudut pandang arsitektur terlihat sangat identik. Mereka adalah satu set pipa, yang masing-masing memproses satu jenis pesan. Pipa terdiri dari dua elemen: produsen dan konsumen.



Satu-satunya tugas produsen adalah membaca pesan dari antrian. Oleh karena itu, ini diterapkan sepenuhnya dalam bentuk umum dan untuk setiap saluran pipa kita hanya perlu menunjukkan berapa banyak produsen yang ada, dari mana antrean dibaca dan berapa banyak konsumen yang akan dilayani oleh masing-masing produsen. Konsumen, di sisi lain, diimplementasikan secara terpisah untuk setiap pipa dan merupakan modul dengan satu-satunya fungsi wajib yang menerima satu pesan, melakukan semua pekerjaan yang diperlukan dan mengembalikan daftar pesan yang perlu dikirim ke layanan lain ke klien atau ke server game. Produser juga menerapkan tekanan balik sehingga dengan peningkatan tajam dalam jumlah pesan tidak ada kelebihan, dan meminta pesan tidak lebih dari yang dimiliki konsumen bebas.

Layanan Backend tidak mengandung keadaan apa pun, sehingga mudah bagi kami untuk menambah dan menghapus instance lama. Satu-satunya hal yang harus dilakukan sebelum menghapus adalah meminta produsen untuk berhenti membaca pesan baru dan memberi konsumen sedikit waktu untuk menyelesaikan pemrosesan pesan aktif.

Bagaimana interaksi dengan GameLift terjadi? GameLift terdiri dari beberapa komponen. Dari yang kami gunakan, ini adalah mak comblang FlexMatch, antrian penempatan yang menentukan wilayah tertentu untuk menyelenggarakan sesi permainan dengan para pemain ini, dan armadanya sendiri, yang terdiri dari server game.



Bagaimana interaksi ini? Meta berkomunikasi langsung hanya dengan mak comblang, mengirimkan permintaan untuk menemukan kecocokan. Dan dia memberitahukan meta dari semua peristiwa selama perjodohan melalui antrian pesan yang sama. Dan begitu dia menemukan kelompok pemain yang cocok untuk memulai pertandingan, dia mengirimkan permintaan ke antrian penempatan, yang pada gilirannya memilih server untuk mereka.

Interaksi meta dengan server game sangat sederhana. Server permainan membutuhkan informasi tentang akun, bot, dan peta, dan meta mengirimkan semua informasi ini ke antrian yang dibuat khusus untuk pertandingan ini dalam satu pesan.



Dan server permainan, setelah aktivasi, mulai mendengarkan antrian ini dan menerima semua data yang dibutuhkan. Di akhir pertandingan, ia mengirimkan hasilnya ke antrian umum yang didengarkan oleh meta.

Sekarang mari kita beralih ke infrastruktur tambahan yang kita gunakan.



Menyebarkan layanan cukup sederhana. Mereka semua bekerja dalam wadah buruh pelabuhan, dan kami menggunakan Amazon ECS untuk orkestrasi. Ini jauh lebih sederhana daripada Kubernetes, tentu saja, kurang canggih, tetapi melakukan tugas yang kita butuhkan darinya. Yaitu: layanan penskalaan dan rilis bergulir, ketika kita perlu mengisi semacam perbaikan bug.

Dan layanan terakhir yang juga kami gunakan adalah AWS Fargate. Ini menyelamatkan kita dari keharusan mengelola secara mandiri gugusan mesin tempat wadah buruh pelabuhan kami beroperasi.



Sebagai penyimpanan utama kami menggunakan DynamoDB. Pertama-tama, kami memilihnya karena sangat mudah dioperasikan dan berskala. Kami juga menggunakan Redis sebagai penyimpanan tambahan melalui layanan yang dikelola Amazon ElasiCache. Kami menggunakannya untuk tugas pemeringkatan pemain global dan untuk caching data akun dasar dalam situasi di mana kami harus segera mengembalikan data pada ratusan akun game kepada klien (misalnya, dalam tabel penilaian yang sama atau dalam daftar teman).

Untuk menyimpan konfigurasi, mekanisme meta-gameplay, deskripsi senjata, pahlawan, dll. kami menggunakan file JSON yang kami lampirkan ke gambar layanan yang membutuhkannya. Karena jauh lebih mudah bagi kami untuk meluncurkan versi baru layanan dengan data yang diperbarui (jika beberapa jenis bug terdeteksi) daripada membuat keputusan yang secara dinamis akan memperbarui data ini dari beberapa penyimpanan eksternal dalam runtime.

Untuk logging dan pemantauan, kami menggunakan beberapa layanan.



Mari kita mulai dengan CloudWatch. Ini adalah layanan pemantauan di mana metrik dari semua layanan Amazon berduyun-duyun. Karenanya, kami memutuskan untuk mengirim metrik dari server meta kami ke sana juga. Dan untuk logging, kami menggunakan pendekatan umum baik pada klien dan pada server game dan pada server meta. Kami mengirim semua log ke layanan Amazon Kinesis Firehose, yang pada gilirannya mentransfernya ke Elasticseach dan S3.

Di Elasticseach, kami hanya menyimpan data yang relatif baru dan dengan bantuan Kibana kami mencari kesalahan, menyelesaikan beberapa tugas analisis game dan membangun dashboard operasional, misalnya, dengan jadwal CCU dan jumlah instalasi baru. S3 berisi semua data historis dan kami menggunakannya melalui layanan Athena, yang menyediakan antarmuka SQL di atas data dalam S3.

Sekarang sedikit tentang bagaimana kita menggunakan Terraform.



Terraform adalah alat yang memungkinkan Anda untuk mendeskripsikan infrastruktur secara deklaratif dan, jika ada perubahan dalam deskripsi, secara otomatis menentukan tindakan yang perlu Anda ambil untuk membawa infrastruktur Anda ke tampilan yang diperbarui. Dengan demikian, memiliki deskripsi tunggal, kami mendapatkan lingkungan yang hampir identik untuk pementasan dan produksi. Juga, lingkungan ini sepenuhnya terisolasi, karena mereka ditempatkan di bawah akun yang berbeda. Satu-satunya kekurangan Terraform bagi kami adalah dukungan GameLift yang tidak lengkap.

Saya juga akan berbicara tentang bagaimana kami menerapkan pembaruan tanpa downtime.



Ketika kami merilis pembaruan, kami meningkatkan salinan dari sebagian besar sumber daya: layanan, antrian pesan, beberapa label dalam database. Dan para pemain yang mengunduh versi baru gim ini akan terhubung ke kluster yang diperbarui ini. Tetapi para pemain yang belum diperbarui dapat terus bermain untuk beberapa waktu di versi lama gim, terhubung ke kluster lama.

Bagaimana kami menerapkannya. Pertama, menggunakan mesin modul di Terraform. Kami telah mengalokasikan modul tempat kami menggambarkan semua sumber daya versi. Dan modul ini dapat diimpor beberapa kali, dengan parameter berbeda. Dengan demikian, untuk setiap versi kami mengimpor modul ini, yang menunjukkan jumlah versi ini. Juga, tidak adanya skema dalam DynamoDB membantu kami, yang memungkinkan untuk melakukan migrasi data tidak selama pembaruan, tetapi untuk menundanya untuk setiap akun sampai pemiliknya masuk ke versi baru permainan. Dan di penyeimbang, kami cukup menunjukkan untuk setiap versi aturan sehingga ia tahu ke mana harus mengarahkan pemain dengan versi yang berbeda.

Akhirnya, beberapa hal yang kami pelajari. Pertama, konfigurasi seluruh infrastruktur harus otomatis. Yaitu kami mengatur beberapa hal dengan tangan kami untuk sementara waktu, tetapi cepat atau lambat kami membuat kesalahan dalam pengaturan, karena ada beberapa fakap.



Dan hal terakhir - Anda harus memiliki replika atau salinan cadangan untuk setiap elemen infrastruktur Anda. Dan jika Anda tidak melakukannya untuk sesuatu, maka hal khusus ini akan mengecewakan kami.

Pertanyaan dari audiens


- Tapi apakah itu tidak mengganggu Anda bahwa autoscaling dapat bertahan terlalu banyak karena beberapa jenis kesalahan dan Anda akan mendapatkan banyak uang?

- Untuk penskalaan otomatis, batas masih ditentukan. Kami tidak akan menetapkan batas terlalu besar agar tidak jatuh untuk banyak uang. Ini adalah solusi + pemantauan utama. Anda dapat mengatur peringatan jika ada sesuatu yang terlalu kuat.

- Berapa batasan Anda saat ini? Relatif terhadap infrastruktur saat ini sebagai persentase.

- Sekarang kami memiliki fase uji beta terbuka di 11 negara, jadi bukan CCU yang besar untuk setidaknya entah bagaimana mengevaluasi. Sekarang infrastrukturnya terlalu banyak untuk jumlah orang yang kita miliki.

- Dan belum ada batasan?

- Ya, hanya saja mereka 10-100 kali lebih banyak dari CCU kami. Jangan berbuat lebih sedikit.

- Anda mengatakan bahwa Anda memiliki garis antara depan dan backend - ini sangat tidak biasa. Kenapa tidak langsung?

- Kami ingin layanan berkewarganegaraan dengan mudah menerapkan mekanisme pencadangan, sehingga layanan tidak meminta lebih banyak pesan daripada yang dimiliki penangan gratis. Juga, misalnya, ketika pawang gagal, antrian akan memberikan pesan yang sama ke pawang lain - mungkin itu akan berhasil.

- Apakah antriannya tetap ada?

- Ya. Ini adalah layanan SQS Amazon.

- Mengenai antrian: berapa banyak saluran yang dibuat selama pertandingan? Apakah Anda memiliki sejumlah saluran untuk setiap pertandingan?

- Ini menciptakan relatif sedikit. Sebagian besar antrian, seperti antrian permintaan, bersifat statis. Ada antrian permintaan untuk otorisasi, ada antrian untuk memulai pertandingan. Dari antrian yang dibuat secara dinamis, kami hanya memiliki antrian untuk setiap frontend (itu menciptakan untuk pesan masuk untuk klien saat startup) dan untuk setiap kecocokan kami membuat satu antrian. Dalam layanan ini, hampir tidak ada biaya, mereka memiliki permintaan yang sama. Yaitu setiap permintaan untuk SQS (buat antrian, baca sesuatu darinya) biayanya sama dan pada saat yang sama kami tidak menghapus antrian ini untuk disimpan, mereka akan dihapus nanti. Dan fakta bahwa mereka ada tidak membebani kita.

- Dalam arsitektur ini, ini tidak akan menjadi batas untuk Anda?

- Tidak.

Lebih banyak pembicaraan dengan Pixonic DevGAMM Talks


Source: https://habr.com/ru/post/id426875/


All Articles