
Saat Anda mengembangkan fungsionalitas baru menggunakan database, siklus pengembangan biasanya mencakup (tetapi tidak terbatas pada) tahapan berikut:
Menulis migrasi SQL → menulis kode → pengujian → rilis → pemantauan.
Pada artikel ini, saya ingin membagikan beberapa saran praktis tentang cara mempersingkat waktu siklus ini di setiap tahap, sekaligus tidak mengurangi kualitas, tetapi malah meningkatkannya.
Karena kami bekerja dengan PostgreSQL di perusahaan dan menulis kode server di Java, contohnya akan didasarkan pada tumpukan ini, meskipun sebagian besar idenya tidak bergantung pada database dan bahasa pemrograman yang digunakan.
Migrasi SQL
Tahap pertama pengembangan setelah perancangan adalah penulisan migrasi SQL. Saran utama - jangan membuat perubahan manual apa pun pada skema data, tetapi selalu lakukan melalui skrip dan simpan di satu tempat.
Di perusahaan kami, pengembang menulis sendiri migrasi SQL, jadi semua migrasi disimpan dalam repositori dengan kode utama. Di beberapa perusahaan, administrator database terlibat dalam mengubah skema, dalam hal ini registri migrasi ada di suatu tempat dengan mereka. Dengan satu atau lain cara, pendekatan ini membawa keuntungan sebagai berikut:
- Anda selalu dapat dengan mudah membuat basis baru dari awal atau memutakhirkan yang sudah ada ke versi saat ini. Ini memungkinkan Anda dengan cepat menerapkan lingkungan pengujian baru dan lingkungan pengembangan lokal.
- Semua basis memiliki tata letak yang sama - tidak ada kejutan dalam layanan.
- Ada riwayat semua perubahan (pembuatan versi).
Ada banyak alat siap pakai untuk mengotomatisasi proses ini, baik komersial dan gratis: jalur terbang , liquibase , sqitch , dll Dalam artikel ini saya tidak akan membandingkan dan memilih alat terbaik - ini adalah topik besar yang terpisah, dan Anda dapat menemukan banyak artikel di atasnya ...
Kami menggunakan flyway, berikut sedikit informasi tentangnya:
- Ada 2 jenis migrasi: berbasis sql dan berbasis java
- Migrasi SQL tidak dapat diubah (tidak dapat diubah). Setelah eksekusi pertama, migrasi SQL tidak dapat diubah. Flyway menghitung checksum untuk konten file migrasi dan memverifikasinya di setiap proses. Manipulasi manual tambahan diperlukan untuk membuat migrasi Java tidak dapat diubah .
- flyway_schema_history ( schema_version). , , , .
Menurut perjanjian internal kami, semua perubahan skema data hanya dilakukan melalui migrasi SQL. Kekekalannya memastikan bahwa kita selalu bisa mendapatkan skema aktual yang sepenuhnya identik dengan semua lingkungan.
Migrasi Java hanya digunakan untuk DML , ketika tidak mungkin untuk menulis dalam SQL murni. Bagi kami, contoh khas dari situasi seperti itu adalah migrasi untuk mentransfer data ke Postgres dari database lain (kami pindah dari Redis ke Postgres, tetapi ini adalah cerita yang sama sekali berbeda). Contoh lainnya adalah memperbarui data tabel besar, yang dilakukan dalam beberapa transaksi untuk meminimalkan waktu penguncian tabel. Perlu dikatakan bahwa dari Postgres versi 11 ini dapat dilakukan dengan menggunakan prosedur SQL di plpgsql.
Jika kode Java sudah kedaluwarsa, migrasi dapat dihapus agar tidak menghasilkan warisan (kelas migrasi Java itu sendiri tetap ada, tetapi di dalamnya kosong). Di negara kami, hal ini dapat terjadi tidak lebih dari sebulan setelah migrasi ke produksi - kami yakin bahwa ini adalah waktu yang cukup untuk memperbarui semua lingkungan pengujian dan lingkungan pengembangan lokal. Perlu dicatat bahwa karena migrasi Java hanya digunakan untuk DML, penghapusannya tidak memengaruhi pembuatan database baru dari awal dengan cara apa pun.
Nuansa penting bagi mereka yang menggunakan pg_bouncer
Jalur terbang menerapkan kunci selama migrasi untuk mencegah eksekusi beberapa migrasi secara bersamaan. Sederhananya, ini berfungsi seperti ini:
- penguncian terjadi
- melakukan migrasi dalam transaksi terpisah
- membuka blokir.
Untuk Postgres, ia menggunakan kunci penasehat dalam mode sesi, yang berarti agar berfungsi dengan benar, server aplikasi harus berjalan pada koneksi yang sama selama pengambilan dan pelepasan kunci. Jika Anda menggunakan pg_bouncer dalam mode transaksional (yang paling umum) atau dalam mode permintaan tunggal, maka untuk setiap transaksi dapat mengembalikan koneksi baru dan jalur terbang tidak akan dapat melepaskan kunci yang sudah ada.
Untuk mengatasi masalah ini, kami menggunakan kumpulan koneksi kecil terpisah di pg_bouncer dalam mode sesi, yang hanya dimaksudkan untuk migrasi. Dari sisi aplikasi juga terdapat pool terpisah yang berisi 1 koneksi dan ditutup oleh timeout setelah migrasi, agar tidak membuang resource.
Pengodean
Migrasi telah dibuat, sekarang kita sedang menulis kodenya.
Ada 3 pendekatan untuk bekerja dengan database dari sisi aplikasi:
- Menggunakan ORM (jika kita berbicara tentang Java, maka hibernate secara de facto adalah standarnya)
- Menggunakan sql + jdbcTemplate biasa dll.
- Menggunakan perpustakaan DSL.
Menggunakan ORM memungkinkan Anda mengurangi persyaratan pengetahuan SQL - banyak yang dihasilkan secara otomatis:
- skema data dapat dibuat dari xml-description atau Java-entity yang tersedia dalam kode
- hubungan objek ditentukan menggunakan deskripsi deklaratif - ORM akan membuat gabungan untuk Anda
- saat menggunakan Spring Data JPA, kueri yang lebih rumit pun dapat dibuat secara otomatis berdasarkan tanda tangan metode repositori .
"Bonus" lainnya adalah adanya data caching di luar kotak (untuk hibernate, ini adalah 3 level cache).
Tetapi penting untuk dicatat bahwa ORM, seperti alat canggih lainnya, memerlukan kualifikasi tertentu saat menggunakannya. Tanpa konfigurasi yang tepat, kode kemungkinan besar akan berfungsi, tetapi jauh dari optimal.
Kebalikannya adalah menulis SQL dengan tangan. Ini memungkinkan Anda untuk memiliki kendali penuh atas permintaan - apa yang Anda tulis dieksekusi, tidak ada kejutan. Tapi, jelas, ini meningkatkan jumlah tenaga kerja manual dan meningkatkan persyaratan kualifikasi pengembang.
Perpustakaan DSL
Kira-kira di tengah-tengah antara pendekatan ini ada satu lagi, yang terdiri dari penggunaan perpustakaan DSL ( jOOQ , Querydsl , dll.). Mereka biasanya jauh lebih ringan daripada ORM, tetapi lebih nyaman daripada pekerjaan database yang sepenuhnya manual. Penggunaan DSL kurang umum, jadi artikel ini akan membahas sekilas pendekatan ini.
Kami akan berbicara tentang salah satu perpustakaan - jOOQ . Apa yang dia tawarkan:
- inspeksi database dan pembuatan kelas secara otomatis
- API fasih untuk menulis permintaan.
jOOQ bukan ORM - tidak ada kueri yang dibuat secara otomatis, tidak ada cache, tetapi pada saat yang sama, beberapa masalah dari pendekatan manual sepenuhnya ditutup:
- kelas untuk tabel, tampilan, fungsi, dll. objek database dibuat secara otomatis
- permintaan ditulis dalam Java, ini menjamin jenis aman - permintaan yang salah secara sintaksis atau permintaan dengan parameter jenis yang salah tidak akan dikompilasi - IDE Anda akan segera meminta kesalahan, dan Anda tidak perlu membuang waktu meluncurkan aplikasi untuk memeriksa kebenaran permintaan. Ini mempercepat proses pengembangan dan mengurangi kemungkinan kesalahan.
Di dalam kode, permintaannya terlihat seperti ini :
BookRecord book = dslContext.selectFrom(BOOK)
.where(BOOK.LANGUAGE.eq("DE"))
.orderBy(BOOK.TITLE)
.fetchAny();
Anda dapat menggunakan sql biasa jika Anda ingin:
Result<Record> records = dslContext.fetch("SELECT * FROM BOOK WHERE LANGUAGE = ? ORDER BY TITLE LIMIT 1", "DE");
Jelas, dalam hal ini, kebenaran kueri dan analisis hasil sepenuhnya ada di pundak Anda.
jOOQ Record dan POJO
BookRecord pada contoh di atas adalah pembungkus baris dalam tabel buku dan mengimplementasikan pola rekaman aktif . Karena kelas ini adalah bagian dari lapisan akses data (selain implementasi spesifiknya), maka Anda mungkin tidak ingin mentransfernya ke lapisan lain aplikasi, tetapi gunakan beberapa jenis objek pojo Anda sendiri. Untuk kenyamanan mengonversi catatan <–> pojo jooq menawarkan beberapa mekanisme: otomatis dan manual . Dokumentasi untuk tautan di atas memiliki berbagai contoh penggunaan baca, tetapi tidak ada contoh untuk memasukkan dan memperbarui data baru. Mari kita isi celah ini:
private static final RecordUnmapper<Book, BookRecord> unmapper =
book -> new BookRecord(book.getTitle(), ...); // -
public void create(Book book) {
context.insertInto(BOOK)
.set(unmapper.unmap(book))
.execute();
}
Seperti yang Anda lihat, semuanya cukup sederhana.
Pendekatan ini memungkinkan Anda menyembunyikan detail implementasi di dalam kelas lapisan akses data dan menghindari "kebocoran" ke lapisan lain aplikasi.
Juga jooq dapat menghasilkan kelas DAO dengan seperangkat metode dasar untuk menyederhanakan bekerja dengan data tabel dan mengurangi jumlah kode manual (ini sangat mirip dengan JPA Data Musim Semi):
public interface DAO<R extends TableRecord<R>, P, T> {
void insert(P object) throws DataAccessException;
void update(P object) throws DataAccessException;
void delete(P... objects) throws DataAccessException;
void deleteById(T... ids) throws DataAccessException;
boolean exists(P object) throws DataAccessException;
...
}
Di perusahaan kami tidak menggunakan pembuatan kelas DAO secara otomatis - kami hanya membuat pembungkus di atas objek database, dan menulis kueri sendiri. Wrappers dibuat setiap kali modul maven terpisah dibuat ulang, tempat migrasi disimpan. Beberapa saat kemudian, akan ada detail tentang bagaimana ini diterapkan.
Menguji
Menulis tes adalah bagian penting dari proses pengembangan - tes yang baik menjamin kualitas kode Anda dan menghemat waktu sambil mempertahankannya. Pada saat yang sama, adil untuk mengatakan bahwa kebalikannya juga benar - pengujian yang buruk dapat menciptakan ilusi kode kualitas, menyembunyikan kesalahan, dan memperlambat proses pengembangan. Jadi, tidak cukup hanya dengan memutuskan bahwa Anda akan menulis tes, Anda harus melakukannya dengan benar . Pada saat yang sama, konsep kebenaran tes sangat kabur dan setiap orang memiliki bagiannya sendiri.
Hal yang sama berlaku untuk pertanyaan klasifikasi tes. Artikel ini menyarankan penggunaan opsi pemisahan berikut:
- pengujian unit (pengujian unit)
- tes integrasi
- pengujian ujung ke ujung (ujung ke ujung).
Pengujian unit melibatkan pemeriksaan fungsionalitas modul individu secara terpisah satu sama lain. Ukuran modul lagi-lagi merupakan hal yang tidak terdefinisi, untuk beberapa itu adalah metode terpisah, untuk beberapa itu adalah kelas. Isolasi berarti bahwa semua modul lain adalah tiruan atau rintisan (dalam bahasa Rusia ini adalah tiruan atau rintisan, tetapi entah bagaimana kedengarannya tidak terlalu bagus). Ikuti tautan ini untuk membaca artikel Martin Fowler tentang perbedaan antara keduanya. Tes unit kecil, cepat, tetapi hanya dapat menjamin kebenaran logika dari unit individu.
Tes integrasitidak seperti unit test, mereka memeriksa interaksi beberapa modul satu sama lain. Bekerja dengan database adalah contoh yang baik ketika tes integrasi masuk akal, karena sangat sulit untuk "mengunci" database dengan kualitas tinggi, dengan mempertimbangkan semua nuansanya. Pengujian integrasi dalam banyak kasus merupakan kompromi yang baik antara kecepatan eksekusi dan jaminan kualitas saat menguji database dibandingkan dengan jenis pengujian lainnya. Oleh karena itu, dalam artikel ini kita akan membahas lebih lanjut tentang jenis pengujian ini.
Pengujian ujung ke ujung adalah yang paling ekstensif. Untuk melaksanakannya, perlu untuk meningkatkan seluruh lingkungan. Ini menjamin tingkat kepercayaan tertinggi pada kualitas produk, tetapi paling lambat dan paling mahal.
Tes integrasi
Ketika datang ke pengujian integrasi kode yang bekerja dengan database, sebagian besar pengembang mengajukan pertanyaan: bagaimana memulai database, bagaimana menginisialisasi statusnya dengan data awal, dan bagaimana melakukannya secepat mungkin?
Beberapa waktu lalu, h2 adalah praktik yang cukup umum dalam pengujian integrasi . Ini adalah database dalam memori yang ditulis di Java yang memiliki mode kompatibilitas dengan database paling populer. Tidak adanya kebutuhan untuk menginstal database dan keserbagunaan h2 membuatnya menjadi pengganti yang sangat nyaman untuk database nyata, terutama jika aplikasi tidak bergantung pada database tertentu dan hanya menggunakan apa yang termasuk dalam standar SQL (yang tidak selalu terjadi).
Tetapi masalah dimulai pada saat Anda menggunakan beberapa fungsionalitas database yang rumit (atau yang benar-benar baru dari versi baru), dukungan yang tidak diimplementasikan di h2. Secara umum, karena ini adalah "simulasi" dari DBMS tertentu, selalu ada beberapa perbedaan dalam perilakunya.
Pilihan lainnya adalah menggunakan postgres tertanam . Ini adalah Postgres asli, dikirim sebagai arsip dan tidak memerlukan instalasi. Ini memungkinkan Anda untuk bekerja seperti versi Postgres biasa.
Ada beberapa implementasi, yang paling populer dari Yandex dan openTable... Kami di perusahaan menggunakan versi dari Yandex. Dari minusnya - ini cukup lambat saat startup (setiap kali arsip dibuka dan database diluncurkan - dibutuhkan 2-5 detik, tergantung pada kekuatan komputer), ada juga masalah dengan kelambatan di belakang versi rilis resmi. Kami juga menghadapi masalah bahwa setelah upaya untuk menghentikan kode, beberapa kesalahan terjadi dan proses Postgres tetap menggantung di OS - Anda harus membunuhnya secara manual.
testcontainers
Pilihan ketiga adalah menggunakan buruh pelabuhan. Untuk Java, ada pustaka testcontainers yang menyediakan api untuk bekerja dengan kontainer buruh pelabuhan dari kode. Jadi, setiap dependensi dalam aplikasi Anda yang memiliki image buruh pelabuhan bisa diganti dalam pengujian menggunakan testcontainers. Selain itu, untuk banyak teknologi populer, ada kelas siap pakai terpisah yang menyediakan api yang lebih nyaman, bergantung pada gambar yang digunakan:
- database (Postgres, Oracle, Cassandra, MongoDB, dll.),
- nginx
- kafka, dll.
Ngomong-ngomong, ketika proyek tescontainers menjadi cukup populer, pengembang yandex secara resmi mengumumkan bahwa mereka menghentikan pengembangan proyek postgres yang disematkan dan menyarankan untuk beralih ke penguji.
Apa kelebihannya:
- testcontainers cepat (memulai Postgres kosong membutuhkan waktu kurang dari satu detik)
- komunitas postgres merilis gambar buruh pelabuhan resmi untuk setiap versi baru
- testcontainers memiliki proses khusus yang membunuh container yang menggantung setelah jvm mematikan, kecuali Anda melakukannya secara terprogram
- dengan testcontainers, Anda bisa menggunakan pendekatan seragam untuk menguji dependensi eksternal aplikasi Anda, yang jelas membuat segalanya lebih mudah.
Contoh tes menggunakan Postgres:
@Test
public void testSimple() throws SQLException {
try (PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>()) {
postgres.start();
ResultSet resultSet = performQuery(postgres, "SELECT 1");
int resultSetInt = resultSet.getInt(1);
assertEquals("A basic SELECT query succeeds", 1, resultSetInt);
}
}
Jika tidak ada kelas terpisah untuk image di testcontainers, maka pembuatan container terlihat seperti ini :
public static GenericContainer redis = new GenericContainer("redis:3.0.2")
.withExposedPorts(6379);
Jika Anda menggunakan JUnit4, JUnit5 atau Spock, maka testcontainers memiliki tambahan. dukungan untuk kerangka kerja ini, yang membuat tes menulis lebih mudah.
Mempercepat pengujian dengan testcontainers
Meskipun beralih dari postgres tertanam ke testcontainers membuat pengujian kami lebih cepat dengan menjalankan Postgres lebih cepat, seiring waktu pengujian mulai melambat lagi. Ini disebabkan oleh peningkatan jumlah migrasi SQL yang dilakukan flyway saat startup. Ketika jumlah migrasi melebihi seratus, waktu eksekusi sekitar 7-8 detik, yang secara signifikan memperlambat pengujian. Ini bekerja seperti ini:
- sebelum kelas pengujian berikutnya, wadah "bersih" dengan Postgres diluncurkan
- jalur terbang melakukan migrasi
- tes kelas ini dilaksanakan
- wadah itu dihentikan dan dipindahkan
- ulangi dari item 1 untuk kelas tes berikutnya.
Jelas, seiring berjalannya waktu langkah ke-2 membutuhkan lebih banyak waktu.
Mencoba memecahkan masalah ini, kami menyadari bahwa melakukan migrasi hanya sekali sebelum semua pengujian, menyimpan status container, dan kemudian menggunakan container ini di semua pengujian. Jadi algoritme telah berubah:
- wadah "bersih" dengan Postgres diluncurkan sebelum semua pengujian
- flyway melakukan migrasi
- status kontainer tetap ada
- sebelum kelas pengujian berikutnya, penampung yang telah disiapkan sebelumnya diluncurkan
- tes kelas ini dijalankan
- wadah berhenti dan dibuang
- ulangi dari langkah 4 untuk kelas tes berikutnya.
Sekarang waktu pelaksanaan pengujian individu tidak bergantung pada jumlah migrasi, dan dengan jumlah migrasi saat ini (200+), skema baru menghemat beberapa menit pada setiap menjalankan semua pengujian.
Berikut ini beberapa detail teknis tentang cara menerapkannya.
Docker memiliki mekanisme built-in untuk membuat image baru dari container yang sedang berjalan menggunakan perintah commit . Ini memungkinkan Anda untuk menyesuaikan gambar, misalnya, dengan mengubah pengaturan apa pun.
Peringatan penting adalah bahwa perintah tersebut tidak menyimpan data dari partisi yang dipasang. Tetapi jika Anda mengambil image docker Postgres resmi, maka direktori PGDATA, tempat data disimpan, terletak di bagian terpisah (sehingga setelah kontainer dimulai ulang, data tidak hilang), oleh karena itu, ketika komit dijalankan, status database itu sendiri tidak disimpan.
Solusinya sederhana - jangan gunakan bagian untuk PGDATA, tetapi simpan data di memori, yang cukup normal untuk pengujian. Ada 2 cara untuk melakukan ini - gunakan file dok Anda (sesuatu seperti ini) tanpa membuat bagian, atau mengganti variabel PGDATA saat memulai penampung resmi (bagian akan tetap ada, tetapi tidak akan digunakan). Cara kedua terlihat jauh lebih sederhana:
PostgreSQLContainer<?> container = ...
container.addEnv("PGDATA", "/var/lib/postgresql/data-no-mounted");
container.start();
Sebelum melakukan, dianjurkan bahwa Anda pos pemeriksaan postgres perubahan siram dari buffer bersama untuk "disk" (yang berkorespondensi dengan variabel PGDATA ditimpa):
container.execInContainer("psql", "-c", "checkpoint");
Komit itu sendiri berjalan seperti ini:
CommitCmd cmd = container.getDockerClient().commitCmd(container.getContainerId())
.withMessage("Container for integration tests. ...")
.withRepository(imageName)
.withTag(tag);
String imageId = cmd.exec();
Perlu dicatat bahwa pendekatan ini menggunakan gambar yang disiapkan dapat diterapkan ke banyak gambar lain, yang juga akan menghemat waktu saat menjalankan uji integrasi.
Beberapa kata lagi tentang mengoptimalkan waktu pembuatan
Seperti disebutkan sebelumnya, saat merakit modul maven terpisah dengan migrasi, antara lain, pembungkus java dibuat di atas objek database. Untuk ini, plugin maven yang ditulis sendiri digunakan, yang diluncurkan sebelum mengompilasi kode utama dan melakukan 3 tindakan:
- Menjalankan container buruh pelabuhan "bersih" dengan postgres
- Meluncurkan Flyway, yang melakukan migrasi sql untuk semua database, dengan demikian memeriksa validitasnya
- Menjalankan Jooq, yang memeriksa skema database dan menghasilkan kelas java untuk tabel, tampilan, fungsi, dan objek skema lainnya.
Seperti yang dapat Anda lihat dengan mudah, 2 langkah pertama identik dengan yang dilakukan saat pengujian dijalankan. Untuk menghemat waktu dalam memulai penampung dan menjalankan migrasi sebelum pengujian, kami memindahkan penyimpanan status penampung ke sebuah plugin. Jadi, sekarang, segera setelah membangun kembali modul, gambar siap pakai untuk pengujian integrasi semua database yang digunakan dalam kode muncul di repositori lokal gambar buruh pelabuhan.
Contoh kode yang lebih detail
( «start»):
save-state stop .
:
@ThreadSafe
public class PostgresContainerAdapter implements PostgresExecutable {
private static final String ORIGINAL_IMAGE = "postgres:11.6-alpine";
@GuardedBy("this")
@Nullable
private PostgreSQLContainer<?> container; // not null if it is running
@Override
public synchronized String start(int port, String db, String user, String password)
{
Preconditions.checkState(container == null, "postgres is already running");
PostgreSQLContainer<?> newContainer = new PostgreSQLContainer<>(ORIGINAL_IMAGE)
.withDatabaseName(db)
.withUsername(user)
.withPassword(password);
newContainer.addEnv("PGDATA", "/var/lib/postgresql/data-no-mounted");
// workaround for using fixed port instead of random one chosen by docker
List<String> portBindings = new ArrayList<>(newContainer.getPortBindings());
portBindings.add(String.format("%d:%d", port, POSTGRESQL_PORT));
newContainer.setPortBindings(portBindings);
newContainer.start();
container = newContainer;
return container.getJdbcUrl();
}
@Override
public synchronized void saveState(String name) {
try {
Preconditions.checkState(container != null, "postgres isn't started yet");
// flush all changes
doCheckpoint(container);
commitContainer(container, name);
} catch (Exception e) {
stop();
throw new RuntimeException("Saving postgres container state failed", e);
}
}
@Override
public synchronized void stop() {
Preconditions.checkState(container != null, "postgres isn't started yet");
container.stop();
container = null;
}
private static void doCheckpoint(PostgreSQLContainer<?> container) {
try {
container.execInContainer("psql", "-c", "checkpoint");
} catch (IOException | InterruptedException e) {
throw new RuntimeException(e);
}
}
private static void commitContainer(PostgreSQLContainer<?> container, String image)
{
String tag = "latest";
container.getDockerClient().commitCmd(container.getContainerId())
.withMessage("Container for integration tests. It uses non default location for PGDATA which is not mounted to a volume")
.withRepository(image)
.withTag(tag)
.exec();
}
// ...
}
( «start»):
@Mojo(name = "start")
public class PostgresPluginStartMojo extends AbstractMojo {
private static final Logger logger = LoggerFactory.getLogger(PostgresPluginStartMojo.class);
@Nullable
static PostgresExecutable postgres;
@Parameter(defaultValue = "5432")
private int port;
@Parameter(defaultValue = "dbName")
private String db;
@Parameter(defaultValue = "userName")
private String user;
@Parameter(defaultValue = "password")
private String password;
@Override
public void execute() throws MojoExecutionException {
if (postgres != null) {
logger.warn("Postgres already started");
return;
}
logger.info("Starting Postgres");
if (!isDockerInstalled()) {
throw new IllegalStateException("Docker is not installed");
}
String url = start();
testConnection(url, user, password);
logger.info("Postgres started at " + url);
}
private String start() {
postgres = new PostgresContainerAdapter();
return postgres.start(port, db, user, password);
}
private static void testConnection(String url, String user, String password) throws MojoExecutionException {
try (Connection conn = DriverManager.getConnection(url, user, password)) {
conn.createStatement().execute("SELECT 1");
} catch (SQLException e) {
throw new MojoExecutionException("Exception occurred while testing sql connection", e);
}
}
private static boolean isDockerInstalled() {
if (CommandLine.executableExists("docker")) {
return true;
}
if (CommandLine.executableExists("docker.exe")) {
return true;
}
if (CommandLine.executableExists("docker-machine")) {
return true;
}
if (CommandLine.executableExists("docker-machine.exe")) {
return true;
}
return false;
}
}
save-state stop .
:
<build>
<plugins>
<plugin>
<groupId>com.miro.maven</groupId>
<artifactId>PostgresPlugin</artifactId>
<executions>
<!-- running a postgres container -->
<execution>
<id>start-postgres</id>
<phase>generate-sources</phase>
<goals>
<goal>start</goal>
</goals>
<configuration>
<db>${db}</db>
<user>${dbUser}</user>
<password>${dbPassword}</password>
<port>${dbPort}</port>
</configuration>
</execution>
<!-- applying migrations and generation java-classes -->
<execution>
<id>flyway-and-jooq</id>
<phase>generate-sources</phase>
<goals>
<goal>execute-mojo</goal>
</goals>
<configuration>
<plugins>
<!-- applying migrations -->
<plugin>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-maven-plugin</artifactId>
<version>${flyway.version}</version>
<executions>
<execution>
<id>migration</id>
<goals>
<goal>migrate</goal>
</goals>
<configuration>
<url>${dbUrl}</url>
<user>${dbUser}</user>
<password>${dbPassword}</password>
<locations>
<location>filesystem:src/main/resources/migrations</location>
</locations>
</configuration>
</execution>
</executions>
</plugin>
<!-- generation java-classes -->
<plugin>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen-maven</artifactId>
<version>${jooq.version}</version>
<executions>
<execution>
<id>jooq-generate-sources</id>
<goals>
<goal>generate</goal>
</goals>
<configuration>
<jdbc>
<url>${dbUrl}</url>
<user>${dbUser}</user>
<password>${dbPassword}</password>
</jdbc>
<generator>
<database>
<name>org.jooq.meta.postgres.PostgresDatabase</name>
<includes>.*</includes>
<excludes>
#exclude flyway tables
schema_version | flyway_schema_history
# other excludes
</excludes>
<includePrimaryKeys>true</includePrimaryKeys>
<includeUniqueKeys>true</includeUniqueKeys>
<includeForeignKeys>true</includeForeignKeys>
<includeExcludeColumns>true</includeExcludeColumns>
</database>
<generate>
<interfaces>false</interfaces>
<deprecated>false</deprecated>
<jpaAnnotations>false</jpaAnnotations>
<validationAnnotations>false</validationAnnotations>
</generate>
<target>
<packageName>com.miro.persistence</packageName>
<directory>src/main/java</directory>
</target>
</generator>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</configuration>
</execution>
<!-- creation an image for integration tests -->
<execution>
<id>save-state-postgres</id>
<phase>generate-sources</phase>
<goals>
<goal>save-state</goal>
</goals>
<configuration>
<name>postgres-it</name>
</configuration>
</execution>
<!-- stopping the container -->
<execution>
<id>stop-postgres</id>
<phase>generate-sources</phase>
<goals>
<goal>stop</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
Melepaskan
Kode ditulis dan diuji - saatnya untuk rilis. Secara umum, kompleksitas rilis bergantung pada faktor-faktor berikut:
- pada jumlah database (satu atau lebih)
- pada ukuran database
- pada jumlah server aplikasi (satu atau lebih)
- rilis mulus atau tidak (apakah waktu henti aplikasi diizinkan).
Item 1 dan 3 memberlakukan persyaratan kompatibilitas mundur pada kode, karena dalam banyak kasus tidak mungkin untuk memperbarui semua database dan semua server aplikasi secara bersamaan - akan selalu ada titik waktu ketika database akan memiliki skema yang berbeda, dan server akan memiliki versi kode yang berbeda.
Ukuran database mempengaruhi waktu migrasi - semakin besar database, semakin besar kemungkinan Anda perlu melakukan migrasi yang lama.
Kelancaran sebagian merupakan faktor resultan - jika rilis dilakukan dengan shutdown (downtime), maka 3 poin pertama tidak begitu penting dan hanya mempengaruhi waktu tidak tersedianya aplikasi.
Jika kita berbicara tentang layanan kita, maka ini adalah:
- sekitar 30 cluster database
- ukuran satu basis 200 - 400 GB
- ( 100),
- .
Kami menggunakan rilis canary : versi baru aplikasi pertama kali ditampilkan di sejumlah kecil server (kami menyebutnya pra-rilis), dan setelah beberapa saat, jika tidak ada kesalahan yang ditemukan dalam pra-rilis, versi tersebut akan dirilis ke server lain. Dengan demikian, server produksi dapat berjalan pada versi yang berbeda.
Saat meluncurkan, setiap server aplikasi memeriksa versi database dengan versi skrip yang ada di kode sumber (dalam istilah flyway, ini disebut validasi ). Jika berbeda, server tidak akan mulai. Ini memastikan kompatibilitas kode dan database . Situasi seperti itu tidak dapat muncul ketika, misalnya, kode bekerja dengan tabel yang belum dibuat, karena migrasi berada dalam versi server yang berbeda.
Namun hal ini tentu saja tidak menyelesaikan masalah ketika, misalnya, di versi baru aplikasi ada migrasi yang menghapus kolom di tabel yang dapat digunakan di versi lama server. Sekarang kami memeriksa situasi seperti itu hanya pada tahap peninjauan (wajib), tetapi dengan cara yang bersahabat perlu untuk memperkenalkan tambahan. tahap dengan pemeriksaan seperti itu dalam siklus CI / CD.
Terkadang migrasi bisa memakan waktu lama (misalnya, saat mengupdate data dari tabel besar) dan agar tidak memperlambat rilis pada saat yang sama, kami menggunakan teknik migrasi gabungan... Kombinasi tersebut terdiri dari menjalankan migrasi secara manual pada server yang sedang berjalan (melalui panel administrasi, tanpa jalur terbang dan, karenanya, tanpa merekam dalam riwayat migrasi), dan kemudian keluaran "reguler" dari migrasi yang sama di versi server berikutnya. Migrasi ini tunduk pada persyaratan berikut:
- Pertama, itu harus ditulis sedemikian rupa agar tidak memblokir aplikasi selama eksekusi yang lama (poin utama di sini bukanlah untuk memperoleh kunci jangka panjang di tingkat DB). Untuk melakukan ini, kami memiliki pedoman internal untuk pengembang tentang cara menulis migrasi. Di masa mendatang, saya juga dapat membagikannya di Habré.
- Kedua, migrasi selama peluncuran "reguler" harus menentukan bahwa migrasi telah dilakukan dalam mode manual dan tidak melakukan apa pun dalam kasus ini - cukup lakukan rekaman baru dalam riwayat. Untuk migrasi SQL, pemeriksaan seperti itu dilakukan dengan menjalankan beberapa kueri SQL untuk perubahan. Ada pendekatan lain untuk migrasi Java - menggunakan flag boolean tersimpan, yang disetel setelah proses manual.

