Node.js 開發實踐總結-定時腳本的設計與實現

2021-02-20 Nodejs技術棧
作者@騰訊IMWeb前端團隊 | 地址@https://mp.weixin.qq.com/s/a_kIRvJUuICtw0CG7EFAtg前言

作為Node語言的初學者去實踐後端開發時,不僅僅有見獵心喜,也有一些忐忑,好在大家都很open,給予了很多建議和分享,到目前為止,也成功建立了三個基於Node.js + TypeScript + IMServer 1 的工程,也是時候將自己最近的學習過程進行總結,下面就以一個小小的開發任務為載體分享下我的成長過程。

需求

在完成Node工程的搭建之後,我接受到第一個Node後臺開發任務:定時將企業微信的組織架構信息拉取到業務資料庫系統中,並且提供手機號查詢用戶查詢接口。一開始對這個任務還是比較樂觀的,信心滿滿的去開發了。

初步方案

經過方案設計之後形成了上述的方案:

在伺服器部署初始化時(init.ts初始啟動文件中)啟動node-schedule的定時任務,讀取資料庫中的企業微信的企業配置,然後並行啟動若干企業的組織架構更新進程。

企業微信提供了獲取部門成員的詳情,因此只需並行更新每個部門的信息,並且寫入mysql資料庫中。

當查詢接口到達伺服器後,首先從資料庫中查詢該手機號對應的成員,若不存在則從企業微信側調用手機號獲取userid API,然後通過獲取用戶信息API獲取最新的用戶信息,避免定時更新帶來的更新時間gap;若存在則直接返回資料庫中的信息。

開發過程中的踩雷

整體業務邏輯並不複雜,調試和部署的過程中遇到許多問題,這裡給大家一一列舉下:

訪問頻率受限
企業微信官方規定同一時間對同一份資源的請求數不可超過一定數值(60),由於部門詳情的請求接口採用的並行模式,因此超過了閾值,測試過程中被官方封禁了IP。

過多進程導致SQL慢查詢
沒有考慮多地部署(3地 * 5伺服器 * 8 worker)導致同時存在了120個更新進程,進而導致資料庫mysql的讀寫混亂,也消耗了大量性能,導致資料庫讀寫壓力比較大時,出現了部分慢查詢的情況

無效手機號不可調用企業微信api
企業微信對手機號獲取userid的接口,具有以下限制:當查詢中出現一定數量的無效手機號時,會觸發企業微信官方IP封禁。但是業務系統中存在大量離職後的無效手機號,因此當檢查到資料庫中不存在時,頻繁調用上述接口則會觸發封禁。

資料庫讀寫衝突
由於存在多臺伺服器同時讀寫資料庫,導致資料庫出現了部分重複、缺少的情況。

網絡環境導致讀寫鎖的平衡性失效,產生衍生問題
為了優化上述部分,引入的任務讀寫鎖,保證單一進程更新。但是未考慮各地服務網絡情況,導致內網伺服器一直持有讀寫鎖,失去了均衡負載的設計有效性。也導致在配置預上線環境時,預上線環境由於網絡環境良好一直持有讀寫鎖,進而影響線上的實時數據。

未考慮失敗情況進行報警和恢復

深度優化設計

下面介紹下如何解決這些問題和思路和方案。

這裡針對「部門成員信息API「的並行請求,改造成基於有效頻率值的串行發送機制,設計成10個/每秒的調用速度。

這個解決方案比較明確,就是減少啟動定時任務的進程數。

由於後端服務一般分為測試環境、預上線環境、正式環境,不同的環境中是否需要啟動各個定時器腳本可以通過部署時(以SKTE為例),設置環境變量「SCHEDULE_ENV」來管理。

每臺伺服器會啟動8個worker進程,每個worker使用「process.env.IMSERVER_WORKER_ID」變量進行標識,因此可以設計只有「worker1」進程來進行定時任務的啟動;

3、無效手機號不可調用企業微信api

這個是在技術調研中沒能發現的情況,發現前期技術調研的工作疏忽。

