Bagaimana kami menemukan proses pengembangan, debugging, dan pengiriman perubahan database pada tahun 2020

Saat ini tahun 2020 di halaman dan Anda sudah terbiasa mendengar dengan kebisingan latar belakang: "Kubernetes adalah jawabannya!", "Layanan mikro!", "Layanan mesh!", "Kebijakan Sesuriti!" Semua orang di sekitar berlari menuju masa depan yang cerah.



Perusahaan kami memiliki pendekatan yang lebih konservatif dalam hal database daripada aplikasi. Basis data berputar tidak di Kubernetes, tetapi di perangkat keras atau di mesin virtual. Kami memiliki proses mapan untuk perubahan pada database pemrosesan pembayaran, yang mencakup banyak pemeriksaan otomatis, tinjauan besar, dan rilis dengan partisipasi DBA. Jumlah pemeriksaan dan orang yang terlibat dalam kasus ini berdampak negatif pada waktu ke pasar. Di sisi lain, ini di-debug dan memungkinkan Anda untuk membuat perubahan pada produksi dengan andal, meminimalkan kemungkinan merusak sesuatu. Dan jika ada yang rusak, maka orang yang tepat sudah termasuk dalam proses perbaikan. Pendekatan ini membuat pekerjaan layanan utama perusahaan lebih stabil.



Kami memulai sebagian besar database relasional baru untuk layanan mikro di PostgreSQL. Proses fine-tuned untuk Oracle, meski kuat, membawa serta kerumitan yang tidak perlu untuk database kecil. Tidak ada yang ingin menyeret proses yang sulit dari masa lalu ke masa depan yang cerah. Tidak ada yang mulai mengerjakan proses untuk masa depan yang cerah sebelumnya. Akibatnya, kami kekurangan standar dan raznozhopitsu. Jika Anda ingin tahu masalah apa yang ditimbulkan dan bagaimana kami menyelesaikannya, selamat datang di cat.











Masalah yang Kami Selesaikan



Tidak ada standar seragam untuk pembuatan versi



Dalam kasus terbaik, ini adalah file DDL SQL yang terletak di suatu tempat di direktori db di repositori dengan layanan mikro. Sangat buruk jika ini hanya status database saat ini, berbeda pada pengujian dan produksi, dan tidak ada skrip referensi untuk skema database.



Selama proses debug, kami menghancurkan basis pengujian



β€œSaya akan mengguncang database pengujian sedikit sekarang, jangan khawatir di sana” - dan pergi untuk men-debug kode perubahan skema yang baru ditulis pada database pengujian. Terkadang butuh waktu lama, dan selama ini rangkaian tes tidak berfungsi.



Pada saat yang sama, sirkuit pengujian dapat rusak di bagian tempat layanan mikro lain berinteraksi dengan layanan mikro, yang basisnya telah dirusak oleh pengembang.



Metode DAO tidak tercakup dalam pengujian, tidak divalidasi di CI



Saat mengembangkan dan men-debug, metode DAO dipanggil dengan menarik tuas luar beberapa lapisan di atas. Ini mengekspos seluruh skenario logika bisnis, bukan interaksi spesifik antara layanan mikro dan database.



Tidak ada jaminan bahwa tidak ada yang akan berantakan di masa depan. Kualitas dan pemeliharaan layanan mikro menderita.



Non-isomorfisme media



Jika loop perubahan dikirimkan secara berbeda untuk pengujian dan produksi, Anda tidak dapat memastikan bahwa ini akan bekerja dengan cara yang sama. Terutama ketika pengembangan dan debugging benar-benar dilakukan dalam pengujian.



Objek di tes dapat dibuat di bawah akun pengembang atau aplikasi. Hibah diserahkan secara acak, biasanya memberikan semua hak istimewa. Hibah diberikan untuk aplikasi dengan prinsip "Saya melihat kesalahan dalam log - Saya memberikan hibah". Hibah sering kali dilupakan saat dirilis. Kadang-kadang, setelah rilis, pengujian asap tidak mencakup semua fungsi baru dan kurangnya dana tidak langsung muncul.



Proses berat dan rapuh untuk memasuki produksi



