﻿# 在 Docker 或 Kubernetes 執行 SQL Server Linux 容器應注意記憶體限制問題

前幾天一樣在客戶端進行 Kubernetes 教育訓練的時候，發現有少部分學員無法正確部署應用程式，我們的例子是一個 ASP.NET Core 網站，搭配一個 SQL Server Linux 容器，然而在練習實作 `livenessProbe` 的時候，卻發現有些人怎樣都無法將 SQL Server Linux 容器啟動，但是若將 `livenessProbe` 設定移除，服務可以啟動。若將 Pod 中**容器**的**記憶體限制**移除，服務也可以順利啟動。是不是有點詭異？這篇文章我就來說說這個來龍去脈！

### 情境描述

首先，坐在我隔壁的學員因為無法啟動 SQL Server Linux 容器，所以我先看了一下節點的記憶體用量，結果發現以下狀況：

![image](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/6d07db1be899cc4d0ed0-185651977-3031874e-5615-4a55-a44d-1730d9a8cfb9.png)

基本上他的 Node 配置了 4 GiB 記憶體，但是目前已經用了 2694 MiB 空間，佔了整體 70% 的用量，所以「理論上」記憶體還有 1.3 GiB 左右的剩餘空間才對，但一台 SQL Server Linux 容器「感覺上」只吃了 665 MiB 左右，怎麼會無法再啟動一個容器呢？

