Cara berhenti melupakan indeks dan mulai memeriksa rencana eksekusi dalam pengujian

cdpv

Beberapa waktu yang lalu, sebuah cerita yang tidak menyenangkan terjadi pada saya, yang berfungsi sebagai pemicu untuk proyek kecil di github dan menghasilkan artikel ini.

Hari biasa, pelepasan normal: semua tugas diperiksa naik turun oleh insinyur QA kami, jadi dengan ketenangan sapi suci kami "berguling" ke panggung. Aplikasi berperilaku baik, dalam log - diam. Kami memutuskan untuk beralih (tahap <-> prod). Kami beralih, lihat perangkat ...

Butuh beberapa menit, penerbangannya stabil. Insinyur QA melakukan tes asap, memperhatikan bahwa aplikasi tersebut entah bagaimana melambat secara tidak wajar. Kami menulis untuk menghangatkan cache.

Beberapa menit berlalu, keluhan pertama berasal dari baris pertama: data diunduh dari pelanggan untuk waktu yang sangat lama, aplikasi melambat, perlu waktu lama untuk merespons, dll. Kami mulai khawatir ... kami melihat log, kami mencari kemungkinan alasan.

Beberapa menit kemudian, surat datang dari admin DB. Mereka menulis bahwa waktu pelaksanaan query ke database (selanjutnya disebut sebagai database) telah menembus semua batas yang mungkin dan cenderung tak terhingga.

Saya membuka pemantauan (saya menggunakan JavaMelody ), saya menemukan permintaan ini. Saya memulai PGAdmin, saya mereproduksi. Sangat panjang. Saya menambahkan "jelaskan", saya melihat rencana eksekusi ... itu, kami lupa tentang indeks.

Mengapa review kode tidak cukup?


Kejadian itu banyak mengajari saya. Ya, saya "memadamkan api" selama satu jam, membuat indeks yang tepat langsung pada prod dalam beberapa cara (jangan lupa tentang opsi CONCURRENTLY):

CREATE INDEX CONCURRENTLY IF NOT EXISTS ix_pets_name ON pets_table (name_column); 

Setuju, ini sama dengan penyebaran dengan downtime. Untuk aplikasi yang sedang saya kerjakan, ini tidak dapat diterima.

Saya membuat kesimpulan dan menambahkan titik tebal khusus ke daftar periksa untuk tinjauan kode: jika saya melihat bahwa selama proses pengembangan salah satu kelas Repositori ditambahkan / diubah - Saya memeriksa migrasi sql untuk keberadaan skrip yang membuat dan mengubah indeks di sana. Jika dia tidak ada di sana, saya menulis pertanyaan kepada penulis: apakah dia yakin bahwa indeks tidak diperlukan di sini?

Kemungkinan indeks tidak diperlukan jika ada sedikit data, tetapi jika kita bekerja dengan tabel di mana jumlah baris dihitung dalam jutaan, kesalahan indeks bisa menjadi fatal dan mengarah pada cerita yang ditetapkan di awal artikel.

Dalam hal ini, saya meminta penulis permintaan tarik (selanjutnya PR) untuk menjadi 100% yakin bahwa permintaan yang ia tulis dalam HQL setidaknya tercakup sebagian oleh indeks (Indeks Scan digunakan). Untuk ini, pengembang:

  1. meluncurkan aplikasi
  2. mencari kueri yang dikonversi (HQL -> SQL) di log
  3. membuka PGAdmin atau alat administrasi database lainnya
  4. menghasilkan dalam database lokal, agar tidak mengganggu siapa pun dengan eksperimen mereka, sejumlah data dapat diterima untuk tes (catatan minimum 10K - 20K)
  5. memenuhi permintaan
  6. meminta rencana eksekusi
  7. hati-hati mempelajarinya dan menarik kesimpulan yang tepat
  8. menambah / memodifikasi indeks, memastikan bahwa rencana eksekusi sesuai dengan itu
  9. berhenti berlangganan dalam PR yang telah dicentang oleh cakupan permintaan
  10. ahli menilai risiko dan tingkat keparahan permintaan, saya dapat memeriksa kembali tindakannya

Banyak tindakan rutin dan faktor manusia, tetapi untuk beberapa waktu saya puas, dan saya hidup dengan ini.

Di perjalanan pulang


Mereka mengatakan itu sangat berguna setidaknya kadang-kadang untuk pergi dari tempat kerja tanpa mendengarkan musik / podcast di sepanjang jalan. Pada saat ini, hanya dengan memikirkan kehidupan, Anda bisa sampai pada kesimpulan dan gagasan yang menarik.

