首頁/資料庫

在 Ubuntu 上檢查 Docker 版 SQL Server 的狀態

2026年09月29日 資料庫 SQL Server Docker Ubuntu Linux 核心 sqlcmd 資料庫管理

有台 Ubuntu 伺服器上的 SQL Server,我想確認它是不是正常。網路上查到的做法幾乎都是 systemctl status mssql-server,但這台的 SQL Server 是用 Docker 跑的,主機上根本沒有這個服務,照查只會得到一行:

Unit mssql-server.service could not be found.

我第一個懷疑是 Docker 沒啟動。結果 Docker 好好的,容器也都在跑。接著用 DataGrip 連進去,資料庫卻一直顯示「正在復原」。我以為是剛開機的正常現象,等了 40 多分鐘都沒好,最後才查到是 Linux 核心卡住,SQL Server 送出去的讀取請求根本沒有送到磁碟。

這篇把整個過程整理成固定的檢查順序:Docker、容器、SQL Server 日誌、資料庫狀態、對外連線。前一關沒過,後面查了也沒意義。後半段記錄「復原一直沒完成」時,怎麼判斷該繼續等還是該動手。

檢查順序:Docker 服務、容器狀態、SQL Server 就緒、資料庫狀態、對外連線,每一關沒過時的處理方式

文中的主機位址、容器名稱、主機連接埠、資料庫名稱與路徑都是示例,終端機輸出也已匿名化,操作時請換成自己環境的值:

項目 示例值
主機位址 192.0.2.10
容器名稱 example_mssql
映像檔 mcr.microsoft.com/mssql/server:2019-latest
連接埠對應 主機 14330 → 容器 1433
資料庫名稱 example_app_db、example_report_db
主機上的資料目錄 /path/to/mssql-data

先確認 Docker 有沒有在跑

sudo systemctl status docker

看到 active (running) 再往下。如果是 inactive 或 failed,先啟動,順便設定開機自動啟動:

sudo systemctl start docker
sudo systemctl enable docker

enable 不能省。少了它,下次主機重開,Docker 和底下所有容器都不會自己起來。

docker ps 要加 -a

sudo docker ps -a

不加 -a 的話,只會列出執行中的容器。SQL Server 容器如果停了,清單裡根本不會出現,很容易誤以為這台沒裝。

預設輸出的欄位很多,一行塞不下,可以只挑需要的欄位:

sudo docker ps -a --format 'table {{.Names}}\t{{.Image}}\t{{.Status}}\t{{.Ports}}'

以下是節錄的輸出,已經匿名化,容器名稱和連接埠都換成示例值:

NAMES              IMAGE                                        STATUS         PORTS
example_redis      redis:7                                      Up 6 minutes   0.0.0.0:6379->6379/tcp, [::]:6379->6379/tcp
example_mssql      mcr.microsoft.com/mssql/server:2019-latest   Up 6 minutes   0.0.0.0:14330->1433/tcp, [::]:14330->1433/tcp
example_postgres   postgres:16                                  Up 6 minutes   0.0.0.0:5432->5432/tcp, [::]:5432->5432/tcp

IMAGE 是 mcr.microsoft.com/mssql/server 的那一列就是 SQL Server。STATUS 的看法:

狀態 意思
Up 6 minutes 正在執行,後面是這次啟動了多久
Exited (0) 正常停止,通常是有人下過 docker stop
Exited (1) 或其他非 0 值 啟動失敗或異常結束,要看日誌
Restarting 一直啟動失敗,又被重啟策略拉起來,一樣要看日誌

當時所有容器都是 Up 6 minutes,表示它們幾乎同時啟動,這台剛重開過。這個線索後面會用到。

容器停了的話,直接啟動:

sudo docker start example_mssql

用 docker compose 建立的容器,到 docker-compose.yml 所在的目錄執行:

sudo docker compose ps -a
sudo docker compose up -d