Proses produksi dilakukan secara manual, tetapi dengan analogi dengan proses untuk Oracle, melalui persetujuan DBA, manajer rilis, dan dilanjutkan oleh insinyur rilis.



Ini memperlambat rilis. Dan jika terjadi masalah, ini meningkatkan waktu henti, mempersulit akses pengembang ke database. Skrip exec.sql dan rollback.sql sering kali tidak diuji pada pengujian, karena tidak ada standar patchsetting untuk non-Oracle, dan pengujian tersebut berjalan terus.



Oleh karena itu, kebetulan pengembang menggulung perubahan ke layanan non-kritis tanpa proses ini sama sekali.



Bagaimana Anda bisa berbuat baik



Debugging pada DB lokal di kontainer buruh pelabuhan



Untuk beberapa, semua solusi teknis yang dijelaskan dalam artikel mungkin tampak jelas. Tapi entah mengapa, dari tahun ke tahun saya melihat orang-orang yang antusias menginjak penggaruk yang sama.



Anda tidak pergi ke server pengujian melalui ssh untuk menulis dan men-debug kode aplikasi, bukan? Saya merasa tidak masuk akal untuk mengembangkan dan men-debug kode database pada Instans DB uji. Ada pengecualian, kebetulan sangat sulit untuk meningkatkan database secara lokal. Tetapi biasanya, jika kita berbicara tentang sesuatu yang ringan dan non-warisan, maka tidak sulit untuk meningkatkan basis secara lokal dan menggulirkan semua migrasi secara konsisten. Sebagai gantinya, Anda akan mendapatkan instans stabil di pihak Anda, yang tidak akan dihambat oleh pengembang lain, yang tidak akan Anda akses dan Anda miliki hak yang diperlukan untuk pengembangan.



Berikut adalah contoh betapa mudahnya memunculkan database lokal:



Mari kita tulis Dockerfile dua baris:



FROM postgres:12.3
ADD init.sql /docker-entrypoint-initdb.d/


Di init.sql kita membuat database "bersih", yang kita harapkan akan diperoleh baik pada pengujian maupun dalam produksi. Ini harus berisi:



  • Pemilik skema dan skema itu sendiri.
  • Pengguna aplikasi dengan izin untuk menggunakan skema.
  • EXTENSION yang dibutuhkan


Contoh init.sql
create role my_awesome_service
with login password *** NOSUPERUSER inherit CREATEDB CREATEROLE NOREPLICATION;
create tablespace my_awesome_service owner my_awesome_service location '/u01/postgres/my_awesome_service_data';
create schema my_awesome_service authorization my_awesome_service;
grant all on schema my_awesome_service to my_awesome_service;
grant usage on schema my_awesome_service to my_awesome_service;
alter role my_awesome_service set search_path to my_awesome_service,pg_catalog, public;

create user my_awesome_service_app with LOGIN password *** NOSUPERUSER inherit NOREPLICATION;
grant usage on schema my_awesome_service to my_awesome_service_app;

create extension if not exists "uuid-ossp";




Untuk kenyamanan, Anda dapat menambahkan tugas db ke Makefile, yang akan memulai (kembali) kontainer dengan basis dan menonjolkan port untuk koneksi:



db:
    docker container rm -f my_awesome_service_db || true
    docker build -t my_awesome_service_db docker/db/.
    docker run -d --name my_awesome_service_db -p 5433:5432 my_awesome_service_db


Membuat versi perubahan dengan standar industri sesuatu



Ini juga terlihat jelas: Anda perlu menulis migrasi dan menyimpannya di sistem kontrol versi. Tetapi sangat sering saya melihat skrip sql "telanjang", tanpa ikatan apa pun. Dan ini berarti bahwa tidak ada kontrol atas rollback dan rollback, oleh siapa, apa, dan kapan dipompa. Bahkan tidak ada jaminan bahwa skrip SQL Anda dapat dijalankan pada database pengujian dan produksi, karena strukturnya mungkin telah berubah.



Secara umum, Anda membutuhkan kendali. Sistem migrasi hanyalah tentang kontrol.

Kami tidak akan membandingkan berbagai sistem versi skema database. FlyWay vs Liquibase bukan topik artikel ini. Kami memilih Liquibase.



