Egg.js 多机平滑重启实践
前提:
首先要聲明的是,我們的應用都是在阿里云上多機部署的。當然這里不是安利文,而是給有相同問題的朋友一個實踐的參考。
背景:
我在公司處在一個側重 js 技術方向的團隊,后端項目也較多基于 node.js 開發。項目幾經更迭也經歷了 koa1 --> koa2 --> egg.js 的框架變更。
在早期項目依賴 koa 的時候,部署方案就是依賴 gitlab-ci + pm2 的方式做自動化部署和進程管理。pm2 可以管理進程的啟動和監控,也可以在進程意外終止的時候重新拉起新的進程保證項目持續運作。但缺點也很明顯,首先 pm2 本身會需要資源,這個資源與項目進程的負載是成線性關系的,也就是當我們的并發量大的時候,進程需要更多的資源來處理請求,而 pm2 作為資源的分配者,也需要更多的資源來管理請求和進程的資源調度。甚至出現 god deamon 進程占用了1個多g內存😖。這本身來講是額外的資源消耗,并不是我們所希望的。另一方面我們也遇到了在高并發的情況下,pm2 并不能實現 100% 的平滑重啟。每當有新的代碼被部署的時候,還是會出現一定的請求失敗的情況。這與 pm2 本身有關,相關的問題不是單一的,這里不一一展開🙍?♂?。
切換到 egg.js 之后,請求的調度任務由項目本身的 master 進程來管理,可以盡可能讓項目最大化的利用硬件資源。而且少了 pm2 作為媒介,不用去理會 pm2 造成的影響,可以更加關注項目本身的問題。
但是 egg.js 提供的啟動方案只有簡單的 start 和 stop。也就是當我要更新項目的時候,一定要關閉所有進程然后再啟動項目。這樣會造成服務的短暫不可用的情況,顯然不是我們希望看見的。
所以,我們通過各種嘗試來完善 egg.js 的重啟問題😁。
嘗試:編寫熱重啟腳本
在簡單了解 pm2 的重啟原理后,我們知道,pm2 先 fork 出一個新的進程,然后通過 ipc 通知一個進程關閉,當進程關閉后,pm2 再 fork 新的進程,這樣逐個重啟過去的,可以理解為串行。
通過 pm2 的這個方案,結合 egg-scripts 的源碼,我們修改出了一個可以逐個啟動進程的啟動腳本?egg-cluster-script?😄
起初在請求量低的時候,這個方案看似是可行的(因為錯誤少,沒發現)。但是當我們把服務對接給公司其他業務方后,請求量激增,這個方案的問題就暴露出來了😓:
如果要深入到請求調度上的問題,這個改動的成本就相對較高了。最終,我們放棄了這個方案。轉而尋求通過外部手段的方式來達到平滑重啟的目的😖。
新的方向:SLB 的利用
首先我們前提中提到我司的服務都是部署在阿里云上的,基本的部署情況差不多如圖:
通常我們的服務是部署在多臺 ecs 上的,每臺 ecs 上部署多個進程的應用。通過 SLB 做負載均衡,把請求根據權重適當的分配給每個 ecs🤔。
在 SLB 中,定時的健康檢查判斷每個 ecs 上的服務是不是可用的,當不健康的檢查超出了給定的閾值,SLB 就會將 ecs 摘除,不會再將請求分發給這個 ecs,直到這臺 ecs 的健康檢查恢復正常。
通過這個健康檢查的原理,當 ecs 被摘除的時候,我們就可以任意去擺布這臺 ecs 上的進程了。
有了思路后,接下來就是指定實現的方案🧾:
2. 項目提供一個健康檢查的接口 /devops/health ,通常情況下我們采取 head 請求直接返回 狀態碼,當 app.running = true 的時候返回 204,否則返回 500;
const { Controller } = require('egg');module.exports = class DevopsController extends Controller {healthCheck() {const { ctx } = this;if(this.app.runnint === true) {ctx.body = null;}else {ctx.status = 500;ctx.body = '';}} }3. 編寫信號發送腳本改變 app 的健康狀態;
// scripts/health-down.js // 這里的 findNodeProcess,appWorkerPath,titleTemplate 都可以從 egg-script 中找到 async function run () {const processList = await findNodeProcess(item => {const cmd = item.cmd;const title = 'your-app-name'return cmd.includes(appWorkerPath) && cmd.includes(util.format(titleTemplate, title));});for(const pro of processList) {const pid = pro.pid;process.kill(pid,'SIGINT');}// 健康狀態修改之后暫定 5s 讓 slb 摘除 ecs 后再進行進程處理await new Promise(resolve => setTimeout(resolve, 5000)); }run();4. 給 package.json 添加 scripts: "health:down": ''node scripts/health-down.js", 我們是使用 pm2-depoly 執行的部署。所以在 ecosystem.config.js 中,應用的 deploy 做修改;
// ecosystem.config.js module.exports = {deploy: {production: {user: 'your-deploy-user',host: [...'your-ecs-hosts'],ref: 'deploy-ref',repo: 'project-repo',ssh_options: ['StrictHostKeyChecking=no'],path: 'deploy-path-on-ecs','pre-deploy': 'git fetch && npm run health:down','post-deploy': 'npm install --production --no-save && npm stop && npm start'}} };5. 設置健康檢查策略,讓 slb 可以動態摘除/添加 ecs;
我這里健康檢查的頻率設置的相對頻繁,可以根據自己的需要修改這里的配置。大體的意思就是只要 2 次檢查不通過就會把 ecs 摘除,之后只要連續兩次檢查正常就會把 ecs 重新添加回來。
之后就是結合自己的 ci 來自動部署了。通過這個方式,我們的項目可以在任何時候實現項目的平滑重啟,經驗證即使在高峰時段也沒有出現異常。
當前已經應用的項目是日訪問量在 2億😺 左右的服務,正在逐步推廣到其他服務中去。這個實踐也并非針對 eggjs 項目,應該是具有相對通用性的方案,可以在任意語言和框架中采用😁。
結尾:
一定有人問為什么一開始不直接采用 SLB 的方案而要繞這么大個彎子。其實原因挺多的
轉自?https://zhuanlan.zhihu.com/p/84632879
總結
以上是生活随笔為你收集整理的Egg.js 多机平滑重启实践的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CUBA平台使用感想 - 架构师角度
- 下一篇: 加密衍生品赛道异军突起 CBOEX如何做