Migrasi skema database tanpa downtime untuk postgresql menggunakan Django sebagai contoh

Pendahuluan


Halo, Habr!


Saya ingin berbagi pengalaman menulis migrasi untuk postgres dan Django. Ini terutama tentang postgres, Django adalah tambahan yang baik di sini, karena memiliki migrasi otomatis skema data untuk perubahan model di luar kotak, yaitu, ia memiliki daftar operasi kerja yang cukup lengkap untuk mengubah skema. Django dapat diganti dengan kerangka / pustaka favorit - pendekatannya kemungkinan besar akan serupa.


Saya tidak akan menjelaskan bagaimana saya sampai pada hal ini, tetapi sekarang membaca dokumentasi saya menangkap ide bahwa perlu melakukan ini sebelumnya dengan lebih hati-hati dan kesadaran, jadi saya sangat merekomendasikannya.


Sebelum saya melangkah lebih jauh, izinkan saya membuat asumsi berikut.


Anda dapat membagi logika bekerja dengan database sebagian besar aplikasi menjadi 3 bagian:


  1. Migrasi - mengubah skema basis data (tabel), misalkan kita selalu menjalankannya dalam satu utas.
  2. Logika bisnis - pekerjaan langsung dengan data (dalam tabel pengguna), bekerja dengan data yang sama secara konstan dan kompetitif.
  3. Migrasi data - jangan mengubah skema data, mereka pada dasarnya bekerja seperti logika bisnis, secara default, ketika kita berbicara tentang logika bisnis, kami juga akan berarti migrasi data.

Downtime adalah keadaan ketika bagian dari logika bisnis kami tidak tersedia / jatuh / dimuat untuk waktu yang nyata bagi pengguna, anggap ini adalah beberapa detik.


Tidak adanya downtime dapat menjadi kondisi kritis untuk bisnis, yang harus dipatuhi oleh segala upaya.


Proses roll out


Persyaratan utama saat meluncurkan:


  1. kami memiliki satu basis kerja.
  2. kami memiliki beberapa mesin di mana logika bisnis berputar.
  3. mobil dengan logika bisnis disembunyikan di balik penyeimbang.
  4. aplikasi kita berfungsi baik sebelum, selama, dan setelah migrasi bergulir (kode lama berfungsi dengan benar dengan skema basis data lama dan baru).
  5. Aplikasi kita berfungsi dengan baik sebelum, selama dan setelah memperbarui kode pada mobil (kode lama dan baru berfungsi dengan benar dengan skema database saat ini).

Jika ada sejumlah besar perubahan dan peluncuran berhenti untuk memenuhi kondisi ini, maka dibagi menjadi jumlah peluncuran yang diperlukan lebih kecil memenuhi kondisi ini, jika tidak, kami memiliki downtime.


Pesanan peluncuran langsung:


  1. membanjiri migrasi;
  2. menghapus satu mesin dari balancer, memperbarui mesin dan me-restart, mengembalikan mesin ke balancer;
  3. mengulangi langkah sebelumnya untuk memperbarui semua mobil.

Urutan peluncuran terbalik relevan untuk menghapus tabel dan kolom dalam tabel, saat kami secara otomatis membuat migrasi sesuai dengan skema yang diubah dan memvalidasi keberadaan semua migrasi ke CI:


  1. menghapus satu mesin dari balancer, memperbarui mesin dan me-restart, mengembalikan mesin ke balancer;
  2. mengulangi langkah sebelumnya untuk memperbarui semua mobil;
  3. membanjiri migrasi.

Teori


Postgres adalah basis data yang sangat baik, kita dapat menulis aplikasi yang akan menulis dan membaca data yang sama dalam ratusan dan ribuan aliran, dan dengan probabilitas tinggi kita dapat memastikan bahwa data kita akan tetap valid dan tidak akan rusak, secara umum, ACID penuh. Postgres mengimplementasikan beberapa mekanisme untuk mencapai hal ini, salah satunya adalah pemblokiran.