Suatu hari saya berjalan pulang dan memikirkan apa yang terjadi hari itu. Ada beberapa ulasan, saya memeriksa masing-masing dengan daftar periksa dan melakukan serangkaian tindakan yang dijelaskan di atas. Aku sangat lelah waktu itu, pikirku, apa-apaan ini? Apakah tidak mungkin untuk melakukan ini secara otomatis? .. Saya mengambil langkah cepat, ingin cepat "memotong" ide ini.

Pernyataan masalah


Apa yang paling penting bagi pengembang dalam rencana eksekusi?
Tentu saja, pemindaian seq pada sejumlah besar data yang disebabkan oleh kurangnya indeks.

Maka, perlu dilakukan tes bahwa:

  1. Dilakukan pada database dengan konfigurasi yang mirip dengan prod
  2. Mencegah kueri basis data yang dibuat oleh repositori JPA (Hibernate)
  3. Dapatkan Rencana Eksekusi
  4. Rencana Eksekusi Parsit, meletakkannya dalam struktur data yang nyaman untuk pemeriksaan
  5. Menggunakan seperangkat metode Assert yang mudah digunakan, periksa harapan. Misalnya, pemindaian seq itu tidak digunakan.


Itu perlu untuk dengan cepat menguji hipotesis ini dengan membuat prototipe.

Arsitektur Solusi


arsitektur checkinx

Masalah pertama yang harus dipecahkan adalah peluncuran tes pada database nyata yang cocok dengan versi dan pengaturan dengan yang digunakan pada prod.

Terima kasih kepada Docker & TestContainers , mereka memecahkan masalah ini.

SqlInterceptor, ExecutionPlanQuery, ExecutionPlanParse, dan AssertService adalah antarmuka yang saat ini saya terapkan untuk Postgres. Rencana tersebut akan diterapkan untuk database lain. Jika Anda ingin berpartisipasi - selamat datang. Kode ini ditulis dalam Kotlin.

Semua ini bersama-sama saya posting di GitHub dan memanggil checkinx-utils . Anda tidak perlu mengulangi ini, cukup sambungkan ketergantungan ke checkinx dalam maven / gradle dan gunakan konfirmasi yang mudah. Cara melakukan ini, saya akan jelaskan secara lebih rinci di bawah ini.

Deskripsi interaksi komponen CheckInx


ProxyDataSource


Masalah pertama yang harus dipecahkan adalah intersepsi permintaan basis data yang siap dieksekusi. Sudah dengan parameter yang ditetapkan, tanpa pertanyaan, dll.

Untuk melakukan ini, Anda perlu membungkus sumber data nyata dalam Proksi tertentu, yang akan memungkinkan Anda untuk mengintegrasikan ke dalam pipa eksekusi permintaan dan, karenanya, mencegat mereka.

ProxyDataSource semacam itu telah diterapkan oleh banyak orang. Saya menggunakan solusi ttddyy siap pakai , yang memungkinkan saya untuk menginstal Listener saya memotong permintaan yang saya butuhkan.

Saya mengganti sumber DataSource menggunakan kelas DataSourceWrapper (BeanPostProcessor).

SqlInterceptor


Bahkan, metode start ()-nya menyetel Listener di proxyDataSource dan mulai memotong permintaan, menyimpannya dalam daftar pernyataan internal. Metode stop (), masing-masing, menghapus Listener yang diinstal.

ExecutionPlanQuery


Di sini, permintaan awal diubah menjadi permintaan untuk rencana eksekusi. Dalam kasus Postgres, ini merupakan tambahan untuk kata kunci kueri "JELAS".

Lebih lanjut, kueri ini dieksekusi pada database yang sama dari testcontainders dan rencana eksekusi "mentah" (daftar baris) dikembalikan.

ExecutionPlanParser


Tidak nyaman untuk bekerja dengan rencana eksekusi mentah. Oleh karena itu, saya menguraikannya menjadi pohon yang terdiri dari node (PlanNode).

Mari kita menganalisis bidang PlanNode menggunakan contoh ExecutionPlan nyata:

 Index Scan using ix_pets_age on pets  (cost=0.29..8.77 rows=1 width=36) Index Cond: (age < 10) Filter: ((name)::text = 'Jack'::text) 

PropertiContohDeskripsi
mentah: StringPemindaian indeks menggunakan ix_pets_age pada hewan peliharaan (biaya = 0,29..8,77 baris = 1 lebar = 36)string sumber
tabel: String?hewan peliharaan
nama tabel
target: String?ix_pets_agenama indeks
cakupan: String?Pemindaian indekspenutup
liputanLevelSetengahabstraksi pelapis (NOL, SETENGAH, PENUH)
anak-anak: MutableList <PlanNode>-simpul anak
properti: MutableList <Pair <String, String >>kunci : Indeks Cond, nilai : (usia <10);
kunci : Filter, nilai : ((nama) :: text = 'Jack' :: text)
properti
yang lain: MutableList <String>-Semua itu tidak dapat dikenali dalam versi checkinx saat ini

