Menteleportasi banyak data ke PostgreSQL

Hari ini saya akan membagikan beberapa solusi arsitektur berguna yang muncul selama pengembangan alat kami untuk analisis massal kinerja server PostgeSQL , dan yang membantu kami sekarang "menyesuaikan" pemantauan dan analisis penuh lebih dari seribu host ke dalam perangkat keras yang sama, yang pada awalnya hampir tidak cukup untuk seratus ...





Intro



Izinkan saya mengingatkan Anda tentang beberapa catatan pengantar:



  • kami sedang membangun layanan yang menerima informasi dari log server PostgreSQL
  • mengumpulkan log, kami ingin melakukan sesuatu dengannya (mengurai, menganalisis, meminta informasi tambahan) secara online
  • segala sesuatu yang dikumpulkan dan "dianalisis" harus disimpan di suatu tempat


Mari kita bicara tentang poin terakhir - bagaimana semua ini dapat dikirim ke penyimpanan PostgreSQL . Dalam kasus kami, data ini adalah kelipatan dari aslinya - memuat statistik dalam konteks aplikasi tertentu dan templat rencana, konsumsi sumber daya dan kalkulasi masalah turunan yang akurat untuk satu simpul rencana, kunci pemantauan, dan banyak lagi.

Lebih lengkapnya tentang prinsip-prinsip layanan dapat dilihat di laporan video dan baca di artikel "Optimalisasi massal kueri PostgreSQL" .


dorong vs tarik



Ada dua model utama untuk mendapatkan log atau beberapa metrik yang terus berdatangan:



  • push - ada banyak penerima peer-to-peer di layanan , di server yang dipantau - beberapa agen lokal secara berkala membuang informasi yang terkumpul ke dalam layanan
  • pull - on layanan, setiap proses / utas / coroutine / ... memproses informasi hanya dari satu sumber "sendiri" , penerimaan data yang dimulai dengan sendirinya


Masing-masing model ini memiliki sisi positif dan negatif.



Dorong



Interaksi dimulai oleh node yang diamati:



... bermanfaat jika:



  • Anda memiliki banyak sumber (ratusan ribu)
  • beban pada mereka tidak berbeda jauh di antara mereka sendiri dan tidak melebihi ~ 1rps
  • beberapa pemrosesan yang rumit tidak diperlukan




Contoh: penerima operator OFD menerima cek dari setiap kasir klien.



... menyebabkan masalah:



  • kunci / kebuntuan saat mencoba menulis kamus / analitik / agregat dalam konteks objek pemantauan dari aliran yang berbeda
  • pemanfaatan terburuk cache dari setiap proses / koneksi BL ke database - misalnya, koneksi yang sama ke database harus terlebih dahulu ditulis ke satu tabel atau segmen indeks, dan segera ke yang lain
  • agen khusus diperlukan untuk ditempatkan pada setiap sumber, yang meningkatkan beban padanya
  • overhead yang tinggi dalam interaksi jaringan - header harus "mengikat" pengiriman setiap paket, dan bukan seluruh koneksi ke sumber secara keseluruhan


Tarik



Inisiator adalah host / proses / utas khusus dari kolektor, yang "mengikat" node ke dirinya sendiri dan secara independen mengekstrak data dari "target":



... bermanfaat jika:



  • Anda memiliki sedikit sumber (ratusan ribu)
  • hampir selalu ada beban dari mereka, terkadang mencapai 1Krps
  • membutuhkan pemrosesan yang kompleks dengan segmentasi berdasarkan sumber




Contoh: pemuat / penganalisis perdagangan dalam konteks masing-masing platform perdagangan.



... menyebabkan masalah:



  • membatasi sumber daya untuk memproses satu sumber dengan satu proses (inti CPU), karena tidak dapat "disebarkan" ke dua penerima
  • diperlukan seorang koordinator yang secara dinamis mendistribusikan kembali beban dari sumber ke seluruh proses / utas / sumber daya yang ada


Karena model pemuatan kami untuk pemantauan PostgreSQL dengan jelas condong ke algoritme tarik, dan sumber daya dari satu proses dan inti CPU modern cukup bagi kami untuk satu sumber, kami menetapkannya.



Tarik-tarik log



Komunikasi kita dengan server yang disediakan untuk sangat operasi jaringan dan bekerja banyak dengan string teks slaboformatirovannymi , sehingga sebagai inti kolektor JavaScript pergi sempurna dalam inkarnasi sebagai server Node.js .