14330 跟 1433 不是同一個埠

PORTS 欄位的 0.0.0.0:14330->1433/tcp 很容易看錯。箭頭左邊是主機的連接埠,右邊是容器裡的連接埠。SQL Server 在容器裡照樣監聽 1433,但從主機外面要連 14330。

DataGrip 連主機的 14330,Docker 再轉送到容器內 SQL Server 監聽的 1433

只想看這個容器的對應,可以用:

sudo docker port example_mssql
1433/tcp -> 0.0.0.0:14330
1433/tcp -> [::]:14330

所以 DataGrip、SSMS 或應用程式的連線字串,伺服器都要寫成 192.0.2.10,14330。SQL Server 用逗號分隔主機和連接埠,不是冒號。沿用預設的 1433 會直接連不上。

容器 Up 了,SQL Server 不一定好了

Up 只代表容器裡的程序開始跑了,SQL Server 本身可能還在初始化。要看日誌:

sudo docker logs --tail 50 example_mssql

出現這一行,SQL Server 才開始接受連線:

SQL Server is now ready for client connections. This is an informational message; no user action is required.

想持續看的話加 -f,按 Ctrl+C 離開:

sudo docker logs -f example_mssql

容器起不來的時候,原因幾乎都寫在日誌裡,常見的有四種:

日誌裡的訊息 原因 處理方式
The SQL Server End-User License Agreement (EULA) must be accepted 沒有接受授權條款 建立容器時加上環境變數 ACCEPT_EULA=Y
Password validation failed SA 密碼不符合複雜度規則 MSSQL_SA_PASSWORD 至少 8 碼,大寫、小寫、數字、符號四類中要有三類
This program requires a machine with at least 2000 megabytes of memory 記憶體不足 SQL Server 至少要 2 GB,用 free -h 看剩多少
Access is denied 掛載目錄的權限不對 見下方說明

權限的問題是因為 2019 之後的映像檔不再用 root 執行,而是用 UID 10001 的 mssql 帳號。把主機目錄掛到 /var/opt/mssql 時,擁有者要改成這個 UID:

sudo chown -R 10001:0 /path/to/mssql-data

授權和密碼是建立容器時傳入的環境變數,已經建好的容器改不了,只能刪掉重建。刪之前一定要確認資料放在掛載的磁碟區或主機目錄,不然資料庫會跟著容器一起刪掉:

sudo docker inspect example_mssql --format '{{json .Mounts}}'

輸出裡要看得到 Destination 是 /var/opt/mssql 的項目。

資料庫顯示正在復原

Docker、容器、日誌都沒問題,我用 DataGrip 連進去查詢,卻一直收到這個錯誤(資料庫名稱已換成示例):

Database 'example_app_db' is being recovered. Waiting until recovery is finished.

這本身不代表資料庫壞了。SQL Server 每次啟動都會對每個資料庫做一次復原:把已經提交、但還沒寫進資料檔的交易重新套用,並取消執行到一半的交易。做完之前,資料庫的狀態是 RECOVERING,所有查詢都會收到這個錯誤。主機剛重開,遇到復原很正常,資料庫越大要等越久。

狀態可以直接查:

SELECT name, state_desc FROM sys.databases;

變成 ONLINE 就能用了。在那之前不要急著重啟容器,重啟不會比較快,復原反而要從頭再跑一次。

問題是,要等多久才算不正常?我一開始也只是等,等到第 40 分鐘才覺得不對。

等多久算不正常:看日誌有沒有進度

正常的復原是有進度的。每個資料庫會依序出現開始、重新套用交易、完成的紀錄,時間比較長的還會定時印出百分比。用這個指令只看復原相關的紀錄:

sudo docker logs example_mssql 2>&1 | grep -E "Starting up database|Parallel redo|Recovery of database|Recovery completed"

我這台的輸出節錄如下,已去除時間並換成示例名稱:

Starting up database 'example_report_db'.
Starting up database 'example_app_db'.
Parallel redo is started for database 'example_report_db' with worker pool size [6].
Parallel redo is started for database 'example_app_db' with worker pool size [6].

Parallel redo is started 表示開始重新套用交易,正常情況下接著會出現 Parallel redo is shutdown,代表這個階段做完了。同一台主機上的另一個大資料庫很快就出現了 shutdown,這兩個小資料庫卻停在 started 之後,什麼都沒有了。

取而代之的是每隔幾分鐘出現一次的這行(檔案識別值與位移已省略):

SQL Server has encountered 20 occurrence(s) of I/O requests taking longer than 15 seconds to complete on file [/var/opt/mssql/data/example_app_db_log.ldf] in database id 5.  The OS file handle is <省略>.  The offset of the latest long I/O is: <省略>.  The duration of the long I/O is: 2721229 ms.

重點在最後的 duration。2,721,229 ms 大約是 45 分鐘,而且這個數字每次出現都在變大,位移卻一直是同一個。意思是:有一筆讀取從 SQL Server 啟動的那一刻送出去,到現在都還沒回來。復原要讀交易記錄檔才能往下走,這筆讀取不回來,復原就永遠停在原地。同樣的訊息也出現在系統資料庫 model 的記錄檔上。

日誌裡還有大量這種錯誤,每 20 秒一次:

Error: 1222, Severity: 16, State: 24.
Lock request time out period exceeded.

spid 後面帶 s 的是 SQL Server 自己的背景工作。它們要的鎖被卡住的復原占著,拿不到就逾時。這是結果不是原因,不用另外處理。

看到這兩種訊息,就不用再等了,要去查 I/O 為什麼不回來。

I/O 卡了 45 分鐘,磁碟卻很閒

直覺會先懷疑磁碟壞了或滿了。我查了一輪,結果剛好相反:

df -h /path/to/mssql-data          # 空間只用了 44%
df -i /path/to/mssql-data          # inode 用了 2%
vmstat 1 5                          # wa 是 0,b 是 0

vmstat 的 wa 是 CPU 花在等 I/O 的比例,b 是卡在 I/O 的程序數,兩個都是 0。再用 /proc/diskstats 取樣 5 秒,NVMe SSD 的寫入延遲 0.6 ms,進行中的 I/O 是 0,忙碌率 0%。磁碟閒得很。

真正的線索在「不可中斷等待」的程序。這種程序的狀態是 D,通常是在等磁碟,但 WCHAN 欄位會告訴你它卡在哪個核心函式:

ps -eo pid,stat,etime,wchan:32,comm | awk 'NR == 1 || $2 ~ /^D/'
    PID STAT     ELAPSED WCHAN                            COMMAND
     93 DN         43:59 flush_work.isra.0                khugepaged
   3108 Ds         38:36 synchronize_rcu_expedited        systemd-timedat
   3657 Ds         31:24 exp_funnel_lock                  (fwupdmgr)
   4501 Dl         16:10 exp_funnel_lock                  runc

如果是在等磁碟,WCHAN 會是 io_schedule、blk_ 或 nvme_ 開頭的函式。這裡一個都沒有,全部卡在 synchronize_rcu_expedited、exp_funnel_lock 這類 RCU 相關的函式。RCU 是核心內部用來協調同時存取的機制。它停住的話,所有需要它的工作都會跟著停住,I/O 的完成處理也可能包含在內。

khugepaged 的 ELAPSED 跟開機時間一樣長,表示從開機就卡住了。回頭翻 dmesg,開機後約 10 秒有一段 Call Trace,之後就再也沒有恢復。

SQL Server 送出的 I/O 停在核心,沒有送到磁碟;同一時間 NVMe SSD 的延遲與忙碌率都很低

所以 SQL Server 回報的「I/O 很慢」,實際上是請求停在核心裡,根本沒有送到磁碟,也不會逾時失敗。這種狀態不會自己恢復。