Postgres memiliki beberapa jenis kunci, lebih banyak detail dapat ditemukan di sini , sebagai bagian dari topik, saya hanya akan menyentuh pada meja dan merekam kunci level.


Kunci level meja


Pada level tabel, postgres memiliki beberapa jenis kunci , fitur utamanya adalah mereka memiliki konflik, yaitu, dua operasi dengan kunci yang saling bertentangan tidak dapat dilakukan secara bersamaan:


ACCESS SHAREROW SHAREROW EXCLUSIVESHARE UPDATE EXCLUSIVESHARESHARE ROW EXCLUSIVEEXCLUSIVEACCESS EXCLUSIVE
ACCESS SHAREX
ROW SHAREXX
ROW EXCLUSIVEXXXX
SHARE UPDATE EXCLUSIVEXXXXX
SHAREXXXXX
SHARE ROW EXCLUSIVEXXXXXX
EXCLUSIVEXXXXXXX
ACCESS EXCLUSIVEXXXXXXXX

Sebagai contoh, ALTER TABLE tablename ADD COLUMN newcolumn integer dan SELECT COUNT(*) FROM tablename harus dieksekusi secara ketat satu per satu, jika tidak kita tidak dapat menemukan kolom mana yang harus dikembalikan ke COUNT(*) .


Dalam migrasi Django (daftar lengkap di bawah), ada operasi berikut dan kunci yang sesuai:


memblokiroperasi
ACCESS EXCLUSIVECREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE , ALTER TABLE , DROP INDEX
SHARECREATE INDEX
SHARE UPDATE EXCLUSIVECREATE INDEX CONCURRENTLY ALTER TABLE VALIDATE CONSTRAINT DROP INDEX CONCURRENTLY ALTER TABLE VALIDATE CONSTRAINT

Dari komentar, tidak semua ALTER TABLE memiliki penguncian ACCESS EXCLUSIVE , juga migrasi Django tidak memiliki CREATE INDEX CONCURRENTLY dan ALTER TABLE VALIDATE CONSTRAINT , tetapi mereka akan diperlukan untuk alternatif yang lebih aman daripada operasi standar sedikit kemudian.


Jika migrasi dilakukan dalam satu utas secara berurutan, maka semuanya terlihat baik, karena migrasi tidak akan bertentangan dengan migrasi lain, tetapi logika bisnis kami akan berfungsi hanya selama migrasi dan konflik.


memblokiroperasikonflik dengan kuncikonflik dengan operasi
ACCESS SHARESELECTACCESS EXCLUSIVEALTER TABLE , DROP INDEX
ROW SHARESELECT FOR UPDATEACCESS EXCLUSIVE , EXCLUSIVEALTER TABLE , DROP INDEX
ROW EXCLUSIVEINSERT , UPDATE , DELETEACCESS EXCLUSIVE , EXCLUSIVE , SHARE ROW EXCLUSIVE , SHAREALTER TABLE , DROP INDEX , CREATE INDEX

Dua poin dapat diringkas di sini:


  1. jika ada alternatif dengan penguncian yang lebih mudah, Anda dapat menggunakannya sebagai CREATE INDEX dan CREATE INDEX CONCURRENTLY .
  2. sebagian besar migrasi mengubah konflik skema data dengan logika bisnis, terlebih lagi, konflik dengan ACCESS EXCLUSIVE , yaitu, kami bahkan tidak dapat membuat SELECT sambil memegang kunci ini dan berpotensi mengharapkan waktu henti di sini, kecuali jika operasi ini tidak bekerja dengan segera dan waktu henti kami akan menjadi beberapa detik.

Harus ada pilihan, atau kita selalu menghindari ACCESS EXCLUSIVE , yaitu, kita membuat pelat baru dan menyalin data di sana - andal, tetapi untuk waktu yang lama untuk sejumlah besar data, atau kita membuat ACCESS EXCLUSIVE secepat mungkin dan membuat peringatan tambahan terhadap downtime - ini berpotensi berbahaya, tetapi cepat.