Solusi paling sederhana untuk mendapatkan data dari log server adalah dengan "mencerminkan" seluruh file log ke konsol menggunakan perintah linux sederhana tail -F <current.log>. Hanya konsol kami yang tidak sederhana, tetapi virtual - di dalam koneksi aman ke server yang membentang di atas protokol SSH .



Oleh karena itu, duduk di sisi kedua dari koneksi SSH, kolektor menerima salinan lengkap dari semua lalu lintas log sebagai masukan. Dan jika perlu, ia meminta server untuk informasi sistem yang diperpanjang tentang keadaan saat ini.



Mengapa tidak syslog



Ada dua alasan utama:



  1. syslogbekerja pada model dorong, jadi tidak mungkin untuk dengan cepat mengelola beban pemrosesan aliran yang dihasilkan olehnya di titik penerima. Artinya, jika beberapa pasang host tiba-tiba mulai "menuangkan" ribuan rencana permintaan yang lambat, maka sangat sulit untuk memisahkan pemrosesan mereka di node yang berbeda.



    Pemrosesan di sini tidak berarti terlalu banyak penerimaan / penguraian log yang "bodoh", melainkan penguraian rencana dan penghitungan intensitas sumber daya nyata dari masing-masing node .
  2. PostgreSQL, , «» (relation/page/tuple/...).

    «DBA: ».


-



Pada prinsipnya, solusi lain dapat digunakan sebagai DBMS untuk menyimpan data yang diurai dari log, tetapi volume informasi yang masuk 150-200GB / hari tidak menyisakan terlalu banyak ruang untuk bermanuver. Oleh karena itu, kami juga memilih PostgreSQL sebagai tempat penyimpanan.



- PostgreSQL untuk menyimpan log? Sungguh?

- Pertama, ada jauh dari hanya dan tidak begitu banyak log sebagai berbagai representasi analitis . Kedua, "Anda tidak tahu cara memasaknya!" :)






Pengaturan server



Poin ini subjektif dan sangat bergantung pada perangkat keras Anda, tetapi kami telah membuat prinsip-prinsip berikut untuk kami sendiri dalam mengonfigurasi host PostgreSQL untuk perekaman aktif.



Pengaturan sistem file

Faktor paling signifikan yang mempengaruhi kinerja tulis adalah [tidak] pemasangan partisi data yang benar. Kami telah memilih aturan berikut:



  • direktori PGDATA dipasang (dalam kasus ext4) dengan parameternoatime,nodiratime,barrier=0,errors=remount-ro,data=writeback,nobh
  • direktori PGDATA / pg_stat_tmp dipindahkan ketmpfs
  • yang PGDATA / direktori pg_wal yang pindah ke media lain, jika masuk akal


lihat Penalaan Sistem File PostgreSQL



Memilih Penjadwal I / O yang Optimal

Secara default, banyak distribusi telah memilih sebagai penjadwal I / Ocfq , dipertajam untuk penggunaan "desktop", di RedHat dan CentOS - noop. Tapi ternyata lebih bermanfaat buat kita deadline.



lihat PostgreSQL vs. Penjadwal I / O (cfq, noop, dan tenggat waktu)



Mengurangi ukuran cache "kotor"

Parameter ini vm.dirty_background_bytesmenetapkan ukuran cache dalam byte, setelah mencapai sistem yang memulai proses latar belakang untuk membuangnya ke disk. Ada parameter yang serupa, tetapi saling eksklusifvm.dirty_background_ratio - ia menetapkan nilai yang sama sebagai persentase dari ukuran memori total - secara default, ini disetel, dan bukan "... byte".



Pada kebanyakan distribusi adalah 10%, di CentOS adalah 5%. Ini berarti bahwa dengan total memori 16 GB di server, sistem dapat mencoba menulis lebih dari 850 MB ke disk satu kali - akibatnya, beban IOps puncak terjadi.



Kami menguranginya secara eksperimental sampai puncak rekaman mulai mulus. Dari pengalaman, untuk menghindari lonjakan, ukurannya harus kurang dari throughput media maksimum (dalam IOps) dikalikan dengan ukuran halaman memori. Artinya, misalnya, untuk 7K IOps (~ 7000 x 4096) - sekitar 28MB.



