
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:
- meluncurkan aplikasi
- mencari kueri yang dikonversi (HQL -> SQL) di log
- membuka PGAdmin atau alat administrasi database lainnya
- menghasilkan dalam database lokal, agar tidak mengganggu siapa pun dengan eksperimen mereka, sejumlah data dapat diterima untuk tes (catatan minimum 10K - 20K)
- memenuhi permintaan
- meminta rencana eksekusi
- hati-hati mempelajarinya dan menarik kesimpulan yang tepat
- menambah / memodifikasi indeks, memastikan bahwa rencana eksekusi sesuai dengan itu
- berhenti berlangganan dalam PR yang telah dicentang oleh cakupan permintaan
- 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:
- Dilakukan pada database dengan konfigurasi yang mirip dengan prod
- Mencegah kueri basis data yang dibuat oleh repositori JPA (Hibernate)
- Dapatkan Rencana Eksekusi
- Rencana Eksekusi Parsit, meletakkannya dalam struktur data yang nyaman untuk pemeriksaan
- 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

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)
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:
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() {
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 berpengalamanSaya 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() {
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:
- DataSourceWrapper
- PostgresInterceptor
- PostgresExecutionPlanParser
- PostgresExecutionPlanQuery
- 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
- 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.
- Lihat apa yang benar-benar Anda butuhkan dan perluas daftar standar metode pengesahan.
- Tulis implementasi untuk database lain.
- 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
- https://github.com/TinkoffCreditSystems/checkinx-utils
- https://github.com/dsemyriazhko/checkinx-demo
- https://github.com/ttddyy/datasource-proxy
- https://mvnrepository.com/artifact/org.testcontainers/postgresql
- https://github.com/javamelody/javamelody/wiki