Rekam Kunci


Pada tingkat perekaman, ada juga kunci https://www.postgresql.org/docs/current/static/explicit-locking.html#LOCKING-ROWS , mereka juga bertentangan, tetapi hanya memengaruhi logika bisnis kami:


FOR KEY SHAREFOR SHAREFOR NO KEY UPDATEFOR UPDATE
FOR KEY SHAREX
FOR SHAREXX
FOR NO KEY UPDATEXXX
FOR UPDATEXXXX

Ini adalah poin utama dalam migrasi data, yaitu, jika kita melakukan migrasi data UPDATE di seluruh plat, maka sisa logika bisnis, yang memperbarui data, akan menunggu kunci untuk dirilis dan mungkin melebihi ambang batas waktu henti kami, oleh karena itu lebih baik melakukan pembaruan di bagian migrasi data. Perlu juga dicatat bahwa ketika menggunakan kueri sql yang lebih kompleks untuk migrasi data, pemisahan menjadi beberapa bagian dapat bekerja lebih cepat, karena dapat menggunakan rencana dan indeks yang lebih optimal.


Urutan operasi


Pengetahuan penting lainnya adalah bagaimana operasi akan dilakukan, kapan dan bagaimana mereka mengambil dan melepaskan kunci:


gambar


Di sini Anda dapat menyorot item-item berikut:


  1. waktu pelaksanaan operasi - untuk migrasi ini adalah waktu memegang kunci, jika kunci berat ditahan untuk waktu yang lama, kita akan memiliki waktu henti, misalnya, bisa dengan CREATE INDEX atau ALTER TABLE ADD COLUMN SET DEFAULT (di postgres 11 ini lebih baik).
  2. waktu tunggu untuk kunci yang saling bertentangan - yaitu, migrasi menunggu hingga semua permintaan yang saling bertentangan berhasil, dan pada saat ini permintaan baru akan menunggu migrasi kami, permintaan yang lambat bisa sangat berbahaya di sini, baik karena tidak optimal atau analitis, sehingga seharusnya tidak ada permintaan yang lambat selama migrasi.
  3. jumlah permintaan per detik - jika kami memiliki banyak permintaan yang bekerja untuk waktu yang lama, maka koneksi gratis dapat berakhir dengan cepat dan alih-alih satu tempat yang bermasalah, seluruh database dapat menjadi downtime (hanya akan ada batas koneksi untuk superuser), di sini Anda harus menghindari permintaan yang lambat, mengurangi jumlah permintaan misalnya, memulai migrasi selama pemuatan minimum, pisahkan komponen-komponen penting ke dalam berbagai layanan dengan basis datanya sendiri.
  4. ada banyak operasi migrasi dalam satu transaksi - semakin banyak operasi dalam satu transaksi, semakin lama kunci berat dipegang, oleh karena itu lebih baik untuk memisahkan operasi berat, tidak ada ALTER TABLE VALIDATE CONSTRAINT atau migrasi data dalam satu transaksi dengan kunci besar.

Batas waktu


lock_timeout memiliki pengaturan seperti lock_timeout dan statement_timeout , yang dapat melindungi awal migrasi, baik dari migrasi yang ditulis dengan buruk dan dari kondisi yang buruk di mana migrasi dapat dipicu. Mereka dapat diinstal secara global dan untuk koneksi saat ini.


SET lock_timeout TO '2s' akan menghindari downtime ketika menunggu permintaan / transaksi lambat sebelum migrasi: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-LOCK-TIMEOUT .


SET statement_timeout TO '2s' akan menghindari downtime ketika memulai migrasi berat dengan kunci besar: https://www.postgresql.org/docs/current/static/runtime-config-client.html#GUC-STATEMENT-TIMEOUT .


Jalan buntu


