摘要:相關環境由于是一個幾年前的項目,所以使用的是這樣的。一些小提示本次優化筆記,并不會有什么文件的展示。將異步改為了串行,喪失了作為異步事件流的優勢。
這兩天針對一個Node項目進行了一波代碼層面的優化,從響應時間上看,是一次很顯著的提升。背景
一個純粹給客戶端提供接口的服務,沒有涉及到頁面渲染相關。
首先這個項目是一個幾年前的項目了,期間一直在新增需求,導致代碼邏輯變得也比較復雜,接口響應時長也在跟著上漲。
之前有過一次針對服務器環境方面的優化(node版本升級),確實性能提升不少,但是本著“青春在于作死”的理念,這次就從代碼層面再進行一次優化。
由于是一個幾年前的項目,所以使用的是Express+co這樣的。
因為早年Node.js版本為4.x,遂異步處理使用的是yield+generator這種方式進行的。
確實相對于一些更早的async.waterfall來說,代碼可讀性已經很高了。
關于數據存儲方面,因為是一些實時性要求很高的數據,所以數據均來自Redis。
Node.js版本由于前段時間的升級,現在為8.11.1,這讓我們可以合理的使用一些新的語法來簡化代碼。
因為訪問量一直在上漲,一些早年沒有什么問題的代碼在請求達到一定量級以后也會成為拖慢程序的原因之一,這次優化主要也是為了填這部分坑。
一些小提示本次優化筆記,并不會有什么profile文件的展示。
我這次做優化也沒有依賴于性能分析,只是簡單的添加了接口的響應時長,匯總后進行對比得到的結果。(異步的寫文件appendFile了開始結束的時間戳)
依據profile的優化可能會作為三期來進行。
profile主要會用于查找內存泄漏、函數調用堆棧內存大小之類的問題,所以本次優化沒有考慮profile的使用
而且我個人覺得貼那么幾張內存快照沒有任何意義(在本次優化中),不如拿出些實際的優化前后代碼對比來得實在。
這里列出了在本次優化中涉及到的地方:
一些不太合理的數據結構(用的姿勢有問題)
串行的異步代碼(類似callback地獄那種格式的)
數據結構相關的優化這里說的結構都是與Redis相關的,基本上是指部分數據過濾的實現
過濾相關的主要體現在一些列表數據接口中,因為要根據業務邏輯進行一些過濾之類的操作:
過濾的參考來自于另一份生成好的數據集
過濾的參考來自于Redis
其實第一種數據也是通過Redis生成的。:)
過濾來自另一份數據源的優化就像第一種情況,在代碼中可能是類似這樣的:
let data1 = getData1() // [{id: XXX, name: XXX}, ...] let data2 = getData2() // [{id: XXX, name: XXX}, ...] data2 = data2.filter(item => { for (let target of data1) { if (target.id === item.id) { return false } } return true })
有兩個列表,要保證第一個列表中的數據不會出現在第二個列表中
當然,這個最優的解決方案一定是服務端不進行處理,由客戶端進行過濾,但是這樣就失去了靈活性,而且很難去兼容舊版本
上面的代碼在遍歷data2中的每一個元素時,都會嘗試遍歷data1,然后再進行兩者的對比。
這樣做的缺點在于,每次都會重新生成一個迭代器,且因為判斷的是id屬性,每次都會去查找對象屬性,所以我們對代碼進行如下優化:
// 在外層創建一個用于過濾的數組 let filterData = data1.map(item => item.id) data2 = data2.filter(item => filterData.includes(item.id) )
這樣我們在遍歷data2時只是對filterData對象進行調用了includes進行查找,而不是每次都去生成一個新的迭代器。
當然,其實關于這一塊還是有可以再優化的地方,因為我們上邊創建的filterData其實是一個Array,這是一個List,使用includes,可以認為其時間復雜度為O(N)了,N為length。
所以我們可以嘗試將上邊的Array切換為Object或者Map對象。
因為后邊兩個都是屬于hash結構的,對于這種結構的查找可以認為時間復雜度為O(1)了,有或者沒有。
let filterData = new Map() data.forEach(item => filterData.set(item.id, null) // 填充null占位,我們并不需要它的實際值 ) data2 = data2.filter(item => filterData.has(item.id) )
P.S. 跟同事討論過這個問題,并做了一個測試腳本實驗,證明了在針對大量數據進行判斷item是否存在的操作時,Set和Array表現是最差的,而Map和Object基本持平。
關于來自Redis的過濾關于這個的過濾,需要考慮優化的Redis數據結構一般是Set、SortedSet。
比如Set調用sismember來進行判斷某個item是否存在,
或者是SortedSet調用zscore來判斷某個item是否存在(是否有對應的score值)
這里就是需要權衡一下的地方了,如果我們在循環中用到了上述的兩個方法。
是應該在循環外層直接獲取所有的item,直接在內存中判斷元素是否存在
還是在循環中依次調用Redis進行獲取某個item是否存在呢?
如果是SortedSet,建議在循環中使用zscore進行判斷(這個時間復雜度為O(1))
如果是Set,如果已知的Set基數基本都會大于循環的次數,建議在循環中使用sismember進行判斷
如果代碼會循環很多次,而Set基數并不大,可以取出來放到循環外部使用(smembers時間復雜度為O(N),N為集合的基數)
而且,還有一點兒,網絡傳輸成本也需要包含在我們權衡的范圍內,因為像sismbers的返回值只是1|0,而smembers則會把整個集合都傳輸過來
如果現在有一個列表數據,需要針對某些省份進行過濾掉一些數據。我們可以選擇在循環外層取出集合中所有的值,然后在循環內部直接通過內存中的對象來判斷過濾。
如果這個列表數據是要針對用戶進行黑名單過濾的,考慮到有些用戶可能會拉黑很多人,這個Set的基數就很難估,這時候就建議使用循環內判斷的方式了。
降低網絡傳輸成本 杜絕Hash的濫用確實,使用hgetall是一件非常省心的事情,不管Redis的這個Hash里邊有什么,我都會獲取到。
但是,這個確實會造成一些性能上的問題。
比如,我有一個Hash,數據結構如下:
{ name: "Niko", age: 18, sex: 1, ... }
現在在一個列表接口中需要用到這個hash中的name和age字段。
最省心的方法就是:
let info = {} let results = await redisClient.hgetall("hash") return { ...info, name: results.name, age: results.age }
在hash很小的情況下,hgetall并不會對性能造成什么影響,
可是當我們的hash數量很大時,這樣的hgetall就會造成很大的影響。
hgetall時間復雜度為O(N),N為hash的大小
且不說上邊的時間復雜度,我們實際僅用到了name和age,而其他的值通過網絡傳輸過來其實是一種浪費
所以我們需要對類似的代碼進行修改:
let results = await redisClient.hgetall("hash") // == > let [name, age] = await redisClient.hmget("hash", "name", "age")
**P.S. 如果hash的item數量超過一定量以后會改變hash的存儲結構,
此時使用hgetall性能會優于hmget,可以簡單的理解為,20個以下的hmget都是沒有問題的**
從co開始,到現在的async、await,在Node.js中的異步編程就變得很清晰,我們可以將異步函數寫成如下格式:
async function func () { let data1 = await getData1() let data2 = await getData2() return data1.concat(data2) } await func()
看起來是很舒服對吧?
你舒服了程序也舒服,程序只有在getData1獲取到返回值以后才會去執行getData2的請求,然后又陷入了等待回調的過程中。
這個就是很常見的濫用異步函數的地方。將異步改為了串行,喪失了Node.js作為異步事件流的優勢。
像這種類似的毫無相關的異步請求,一個建議:
能合并就合并,這個合并不是指讓你去修改數據提供方的邏輯,而是要更好的去利用異步事件流的優勢,同時注冊多個異步事件。
async function func () { let [ data1, data2 ] = await Promise.all([ getData1(), getData2() ]) }
這樣的做法能夠讓getData1與getData2的請求同時發出去,并統一處理回調結果。
最理想的情況下,我們將所有的異步請求一并發出,然后等待返回結果。
然而一般來講不太可能實現這樣的,就像上邊的幾個例子,我們可能要在循環中調用sismember,亦或者我們的一個數據集依賴于另一個數據集的過濾。
這里就又是一個權衡取舍的地方了,就像本次優化的一個例子,有兩份數據集,一個有固定長度的數據(個位數),第二個為不固定長度的數據。
第一個數據集在生成數據后會進行裁剪,保證長度為固定的個數。
第二個數據集長度則不固定,且需要根據第一個集合的元素進行過濾。
此時第一個集合的異步調用會占用很多的時間,而如果我們在第二個集合的數據獲取中不依據第一份數據進行過濾的話,就會造成一些無效的請求(重復的數據獲取)。
但是在對比了以后,還是覺得將兩者改為并發性價比更高。
因為上邊也提到了,第一個集合的數量大概是個位數,也就是說,第二個集合即使重復了,也不會重復很多數據,兩者相比較,果斷選擇了并發。
在獲取到兩個數據集以后,在拿第一個集合去過濾第二個集合的數據。
如果兩者異步執行的時間差不太多的話,這樣的優化基本可以節省40%的時間成本(當然缺點就是數據提供方的壓力會增大一倍)。
如果串行執行多次異步操作,任何一個操作的緩慢都會導致整體時間的拉長。
而如果選擇了并行多個異步代碼,其中的一個操作時間過長,但是它可能在整個隊列中不是最長的,所以說并不會影響到整體的時間。
總體來說,本次優化在于以下幾點:
合理利用數據結構(善用hash結構來代替某些list)
減少不必要的網絡請求(hgetall to hmget)
將串行改為并行(擁抱異步事件)
以及一個新鮮的剛出爐的接口響應時長對比圖:
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/108081.html
摘要:手頭做的項目開發得差不多了,而打包配置是一開始粗略配置的,不大的項目打包出來得,所以現在必須進行優化。用于生產環境的打包,設置其為后,這些庫會提供最小體積的文件。這種情況打包后的體積要更小一些。最后打包結果的體積開銷主要就是以上幾項。 手頭做的項目開發得差不多了,而打包配置是一開始粗略配置的,不大的項目打包出來得6MB+,所以現在必須進行優化。 打包結果分析 執行命令 webpack ...
摘要:寫在最前本次分享一下在作者上一次失利即拿到畢業證第二天突然收到阿里社招面試通知失敗之后,通過分析自己的定位與實際情況,做出的未來一到兩年的規劃。在博客有一定曝光度的積累中,陸續收到了一些面試邀請,基本上是阿里的但是我知道我菜。。 寫在最前 本次分享一下在作者上一次失利即拿到畢業證第二天突然收到阿里社招面試通知失敗之后,通過分析自己的定位與實際情況,做出的未來一到兩年的規劃。以及本次社招...
摘要:同時也要引入對應版本的先引入引入組件庫因為依賴是從外部引入的,所以需要告知在打包時,依賴的來源。然后在中加入一條命令執行或者即可完成打包。因此將此次優化記錄下來,并傳上了中。 本文原文 前言 公司有好幾個項目都有后臺管理系統,為了方便開發,所以選擇了 vue 中比較火的 后臺模板 作為基礎模板進行開發。但是,開始用的時候,作者并沒有對此進行優化,到項目上線的時候,才發現,打包出來的文件...
閱讀 964·2023-04-26 02:56
閱讀 9437·2021-11-23 09:51
閱讀 1849·2021-09-26 10:14
閱讀 2979·2019-08-29 13:09
閱讀 2153·2019-08-26 13:29
閱讀 571·2019-08-26 12:02
閱讀 3561·2019-08-26 10:42
閱讀 2999·2019-08-23 18:18