Database Migration — Without Downtime

Sandi Wu
iCHEF
Published in
11 min readMay 12, 2021

這篇主要會著重在 Schema Migration,從最基本的「什麼是 Database Migration」、「為什麼需要 Database Migration」,到「Migration 如何盡可能地避免 Downtime」的實作方法。

什麼是 Database Migration?

想像今天有一個情境,我有一個電商網站,使用者可以透過手機號碼登入。

今天我要做一個新功能:要求使用者在登入的時候也填入 Email,所以新的 Code 就要包含拿取使用者的邏輯,新的 Database Schema 就要多一個新欄位拿來存 Email。

Example 1: Sign-up need to fill out the email

如果今天這個網站根本沒有人在用、我也是唯一的工程師,那就簡單,直接用 SQL 操作資料庫,用 Sqldump 更新到機器上、更新程式碼,一切就完成了。

但事情通常沒有那麼簡單。

1. 如果今天這個網站有人在使用,在更新 Database 的時候,使用者是無法登入的。如果這個 Table 有幾億筆使用者資料,更新的時間就造成你的服務的 Downtime。
2. 資料有可能在更新的過程中遺失。
3. 又或者有多個工程師合作,如果你新增了 email 欄位,我新增了 address 欄位,那這樣後面更新的人就會把前面的覆蓋掉。

所以這時候我們就會使用 Database Migration 來更新資料庫。回到「什麼是 Database Migration?」,Wikipedia 的定義如下:

Schema migration (also database migration, database change management) refers to the management of incremental, reversible changes and version control to relational database schemas. A schema migration is performed on a database whenever it is necessary to update or revert that database’s schema to some newer or older version.

簡單來說,Database Migraiton 是管理 Database 的改動和版本控制,包含更新和回覆,這篇的實作會以更新為主。通常會透過 Schema Migration Tool,像是 Django ORM 等工具來做管理。

提醒:Database Migration 是針對「關聯式資料庫 (Relational Database)」,如果是「非關聯式資料庫 (NoSQL Database)」,像是 MongoDB,我今天新增一個欄位 email,對於 DB 來說是沒有差異的,所以只需要在 Code 裡面拿取 user 資料的時候要注意沒有 email 時的處理。

Django ORM 怎麼做 Database Migration?

回到一開始的情境,我要新增一個 Email 欄位。

在 Django 的 Model Class 裡面加上一個 email 屬性 (property),並指定型別和一些期望的設定,透過 Django 的語法可以產出一個有編號的 Migration file,裡面就是 ORM 會對 Database 執行的語法,在跑 Migration 的時候,Django 就會依照這些編號一個一個執行。

為什麼需要 Database Migration?

剛剛有提到另一個情境,今天你在做新增 email 的功能,我在做新增 address 的功能,那這樣用 Migration 就不會衝突嗎?

還好 ORM 很聰明,他發現有兩個重複的號碼時,他就會產生錯誤訊息,這時候只要重新跑 Merge 語法,就會產生新的檔案,裡面會包含衝突的兩個檔案。

透過以上的範例,總結一下 Database Migration 的好處:

1. 留下紀錄:每次對 Database 的操作都會留下紀錄,如果是可回復的操作,也可以回復。
2. 避免衝突:如果有衝突的情況,ORM 可以直接產生合併的檔案。
3. 方便開發:如果今天我要開發不同功能,希望資料庫可以回覆到沒有 email 欄位的狀態下,就可以透過 Migration 回復,不需要直接操作 Database,產生不預期的問題。

那為什麼 Database Migration 會造成 Downtime?

上面提到,用 Sqldump 會造成 Downtime,那為什麼 Database Migration 也會造成 Downtime 呢?

回到第一個情境,今天我要新增 Email 的功能,那我的 Code 和 Database 都必須要更新,就算同時執行,還是會有一個誰先誰後的問題,所以接下來會分成 Code First 和 Migration First 來看。

Example 1: Sign-up need to fill out the email

Code First

今天 Code 更新了、但 Database 還沒,那新的 Code 就會配到舊的 Schema,新的 Code 要取出 email 的時候就會壞掉,在 Schema 更新好之前,這段時間都是 Downtime。

Code First

Migration First

今天 Database 更新了、但 Code 還沒,那舊的 Code 就會配到新的 Schema,舊的 Code 也還不知道 email 的存在,所以也不會有影響。

Migration First

那 iCHEF 怎麼做?

為了減少 Downtime 所以選擇 Migration First,並且手動分開 Migration 和 Code Deployment。

以前也嘗試過直接一個 Scrpit 裡面跑 Migration 再跑 Deployment,但遇到過 Migration 開始跑但還沒跑完就上 Code,所以直接把他們拆成兩個流程。

新增欄位沒問題,那其他操作呢?