Kami versi:



  • DDL-struktur objek database (buat tabel).
  • Konten DML dari tabel pemeta (sisipkan, perbarui).
  • Hibah DCL untuk Aplikasi UZ (pilih hibah, sisipkan pada ...).


Saat meluncurkan dan men-debug layanan mikro pada database lokal, pengembang akan dihadapkan pada kebutuhan untuk menangani hibah. Satu-satunya cara legal untuk itu adalah dengan menambahkan skrip DCL ke set perubahan. Ini memastikan bahwa hibah akan dijual.



Contoh patchset Sql
0_ddl.sql:

create table my_awesome_service.ref_customer_type
(
    customer_type_code    	varchar not null,
    customer_type_description varchar not null,
    constraint ref_customer_type_pk primary key (customer_type_code)
);
 
alter table my_awesome_service.ref_customer_type
    add constraint customer_type_code_ck check ( (customer_type_code)::text = upper((customer_type_code)::text) );


1_dcl.sql:



grant select on all tables in schema my_awesome_service to ru_svc_qw_my_awesome_service_app;
grant insert, update on my_awesome_service.some_entity to ru_svc_qw_my_awesome_service_app;


2_dml_refs.sql:



insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('INDIVIDUAL', '. ');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('LEGAL_ENTITY', '. ');
insert into my_awesome_service.ref_customer_type (customer_type_code, customer_type_description)
values ('FOREIGN_AGENCY', ' . ');


Fixtures. dev

3_dml_dev.sql:



insert into my_awesome_service.some_entity_state (state_type_code, state_data, some_entity_id)
values ('BINDING_IN_PROGRESS', '{}', 1);


rollback.sql:



drop table my_awesome_service.ref_customer_type;




Contoh Changeset.yaml
databaseChangeLog:
 - changeSet:
     id: 1
     author: "mr.awesome"
     changes:
       - sqlFile:
           path: db/changesets/001_init/0_ddl.sql
       - sqlFile:
           path: db/changesets/001_init/1_dcl.sql
       - sqlFile:
           path: db/changesets/001_init/2_dml_refs.sql
     rollback:
       sqlFile:
         path: db/changesets/001_init/rollback.sql
 - changeSet:
     id: 2
     author: "mr.awesome"
     context: dev
     changes:
       - sqlFile:
           path: db/changesets/001_init/3_dml_dev.sql




Liquibase membuat tabel databasechangelog di database, di mana ia mencatat perubahan-perubahan yang dipompa.

Secara otomatis menghitung berapa banyak perubahan yang Anda perlukan untuk menggulung ke database.



Ada maven dan plugin gradle dengan kemampuan untuk menghasilkan skrip dari beberapa set perubahan yang perlu dimasukkan ke dalam database.



Integrasi sistem migrasi database ke dalam fase peluncuran aplikasi



Ini bisa berupa adaptor apa pun dari sistem kontrol migrasi dan kerangka kerja tempat aplikasi Anda dibuat. Dengan banyak kerangka kerja, itu dibundel dengan ORM. Misalnya Ruby-On-Rails, Yii2, Nest.JS.



Mekanisme ini diperlukan untuk menggulung migrasi saat konteks aplikasi dimulai.

Misalnya:



  1. Pada database pengujian, patchsets 001, 002, 003.
  2. Pogromist mengembangkan patchsets 004, 005 dan tidak menerapkan aplikasi untuk pengujian.
  3. Terapkan ke pengujian. Patchsets 004, 005 sedang diluncurkan.


Jika mereka tidak menggulung, aplikasi tidak dimulai. Pembaruan berkelanjutan tidak mematikan pod lama.



Tumpukan kami adalah JVM + Spring dan kami tidak menggunakan ORM. Oleh karena itu, kami membutuhkan integrasi Spring-Liquibase .



Kami memiliki persyaratan keamanan penting di perusahaan kami: pengguna aplikasi harus memiliki serangkaian hibah terbatas dan tentunya tidak boleh memiliki akses level pemilik skema. Dengan Spring-Liquibase, Anda dapat menggulung migrasi atas nama pengguna pemilik skema. Dalam kasus ini, kumpulan koneksi tingkat aplikasi tidak memiliki akses ke Liquibase DataSource. Oleh karena itu, aplikasi tidak akan mendapatkan akses dari pengguna pemilik skema.



