蒼時弦也
蒼時弦也
資深軟體工程師
發表於
這篇文章是 Rails 部署實踐 系列的一部分。

完成網站部署後,我們還需要持續的更新網站。然而如果只有一個節點的話,網站是會有一瞬間服務暫停的狀況,為了避免這樣的問題可以採取滾動更新(Rolling Upgrade)的方式處理。

過去我們需要仰賴人工切換,然而到了虛擬機、容器的階段就能夠透過預先安裝好環境來做到半自動、自動的處理。

滾動更新的原理

滾動更新簡單來說就是我們「依序」的將舊的伺服器替換掉的過程,所以在伺服器的前端通常會有負載平衡(Load Balancer)來提供切換的動作,步驟大致上如下。

  1. 運行新的服務(Service B)
  2. 將新的服務登記到負載平衡上(Service A 和 Service B 都可以被連上)
  3. 將舊的服務移除(Service A 無法連上)

這個做法就能夠讓使用者不會注意到我們正在更新服務,因為體驗上是不會有「中斷」的狀況,然而還是要注意以下幾種狀況。

  1. 資料庫的更新,像是 Migration(遷移)是否會影響服務
  2. 應用程式的更新,像是程式碼的變動造成行為不一致

這些都會需要應用程式的實作上進行對應的處理,像是使用 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 部署實踐回饋表單告訴我。