不停機條件下部署 Django 應用

Namco發表於2015-11-02

當我們上線新的服務應用時,經常不得不重啟Web伺服器以完成部署。但這會對使用者造成一定影響,特別是伺服器處於繁忙狀態時,問題更嚴重。本文中,作者將針對這一問題,講述其如何在不停機條件下部署Django應用。

當我的網站 healthchecks.io 每秒接收的請求次數超過一次之後,我就非常清楚地認識到我不能再像以前那樣部署程式碼之後隨意重啟 web 伺服器了。作為一個監控服務,哪怕是漏掉幾條 HTTP 的請求也是不能接受的。並且隨著時間推移,伺服器越來越忙碌,這個問題只會更嚴重。

先讓我簡單介紹一下我在做什麼吧:我在做的 app 是一個相對簡單的 Django app,搭建在 gunicorn 的 WSGI 服務上,使用 nginx 作為代理伺服器,而資料儲存在 PostgreSQL 資料庫上。gunicorn 程式和一個附加的後臺作業都由 supervisor 管理。app 主機是一臺區區 20 美元的 DigitalOcean 伺服器。

另外,鑑於當前技術選擇的日新月異,我遵循的指導原則就是保證技術棧儘可能長時間地保持簡單和可擴充套件。新增一些諸如負載均衡、資料庫複製、鍵值對儲存、訊息佇列等等的功能,這些功能的確都能帶來某些益處。然而從另一方面來講,這也意味著有更多東西需要管理、監控和備份。同時對於參與專案的新人來說,他們不得不花更多時間弄清楚系統的資料流向,從頭開始建立起所有的東西。我認為在不犧牲效能和特性的前提下,能夠保持簡單不冗餘的架構是一種有趣的挑戰。

我使用的部署機制是 Fabric 指令碼加上 supervisor 和 nginx 的配置模板。每次我在工作站上執行 “fab deploy”命令時,Fabric 指令碼會在遠端伺服器上完成如下事項:

  • 為新的部署建立一個目錄,姑且將之稱為 $TARGET
  • $TARGET/venv 目錄下建立一個 python3 的虛擬環境。
  • 從 GitHub 上抓取最新的程式碼快照到你的$TARGET目錄。你可以使用 GitHub 的 SVN 介面執行“svn export”命令完成這項操作,非常簡便。這項操作如你所願地只是生成了原始檔,但是沒有任何版本控制的後設資料。
  • 根據 requirements 檔案安裝依賴包。這些依賴包會安裝在虛擬環境下而不會影響現有應用。下載和安裝依賴包大概需要一分鐘(這裡的時間估算應當針對國外環境,國內的網路狀況,你懂的 :) 譯者注。)
  • 執行 Django 管理命令收集靜態檔案,執行資料庫遷移等等準備工作。
  • 重寫 supervisor 的配置檔案,在新的虛擬環境下執行 gunicorn。
  • 更新 nginx 配置檔案,以防改動過 nginx 配置模板中的某些配置沒有生效。
  • 執行 “supervisorctl reload” 和“/etc/init.d/nginx restart”。當前 web 應用還是不能訪問的,直到 supervisor 開始執行,啟動 gunicorn 程式並且初始 Django 程式碼完成之後才能訪問。這個過程通常需要 5 至 10秒,在這期間 nginx 一般會返回 “502 Bad Gateway”。
  • 大功告成!

下面是相關的 Fabric 指令碼程式碼示例。指令碼中使用的虛擬環境上下文管理器(virtualenv context manager)來源於非常棒的 fabtools 庫。

現在問題來了:在每次釋出的最後一步怎樣儘量避免停機?首先我們先設定如下約束條件:沒有負載均衡(至少現在沒有)。所有服務執行在一個環境下,儘可能避免一切錯誤返回。現在我們從最簡單的步驟開始。我們先來考慮一種簡單的(並且是常見的)場景:不會發生資料庫遷移,並且部署的改動是向後相容的:舊版本的 app 可以在遷移之後正常執行。

我想到的第一個想法是基於這樣一個觀點:可用性對於 app 的某些部分更為重要,而對於其他部分可能就沒那麼重要了。具體到我的 app 來說,比如 app 監聽客戶機發來的 ping 包的 API 介面部分就需要更高的可用性,而給普通訪問者提供的前端服務頁面部分就沒有那麼重要了。儘管給訪問者返回了錯誤頁面十分尷尬,但是不遺漏任何一個 ping 包才是最重要的。畢竟我們提供的是監控服務,一個丟失的 ping 包可能導致在之後某個時間發出一個假警報——這才是最尷尬的!