lihat Mengonfigurasi Opsi Kernel Linux untuk Pengaturan Optimasi PostgreSQL



di postgresql.conf

Parameter apa yang harus dilihat, diputar untuk mempercepat perekaman. Semuanya di sini murni individual, jadi saya hanya akan memberikan beberapa pemikiran tentang topik ini:



  • shared_buffers - harus dibuat lebih kecil, karena dengan perekaman yang ditargetkan dari data "umum" yang tumpang tindih, proses tidak muncul
  • synchronous_commit = off - Anda selalu dapat menonaktifkan menunggu untuk menulis komit jika Anda mempercayai baterai pengontrol RAID Anda
  • fsync- jika data sama sekali tidak penting, Anda dapat mencoba mematikannya - "dalam batas" Anda bahkan bisa mendapatkan DB dalam memori


Struktur tabel database



Saya telah menerbitkan beberapa artikel tentang pengoptimalan penyimpanan data fisik:





Tapi tentang kunci yang berbeda dalam data - belum ada. Aku akan memberitahumu tentang mereka.



Kunci Asing jahat untuk sistem tulis-berat. Sebenarnya, ini adalah "kruk" yang tidak memungkinkan programmer ceroboh untuk menulis ke database apa yang seharusnya tidak ada di sana.



Banyak pengembang yang terbiasa dengan fakta bahwa entitas bisnis yang terkait secara logis pada tingkat mendeskripsikan tabel database harus ditautkan melalui FK. Tapi bukan ini masalahnya!



Tentu saja, poin ini sangat bergantung pada tujuan yang Anda tetapkan saat menulis data ke database. Jika Anda bukan bank (dan jika Anda juga bank, maka tidak memproses!), Maka kebutuhan FK dalam database tulis-berat dipertanyakan.



"Secara teknis" setiap FK membuat SELECT terpisah saat memasukkan recorddari tabel referensi. Sekarang lihat tabel di mana Anda secara aktif menulis, di mana Anda memiliki 2-3 FK tergantung, dan evaluasi apakah layak untuk tugas spesifik Anda untuk memberikan penurunan kinerja semacam integritas sebanyak 3-4 kali ... Atau apakah koneksi logis berdasarkan nilai cukup? Di sini kami memiliki semua FK - dihapus.



UUID Keys bagus . Karena probabilitas tabrakan UUID yang dihasilkan di berbagai titik tidak terkait sangat kecil, beban ini (dengan membuat beberapa ID pengganti) dapat dengan aman dihapus dari database ke "konsumen". Penggunaan UUID adalah praktik yang baik dalam sistem terdistribusi yang terhubung dan tidak sinkron.

Anda dapat membaca tentang varian lain dari pengidentifikasi unik di PostgreSQL di artikel "PostgreSQL Antipatterns: Unique Identifiers ".


Kunci natural juga bagus , meskipun terdiri dari beberapa bidang. Seseorang harus takut bukan pada kunci komposit, tetapi pada bidang PK pengganti ekstra dan indeks di atasnya dalam tabel yang dimuat, yang dapat dengan mudah Anda lakukan tanpanya.



Pada saat yang sama, tidak ada yang melarang penggabungan pendekatan. Sebagai contoh, kita memiliki UUID pengganti ditugaskan untuk "batch" catatan log berurutan terkait satu transaksi asli (karena tidak ada cukup kunci alam), tapi pasangan digunakan sebagai PK (pack::uuid, recno::int2), di mana recnoadalah "alami" nomor urut dari record dalam batch.



Streaming SALIN "tak berujung"



PostgreSQL, seperti OC, "tidak suka" jika data ditulis dalam batch besar (seperti INSERT1000 baris ). Tetapi COPYjauh lebih toleran terhadap aliran tulis yang seimbang (melalui ). Tapi mereka harus bisa memasak dengan sangat hati-hati.



  1. Karena pada tahap sebelumnya kami menghapus semua FK , sekarang kami dapat menulis informasi tentang dirinya sendiri packdan satu set yang terkait reorddalam urutan yang sewenang-wenang, secara asinkron . Dalam hal ini, hal ini sangat efektif untuk menjaga terus aktif COPYsaluran untuk setiap tabel target .
  2. , , «», ( — COPY-) . , — 100, .
  3. , , . . .



    , , «» , . , .
  4. , node-pg, PostgreSQL Node.js, API — stream.write(data) COPY- true, , false, .





    , , « », COPY .
  5. COPY- LRU «». .