Kebuntuan dalam migrasi bukan tentang downtime, tetapi tidak menyenangkan ketika migrasi ditulis, itu berfungsi dengan baik pada lingkungan pengujian, tetapi kebuntuan menangkap kebuntuan saat bergulir di prod. Sumber utama masalah dapat berupa sejumlah besar operasi dalam satu transaksi dan Foreign Key, karena itu menciptakan kunci di kedua tabel, sehingga lebih baik untuk memisahkan operasi migrasi, semakin banyak atom semakin baik.


Rekam penyimpanan


Postgres menyimpan nilai dari tipe yang berbeda dengan cara yang berbeda : jika tipe disimpan dengan cara yang berbeda, maka mengkonversi di antara mereka akan memerlukan penulisan ulang lengkap dari semua nilai, untungnya beberapa tipe disimpan dengan cara yang sama dan tidak perlu ditulis ulang ketika diubah. Sebagai contoh, baris disimpan sama tanpa memandang ukuran dan pengurangan / peningkatan dimensi baris tidak akan membutuhkan penulisan ulang, tetapi pengurangan membutuhkan verifikasi bahwa semua baris tidak melebihi ukuran yang lebih kecil. Tipe lain juga dapat disimpan dengan cara yang serupa dan memiliki karakteristik yang serupa.


Multiversion Concurrency Control (MVCC)


Menurut dokumentasi , konsistensi postgres didasarkan pada multiversion data, yaitu, setiap transaksi dan operasi melihat versi datanya sendiri. Fitur ini mengatasi dengan akses kompetitif dengan sempurna, dan juga memberikan efek yang menarik ketika mengubah skema seperti menambah dan menghapus kolom hanya mengubah skema, jika tidak ada operasi tambahan untuk mengubah data, indeks atau konstanta, setelah itu memasukkan dan memperbarui operasi pada level rendah akan membuat baru catatan dengan semua nilai yang diperlukan, penghapusan akan menandai catatan terkait dihapus. VACUUM atau AUTO VACUUM bertanggung jawab untuk membersihkan puing-puing yang tersisa.


Contoh Django