> 這裡的 Node 吃了 `2694Mi` 的原因，是因為許多 Pod 有設定 `.spec.containers[0].resources.requests.memory` 欄位，Pod 要求的記憶體用量會被節點優先保留下來，所以才會看到節點吃了這麼多記憶體。想要查詢 Kubernetes 節點的詳細記憶體用量配置，可以參考 [Phil Huang](https://blog.pichuang.com.tw/) (小飛機) 的 [如何條列出單一 Namespace 內的 Kubernetes 計算資源?](https://blog.pichuang.com.tw/20200828-kubectl-view-allocations.html) 文章說明。

接著我去看這個 SQL Server Linux 容器的 Logs，發現了一下訊息：

> Detected 820 MB of RAM. This is an informational message; no user action is required.

我看了他的 YAML 檔，發現他對 SQL Server 的 Pod 要求了 `500 MiB` 記憶體用量，限制了 `1 GiB` 的記憶體用量 (`.spec.containers[0].resources.limits.memory`)。

在問題尚未得到釐清前，我只有直覺認為他給太少記憶體了，所以觸發了 Pod 的 OOM 機制，非常有可能因為 SQL Server 啟動時剛好超過記憶體限制，所以被 `kubelet` 砍掉了！🔥

但是學員立刻提問：「可是我的 SQL Server 沒吃這麼多 RAM 啊，你看，另一個 SQL Server 只有 664 MiB 而已耶。」

### 問題重現

我為了想釐清這個問題，先用 Docker Desktop 測試一下限制 SQL Server Linux 容器的記憶體用量，看看是否可以重現相同的問題。

首先，我先用以下命令啟用一個 SQL Server 容器，並限制給予 1 GiB 的記憶體限制：

```bash
docker run -e "ACCEPT_EULA=Y" -e "MSSQL_SA_PASSWORD=Pa@@w0rd1234" -p 1433:1433 --name sql1 --hostname sql1 -d -m 1073741824 mcr.microsoft.com/mssql/server:2019-latest
```

> 注意: `1073741824` = `1` \* `1024` \* `1024` \* `1024` = `1 GiB`

接著我直接可以[重現問題](https://gist.github.com/doggy8088/2460cac9032e27f4f84f7fe5acfaa7f5)，並發現以下訊息：

> Detected `820 MB` of RAM. This is an informational message; no user action is required.

> Failed allocate pages: `FAIL_PAGE_ALLOCATION` 1

很明顯的，真的是記憶體不足造成的！

### 查詢根因

首先，我有查到在 [Microsoft SQL Server by Microsoft | Docker Hub](https://hub.docker.com/_/microsoft-mssql-server) 頁面中有寫到以下最低記憶體需求，基本上低於 `2GB` 就是有問題的！

> At least `2GB` of RAM (3.25 GB prior to 2017-CU2). Make sure to assign enough memory to the Docker VM if you're running on Docker for Mac or Windows.

當然這是根因所在，但是這個 `820 MB` 是怎麼來的呢？

我後來有查到在 [Configure SQL Server settings on Linux](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-configure-mssql-conf) 有查到一個 [Set the memory limit](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-configure-mssql-conf?view=sql-server-ver16&WT.mc_id=DT-MVP-4015686#memorylimit) 段落，裡面寫到：

> The `memory.memorylimitmb` setting controls the amount **physical memory** (in MB) available to SQL Server. The default is `80%` of the **physical memory**.

此時真相大白，原來在不設定 SQL Server Linux 容器的參數的情況下，預設他就是會吃掉 `80%` 的實體記憶體！ 👍

### 深入探究

以我過去的經驗得知，所有 SQL Server 實例原則上都是「有多少記憶體就會用多少記憶體」的狀況，因為 SQL Server 會自動的追求最有效率的運行方式，因此「記憶體」的配置絕對需要精打細算。

原來 SQL Server Linux 容器中也有一樣的機制，不過 SQL Server Linux 容器中的記憶體機制預設是 `soft limit` 而已，如果你在執行容器的時候沒有設定 `hard limit` 的話，他預設會把所有可用的記憶體吃光。

這個機制跑在 Kubernetes 之後，問題將會更加嚴重，因為不設定 hard limit 的結果，就是遲早被 Node 的 OOM 機制砍掉，不得不慎！

### 疑惑釐清

學員最後的疑惑是，雖然官方文件說要 2 GiB 記憶體，但我為什麼「有時候」還是可以啟動呢？而且沒有加上**記憶體限制**是可以理解，但為什麼這件事跟 `livenessProbe` 有關係？我「有時候」還是可以啟動的耶！

我自己在 Kubernetes 實測，在給定 1 GiB 的記憶體用量下，確實有一定的機率是可以啟動成功的 (但我不知道為什麼)，難道是 [containerd](https://containerd.io/) 比 [Docker Desktop](https://www.docker.com/products/docker-desktop/) 更省記憶體？這是相當有可能的啊！

而我們在課堂上的 `livenessProbe` 實作，是要登入 SQL Server 並執行 `SELECT * FROM sys.databases` 這個 T-SQL 語法，其 `livenessProbe` 設定的 `command` 命令如下：

```yaml
livenessProbe:
  exec:
    command: ["bash","-c","/opt/mssql-tools/bin/sqlcmd -S . -U sa -P $MSSQL_SA_PASSWORD -Q 'SELECT name FROM sys.databases;' -b"]
  initialDelaySeconds: 5
  timeoutSeconds: 5
  successThreshold: 1
  failureThreshold: 3
  periodSeconds: 10
```

原本 SQL Server Linux 容器在[捉襟見肘](https://www.moedict.tw/%E6%8D%89%E8%A5%9F%E8%A6%8B%E8%82%98)的記憶體之中剛好啟動，但在嘗試執行 `livenessProbe` 的過程把記憶體用光了，導致 SQL Server Linux 的 `FAIL_PAGE_ALLOCATION` 的問題發生！ 😅

### 相關連結

-   [Configure Liveness, Readiness and Startup Probes | Kubernetes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/)
-   [Configure and customize SQL Server Docker containers](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-docker-container-configure?view=sql-server-ver16&WT.mc_id=DT-MVP-4015686&pivots=cs1-bash)
-   [Microsoft SQL Server by Microsoft | Docker Hub](https://hub.docker.com/_/microsoft-mssql-server)
-   [Docker: Install containers for SQL Server on Linux - SQL Server | Microsoft Docs](https://learn.microsoft.com/en-us/sql/linux/quickstart-install-connect-docker?view=sql-server-ver16&pivots=cs1-bash&WT.mc_id=DT-MVP-4015686)
-   [Configure SQL Server settings on Linux - SQL Server | Microsoft Docs](https://learn.microsoft.com/en-us/sql/linux/sql-server-linux-configure-mssql-conf?view=sql-server-ver16&WT.mc_id=DT-MVP-4015686#memorylimit)
-   [KB4347055 - FIX: Out of memory error when you run SQL Server 2017 in a Linux Docker container](https://support.microsoft.com/en-us/topic/kb4347055-fix-out-of-memory-error-when-you-run-sql-server-2017-in-a-linux-docker-container-275919e6-0ed3-1666-1169-d5167bec2fbd#:~:text=SQL%20Server%20on%20Linux%20by,allocated%20to%20the%20docker%20container)