首先是業務調用方是無法得知手機號是否有效,且也不應該去關心這個限制,因此原先為了解決部分新紀錄更新不及時的問題,而引入的實時查詢機制是不合理的。

實時查詢機制:「對於資料庫中不存在的手機號,通過企業微信官方api進行實時查詢來返回結果」

因此移除了這個機制,並且提供了一個基於企業微信官方API的實時查詢接口,每次業務方調用時,也將結果同步更新到組織架構中。

4、資料庫讀寫衝突

引入redis任務鎖機制,保證同一時間內只有一個進程能夠進行資料庫更新操作。

其次是企業之間的更新採用並行機制,由於相互之間是互不衝突的,因此不會引起同一條記錄的讀寫衝突,也可以提升其更新速度。

5、網絡環境導致讀寫鎖的平衡性失效,產生衍生問題

在最初的設計中,我希望伺服器之間能夠根據自身的負載情況來進行公平競爭任務鎖,但是實際情況是由於多地部署,其中穩定的內網環境可以一直優先獲取到任務鎖,就是沒有所謂的公平性了。

特別是當壓測需要部署預上線環境時,如果沒有設置只讀db帳號並且沒有設置啟動定時任務環境變量,這兩個失誤會導致某一次的組織架構更新邏輯調整的代碼更新到線上時,線上一直是舊的邏輯在執行,經過一系列排查我們發現預上線環境一直獲取了讀寫鎖,使用舊的邏輯更新資料庫。

因此增加環境變量來控制定時任務啟動、對於壓測的環境的中的資料庫權限進行了區分,增加了只讀模式。

6、報警和錯誤恢復

這裡有一點前端思維定勢的影響了,這一部分是同樣重要的。

報警方面

則是接入IMLog的Node SDK,通過 Kibana 和 Grafana 的系統配置,可以有效監控組織架構的更新情況。

錯誤恢復方面

這裡的錯誤主要是發生在企業微信API的access_token過期的情況,常發生於以下兩種情況:

企業微信官方主動使access_token過期

在組織架構更新過程中,access_token剛好失效情況,也就是http傳輸到企業微信剛好失效的情況

以上的情況是無法避免的。這裡使用中間件對node.fetch進行封裝,增加對response的返回值的校驗,如果企業微信api的返回值是 「WX_CODE.INVALIDE_TOKEN」 則進行預警和重置accessToken。

export default (app) => {
 const { utils: { imlogHelper } } = app;
 const wrapperLogFetch = (originFetch, {
   traceId,
   header,
   client_ip,
 }) => async (...args) => {
   const res = await originFetch(...args);
   if (res.errcode === WX_CODE.INVALIDE_TOKEN) {
     // 進行更新邏輯
     wxService.clearAllRedisKey();
     imlogHelper({
       cmd: url,
       message: 'accessToken_update_warning',
       body: JSON.stringify(res),
       trace_id: traceId,
       retcode,
       headers: header,
     });
   }
   return res;
 };
   // 覆蓋context.fetch方法
 return async (ctx, next) => {
   if (!ctx.logFetch) {
     const originFetch = ctx.fetch;
     const { traceId, ip: client_ip } = ctx.request;
     const header = JSON.stringify(ctx.request.header);
     const logFetch = wrapperLogFetch(originFetch, {
       traceId,
       header,
       client_ip,
     });
     ctx.logFetch = logFetch;
   }
   if (ctx.fetch !== ctx.logFetch) {
     ctx.fetch = ctx.logFetch;
   }
   await next();
 };
}

總結

經過重新設計和驗證後形成以上的設計方案,具有以下優化點:

首先通過基於redis setnx實現的任務鎖,來實現同一時間單進程更新資料庫;

通過部署時設置定時任務啟動環境變量和資料庫讀寫帳號設置,來保證不同環境的分離;

通過企業並行,部門數據拉取接口串行的模式,最大化性能和避免API調用封禁;

完善錯誤恢復機制和報警,實時查看運行狀況。

希望本文能夠給你在Node之路上有所進益,歡迎點讚、收藏和評論區交流。

IMServer: IMWeb 團隊的 Node 版 Web 框架,類似Koa。↩

