摘要:因此這種方式可以有效減少響應次數(shù),從而減少觸發(fā)事件的次數(shù),盡可能快的拿到響應體。
問題初現(xiàn)
某基于node.js開發(fā)的業(yè)務系統(tǒng)向外提供了一個dubbo服務,提供向第三方緩存查詢、設置多項業(yè)務數(shù)據(jù)并聚合操作結果。在QPS達到800時(兩臺虛擬機,每臺機器4Core8G4node進程),在監(jiān)控平臺上出現(xiàn)了非常多的slow rt警告,平均接口響應達到60+ms,請求報警率達到80%+。
為找到造成該服務吞吐量過低的罪魁禍首,業(yè)務人員在請求日志中打點了所有查詢緩存的操作,結果顯示每個請求查詢緩存耗時在50-100ms之間跳動。查詢了redis-server的監(jiān)控數(shù)據(jù)發(fā)現(xiàn),不存在server端的慢查詢,在整個監(jiān)控區(qū)間內(nèi)服務端處理時間在40us徘徊,因此排除了redis-server的處理能力不足原因;
通過登錄內(nèi)網(wǎng)機器進行不斷測試到對應redis server機器的端到端時延發(fā)現(xiàn)內(nèi)部局域網(wǎng)的帶寬、時延與抖動足夠正常,都不是造成該問題的原因。
因此,錯誤原因定位到了調(diào)用redis client的業(yè)務代碼以及redis client的I/O性能。
本文中提到的node redis client采用的基于node-redis封裝的二方包,因此問題排查也基于node-redis這個模塊。瓶頸在哪
為了在本地模擬線上環(huán)境的并發(fā),可以做一個不是很嚴謹?shù)臏y試:
async ()=>{ let dd = Date.now() let arr = [] for(let i=0;i<200;i++){ arr.push(new Promise((res,rej)=>{ let hrtime = process.hrtime(); client.send_command("get",["key"], function(e,r) { let diff = process.hrtime(hrtime); let cost = (diff[0] * NS_PER_SEC + diff[1])/1000000; console.log(`final: ${cost} ms`) res(); }); })); } await Promise.all(arr) console.log("ops/sec:",200*1000/(Date.now() - dd),Date.now() - dd); }
會發(fā)現(xiàn)每個請求的rt都會比前一個請求來的大
](https://si.geilicdn.com/viewm...
最后一個請求的rt竟然達到了257 ms!雖然在node單進程像示例代碼那樣并發(fā)執(zhí)行200次get請求是非常少見而且愚蠢的(關于示例代碼的優(yōu)化在在下節(jié)講述),但是針對這個示例必須找到請求delay增加的原因。
為此繼續(xù)分析,redis client采用的是單連接模式,底層采用的非阻塞網(wǎng)絡I/O,socket.recv()在node層面是通過監(jiān)聽socket的data事件完成的,因此先分析redis-client讀性能如何:
](https://si.geilicdn.com/viewm...
上圖每段日志的含義分別表示:
- data events trigger times: socket data事件觸發(fā)的次數(shù) - data event start from prevent event: data事件距離上次觸發(fā)的時間間隔 - data events exec time(ms): 本次事件處理函數(shù)執(zhí)行時間
上圖只是截取了最初的請求日志,發(fā)現(xiàn)當?shù)?次觸發(fā)data事件時,竟然距離上次觸發(fā)事件隔了35ms,在隨后的請求中會復現(xiàn)這種現(xiàn)象,因此這也就導致了在并發(fā)200次查詢請求時,每個請求的rt都會隨之增大,并且有些響應之間間隔了30ms。
從表象看造成問題在于redis-server發(fā)送的響應不是一個數(shù)據(jù)塊,而是多個數(shù)據(jù)塊導致觸發(fā)socket的data事件過多,而且data事件抖動過大導致響應之間存在30ms的突變(data事件是無法同時觸發(fā)兩次的,每次data事件處理函數(shù)執(zhí)行完后才能繼續(xù)觸發(fā)下一個data事件);當然也有可能和socket寫入(即發(fā)送req)有關,如緩存請求等。為了繼續(xù)探查,監(jiān)控與socket寫入相關的接口 _write(),記錄每次寫入socket的數(shù)據(jù)時距離上一次寫入的間隔:
](https://si.geilicdn.com/viewm...
可見,在使用redis-client發(fā)送請求時,write方法也不是瓶頸。
采用同樣方法,對socket的push()(該方法觸發(fā)socket的data事件)進行監(jiān)控,發(fā)現(xiàn)socket的數(shù)據(jù)到達間隔抖動非常大:
](https://si.geilicdn.com/viewm...
因此,造成redis-client并發(fā)請求下響應rt抖動較大的情況與單連接下響應數(shù)據(jù)到達本地的時刻有關,具體可能與底層libuv的緩存策略有關(筆者并未再往下探查)。
](https://si.geilicdn.com/viewm...
在一個node實例中通過一個單連接與redis server通信,在高并發(fā)下會出現(xiàn)排隊等待響應的情況,并且有可能會出現(xiàn)響應rt雪崩效應(如上文demo所示),因此需要盡可能減少或緩存客戶端的請求數(shù)量,進行批量發(fā)送。
調(diào)優(yōu)1. pipeline(涉及到寫模式及時序) 2. script
對于pipeline方式,redis server是默認支持的。通俗點說,pipeline可以合并一系列請求一次發(fā)送,并將這些請求對應的結果一次性拿到。因此這種方式可以有效減少響應次數(shù),從而減少socket觸發(fā)data事件的次數(shù),盡可能快的拿到響應體。
](https://si.geilicdn.com/viewm...
需要強調(diào)的是,在node中,是通過底層socket的_writev實現(xiàn)一次發(fā)送多條redis命令的,_writev又叫做聚合寫,它支持將不同緩沖區(qū)的多條數(shù)據(jù)通過一次系統(tǒng)調(diào)用寫入目標流,因此性能上比每次寫單個緩沖區(qū)的單個數(shù)據(jù)來的好得多。在node的Writeable對象中,有cork和uncork方法,通過這兩個方法可以在node write stream中緩存多條數(shù)據(jù),通過_writev一次性發(fā)送。
關于 _writev的數(shù)據(jù)結構
redis在拿到數(shù)據(jù)后,根據(jù)resp協(xié)議解析出命令集合緩存在隊列中,直到收到exec命令,開始批量執(zhí)行命令集,并將所有命令執(zhí)行的結果轉(zhuǎn)換為數(shù)組返回給redis client。這樣就可以通過一次寫、一次讀實現(xiàn)高性能I/O。
async ()=>{ let dd = Date.now() let batch = await client.batch(); for(let i=0;i<200;i++){ batch.get("vdWeex_com.koudai.weidian.buyer_1"); } let rt = await batch.exec(); process.exit(); }
而對于script方法,則是由redis client傳入script命令,在server端執(zhí)行script邏輯,批量執(zhí)行命令,并返回結果。同樣是一次寫、一次讀。
收獲1. node socket默認采用writev 集合寫 2. 無依賴批量請求采用pipeline 3. eval script解決有依賴批量請求 4. redis高性能體現(xiàn)在服務端處理能力,但瓶頸往往出現(xiàn)在客戶端,因此增強客戶端I/O能力與并發(fā)并行多客戶端才是高并發(fā)解決方案
文章版權歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/98909.html
摘要:相關環(huán)境由于是一個幾年前的項目,所以使用的是這樣的。一些小提示本次優(yōu)化筆記,并不會有什么文件的展示。將異步改為了串行,喪失了作為異步事件流的優(yōu)勢。 這兩天針對一個Node項目進行了一波代碼層面的優(yōu)化,從響應時間上看,是一次很顯著的提升。 一個純粹給客戶端提供接口的服務,沒有涉及到頁面渲染相關。 背景 首先這個項目是一個幾年前的項目了,期間一直在新增需求,導致代碼邏輯變得也比較復雜,接...
摘要:前言這將是一個分為兩部分,內(nèi)容是關于在生產(chǎn)環(huán)境下,跑應用的最佳實踐。第一部分會關注安全性,第二部分則會關注性能和可靠性。關于第一部分,請參閱在生產(chǎn)環(huán)境下的最佳實踐安全性。 前言 這將是一個分為兩部分,內(nèi)容是關于在生產(chǎn)環(huán)境下,跑Express應用的最佳實踐。第一部分會關注安全性,第二部分則會關注性能和可靠性。當你讀這篇文章時,會假設你已經(jīng)對Node.js和web開發(fā)有所了解,并且對生產(chǎn)環(huán)...
摘要:模式,單實例多進程,常用于多語言混編,比如等,不支持端口復用,需要自己做應用的端口分配和負載均衡的子進程業(yè)務代碼。就是我們需要一個調(diào)度者,保證所有后端服務器都將性能充分發(fā)揮,從而保持服務器集群的整體性能最優(yōu),這就是負載均衡。 showImg(https://segmentfault.com/img/remote/1460000019425391?w=1440&h=1080); Nod...
閱讀 2382·2021-10-09 09:41
閱讀 3172·2021-09-26 09:46
閱讀 835·2021-09-03 10:34
閱讀 3151·2021-08-11 11:22
閱讀 3365·2019-08-30 14:12
閱讀 711·2019-08-26 11:34
閱讀 3344·2019-08-26 11:00
閱讀 1750·2019-08-26 10:26