Sebagai Senior Software Engineer di platform perpesanan perusahaan untuk industri kesehatan, saya bertanggung jawab, termasuk tugas-tugas lain, untuk kinerja aplikasi kami. Kami mengembangkan layanan web standar cantik menggunakan aplikasi Ruby on Rails untuk logika bisnis dan API, React + Redux untuk aplikasi halaman tunggal yang menghadap pengguna, karena database kami menggunakan PostgreSQL. Alasan umum untuk masalah kinerja di tumpukan yang sama adalah pertanyaan berat ke database dan saya ingin menceritakan kisah bagaimana kami menerapkan optimasi yang tidak standar tetapi cukup sederhana untuk meningkatkan kinerja.
Bisnis kami beroperasi di AS, jadi kami harus mematuhi HIPAA dan mengikuti kebijakan keamanan tertentu, audit keamanan adalah sesuatu yang selalu kami persiapkan. Untuk mengurangi risiko dan biaya, kami mengandalkan penyedia cloud khusus untuk menjalankan aplikasi dan database kami, sangat mirip dengan yang dilakukan Heroku. Di satu sisi itu memungkinkan kami untuk fokus membangun platform kami, tetapi di sisi lain itu menambah batasan tambahan untuk infrastruktur kami. Berbicara sesaat lagi - kita tidak dapat meningkatkan secara tak terbatas. Sebagai startup yang sukses, kami menggandakan jumlah pengguna setiap beberapa bulan dan suatu hari pemantauan kami memberi tahu kami bahwa kami melebihi kuota disk IO pada server database. AWS yang mendasarinya mulai melakukan pelambatan yang mengakibatkan penurunan kinerja yang signifikan. Aplikasi Ruby tidak mampu melayani semua lalu lintas masuk karena pekerja Unicorn menghabiskan terlalu banyak waktu menunggu respons database, pelanggan tidak senang.
Solusi standar
Di awal artikel saya menyebutkan frasa "optimisasi non standar" karena semua buah-buahan rendah sudah dipetik:
- kami menghapus semua permintaan N + 1. Ruby gem Bullet adalah alat utama
- semua indeks yang diperlukan pada basis data ditambahkan, semua yang tidak diperlukan dihapus, berkat pg_stat_statements
- beberapa pertanyaan dengan banyak gabungan ditulis ulang untuk efisiensi yang lebih baik
- kami memisahkan kueri untuk mengambil koleksi paginasi dari kueri dekorasi. Misalnya, pada awalnya kami menambahkan penghitung pesan per dialog dengan bergabung dengan tabel tetapi diganti dengan permintaan tambahan untuk menambah hasil. Kueri berikutnya tidak hanya Pindai Indeks dan sangat murah:
SELECT COUNT(1), dialog_id FROM messages WHERE dialog_id IN (1, 2, 3, 4) GROUP BY dialog_id;
- menambahkan beberapa cache. Sebenarnya, ini tidak berfungsi dengan baik karena sebagai aplikasi perpesanan kami memiliki banyak pembaruan
Semua trik ini melakukan pekerjaan besar selama beberapa bulan sampai kami menghadapi lagi masalah kinerja yang sama - lebih banyak pengguna, beban lebih tinggi. Kami sedang mencari sesuatu yang lain.
Solusi canggih
Kami tidak ingin menggunakan artileri berat dan menerapkan denormalisasi dan partisi karena solusi ini membutuhkan pengetahuan mendalam dalam basis data, mengalihkan fokus tim dari menerapkan fitur ke pemeliharaan dan pada akhirnya kami ingin menghindari kompleksitas dalam aplikasi kami. Terakhir, kami menggunakan PostgreSQL 9.3 di mana partisi didasarkan pada pemicu dengan semua biayanya. Prinsip KISS beraksi.
Solusi khusus
Kompres data
Kami memutuskan untuk fokus pada gejala utama - IO disk. Semakin sedikit data yang kami simpan, semakin sedikit kapasitas IO yang kami butuhkan, ini adalah gagasan utama. Kami mulai mencari peluang untuk mengompresi data dan kandidat pertama adalah kolom seperti user_type
disediakan dengan asosiasi polimorfik oleh ActiveRecord. Dalam aplikasi kita menggunakan banyak modul yang membuat kita memiliki string panjang seperti Module::SubModule::ModelName
untuk asosiasi polimorfik. Apa yang kami lakukan - mengonversi semua jenis kolom ini dari varchar ke ENUM. Migrasi Rails terlihat seperti ini:
class AddUserEnumType < ActiveRecord::Migration[5.2] disable_ddl_transaction! def up ActiveRecord::Base.connection.execute <<~SQL CREATE TYPE user_type_enum AS ENUM ( 'Module::Submodule::UserModel1', 'Module::Submodule::UserModel2', 'Module::Submodule::UserModel3' ); SQL add_column :messages, :sender_type_temp, :user_type_enum Message .in_batches(of: 10000) .update_all('sender_type_temp = sender_type::user_type_enum') safety_assured do rename_column :messages, :sender_type, :sender_type_old rename_column :messages, :sender_type_temp, :sender_type end end end
Beberapa catatan tentang migrasi ini untuk orang-orang yang tidak terbiasa dengan Rails:
- disable_ddl_transaction! menonaktifkan migrasi transaksional. Ini sangat berisiko untuk dilakukan tetapi kami ingin menghindari transaksi lama. Harap pastikan bahwa Anda tidak menonaktifkan transaksi pada migrasi tanpa perlu melakukannya.
- Pada langkah pertama kita membuat tipe data ENUM baru pada PostgreSQL. Fitur terbaik pada ENUM adalah ukuran kecil, sangat kecil dibandingkan dengan varchar. ENUM memiliki beberapa kesulitan dengan menambahkan nilai baru tetapi biasanya kami tidak sering menambahkan tipe pengguna baru.
- tambahkan sender_type_temp kolom baru dengan user_type_enum
- mengisi nilai ke kolom baru di_batch untuk menghindari kunci panjang pada pesan tabel
- langkah terakhir menukar kolom lama dengan yang baru. Ini adalah langkah paling berbahaya karena jika kolom sender_type diubah menjadi sender_type_old tetapi sender_type_temp gagal menjadi sender_type kita akan mendapatkan banyak masalah.
- safety_assured berasal dari gem strong_migration yang membantu menghindari kesalahan penulisan migrasi. Mengganti nama kolom bukanlah operasi yang aman, jadi kami harus mengonfirmasi bahwa kami memahami apa yang kami lakukan. Sebenarnya, ada cara yang lebih aman tetapi lebih lama termasuk beberapa penyebaran.
Tidak perlu dikatakan bahwa kami menjalankan semua migrasi serupa selama periode aktivitas terendah dengan pengujian yang tepat.
Kami mengkonversi semua kolom polimorfik menjadi ENUM, menjatuhkan kolom lama setelah beberapa hari pemantauan dan akhirnya menjalankan VACUUM untuk mengurangi fragmentasi. Ini menghemat sekitar 10% dari total ruang disk tetapi
beberapa tabel dengan beberapa kolom dikompres dua kali! Apa yang lebih penting - beberapa tabel mulai di-cache dalam memori (ingat, kita tidak dapat dengan mudah menambahkan lebih banyak RAM) oleh PostgreSQL dan ini secara dramatis mengurangi IO disk yang diperlukan.
Jangan percaya penyedia layanan Anda
Hal lain ditemukan dalam artikel Bagaimana perubahan konfigurasi PostgreSQL tunggal meningkatkan kinerja permintaan lambat sebesar 50x - penyedia PostgreSQL kami membuat konfigurasi otomatis untuk server berdasarkan volume yang diminta dari RAM, Disk dan CPU tetapi dengan alasan apa pun mereka meninggalkan parameter random_page_cost dengan nilai default yang dioptimalkan untuk HDD 4. Mereka menagih kami untuk menjalankan basis data pada SSD tetapi tidak mengonfigurasi PostgreSQL dengan benar. Setelah menghubungi mereka, kami punya rencana eksekusi yang jauh lebih baik:
EXPLAIN ANALYSE SELECT COUNT(*) AS count_dialog_id, dialog_id as dialog_id FROM messages WHERE sender_type = 'Module::Submodule::UserModel1' AND sender_id = 1234 GROUP BY dialog_id; db=# SET random_page_cost = 4; QUERY PLAN
Pindahkan data
Kami memindahkan meja besar ke database lain. Kami harus terus mengaudit setiap perubahan dalam sistem secara hukum dan persyaratan ini diterapkan dengan gem PaperTrail . Pustaka ini membuat tabel di database produksi tempat semua perubahan objek yang diawasi disimpan. Kami menggunakan pustaka multiverse untuk mengintegrasikan instance database lain ke aplikasi Rails kami. Omong-omong - ini akan menjadi fitur standar Rails 6. Ada beberapa konfigurasi:
Jelaskan koneksi dalam file config/database.yml
external_default: &external_default url: "<%= ENV['AUDIT_DATABASE_URL'] %>" external_development: <<: *external_default
Kelas dasar untuk model ActiveRecord dari database lain:
class ExternalRecord < ActiveRecord::Base self.abstract_class = true establish_connection :"external_
Model yang mengimplementasikan versi PaperTrail:
class ExternalVersion < ExternalRecord include PaperTrail::VersionConcern end
Gunakan kasing dalam model yang sedang diaudit:
class Message < ActiveRecord::Base has_paper_trail class_name: "ExternalVersion" end
Ringkasan
Kami akhirnya menambahkan lebih banyak RAM ke instance PostgreSQL kami dan saat ini kami hanya mengonsumsi 10% dari IO disk yang tersedia. Kami bertahan hingga titik ini karena kami menerapkan beberapa trik - data terkompresi dalam basis data produksi kami, konfigurasi yang diperbaiki dan memindahkan data yang tidak relevan. Mungkin, ini tidak akan membantu dalam kasus khusus Anda tetapi saya harap artikel ini dapat memberikan beberapa gagasan tentang pengoptimalan kustom dan sederhana. Tentu saja, jangan lupa memeriksa daftar masalah standar yang tercantum di awal.
PS: Sangat merekomendasikan add-on DBA yang bagus untuk psql .