
Sebuah pertanyaan / jawaban kecil:
Untuk siapa ini? Orang-orang yang memiliki sedikit atau tanpa pengalaman dengan sistem terdistribusi, dan yang tertarik melihat bagaimana mereka dapat dibangun, pola dan solusi apa yang ada.
Kenapa ini? Dirinya menjadi tertarik pada apa dan bagaimana. Saya mengambil informasi dari berbagai sumber, saya memutuskan untuk mempostingnya dalam bentuk yang terkonsentrasi, karena pada suatu waktu saya sendiri ingin melihat karya seperti itu. Sebenarnya, ini adalah pernyataan tekstual tentang pelemparan dan pemikiran pribadi saya. Juga, mungkin akan ada banyak koreksi dalam komentar dari orang-orang yang berpengetahuan, dan ini sebagian tujuan menulis semua ini dalam bentuk artikel.
Pernyataan masalah
Bagaimana cara ngobrol? Ini seharusnya menjadi tugas yang sepele, mungkin setiap beckender detik menggergaji sendiri, sama seperti pengembang game membuat tetris / ular mereka, dll. Saya mengambil yang ini, tetapi untuk membuatnya lebih menarik, itu harus siap untuk mengambil alih dunia, sehingga bisa menahan ratusan miliar pengguna aktif dan secara umum sangat keren. Kebutuhan yang jelas untuk arsitektur terdistribusi berasal dari ini, karena tidak realistis untuk memiliki kapasitas saat ini untuk memenuhi semua jumlah pelanggan imajiner pada satu mesin. Alih-alih hanya duduk dan menunggu kemunculan komputer kuantum, saya mulai mempelajari topik sistem terdistribusi.
Perlu dicatat bahwa respons cepat sangat penting, realtime terkenal, itu obrolan ! bukan pengiriman surat merpati.
% lelucon acak tentang pos Rusia %
Kami akan menggunakan Node.JS, ini sangat ideal untuk membuat prototipe. Untuk soket, ambil Socket.IO. Tulis di TypeScript.
Jadi apa yang kita inginkan:
- Sehingga pengguna bisa saling mengirim pesan
- Ketahui siapa yang online / offline
Bagaimana kita menginginkannya:
Server tunggal
Tidak ada yang bisa dikatakan terutama, langsung ke kode. Deklarasikan antarmuka pesan:
interface Message{ roomId: string,// message: string,// }
Di server:
io.on('connection', sock=>{ // sock.on('join', (roomId:number)=> sock.join(roomId)) // // sock.on('message', (data:Message)=> io.to(data.roomId).emit('message', data)) })
Pada klien, sesuatu seperti:
sock.on('connect', ()=> { const roomId = 'some room' // sock.on('message', (data:Message)=> console.log(`Message ${data.message} from ${data.roomId}`)) // sock.emit('join', roomId) // sock.emit('message', <Message>{roomId: roomId, message: 'Halo!'}) })
Anda dapat bekerja dengan status online seperti ini:
io.on('connection', sock=>{ // // , - // sock.on('auth', (uid:string)=> sock.join(uid)) //, , // // sock.on('isOnline', (uid:string, resp)=> resp(io.sockets.clients(uid).length > 0)) })
Dan pada klien:
sock.on('connect', ()=> { const uid = 'im uid, rly' // sock.emit('auth', uid) // sock.emit('isOnline', uid, (isOnline:boolean)=> console.log(`User online status is ${isOnline}`)) })
Catatan: kode tidak berjalan, saya hanya menulis dari memori misalnya
Sama seperti kayu bakar, kami memutar otorisasi nyata syudy, manajemen kamar (sejarah pesan, menambah / menghapus peserta) dan keuntungan.
TAPI! Tetapi kita akan mengambil alih perdamaian dunia, yang berarti ini bukan saatnya untuk berhenti, kita dengan cepat bergerak:
Node.JS cluster
Contoh penggunaan Socket.IO pada banyak node tepat di situs web resmi . Termasuk juga ada cluster Node.JS asli, yang bagi saya kelihatannya tidak dapat diterapkan pada tugas saya: memungkinkan kita untuk memperluas aplikasi kita di seluruh mesin, TAPI tidak di luar cakupannya, jadi kita pasti melewatkannya. Kita akhirnya perlu melampaui batas-batas sepotong besi!
Bagikan dan bersepeda
Bagaimana cara melakukannya? Jelas, Anda harus menghubungkan contoh kami, entah bagaimana diluncurkan tidak hanya di rumah di ruang bawah tanah, tetapi juga di ruang bawah tanah tetangga. Apa yang pertama kali terlintas dalam pikiran: kami membuat semacam tautan perantara yang akan berfungsi sebagai bus di antara semua simpul kami:

Ketika sebuah node ingin mengirim pesan ke yang lain, itu membuat permintaan ke Bus, dan sudah, pada gilirannya, meneruskannya ke tempat yang dibutuhkan, semuanya sederhana. Jaringan kami siap!
FIN.
... tapi tidak sesederhana itu?)
Dengan pendekatan ini, kami mengalami kinerja tautan perantara ini, dan memang kami ingin langsung menghubungi node yang diperlukan, karena apa yang bisa lebih cepat daripada komunikasi langsung? Jadi mari kita bergerak ke arah ini!
Apa yang dibutuhkan terlebih dahulu? Sebenarnya, sahkan satu contoh ke yang lain. Tetapi bagaimana yang pertama belajar tentang keberadaan yang kedua? Tetapi kami ingin memiliki jumlah yang tak terbatas dari mereka, secara acak menaikkan / menghapus! Kami membutuhkan server master yang alamatnya diketahui diketahui, semua orang terhubung, karena yang mengetahui semua node yang ada dalam jaringan dan berbagi informasi ini dengan semua orang.

Node naik, memberi tahu master tentang kebangkitannya, ia memberikan daftar node aktif lainnya, kami terhubung ke mereka dan hanya itu, jaringan sudah siap. Master mungkin Konsul atau sesuatu seperti itu, tetapi karena kita bersepeda, master harus dibuat sendiri.
Hebat, sekarang kita punya skynet kita sendiri! Tetapi implementasi obrolan saat ini di dalamnya tidak lagi cocok. Mari kita benar-benar datang dengan persyaratan:
- Ketika seorang pengguna mengirim pesan, kita perlu tahu kepada siapa dia mengirimkannya, yaitu, memiliki akses ke para peserta di ruangan itu.
- Ketika kami menerima peserta, kami harus mengirimkan pesan kepada mereka.
- Kita perlu tahu pengguna mana yang sedang online sekarang.
- Untuk kenyamanan - beri pengguna kesempatan untuk berlangganan status online pengguna lain, sehingga secara real time mereka belajar tentang perubahannya
Mari kita berurusan dengan pengguna. Misalnya, Anda bisa membuat master tahu simpul mana yang terhubung ke simpul mana. Situasinya adalah sebagai berikut:

Dua pengguna terhubung ke node yang berbeda. Tuan tahu ini, simpul tahu apa yang tuan tahu. Ketika UserB masuk, Node2 memberitahu Master, yang "mengingat" bahwa UserB terhubung ke Node2. Ketika UserA ingin mengirim pesan UserB, Anda mendapatkan gambar berikut:

Pada prinsipnya, semuanya berfungsi, tetapi saya ingin menghindari putaran perjalanan tambahan dalam bentuk menginterogasi master, akan lebih ekonomis untuk segera menghubungi simpul yang tepat secara langsung, karena itu sebabnya semuanya dimulai. Ini dapat dilakukan jika mereka memberi tahu semua orang di sekitar pengguna yang terhubung dengan mereka, masing-masing dari mereka menjadi analog mandiri dari wizard, dan wizard itu sendiri menjadi tidak perlu, karena daftar rasio "Pengguna => Node" diduplikasi untuk semua orang. Pada awal node, cukup terhubung ke yang sudah berjalan, tarik daftarnya ke diri sendiri dan voila, itu juga siap untuk pertempuran.


Tetapi sebagai trade off, kami mendapatkan duplikat dari daftar, yang, meskipun merupakan rasio "id pengguna -> [koneksi host]", tetapi dengan jumlah pengguna yang cukup itu akan berubah menjadi memori yang cukup besar. Dan secara umum, memotongnya sendiri - itu jelas menampar industri sepeda. Semakin banyak kode, semakin banyak kesalahan potensial. Mungkin kami membekukan opsi ini dan melihat apa yang sudah siap:
Pialang pesan
Entitas yang mengimplementasikan "Bus" yang sama, "tautan perantara" yang disebutkan di atas. Tugasnya adalah menerima dan mengirim pesan. Kami, sebagai pengguna, dapat berlangganan dan mengirim milik kami sendiri. Semuanya sederhana.
Ada RabbitMQ dan Kafka yang telah terbukti: mereka hanya melakukan apa yang mereka kirimkan pesan - ini adalah tujuan mereka, dijejali dengan semua fungsi yang diperlukan ke leher. Di dunia mereka, sebuah pesan harus disampaikan apa pun yang terjadi.
Pada saat yang sama, ada Redis dan pub / sub-nya - sama dengan orang-orang yang disebutkan di atas, tetapi lebih meragukan: ia hanya dengan bodohnya menerima pesan dan mengirimkannya ke pelanggan, tanpa ada antrian dan biaya tambahan lainnya. Dia benar-benar tidak peduli dengan pesan itu sendiri, mereka akan menghilang, jika pelanggan hang - dia akan membuangnya dan mengambil yang baru, seolah-olah mereka akan melemparkan poker panas ke tangannya yang ingin Anda singkirkan lebih cepat. Juga, jika dia tiba-tiba jatuh - semua pesan juga akan tenggelam bersamanya. Dengan kata lain, tidak ada pertanyaan tentang jaminan pengiriman.
... dan inilah yang Anda butuhkan!
Yah, sungguh, kami hanya mengobrol. Bukan semacam layanan uang penting atau pusat kendali penerbangan luar angkasa, tapi ... hanya obrolan. Risiko bahwa Pete bersyarat setahun sekali tidak akan menerima satu pesan dari seribu - itu dapat diabaikan jika sebagai imbalannya kita mendapatkan pertumbuhan produktivitas dan di tempat itu dengan jumlah pengguna untuk hari yang sama, menukarkan semua kejayaannya. Selain itu, pada saat yang sama, Anda dapat menyimpan riwayat pesan dalam beberapa jenis repositori persisten, yang berarti bahwa Petya masih akan melihat pesan yang terlewat itu dengan memuat ulang halaman / aplikasi. Itu sebabnya kami akan fokus pada Redis pub / sub, atau lebih tepatnya: lihat adaptor yang ada untuk SocketIO, yang disebutkan dalam artikel di kantor. situs
Jadi apa ini?
Adaptor redis
https://github.com/socketio/socket.io-redis
Dengan bantuannya, aplikasi biasa melalui beberapa baris dan jumlah gerakan minimum berubah menjadi obrolan yang nyata! Tapi bagaimana caranya? Jika Anda melihat ke dalam - ternyata hanya ada satu file per setengah ratus baris.
Dalam kasus ketika kami mengeluarkan pesan
io.emit("everyone", "hello")
itu didorong ke lobak, ditransmisikan ke semua contoh lain dari obrolan kami, yang pada gilirannya mengeluarkannya secara lokal di soket

Pesan tersebut akan didistribusikan ke semua node bahkan jika kami mengeluarkan ke pengguna tertentu. Artinya, setiap node menerima semua pesan dan sudah mengerti apakah perlu.
Juga, ada rpc sederhana (memanggil prosedur jarak jauh), yang memungkinkan tidak hanya mengirim tetapi juga menerima jawaban. Misalnya, Anda dapat mengontrol soket dari jarak jauh, seperti "siapa yang ada di ruangan yang ditentukan", "memesan soket untuk bergabung dengan ruangan", dll.
Apa yang bisa dilakukan dengan ini? Misalnya, gunakan ID pengguna sebagai nama kamar (id pengguna == id kamar). Saat memberi otorisasi, untuk menghubungkan soket ke dalamnya, dan ketika kami ingin mengirim pesan ke pengguna - cukup helm ke dalamnya. Juga, kita dapat mengetahui apakah pengguna sedang online, cukup melihat apakah ada soket di ruangan yang ditentukan.
Pada prinsipnya, kita bisa berhenti di sini, tetapi seperti biasa, itu tidak cukup bagi kita:
- Leher botol dalam satu contoh lobak
- Redundansi, saya ingin agar node hanya menerima pesan yang mereka butuhkan
Dengan mengorbankan paragraf satu, lihat hal seperti:
Kelompok redis
Ini menghubungkan beberapa contoh lobak, setelah itu mereka bekerja secara keseluruhan. Tetapi bagaimana dia melakukannya? Ya seperti ini:

... dan kami melihat bahwa pesan tersebut digandakan ke semua anggota kluster. Artinya, ini tidak dimaksudkan untuk meningkatkan produktivitas, tetapi untuk meningkatkan keandalan, yang tentu saja baik dan perlu, tetapi untuk kasus kami itu tidak memiliki nilai dan tidak menyelamatkan situasi dengan hambatan dengan cara apa pun, ditambah jumlah itu bahkan lebih banyak pemborosan sumber daya.

Saya seorang pemula, saya tidak tahu banyak, kadang-kadang saya harus kembali ke pitchforking, yang akan kita lakukan. Tidak, biarkan lobak agar tidak tergelincir sama sekali, tetapi Anda perlu memikirkan sesuatu dengan arsitektur karena yang saat ini tidak baik.
Berbelok ke arah yang salah
Apa yang kita butuhkan Tingkatkan throughput keseluruhan. Sebagai contoh, mari kita coba secara bodoh menelurkan contoh lain. Bayangkan bahwa socket.io-redis dapat terhubung ke beberapa, ketika mendorong pesan, ia memilih secara acak, dan berlangganan semuanya. Ternyata seperti ini:

Voila! Secara umum, masalah terpecahkan, lobak tidak lagi menjadi hambatan, Anda dapat menelurkan jumlah salinan! Tetapi mereka menjadi simpul. Ya, ya, instance obrolan kami masih mencerna SEMUA pesan, kepada siapa pesan itu tidak dimaksudkan.
Anda dapat sebaliknya: berlangganan satu secara acak, yang akan mengurangi beban pada node, dan mendorong semuanya:

Kita melihat bahwa itu telah menjadi sebaliknya: node merasa lebih tenang, tetapi beban pada contoh lobak telah meningkat. Ini juga tidak baik. Anda perlu sedikit bersepeda.
Untuk memompa sistem kami, kami akan meninggalkan paket socket.io-redis sendiri, meskipun keren, kami membutuhkan lebih banyak kebebasan. Jadi, kami menghubungkan lobak:
// : const pub = new RedisClient({host: 'localhost', port: 6379})// const sub = new RedisClient({host: 'localhost', port: 6379})// // interface Message{ roomId: string,// message: string,// }
Siapkan sistem pengiriman pesan kami:
// sub.on('message', (channel:string, dataRaw:string)=> { const data = <Message>JSON.parse(dataRaw) io.to(data.roomId).emit('message', data)) }) // sub.subscribe("messagesChannel") // sock.on('join', (roomId:number)=> sock.join(roomId)) // sock.on('message', (data:Message)=> { // pub.publish("messagesChannel", JSON.stringify(data)) })
Saat ini, ternyata seperti di socket.io-redis: kami mendengarkan semua pesan. Sekarang kita akan memperbaikinya.
Kami mengatur langganan sebagai berikut: ingat konsep dengan "id pengguna == id kamar", dan ketika pengguna muncul, kami berlangganan saluran dengan nama yang sama di lobak. Dengan demikian, node kami hanya akan menerima pesan yang ditujukan untuk mereka, dan tidak mendengarkan "seluruh siaran".
// sub.on('message', (channel:string, message:string)=> { io.to(channel).emit('message', message)) }) let UID:string|null = null; sock.on('auth', (uid:string)=> { UID = uid // - // UID sub.subscribe(UID) // sock.join(UID) }) sock.on('writeYourself', (message:string)=> { // , UID if (UID) pub.publish(UID, message) })
Luar biasa, sekarang kami yakin bahwa node hanya menerima pesan yang ditujukan untuk mereka, tidak lebih! Perlu dicatat, bagaimanapun, bahwa langganan itu sendiri sekarang jauh, jauh lebih besar, yang berarti bahwa mereka akan memakan memori yoy yoy, + lebih banyak operasi berlangganan / berhenti berlangganan, yang relatif mahal. Tetapi bagaimanapun juga, ini memberi kami beberapa fleksibilitas, Anda bahkan dapat berhenti pada saat ini dan mengunjungi kembali semua opsi sebelumnya, yang sudah memperhitungkan properti simpul baru kami dalam bentuk pesan penerimaan yang lebih selektif dan murni. Misalnya, node dapat berlangganan ke salah satu dari beberapa contoh lobak, dan ketika mendorong, mengirim pesan ke semua contoh:

... tetapi, apa pun yang dikatakan orang, mereka masih tidak memberikan perpanjangan yang tak terbatas dengan overhead yang masuk akal, Anda harus melahirkan opsi lain. Pada satu titik, skema berikut muncul di benak saya: bagaimana jika contoh lobak dibagi menjadi beberapa kelompok, katakanlah A dan B, dua contoh di masing-masing. Saat berlangganan, node ditandatangani oleh satu instance dari setiap grup, dan ketika mendorong, mereka mengirim pesan ke semua instance dari satu grup acak.


Dengan demikian, kita mendapatkan struktur operasi dengan potensi ekspansi yang tak terbatas dalam waktu nyata, beban pada simpul individu pada titik mana pun tidak bergantung pada ukuran sistem, karena:
- Total bandwidth dibagi antara grup, yaitu, dengan peningkatan pengguna / aktivitas, kami cukup membandingkan grup tambahan.
- Manajemen pengguna (langganan) dibagi dalam grup itu sendiri, yaitu ketika meningkatkan pengguna / langganan, kami hanya menambah jumlah instance dalam grup.
... dan seperti biasa ada satu "TETAPI": semakin banyak semuanya didapat, semakin banyak sumber daya yang dibutuhkan untuk keuntungan selanjutnya, bagi saya sepertinya trade off terlalu tinggi.
Secara umum, jika Anda berpikir tentang hal itu, colokan yang disebutkan di atas berasal dari tidak mengetahui pengguna mana yang berada pada simpul mana. Ya, memang, jika kami memiliki informasi ini, kami dapat mendorong pesan tepat di mana mereka perlu, tanpa duplikasi yang tidak perlu. Apa yang telah kami coba lakukan selama ini? Mereka mencoba membuat sistem ini dapat diskalakan tanpa batas, sementara tidak memiliki mekanisme pengalamatan yang jelas, yang pasti akan menemui jalan buntu atau redundansi yang tidak dapat dibenarkan. Misalnya, Anda dapat mengingat wizard yang bertindak sebagai "buku alamat":

Sesuatu yang mirip memberitahu pria ini:
Untuk mendapatkan lokasi pengguna, kami melakukan perjalanan bolak-balik tambahan, yang pada prinsipnya OK, tetapi tidak dalam kasus kami. Sepertinya kita menggali ke arah yang salah, kita membutuhkan sesuatu yang lain ...
Kekuatan hash
Ada yang namanya hash. Ini memiliki beberapa kisaran nilai yang terbatas. Anda bisa mendapatkannya dari data apa pun. Tetapi bagaimana jika Anda membagi rentang ini di antara contoh lobak? Yah, kami mengambil ID pengguna, menghasilkan hash, dan tergantung pada kisaran di mana ia ternyata berlangganan / mendorong ke satu contoh spesifik. Artinya, kami tidak tahu sebelumnya di mana pengguna itu ada, tetapi setelah menerimanya, kami dapat dengan yakin mengatakan bahwa itu ada dalam contoh, inf 100. Sekarang hal yang sama, tetapi dengan kode:
function hash(val:string):number{/**/}// -, const clients:RedisClient[] = []// const uid = "some uid"// //, // const selectedClient = clients[hash(uid) % clients.length]
Voila! Sekarang kita tidak tergantung pada jumlah contoh kata secara umum, kita dapat skala sebanyak yang kita suka tanpa biaya tambahan! Nah, serius, ini adalah opsi yang brilian, satu-satunya minus adalah kebutuhan untuk me-restart sistem ketika memperbarui jumlah contoh lobak. Ada yang namanya dering Standar dan cincin Partisi yang memungkinkan Anda untuk mengatasinya, tetapi itu tidak berlaku di sistem pesan. Ya, Anda bisa membuat logika migrasi langganan di antara instance, tetapi masih membutuhkan tambahan kode dengan ukuran yang tidak bisa dipahami, dan seperti yang kita tahu - semakin banyak kode, semakin banyak bug, kita tidak membutuhkan ini, terima kasih. Dan dalam kasus kami, downtime merupakan tradeoff yang cukup dapat diterima.
Anda juga dapat melihat RabbitMQ dengan plugin -nya, yang memungkinkan Anda untuk melakukan hal yang sama seperti yang kami lakukan, dan + menyediakan migrasi langganan (seperti yang saya katakan di atas - itu terkait dengan fungsionalitas dari ujung kepala hingga ujung kaki). Pada prinsipnya, Anda dapat mengambilnya dan tidur nyenyak, tetapi jika seseorang meraba-raba dalam penyetelannya untuk membawa mode ke waktu nyata, hanya menyisakan fitur dengan cincin hash.
Membanjiri repositori di github.
Ini mengimplementasikan versi final yang telah kami datangi. Selain itu, ada logika tambahan untuk bekerja dengan kamar (dialog).
Secara umum, saya puas dan dapat dibulatkan.
Total
Anda dapat melakukan apa saja, tetapi ada yang namanya sumber daya, dan mereka terbatas, jadi Anda perlu menggeliat.
Kami mulai dengan ketidaktahuan sepenuhnya tentang bagaimana sistem terdistribusi dapat bekerja pada pola beton yang kurang lebih nyata, dan itu bagus.