在維護微服務或個人專案的容器化環境時,MariaDB 絕對是許多工程師的首選資料庫。然而,當隨著時間推進,伺服器上的 MariaDB 需要進行大版本升級(例如從 10.5 跨到 10.11 LTS 甚至 11.4)時,你是否也曾遭遇過資料庫啟動失敗、Schema 毀損,甚至是跨版本跳太快導致 mariadb-upgrade 報錯的噩夢?
今天這篇文章,我們就來聊聊 MariaDB 跨版本升級常見的痛點,並分享一套我寫好的自動化 Bash 腳本 mariadb-upgrade.sh,讓你透過容器化環境實現「無痛、自動化且具備備份防護」的升級流程!
為什麼 MariaDB 版號升級這麼難搞?
在容器化(Docker / Docker Compose)環境下,新手最常犯的錯誤就是直接把 Compose 檔裡面的 image: mariadb:10.5 改成 image: mariadb:11.4 然後執行 docker compose up -d。結果往往是 MariaDB 容器不斷崩潰重啟(CrashLoopBackOff)。
這背後主要有三大痛點:
痛點 1:無法「跨太多大版本」直接升級
MariaDB 的底層 Data Directory(預設位於 /var/lib/mysql)包含許多系統表單與引擎內部結構。官方社群與實務經驗強烈建議:升級時應循序漸進(Gradual Upgrade)。例如從 10.5 升到 11.4,最安全的做法是依序經歷 10.5 ➔ 10.6 ➔ 10.11 ➔ 11.4。如果直接跳版,極易發生非預期的資料格式衝突。
痛點 2:手動啟動臨時容器與執行 mariadb-upgrade 流程繁瑣
每次跨版本升級,你都需要:
- 停止生產環境容器。
- 開啟指定舊版本的臨時容器,掛載 Data Volume。
- 進入容器執行
mariadb-upgrade -u root -p升級內部 Schema。 - 清理並關閉臨時容器。
- 重複以上步驟,切換到下一個目標版本。
這是一套極度耗時且容易因人為失誤(如忘記清理容器或傳錯參數)造成事故的流程。
痛點 3:缺乏升級前的防禦性備份
在升級前「手動壓縮備份 Data Directory」非常重要。許多工程師為了省時間跳過備份,萬一升級途中斷電或程序崩潰,原本的 Data Volume 就會變成不可讀的垃圾,只能從備份還原(如果你有的話)。
自動化救援神兵:mariadb-upgrade.sh
為了徹底解決上述繁瑣流程,我撰寫了一套自動化 Bash 工具 mariadb-upgrade.sh。這個腳本主要結合了 Docker 容器化運算與自動化階段升級機制,讓原本需要花半小時人工盯盤的流程,縮短為一行指令!
腳本核心特色
- 自動打成
.tar.gz壓檔備份:升級前自動隔離檔案並建立全量備份。 - 支援多重指定
-u(漸進式升級):依照傳入的版號順序,自動輪流起臨時容器完成關聯升級。 - 完全隔離與自動清理:使用獨立的臨時容器名稱,完成後自動清除不留垃圾。
完整 Bash 腳本原始碼 (Script Source Code)
你可以直接複製以下程式碼並儲存為 mariadb-upgrade.sh,接著給予執行權限即可使用:
chmod +x mariadb-upgrade.sh#!/usr/bin/env bash
# Program:
# Auto upgrade MariaDB data directories across major versions.
# History:
# 2026-09-19 king First release
# Contact:
# [email protected]
set -euo pipefail
# Script name for usage output
# 腳本名稱,用於說明輸出
COMMAND_NAME="$(basename "$0")"
# Display help and usage information
# 顯示幫助與說明資訊
displayHelp() {
cat << EOF
Auto upgrade MariaDB data directory across target versions.
Usage:
$COMMAND_NAME [flags]
Examples:
# Backup only
$COMMAND_NAME -b mysql_backup -d /var/lib/mysql
# Gradual upgrade with explicit mariadb-upgrade execution
$COMMAND_NAME -b mysql_backup -d /var/lib/mysql -p "my_secret_pass" -c my_upgrader -u 10.6 -u 10.11 -u 11.4
Flags:
-b, --backup FILE Create a .tar.gz backup archive before performing upgrades.
-c, --container NAME Temporary Docker container name for upgrade (default: mariadb_upgrade_container).
-d, --data PATH MariaDB data directory path (default: /var/lib/mysql).
-p, --password PASS Root password for the MariaDB database (if configured).
-u, --upgrade VER Specify target version to upgrade to (can be used multiple times).
-h, --help Help for $COMMAND_NAME
EOF
}
# Initialize default variable values
# 初始化預設變數值
DATA_DIR='/var/lib/mysql'
BACKUP_FILE=""
CONTAINER_NAME="mariadb_upgrade_container"
ROOT_PASSWORD=""
UPGRADE_VERSIONS=()
# Portable argument parsing loop (supports both short and long flags across OS platforms)
# 可攜式引數解析迴圈(跨作業系統平台支援短選項與長選項)
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
displayHelp
exit 0
;;
-b|--backup)
# Ensure a backup filename argument was provided
# 確保有提供備份檔案名稱引數
if [[ -z "${2:-}" || "${2:-}" == -* ]]; then
echo "Error: Option '$1' requires a valid target backup filename." >&2
# 錯誤:選項 '$1' 需要有效的目標備份檔案名稱。
exit 1
fi
BACKUP_FILE="$2"
shift 2
;;
-c|--container)
# Ensure a container name argument was provided
# 確保有提供容器名稱引數
if [[ -z "${2:-}" || "${2:-}" == -* ]]; then
echo "Error: Option '$1' requires a valid container name argument." >&2
# 錯誤:選項 '$1' 需要有效的容器名稱引數。
exit 1
fi
CONTAINER_NAME="$2"
shift 2
;;
-d|--data)
# Ensure a data directory path argument was provided
# 確保有提供資料目錄路徑引數
if [[ -z "${2:-}" || "${2:-}" == -* ]]; then
echo "Error: Option '$1' requires a non-empty directory path argument." >&2
# 錯誤:選項 '$1' 需要非空的路徑引數。
exit 1
fi
DATA_DIR="$2"
shift 2
;;
-p|--password)
# Ensure a password argument was provided
# 確保有提供密碼引數
if [[ -z "${2:-}" || "${2:-}" == -* ]]; then
echo "Error: Option '$1' requires a password value." >&2
# 錯誤:選項 '$1' 需要密碼數值。
exit 1
fi
ROOT_PASSWORD="$2"
shift 2
;;
-u|--upgrade)
# Ensure a version argument value was provided
# 確保有提供版本引數值
if [[ -z "${2:-}" || "${2:-}" == -* ]]; then
echo "Error: Option '$1' requires a version argument (e.g., 10.11)." >&2
# 錯誤:選項 '$1' 需要版本引數。
exit 1
fi
UPGRADE_VERSIONS+=("$2")
shift 2
;;
--)
# End of all options
# 所有選項解析結束
shift
break
;;
-*)
echo "Error: Unknown option '$1' for $COMMAND_NAME" >&2
# 錯誤:未知的選項 '$1'
echo "Run '$COMMAND_NAME --help' for usage." >&2
exit 1
;;
*)
# Positional arguments handling
# 位置引數處理
shift
;;
esac
done
# Check if at least one operation (backup or upgrade) is specified
# 檢查是否至少指定了一項操作(備份或升級)
if [[ -z "$BACKUP_FILE" && "${#UPGRADE_VERSIONS[@]}" -eq 0 ]]; then
echo "Error: No action specified. Please provide -b/--backup or -u/--upgrade." >&2
# 錯誤:未指定任何操作,請提供 -b/--backup 或 -u/--upgrade。
echo "Run '$COMMAND_NAME --help' for usage." >&2
exit 1
fi
# Convert DATA_DIR to absolute path for Docker volume mounting
# 將 DATA_DIR 轉換為絕對路徑以供 Docker 磁碟卷掛載使用
DATA_DIR="$(cd "$(dirname "$DATA_DIR")" && pwd)/$(basename "$DATA_DIR")"
# Automatically append .tar.gz extension if omitted by the user
# 若使用者未指定 .tar.gz 後綴,則自動補全
if [[ -n "$BACKUP_FILE" && ! "$BACKUP_FILE" =~ \.tar\.gz$ ]]; then
BACKUP_FILE="${BACKUP_FILE}.tar.gz"
fi
# Print configuration summary for debugging
# 印出設定摘要以供偵錯
echo "=== Script Configuration ==="
# === 腳本設定摘要 ===
if [[ -n "$BACKUP_FILE" ]]; then
echo "Backup File : $BACKUP_FILE"
# 備份檔案名稱
else
echo "Backup Enabled : Disabled"
# 備份狀態:未啟用
fi
echo "Data Directory : $DATA_DIR"
# 資料目錄
echo "Container Name : $CONTAINER_NAME"
# 容器名稱
if [[ -n "$ROOT_PASSWORD" ]]; then
echo "Password Set : Yes"
# 密碼狀態:已設定
else
echo "Password Set : No (Using Allow Empty Password)"
# 密碼狀態:未設定(使用允許空密碼模式)
fi
if [[ "${#UPGRADE_VERSIONS[@]}" -gt 0 ]]; then
echo "Upgrade Chain : ${UPGRADE_VERSIONS[*]}"
# 升級鏈結
else
echo "Upgrade Chain : None"
# 升級鏈結:無
fi
echo
# Execute backup process if specified
# 如果有指定備份檔案,則執行備份流程
if [[ -n "$BACKUP_FILE" ]]; then
# Verify that the data directory exists before backup
# 備份前先檢查資料目錄是否存在
if [[ ! -d "$DATA_DIR" ]]; then
echo "Error: Data directory '$DATA_DIR' does not exist. Cannot create backup." >&2
# 錯誤:資料目錄 '$DATA_DIR' 不存在,無法建立備份。
exit 1
fi
echo "[INFO] Creating database backup archive..."
# [資訊] 正在建立資料庫備份壓縮檔...
# Extract parent directory and base directory name for clean archive extraction
# 提取父目錄與基底目錄名稱,確保打包解壓時路徑乾淨
PARENT_DIR="$(dirname "$DATA_DIR")"
TARGET_DIR="$(basename "$DATA_DIR")"
# Create .tar.gz backup from parent directory context
# 從父目錄切換並建立 .tar.gz 備份檔
tar -zcf "$BACKUP_FILE" -C "$PARENT_DIR" "$TARGET_DIR"
echo "[INFO] Backup completed successfully: $BACKUP_FILE"
# [資訊] 備份成功完成:$BACKUP_FILE
echo
fi
# Execute database upgrade sequence if upgrade versions are specified
# 若有指定升級版本,則執行資料庫升級流程
if [[ "${#UPGRADE_VERSIONS[@]}" -gt 0 ]]; then
echo "[INFO] Starting database upgrade sequence..."
# [資訊] 開始執行資料庫升級流程...
for VERSION in "${UPGRADE_VERSIONS[@]}"; do
echo "--------------------------------------------------"
# --------------------------------------------------
echo "[INFO] Upgrading MariaDB data to target version: $VERSION"
# [資訊] 正在將 MariaDB 資料升級至目標版本:$VERSION
echo "--------------------------------------------------"
# Cleanup existing leftover container with the same name if present
# 清理若先前殘留的同名容器
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
echo "[INFO] Starting MariaDB $VERSION container ('$CONTAINER_NAME')..."
# [資訊] 啟動 MariaDB $VERSION 容器 ('$CONTAINER_NAME')...
# Prepare Docker environment variables based on password configuration
# 根據密碼設定準備 Docker 環境變數選項
ENV_OPTS=()
if [[ -n "$ROOT_PASSWORD" ]]; then
ENV_OPTS+=("-e" "MARIADB_ROOT_PASSWORD=$ROOT_PASSWORD")
else
ENV_OPTS+=("-e" "MARIADB_ALLOW_EMPTY_ROOT_PASSWORD=1")
fi
# Start MariaDB service container in background
# 在背景啟動 MariaDB 服務容器
docker run -d \
--name "$CONTAINER_NAME" \
-v "$DATA_DIR":/var/lib/mysql \
"${ENV_OPTS[@]}" \
"mariadb:$VERSION" >/dev/null
echo "[INFO] Waiting for MariaDB service to become ready..."
# [資訊] 等待 MariaDB 服務啟動就緒...
# Poll the container until health check passes (mariadb-admin ping succeeds)
# 輪詢容器直到健康檢查通過(mariadb-admin ping 成功)
MAX_RETRIES=30
RETRY_COUNT=0
SERVER_READY=false
while [[ $RETRY_COUNT -lt $MAX_RETRIES ]]; do
PING_CMD=("mariadb-admin" "ping" "-u" "root" "--silent")
if [[ -n "$ROOT_PASSWORD" ]]; then
PING_CMD+=("-p$ROOT_PASSWORD")
fi
# Direct evaluation inside 'if' prevents 'set -e' from terminating the script on non-zero exit codes
# 在 'if' 內直接求值可防止非零狀態碼引發 'set -e' 終止腳本
if docker exec "$CONTAINER_NAME" "${PING_CMD[@]}" >/dev/null 2>&1; then
SERVER_READY=true
break
fi
sleep 2
RETRY_COUNT=$((RETRY_COUNT + 1))
done
if [[ "$SERVER_READY" == "false" ]]; then
echo "Error: MariaDB $VERSION container failed to start within timeout." >&2
# 錯誤:MariaDB $VERSION 容器未能在時限內成功啟動。
echo "Printing container logs for diagnosis:" >&2
# 印出容器日誌以供診斷:
docker logs "$CONTAINER_NAME" >&2
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
exit 1
fi
echo "[INFO] Executing mariadb-upgrade manually inside container..."
# [資訊] 在容器內手動執行 mariadb-upgrade...
# Construct mariadb-upgrade command array
# 構建 mariadb-upgrade 指令陣列
UPGRADE_CMD=("mariadb-upgrade" "-u" "root")
if [[ -n "$ROOT_PASSWORD" ]]; then
UPGRADE_CMD+=("-p$ROOT_PASSWORD")
fi
# Execute mariadb-upgrade directly inside 'if' to control error behavior cleanly
# 在 'if' 中直接執行 mariadb-upgrade 以乾淨掌控錯誤行為
if docker exec "$CONTAINER_NAME" "${UPGRADE_CMD[@]}"; then
echo "[INFO] mariadb-upgrade executed successfully for version $VERSION."
# [資訊] 版本 $VERSION 之 mariadb-upgrade 執行成功。
else
echo "Error: mariadb-upgrade failed for MariaDB $VERSION." >&2
# 錯誤:MariaDB $VERSION 之 mariadb-upgrade 執行失敗。
docker stop "$CONTAINER_NAME" >/dev/null 2>&1 || true
docker rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
exit 1
fi
echo "[INFO] Safely stopping container to flush buffer pools to disk..."
# [資訊] 安全停止容器以確保記憶體緩衝區資料完整寫回磁碟...
# Stop and remove upgrade container safely
# 安全停止並清理升級容器
docker stop "$CONTAINER_NAME" >/dev/null
docker rm "$CONTAINER_NAME" >/dev/null
echo "[INFO] Successfully upgraded data directory to MariaDB $VERSION"
# [資訊] 成功將資料目錄升級至 MariaDB $VERSION
echo
done
echo "[SUCCESS] All MariaDB version upgrades completed successfully! ${UPGRADE_VERSIONS[*]}"
# [成功] 所有 MariaDB 版本升級流程順利完成!
fi實作與範例使用
我們直接來看看 mariadb-upgrade.sh 的指令用法與參數規格:
Bash
Auto upgrade MariaDB data directory across target versions.
Usage:
mariadb-upgrade.sh [flags]
Flags:
-b, --backup FILE Create a .tar.gz backup archive before performing upgrades.
-c, --container NAME Temporary Docker container name for upgrade (default: mariadb_upgrade_container).
-d, --data PATH MariaDB data directory path (default: /var/lib/mysql).
-p, --password PASS Root password for the MariaDB database (if configured).
-u, --upgrade VER Specify target version to upgrade to (can be used multiple times).
-h, --help Help for mariadb-upgrade.sh
情境範例 1:升級前的資料安全備份
如果你只是想在執行任何危險操作前,先為 Data Directory 做一次完整的打包備份:
Bash
mariadb-upgrade.sh -b mysql_backup -d /var/lib/mysql
關鍵說明:這會自動把
/var/lib/mysql資料打包成mysql_backup.tar.gz,方便後續還原。
情境範例 2:跨多個 LTS 版本漸進式自動升級
假設你的 MariaDB Data Directory 目前停留在 10.5 版,目標是升級到全新的 11.4,最佳實踐是依序過渡 10.6 與 10.11。你可以直接傳入多個 -u 參數:
Bash
mariadb-upgrade.sh \
-b mysql_backup_before_11_4 \
-d /var/lib/mysql \
-p "my_secret_pass" \
-c mariadb_upgrade_worker \
-u 10.6 \
-u 10.11 \
-u 11.4
腳本內部自動化的運作邏輯
- 備份階段:先確認路徑權限,並將
/var/lib/mysql打包至mysql_backup_before_11_4.tar.gz。 - 第一階段升級 (10.6):啟動
mariadb:10.6臨時容器,掛載資料路徑,等待資料庫 Ready 後自動執行mariadb-upgrade檢查並修復 Schema,完成後關閉並移除容器。 - 第二階段升級 (10.11):接著自動以
mariadb:10.11重複上述步驟。 - 最後階段升級 (11.4):順利升級至
11.4結構。升級完成!
常見坑點與 Troubleshooting
在使用本腳本或進行 MariaDB 升級時,建議注意以下幾點:
- 務必先停止正在讀寫該 Data Directory 的原容器
- 坑點:若原本的 MariaDB 容器仍在運行中,多個容器同時存取同一份 Data Volume 會造成 InnoDB 資料頁損毀。
- 解決方案:執行腳本前,請先執行
docker compose down或docker stop <your-mariadb-container>。
- 記憶體與權限問題
- 坑點:升級過程中臨時容器需要對
/var/lib/mysql寫入新結構,若檔名權限(Chown)錯亂會導致啟動失敗。 - 解決方案:確保執行腳本的使用者擁有讀寫資料目錄與執行
docker的權限(如sudo權限)。
- 坑點:升級過程中臨時容器需要對
- 版本選擇不可逆
- 坑點:MariaDB 的 Data Directory 無法降級(Downgrade)。一旦你升級到
11.4,就無法直接拿這份資料放回10.6的容器。 - 解決方案:幸好腳本預設要求
-b參數,若升級後應用程式有相容性問題,可以立刻拿.tar.gz解壓縮還原至升級前的狀態。
- 坑點:MariaDB 的 Data Directory 無法降級(Downgrade)。一旦你升級到
總結與 Takeaways
資料庫升級往往是維運中最讓人膽戰心驚的一環,但只要建立良好的自動化機制,就能把風險降至最低:
- 防禦第一:永遠在升級前進行物理備份(.tar.gz 或快照)。
- 循序漸進:跨大版本升級時,切忌貪快跳版,應依序經歷中介 LTS 版本。
- 工具化維運:透過
mariadb-upgrade.sh這種 Shell 腳本自動處理容器啟閉與修復,避免人工踩雷。
這套腳本非常適合整合進 CI/CD 或系統維護 Cronjob 中。如果你在維運 MariaDB 時也遇到過升級痛點,不妨嘗試看看這套腳本!