Contoh application-testing.yaml
spring:
  liquibase:
    enabled: true
    database-change-log-lock-table: "databasechangeloglock"
    database-change-log-table: "databasechangelog"
    user: ${secret.liquibase.user:}
    password: ${secret.liquibase.password:}
    url: "jdbc:postgresql://my.test.db:5432/my_awesome_service?currentSchema=my_awesome_service"




Tes DAO dalam tahap CI memverifikasi



Perusahaan kami memiliki tahap CI - verifikasi. Pada tahap ini, perubahan diperiksa kesesuaiannya dengan standar kualitas internal. Untuk layanan mikro, ini biasanya berupa proses linter untuk memeriksa gaya kode dan bug, pengujian unit, dan peluncuran aplikasi dengan pengangkat konteks. Sekarang, pada tahap verifikasi, Anda dapat memeriksa migrasi database dan interaksi lapisan DAO aplikasi dengan database.



Menaikkan container dengan database dan rolling patchsets akan meningkatkan waktu mulai konteks Spring sebesar 1,5-10 detik, bergantung pada kekuatan mesin yang bekerja dan jumlah patchset.



Ini sebenarnya bukan pengujian unit, ini adalah pengujian untuk mengintegrasikan lapisan DAO aplikasi dengan database.

Dengan menyebut database sebagai bagian dari layanan mikro, kami mengatakan bahwa itu menguji integrasi dua bagian dari satu layanan mikro. Tidak ada ketergantungan eksternal. Oleh karena itu, pengujian ini stabil dan dapat dijalankan selama fase verifikasi. Mereka memperbaiki kontrak layanan mikro dan database, memberikan jaminan untuk perbaikan di masa mendatang.



Ini juga cara praktis untuk men-debug DAO. Alih-alih memanggil RestController, mensimulasikan perilaku pengguna dalam beberapa skenario bisnis, kami segera memanggil DAO dengan argumen yang diperlukan.



Contoh tes DAO
@Test
@Transactional
@Rollback
fun `create cheque positive flow`() {
      jdbcTemplate.update(
       "insert into my_awesome_service.some_entity(inn, registration_source_code)" +
               "values (:inn, 'QIWICOM') returning some_entity_id",
       MapSqlParameterSource().addValue("inn", "526317984689")
   )
   val insertedCheque = chequeDao.addCheque(cheque)
   val resultCheque = jdbcTemplate.queryForObject(
       "select cheque_id from my_awesome_service.cheque " +
               "order by cheque_id desc limit 1", MapSqlParameterSource(), Long::class.java
   )
   Assert.assertTrue(insertedCheque.isRight())
   Assert.assertEquals(insertedCheque, Right(resultCheque))
}




Ada dua tugas terkait untuk menjalankan pengujian ini di pipeline verifikasi:



  1. Agen build berpotensi sibuk dengan port PostgreSQL standar 5432 atau yang statis. Anda tidak pernah tahu, seseorang tidak mengeluarkan wadah dengan pangkalan setelah tes selesai.
  2. Dari sini, tugas kedua: Anda harus memadamkan kontainer setelah pengujian selesai.


Pustaka TestContainers menyelesaikan dua tugas ini . Ini menggunakan gambar buruh pelabuhan yang ada untuk memunculkan wadah database dalam status init.sql.



Contoh penggunaan TestContainers
@TestConfiguration
public class DatabaseConfiguration {

   @Bean
   GenericContainer postgreSQLContainer() {
       GenericContainer container = new GenericContainer("my_awesome_service_db")
               .withExposedPorts(5432);

       container.start();
       return container;
   }

   @Bean
   @Primary
   public DataSource onlineDbPoolDataSource(GenericContainer postgreSQLContainer) {
       return DataSourceBuilder.create()
               .driverClassName("org.postgresql.Driver")
               .url("jdbc:postgresql://localhost:"
                       + postgreSQLContainer.getMappedPort(5432)
                       + "/postgres")
               .username("my_awesome_service_app")
               .password("my_awesome_service_app_pwd")
               .build();
   }
    
