Appetites Besar untuk Little Buffers di Node.js

Saya sudah berbicara tentang layanan untuk memonitor permintaan ke PostgreSQL , di mana kami mengimplementasikan pengumpul log server online, yang tugas utamanya adalah secara bersamaan menerima log stream dari sejumlah besar host sekaligus, dengan cepat menguraikannya menjadi beberapa baris , mengelompokkannya ke dalam paket, mengelompokkannya dalam paket sesuai dengan aturan, proses dan penulisan tertentu menghasilkan penyimpanan PostgreSQL .



Dalam kasus kami, kami berbicara tentang beberapa ratus server dan jutaan permintaan dan rencana yang menghasilkan lebih dari 100GB log per hari . Oleh karena itu, sama sekali tidak mengejutkan ketika kami menemukan bahwa bagian terbesar dari sumber daya dihabiskan tepat pada dua operasi ini: menguraikan garis dan menulis ke database.

Kami terjun ke usus profiler dan menemukan beberapa fitur bekerja dengan BufferNode.js, pengetahuan yang dapat sangat menghemat waktu dan sumber daya server Anda.

Beban CPU




Sebagian besar waktu prosesor dihabiskan untuk memproses aliran log masuk, yang dapat dimengerti. Tetapi yang tidak jelas adalah keintensifan sumber daya dari β€œpengirisan” primitif dari aliran masuk blok biner ke dalam baris oleh \r\n: Pengembang yang



penuh perhatian akan segera melihat di sini siklus byte yang tidak begitu efisien melalui buffer yang masuk. Nah, karena garis bisa "sobek" antara blok tetangga, ada juga "lampiran ekor" fungsional yang tersisa dari blok yang diproses sebelumnya.

Mencoba membaca


Tinjauan singkat dari solusi yang tersedia membawa kami ke modul readline reguler persis dengan fungsionalitas yang diperlukan untuk mengiris menjadi beberapa baris:



Setelah diimplementasikan, "mengiris" dari atas profiler masuk lebih dalam:



Tetapi, ternyata, readline memaksa string ke UTF-8 secara internal , yang tidak mungkin lakukan jika entri log (permintaan, rencana, teks kesalahan) memiliki penyandian sumber yang berbeda.

Memang, bahkan pada satu server PostgreSQL, beberapa database dapat aktif secara bersamaan, yang masing-masing menghasilkan output ke log server umum persis dalam pengkodean aslinya. Akibatnya, pemilik basis data pada win-1251 (kadang-kadang lebih nyaman menggunakannya untuk menghemat ruang disk jika UNICODE multibyte "jujur" tidak diperlukan) dapat mengamati rencana mereka dengan nama tabel dan indeks "Rusia" yang kira-kira sama:



Memodifikasi sepeda


Ini masalah ... Tetap saja, Anda harus melakukan pemotongan sendiri, tetapi dengan optimisasi seperti Buffer.indexOf()pemindaian "byte":



Segalanya tampak baik-baik saja, beban dalam loop tes tidak meningkat, nama win1251 telah diperbaiki, kami mulai berperang ... Ta-dam! Penggunaan CPU secara berkala menerobos langit-langit 100% :



Bagaimana? .. Ternyata itu adalah kesalahan Buffer.concatkita yang mana kita "menempelkan ekor" yang tersisa dari blok sebelumnya:



Tapi kita hanya memiliki perekatan ketika sebuah garis melewati sebuah blok , tetapi mereka seharusnya tidak menjadi banyak - sungguh, sungguh? Ya, hampir. Hanya sekarang kadang-kadang "string" dari beberapa ratus 16KB segmen datang :



Terima kasih kepada sesama pengembang yang berhati-hati untuk menghasilkan ini. Itu terjadi "jarang, tetapi akurat", jadi itu tidak mungkin untuk melihat terlebih dahulu di sirkuit uji.