Di sini perlu dicatat keuntungan utama yang kami peroleh dengan skema membaca dan menulis log ini - dalam database kami "fakta" menjadi tersedia untuk analisis hampir online , setelah beberapa detik.



Sempurnakan dengan file



Segalanya tampak baik-baik saja. Dimana "rake" pada skema sebelumnya? Mari kita mulai dengan sederhana ...



Sinkronisasi berlebihan



Salah satu masalah besar dari sistem yang dimuat adalah sinkronisasi berlebihan dari beberapa operasi yang tidak memerlukannya. Terkadang "karena mereka tidak memperhatikan", terkadang "lebih mudah seperti itu," tetapi cepat atau lambat Anda harus menyingkirkannya.



Ini mudah dicapai. Kami telah menyiapkan hampir 1000 server untuk pemantauan, masing-masing diproses oleh utas logika terpisah, dan setiap utas membuang informasi yang terkumpul untuk dikirim ke database dengan frekuensi tertentu, seperti ini:



setInterval(writeDB, interval)


Masalahnya di sini justru terletak pada kenyataan bahwa semua aliran dimulai pada waktu yang hampir bersamaan, jadi momen pengirimannya hampir selalu bertepatan "ke intinya".





Untungnya, ini cukup mudah untuk diperbaiki - dengan menambahkan interval waktu "acak" baik untuk momen awal maupun untuk interval:



setInterval(writeDB, interval * (1 + 0.1 * (Math.random() - 0.5)))






Metode ini memungkinkan Anda untuk "menyebarkan" beban pada rekaman secara statistik, mengubahnya menjadi hampir seragam.



Penskalaan berdasarkan inti CPU



Satu inti prosesor jelas tidak cukup untuk seluruh beban kami, tetapi modul cluster akan membantu kami di sini , yang memungkinkan kami untuk dengan mudah mengelola pembuatan proses anak dan berkomunikasi dengannya melalui IPC.



Sekarang kami memiliki 16 proses anak untuk 16 inti prosesor - dan itu bagus, kami dapat menggunakan seluruh CPU! Tetapi dalam setiap proses kami menulis dalam 16 pelat target , dan ketika beban puncak datang, kami juga membuka saluran COPY tambahan. Artinya, berdasarkan 256+ thread yang terus aktif menulis ... oh! Kekacauan semacam itu tidak berdampak baik pada kinerja disk, dan basis mulai terbakar.



Ini sangat menyedihkan ketika mencoba menulis beberapa kamus umum - misalnya, teks permintaan yang sama yang berasal dari node berbeda - kunci yang tidak perlu, menunggu ...





Mari kita "membalikkan" situasinya - yaitu, biarkan proses anak masih mengumpulkan dan memproses informasi dari sumber mereka, tetapi jangan menulis ke database! Sebagai gantinya, biarkan mereka mengirim pesan melalui IPC ke master, dan dia sudah menulis sesuatu yang seharusnya:





Siapa pun yang segera melihat masalah dalam skema paragraf sebelumnya - bagus sekali. Itu terletak tepat pada saat master juga merupakan proses dengan sumber daya terbatas. Oleh karena itu, pada titik tertentu, kami menemukan bahwa dia sudah mulai terbakar - itu berhenti mengatasi pemindahan semua utas ke database, karena itu juga dibatasi oleh sumber daya dari satu inti CPU . Akibatnya, kami meninggalkan sebagian besar aliran "kamus" yang paling sedikit dimuat untuk ditulis melalui master, dan yang paling banyak dimuat, tetapi tidak memerlukan pemrosesan tambahan, dikembalikan ke pekerja:





Multikolektor



Tetapi bahkan satu node tidak cukup untuk melayani semua beban yang tersedia - inilah saatnya untuk memikirkan tentang penskalaan linier. Solusinya adalah multi- kolektor, self-balancing sesuai dengan beban, dengan koordinator di kepala.





Setiap master membuang beban saat ini dari semua pekerjanya kepadanya, dan sebagai tanggapan menerima rekomendasi tentang pemantauan node mana yang harus ditransfer ke pekerja lain atau bahkan ke pengumpul lain. Akan ada artikel terpisah tentang algoritma penyeimbangan tersebut.



Pooling dan Queue Limiting