Kami sekarang memiliki gagasan tentang apa yang dapat bergantung pada waktu henti dan bagaimana menghindarinya, tetapi sebelum menerapkan pengetahuan, Anda dapat melihat apa yang diberikan django di luar kotak ( https://github.com/django/django/django/blob/2.1.2/django /db/backends/base/schema.py dan https://github.com/django/django/blob/2.1.2/django/db/backends/postgresql/schema.py ):


operasi
1CREATE SEQUENCE
2DROP SEQUENCE
3CREATE TABLE
4DROP TABLE
5ALTER TABLE RENAME TO
6ALTER TABLE SET TABLESPACE
7ALTER TABLE ADD COLUMN [SET DEFAULT] [SET NOT NULL] [PRIMARY KEY] [UNIQUE]
8ALTER TABLE ALTER COLUMN [TYPE] [SET NOT NULL|DROP NOT NULL] [SET DEFAULT|DROP DEFAULT]
9ALTER TABLE DROP COLUMN
10ALTER TABLE RENAME COLUMN
11ALTER TABLE ADD CONSTRAINT CHECK
12ALTER TABLE DROP CONSTRAINT CHECK
13ALTER TABLE ADD CONSTRAINT FOREIGN KEY
14ALTER TABLE DROP CONSTRAINT FOREIGN KEY
15ALTER TABLE ADD CONSTRAINT PRIMARY KEY
16ALTER TABLE DROP CONSTRAINT PRIMARY KEY
17ALTER TABLE ADD CONSTRAINT UNIQUE
18ALTER TABLE DROP CONSTRAINT UNIQUE
19CREATE INDEX
20DROP INDEX

Django mencakup kebutuhan migrasi saya dengan sangat baik, sekarang kami dapat mendiskusikan operasi yang aman dan berbahaya untuk migrasi tanpa downtime dengan sepengetahuan kami.


Kami akan memanggil migrasi yang lebih aman dengan penguncian SHARE UPDATE EXCLUSIVE atau ACCESS EXCLUSIVE , yang bekerja secara instan.
Kami akan memanggil migrasi berbahaya dengan kunci SHARE dan ACCESS EXCLUSIVE , yang membutuhkan waktu cukup lama.


Saya akan meninggalkan tautan yang bermanfaat ke dokumentasi terlebih dahulu dengan contoh-contoh hebat.


Buat dan hapus tabel


CREATE SEQUENCE , DROP SEQUENCE , CREATE TABLE , DROP TABLE dapat disebut aman, karena logika bisnis tidak lagi berfungsi dengan tabel yang dimigrasikan, perilaku menghapus tabel dengan FOREIGN KEY akan sedikit lebih lambat.


Sangat mendukung operasi lembar kerja


ALTER TABLE RENAME TO - Saya tidak bisa menyebutnya aman, karena sulit untuk menulis logika yang bekerja dengan tabel seperti itu sebelum dan sesudah migrasi.


ALTER TABLE SET TABLESPACE - tidak aman, karena secara fisik menggerakkan plat, dan ini bisa memakan waktu lama pada volume yang besar.


Di sisi lain, operasi ini agak sangat langka, sebagai alternatif Anda dapat menawarkan pembuatan tabel baru dan menyalin data ke dalamnya.


Buat dan hapus kolom


ALTER TABLE ADD COLUMN , ALTER TABLE DROP COLUMN - dapat disebut safe (kreasi tanpa DEFAULT / BUKAN NULL / PRIMARY KEY / UNIK), karena logika bisnis tidak lagi berfungsi dengan kolom yang dimigrasikan, perilaku menghapus kolom dengan KUNCI ASING, konstanta dan indeks lainnya akan muncul kemudian.


ALTER TABLE ADD COLUMN SET DEFAULT , ALTER TABLE ADD COLUMN SET NOT NULL , ALTER TABLE ADD COLUMN PRIMARY KEY , ALTER TABLE ADD COLUMN UNIQUE - operasi yang tidak aman, karena mereka menambahkan kolom dan, tanpa mengeluarkan kunci, memperbarui data dengan default atau membuat konstruksi sebagai alternatif, kolom yang dapat dibatalkan dan perubahan lebih lanjut.


Perlu disebutkan SET DEFAULT lebih cepat di postgres 11, ini dapat dianggap aman, tetapi tidak menjadi sangat berguna dalam Django, karena Django menggunakan SET DEFAULT hanya untuk mengisi kolom dan kemudian membuat DROP DEFAULT , dan dalam interval antara migrasi dan memperbarui mesin dengan logika bisnis, catatan dapat dibuat di mana default akan tidak ada, yaitu, kemudian semuanya sama, lakukan migrasi data.


Sangat mendukung operasi pada lembar kerja


ALTER TABLE RENAME COLUMN - Saya juga tidak bisa menyebutnya aman, karena sulit untuk menulis logika yang berfungsi dengan kolom seperti itu sebelum dan sesudah migrasi. Sebaliknya, operasi ini juga tidak akan sering, karena alternatif dapat diusulkan untuk membuat kolom baru dan menyalin data ke dalamnya.


Perubahan kolom


ALTER TABLE ALTER COLUMN TYPE - operasi dapat berbahaya dan aman. Aman jika postgres hanya mengubah skema, dan data sudah disimpan dalam format yang diperlukan dan pemeriksaan tipe tambahan tidak diperlukan, misalnya:


  • ketik perubahan dari varchar(LESS) ke varchar(MORE) ;
  • ketik perubahan dari varchar(ANY) ke text ;
  • ketik perubahan dari numeric(LESS, SAME) ke numeric(MORE, SAME) .

ALTER TABLE ALTER COLUMN SET NOT NULL berbahaya karena melewati data di dalam dan memeriksa NULL, untungnya konstruk ini dapat diganti dengan CHECK IS NOT NULL lain CHECK IS NOT NULL . Perlu dicatat bahwa penggantian ini akan mengarah pada skema yang berbeda, tetapi dengan sifat yang identik.


ALTER TABLE ALTER COLUMN DROP NOT NULL , ALTER TABLE ALTER COLUMN SET DEFAULT , ALTER TABLE ALTER COLUMN DROP DEFAULT - operasi yang aman.


Membuat dan menghapus indeks dan konstanta


ALTER TABLE ADD CONSTRAINT CHECK dan ALTER TABLE ADD CONSTRAINT FOREIGN KEY adalah operasi-operasi yang tidak aman, tetapi mereka dapat dinyatakan sebagai NOT VALID dan kemudian ALTER TABLE VALIDATE CONSTRAINT .


ALTER TABLE ADD CONSTRAINT PRIMARY KEY dan ALTER TABLE ADD CONSTRAINT UNIQUE aman, karena mereka membuat indeks unik di dalamnya, tetapi Anda dapat membuat indeks unik sebagai CONCURRENTLY , kemudian buat konstanta yang sesuai menggunakan indeks yang sudah jadi melalui USING INDEX .


CREATE INDEX adalah operasi yang tidak aman, tetapi indeks dapat dibuat dengan CONCURRENTLY .


ALTER TABLE DROP CONSTRAINT CHECK , ALTER TABLE DROP CONSTRAINT FOREIGN KEY , ALTER TABLE DROP CONSTRAINT PRIMARY KEY , ALTER TABLE DROP CONSTRAINT UNIQUE , DROP INDEX - operasi aman.


Perlu dicatat bahwa ALTER TABLE ADD CONSTRAINT FOREIGN KEY dan ALTER TABLE DROP CONSTRAINT FOREIGN KEY mengunci dua tabel sekaligus.


Menerapkan pengetahuan dalam Django


Django memiliki operasi dalam migrasi untuk mengeksekusi SQL apa pun: https://docs.djangoproject.com/en/2.1/ref/migration-operations/#django.db.migrations.operations.RunSQL . Melalui itu, Anda dapat mengatur batas waktu yang diperlukan dan menerapkan operasi alternatif untuk migrasi, menunjukkan state_operations - migrasi yang kami ganti.


Ini berfungsi dengan baik untuk kodenya, walaupun membutuhkan penulisan tambahan, tetapi Anda dapat meninggalkan pekerjaan kotor di db backend, misalnya, https://github.com/tbicr/django-pg-zero-downtime-migrations/blob/master/django_zero_downtime_migrations_postgres_backend/schema .py mengumpulkan praktik yang dijelaskan dan menggantikan operasi yang tidak aman dengan rekan yang aman, dan ini akan berfungsi untuk perpustakaan pihak ketiga.


Pada akhirnya


Praktik-praktik ini memungkinkan saya untuk mendapatkan skema identik yang dibuat oleh Django di luar kotak, dengan pengecualian mengganti CHECK IS NOT NULL konstruk NOT NULL bukan NOT NULL dan beberapa nama konstruk (misalnya, untuk ALTER TABLE ADD COLUMN UNIQUE dan alternatif). ALTER TABLE VALIDATE CONSTRAINT lain mungkin adalah kurangnya transaksionalitas untuk operasi migrasi alternatif, terutama di mana CREATE INDEX CONCURRENTLY dan ALTER TABLE VALIDATE CONSTRAINT .


Jika Anda tidak melampaui postgres, maka ada banyak opsi untuk mengubah skema data, dan mereka dapat bervariasi dalam kombinasi dalam kondisi tertentu:


  • menggunakan jsonb sebagai solusi schamaless
  • kesempatan untuk pergi ke downtime
  • persyaratan untuk melakukan migrasi tanpa downtime

Bagaimanapun, saya berharap materi itu bermanfaat baik untuk meningkatkan waktu kerja atau memperluas kesadaran.

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


All Articles