AssertService


Dimungkinkan untuk bekerja secara normal dengan struktur data yang dikembalikan oleh parser. CheckInxAssertService adalah serangkaian pemeriksaan pohon PlanNode yang dijelaskan di atas. Hal ini memungkinkan Anda untuk mengatur lambda Anda sendiri dari cek atau menggunakan yang sudah ditentukan, menurut pendapat saya, yang paling populer. Misalnya, agar kueri Anda tidak memiliki Pemindaian Seq, atau Anda ingin memastikan bahwa indeks tertentu digunakan / tidak digunakan.

Coveragelevel


Sangat penting Enum, saya akan jelaskan secara terpisah:
NilaiDeskripsi
TIDAK ADA
memeriksa apakah target tertentu (indeks) tidak digunakan
Nol
indeks tidak digunakan (Pemindaian Seq)
Setengah
cakupan sebagian dari permintaan dengan indeks (Pemindaian Indeks). Misalnya, pencarian dilakukan berdasarkan indeks, tetapi untuk data yang dihasilkan, ini merujuk pada tabel
LENGKAP
cakupan penuh kueri menurut indeks (Pemindaian Hanya Indeks)
TIDAK DIKENAL
cakupan tidak diketahui. Untuk beberapa alasan, itu tidak mungkin untuk menginstalnya.

Selanjutnya, kita akan melihat beberapa contoh penggunaan.

Contoh Uji Menggunakan CheckInx


Saya melakukan proyek terpisah pada GitHub checkinx-demo , di mana saya menerapkan repositori JPA untuk tabel hewan peliharaan dan tes untuk cakupan pemeriksaan repositori, indeks, dll. Akan berguna untuk melihat di sana sebagai titik awal.

