喚醒資料庫時不帶著失效的連線
已發佈的 Mythex App 及其資料庫在無人使用時會進入休眠。App 醒來時,第一個請求可能會卡住 10–12 秒,因為它嘗試重用在一切休眠期間已經失效的資料庫連線。現在我們為 Spring Boot App 提供了在休眠後仍安全的資料庫設定,並告訴代理人以相同方式建置任何技術堆疊的 App。在託管平台上,休眠後的第一個請求從 12.7 秒降到了 2.3 秒。
為什麼 App 和資料庫會休眠
已發佈的 Mythex App 運行在一台閒置數分鐘後就會暫停的機器上,而它的資料庫是無伺服器的 Postgres,閒置五分鐘後休眠,並在下一個請求時喚醒。這讓閒置的 App 保持便宜:你只為資料庫實際使用的部分付費,而休眠中的 App 不會在運算上花費任何費用。我們在依實際使用量計費的資料庫一文中寫過計費的部分。
問題在於,兩者各自休眠,而它們對「連線」的定義並不一致。
症狀:第一次造訪很慢
大多數伺服器端 App 都會保留一個 連線池:一組隨時待命的開啟資料庫連線,讓每個請求不必付出開新連線的成本。對於一直在線的 App 來說,這是正確的設計。
加入休眠之後,會發生以下情況:
- App 正在運行,連線池中有幾條閒置的連線。
- 沒有人造訪。App 的機器暫停——凍結,連線池保持原樣。
- 資料庫進入休眠,並捨棄它那一端的所有連線。
- 訪客到來。App 完全照原樣醒來,仍然持有資料庫已經不認識的連線。
- 連線池會把其中一條連線交出來。在放棄它之前,必須先嘗試並等待,才能發現它已經失效。
在 Spring Boot App 中,驗證這些失效的連線會讓第一個請求卡住約 10 秒,之後才以「Connection is not available」失敗。對訪客來說,網站就像卡住了,然後可能顯示一個錯誤。
為什麼 Spring App 受害最深
Spring Boot 是一個受歡迎的 Java 框架,預設使用一個名為 HikariCP 的連線池。Spring 可以透過設定來配置該連線池,包括要保留多少條閒置連線。
但 Mythex 會把每個 App 的資料庫以單一的 DATABASE_URL 提供,格式為 postgresql://user:pass@host/db。Spring 無法直接讀取這種格式。因此,在 Mythex 上建置的 Spring App 會透過解析該 URL 在程式碼中自行建立連線池——而手動建立的連線池會忽略 Spring 所有的 spring.datasource.* 設定。即使有人知道正確的連線池設定,也沒有地方能讓它生效。
修正方式之一:Spring 能使用的設定
當專案有資料庫時,我們現在也會提供 Spring 原生讀取的環境變數:
SPRING_DATASOURCE_URL、SPRING_DATASOURCE_USERNAME與SPRING_DATASOURCE_PASSWORD,由DATABASE_URL轉換而來(除非 URL 另有指定,否則需要 SSL)。- 能在休眠後保留的連線池設定:
| 設定 | 數值 | 原因 |
|---|---|---|
| 最小閒置連線數 | 0 | 請求之間不保留任何開啟的連線,因此沒有可能失效的東西 |
| 閒置逾時 | 30 秒 | 盡快關閉未使用的連線 |
| 最長存活時間 | 2 分鐘 | 在任何連線變老之前將其淘汰 |
| 連線逾時 | 5 秒 | 若資料庫回應緩慢,不讓請求卡太久 |
由於不保留任何閒置連線,醒來的 App 只需開啟一條新連線,而不必在失效的連線之間摸索。
當這些變數存在時,Spring 會自行配置連線池,這些設定就會生效。專案自己設定的值永遠優先;我們只補上缺少的部分。非 Spring 的 App 則會忽略這些變數。
差點讓它毫無作用的細節
Spring 將環境變數對應到其設定的規則很容易弄錯。minimum-idle 這個設定必須寫成 SPRING_DATASOURCE_HIKARI_MINIMUMIDLE——去掉連字號,字詞連在一起。若寫成看起來比較自然的 SPRING_DATASOURCE_HIKARI_MINIMUM_IDLE,它會對應到另一個不存在的設定(minimum.idle),並被靜默忽略。
這種錯誤能通過每次審查,但在正式環境中毫無作用。因此有一項測試會同時檢查這些值是否已設定,以及沒有任何變數使用以底線分隔的寫法。
修正方式之二:其他所有技術堆疊
Spring 並不是唯一有連線池的框架,而我們也無法為每種語言中的每個函式庫預先配置。因此,負責撰寫你的 App 的 Mythex 代理人,現在對於任何技術堆疊都有一條常駐指示:已發佈的 App 及其資料庫在閒置時會休眠,因此保持開啟的連線在下一個請求時可能已經失效。它應該:
- 不保留任何閒置的資料庫連線,或在使用前檢查每一條連線;
- 遇到連線錯誤時重試一次;
- 使用平台在環境中提供的資料庫設定,而不是透過解析
DATABASE_URL自行建立連線池。
最後一點與第一部分形成閉環:使用平台提供的設定所建置的 App,會自動取得能在休眠後保持安全的配置。
結果
在我們執行已發佈 App 的託管平台上測量,休眠後的第一個請求從 12.7 秒降到 2.3 秒。
我們從中學到什麼
- 每一個會休眠的層級,都必須就什麼能存活達成一致。 凍結的 App 與休眠的資料庫各自做了合理的選擇。合在一起,App 醒來時卻持有已不再真實的狀態。
- 為常駐伺服器設計的預設值,不適用於會縮放到零的 App。 讓連線保持溫熱是好主意,直到它周圍的一切都冷卻下來為止。
- 把資料以框架能理解的格式交給它。 對我們而言,單一 URL 很簡單,卻迫使每個 Spring App 手動建立一個忽略自身設定的連線池。
- 測試設定的拼寫是否符合框架的讀取方式。 名稱錯誤的正確數值,會靜默地毫無作用。
如果你還不熟悉 Mythex 上的資料庫運作方式,請參見資料庫文件,以及我們介紹 什麼是 Postgres 的說明。