完成網站部署後,我們還需要持續的更新網站。然而如果只有一個節點的話,網站是會有一瞬間服務暫停的狀況,為了避免這樣的問題可以採取滾動更新(Rolling Upgrade)的方式處理。
過去我們需要仰賴人工切換,然而到了虛擬機、容器的階段就能夠透過預先安裝好環境來做到半自動、自動的處理。
滾動更新的原理
滾動更新簡單來說就是我們「依序」的將舊的伺服器替換掉的過程,所以在伺服器的前端通常會有負載平衡(Load Balancer)來提供切換的動作,步驟大致上如下。
- 運行新的服務(Service B)
- 將新的服務登記到負載平衡上(Service A 和 Service B 都可以被連上)
- 將舊的服務移除(Service A 無法連上)
這個做法就能夠讓使用者不會注意到我們正在更新服務,因為體驗上是不會有「中斷」的狀況,然而還是要注意以下幾種狀況。
- 資料庫的更新,像是 Migration(遷移)是否會影響服務
- 應用程式的更新,像是程式碼的變動造成行為不一致
這些都會需要應用程式的實作上進行對應的處理,像是使用 Feature Flag(功能旗標)來切換新版的功能,確保滾動更新的過程中行為一致等等。
Docker Compose 的滾動更新
要使用 Docker Compose 來實現滾動更新,可以參考 Apple Boy 大大的教學文章然而這篇文章的適用範圍比較偏向 Golang 所撰寫的服務,如果是針對 Ruby on Rails 的話,我們還是需要一些處理才能夠順利將「中斷」的情況避免掉。
因為我們已經透過 Traefik 來實現了負載平衡以及服務發現(Service Discovery)的機制,因此可以很輕鬆地利用 docker-compose up -d --scale rails=2 --no-recreate
這樣的方式直接在原本的伺服器上將 Rails 的容器從一個增加到兩個。
當我們利用 --scale
產生出新的版本後,如果接著運行 docker-compose up -d --scale rails=1
的指令,會發現我們的 Rails 可能還沒準備好,反而會讓服務中斷一段時間。
比較標準的做法是使用 docker ps
指令搭配 -f
(Filter,篩選) 功能,先確認我們的 Rails 已經是健康(Healthy)的狀態後再繼續,這時候就會需要在我們的 Dockerfile 裡面定義 HEALTHCHECK
命令,而簡單的方法就是單純等待一定秒數即可。
大多數時候,我們都只需要簡單的版本,然而每次操作都需要不少指令。我們可以將指令撰寫成 Makefile 來自動處理,不使用 Rake 的原因是大部分 Linux 伺服器上都會有 make 指令,而 Rake 需要先安裝 Ruby 在這個情境下比較適合使用 Make 來處理。
我們在 docker-compose.yml
相同的目錄加入 Makefile
並且寫下以下內容
1.PHONY: stats
2
3stats:
4 @docker-compose ps
5
6update: refresh scale-up rollout scale-down
7
8refresh:
9 @docker-compose pull
10
11rollout:
12 @echo "Stop old version"
13 @docker stop -t 30 \
14 $$(docker ps -a --format "table {{.ID}} {{.Names}} {{.CreatedAt}}" | \
15 grep -E 'rails' | \
16 awk -F " " '{print $$1 " " $$3 "T" $$4}' | \
17 sort -k2 | \
18 awk -F " " '{print $$1}' | head -1) # remove 1 oldest container
19 @docker container prune -f
20
21scale-up:
22 @echo "Scaling up"
23 @docker-compose up -d --scale rails=2 --no-recreate
24 @sleep 30
25
26scale-down:
27 @echo "Scaling down"
28 @docker-compose up -d --scale rails=1 --no-recreate
有了這個腳本後,我們就可以用 make update
這個命令自動的進行滾動更新,而不需要去記複雜的指令。
優雅關閉
我們在 Ruby on Rails 部署經常使用的 Puma 以及 Sidekiq 這類工具,已經實踐好了優雅關閉(Graceful Shutdown)的機制,因此我們不需要特別處理。然而如果在專案中有自己撰寫的服務,就需要考慮到這個情況。
在 Linux 中,嘗試「停止」一個進程(Process)的方式有好幾中,一般來說會發送一個訊號(Signal)來處理,通常會用 SIGINT
(Interupt,中斷)來通知,因此我們需要在我們撰寫的程式中「捕捉」這個訊號,並且進行停止前的處理,再停止執行。
關於信號機制,可參考我在 Ruby 埋了一個陷阱 - Signal 的應用這篇文章。
以 Sidekiq 為例子,我們使用 Sidekiq 是為了執行一些「處理時間比較長」的任務,因此很高的機率我們是正在執行一些任務的,如果直接的中斷 Sidekiq 的話,就可能會造成處理到一半而資料不正確。
也因此,當 Sidekiq 收到中斷的訊號時,會先停止處理「待處理」的任務並且把「處理中」的任務全部執行完畢,最後再停止自己的運行,這也是為什麼有時候我們使用 Ctrl + C
嘗試中斷一些程式時,會需要等待一段時間的關係,很有可能是在將任務收尾或者把資料保存到硬碟等等「確保運作正確」的手段。
如果想在第一時間收到更新,歡迎訂閱弦而時習之在這系列文章更新時收到通知,如果有希望了解的知識,可以利用Rails 部署實踐回饋表單告訴我。
如果對這篇文章有興趣,可以透過以下連結繼續閱讀這系列的其他文章。
- Rails 部署實踐 - 補上 Rails 教學缺少的一塊
- Rails 部署實踐 - 以容器部署 Rails 的方案
- Rails 部署實踐 - 部署前置準備
- Rails 部署實踐 - 容器化 Rails 專案概述
- Rails 部署實踐 - 上傳容器鏡像
- Rails 部署實踐 - 伺服器搭建
- Rails 部署實踐 - 撰寫 Docker Compose
- Rails 部署實踐 - 使用 HTTPS 協定加密連線
- Rails 部署實踐 - 健康檢查
- Rails 部署實踐 - 滾動更新
- Rails 實踐部署 - 使用 Alpine 製作容器鏡像
- Rails 部署實踐 - 容器化的 Bundler 最佳設定
- Rails 部署實踐 - 多階段建置
- Rails 部署實踐 - 素材預先編譯
- Rails 部署實踐 - 容器進入點
- Rails 部署實踐 - 容器相關工具
- Rails 部署實踐 - 持續部署
- Rails 部署實踐 - 使用 GitLab CI 自動化建置
- Rails 部署實踐 - 使用 GitHub Actions 自動化建置
- Rails 部署實踐 - 使用 Watchtower 自動更新
- Rails 部署實踐 - Docker Swarm 與 Docker Compose
- Rails 部署實踐 - Docker Swarm 安裝與設定
- Rails 部署實踐 - 部署到 Docker Swarm
- Rails 部署實踐 - 整合 GitLab CI 自動部署
- Rails 部署實踐 - 使用 GitLab 的 Review Apps 機制
- Rails 部署實踐 - 部署不是終點