相關焦點

  • Node開發實踐總結——定時腳本的設計與實現
    來源:騰訊IMWeb前端團隊前言作為Node語言的初學者去實踐後端開發時,不僅僅有見獵心喜,也有一些忐忑,好在大家都很open,給予了很多建議和分享,到目前為止,也成功建立了三個基於Node.js + TypeScript + IMServer 1 的工程,也是時候將自己最近的學習過程進行總結,下面就以一個小小的開發任務為載體分享下我的成長過程
  • Node開發實踐總結-定時腳本的設計與實現
    前言作為Node語言的初學者去實踐後端開發時,不僅僅有見獵心喜,也有一些忐忑,好在大家都很open,給予了很多建議和分享,到目前為止,也成功建立了三個基於Node.js + TypeScript + IMServer   1   的工程,也是時候將自己最近的學習過程進行總結,下面就以一個小小的開發任務為載體分享下我的成長過程
  • centos7編程實踐:安裝nodejs
    Node.js是一個javascript運行環境。它讓javascript可以開發後端程序,實現幾乎其他後端語言實現的所有功能,可以與PHP、Java、Python、.NET、Ruby等後端語言平起平坐。
  • 前端頁面開發之Node JS初學者指南
    【IT168技術】目前,Node.js是在前端頁面開發中十分受歡迎的,它是一套用來編寫高性能網絡伺服器的JavaScript工具包,在本文中,將帶領各位初學者介紹Node JS的基本知識,要求本文的閱讀對象為有一定Javascript和其他開發語言基礎的讀者。
  • Node.js 中實踐基於 Redis 的分布式鎖實現
    基於 Redis 的分布式鎖實現思路實現分布式鎖的方式有很多:資料庫、Redis、Zookeeper。這裡主要介紹的是通過 Redis 來實現一個分布式鎖,至少要保證三個特性:安全性、死鎖、容錯。舉一個例子客戶端 A 獲取到一把 key = name1 的鎖(2 秒中),緊接著處理自己的業務邏輯,但是在業務邏輯處理這塊阻塞了耗時超過了鎖的時間,鎖是會自動被釋放的,這期間該資源又被客戶端 B 獲取了 key = name1 的鎖,那麼客戶端 A 在自己的業務處理結束之後直接使用 del key 命令刪除會把客戶端 B 的鎖給釋放掉了,所以釋放鎖的時候要做到僅釋放自己佔有的鎖。
  • 【 Node.js】你應該知道的 NPM 知識都在這!
    所以相應地,自定義 npm init 命令的實現方式也很簡單,在電腦 npmStudy 目錄創建一個 .npm-init.js 即可,該文件的 module.exports 即為 package.json 配置內容,需要獲取用戶輸入時候,使用 prompt() 方法即可。
  • node.js、MongoDB下一代的LAMP
    node.js、MongoDB下一代的LAMP 我們大部分人在做網站時,都用的是LAMP,殊不知LAMP已成過去式,新一代的小生:nix、node.js、MongoDB誕生了,讓我們走進他們,知道他們的故事!
  • 你不知道的 Npm(Node.js 進階必備好文)
    所以相應地,自定義 npm init 命令的實現方式也很簡單,在電腦 npmStudy 目錄創建一個 .npm-init.js 即可,該文件的 module.exports 即為 package.json 配置內容,需要獲取用戶輸入時候,使用 prompt() 方法即可。
  • 【 Node.js 進階】你應該知道的 NPM 知識都在這!
    所以相應地,自定義 npm init 命令的實現方式也很簡單,在電腦 npmStudy 目錄創建一個 .npm-init.js 即可,該文件的 module.exports 即為 package.json 配置內容,需要獲取用戶輸入時候,使用 prompt() 方法即可。
  • 找回 Node.js 裡面那些遺失的 ES6 特性
    但是等等,對於小明這樣既寫 React 又寫 Node.js 的的人來說,又有點分裂了。經常性的在 Node.js 裡面寫 import xxx from 'xxx'; 然後被 Node.js 的解析器無情的拒絕;另外對於一致性有點追求的人肯定受不了同一個倉庫, 同一類腳本, 同一個人維護卻有兩種風格。恩, 不能忍!
  • Node.js 學習資料和教程(值得收藏)
    百萬級並發 Node.js也能行高性能Node.js:來自LinkedIn Mobile的10條優化建議Kraken:改變PayPal開發文化的Node.js框架Groupon拋棄Rails,轉向>一個周末掌握IT前沿技術之node.js篇用node+express搭建多人博客教程系列Node.js靜態文件伺服器實戰人人和微博登錄模塊的實現《京JS》會議 & V8上的
  • 如何利用Node.js 構建分布式集群
    本文為UCloud 公司高級工程師文天樂在深JS大會上發表的演講內容,主要介紹了UCloud內部如何利用Node.js 構建分布式集群,並分享了實踐過程中走過的坑,希望對正在使用Node.js或是即將使用Node.js的朋友有一些幫助。
  • 專門針對初學者的Node.js教程
    我大部時候將Node.js作為腳本使用,正如上面所展示的那樣。它更易於使用,是腳本程序有力的替代者。異步回調正如在上例中看到的那樣,Node.js典型的模式是使用異步回調。基本上,你告訴Node.js要做的事,它執行完後便會調用你的函數(回調函數)。這是因為Node是單線程的。在你等待回調函數執行過程中,Node可繼續執行其他事務,不必被阻塞直到該請求完畢。
  • Node.js為何在後端開發中不受重視?
    語言只是一個工具,對高手來講Java、golang、python、Javascript都可以完成複雜的後端開發工作,這些語言最大的區別是生態。Java無疑是所有後端開發語言中的佼佼者,它的生態完善度超乎你的想像,這也是Java在後端開發領域無法撼動的根本。其他幾種語言我覺得沒有根本性的區別,論生態完善度,都是半斤八兩,論高並發,golang當之無愧,論語言友好度,Node.js可以排到首位。
  • 學習筆記:node後臺開發總結
    知識總結:曹迎成。
  • 我開始討厭node.js了
    擁抱JavaScript2014年,我從一個半吊子c#程式設計師轉速前端,突然就愛上了JavaScript,感覺好像一顆被c#束縛已久的心終於解放了,js那种放蕩不羈愛自由的操作領悟神魂顛倒,我感覺自己擺脫了那些過度封裝的程序,所有的代碼都盡在掌握之中,那種感覺說不出的爽。
  • Nodejs 14 大版本中新增特性總結
    AsyncLocalStorage 類可以方便實現上下文本地存儲,在異步調用之間共享數據,對於實現日誌鏈路追蹤場景很有用。package.json 文件中 設置 type 欄位為 module 或在運行 node 時加上標誌 --input-type=module 告訴 Node.js 將 JavaScript 代碼視為 ES Modules。
  • 10 個最適合 Web 和 APP 開發的 NodeJS 框架
    Node.js Express 對於一個已經在使用 node.js 的開發人員來說,Express 或者」node.js express」並不是一個新鮮事。Express 框架提供了對 node.js 原生 API 的比較好的封裝,從而使開發者更加容易地使用node.js。 Express 框架提供了用來開發強壯的 web/移動應用,以及 API 的所有功能。
  • 10+ 最佳的 Node.js 教程結合實例
    我們要介紹的指南由Krasimir Tsonev所撰寫,發布在tutsplus.com上面,從node.js的環境搭建,使用Express框架的後端服務,到最後使用Jade模板引擎的前端UI開發,覆蓋了所有前端到後端的方方面面。你可以在後面這個連結中找到這篇指南:  即時通訊(Node.js Tutorial – Real Time Chat) .
  • 沒有等來Node.js 2.0 Node 之父卻帶來新作Deno 1.0
    雖然作為一個開發人員,擁有這種訪問權限是一個好處,但如果在編寫自己的代碼時不小心,也會帶來安全風險。而 Deno 則不同,它使用命令行參數來啟用或禁用對於一個訪問不同的安全特性。比如說,如果你需要對腳本啟用訪問/etc 文件夾的權限,你可以執行以下操作:deno --allow-read=/etc myscript.