我考慮使用亞馬遜 API 監聽這些 ping 包,並且設計了個原型。它可以把 ping 包資訊放在 亞馬遜 SQS 佇列中,當 Django app 有空閒時再處理。這是一種相對簡單的處理方法,大幅提高了可用性和擴充套件性,但是也是以增加複雜度和新增新的外部依賴為代價的。我可能在之後還需要重新審視一下這個方案。

下一個想法是這樣的:把監聽 ping 包的功能從原來的 app 中剝離出來。ping 監聽的邏輯其實非常簡單,最終只相當於兩個 SQL 操作:update 和 insert。這一部分程式碼重構起來非常容易,比如使用一個 python 的微型框架,或者使用其他的語言實現,甚至可以用 nginx 本身的 ngx_postgres 模型來實現。下面提供了基本實現了這個功能的 nginx 配置檔案,僅供消遣(忽略掉那個寫得很滑稽的正規表示式 :))。

上面配置的執行邏輯是這樣的:當使用者請求特定格式的 URL 時,伺服器會在 PostgreSQL 資料庫上查詢並且返回,返回碼是 200 或者 400。這對效能也是一種提升,因為請求不需要在跑一遍 gunicorn,Django 和 psycopg2 然後才返回了。只要資料庫可用,nginx 就可以處理這些 ping 包,即便是 Django 應用意外當機了也沒關係。

然而這種實現方式在某些情況下並非最好選擇,這種方式有一定技巧性,並且要求開發者和系統管理員的知識儲備要足夠豐富才行。舉個例子,當資料庫模式發生改變,上面的 SQL 查詢也需要更新和測試。如果啟用了 ngx_postgres 的擴充套件元件的話,這就不像“apt-get install” 這種操作這麼簡單了。

我們的主要目的還是達成不停機部署,如果再仔細考慮一下的話,如果能仔細協調好重啟和重新載入服務的流程的話,也是可以達成的。

我的部署指令碼使用“/etc/init.d/nginx restart”是因為我不知道更好的選擇了。但是據我瞭解,這個命令可以替換成“/etc/init.d/nginx reload”,以實現優雅地重啟:

執行“service nginx reload”或者“/etc/init.d/nginx reload”將會熱過載配置從而消除停機時間。如果還有等待的請求,只要連線還沒有斷開,nginx程式就會接著處理這些連線。因此這是一個非常優雅地過載配置的方式。—— “Nginx config reload without downtime” on ServerFault

同樣的,我的部署指令碼使用的“supervisorctl reload”命令會停止所有管理的服務,重新讀取配置,最後啟動所有服務。而“supervisorctl update”可以按需啟動、停止和重啟修改後的任務。

改良之後的 “fab deploy” 可以完成以下任務:

  • 建立一個新的虛擬環境,和之前一樣。
  • 建立一個唯一名稱的 supervisor 任務(比如“hc_timestamp”)。
  • 與已經執行的 gunicorn 程式並行啟動一個新的 gunicorn 程式。nginx 與使用 UNIX 套接字的 gunicorn 程式通訊,每個程式使用單獨的,帶有時間戳的套接字檔案。
  • 等待一段時間,直到新的 gunicorn 程式已啟動並且可以正常服務。
  • 更新 nginx 配置檔案並且指向新的套接字配置檔案,重新載入 nginx。
  • 停掉老的 gunicorn 程式

下面是更新之後的 Fabric 指令碼,也實現了 supervisor 任務:

這樣的話 nginx 就可以一直提供服務響應,然後和正在執行的 gunicorn 程式持續保持通訊。為了實際驗證,我寫了一個指令碼無限迴圈地請求特定的 URL。如果得到了一個非正確的返回,就會列印出來一個顯眼的錯誤訊息。使用這個指令碼不斷衝擊我的測試虛擬機器的同時,我做了一些部署操作,並沒有發現丟失的請求。大功告成!

總結

要實現在程式碼部署時不停機,有很多種方式,每種方式都要權衡利弊。比如說一個合理的策略是把關鍵部分從原來的應用中剝離出來,每個部分都可以獨立更新。之後每個部分也可以獨立擴充套件。但是這種策略的缺點就是有更多的程式碼和配置需要維護。

最終我達成的效果是:

  • 熱過載 supervisor 和 nginx 配置,而不是直接重啟它們。根據之前的經驗,這種做法的好處是顯而易見的。
  • 在停止舊的 gunicorn 程式之前,確認新的 gunicorn 程式已經啟動並且與 nginx 正常通訊
  • 保持整個架構相對簡單。當這個專案有更多人在用時,我需要找出效能短板並且想辦法水平擴充套件。但是現在就要考慮到這一點。

pic1

打個廣告:healthchecks.io 是一個免費的開源 cron 監控服務。你只需要花幾分鐘的時間就可以啟動對你的 cron 任務的監控。媽媽再也不用擔心我晚上睡不好啦!

相關文章