在一個(gè)處理用戶點(diǎn)擊廣告的高并發(fā)服務(wù)上找到了問(wèn)題。看到服務(wù)打印的日記后我完全蒙了,全是jedis讀超時(shí),Read time out!一直用的是亞馬遜的redis服務(wù),很難想象Jedis會(huì)讀超時(shí)。
成都創(chuàng)新互聯(lián)公司專注于和平企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站,商城網(wǎng)站開(kāi)發(fā)。和平網(wǎng)站建設(shè)公司,為和平等地區(qū)提供建站服務(wù)。全流程按需求定制開(kāi)發(fā),專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,成都創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務(wù)
看了服務(wù)的負(fù)載均衡統(tǒng)計(jì),發(fā)現(xiàn)并發(fā)增長(zhǎng)了一倍,從每分鐘3到4萬(wàn)的請(qǐng)求數(shù),增長(zhǎng)到8.6萬(wàn),很顯然,是并發(fā)翻倍導(dǎo)致的服務(wù)雪崩。
服務(wù)的部署:
處理廣告點(diǎn)擊的服務(wù):2臺(tái)2核8g的實(shí)例,每臺(tái)部署一個(gè)節(jié)點(diǎn)(服務(wù))。下文統(tǒng)稱服務(wù)A
規(guī)則匹配服務(wù)(Rpc遠(yuǎn)程調(diào)用服務(wù)提供者):2個(gè)節(jié)點(diǎn),2臺(tái)2核4g實(shí)例。下文統(tǒng)稱服務(wù)B
還有其它的服務(wù)提供者,但不是影響本次服務(wù)雪崩的兇手,這里就不列舉了。
從日記可以看出的問(wèn)題:
一是遠(yuǎn)程rpc調(diào)用大量超時(shí),我配置的dubbo參數(shù)是,每個(gè)接口的超時(shí)時(shí)間都是3秒。服務(wù)提供者接口的實(shí)現(xiàn)都是緩存級(jí)別的操作,3秒的超時(shí)理論上除了網(wǎng)絡(luò)問(wèn)題,調(diào)用不應(yīng)該會(huì)超過(guò)這個(gè)值。在服務(wù)消費(fèi)端,我配置每個(gè)接口與服務(wù)端保持10個(gè)長(zhǎng)連接,避免共享一個(gè)長(zhǎng)連接導(dǎo)致應(yīng)用層數(shù)據(jù)包排隊(duì)發(fā)送和處理接收。
二是剛說(shuō)的Jedis讀操作超時(shí),Jedis我配置每個(gè)服務(wù)節(jié)點(diǎn)200個(gè)最小連接數(shù)的連接池,這是根據(jù)netty工作線程數(shù)配置的,即讀寫操作就算200個(gè)線程并發(fā)執(zhí)行,也能為每個(gè)線程分配一個(gè)連接。這是我設(shè)置Jedis連接池連接數(shù)的依據(jù)。
三是文件句柄數(shù)達(dá)到上線。SocketChannel套接字會(huì)占用一個(gè)文件句柄,有多少個(gè)客戶端連接就占用多少個(gè)文件句柄。我在服務(wù)的啟動(dòng)腳本上為每個(gè)進(jìn)程配置102400的最大文件打開(kāi)數(shù),理論上目前不會(huì)達(dá)到這個(gè)值。服務(wù)A底層用的是基于Netty實(shí)現(xiàn)的http服務(wù)引擎,沒(méi)有限制最大連接數(shù)。
所以,解決服務(wù)雪崩問(wèn)題就是要圍繞這三個(gè)問(wèn)題出發(fā)。
第一次是懷疑redis服務(wù)扛不住這么大的并發(fā)請(qǐng)求。估算廣告的一次點(diǎn)擊需要執(zhí)行20次get操作從redis獲取數(shù)據(jù),那么每分鐘8w并發(fā),就需要執(zhí)行160w次get請(qǐng)求,而redis除了本文提到的服務(wù)A和服務(wù)B用到外,還有其它兩個(gè)并發(fā)量高的服務(wù)在用,保守估計(jì),redis每分鐘需要承受300w的讀寫請(qǐng)求。轉(zhuǎn)為每秒就是5w的請(qǐng)求,與理論值redis每秒可以處理超過(guò) 10萬(wàn)次讀寫操作已經(jīng)過(guò)半。
由于歷史原因,redis使用的還是2.x版本的,用的一主一從,jedis配置連接池是讀寫分離的連接池,也就是寫請(qǐng)求打到主節(jié)點(diǎn),讀請(qǐng)求打到從節(jié)點(diǎn),每秒接近5w讀請(qǐng)求只有一個(gè)redis從節(jié)點(diǎn)處理,非常的吃力。所以我們將redis升級(jí)到4.x版本,并由主從集群改為分布式集群,兩主無(wú)從。別問(wèn)兩主無(wú)從是怎么做到的,我也不懂,只有亞馬遜清楚。
Redis升級(jí)后,理論上,兩個(gè)主節(jié)點(diǎn),分槽位后請(qǐng)求會(huì)平攤到兩個(gè)節(jié)點(diǎn)上,性能會(huì)好很多。但好景不長(zhǎng),服務(wù)重新上線一個(gè)小時(shí)不到,并發(fā)又突增到了六七萬(wàn)每分鐘,這次是大量的RPC遠(yuǎn)程調(diào)用超時(shí),已經(jīng)沒(méi)有jedis的讀超時(shí)Read time out了,相比之前好了點(diǎn),至少不用再給Redis加節(jié)點(diǎn)。
這次的事故是并發(fā)量超過(guò)臨界值,超過(guò)redis的實(shí)際最大qps(跟存儲(chǔ)的數(shù)據(jù)結(jié)構(gòu)和數(shù)量有關(guān)),雖然升級(jí)后沒(méi)有Read time out! 但Jedis的Get讀操作還是很耗時(shí),這才是罪魁禍?zhǔn)住edis的命令耗時(shí)與Jedis的讀操作Read time out不同。
redis執(zhí)行一條命令的過(guò)程是:
接收客戶端請(qǐng)求
進(jìn)入隊(duì)列等待執(zhí)行
執(zhí)行命令
由于redis執(zhí)行命令是單線程的,所以命令到達(dá)服務(wù)端后不是立即執(zhí)行,而是進(jìn)入隊(duì)列等待。redis慢查詢?nèi)沼浻涗泂lowlog get的是執(zhí)行命令的耗時(shí),對(duì)應(yīng)步驟3,執(zhí)行命令耗時(shí)是根據(jù)key去找到數(shù)據(jù)所在的內(nèi)存地址這段時(shí)間的耗時(shí),所以這對(duì)于key-value字符串類型的命令而言,并不會(huì)因?yàn)関alue的大小而導(dǎo)致命令耗時(shí)長(zhǎng)。
為驗(yàn)證這個(gè)觀點(diǎn),我進(jìn)行了簡(jiǎn)單的測(cè)試。
分別寫入四個(gè)key,每個(gè)key對(duì)應(yīng)的value長(zhǎng)度都不等,一個(gè)比一個(gè)長(zhǎng)。再來(lái)看下兩組查詢?nèi)沼洝O韧ㄟ^(guò)CONFIG SET slowlog-log-slower-than 0命令,讓每條命令都記錄耗時(shí)。
key_4的value長(zhǎng)度比key_3的長(zhǎng)兩倍,但get耗時(shí)比key_3少,而key_1的value長(zhǎng)度比key_2短,但耗時(shí)比key_2長(zhǎng)。
第二組數(shù)據(jù)也是一樣的,跟value的值大小無(wú)關(guān)。所以可以排除項(xiàng)目中因value長(zhǎng)度過(guò)長(zhǎng)導(dǎo)致的slowlog記錄到慢查詢問(wèn)題。慢操作應(yīng)該是set、hset、hmset、hget、hgetall等命令耗時(shí)比較長(zhǎng)導(dǎo)致。
而Jedis的Read time out則是包括1、2、3、4步驟,從命令的發(fā)出到接收完成Redis服務(wù)端的響應(yīng)結(jié)果,超時(shí)原因有兩大原因:
redis的并發(fā)量增加,導(dǎo)致命令等待隊(duì)列過(guò)長(zhǎng),等待時(shí)間長(zhǎng)
所以將Redis從一主一從改為兩主之后,導(dǎo)致Jedis的Read time out的原因一有所緩解,分?jǐn)偭瞬糠謮毫Α5窃?還是存在,耗時(shí)依然是問(wèn)題。
Jedis的get耗時(shí)長(zhǎng)導(dǎo)致服務(wù)B接口執(zhí)行耗時(shí)超過(guò)設(shè)置的3s。由于dubbo消費(fèi)端超時(shí)放棄請(qǐng)求,但是請(qǐng)求已經(jīng)發(fā)出,就算消費(fèi)端取消,提供者無(wú)法感知服務(wù)端超時(shí)放棄了,還是要執(zhí)行完一次調(diào)用的業(yè)務(wù)邏輯,就像說(shuō)出去的話收不回來(lái)一樣。
由于dubbo有重試機(jī)制,默認(rèn)會(huì)重試兩次,所以并發(fā)8w對(duì)于服務(wù)b而言,就變成了并發(fā)24w。最后導(dǎo)致業(yè)務(wù)線程池一直被占用狀態(tài),RPC遠(yuǎn)程調(diào)用又多出了一個(gè)異常,就是遠(yuǎn)程服務(wù)線程池已滿,直接響應(yīng)失敗。
問(wèn)題最終還是要回到Redis上,就是key對(duì)應(yīng)的value太大,傳輸耗時(shí),最終業(yè)務(wù)代碼拿到value后將value分割成數(shù)組,判斷請(qǐng)求參數(shù)是否在數(shù)組中,非常耗時(shí),就會(huì)導(dǎo)致服務(wù)B接口耗時(shí)超過(guò)3s,從而拖垮整個(gè)服務(wù)。
模擬服務(wù)B接口做的事情,業(yè)務(wù)代碼(1)。
/**
* @author wujiuye
* @version 1.0 on 2019/10/20 {描述:}
*/
public class Match {
static class Task implements Runnable {
private String value;
public Task(String value) {
this.value = value;
}
@Override
public void run() {
for (; ; ) {
// 模擬jedis get耗時(shí)
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
// =====> 實(shí)際業(yè)務(wù)代碼
long start = System.currentTimeMillis();
List<String> ids = Arrays.stream(value.split(",")).collect(Collectors.toList());
boolean exist = ids.contains("4029000");
// ====> 輸出結(jié)果,耗時(shí)171ms .
System.out.println("exist:" + exist + ",time:" + (System.currentTimeMillis() - start));
}
}
}
;
public static void main(String[] args) {
// ====> 模擬業(yè)務(wù)場(chǎng)景,從緩存中獲取到的字符串
StringBuilder value = new StringBuilder();
for (int i = 4000000; i <= 4029000; i++) {
value.append(String.valueOf(i)).append(",");
}
String strValue = value.toString();
System.out.println(strValue.length());
for (int i = 0; i < 200; i++) {
new Thread(new Task(strValue)).start();
}
}
}這段代碼很簡(jiǎn)單,就是模擬高并發(fā),把200個(gè)業(yè)務(wù)線程全部耗盡的場(chǎng)景下,一個(gè)簡(jiǎn)單的判斷元素是否存在的業(yè)務(wù)邏輯執(zhí)行需要多長(zhǎng)時(shí)間。把這段代碼跑一遍,你會(huì)發(fā)現(xiàn)很多執(zhí)行耗時(shí)超過(guò)1500ms,再加上Jedis讀取到數(shù)據(jù)的耗時(shí),直接導(dǎo)致接口執(zhí)行耗時(shí)超過(guò)3000ms。
這段代碼不僅耗時(shí),還很耗內(nèi)存,沒(méi)錯(cuò),就是這個(gè)Bug了。改進(jìn)就是將id拼接成字符串的存儲(chǔ)方式改為hash存儲(chǔ),直接hget方式判斷一個(gè)元素是否存在,不需要將這么大的數(shù)據(jù)讀取到本地,即避免了網(wǎng)絡(luò)傳輸消耗,也優(yōu)化了接口的執(zhí)行速度。
由于并發(fā)量的增長(zhǎng),導(dǎo)致redis讀并發(fā)上升,Jedis的get耗時(shí)長(zhǎng),加上業(yè)務(wù)代碼的缺陷,導(dǎo)致服務(wù)B接口耗時(shí)長(zhǎng),從而導(dǎo)致服務(wù)A遠(yuǎn)程RPC調(diào)用超時(shí),導(dǎo)致dubbo超時(shí)重試,導(dǎo)致服務(wù)B并發(fā)乘3,再導(dǎo)致服務(wù)B業(yè)務(wù)線程池全是工作狀態(tài)以及Redis并發(fā)又增加,導(dǎo)致服務(wù)A調(diào)用異常。正是這種連環(huán)效應(yīng)導(dǎo)致服務(wù)雪崩。
最后優(yōu)化分三步
一是優(yōu)化數(shù)據(jù)的redis緩存的結(jié)構(gòu),剛也提到,由大量id拼接成字符串的key-value改成hash結(jié)構(gòu)緩存,請(qǐng)求判斷某個(gè)id是否在緩存中用hget,除了能降低redis的大value傳輸耗時(shí),也能將判斷一個(gè)元素是否存在的時(shí)間復(fù)雜度從O(n)變?yōu)镺(1),接口耗時(shí)降低,消除RPC遠(yuǎn)程調(diào)用超時(shí)。
二是業(yè)務(wù)邏輯優(yōu)化,降低Redis并發(fā)。將服務(wù)B由一個(gè)服務(wù)拆分成兩個(gè)服務(wù)。這里就不多說(shuō)了。
三是Dubbo調(diào)優(yōu),將Dubbo的重試次數(shù)改為0,失敗直接放棄當(dāng)前的廣告點(diǎn)擊請(qǐng)求。為避免突發(fā)性的并發(fā)量上升,導(dǎo)致服務(wù)雪崩,為服務(wù)提供者加入熔斷器,估算服務(wù)所能承受的最大QPS,當(dāng)服務(wù)達(dá)到臨界值時(shí),放棄處理遠(yuǎn)程RPC調(diào)用。
(我用的是Sentinel,官方文檔傳送門:
https://github.com/alibaba/Sentinel/wiki/%E6%8E%A7%E5%88%B6%E5%8F%B0)
所以,緩存并不是簡(jiǎn)單的Get,Set就行了,Redis提供這么多的數(shù)據(jù)結(jié)構(gòu)的支持要用好,結(jié)合業(yè)務(wù)邏輯優(yōu)化緩存結(jié)構(gòu)。避免高并發(fā)接口讀取的緩存value過(guò)長(zhǎng),導(dǎo)致數(shù)據(jù)傳輸耗時(shí)。同時(shí),Redis的特性也要清楚,分布式集群相比單一主從集群的優(yōu)點(diǎn)。反省img。
經(jīng)過(guò)兩次的項(xiàng)目重構(gòu),項(xiàng)目已經(jīng)是分布式微服務(wù)架構(gòu),同時(shí)業(yè)務(wù)的合理劃分讓各個(gè)服務(wù)之間完美解耦,每個(gè)服務(wù)內(nèi)部的實(shí)現(xiàn)合理利用設(shè)計(jì)模式,完成業(yè)務(wù)的高內(nèi)聚低耦合,這是一次非常大的改進(jìn),但還是有還多歷史遺留的問(wèn)題不能很好的解決。同時(shí),分布式也帶來(lái)了很多問(wèn)題,總之,有利必有弊。
有時(shí)候就需要這樣,被項(xiàng)目推著往前走。在未發(fā)生該事故之前,我花一個(gè)月時(shí)間也沒(méi)想出困擾我的兩大難題,是這次的事故,讓我從一個(gè)短暫的夜晚找出答案,一個(gè)通宵讓我想通很多問(wèn)題。
本文名稱:我所經(jīng)歷的一次Dubbo服務(wù)雪崩,這是一個(gè)漫長(zhǎng)的故事
本文來(lái)源:http://chinadenli.net/article2/ppceic.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供營(yíng)銷型網(wǎng)站建設(shè)、域名注冊(cè)、網(wǎng)站內(nèi)鏈、自適應(yīng)網(wǎng)站、Google、外貿(mào)網(wǎng)站建設(shè)
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)