我的診斷腳本也被卡住了

為了一次收集這些資訊,我準備了一支診斷腳本,裡面用 docker exec 進容器找 sqlcmd 查資料庫狀態。第一次跑完,腳本回報「容器內找不到 sqlcmd」。

這是誤判。上面清單裡最後那個 runc,開始卡住的時間正好是我跑腳本的那一刻。docker exec 要靠 runc 在容器裡建立新程序,而 runc 也需要 RCU,於是跟著卡住。腳本只等了 10 秒就放棄,拿到空的結果,就當成找不到。

這裡學到兩件事。第一,核心卡住的時候,docker exec、docker stop 這類需要 runc 的指令都可能卡住,而且卡住的 runc 殺不掉,每多跑一次就多一個。第二,狀態 D 的程序連 kill -9 都不理,timeout 也沒用。腳本裡的 df、ls 要是剛好碰到卡住的檔案系統,整支腳本就停在那裡。

後來腳本改成把每個指令丟到背景執行,超過時間就放棄等待,繼續下一項:

# run <秒數> <指令字串>:超過時間就不等了,記下來繼續下一項
run() {
  local t=$1 cmd=$2 i pid
  bash -c "$cmd" >"$TMP/last" 2>&1 &
  pid=$!
  for ((i = 0; i < t * 10; i++)); do
    kill -0 "$pid" 2>/dev/null || break
    sleep 0.1
  done
  if kill -0 "$pid" 2>/dev/null; then
    kill -9 "$pid" 2>/dev/null
    cat "$TMP/last"
    warn "指令超過 ${t} 秒沒有回應,可能卡在 I/O:$cmd"
    return 124
  fi
  wait "$pid"
  cat "$TMP/last"
}

run 10 "df -h /path/to/mssql-data"

背景的指令如果真的卡在 D 狀態,kill -9 一樣殺不掉,但主程式不用等它,可以繼續收集其他資訊。「這個指令沒有回應」本身也是一條線索。

docker exec 那一段則改成在輸出最後多印一個 END。沒收到 END 就是 exec 沒有回應,跟「真的找不到 sqlcmd」分開判斷。

磁碟取樣沒有用 iostat,因為很多主機沒裝 sysstat,改成直接讀 /proc/diskstats 兩次相減:

# /proc/diskstats 欄位:3 名稱、4 讀取次數、7 讀取毫秒、8 寫入次數、11 寫入毫秒、12 進行中、13 忙碌毫秒
snap() { awk '$3 !~ /^(loop|ram|sr|zram)/ {print $3, $4, $7, $8, $11, $12, $13}' /proc/diskstats; }
snap >"$TMP/d1"; sleep 5; snap >"$TMP/d2"

awk -v s=5 '
  NR == FNR { r[$1] = $2; rms[$1] = $3; w[$1] = $4; wms[$1] = $5; busy[$1] = $7; next }
  ($1 in r) {
    dr = $2 - r[$1]; dw = $4 - w[$1]
    ra = dr > 0 ? ($3 - rms[$1]) / dr : 0
    wa = dw > 0 ? ($5 - wms[$1]) / dw : 0
    flag = ($6 > 0 && dr == 0 && dw == 0) ? "卡住" : ""
    printf "%-12s 讀取延遲 %.1f ms,寫入延遲 %.1f ms,進行中 %d,忙碌率 %.0f%%  %s\n",
           $1, ra, wa, $6, ($7 - busy[$1]) / (s * 10), flag
  }' "$TMP/d1" "$TMP/d2"

總延遲毫秒數除以完成次數就是平均延遲。「有進行中的 I/O,5 秒內卻一筆都沒完成」會標成卡住,那才是磁碟本身卡住的樣子。這次的結果是全部正常,反過來證明問題不在磁碟。

只能重開主機