Jelas bahwa menempel beberapa ratus kali ke buffer oleh beberapa megabyte potongan kecil adalah jalur langsung ke jurang realokasi memori dengan konsumsi sumber daya CPU, yang kami amati. Jadi, jangan menempelkannya sampai garis berakhir sepenuhnya. Kami hanya akan menempatkan "potongan-potongan" dalam array sampai tiba waktunya untuk memberikan seluruh baris "keluar":



Sekarang beban telah kembali ke indikator readline.

Konsumsi memori


Banyak orang yang menulis dalam bahasa dengan alokasi memori dinamis menyadari bahwa salah satu "pembunuh kinerja" yang paling tidak menyenangkan adalah aktivitas latar belakang Garbage Collector (GC), yang memindai objek yang dibuat dalam memori dan menghapus objek yang lebih besar. tidak ada yang diperlukan. Masalah ini juga menyusul kami - pada titik tertentu kami mulai memperhatikan bahwa aktivitas GC entah bagaimana terlalu banyak, dan tidak pada tempatnya.



"Liku" tradisional tidak benar-benar membantu ... "Jika semuanya gagal, buang!" Dan kebijaksanaan rakyat tidak mengecewakan - kami melihat awan Penyangga 8360 byte dengan ukuran total 520MB ...



Dan semuanya dihasilkan di dalam CopyBinaryStream - situasinya mulai cerah ...

SALIN ... DARI STDIN DENGAN BINARY


Untuk mengurangi jumlah lalu lintas yang ditransmisikan ke database, kami menggunakan COPY format biner . Bahkan, untuk setiap catatan, Anda perlu mengirim buffer ke aliran, yang terdiri dari "potongan" - jumlah bidang dalam catatan (2 byte) dan kemudian representasi biner dari nilai-nilai setiap kolom (4 byte per tipe ID + data).

Karena deretan tabel seperti itu hampir selalu memiliki panjang variabel "diringkas", segera mengalokasikan buffer dengan panjang tetap bukan merupakan pilihan ; realokasi jika ada kekurangan ukuran akan dengan mudah "memakan" kinerja; Jadi, juga bermanfaat untuk "merekatkan dari potongan-potongan" menggunakan Buffer.concat().

memo


Nah, karena kita memiliki banyak potongan berulang-ulang (misalnya, jumlah bidang dalam catatan dari tabel yang sama) - mari kita ingat saja , dan kemudian ambil yang sudah jadi , dihasilkan sekali pada panggilan pertama. Berdasarkan format SALINAN, ada beberapa pilihan - potongan tipikal panjangnya 1, 2 atau 4 byte:



Dan ... bam, sebuah rake telah tiba!



Ya, setiap kali Anda membuat Buffer, sepotong memori 8KB dialokasikan secara default, sehingga buffer kecil yang dibuat dalam satu baris dapat ditumpuk "berdampingan" di memori yang sudah dialokasikan. Dan alokasi kami berfungsi "sesuai permintaan", dan ternyata sama sekali tidak "dekat" - itulah mengapa masing-masing buffer 1-2-4 byte kami secara fisik menempati header 8KB + - di sinilah tempatnya, 520MB kami!

memo cerdas


Hmm ... Kenapa kita harus menunggu sampai buffer ini dibutuhkan 1/2-byte? Dengan 4-byte adalah masalah yang terpisah, tetapi beberapa opsi yang berbeda ini untuk total 256 + 65536. Jadi biarkan nagenerim baris mereka sekaligus ! Pada saat yang sama, kami memotong kondisi untuk keberadaan setiap pemeriksaan - ini juga akan bekerja lebih cepat, karena inisialisasi dilakukan hanya pada awal proses.



Artinya, selain buffer 1/2-byte, kami segera menginisialisasi nilai yang paling berjalan (lebih rendah 2 byte dan -1) untuk yang 4-byte. Dan - itu membantu, hanya 10MB bukannya 520MB!


All Articles