Anda mungkin memiliki tes seperti ini:

 @Test fun testFindByLocation() {  // ARRANGE  val location = "Moscow"  //   ,      10-20.  //   TestNG      @BeforeClass  IntRange(1, 10000).forEach {      val pet = Pet()      pet.id = UUID.randomUUID()      pet.age = it      pet.location = "Saint Petersburg"      pet.name = "Jack-$it"      repository.save(pet)  }  // ACT  //     sqlInterceptor.startInterception()  //    val pets = repository.findByLocation(location)  //    sqlInterceptor.stopInterception()  // ASSERT  //         assertEquals(1, sqlInterceptor.statements.size.toLong())  // ,    ix_pets_location    (Index Scan)  checkInxAssertService.assertCoverage(CoverageLevel.HALF, "ix_pets_location", sqlInterceptor.statements[0])  //        ,      Seq Scan,        checkInxAssertService.assertCoverage(CoverageLevel.HALF, sqlInterceptor.statements[0])  // ...  ,      checkInxAssertService.assertPlan(plan) {          it.coverageLevel.level < CoverageLevel.FULL.level      } } 

Rencana implementasi dapat sebagai berikut:

 Index Scan using ix_pets_location on pets pet0_  (cost=0.29..4.30 rows=1 width=46) Index Cond: ((location)::text = 'Moscow'::text) 

... atau seperti ini jika kita lupa tentang indeks (tes menjadi merah):

 Seq Scan on pets pet0_  (cost=0.00..19.00 rows=4 width=84) Filter: ((location)::text = 'Moscow'::text) 

Dalam proyek saya, saya kebanyakan menggunakan pernyataan paling sederhana, yang mengatakan bahwa tidak ada Pemindaian Seq dalam rencana eksekusi:

 checkInxAssertService.assertCoverage(CoverageLevel.HALF, sqlInterceptor.statements[0]) 

Kehadiran tes semacam itu menunjukkan bahwa saya, setidaknya, mempelajari rencana implementasi.
Ini juga membuat manajemen proyek lebih eksplisit, dan kemampuan mendokumentasikan dan dapat diprediksi kode meningkat.

Mode berpengalaman
Saya sarankan menggunakan CheckInxAssertService, tetapi jika perlu, Anda dapat mem-bypass pohon yang diurai (ExecutionPlanParser) sendiri atau, secara umum, menguraikan rencana eksekusi mentah (hasil menjalankan ExecutionPlanQuery).

 @Test fun testFindByLocation() {  // ARRANGE  val location = "Moscow"  // ACT  //     sqlInterceptor.startInterception()  //    val pets = repository.findByLocation(location)  //    sqlInterceptor.stopInterception()  // ASSERT  //  ""    val executionPlan = executionPlanQuery.execute(sqlInterceptor.statements[0])  //    -   val plan = executionPlanParser.parse(executionPlan)  assertNotNull(plan)  // ...     val rootNode = plan.rootPlanNode  assertEquals("Index Scan", rootNode.coverage)  assertEquals("ix_pets_location", rootNode.target)  assertEquals("pets pet0_", rootNode.table) } 


Koneksi ke proyek


Dalam proyek saya, saya mengalokasikan tes semacam itu ke grup terpisah, menyebutnya Tes Integrasi Intensif.

Menghubungkan dan mulai menggunakan checkinx-utils cukup mudah. Mari kita mulai dengan skrip build.

Hubungkan repositori terlebih dahulu. Suatu hari saya akan mengunggah checkinx ke maven, tetapi sekarang Anda dapat mengunduh artefak hanya dari GitHub melalui jitpack.

 repositories { // ...  maven { url 'https://jitpack.io' } } 

Selanjutnya, tambahkan ketergantungan:

 dependencies { // ...  implementation 'com.github.tinkoffcreditsystems:checkinx-utils:0.2.0' } 

Kami menyelesaikan koneksi dengan menambahkan konfigurasi. Hanya Postgres yang didukung saat ini.

 @Profile("test") @ImportAutoConfiguration(classes = [PostgresConfig::class]) @Configuration open class CheckInxConfig 

Perhatikan profil uji. Kalau tidak, Anda akan menemukan ProxyDataSource di prod Anda.

PostgresConfig menghubungkan beberapa kacang:

  1. DataSourceWrapper
  2. PostgresInterceptor
  3. PostgresExecutionPlanParser
  4. PostgresExecutionPlanQuery
  5. PeriksaInxAssertServiceImpl

Jika Anda memerlukan semacam penyesuaian yang tidak disediakan oleh API saat ini, Anda selalu dapat mengganti salah satu kacang dengan implementasi Anda.

Masalah yang Diketahui


Terkadang DataSourceWrapper gagal mengganti dataSource asli karena proksi Spring CGLIB. Dalam hal ini, bukan DataSource yang datang ke BeanPostProcessor, tetapi ScopedProxyFactoryBean dan ada masalah dengan pengecekan tipe.

Solusi termudah adalah dengan membuat HikariDataSource secara manual untuk pengujian. Maka konfigurasi Anda adalah sebagai berikut:

 @Profile("test") @ImportAutoConfiguration(classes = [PostgresConfig::class]) @Configuration open class CheckInxConfig {  @Primary  @Bean  @ConfigurationProperties("spring.datasource")  open fun dataSource(): DataSource {      return DataSourceBuilder.create()          .type(HikariDataSource::class.<i>java</i>)          .build()  }  @Bean  @ConfigurationProperties("spring.datasource.configuration")  open fun dataSource(properties: DataSourceProperties): HikariDataSource {      return properties.initializeDataSourceBuilder()          .type(HikariDataSource::class.<i>java</i>)          .build()  } } 


Rencana pengembangan


  1. Saya ingin mengerti jika ada orang lain selain saya yang membutuhkan ini? Untuk melakukan ini, buat survei. Saya akan dengan senang hati menjawab dengan jujur.
  2. Lihat apa yang benar-benar Anda butuhkan dan perluas daftar standar metode pengesahan.
  3. Tulis implementasi untuk database lain.
  4. Konstruksi sqlInterceptor.statements [0] tidak terlihat sangat jelas, saya ingin memperbaikinya.

Saya akan senang jika seseorang ingin bergabung dan mendapatkan kredit dengan berlatih di Kotlin.

Kesimpulan


Saya yakin akan ada komentar: tidak mungkin untuk memprediksi bagaimana perencana kueri akan berperilaku pada prod, itu semua tergantung pada statistik yang dikumpulkan .

Memang, seorang perencana. Menggunakan statistik yang dikumpulkan sebelumnya, itu dapat membangun rencana yang berbeda dari yang sedang diuji. Artinya sedikit berbeda.

Tugas perencana adalah memperbaiki, bukan memperburuk, permintaan. Karena itu, tanpa alasan yang jelas, dia tidak akan tiba-tiba menggunakan Seq Scan, tetapi Anda bisa tanpa sadar.

Anda perlu CheckInx sehingga saat menulis tes, jangan lupa untuk mempelajari rencana eksekusi permintaan dan mempertimbangkan kemungkinan membuat indeks, atau sebaliknya, jelas menunjukkan dengan tes bahwa tidak ada indeks yang diperlukan di sini dan Anda puas dengan Seq Scan. Ini akan menghemat pertanyaan yang tidak perlu pada ulasan kode.

Referensi


  1. https://github.com/TinkoffCreditSystems/checkinx-utils
  2. https://github.com/dsemyriazhko/checkinx-demo
  3. https://github.com/ttddyy/datasource-proxy
  4. https://mvnrepository.com/artifact/org.testcontainers/postgresql
  5. https://github.com/javamelody/javamelody/wiki

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


All Articles