核心卡在這種狀態,重啟容器沒有用,只能重開主機。我的做法是:

# 1. 先把核心日誌存下來,重開就看不到了
sudo dmesg -T > ~/dmesg-before-reboot.txt

# 2. 其他容器先正常停掉,免得它們也要做復原;docker stop 需要 runc,加 timeout 以免卡住
sudo timeout 180 docker stop -t 60 example_postgres example_redis

# 3. 重開
sudo reboot

SQL Server 的容器不用特別處理。卡住的 I/O 在核心裡,docker stop 也等不到它結束。那兩筆讀寫從頭到尾沒有寫到磁碟,重開的效果跟斷電差不多,SQL Server 會用交易記錄檔處理。

有程序卡在 D 狀態時,關機流程可能停住。如果 5 分鐘後還沒重開,可以用核心的緊急功能,依序同步寫入、把檔案系統改成唯讀、重開:

echo 1 | sudo tee /proc/sys/kernel/sysrq
echo s | sudo tee /proc/sysrq-trigger
echo u | sudo tee /proc/sysrq-trigger
echo b | sudo tee /proc/sysrq-trigger

重開之後,兩個資料庫的復原紀錄變成這樣(已去除時間並換成示例名稱):

Starting up database 'example_report_db'.
Starting up database 'example_app_db'.
Parallel redo is started for database 'example_report_db' with worker pool size [6].
Parallel redo is started for database 'example_app_db' with worker pool size [6].
Parallel redo is shutdown for database 'example_report_db' with worker pool size [6].
Parallel redo is shutdown for database 'example_app_db' with worker pool size [6].

從 started 到 shutdown 只花了 0.13 秒。之前卡了 45 分鐘的復原,I/O 正常的時候一眨眼就做完了。

重開後還要確認三件事:沒有程序卡在 D 狀態、日誌沒有新的長時間 I/O、每個資料庫都是 ONLINE。經歷過這種異常關機,也建議對受影響的資料庫跑一次一致性檢查:

DBCC CHECKDB('example_app_db') WITH NO_INFOMSGS;

沒有輸出就代表沒有發現問題。

原因還沒查到

核心為什麼會在開機時卡住,目前還不知道。能確定的是:

  • 這台用同一版核心,上一次開機正常跑了兩天,關機也是正常關機,所以不是「換了新核心就壞」。

  • 磁碟、空間、記憶體都沒有異常,dmesg 裡也沒有 NVMe 錯誤或重設的紀錄。

  • 問題從開機後約 10 秒那段 Call Trace 開始。

要往下查,就要看那段 Call Trace 前面印了什麼。重開前存下的 dmesg 可以這樣找:

grep -B 30 -A 40 'Call Trace' ~/dmesg-before-reboot.txt | head -120

沒存到的話,journald 可能還保留上一次開機的核心紀錄:

sudo journalctl -k -b -1 --no-pager | grep -B 30 -A 40 'Call Trace' | head -120

如果之後又發生,可以在 GRUB 選單的「Advanced options for Ubuntu」改用上一版核心開機,比對是不是核心本身的問題。

用 sqlcmd 從容器裡面連一次

平常要確認 SQL Server 和帳密,從容器內部連線最直接,可以先排除網路和連接埠的問題:

sudo docker exec -it example_mssql /opt/mssql-tools18/bin/sqlcmd \
  -S localhost -U sa -C -Q "SELECT name, state_desc FROM sys.databases"

這段有四個地方要注意:

  • 我刻意沒加 -P。sqlcmd 會提示你輸入密碼,密碼就不會留在 shell 的指令紀錄裡。

  • 較新的 2019 和 2022 映像檔改用 mssql-tools18。如果出現找不到檔案,表示是較舊的映像檔,改用 /opt/mssql-tools/bin/sqlcmd,並拿掉 -C。

  • mssql-tools18 預設會加密連線,容器用的是自簽憑證,不加 -C 會出現 certificate verify failed。

  • docker exec 沒有任何回應時,先照前面的方法看有沒有程序卡在 D 狀態,不要一直重試。