可以看到 Code First 在新增欄位就會產生問題,所以如果主要目標希望避免 Downtime,通常會選擇 Migration First。那其他的操作呢?以下會分成新增、修改、刪除,三個主要大的分類來細講。

【新增】資料表 (Table)

今天我要新增一個功能:使用者可以在網站上看到所有的訂單記錄。

Example 2: User can check their orders

今天我先做了 Migration 產生新的資料表:order,舊的 Code 不知道 order 的存在,所以當然沒問題。

【新增】欄位 (Column)

回到我們一直用到例子:新增 email 欄位。剛剛都沒有提到細節,但這次我指定他可以是 null。

Example 1: Sign-up need to fill out the email

今天我先做了 Migration 產生新的欄位:Email,舊的 Code 不知道 email 的存在,所以當然沒問題。

那萬一我指定 email 是 not null 呢?會發生什麼事?

首先,舊的 user 之前沒有 email 的值,在 Alter table 的時候,Database 就會不知道怎麼辦;再來,舊的 Code 裡面只有給 name 和 mobile、沒有給 email。

那就給他一個預設值 (Default Value) 吧!

像是以這裡的例子,可以讓 email 預設值為空字串,在更新 Database 時舊的資料就不會壞掉了。

注意:MySQL 8 / Postgres 11 (不含)以下,在新增欄位時,如果指定預設值,會整個資料表刪掉再重建。又因為 Alter table 的時候,其他的操作,包含 Select 都會被 Lock 住,就會造成 Downtime。在 MySQL 8 / Postgres 11 改為把 Default 值放在 Meta Data。
Reference: MySQL / PostgreSQL

【修改】資料表/欄位名稱 (Name)

現在假設有一個需求,我發現不是所有人都有手機號碼,所以打算把 mobile 改成 phone,那要怎麼做呢?

Example 3: Use phone istead of mobile

如果直接改掉,那舊的 Code 就會找不到 mobile 而壞掉。所以我們會這樣做:建立一個新欄位 phone,把資料從 mobile 複製到 phone,再把 mobile 刪掉。(如下圖)

實作上有兩種方式:
1. 用 Database Trigger:直接進行強制的複製和轉換,但有些資料庫沒有支援 Trigger
2. 用寫 Code 來解決,透過程式複製和轉換

那 iCHEF 怎麼做?

在 iCHEF,我們會把上面的步驟拆成兩次的 Release。

第一次 Release 只新增 phone 並複製資料,確定資料都沒問題後,在第二次 Release 再把 mobile 刪掉。

【修改】欄位資料型態 (Data Type)

假設我今天就是想把 mobile 從 char 改成 text,那要怎麼做呢?

Example 4: Change the mobile type to text

跟上面一模模一樣樣,先建立一個新的 mobile 是 text 型別,把資料轉換並同步後,再刪除舊的 mobile。

【刪除】欄位

假設我本來有個 nickname 欄位,但發現我從頭到尾都沒用到,要怎麼做?

Example 5: Nickname is not used

直接毫無懸念地把它刪除吧!

那假設 nickname 是還有在使用的呢?

Example 6: Nickname is still being used

刪不得!

只能先把 Code 檢查一遍,都確定把有取用 nickname 的地方都刪掉後,才能再來做 Migration。

那 Data Migration 是什麼呢?

假設我今天原本有 first_name 和 last_name,我決定要改成用 full_name 來儲存就好,那這樣的情況我們就會稱為 Data Migration。

Example 7: Combine first name and last name

需要注意的重點主要有兩個:

1. 時間:Lock 的時間會受到 Table 的資料多寡影響,如果評估會跑很久,可以分批跑;或是有人會選擇在該使用者登入時才更新這個使用者的那行資料,把時間分散在每次有人登入的時候
2. 回復:在確定完全沒問題之前,最好都不要把舊的欄位給刪掉

總結 Database Migration

Code First 和 Migration First

最大的差異可以從以下表格看出來。Code First 在「新增欄位」時,一定會造成 Downtime,但是可以在「刪除欄位」時,一次的 Release 中就可以完成,可以算是 Code First 唯一的好處。而 Migration First 在適當的操作下,可以盡可能地把 Downtime 的傷害減到最低。

如何透過 Migration First 降低 Downtime?

第一,舊的 Code 配上新的 Schema 不會壞。第二,如果無法避免 Downtime,那就盡可能減少 Migration 所需的時間。

希望以上的分享有幫助到大家,有任何問題、或是有其他的做法,都歡迎留言討論,iCHEF 也在尋找優秀的夥伴 (看有什麼職缺),歡迎加入我們。

Reference

  1. Wiki — Schema migration
  2. Wiki — Data migration
  3. Update database without downtime
  4. Postgresql — Lock Matrix for different operations

--

--

Sandi Wu
iCHEF
Editor for

iCHEF Backend Engineer. Graduated from NTU Finance. Used to be an analyst at AppWorks.