Membuat profil Pergi kode proyek dan menyelesaikan masalah alokasi memori

Mungkin setiap programmer tahu kata-kata Kent Beck: "Jadikan itu berhasil, perbaikilah, buatlah dengan cepat." Pertama Anda perlu membuat program bekerja, maka Anda harus membuatnya bekerja dengan benar, dan hanya kemudian Anda dapat melanjutkan ke optimasi.



Penulis artikel tersebut, yang terjemahannya kami terbitkan, mengatakan bahwa baru-baru ini ia memutuskan untuk mengambil profiling dari Go-project Flipt open-source- nya . Dia ingin menemukan kode dalam proyek yang dapat dioptimalkan dengan mudah dan dengan demikian mempercepat program. Selama pembuatan profil, ia menemukan beberapa masalah tak terduga dalam proyek open source populer yang digunakan Flipt untuk mengatur perutean dan dukungan middleware. Sebagai hasilnya, adalah mungkin untuk mengurangi jumlah memori yang dialokasikan oleh aplikasi selama operasi sebanyak 100 kali. Ini, pada gilirannya, menyebabkan pengurangan jumlah operasi pengumpulan sampah dan meningkatkan kinerja keseluruhan proyek. Begini caranya.

Generasi lalu lintas tinggi


Sebelum saya mulai membuat profil, saya tahu bahwa pertama-tama saya perlu menghasilkan sejumlah besar lalu lintas memasuki aplikasi, yang akan membantu saya melihat beberapa pola perilakunya. Di sini, saya langsung mengalami masalah, karena saya tidak punya apa-apa yang akan menggunakan Flipt dalam produksi dan mendapatkan lalu lintas yang memungkinkan saya untuk mengevaluasi pekerjaan proyek yang sedang dimuat. Akibatnya, saya menemukan alat yang hebat untuk proyek pengujian beban. Ini adalah Vegeta . Para penulis proyek mengatakan bahwa Vegeta adalah alat HTTP universal untuk pengujian beban. Proyek ini lahir dari kebutuhan untuk memuat layanan HTTP dengan sejumlah besar permintaan datang ke layanan dengan frekuensi yang diberikan.

Proyek Vegeta ternyata persis alat yang saya butuhkan, karena memungkinkan saya untuk membuat aliran permintaan ke aplikasi. Dengan permintaan ini, Anda dapat "membasmi" aplikasi sebanyak yang diperlukan untuk mengetahui indikator seperti alokasi / penggunaan memori pada heap, fitur goroutine, waktu yang dihabiskan untuk pengumpulan sampah.

Setelah melakukan beberapa percobaan, saya pergi ke konfigurasi peluncuran Vegeta berikut:

echo 'POST http://localhost:8080/api/v1/evaluate' | vegeta attack -rate 1000 -duration 1m -body evaluate.json 

Perintah ini meluncurkan Vegeta dalam mode attack , mengirimkan permintaan HTTP POST REST API Flipt dengan kecepatan 1000 permintaan per detik (dan ini, memang, merupakan beban serius) selama satu menit. Data JSON yang dikirim oleh Flipt tidak terlalu penting. Mereka diperlukan hanya untuk pembentukan yang benar dari badan permintaan. Permintaan semacam itu diterima oleh server Flipt, yang dapat melakukan prosedur verifikasi permintaan.

Harap dicatat bahwa saya pertama kali memutuskan untuk menguji /evaluate Flipt /evaluate . Faktanya adalah ia menjalankan sebagian besar kode yang mengimplementasikan logika proyek dan melakukan perhitungan server "kompleks". Saya berpikir bahwa menganalisis hasil titik akhir ini akan memberi saya data paling berharga tentang bidang aplikasi yang dapat ditingkatkan.

Pengukuran


Sekarang saya memiliki alat untuk menghasilkan jumlah lalu lintas yang cukup besar, saya perlu menemukan cara untuk mengukur dampak dari lalu lintas ini pada aplikasi yang berjalan. Untungnya, Go memiliki seperangkat alat standar yang cukup baik yang dapat mengukur kinerja program. Ini tentang paket pprof .

Saya tidak akan membahas detail penggunaan pprof. Saya kira saya tidak akan melakukannya lebih baik daripada Julia Evans, yang menulis artikel yang luar biasa ini tentang membuat profil program Go dengan pprof (jika Anda belum membacanya, saya sarankan Anda memeriksanya).

Karena router HTTP di Flipt diimplementasikan menggunakan go-chi / chi , tidak sulit bagi saya untuk mengaktifkan pprof menggunakan perantara perantara yang sesuai Chi.

