Node開發(fā)實踐總結-定時腳本的設計與實現(xiàn)

作者:騰訊IMWeb前端團隊
原文地址:https://mp.weixin.qq.com/s/a_kIRvJUuICtw0CG7EFAtg
前言
作為Node語言的初學者去實踐后端開發(fā)時,不僅僅有見獵心喜,也有一些忐忑,好在大家都很open,給予了很多建議和分享,到目前為止,也成功建立了三個基于Node.js + TypeScript + IMServer?1?的工程,也是時候將自己最近的學習過程進行總結,下面就以一個小小的開發(fā)任務為載體分享下我的成長過程。
需求
在完成Node工程的搭建之后,我接受到第一個Node后臺開發(fā)任務:定時將企業(yè)微信的組織架構信息拉取到業(yè)務數(shù)據(jù)庫系統(tǒng)中,并且提供手機號查詢用戶查詢接口。一開始對這個任務還是比較樂觀的,信心滿滿的去開發(fā)了。
初步方案

經過方案設計之后形成了上述的方案:
在服務器部署初始化時(init.ts初始啟動文件中)啟動node-schedule的定時任務,讀取數(shù)據(jù)庫中的企業(yè)微信的企業(yè)配置,然后并行啟動若干企業(yè)的組織架構更新進程。
企業(yè)微信提供了獲取部門成員的詳情,因此只需并行更新每個部門的信息,并且寫入mysql數(shù)據(jù)庫中。

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

開發(fā)過程中的踩雷
整體業(yè)務邏輯并不復雜,調試和部署的過程中遇到許多問題,這里給大家一一列舉下:
訪問頻率受限
企業(yè)微信官方規(guī)定同一時間對同一份資源的請求數(shù)不可超過一定數(shù)值(60),由于部門詳情的請求接口采用的并行模式,因此超過了閾值,測試過程中被官方封禁了IP。
過多進程導致SQL慢查詢
沒有考慮多地部署(3地 * 5服務器 * 8 worker)導致同時存在了120個更新進程,進而導致數(shù)據(jù)庫mysql的讀寫混亂,也消耗了大量性能,導致數(shù)據(jù)庫讀寫壓力比較大時,出現(xiàn)了部分慢查詢的情況
無效手機號不可調用企業(yè)微信api
企業(yè)微信對手機號獲取userid的接口,具有以下限制:當查詢中出現(xiàn)一定數(shù)量的無效手機號時,會觸發(fā)企業(yè)微信官方IP封禁。但是業(yè)務系統(tǒng)中存在大量離職后的無效手機號,因此當檢查到數(shù)據(jù)庫中不存在時,頻繁調用上述接口則會觸發(fā)封禁。數(shù)據(jù)庫讀寫沖突
由于存在多臺服務器同時讀寫數(shù)據(jù)庫,導致數(shù)據(jù)庫出現(xiàn)了部分重復、缺少的情況。網絡環(huán)境導致讀寫鎖的平衡性失效,產生衍生問題
為了優(yōu)化上述部分,引入的任務讀寫鎖,保證單一進程更新。但是未考慮各地服務網絡情況,導致內網服務器一直持有讀寫鎖,失去了均衡負載的設計有效性。也導致在配置預上線環(huán)境時,預上線環(huán)境由于網絡環(huán)境良好一直持有讀寫鎖,進而影響線上的實時數(shù)據(jù)。未考慮失敗情況進行報警和恢復
深度優(yōu)化設計
下面介紹下如何解決這些問題和思路和方案。
1、訪問頻率受限
這里針對“部門成員信息API“的并行請求,改造成基于有效頻率值的串行發(fā)送機制,設計成10個/每秒的調用速度。
2、過多進程導致SQL慢查詢
這個解決方案比較明確,就是減少啟動定時任務的進程數(shù)。

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

每臺服務器會啟動8個worker進程,每個worker使用“process.env.IMSERVER_WORKER_ID”變量進行標識,因此可以設計只有“worker1”進程來進行定時任務的啟動;
3、無效手機號不可調用企業(yè)微信api
這個是在技術調研中沒能發(fā)現(xiàn)的情況,發(fā)現(xiàn)前期技術調研的工作疏忽。
首先是業(yè)務調用方是無法得知手機號是否有效,且也不應該去關心這個限制,因此原先為了解決部分新紀錄更新不及時的問題,而引入的實時查詢機制是不合理的。
實時查詢機制:“對于數(shù)據(jù)庫中不存在的手機號,通過企業(yè)微信官方api進行實時查詢來返回結果”
因此移除了這個機制,并且提供了一個基于企業(yè)微信官方API的實時查詢接口,每次業(yè)務方調用時,也將結果同步更新到組織架構中。
4、數(shù)據(jù)庫讀寫沖突
引入redis任務鎖機制,保證同一時間內只有一個進程能夠進行數(shù)據(jù)庫更新操作。

其次是企業(yè)之間的更新采用并行機制,由于相互之間是互不沖突的,因此不會引起同一條記錄的讀寫沖突,也可以提升其更新速度。
5、網絡環(huán)境導致讀寫鎖的平衡性失效,產生衍生問題
在最初的設計中,我希望服務器之間能夠根據(jù)自身的負載情況來進行公平競爭任務鎖,但是實際情況是由于多地部署,其中穩(wěn)定的內網環(huán)境可以一直優(yōu)先獲取到任務鎖,就是沒有所謂的公平性了。
特別是當壓測需要部署預上線環(huán)境時,如果沒有設置只讀db賬號并且沒有設置啟動定時任務環(huán)境變量,這兩個失誤會導致某一次的組織架構更新邏輯調整的代碼更新到線上時,線上一直是舊的邏輯在執(zhí)行,經過一系列排查我們發(fā)現(xiàn)預上線環(huán)境一直獲取了讀寫鎖,使用舊的邏輯更新數(shù)據(jù)庫。
因此增加環(huán)境變量來控制定時任務啟動、對于壓測的環(huán)境的中的數(shù)據(jù)庫權限進行了區(qū)分,增加了只讀模式。
6、報警和錯誤恢復
這里有一點前端思維定勢的影響了,這一部分是同樣重要的。
報警方面
則是接入IMLog的Node SDK,通過 Kibana 和 Grafana 的系統(tǒng)配置,可以有效監(jiān)控組織架構的更新情況。

錯誤恢復方面
這里的錯誤主要是發(fā)生在企業(yè)微信API的access_token過期的情況,常發(fā)生于以下兩種情況:
企業(yè)微信官方主動使access_token過期
在組織架構更新過程中,access_token剛好失效情況,也就是http傳輸?shù)狡髽I(yè)微信剛好失效的情況
以上的情況是無法避免的。這里使用中間件對node.fetch進行封裝,增加對response的返回值的校驗,如果企業(yè)微信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();
?};
}總結

經過重新設計和驗證后形成以上的設計方案,具有以下優(yōu)化點:
首先通過基于redis setnx實現(xiàn)的任務鎖,來實現(xiàn)同一時間單進程更新數(shù)據(jù)庫;
通過部署時設置定時任務啟動環(huán)境變量和數(shù)據(jù)庫讀寫賬號設置,來保證不同環(huán)境的分離;
通過企業(yè)并行,部門數(shù)據(jù)拉取接口串行的模式,最大化性能和避免API調用封禁;
完善錯誤恢復機制和報警,實時查看運行狀況。
最后
歡迎加我微信(winty230),拉你進技術群,長期交流學習...
歡迎關注「前端Q」,認真學前端,做個專業(yè)的技術人...


