Semua startup yang baik mati dengan cepat atau tumbuh sesuai skala. Kami akan membuat model startup seperti itu, yang pertama tentang fitur, dan kemudian tentang kinerja. Kami akan meningkatkan kinerja dengan MongoDB, solusi penyimpanan data NoSQL yang populer. MongoDB mudah untuk memulai, dan banyak masalah memiliki solusi di luar kotak. Namun, ketika beban meningkat, garu keluar yang tidak ada yang memperingatkan Anda tentang sebelumnya ... sampai hari ini!

Pemodelan dilakukan oleh
Sergey Zagursky , yang bertanggung jawab atas infrastruktur backend pada umumnya, dan MongoDB pada khususnya, di
Joom . Itu juga terlihat di sisi server pengembangan MMORPG Skyforge. Seperti yang dideskripsikan oleh Sergei sendiri, ia adalah "seorang pembuat kerucut profesional dengan dahi dan penggaruknya sendiri." Di bawah mikroskop, sebuah proyek yang menggunakan strategi akumulasi untuk mengelola utang teknis. Dalam versi teks
laporan ini di HighLoad ++, kami akan bergerak dalam urutan kronologis dari terjadinya masalah ke solusi menggunakan MongoDB.
Kesulitan pertama
Kami memodelkan sebuah startup yang membuat masalah. Tahap pertama kehidupan - fitur diluncurkan di startup kami dan, tanpa diduga, pengguna datang. Server MongoDB kecil-kecil kami memiliki beban yang tidak pernah kami impikan. Tapi kami ada di cloud, kami startup! Kami melakukan hal yang paling sederhana: lihat permintaan - oh, dan di sini kami memiliki seluruh koreksi dikurangi untuk setiap pengguna, di sini kami akan membuat indeks, kami akan menambahkan perangkat keras di sana, dan di sini kami menyimpan cache.
Semuanya - kita hidup terus!
Jika masalah dapat diselesaikan dengan cara sederhana seperti itu, mereka harus diselesaikan dengan cara ini.
Tetapi jalur masa depan dari sebuah startup yang sukses adalah penundaan lambat dan menyakitkan dari penskalaan horizontal. Saya akan mencoba memberikan saran tentang bagaimana cara bertahan periode ini, mendapatkan scaling dan tidak menginjak menyapu.
Perekaman lambat
Ini adalah salah satu masalah yang mungkin Anda temui. Apa yang harus dilakukan jika Anda bertemu dengannya, dan metode di atas tidak membantu? Jawab:
mode jaminan daya tahan dalam MongoDB secara default . Dalam tiga kata kerjanya seperti ini:
- Kami sampai pada baris utama dan berkata: "Tulis!".
- Replika primer direkam.
- Setelah itu, replika sekunder dibacakan darinya dan mereka berkata primer: "Kami merekam!"
Pada saat sebagian besar replika sekunder melakukan ini, permintaan dianggap lengkap dan kontrol kembali ke driver dalam aplikasi. Jaminan semacam itu memungkinkan kami untuk memastikan bahwa ketika kontrol telah kembali ke aplikasi, daya tahan tidak akan pergi ke mana pun, bahkan jika MongoDB berbaring, kecuali untuk bencana yang benar-benar mengerikan.
Untungnya, MongoDB adalah basis data yang memungkinkan Anda mengurangi jaminan daya tahan untuk setiap permintaan individu.
Untuk permintaan penting, kami dapat membiarkan jaminan daya tahan maksimum secara default, dan untuk beberapa permintaan kami dapat menguranginya.
Kelas Permintaan
Lapisan pertama jaminan yang dapat kita hapus adalah
tidak menunggu konfirmasi catatan oleh sebagian besar replika . Ini menghemat latensi, tetapi tidak menambah bandwidth. Tetapi kadang-kadang latensi adalah yang Anda butuhkan, terutama jika cluster sedikit kelebihan beban dan replika sekunder tidak berfungsi secepat yang kami inginkan.
{w:1, j:true}
Jika kita menulis catatan dengan jaminan seperti itu, maka pada saat kita mendapatkan kendali dalam aplikasi, kita tidak lagi tahu apakah catatan itu akan hidup setelah beberapa jenis kecelakaan. Tapi biasanya, dia masih hidup.
Jaminan berikutnya, yang juga memengaruhi bandwidth dan latensi, adalah
menonaktifkan konfirmasi logging . Entri jurnal tetap ditulis. Majalah adalah salah satu mekanisme fundamental. Jika kami mematikan konfirmasi penulisan untuk itu, maka kami tidak melakukan dua hal:
fsync pada log dan
jangan menunggu sampai selesai . Ini dapat
menghemat banyak sumber daya disk dan mendapatkan
peningkatan banyak dalam throughput hanya
dengan mengubah daya tahan jaminan.
{w:1, j:false}
Jaminan daya tahan yang paling ketat adalah
menonaktifkan segala pengakuan . Kami hanya akan menerima konfirmasi bahwa permintaan telah mencapai replika utama. Ini akan menghemat latensi dan tidak meningkatkan throughput dengan cara apa pun.
{w:0, j:false} — .
Kami juga akan menerima berbagai hal lain, misalnya, rekaman gagal karena konflik dengan kunci unik.
Operasi apa yang berlaku untuk ini?
Saya akan memberi tahu Anda tentang aplikasi ke pengaturan di Joom. Selain beban dari pengguna, di mana tidak ada konsesi daya tahan, ada beban yang dapat digambarkan sebagai beban batch latar belakang: memperbarui, menghitung kembali peringkat, mengumpulkan data analitik.
Operasi latar belakang ini bisa memakan waktu berjam-jam, tetapi dirancang sedemikian rupa sehingga jika istirahat, misalnya, backend macet, mereka tidak akan kehilangan hasil dari semua pekerjaan mereka, tetapi akan dilanjutkan dari titik di masa lalu. Mengurangi jaminan daya tahan berguna untuk tugas-tugas seperti itu, terutama karena fsync dalam log, seperti operasi lainnya, akan meningkatkan latensi juga untuk membaca.
Baca penskalaan
Masalah selanjutnya adalah
bandwidth membaca tidak cukup . Ingatlah bahwa di cluster kami tidak hanya replika primer, tetapi juga replika sekunder
yang dapat Anda baca . Ayo lakukan.
Anda bisa membaca, tetapi ada nuansa. Data sedikit usang akan datang dari replika sekunder - dengan 0,5-1 detik. Dalam kebanyakan kasus, ini normal, tetapi perilaku replika sekunder berbeda dari perilaku replika primer.
Pada sekunder, ada proses menggunakan oplog, yang bukan pada replika primer. Proses ini tidak dirancang untuk latensi rendah - hanya pengembang MongoDB tidak peduli dengan ini. Dalam kondisi tertentu, proses penggunaan oplog dari primer ke sekunder dapat menyebabkan keterlambatan hingga 10 detik.
Replika sekunder tidak cocok untuk permintaan pengguna - pengalaman pengguna mengambil langkah cepat ke tempat sampah.
Pada cluster yang tidak diarsir, paku ini kurang terlihat, tetapi masih ada. Cluster shard menderita karena oplog sangat dipengaruhi oleh penghapusan, dan
penghapusan adalah bagian dari pekerjaan penyeimbang . Penyeimbang andal, dengan penuh rasa menghapus dokumen oleh puluhan ribu dalam waktu singkat.
Jumlah koneksi
Faktor selanjutnya yang harus dipertimbangkan adalah
batas jumlah koneksi pada instance MongoDB . Secara default, tidak ada batasan,
kecuali sumber daya OS - Anda dapat terhubung saat memungkinkan.
Namun, semakin banyak permintaan bersamaan, semakin lambat mereka berjalan.
Kinerja menurun secara nonlinier . Karena itu, jika lonjakan permintaan datang kepada kami, lebih baik melayani 80% daripada tidak melayani 100%. Jumlah koneksi harus dibatasi langsung ke MongoDB.
Tetapi ada bug yang dapat menyebabkan masalah karena ini. Secara khusus,
kumpulan koneksi di sisi MongoDB adalah umum untuk koneksi intracluster pengguna dan layanan . Jika aplikasi "memakan" semua koneksi dari kumpulan ini, maka integritas dapat dilanggar di cluster.
Kami belajar tentang ini ketika kami akan membangun kembali indeks, dan karena kami perlu menghapus keunikan dari indeks, prosedur melewati beberapa tahap. Di MongoDB, Anda tidak dapat membangun di sebelah indeks sama, tetapi tanpa keunikan. Karena itu, kami ingin:
- Bangun indeks serupa tanpa keunikan
- hapus indeks dengan keunikan;
- Buat indeks tanpa keunikan alih-alih jarak jauh;
- hapus sementara.
Ketika indeks sementara masih diselesaikan pada urutan kedua, kami mulai menghapus indeks unik. Pada titik ini, MongoDB sekunder mengumumkan kuncinya. Beberapa metadata diblokir, dan sebagian besar semua catatan berhenti: mereka menggantung di
kolam koneksi dan menunggu mereka untuk mengonfirmasi bahwa catatan telah berlalu. Semua bacaan di sekunder juga berhenti karena log global ditangkap.
Cluster dalam keadaan yang menarik juga kehilangan konektivitasnya. Kadang-kadang muncul dan ketika dua komentar saling terhubung, mereka mencoba membuat pilihan di negara mereka yang tidak bisa mereka buat, karena mereka memiliki kunci global.
Moral cerita: jumlah koneksi harus dipantau.
Ada penyapu MongoDB yang terkenal, yang masih sering diserang sehingga saya memutuskan untuk berjalan-jalan di atasnya.
Jangan kehilangan dokumen
Jika Anda mengirim permintaan dengan indeks ke MongoDB, maka
permintaan tersebut mungkin tidak mengembalikan semua dokumen yang memenuhi syarat, dan dalam kasus yang sama sekali tidak terduga. Hal ini disebabkan oleh fakta bahwa ketika kita pergi ke awal indeks, dokumen, yang pada akhirnya, pindah ke awal untuk dokumen-dokumen yang kami lewati. Ini semata
- mata disebabkan
oleh mutabilitas indeks . Untuk iterasi yang andal, gunakan
indeks pada bidang yang tidak
stabil dan tidak akan ada kesulitan.
MongoDB memiliki pandangan sendiri tentang indeks mana yang akan digunakan. Solusinya sederhana -
dengan bantuan $ hint, kami memaksa MongoDB untuk menggunakan indeks yang kami tentukan .
Ukuran Koleksi
Startup kami sedang berkembang, ada banyak data, tetapi saya tidak ingin menambahkan disk - kami telah menambahkan tiga kali dalam sebulan terakhir. Mari kita lihat apa yang disimpan dalam data kita, lihat ukuran dokumen. Bagaimana memahami di mana dalam koleksi Anda dapat mengurangi ukuran? Menurut dua parameter.
- Ukuran dokumen tertentu untuk dimainkan dengan panjangnya:
Object.bsonsize()
;
- Menurut ukuran rata-rata dokumen dalam koleksi :
db.c.stats().avgObjectSize
.
Bagaimana cara memengaruhi ukuran dokumen?
Saya punya jawaban tidak spesifik untuk pertanyaan ini. Pertama,
nama bidang yang panjang menambah ukuran dokumen. Di setiap dokumen, semua nama bidang disalin, jadi jika dokumen memiliki nama bidang yang panjang, maka ukuran nama harus ditambahkan ke ukuran setiap dokumen. Jika Anda memiliki koleksi dengan sejumlah besar dokumen kecil di beberapa bidang, maka beri nama bidang dengan nama pendek: "A", "B", "CD" - maksimal dua huruf.
Pada disk, ini diimbangi dengan kompresi , tetapi semuanya disimpan dalam cache apa adanya.
Kiat kedua adalah bahwa kadang-kadang
beberapa bidang dengan kardinalitas rendah dapat ditempatkan atas nama koleksi . Misalnya, bidang seperti itu mungkin bahasa. Jika kami memiliki koleksi dengan terjemahan ke dalam bahasa Rusia, Inggris, Prancis, dan bidang dengan informasi tentang bahasa yang disimpan, nilai bidang ini dapat dimasukkan dalam nama koleksi. Jadi kita akan
mengurangi ukuran dokumen dan dapat
mengurangi jumlah dan ukuran indeks - penghematan
semata ! Ini tidak selalu dapat dilakukan, karena kadang-kadang ada indeks di dalam dokumen yang tidak akan berfungsi jika koleksi dibagi menjadi koleksi yang berbeda.
Kiat terakhir pada ukuran dokumen -
gunakan bidang _id . Jika data Anda memiliki kunci unik alami, letakkan langsung di id_field. Bahkan jika kuncinya adalah komposit - gunakan id komposit. Itu diindeks sempurna. Hanya ada satu penggaruk kecil - jika marshaller Anda kadang-kadang mengubah urutan bidang, maka id dengan nilai bidang yang sama, tetapi dengan urutan yang berbeda akan dianggap id berbeda dalam hal indeks unik di MongoDB. Dalam beberapa kasus, ini bisa terjadi di Go.
Ukuran Indeks
Indeks menyimpan salinan bidang yang disertakan di dalamnya . Ukuran indeks terdiri dari data yang diindeks. Jika kami mencoba untuk mengindeks bidang yang besar, maka bersiaplah untuk ukuran indeks menjadi besar.
Momen kedua sangat mengembang indeks:
bidang array dalam indeks kalikan bidang lain dari dokumen dalam indeks ini . Hati-hati dengan array besar dalam dokumen: jangan mengindeks sesuatu yang lain ke array, atau bermain-main dengan urutan bidang dalam indeks terdaftar.
Urutan bidang penting ,
terutama jika salah satu bidang indeks adalah array . Jika bidang berbeda dalam kardinalitas, dan dalam satu bidang jumlah nilai yang mungkin sangat berbeda dari jumlah nilai yang mungkin di bidang lain, maka masuk akal untuk membangunnya dengan meningkatkan kardinalitas.
Anda dapat dengan mudah menyimpan 50% dari ukuran indeks jika Anda bertukar bidang dengan kardinalitas berbeda. Permutasi bidang dapat memberikan pengurangan ukuran yang lebih signifikan.
Terkadang, ketika bidang berisi nilai yang besar, kita tidak perlu membandingkan nilai ini lebih atau kurang, melainkan perbandingan kesetaraan yang jelas. Kemudian
indeks pada bidang dengan konten berat dapat
diganti dengan indeks pada hash dari bidang ini . Salinan hash akan disimpan dalam indeks, bukan salinan dari bidang ini.
Hapus dokumen
Saya sudah menyebutkan bahwa menghapus dokumen adalah operasi yang tidak menyenangkan dan
lebih baik tidak menghapus jika memungkinkan. Saat merancang skema data, cobalah untuk mempertimbangkan meminimalkan penghapusan data individual atau menghapus seluruh koleksi. mereka dapat dihapus dengan seluruh koleksi. Menghapus koleksi adalah operasi yang murah, dan menghapus ribuan dokumen individual adalah operasi yang sulit.
Jika Anda masih perlu menghapus banyak dokumen, pastikan untuk
melakukan pembatasan , jika tidak, penghapusan massal dokumen akan memengaruhi latensi pembacaan dan itu akan menjadi tidak menyenangkan. Ini sangat buruk untuk latensi pada tingkat menengah.
Layak untuk membuat semacam "pena" untuk mengaktifkan pelambatan - sangat sulit untuk mengambil level pertama kali. Kami telah melaluinya berkali-kali sehingga pelambatan diperkirakan dari ketiga, keempat kalinya. Awalnya, pertimbangkan kemungkinan pengetatan itu.
Jika Anda menghapus lebih dari 30% dari koleksi besar, maka transfer dokumen langsung ke koleksi tetangga , dan hapus koleksi lama secara keseluruhan. Jelas bahwa ada nuansa, karena beban dialihkan dari yang lama ke koleksi baru, tetapi bergeser jika mungkin.
Cara lain untuk menghapus dokumen adalah indeks
TTL , yang merupakan indeks yang mengindeks bidang yang berisi cap waktu Mongo, yang berisi tanggal dokumen meninggal. Saat ini tiba, MongoDB akan menghapus dokumen ini secara otomatis.
Indeks TTL nyaman, tetapi
tidak ada pelambatan dalam implementasi. MongoDB tidak peduli tentang cara menghapus penghapusan ini. Jika Anda mencoba untuk menghapus sejuta dokumen secara bersamaan, selama beberapa menit Anda akan memiliki sebuah cluster yang tidak dapat dioperasi yang hanya berurusan dengan penghapusan dan tidak lebih. Untuk mencegah hal ini terjadi, tambahkan beberapa
keacakan ,
sebarkan TTL sebanyak logika bisnis Anda dan efek khusus pada latensi memungkinkan. Mengolesi TTL sangat penting jika Anda memiliki alasan logika bisnis alami yang memusatkan penghapusan pada satu titik waktu.
Sharding
Kami mencoba menunda saat ini, tetapi itu telah datang - kami masih harus skala secara horizontal. Untuk MongoDB, ini sangat buruk.
Jika Anda ragu perlu sharding, maka Anda tidak membutuhkannya.
Sharding memperumit kehidupan pengembang dan devops dalam berbagai cara. Dalam sebuah perusahaan, kami menyebutnya pajak sharding. Ketika kami membuang koleksi,
kinerja spesifik dari koleksi berkurang : MongoDB membutuhkan indeks terpisah untuk sharding, dan parameter tambahan harus diteruskan ke permintaan sehingga dapat dieksekusi lebih efisien.
Beberapa hal beling tidak berfungsi dengan baik. Misalnya, adalah ide yang buruk untuk menggunakan kueri dengan
skip
, terutama jika Anda memiliki banyak dokumen. Anda memberi perintah: "Lewati 100.000 dokumen."
MongoDB berpikir seperti ini: "Pertama, kedua, ketiga ... seratus ribu, mari kita melangkah lebih jauh. Dan kami akan mengembalikan ini kepada pengguna. "
Dalam koleksi yang tidak dibagi, MongoDB akan melakukan operasi di suatu tempat di dalam dirinya sendiri. Dalam shard-like - dia benar-benar membaca dan mengirim semua 100.000 dokumen ke proxy sharding - dalam
mongo , yang sudah di sisinya entah bagaimana akan menyaring dan membuang 100.000 yang pertama. Fitur yang tidak menyenangkan untuk diingat.
Kode pasti akan menjadi lebih rumit dengan sharding - Anda harus menyeret kunci sharding ke banyak tempat. Ini tidak selalu nyaman, dan tidak selalu mungkin. Beberapa pertanyaan akan disiarkan atau multicast, yang juga tidak menambah skalabilitas. Datanglah ke pilihan kunci di mana sharding akan lebih akurat.
Dalam koleksi beling, operasi count
rusak . Dia mulai mengembalikan nomor lebih banyak daripada kenyataan - dia bisa berbohong 2 kali. Alasannya terletak pada proses penyeimbangan, ketika dokumen dituangkan dari satu beling ke beling yang lain. Ketika dokumen-dokumen dituangkan ke beling tetangga, tetapi belum dihapus pada yang asli,
count
tetap
count
. Pengembang MongoDB tidak menyebut ini bug - ini adalah fitur seperti itu. Saya tidak tahu apakah mereka akan memperbaikinya atau tidak.
Cluster yang dikocok jauh lebih sulit untuk dikelola . Devops akan berhenti menyapa Anda, karena proses menghapus cadangan menjadi lebih rumit. Ketika beling, kebutuhan untuk otomatisasi infrastruktur berkedip seperti alarm kebakaran - sesuatu yang bisa Anda lakukan tanpa sebelumnya.
Cara kerja sharding di MongoDB
Ada koleksi, kami ingin entah bagaimana menyebarkannya di sekitar pecahan. Untuk melakukan ini,
MongoDB membagi koleksi menjadi potongan menggunakan kunci beling, mencoba untuk membaginya menjadi potongan yang sama di ruang kunci beling. Berikutnya adalah penyeimbang, yang rajin
memetakan potongan-potongan ini sesuai dengan pecahan di cluster . Selain itu, penyeimbang tidak peduli berapa berat potongan-potongan ini dan berapa banyak dokumen di dalamnya, karena penyeimbangan dilakukan sepotong demi sepotong.
Kunci Sharding
Apakah Anda masih memutuskan apa yang harus beling? Nah, pertanyaan pertama adalah bagaimana memilih kunci sharding. Kunci yang baik memiliki beberapa parameter:
kardinalitas tinggi ,
tidak stabil dan
cocok dengan permintaan yang sering .
Pilihan alami dari kunci sharding adalah kunci utama - bidang id. Jika bidang id cocok untuk sharding, maka lebih baik untuk shard langsung di atasnya. Ini adalah pilihan yang sangat baik - ia memiliki kardinalitas yang baik, ia tidak stabil, tetapi seberapa baik ia cocok dengan permintaan yang sering adalah kekhasan bisnis Anda. Bangun situasi Anda.
Saya akan memberikan contoh kunci sharding yang gagal. Saya sudah menyebutkan koleksi terjemahan - terjemahan. Ini memiliki bidang bahasa yang menyimpan bahasa. Sebagai contoh, koleksi mendukung 100 bahasa dan kami shard bahasa. Ini buruk - kardinalitas, jumlah nilai yang mungkin hanya 100 buah, yang kecil. Tapi ini bukan yang terburuk - mungkin kardinalitas sudah cukup untuk tujuan ini. Lebih buruk lagi, segera setelah kami beralih ke bahasa lain, kami segera mengetahui bahwa kami memiliki pengguna yang berbahasa Inggris 3 kali lebih banyak daripada yang lain. Tiga kali lebih banyak permintaan datang ke pecahan malang di mana bahasa Inggris berada daripada gabungan semua yang lain.
Oleh karena itu, harus diingat bahwa kadang-kadang kunci beling secara alami cenderung distribusi beban yang tidak merata.
Menyeimbangkan
Kami datang ke sharding ketika kebutuhan telah matang untuk kami - MongoDB cluster kami berderit, berderak dengan disk, prosesor - dengan segala yang kami bisa. Kemana harus pergi? Tidak ada tempat, dan kami dengan gagah mengocok tumit koleksi. Kami pecahan, peluncuran, dan tiba-tiba mengetahui bahwa
menyeimbangkan tidak gratis .
Penyeimbangan melewati beberapa tahap. Penyeimbang memilih potongan dan pecahan, dari mana dan di mana ia akan ditransfer. Pekerjaan lebih lanjut berjalan dalam dua fase: pertama,
dokumen disalin dari sumber ke target, dan kemudian dokumen yang disalin
dihapus .
Shard kami kelebihan beban, berisi semua koleksi, tetapi bagian pertama dari operasi mudah baginya. Tapi yang kedua - penghapusan - cukup tidak menyenangkan, karena akan membuat beling di bahu dan sudah menderita beban.
Masalahnya diperparah oleh fakta bahwa jika kita menyeimbangkan banyak potongan, misalnya ribuan, maka dengan pengaturan default semua potongan ini pertama kali disalin, dan kemudian sebuah remover masuk dan mulai menghapusnya secara massal. Pada titik ini, prosedur tidak lagi terpengaruh dan Anda hanya perlu menonton dengan sedih apa yang terjadi.
Oleh karena itu, jika Anda mendekati untuk beling cluster yang kelebihan beban, Anda perlu merencanakan, karena
menyeimbangkan membutuhkan waktu. Dianjurkan untuk mengambil waktu ini bukan pada waktu prime time, tetapi pada periode beban rendah. Balancer - suku cadang terputus. Anda dapat mendekati penyeimbang utama dalam mode manual, mematikan penyeimbang di waktu tayang utama, dan menyalakannya ketika beban telah menurun untuk memungkinkan Anda lebih banyak.
Jika kemampuan cloud masih memungkinkan Anda untuk skala secara vertikal, yang terbaik adalah meningkatkan sumber beling terlebih dahulu untuk sedikit mengurangi semua efek khusus ini.
Sharding harus disiapkan dengan hati-hati.HighLoad ++ Siberia 2019 akan hadir di Novosibirsk pada 24 dan 25 Juni. HighLoad ++ Siberia adalah kesempatan bagi pengembang dari Siberia untuk mendengarkan laporan, berbicara tentang topik-topik yang penuh muatan dan terjun ke lingkungan "di mana setiap orang memiliki milik mereka sendiri", tanpa terbang lebih dari tiga ribu kilometer ke Moskow atau St. Petersburg. Dari 80 aplikasi, Komite Program menyetujui 25, dan kami memberi tahu tentang semua perubahan lain dalam program, pengumuman laporan dan berita lainnya di milis kami. Berlangganan untuk tetap mendapat informasi.