在一個(gè)處理用戶點(diǎn)擊廣告的高并發(fā)服務(wù)上找到了問題??吹椒?wù)打印的日記后我完全蒙了,全是jedis讀超時(shí),Read time out!一直用的是亞馬遜的redis服務(wù),很難想象Jedis會讀超時(shí)。
成都創(chuàng)新互聯(lián)公司專注于和平企業(yè)網(wǎng)站建設(shè),響應(yīng)式網(wǎng)站,商城網(wǎng)站開發(fā)。和平網(wǎng)站建設(shè)公司,為和平等地區(qū)提供建站服務(wù)。全流程按需求定制開發(fā),專業(yè)設(shè)計(jì),全程項(xiàng)目跟蹤,成都創(chuàng)新互聯(lián)公司專業(yè)和態(tài)度為您提供的服務(wù)
看了服務(wù)的負(fù)載均衡統(tǒng)計(jì),發(fā)現(xiàn)并發(fā)增長了一倍,從每分鐘3到4萬的請求數(shù),增長到8.6萬,很顯然,是并發(fā)翻倍導(dǎo)致的服務(wù)雪崩。
服務(wù)的部署:
處理廣告點(diǎn)擊的服務(wù):2臺2核8g的實(shí)例,每臺部署一個(gè)節(jié)點(diǎn)(服務(wù))。下文統(tǒng)稱服務(wù)A
規(guī)則匹配服務(wù)(Rpc遠(yuǎn)程調(diào)用服務(wù)提供者):2個(gè)節(jié)點(diǎn),2臺2核4g實(shí)例。下文統(tǒng)稱服務(wù)B
還有其它的服務(wù)提供者,但不是影響本次服務(wù)雪崩的兇手,這里就不列舉了。
從日記可以看出的問題:
一是遠(yuǎn)程rpc調(diào)用大量超時(shí),我配置的dubbo參數(shù)是,每個(gè)接口的超時(shí)時(shí)間都是3秒。服務(wù)提供者接口的實(shí)現(xiàn)都是緩存級別的操作,3秒的超時(shí)理論上除了網(wǎng)絡(luò)問題,調(diào)用不應(yīng)該會超過這個(gè)值。在服務(wù)消費(fèi)端,我配置每個(gè)接口與服務(wù)端保持10個(gè)長連接,避免共享一個(gè)長連接導(dǎo)致應(yīng)用層數(shù)據(jù)包排隊(duì)發(fā)送和處理接收。
二是剛說的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套接字會占用一個(gè)文件句柄,有多少個(gè)客戶端連接就占用多少個(gè)文件句柄。我在服務(wù)的啟動腳本上為每個(gè)進(jìn)程配置102400的最大文件打開數(shù),理論上目前不會達(dá)到這個(gè)值。服務(wù)A底層用的是基于Netty實(shí)現(xiàn)的http服務(wù)引擎,沒有限制最大連接數(shù)。
所以,解決服務(wù)雪崩問題就是要圍繞這三個(gè)問題出發(fā)。
第一次是懷疑redis服務(wù)扛不住這么大的并發(fā)請求。估算廣告的一次點(diǎn)擊需要執(zhí)行20次get操作從redis獲取數(shù)據(jù),那么每分鐘8w并發(fā),就需要執(zhí)行160w次get請求,而redis除了本文提到的服務(wù)A和服務(wù)B用到外,還有其它兩個(gè)并發(fā)量高的服務(wù)在用,保守估計(jì),redis每分鐘需要承受300w的讀寫請求。轉(zhuǎn)為每秒就是5w的請求,與理論值redis每秒可以處理超過 10萬次讀寫操作已經(jīng)過半。
由于歷史原因,redis使用的還是2.x版本的,用的一主一從,jedis配置連接池是讀寫分離的連接池,也就是寫請求打到主節(jié)點(diǎn),讀請求打到從節(jié)點(diǎn),每秒接近5w讀請求只有一個(gè)redis從節(jié)點(diǎn)處理,非常的吃力。所以我們將redis升級到4.x版本,并由主從集群改為分布式集群,兩主無從。別問兩主無從是怎么做到的,我也不懂,只有亞馬遜清楚。
Redis升級后,理論上,兩個(gè)主節(jié)點(diǎn),分槽位后請求會平攤到兩個(gè)節(jié)點(diǎn)上,性能會好很多。但好景不長,服務(wù)重新上線一個(gè)小時(shí)不到,并發(fā)又突增到了六七萬每分鐘,這次是大量的RPC遠(yuǎn)程調(diào)用超時(shí),已經(jīng)沒有jedis的讀超時(shí)Read time out了,相比之前好了點(diǎn),至少不用再給Redis加節(jié)點(diǎn)。
這次的事故是并發(fā)量超過臨界值,超過redis的實(shí)際最大qps(跟存儲的數(shù)據(jù)結(jié)構(gòu)和數(shù)量有關(guān)),雖然升級后沒有Read time out! 但Jedis的Get讀操作還是很耗時(shí),這才是罪魁禍?zhǔn)?。Redis的命令耗時(shí)與Jedis的讀操作Read time out不同。
redis執(zhí)行一條命令的過程是:
接收客戶端請求
進(jìn)入隊(duì)列等待執(zhí)行
執(zhí)行命令
由于redis執(zhí)行命令是單線程的,所以命令到達(dá)服務(wù)端后不是立即執(zhí)行,而是進(jìn)入隊(duì)列等待。redis慢查詢?nèi)沼浻涗泂lowlog get的是執(zhí)行命令的耗時(shí),對應(yīng)步驟3,執(zhí)行命令耗時(shí)是根據(jù)key去找到數(shù)據(jù)所在的內(nèi)存地址這段時(shí)間的耗時(shí),所以這對于key-value字符串類型的命令而言,并不會因?yàn)関alue的大小而導(dǎo)致命令耗時(shí)長。
為驗(yàn)證這個(gè)觀點(diǎn),我進(jìn)行了簡單的測試。
分別寫入四個(gè)key,每個(gè)key對應(yīng)的value長度都不等,一個(gè)比一個(gè)長。再來看下兩組查詢?nèi)沼?。先通過CONFIG SET slowlog-log-slower-than 0命令,讓每條命令都記錄耗時(shí)。
key_4的value長度比key_3的長兩倍,但get耗時(shí)比key_3少,而key_1的value長度比key_2短,但耗時(shí)比key_2長。
第二組數(shù)據(jù)也是一樣的,跟value的值大小無關(guān)。所以可以排除項(xiàng)目中因value長度過長導(dǎo)致的slowlog記錄到慢查詢問題。慢操作應(yīng)該是set、hset、hmset、hget、hgetall等命令耗時(shí)比較長導(dǎo)致。
而Jedis的Read time out則是包括1、2、3、4步驟,從命令的發(fā)出到接收完成Redis服務(wù)端的響應(yīng)結(jié)果,超時(shí)原因有兩大原因:
redis的并發(fā)量增加,導(dǎo)致命令等待隊(duì)列過長,等待時(shí)間長
所以將Redis從一主一從改為兩主之后,導(dǎo)致Jedis的Read time out的原因一有所緩解,分?jǐn)偭瞬糠謮毫?。但是原?還是存在,耗時(shí)依然是問題。
Jedis的get耗時(shí)長導(dǎo)致服務(wù)B接口執(zhí)行耗時(shí)超過設(shè)置的3s。由于dubbo消費(fèi)端超時(shí)放棄請求,但是請求已經(jīng)發(fā)出,就算消費(fèi)端取消,提供者無法感知服務(wù)端超時(shí)放棄了,還是要執(zhí)行完一次調(diào)用的業(yè)務(wù)邏輯,就像說出去的話收不回來一樣。
由于dubbo有重試機(jī)制,默認(rèn)會重試兩次,所以并發(fā)8w對于服務(wù)b而言,就變成了并發(fā)24w。最后導(dǎo)致業(yè)務(wù)線程池一直被占用狀態(tài),RPC遠(yuǎn)程調(diào)用又多出了一個(gè)異常,就是遠(yuǎn)程服務(wù)線程池已滿,直接響應(yīng)失敗。
問題最終還是要回到Redis上,就是key對應(yīng)的value太大,傳輸耗時(shí),最終業(yè)務(wù)代碼拿到value后將value分割成數(shù)組,判斷請求參數(shù)是否在數(shù)組中,非常耗時(shí),就會導(dǎo)致服務(wù)B接口耗時(shí)超過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ù)場景,從緩存中獲取到的字符串
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();
}
}
}
這段代碼很簡單,就是模擬高并發(fā),把200個(gè)業(yè)務(wù)線程全部耗盡的場景下,一個(gè)簡單的判斷元素是否存在的業(yè)務(wù)邏輯執(zhí)行需要多長時(shí)間。把這段代碼跑一遍,你會發(fā)現(xiàn)很多執(zhí)行耗時(shí)超過1500ms,再加上Jedis讀取到數(shù)據(jù)的耗時(shí),直接導(dǎo)致接口執(zhí)行耗時(shí)超過3000ms。
這段代碼不僅耗時(shí),還很耗內(nèi)存,沒錯(cuò),就是這個(gè)Bug了。改進(jìn)就是將id拼接成字符串的存儲方式改為hash存儲,直接hget方式判斷一個(gè)元素是否存在,不需要將這么大的數(shù)據(jù)讀取到本地,即避免了網(wǎng)絡(luò)傳輸消耗,也優(yōu)化了接口的執(zhí)行速度。
由于并發(fā)量的增長,導(dǎo)致redis讀并發(fā)上升,Jedis的get耗時(shí)長,加上業(yè)務(wù)代碼的缺陷,導(dǎo)致服務(wù)B接口耗時(shí)長,從而導(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)緩存,請求判斷某個(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ù)。這里就不多說了。
三是Dubbo調(diào)優(yōu),將Dubbo的重試次數(shù)改為0,失敗直接放棄當(dāng)前的廣告點(diǎn)擊請求。為避免突發(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)
所以,緩存并不是簡單的Get,Set就行了,Redis提供這么多的數(shù)據(jù)結(jié)構(gòu)的支持要用好,結(jié)合業(yè)務(wù)邏輯優(yōu)化緩存結(jié)構(gòu)。避免高并發(fā)接口讀取的緩存value過長,導(dǎo)致數(shù)據(jù)傳輸耗時(shí)。同時(shí),Redis的特性也要清楚,分布式集群相比單一主從集群的優(yōu)點(diǎn)。反省img。
經(jīng)過兩次的項(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),但還是有還多歷史遺留的問題不能很好的解決。同時(shí),分布式也帶來了很多問題,總之,有利必有弊。
有時(shí)候就需要這樣,被項(xiàng)目推著往前走。在未發(fā)生該事故之前,我花一個(gè)月時(shí)間也沒想出困擾我的兩大難題,是這次的事故,讓我從一個(gè)短暫的夜晚找出答案,一個(gè)通宵讓我想通很多問題。
本文名稱:我所經(jīng)歷的一次Dubbo服務(wù)雪崩,這是一個(gè)漫長的故事
本文來源:http://chinadenli.net/article2/ppceic.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供營銷型網(wǎng)站建設(shè)、域名注冊、網(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)請盡快告知,我們將會在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)