Pendekatan ini memecahkan 2 masalah:
- rilisnya cepat (meskipun dengan tindakan manual)
- ( ) - .
Setelah dirilis, siklus pengembangan tidak berakhir. Untuk memahami apakah fungsionalitas baru berfungsi (dan cara kerjanya), Anda perlu "menyertakan" dengan metrik. Mereka dapat dibagi menjadi 2 kelompok: bisnis dan sistem.
Grup pertama sangat bergantung pada bidang subjek: untuk server email akan berguna untuk mengetahui jumlah surat yang dikirim, untuk sumber berita - jumlah pengguna unik per hari, dll.
Metrik dari grup kedua kira-kira sama untuk semua orang - mereka menentukan status teknis server: cpu, memori, jaringan, database, dll.
Apa sebenarnya yang perlu dipantau dan bagaimana melakukannya - ini adalah topik dari sejumlah besar artikel terpisah dan tidak akan disinggung di sini. Saya hanya ingin mengingatkan hal-hal yang paling dasar (bahkan kapten):
tentukan metrik sebelumnya
Penting untuk menentukan daftar metrik dasar. Dan itu harus dilakukan sebelumnya , sebelum rilis, dan bukan setelah insiden pertama, jika Anda tidak memahami apa yang terjadi dengan sistem.
mengatur peringatan otomatis
Ini akan mempercepat waktu reaksi Anda dan menghemat waktu pada pemantauan manual. Idealnya, Anda harus mengetahui tentang masalah sebelum pengguna merasakannya dan menulis kepada Anda.
mengumpulkan metrik dari semua node
Metrik, seperti log, tidak pernah terlalu banyak. Kehadiran data dari setiap node sistem Anda (server aplikasi, database, penarik koneksi, penyeimbang, dll.) Memungkinkan Anda memiliki gambaran lengkap tentang statusnya, dan jika perlu, Anda dapat dengan cepat melokalkan masalahnya.
Contoh sederhana: memuat data halaman web mulai melambat. Ada banyak alasan:
- server web kelebihan beban dan membutuhkan waktu lama untuk menanggapi permintaan
- Kueri SQL membutuhkan waktu lebih lama untuk dieksekusi
- antrian telah terakumulasi di kumpulan koneksi dan server aplikasi tidak dapat menerima koneksi untuk waktu yang lama
- masalah jaringan
- sesuatu yang lain
Tanpa metrik, menemukan penyebab suatu masalah tidak akan mudah.
Bukan selesai
Saya ingin mengatakan frase yang sangat dangkal tentang fakta bahwa tidak ada peluru perak dan pilihan satu atau pendekatan lain tergantung pada persyaratan tugas tertentu, dan apa yang berhasil dengan baik untuk orang lain mungkin tidak berlaku untuk Anda. Tetapi semakin banyak pendekatan berbeda yang Anda ketahui, semakin menyeluruh dan kualitatif Anda dapat membuat pilihan ini. Saya harap dari artikel ini Anda telah mempelajari sesuatu yang baru untuk diri Anda sendiri yang akan membantu Anda di masa depan. Saya akan dengan senang hati mengomentari pendekatan apa yang Anda gunakan untuk meningkatkan proses bekerja dengan database.