京東京麥:微服務(wù)架構(gòu)下的高可用網(wǎng)關(guān)與容錯(cuò)實(shí)踐
自微服務(wù)概念誕生以來(lái),眾多的軟件架構(gòu)都在踐行著這一優(yōu)秀的設(shè)計(jì)理念。
各自的系統(tǒng)在這一指導(dǎo)思想下收獲了優(yōu)雅的可維護(hù)性,但一方面也給接口調(diào)用提出了新的要求,比如眾多的 API 調(diào)用急需一個(gè)統(tǒng)一的入口來(lái)支持客戶端的調(diào)用。
在這種情況下 API Gateway 誕生,我們將接入、路由、限流等功能統(tǒng)一由網(wǎng)關(guān)負(fù)責(zé),各自的服務(wù)提供方專(zhuān)注于業(yè)務(wù)邏輯的實(shí)現(xiàn),從而給客戶端調(diào)用提供了一個(gè)穩(wěn)健的服務(wù)調(diào)用環(huán)境。
之后,我們?cè)诰W(wǎng)關(guān)大調(diào)用量的情況下,還要保證網(wǎng)關(guān)的可降級(jí)、可限流、可隔離等等一系列容錯(cuò)能力。
今天跟大家分享微服務(wù)下京麥開(kāi)放平臺(tái)的網(wǎng)關(guān)實(shí)現(xiàn),以及我們?nèi)绾慰沽?、如何在大訪問(wèn)量的情況下做容錯(cuò)處理,重點(diǎn)介紹容錯(cuò)的方法及每種容錯(cuò)方法的使用場(chǎng)景與經(jīng)驗(yàn)。
網(wǎng)關(guān)
這里說(shuō)的網(wǎng)關(guān)是指 API 網(wǎng)關(guān),意思是將所有 API 調(diào)用統(tǒng)一接入到 API 網(wǎng)關(guān)層,由網(wǎng)關(guān)層統(tǒng)一接入和輸出。
一個(gè)網(wǎng)關(guān)的基本功能有如下幾種能力:
- 統(tǒng)一接入
- 安全防護(hù)
- 協(xié)議適配
- 流量管控
- 長(zhǎng)短鏈接支持
- 容錯(cuò)
有了網(wǎng)關(guān)之后,各個(gè) API 服務(wù)提供團(tuán)隊(duì)可以專(zhuān)注于自己的的業(yè)務(wù)邏輯處理,而 API 網(wǎng)關(guān)更專(zhuān)注于安全、流量、路由等問(wèn)題。
單體應(yīng)用
當(dāng)業(yè)務(wù)簡(jiǎn)單、團(tuán)隊(duì)組織很小時(shí),我們常常把功能都集中于一個(gè)應(yīng)用中,統(tǒng)一部署、統(tǒng)一測(cè)試,玩得不亦樂(lè)乎。
但隨著業(yè)務(wù)迅速發(fā)展,組織成員日益增多,我們?cè)賹⑺械墓δ芗械揭粋€(gè) Tomcat 中去,每當(dāng)更新一個(gè)功能模塊,勢(shì)必要更新所有的程序,搞不好還會(huì)牽一發(fā)動(dòng)全身,導(dǎo)致實(shí)在難以維護(hù)的情況。
微服務(wù)
單體應(yīng)用滿足不了我們逐漸增長(zhǎng)的擴(kuò)展需求之后,微服務(wù)就出現(xiàn)了,它是將原來(lái)應(yīng)用集中于一體的架構(gòu)。
比如商品功能、訂單功能、用戶功能拆分出去,各自有各自的自成體系的發(fā)布、運(yùn)維等,這樣就解決了在單體應(yīng)用下的弊端。
API 網(wǎng)關(guān)
進(jìn)行微服務(wù)后,原先客戶端調(diào)用服務(wù)端的地方就要有 N 多個(gè) URL 地址,包括商品的、訂單的、用戶等。
這時(shí)就必須要有個(gè)統(tǒng)一的入口和出口,這種情況下,我們的 API Gateway 就出現(xiàn)了,它很好地幫助我們解決了微服務(wù)下客戶端調(diào)用的問(wèn)題。
泛化調(diào)用
對(duì)于普通的 RPC 調(diào)用,我要拿到服務(wù)端提供的 class 或者 jar 包,這樣過(guò)于繁重,更不好維護(hù)。
不過(guò)成熟的 RPC 框架都支持泛化調(diào)用,我們的網(wǎng)關(guān)就是基于這種泛化調(diào)用來(lái)實(shí)現(xiàn)的。
服務(wù)端開(kāi)放出來(lái)他們的 API 文檔,我們拿到接口、參數(shù)、參數(shù)類(lèi)型通過(guò)泛化調(diào)用到服務(wù)端程序。
public Object $invoke(String method, String[] parameterTypes, Object[] args);
容錯(cuò)
容錯(cuò),這個(gè)詞的理解,書(shū)面意思就是可以容下錯(cuò)誤,不讓錯(cuò)誤再次擴(kuò)張,讓這個(gè)錯(cuò)誤產(chǎn)生的影響在一個(gè)固定的邊界之內(nèi)。
“千里之堤,毀于蟻穴”,我們采用容錯(cuò)的方式就是不讓這種蟻穴繼續(xù)變大。在工作中,降級(jí)、限流、熔斷器、超時(shí)重試等都是常見(jiàn)的容錯(cuò)方法。
抗量
所謂的抗量,就是增大我們系統(tǒng)的吞吐量,所以容錯(cuò)的***步就是系統(tǒng)要能抗量,沒(méi)有量的情況下幾乎用不到容錯(cuò)。
我們的容器使用的是 Tomcat,在傳統(tǒng)的 BIO 模型下,一請(qǐng)求一線程,在機(jī)器線程資源有限的情況下是沒(méi)有辦法來(lái)實(shí)現(xiàn)我們的目標(biāo)的。
NIO 給我們提供了這個(gè)機(jī)會(huì),基于 NIO 的機(jī)制,利用較少的線程來(lái)處理更多的連接。
連接多不可怕,通過(guò)調(diào)整機(jī)器的參數(shù)一臺(tái) 8c8g 的機(jī)器,超過(guò) 10w 是不成問(wèn)題的。
Tomcat 的 Conector 修改成 NIO 后,我們?cè)購(gòu)拇a層面引入了 Servlet3,它是從 Tomcat7 以后支持的,NIO 是 Tomcat6 以后就支持的。
利用 Servlet3 的特性,所有的 request 和 response 都由 Tomcat 的工作線程來(lái)處理,我們將業(yè)務(wù)邏輯異步到別的業(yè)務(wù)線程中去。
在異步環(huán)境下,可以提高單位時(shí)間內(nèi)的吞吐量,所有的 Servlet 請(qǐng)求都是由 Tomcat 的 Executor 線程池的線程處理的,也就是 Tomcat 的工作線程。
這些線程處理的時(shí)間越短越好,越短越能迅速地將線程歸還給 Executor 線程池,現(xiàn)在 Servlet 支持異步后就能將耗時(shí)的操作,比如有 RPC 請(qǐng)求的交給業(yè)務(wù)線程池來(lái)處理,使得 Tomcat 工作線程可立即歸還給 Tomcat 工作線程池。
另外,將業(yè)務(wù)異步處理之后,我們可以對(duì)業(yè)務(wù)線程池進(jìn)行線程池隔離,這樣就避免了因一個(gè)業(yè)務(wù)性能問(wèn)題而影響了其他的業(yè)務(wù)。
總結(jié)一下異步的優(yōu)勢(shì):
- 可以用來(lái)做消息推送,通過(guò) Nginx 做代理,設(shè)置連接超時(shí)時(shí)間,客戶端通過(guò)心跳探測(cè)。
- 提高吞吐量。
- 請(qǐng)求線程和業(yè)務(wù)線程分開(kāi),從而可以通過(guò)業(yè)務(wù)線程池對(duì)業(yè)務(wù)線程做隔離。
脫離 DB
脫離 DB,這里不是說(shuō) DB 的性能不行,分庫(kù)分表、DB 集群化之后,在一定量的情況下是沒(méi)有問(wèn)題的。
但是,如果從抗量的角度說(shuō)的話,為何不使用 Redis 呢?如果軟件架構(gòu)里面有一種銀彈的話,那么 Redis 就是這種銀彈。
另外一個(gè)脫離 DB 的原因是:每當(dāng)大促備戰(zhàn)前夕我們一項(xiàng)重點(diǎn)的工作就是優(yōu)化慢 SQL,但它就像小強(qiáng)一樣生命力是那樣的頑強(qiáng),殺不絕。
如果有那么一個(gè)慢 SQL,平時(shí)是沒(méi)有問(wèn)題的,比如一個(gè)查詢大字段的 SQL,平時(shí)量小不會(huì)暴露問(wèn)題,但量一上來(lái)了,就是個(gè)災(zāi)難。
再就是我們的網(wǎng)關(guān),包括接入、分發(fā)、限流等這些功能都應(yīng)該是很輕的,所以我們就通過(guò)數(shù)據(jù)異構(gòu)的方式把數(shù)據(jù)重新轉(zhuǎn)載到 Redis 中,而且是將數(shù)據(jù)持久化到 Redis 里面去。
當(dāng)然,使用 Redis 的過(guò)程中也需要注意大 key,大訪問(wèn)量下也能讓集群趴下。
還有一個(gè)很重要的原因,我們使用的 DB 是 MySQL,鑒于 MySQL 的 failover 機(jī)制生效時(shí)間總是要長(zhǎng)于 Redis 集群,***就是因?yàn)?DB 切換的時(shí)候,常常伴隨 Web 應(yīng)用服務(wù)器要重啟,將原來(lái)的連接釋放掉,才能方便使用新的數(shù)據(jù)庫(kù)連接。
多級(jí)緩存
最簡(jiǎn)單的緩存就是查一次數(shù)據(jù)庫(kù)然后將數(shù)據(jù)寫(xiě)入緩存。比如在 Redis 中設(shè)置過(guò)期時(shí)間,因?yàn)橛羞^(guò)期失效,因此我們要關(guān)注下緩存的穿透率。
這個(gè)穿透率的計(jì)算公式,比如查詢方法 queryOrder(調(diào)用次數(shù) 1000/1s)里面嵌套查詢 DB 方法 query Product From DB(調(diào)用次數(shù) 300/s),那么 Redis 的穿透率就是 300/1000。
在這種使用緩存的方式下,是要重視穿透率的,穿透率大了說(shuō)明緩存的效果不好。
還有一種使用緩存的方式就是將緩存持久化,也就是不設(shè)置過(guò)期時(shí)間,這個(gè)會(huì)面臨一個(gè)數(shù)據(jù)更新的問(wèn)題。
一般有兩種辦法:
- 利用時(shí)間戳,查詢默認(rèn)以 Redis 為主,每次設(shè)置數(shù)據(jù)的時(shí)候放入一個(gè)時(shí)間戳,每次讀取數(shù)據(jù)的時(shí)候用系統(tǒng)當(dāng)前時(shí)間和上次設(shè)置的這個(gè)時(shí)間戳做對(duì)比。
比如超過(guò) 5 分鐘,那么就再查一次數(shù)據(jù)庫(kù),這樣可以保證 Redis 里面永遠(yuǎn)有數(shù)據(jù),一般是對(duì) DB 的一種容錯(cuò)方法。
- 讓 Redis 真正作為 DB 來(lái)使用,就是如圖里畫(huà)的通過(guò)訂閱數(shù)據(jù)庫(kù)的 binlog,通過(guò)數(shù)據(jù)異構(gòu)系統(tǒng)將數(shù)據(jù)推送給緩存,同時(shí)將緩存設(shè)置為多級(jí)。
可以通過(guò)使用 jvm cache 作為應(yīng)用內(nèi)的一級(jí)緩存,一般是體積小,訪問(wèn)頻率大的更適合這種 jvm cache 方式,將一套 Redis 作為二級(jí) remote 緩存,另外的最外層三級(jí) Redis 作為持久化緩存。
超時(shí)與重試
超時(shí)與重試機(jī)制也是容錯(cuò)的一種方法,凡是發(fā)生 RPC 調(diào)用的地方,比如讀取 Redis、DB、MQ 等。
因?yàn)榫W(wǎng)絡(luò)故障或者是所依賴(lài)的服務(wù)故障了,長(zhǎng)時(shí)間不能返回結(jié)果,就會(huì)導(dǎo)致線程增加,加大 CPU 負(fù)載,甚至導(dǎo)致雪崩。所以對(duì)每一個(gè) RPC 調(diào)用都要設(shè)置超時(shí)時(shí)間。
對(duì)于強(qiáng)依賴(lài) RPC 調(diào)用資源的情況,還要有重試機(jī)制,但重試的次數(shù)建議 1-2 次。
另外如果有重試,超時(shí)時(shí)間還要相應(yīng)都調(diào)小,比如重試 1 次,那么一共是發(fā)生 2 次調(diào)用。
如果超時(shí)時(shí)間配置的是 2s,那么客戶端就要等待 4s 才能返回,因此重試+超時(shí)的方式,超時(shí)時(shí)間要調(diào)小。
這里也再談一下 1 次 PRC 調(diào)用的時(shí)間都消耗在哪些環(huán)節(jié)。
1 次正常的調(diào)用統(tǒng)計(jì)的耗時(shí)主要包括:①調(diào)用端RPC框架執(zhí)行時(shí)間 + ②網(wǎng)絡(luò)發(fā)送時(shí)間 + ③服務(wù)端RPC框架執(zhí)行時(shí)間 + ④服務(wù)端業(yè)務(wù)代碼時(shí)間。
調(diào)用方和服務(wù)方都有各自的性能監(jiān)控,比如調(diào)用方 tp99 是 500ms,服務(wù)方 tp99 是 100ms,找了網(wǎng)絡(luò)組的同事確認(rèn)網(wǎng)絡(luò)沒(méi)有問(wèn)題的。
那么時(shí)間都花在什么地方了呢??jī)煞N原因:客戶端調(diào)用方,還有一個(gè)原因是網(wǎng)絡(luò)發(fā)生 TCP 重傳,所以要注意這兩點(diǎn)。
熔斷
熔斷技術(shù)可以說(shuō)是一種“智能化的容錯(cuò)”,當(dāng)調(diào)用滿足失敗次數(shù),失敗比例就會(huì)觸發(fā)熔斷器打開(kāi),有程序自動(dòng)切斷當(dāng)前的 RPC 調(diào)用,來(lái)防止錯(cuò)誤進(jìn)一步擴(kuò)大。
實(shí)現(xiàn)一個(gè)熔斷器主要是考慮三種模式:
- 關(guān)閉
- 打開(kāi)
- 半開(kāi)
各個(gè)狀態(tài)的轉(zhuǎn)換如下圖:
在了解了熔斷器的狀態(tài)機(jī)制后,我們可以自己來(lái)實(shí)現(xiàn)一個(gè)熔斷器。當(dāng)然也可以使用開(kāi)源的解決方案,比如 Hystrix 中的 breaker。
下圖是一個(gè)熔斷器打開(kāi)關(guān)閉的示意圖:
這里要談的是熔斷器的使用注意項(xiàng):
我們?cè)谔幚懋惓r(shí),要根據(jù)具體的業(yè)務(wù)情況來(lái)決定處理方式。比如我們調(diào)用商品接口,對(duì)方只是臨時(shí)做了降級(jí)處理,那么作為網(wǎng)關(guān)調(diào)用就要切到可替換的服務(wù)上來(lái)執(zhí)行或者獲取托底數(shù)據(jù),給用戶友好提示。
還有要區(qū)分異常的類(lèi)型,比如依賴(lài)的服務(wù)崩潰了,這個(gè)可能需要花費(fèi)比較久的時(shí)間來(lái)解決,也可能是由于服務(wù)器負(fù)載臨時(shí)過(guò)高導(dǎo)致超時(shí)。
作為熔斷器應(yīng)該能夠甄別這種異常類(lèi)型,從而根據(jù)具體的錯(cuò)誤類(lèi)型調(diào)整熔斷策略。
增加手動(dòng)設(shè)置,在失敗的服務(wù)恢復(fù)時(shí)間不確定的情況下,管理員可以手動(dòng)強(qiáng)制切換熔斷狀態(tài)。***,熔斷器的使用場(chǎng)景是調(diào)用可能失敗的遠(yuǎn)程服務(wù)程序或者共享資源。
如果是本地緩存本地私有資源,使用熔斷器則會(huì)增加系統(tǒng)的額外開(kāi)銷(xiāo)。還要注意,熔斷器不能作為應(yīng)用程序中業(yè)務(wù)邏輯的異常處理替代品。
線程池隔離
在抗量這個(gè)環(huán)節(jié),Servlet3 異步時(shí),有提到過(guò)線程隔離。線程隔離的直接優(yōu)勢(shì)就是防止級(jí)聯(lián)故障,甚至是雪崩。
當(dāng)網(wǎng)關(guān)調(diào)用 N 多個(gè)接口服務(wù)的時(shí)候,我們要對(duì)每個(gè)接口進(jìn)行線程隔離,比如我們有調(diào)用訂單、商品、用戶。
那么訂單的業(yè)務(wù)不能夠影響到商品和用戶的請(qǐng)求處理。如果不做線程隔離,當(dāng)訪問(wèn)訂單服務(wù)出現(xiàn)網(wǎng)絡(luò)故障導(dǎo)致延時(shí),線程積壓最終導(dǎo)致整個(gè)服務(wù) CPU 負(fù)載滿。
就是我們說(shuō)的服務(wù)全部不可用了,有多少機(jī)器都會(huì)被此刻的請(qǐng)求塞滿。那么,有了線程隔離就會(huì)使得我們的網(wǎng)關(guān)能保證局部問(wèn)題不會(huì)影響全局。
降級(jí)、限流
關(guān)于降級(jí)限流的方法業(yè)界都已經(jīng)有很成熟的方法了,比如 Failback 機(jī)制,限流方法令牌桶、漏桶、信號(hào)量等,這里談一下我們的一些經(jīng)驗(yàn)。
降級(jí)一般都是由統(tǒng)一配置中心的降級(jí)開(kāi)關(guān)來(lái)實(shí)現(xiàn)的,那么當(dāng)有很多個(gè)接口來(lái)自同一個(gè)提供方,這個(gè)提供方的系統(tǒng)或這機(jī)器所在機(jī)房網(wǎng)絡(luò)出現(xiàn)了問(wèn)題,我們就要有一個(gè)統(tǒng)一的降級(jí)開(kāi)關(guān)。
不然就要一個(gè)接口一個(gè)接口地來(lái)降級(jí),也就是要對(duì)業(yè)務(wù)類(lèi)型有一個(gè)大閘刀。
還有就是降級(jí)切記暴力降級(jí),什么是暴力降級(jí)?比如把論壇功能降調(diào),結(jié)果用戶顯示一個(gè)大白板,我們要實(shí)現(xiàn)緩存住一些數(shù)據(jù),也就是有托底數(shù)據(jù)。
限流一般分為分布式限流和單機(jī)限流,如果實(shí)現(xiàn)分布式限流的話就要一個(gè)公共的后端存儲(chǔ)服務(wù),比如 Redis,在大 Nginx 節(jié)點(diǎn)上利用 Lua 讀取 Redis 配置信息。
我們現(xiàn)在的限流都是單機(jī)限流,并沒(méi)有實(shí)施分布式限流。
網(wǎng)關(guān)監(jiān)控與統(tǒng)計(jì)
API 網(wǎng)關(guān)是一個(gè)串行的調(diào)用,每一步發(fā)生的異常都要記錄下來(lái),統(tǒng)一存儲(chǔ)到一個(gè)地方,比如 Elasticsearch 中,便于后續(xù)對(duì)調(diào)用異常的分析。
鑒于公司 Docker 申請(qǐng)都是統(tǒng)一分配,而且分配之前 Docker 上已經(jīng)存在 3 個(gè) Agent 了,不再允許增加。
我們自己實(shí)現(xiàn)了一個(gè) Agent 程序,來(lái)負(fù)責(zé)采集服務(wù)器上面的日志輸出,然后發(fā)送到 Kafka 集群,再通過(guò) Web 查詢消費(fèi)到 Elasticsearch 中?,F(xiàn)在做的追蹤功能還比較簡(jiǎn)單,這塊還需要繼續(xù)豐富。
總結(jié)
網(wǎng)關(guān)基本功能有統(tǒng)一接入、安全防護(hù)、協(xié)議適配等。這篇文章里我們并沒(méi)有講如何來(lái)實(shí)現(xiàn)這些基本的功能,因?yàn)楝F(xiàn)在有很多成熟的解決方案可以直接拿過(guò)來(lái)使用。
比如 Spring Cloud 這種全家桶里面的很多組件,Mashape 的 API 層 Kong 等。
我們更關(guān)注的是:
- 實(shí)現(xiàn)了這些網(wǎng)關(guān)的基本功能之后,如何保證一個(gè)網(wǎng)關(guān)的運(yùn)行?
- 在大訪問(wèn)量的情況下如何能更好的支持客戶端的調(diào)用?
- 在突發(fā)情況下又是如何及時(shí)地響應(yīng)這種突然的異常?
- 如何將錯(cuò)誤最小化,防止級(jí)聯(lián)故障?
- 重點(diǎn)關(guān)注網(wǎng)關(guān)容錯(cuò)方面的經(jīng)驗(yàn)與實(shí)踐。
王新棟,京東資深架構(gòu)師,從事京麥平臺(tái)的架構(gòu)設(shè)計(jì)與開(kāi)發(fā)工作。熟悉各種開(kāi)源軟件架構(gòu),在 Web 開(kāi)發(fā),架構(gòu)優(yōu)化上有較豐富的實(shí)戰(zhàn)經(jīng)歷。有多年在 NIO 領(lǐng)域的設(shè)計(jì)、開(kāi)發(fā)經(jīng)驗(yàn),對(duì) HTTP、TCP 長(zhǎng)連接技術(shù)有深入研究與領(lǐng)悟,目前主要致力于移動(dòng)與 PC 平臺(tái)網(wǎng)關(guān)技術(shù)的優(yōu)化與實(shí)現(xiàn)。個(gè)人公眾號(hào):程序架道(xindongbook17)。