   @Bean
   @LiquibaseDataSource
   public DataSource liquibaseDataSource(GenericContainer postgreSQLContainer) {
       return DataSourceBuilder.create()
               .driverClassName("org.postgresql.Driver")
               .url("jdbc:postgresql://localhost:"
                       + postgreSQLContainer.getMappedPort(5432)
                       + "/postgres")
               .username("my_awesome_service")
               .password("my_awesome_service_app_pwd")
               .build();
   }




Dengan pengembangan dan debugging sudah berhasil. Sekarang kita perlu mengirimkan perubahan pada skema database ke produksi.



Kubernetes adalah jawabannya! Apa pertanyaanmu tadi?



Jadi, Anda perlu mengotomatiskan beberapa proses CI / CD. Kami memiliki pendekatan tim-kota yang terbukti. Tampaknya, di mana alasan artikel lain?



Dan ada alasannya. Selain pendekatan mencoba-dan-benar, ada juga masalah membosankan dari perusahaan besar.



  • Tidak ada cukup pembangun kota tim untuk semua orang.
  • Lisensi membutuhkan uang.
  • Pengaturan mesin virtual agen pembuat dilakukan dengan cara lama, melalui repositori dengan konfigurasi dan boneka.
  • Akses dari pembangun ke jaringan target harus digergaji dengan cara lama.
  • Login dan kata sandi untuk menggulirkan perubahan ke database juga disimpan dengan cara lama.


Dan dalam semua "cara lama" ini masalahnya - semua orang berlari ke masa depan yang cerah, dan dukungan dari Legacy ... Anda tahu. Berhasil dan oke. Tidak berhasil - kita akan membahasnya nanti. Suatu hari nanti. Tidak hari ini.



Katakanlah Anda sudah memiliki satu kaki setinggi lutut di masa depan yang cerah dan Anda sudah memiliki infrastruktur Kubernetes. Bahkan ada peluang untuk menghasilkan layanan mikro lain, yang akan segera dimulai dalam infrastruktur ini, mengambil konfigurasi dan rahasia yang diperlukan, memiliki akses yang diperlukan, dan mendaftar dengan infrastruktur mesh layanan. Dan semua kebahagiaan ini bisa didapatkan oleh developer biasa, tanpa melibatkan orang dengan role * OPS. Kami ingat bahwa di Kubernetes ada jenis beban kerja Job, yang ditujukan hanya untuk beberapa jenis pekerjaan layanan. Nah, kami pergi untuk membuat aplikasi di Kotlin + Spring-Liquibase, mencoba menggunakan kembali sebanyak mungkin infrastruktur untuk layanan mikro di perusahaan di JVM di kubera.



Mari gunakan kembali aspek-aspek berikut:



  • Generasi proyek.
  • Menyebarkan.
  • Pengiriman konfigurasi dan rahasia.
  • Mengakses.
  • Pencatatan dan pengiriman log ke ELK.


Kami mendapatkan pipeline seperti itu: Dapat diklik









Kami sekarang punya



  • Mengubah versi.
  • Kami memeriksanya untuk pembaruan kelayakan β†’ rollback.
  • Menulis tes untuk DAO. Terkadang kami bahkan mengikuti TDD: kami menjalankan debugging DAO menggunakan tes. Pengujian dilakukan pada database yang baru dimunculkan di TestContainers.
  • Jalankan database buruh pelabuhan secara lokal di port standar. Kami sedang men-debug, melihat apa yang tersisa di database. Jika perlu, kita bisa mengelola database lokal secara manual.
  • Kami melakukan pengujian dan rilis otomatis patchsets dengan pipeline standar di kota tim, dengan analogi dengan layanan mikro. Pipeline adalah turunan dari layanan mikro yang memiliki database.
  • Kami tidak menyimpan kredit dari database di kota tim. Dan kami tidak peduli dengan akses dari pembuat virtual.


Saya tahu bahwa bagi banyak orang ini bukanlah wahyu. Namun semenjak Anda sudah selesai membaca, kami akan dengan senang hati membagikan pengalaman Anda di kolom komentar.



All Articles