Pertanyaan benar berikutnya adalah apa yang harus dilakukan dengan aliran tulis ketika ada beban puncak mendadak .



Bagaimanapun, kita tidak dapat membuka lebih banyak dan lebih banyak koneksi baru ke pangkalan tanpa henti - itu tidak efektif, dan itu tidak akan membantu. Solusi sepele - mari kita batasi sehingga kita tidak memiliki lebih dari 16 utas aktif secara bersamaan untuk setiap tabel target. Tetapi apa yang harus dilakukan dengan data yang kita masih "tidak punya waktu" untuk menulis? ..



Jika "lonjakan" beban ini tepat puncak, yaitu jangka pendek , maka kita dapat menyimpan sementara data dalam antrian di memori kolektor itu sendiri. Segera setelah beberapa saluran ke pangkalan dilepaskan, kami mengambil rekaman dari antrian dan mengirimkannya ke aliran.



Ya, ini memerlukan kolektor untuk memiliki beberapa buffer untuk menyimpan antrian, tetapi itu agak kecil dan segera dirilis:





Prioritas antrian



Pembaca yang penuh perhatian, melihat gambar sebelumnya, sekali lagi bingung, "apa yang akan terjadi jika ingatan benar-benar habis ? .." Sudah ada beberapa pilihan - seseorang harus dikorbankan.



Namun tidak semua catatan yang ingin kami kirimkan ke database "sama berguna". Adalah kepentingan kami untuk menuliskannya sebanyak mungkin, secara kuantitatif. "Prioritas eksponensial" primitif dengan ukuran string tertulis akan membantu kita dalam hal ini:



let priority = Math.trunc(Math.log2(line.length));
queue[priority].push(line);


Karenanya, saat menulis ke saluran, kami selalu mulai mengambil dari antrean "lebih rendah" - hanya saja setiap baris terpisah lebih pendek di sana, dan kami dapat mengirimkannya lebih banyak secara kuantitatif:



let qkeys = Object.keys(queue);
qkeys.sort((x, y) => x.valueOf() - y.valueOf()); // - - !


Mengalahkan penyumbatan



Sekarang mari mundur dua langkah. Pada saat kami memutuskan untuk meninggalkan maksimal 16 utas ke alamat satu tabel. Jika tabel target adalah "streaming", artinya, catatan tidak berkorelasi satu sama lain, semuanya baik-baik saja. Maksimum - kami akan memiliki kunci "fisik" pada level disk.



Tetapi jika ini adalah tabel agregat atau bahkan "kamus", maka ketika kita mencoba untuk menulis baris dengan PK yang sama dari aliran yang berbeda, kita akan menerima menunggu di kunci, atau bahkan kebuntuan. Ini menyedihkan ...



Tapi bagaimanapun, apa yang harus ditulis - kita mendefinisikan diri kita sendiri! Intinya adalah jangan mencoba menulis satu PK dari tempat yang berbeda .



Artinya, ketika melewati antrian, kami segera melihat apakah beberapa utas sudah menulis ke tabel yang sama (kami ingat bahwa semuanya berada di ruang alamat yang sama dari satu proses) dengan PK tersebut. Jika belum, kita mengambilnya sendiri dan menuliskannya ke dalam kamus dalam memori "untuk diri kita sendiri", jika itu sudah milik orang lain, kita taruh di antrian.



Di akhir transaksi, kami cukup "membersihkan" keterikatan "dengan diri kami sendiri" dari kamus.



Sedikit bukti



Pertama, dengan LRU, koneksi "pertama" dan proses PostgreSQL yang melayaninya hampir selalu berjalan sepanjang waktu. Ini berarti OS mengalihkannya di antara inti CPU lebih jarang , meminimalkan waktu henti.





Kedua, jika Anda bekerja dengan proses yang sama di sisi server hampir sepanjang waktu, kemungkinan bahwa dua proses akan aktif pada saat yang sama berkurang tajam - dengan demikian, beban puncak pada CPU secara keseluruhan menurun (area abu-abu pada grafik kedua dari kiri ) dan LA jatuh karena lebih sedikit proses yang menunggu giliran.





Sekian untuk hari ini.



Dan izinkan saya mengingatkan Anda bahwa dengan bantuan menjelaskan.tensor.ru Anda dapat melihat berbagai opsi untuk memvisualisasikan rencana eksekusi kueri, yang akan membantu Anda melihat area masalah secara visual.



All Articles