Jadi, di satu jendela Flipt bekerja untuk saya, dan Vegeta, yang mengisi Flipt dengan permintaan, bekerja di jendela lain. Saya meluncurkan jendela terminal ketiga untuk mengumpulkan dan memeriksa data profil tumpukan:

 pprof -http=localhost:9090 localhost:8080/debug/pprof/heap 

Ini menggunakan alat pprof Google, yang dapat memvisualisasikan data profil langsung di browser.

Pertama saya memeriksa inuse_space dan inuse_space untuk memahami apa yang terjadi pada heap. Namun, saya tidak dapat menemukan sesuatu yang luar biasa. Tetapi ketika saya memutuskan untuk melihat alloc_space dan alloc_space , sesuatu mengingatkan saya.


Analisis hasil pembuatan profil ( asli )

Ada perasaan bahwa sesuatu yang disebut flate.NewWriter mengalokasikan 1.970 MB memori selama satu menit. Dan ini, omong-omong, lebih dari 19 gigabyte! Di sini, jelas, sesuatu yang aneh sedang terjadi. Tapi apa sebenarnya? Jika Anda melihat dari dekat diagram asli di atas, ternyata flate.NewWriter dipanggil dari gzip.(*Writer).Write flate.NewWriter , yang, pada gilirannya, dipanggil dari middleware.(*compressResponseWriter).Write . Saya segera menyadari bahwa apa yang terjadi tidak ada hubungannya dengan kode Flipt. Masalahnya ada di suatu tempat dalam kode Chi middleware yang digunakan untuk mengompres tanggapan dari API.

 //   r.Use(middleware.Compress(gzip.DefaultCompression)) 

Saya berkomentar di atas dan menjalankan tes lagi. Seperti yang diharapkan, sejumlah besar operasi alokasi memori telah hilang.

Sebelum saya mulai mencari solusi untuk masalah ini, saya ingin melihat operasi alokasi memori ini dari sisi lain dan memahami bagaimana mereka mempengaruhi kinerja. Secara khusus, saya tertarik dengan dampaknya pada waktu yang dibutuhkan program untuk mengumpulkan sampah. Saya ingat bahwa Go masih memiliki alat pelacak yang memungkinkan Anda untuk menganalisis program selama pelaksanaannya dan mengumpulkan informasi tentang mereka selama periode waktu tertentu. Data yang dikumpulkan oleh trace meliputi indikator penting seperti tumpukan, jumlah goroutine yang dieksekusi, informasi tentang permintaan jaringan dan sistem, dan, yang sangat berharga bagi saya, informasi tentang waktu yang dihabiskan di pengumpul sampah.

Untuk secara efektif mengumpulkan informasi tentang program yang sedang berjalan, saya perlu mengurangi jumlah permintaan per detik yang dikirim ke aplikasi menggunakan Vegeta, karena server secara teratur memberi saya socket: too many open files kesalahan socket: too many open files . Saya berasumsi bahwa ini karena ulimit diset terlalu rendah pada komputer saya, tetapi saya tidak ingin membahasnya saat itu.

Jadi, saya memulai kembali Vegeta dengan perintah ini:

 echo 'POST http://localhost:8080/api/v1/evaluate' | vegeta attack -rate 100 -duration 2m -body evaluate.json 

Akibatnya, jika kita membandingkan ini dengan skenario sebelumnya, hanya sepersepuluh dari permintaan dikirim ke server, tetapi ini dilakukan untuk periode waktu yang lebih lama. Ini memungkinkan saya untuk mengumpulkan data berkualitas tinggi pada pekerjaan program.

Di jendela terminal lain, saya menjalankan perintah ini:

 wget 'http://localhost:8080/debug/pprof/trace?seconds=60' -O profile/trace 

Sebagai hasilnya, saya memiliki file dengan jejak data yang dikumpulkan dalam 60 detik. Anda dapat memeriksa file ini menggunakan perintah berikut:

 go tool trace profile/trace 

Eksekusi perintah ini mengarah pada penemuan informasi yang dikumpulkan di browser. Mereka disajikan dalam bentuk grafik yang nyaman untuk belajar.

Detail tentang go tool trace dapat ditemukan di artikel bagus ini.


Hasil penelusuran flipt. Grafik gigi pengalokasian memori pada heap terlihat jelas ( asli )

Pada grafik ini, mudah untuk melihat bahwa jumlah memori yang dialokasikan pada heap cenderung tumbuh cukup cepat. Dalam hal ini, setelah pertumbuhan harus turun tajam. Tempat di mana memori yang dialokasikan jatuh adalah operasi pengumpulan sampah. Di sini Anda dapat melihat kolom biru yang diucapkan di area GC, mewakili waktu yang dihabiskan untuk pengumpulan sampah.