連進去之後,也可以順便查 SQL Server 的啟動時間,跟 docker ps 的 Up 時間對照:

SELECT sqlserver_start_time FROM sys.dm_os_sys_info;

容器裡連得上,外面連不上

問題就在網路這一段。從另一台機器測試主機的 14330:

nc -zv 192.0.2.10 14330

出現 succeeded 或 open,表示連接埠是通的,要回頭檢查連線字串的連接埠和帳密。連不通的話,就要檢查主機所在網路的防火牆,或雲端主機的安全群組。

Docker 對外開放的連接埠是直接寫進 iptables,不會經過 ufw 的規則。所以 ufw status 裡看不到 14330,不代表外面連不到。

讓容器跟著主機一起起來

主機重開後容器沒有自己起來,多半是沒設定重啟策略。先查目前的設定:

sudo docker inspect example_mssql --format '{{.HostConfig.RestartPolicy.Name}}'

如果是 no,改成 unless-stopped:

sudo docker update --restart unless-stopped example_mssql

unless-stopped 會在 Docker 啟動時自動把容器拉起來,但手動 docker stop 過的不會。用 compose 的話,在 service 底下加上 restart: unless-stopped。我這台重開後所有容器都自己起來了,表示這項本來就有設定。

忘記 SA 密碼的時候

SA 密碼是建立容器時用環境變數傳進去的,會用明文存在容器的設定裡:

sudo docker inspect example_mssql --format '{{range .Config.Env}}{{println .}}{{end}}' | grep -i pass

要注意,這只是建立容器當時的密碼。如果之後在 SQL Server 裡改過 SA 密碼,這裡查到的會是舊的。

反過來說,這也代表任何能下 docker 指令的帳號,都查得到 SA 密碼。docker 群組的成員要管好,應用程式也不應該直接用 SA 連線。

常見狀況對照

現象 原因 解法
systemctl status mssql-server 找不到服務 SQL Server 跑在 Docker 容器裡 改用 docker ps -a 查
docker ps 看不到 SQL Server 容器 容器已停止,預設不列出 加上 -a
狀態是 Exited (1) 或 Restarting SQL Server 啟動失敗 用 docker logs 看錯誤訊息
查詢時回應 Database '...' is being recovered 啟動後的復原還沒完成 查日誌的復原紀錄有沒有進度
復原停在 Parallel redo is started,日誌出現 taking longer than 15 seconds 有 I/O 一直沒有完成 查磁碟延遲與狀態 D 的程序
I/O 回報很慢,磁碟延遲卻很低,程序卡在 RCU 相關函式 核心卡住,I/O 沒有送到磁碟 存下 dmesg 後重開主機
docker exec 沒有回應 runc 被核心問題卡住 不要重試,先查狀態 D 的程序
日誌大量出現 Error: 1222 背景工作拿不到被復原占著的鎖 處理復原的問題即可
資料庫停在 SUSPECT 或 RECOVERY_PENDING 資料檔讀不到或磁碟空間不夠 看日誌的錯誤訊息,用 df -h 確認磁碟
sqlcmd 回應 certificate verify failed mssql-tools18 預設加密連線 加上 -C
應用程式連 1433 連不上 主機對外的連接埠不是 1433 用 docker port 查,改連線字串

回頭看

這次卡最久的地方,是把「資料庫正在復原」當成只要等就會好。復原有沒有在動,日誌其實看得出來。停在 Parallel redo is started 之後沒有下文,又出現越來越長的 I/O 時間,就該去查 I/O 了。

另一個教訓是 WCHAN。同樣是卡在 D 狀態,卡在 io_schedule 和卡在 synchronize_rcu_expedited 是完全不同的問題。前者去查磁碟,後者查磁碟只會浪費時間。