Database Migration 是什麼
Database migration,譯為資料庫遷移,使用程式碼來定義對資料庫資料庫的修改變更。相比手動編寫 SQL,DB migration 將資料庫的修改程式碼化、自動化,並方便在不同環境中同步這些變更。
我們遇到的問題
在早期我們的產品部署到 EC2 後,由人工 SSH 上機器執行 migration,這在當時雖然不理想,但可以接受。
後來隨著團隊的擴大以及部署流程逐漸自動化,由 CI 部署應用到 Kubernetes 叢集,此時繼續依靠人工執行 migration 給我們帶來了幾個問題:
- 需要溝通且容易遺漏:我們的產品會每週 cut 一個 release 到 producton。但並不是每個 release 都包含資料庫變更,所以需要開發團隊告知運維團隊,某次 release 要處理 migration,但我們有遇到過幾次由於溝通失效導致的遺漏執行 migration 的情況。
- Migration 可能在執行中被中斷:在 Kubernetes node 資源不足的情況下,Pod 可能被 reschedule 到其它節點,原有 Pod 將會被 terminated。假如此刻正在使用
kubectl exec
執行 migration,該過程很有可能被打斷,出現不可預料的「中間狀態」。 - 資料庫需要備份:為了確保資料的安全性,我們希望在每次執行 migration 之前先完整備份資料庫,但依靠人工處理不僅浪費了寶貴的工程師時間,還容易造成 human error。
綜上,我們決定將執行 migration 的過程自動化。
使用 Job 自動化執行
為了把 migration 的執行過程自動化,我們在 Helm chart 內新增了一個 Job 資源,在每次部署時都建立一個新的 Job。
與普通的 Job 不同,執行 migration 的 Job 需要保證:
- Pod 優先順序高,儘可能不中斷。
- 禁止自動重試,一旦執行失敗則必須人工介入,確保資料安全。
- 命名唯一不衝突,每次 Helm 部署都應該建立一個全新的 Job。
- 由於每次部署會產生新的 Job,因此成功執行完成一段時間後應當自行刪除。
- 需要資料庫 root 密碼。
基於這些要求,我們編寫的 Job 大概長這樣:
{{- $fullName := printf "migration-%s" (include "chart.fullname" .) -}}
---
apiVersion: batch/v1
kind: Job
metadata:
name: {{ $fullName }}.{{ .Release.Revision }}
labels:
# 略
spec:
ttlSecondsAfterFinished: 172800 # 2 days
backoffLimit: 0
template:
metadata:
# 略
spec:
priorityClassName: high-priority-class
terminationGracePeriodSeconds: 86400 # 24 hours
restartPolicy: Never
containers:
- name: php
# 部分略
env:
- name: DB_ROOT_PASSWORD
valueFrom:
secretKeyRef:
name: {{ include "chart.fullname" . }}-db-root-password
key: dbRootPassword
args:
- php
- artisan
- migrate
- --force
可以看到:
- 在 PodSpec 中,我們配置了超長的
terminationGracePeriodSeconds
,即使收到 terminate 訊號,也留有足夠的時間讓 migration 跑完。更重要的是,我們給這個 Pod 非常高的優先順序(priorityClassName
欄位),當 Kubernetes 因資源問題開始驅逐 Pod 時,能夠儘可能保證高優先順序 Pod 的正常執行。 - Job 的
backoffLimit: 0
,Pod 的restartPolicy: Never
,確保 Job 不會重試、Pod 不會重啟。 - Job 的命名與 helm release 的 revision 掛鉤,因此能夠保證每次部署時唯一。
- Job 的
ttlSecondsAfterFinished
為172800
秒,在 Job 成功執行完成後 48 小時會自動被刪除。 - 在 Container
env
內注入了名為DB_ROOT_PASSWORD
的環境變數,值引用自一個單獨的 secret 而不是直接填寫明文,確保安全。關於機密資訊的存放和管理,我們之前曾介紹過相關方案,可參考我們的前作《使用 SOPS 管理 Secret》。
另外,如果你也在使用 Laravel,在 APP_ENV
為 production
時執行 migration 會有這樣的 interactive prompt:
**************************************
* Application In Production! *
**************************************
Do you really wish to run this command? (yes/no) [no]:
>
而 Pod 執行環境中不存在 TTY,因此別忘了給 php artisan migrate
命令加上 --force
引數,強制在 production 環境下直接執行。
自定義的 Migrate 命令
由於 Laravel 自帶的 php artisan migrate
命令無法完全滿足需求,我們在其上做了一層封裝,並自定義了一些附加功能。
自動進入 Maintenance 狀態
為了保證資料一致性,在遷移過程中不應當有業務請求更新資料。因此在開始 migrate 之前,應用需要進入 maintenance 狀態;在 migration 成功完成後,應當恢復服務。因此我們在呼叫 php artisan migrate
之前分別呼叫了 php artisan down
和 php artisan up
。另外我們還指定了 --message
選項以便於分辨維護原因:
Artisan::call('down', ['--message' => 'running migration'], $this->output);
// ...
Artisan::call('up', [], $this->output);
自動備份資料
我們使用的資料庫是 AWS RDS。為了防止自動化遷移出現任何預料外的問題,我們在 migrate:privileged
命令中,通過 AWS PHP SDK 呼叫 CreateDBSnapshot
API,為 RDS 建立 Snapshot。一旦遷移失敗,我們可以通過 Snapshot 將資料回滾到遷移前的狀態。
禁止同時執行多個 Migration
雖然概率極小,但考慮到以下兩種情況:
- 兩次 CI pipeline 時間非常接近,前者的 migrations 還沒有執行完,第二個 migration job 就被建立出來同時開始執行。
- 出現問題需要手動執行 migration,此時另一個 migration job 還正在執行。
為了避免這兩種可能導致資料異常的情況出現,我們還在該命令內加了互斥鎖,同一時間內只能有單個例項在執行。
Slack 通知
最後,我們給整個流程加上了 Slack 通知,以便於工程師們實時得知 migration 的執行狀況:
通過監聽 \Illuminate\Console\Events\CommandFinished
事件,在 php artisan down
和 up
被呼叫後,將 message 通知到 Slack。
final class MaintenanceNotification
{
public function handle(CommandFinished $event): void
{
if (!in_array($event->command, ['up', 'down'], true)) {
return;
}
if ($event->command === 'down') {
$message = $event->input->getOption('message') === null ? 'maintenance' : $event->input->getOption('message');
}
if (app()->isDownForMaintenance()) {
$color = 'warning';
$text = "API is now DOWN for $message.";
} else {
$color = 'good';
$text = "API is now UP.";
}
// Send to Slack webhook...
}
}
本作品採用《CC 協議》,轉載必須註明作者和本文連結