Sekarang saya telah mengumpulkan semua bukti "kejahatan" yang saya butuhkan dan dapat mulai mencari solusi untuk masalah alokasi memori.

Pemecahan masalah


Untuk mengetahui alasan mengapa memanggil flate.NewWriter menyebabkan begitu banyak operasi alokasi memori, saya perlu melihat kode sumber Chi . Untuk mengetahui versi Chi yang saya gunakan, saya menjalankan perintah berikut:

 go list -m all | grep chi github.com/go-chi/chi v3.3.4+incompatible 

Setelah mencapai kode sumber chi / middleware / compress.go @ v3.3.4 , saya dapat menemukan metode berikut:

 func encoderDeflate(w http.ResponseWriter, level int) io.Writer {    dw, err := flate.NewWriter(w, level)    if err != nil {        return nil    }    return dw } 

Dalam penelitian lebih lanjut, saya menemukan bahwa metode flate.NewWriter , melalui perantara perantara, dipanggil untuk setiap respons. Ini terkait dengan sejumlah besar operasi alokasi memori yang saya lihat sebelumnya, memuat API dengan seribu permintaan per detik.

Saya tidak ingin menolak untuk menekan respons API atau mencari router HTTP baru dan pustaka dukungan middleware baru. Karena itu, saya pertama-tama memutuskan untuk mencari tahu apakah mungkin untuk mengatasi masalah saya hanya dengan memperbarui Chi.

Saya berlari go get -u -v "github.com/go-chi/chi" , ditingkatkan menjadi Chi 4.0.2, tetapi kode middleware untuk kompresi data tampak, seperti yang terlihat bagi saya, sama seperti sebelumnya. Ketika saya menjalankan tes lagi, masalahnya tidak hilang.

Sebelum mengakhiri masalah ini, saya memutuskan untuk mencari masalah atau pesan PR dalam repositori Chi yang menyebutkan sesuatu seperti "kompresi middleware". Saya menemukan satu PR dengan tajuk berikut: "Menulis ulang pustaka kompresi middleware". Penulis PR ini mengatakan sebagai berikut: "Selain itu, sync.Pool digunakan untuk encoders, yang memiliki metode Reset (io.Writer), yang mengurangi beban memori."

Ini dia! Untungnya, PR ini ditambahkan ke cabang master , tetapi karena tidak ada rilis Chi baru yang dibuat, saya perlu memperbarui seperti ini:

 go get -u -v "github.com/go-chi/chi@master" 

Pembaruan ini, yang sangat menyenangkan bagi saya, sangat kompatibel, penggunaannya tidak memerlukan perubahan dalam kode aplikasi saya.

Hasil


Saya menjalankan tes beban dan membuat profil lagi. Ini memungkinkan saya untuk memverifikasi bahwa pembaruan Chi menyelesaikan masalah.


Sekarang flate.NewWriter menggunakan seperseratus dari jumlah memori yang dialokasikan sebelumnya ( asli )

Melihat lagi pada hasil penelusuran, saya melihat bahwa ukuran tumpukan sekarang tumbuh jauh lebih lambat. Selain itu, waktu yang dibutuhkan untuk pengumpulan sampah menurun.


Selamat tinggal - "melihat" ( asli )

Setelah beberapa waktu, saya memperbarui repositori Flipt, lebih percaya diri daripada sebelumnya bahwa proyek saya akan dapat mengatasi dengan cukup beban yang tinggi.

Ringkasan


Berikut adalah kesimpulan yang saya buat setelah saya berhasil menemukan dan memperbaiki masalah di atas:

  1. Anda tidak boleh mengandalkan asumsi bahwa perpustakaan sumber terbuka (bahkan yang populer) telah dioptimalkan, atau bahwa mereka tidak memiliki masalah yang jelas.
  2. Masalah yang tidak bersalah dapat menyebabkan konsekuensi serius, manifestasi dari "efek domino", terutama di bawah beban berat.
  3. Jika memungkinkan, Anda harus menggunakan sync.Pool .
  4. Berguna untuk tetap menggunakan alat untuk menguji proyek yang sedang dimuat dan untuk membuat profil mereka.
  5. Go Toolkit dan Open Source - Hebat!

Pembaca yang budiman! Bagaimana Anda meneliti kinerja proyek Go Anda?


Source: https://habr.com/ru/post/id477282/


All Articles