億級(jí)流量架構(gòu)演進(jìn)實(shí)戰(zhàn):從零構(gòu)建億級(jí)流量 API 網(wǎng)關(guān)
這不是一個(gè)講概念的專欄,而且我也不擅長(zhǎng)講概念,每一篇文章都是一個(gè)故事,我希望你可以通過(guò)這些故事了解我當(dāng)時(shí)在實(shí)際工作中遇到問(wèn)題和背后的思考,架構(gòu)設(shè)計(jì)是種經(jīng)驗(yàn),我有幸參與到多個(gè)億級(jí)系統(tǒng)的架構(gòu)設(shè)計(jì)中,有所收獲的同時(shí)也希望把這些收獲分享與大家。
2013年,我在做 APP 服務(wù)端的平臺(tái)化轉(zhuǎn)型,故事就從這里開(kāi)始。
在最開(kāi)始做網(wǎng)關(guān)時(shí),我并沒(méi)有一開(kāi)始就明確說(shuō)要做個(gè) API 網(wǎng)關(guān),而是做著做著發(fā)現(xiàn)這是個(gè)網(wǎng)關(guān)。因?yàn)楫?dāng)時(shí)我是在做服務(wù)端的平臺(tái)化轉(zhuǎn)型,最開(kāi)始時(shí)只是提供了客戶端登錄、獲取插件列表、插件啟動(dòng)授權(quán)幾個(gè)簡(jiǎn)單的 API,其中客戶端登錄是通過(guò) RSA 和 AES 非對(duì)稱加密算法來(lái)實(shí)現(xiàn),登錄之后平臺(tái)頒發(fā) token 給客戶端,有了 token 之后,客戶端就通過(guò) OAuth 2.0 協(xié)議來(lái)調(diào)用獲取插件列表、插件啟動(dòng)授權(quán)等 API,不過(guò)由于最開(kāi)始沒(méi)想清楚,提供出去的 API 接口定義和格式不統(tǒng)一,雖然都是 json 格式,但幾乎每個(gè) API 都有自己的的格式定義,即每個(gè) method 在服務(wù)端都實(shí)現(xiàn)了一個(gè) Servlet 服務(wù),客戶端天天是要這接口要那接口,搞了上百個(gè)接口還是被客戶端碾著走,更糟糕的是代碼越來(lái)越臃腫還老出問(wèn)題。
后來(lái)就想為何不把接口定義和格式統(tǒng)一了,就只提供一個(gè) Serlvet 服務(wù),通過(guò)解析 API 接口參數(shù)在后端進(jìn)行服務(wù)的分發(fā),這樣至少可以減少每個(gè) API 都寫一遍 Servlet 的工作,當(dāng)時(shí)的這個(gè)架構(gòu)是 C/S 的架構(gòu),客戶端通過(guò)公網(wǎng)訪問(wèn)彈內(nèi)的服務(wù)器,這個(gè)功能上線其實(shí)是上線了一個(gè)新的 API,之后客戶端的新功能都必須使用新的 API,老的 API 在客戶端線上的版本逐步下線后,服務(wù)端再對(duì)老的 API 進(jìn)行清理,當(dāng)整個(gè)架構(gòu)逐漸形成之后,服務(wù)端的開(kāi)發(fā)效率得到了顯著的提升,也是這時(shí),我覺(jué)得這其實(shí)是個(gè)網(wǎng)關(guān)的雛形,所以整個(gè)平臺(tái)演進(jìn)的過(guò)程,在這一階段我總結(jié)為:統(tǒng)一服務(wù)接口。
1. 什么是網(wǎng)關(guān)?
現(xiàn)在來(lái)談?wù)?API 網(wǎng)關(guān),關(guān)于 API 網(wǎng)關(guān)的定義,有很多的說(shuō)法,其字面意思就是系統(tǒng)的統(tǒng)一 API 入口。說(shuō)白了, 就是將客戶端的所有請(qǐng)求統(tǒng)一通過(guò) API 網(wǎng)關(guān)接入服務(wù)端,并完成認(rèn)證、授權(quán)、安全、流控、熔斷、調(diào)度、轉(zhuǎn)發(fā)、監(jiān) 控等處理過(guò)程。API 網(wǎng)關(guān)的價(jià)值,就是為實(shí)現(xiàn)更加安全、高效和穩(wěn)定的 API 調(diào)用提供服務(wù)保障。
就我當(dāng)時(shí)負(fù)責(zé)的平臺(tái)而言,統(tǒng)一了服務(wù)接口還不能說(shuō)是做了一個(gè)網(wǎng)關(guān),因?yàn)檫@僅僅是實(shí)現(xiàn)了網(wǎng)關(guān)統(tǒng)一接入組件的一個(gè)點(diǎn),那網(wǎng)關(guān)的統(tǒng)一接入組件又是什么?下面我們先聊下網(wǎng)關(guān)的每一個(gè)組件,以及每一個(gè)組件的職責(zé)。
API 網(wǎng)關(guān)的核心組件
從 API 調(diào)用的過(guò)程來(lái)看,我把 API 網(wǎng)關(guān)劃分為四個(gè)組件:
-
統(tǒng)一接入組件,管理所有的請(qǐng)求接入,負(fù)責(zé)認(rèn)證鑒權(quán)、安全、校驗(yàn)、限流、降級(jí)和熔斷等,它就像 API 網(wǎng)關(guān)的護(hù)城河;
-
服務(wù)調(diào)度組件,管理請(qǐng)求的路由和調(diào)度,負(fù)責(zé)協(xié)議解析、路由、轉(zhuǎn)換、映射和服務(wù)編排等,它是外部請(qǐng)求調(diào)度后端服務(wù)的中間樞紐,也是 API 網(wǎng)關(guān)的大腦(只有大腦才知道哪個(gè) API 應(yīng)去哪里調(diào)度);
-
服務(wù)發(fā)布組件,管理 API 的注冊(cè)和訂閱,負(fù)責(zé)服務(wù)發(fā)現(xiàn)、服務(wù)訂閱和服務(wù)更新等,它是 API 網(wǎng)關(guān)的心臟(心臟會(huì)不斷的把 API 信息同步給網(wǎng)關(guān));
-
服務(wù)監(jiān)控組件,是對(duì)所有 API 請(qǐng)求的統(tǒng)一監(jiān)控,負(fù)責(zé)日志、監(jiān)控、告警和統(tǒng)計(jì)分析等,它是 API 網(wǎng)關(guān)的守衛(wèi)。
這里我畫(huà)了一張 API 網(wǎng)關(guān)的架構(gòu)示意圖。
統(tǒng)一接入組件
當(dāng)時(shí),統(tǒng)一了服務(wù)接口的確實(shí)現(xiàn)了 API 的統(tǒng)一接入點(diǎn),但很快也暴露出了新的問(wèn)題 —— 這個(gè)接入點(diǎn)很快就過(guò)熱了,之前的登錄 API 和插件 API 都是分開(kāi)的,現(xiàn)在統(tǒng)一后,有些 API 出故障后影響面很大,印象非常深刻的一次是客戶端上線了一個(gè)定時(shí)查詢待出庫(kù)訂單數(shù)的功能,結(jié)果整個(gè)服務(wù)端全面打爆,服務(wù)重啟很快又被打爆,這其實(shí)是統(tǒng)一之后服務(wù)端沒(méi)有及時(shí)跟上必要的限流、熔斷等防御手段。
所以,那次之后,服務(wù)端進(jìn)行了第一次的系統(tǒng)拆分 —— 網(wǎng)關(guān)和服務(wù)中心。
2. 分層架構(gòu)
平臺(tái)提供的所有端能力進(jìn)行服務(wù)下沉,搭建服務(wù)中心新系統(tǒng),原系統(tǒng)作為網(wǎng)關(guān)將重點(diǎn)負(fù)責(zé) API 接入、安全、流控、熔斷、路由、分發(fā)、調(diào)度、監(jiān)控等功能。除了垂直拆分,還做了水平拆分,即對(duì)平臺(tái) API 和業(yè)務(wù) API 進(jìn)行了隔離,簡(jiǎn)單說(shuō),就是提供了兩個(gè) Servlet。當(dāng)時(shí),還沒(méi)有微服務(wù)化的概念,只是想著隔離平臺(tái)調(diào)用與業(yè)務(wù)調(diào)用的相互影響,能解決當(dāng)時(shí)的問(wèn)題。后來(lái),在認(rèn)識(shí)了微服務(wù)之后,有一種后知后覺(jué)的感覺(jué),這次系統(tǒng)的拆分使得平臺(tái)整體的穩(wěn)定性得到很大的提升,不過(guò)后來(lái)玩微服務(wù)有點(diǎn)玩壞了,而這就是后話了。
重構(gòu)之后的網(wǎng)關(guān)架構(gòu)比較整潔,在實(shí)現(xiàn)上,統(tǒng)一接入組件采用的是類似于責(zé)任鏈的方式,由于這時(shí)期的 API 調(diào)用主要是 HTTP 請(qǐng)求,所以網(wǎng)關(guān)是基于 Servlet 來(lái)提供 API 服務(wù)的,通過(guò)攔截器進(jìn)行安全、流控、熔斷等功能的實(shí)現(xiàn)。
其中 FrequencyPipe 是負(fù)責(zé)流控和熔斷的攔截器,這里必須得說(shuō)一下,畢竟是這里栽了跟頭。常見(jiàn)的限流算法有漏斗算法和令牌桶算法,我的理解,令牌桶常用于控制并發(fā),無(wú)論何時(shí),令牌的總數(shù)是固定的,每次調(diào)用開(kāi)始都需要申請(qǐng),調(diào)用結(jié)束都需要釋放;漏桶適用于控制 QPS,漏桶可以在每秒生成 m 個(gè)令牌,每次調(diào)用開(kāi)始都需要申請(qǐng),但調(diào)用結(jié)束不需要釋放,不過(guò)問(wèn)題就是如果上一秒的調(diào)用沒(méi)有結(jié)束,實(shí)際調(diào)用會(huì)大于當(dāng)前生成的 m 個(gè)令牌控制的調(diào)用量。
在實(shí)現(xiàn)上,當(dāng)時(shí)了解 Guava 的 RateLimiter 與 Semaphore 都可以實(shí)現(xiàn),通過(guò)對(duì)比,網(wǎng)關(guān)使用的是 Guava 的 Semaphore 令牌桶策略來(lái)控制并發(fā)數(shù),不過(guò),遇到的問(wèn)題就是每次重啟都會(huì)有瞬時(shí)的流量超過(guò)并發(fā)數(shù)。而在后來(lái)隨著微服務(wù)與網(wǎng)關(guān)越來(lái)越火,又有 Hystrix 或 Sentinel 提供了更強(qiáng)大的功能,比如 Hystrix 的線程熔斷和 Sentinel 的異常熔斷等等。
3. 高可用架構(gòu)
日志的作用不言而言,網(wǎng)關(guān)的調(diào)用日志是必不可少的。而且下定決心要做全鏈路的日志,是已經(jīng)被各種查問(wèn)題逼的不勝其煩的情況下了,你能想象到的,尤其是莫名被拉到一個(gè)群里,被@有個(gè)問(wèn)題要查網(wǎng)關(guān)一次調(diào)用的一個(gè)參數(shù)對(duì)不對(duì)或有沒(méi)有,沒(méi)有個(gè)日志服務(wù)平臺(tái),不僅要親自操刀上陣,更悲催的是還只能去每臺(tái)服務(wù)器上去找日志。
服務(wù)端在拆分了網(wǎng)關(guān)和服務(wù)中心之后,系統(tǒng)都開(kāi)始往微服務(wù)架構(gòu)的方向演進(jìn),一次 API 調(diào)用就需要有全局唯一的標(biāo)識(shí)進(jìn)行串聯(lián),網(wǎng)關(guān)采用的是 UUID,在 API 每次調(diào)用時(shí)都會(huì)生成一個(gè) UUID 傳遞給上游并返回給客戶端,這樣當(dāng)有問(wèn)題需要查詢時(shí),就可以通過(guò) UID 準(zhǔn)確查找相關(guān)日志了。
怎么進(jìn)行日志的采集、查詢、統(tǒng)計(jì),以及如何基于日志實(shí)現(xiàn)監(jiān)控告警?
通常來(lái)講,大多數(shù)系統(tǒng)打印日志采用的是 Log4j,網(wǎng)關(guān)也是,再通過(guò)集團(tuán)提供的日志服務(wù)系統(tǒng),比如 Scribe、Flume 等進(jìn)行日志采集,然后就可以在日志系統(tǒng)或監(jiān)控系統(tǒng)里看到數(shù)據(jù)了。
不過(guò),日志采集看著簡(jiǎn)單,做起來(lái)還是個(gè)技術(shù)活,網(wǎng)關(guān)的調(diào)用量本身是很大的,先不看記錄網(wǎng)關(guān)日志會(huì)有多大的存儲(chǔ)量,關(guān)鍵點(diǎn)是看打印日志會(huì)對(duì)網(wǎng)關(guān)性能有多大的影響。
首先談一下 Log4j,我們知道 Log4j 1.x 會(huì)引發(fā)線程 BLOCKED,所以 Log4j 1.x 不適合高并發(fā)的場(chǎng)景,解決方法一種是升級(jí)到 log4j2 或者更換為 logback,另一種是通過(guò)設(shè)置 BufferedIO 或者使用 AsyncAppender 來(lái)緩解出現(xiàn) BLOCKED 的概率。遺憾的是,網(wǎng)關(guān)采用的是后者,這主要是依賴沖突導(dǎo)致的,不過(guò)這只是做日志采集里的一個(gè)小點(diǎn)。
基于 MMap、Kafka、Storm、ElasticSearch 實(shí)現(xiàn)日志服務(wù)平臺(tái)
除此之外,網(wǎng)關(guān)自己還實(shí)現(xiàn)了一套日志服務(wù)系統(tǒng),這主要是開(kāi)放給平臺(tái)用戶的,當(dāng)時(shí)集團(tuán)的日志系統(tǒng)還不對(duì)外開(kāi)發(fā),所以自己就又搞了一套。
當(dāng)時(shí)技術(shù)選型沒(méi)有選擇 Scribe、Flume,而是自己基于 MMap 技術(shù)來(lái)實(shí)現(xiàn),這也受限于服務(wù)器 agent 權(quán)限,所以,基本思路是通過(guò) Kafka 進(jìn)行日志收集,然后 Storm 接收后寫到 ElasticSearch 提供服務(wù)查詢,這里有個(gè)技術(shù)點(diǎn),最開(kāi)始寫日志是直接發(fā) Kafka,不過(guò)線上發(fā)現(xiàn)網(wǎng)絡(luò)的抖動(dòng)會(huì)影響寫 Kafka 的 RT,后來(lái),我們嘗試了2種方案,第一種是采用線程池異步寫,另一種是基于 MMap 技術(shù)將日志先落盤,然后再異步的讀文件發(fā) Kafka,相比之下,第二種方案更不會(huì)丟數(shù)據(jù)。
日志打不好,找問(wèn)題不僅抓瞎,弄不好系統(tǒng)還要撲街?
說(shuō)到最后,也談?wù)劥蛉罩境龅膯?wèn)題。
第一,throw Exception,這點(diǎn)尤其注意,微服務(wù)架構(gòu)里,如果服務(wù)提供方服務(wù)異常,一定不要將異常堆棧也傳給服務(wù)調(diào)用方,雖然通過(guò)異常信息可以快速定位問(wèn)題,但異常信息會(huì)占用大量的網(wǎng)絡(luò)資源,嚴(yán)重的就變成服務(wù)不可用了,這里,我是有血的教訓(xùn)的,所以,我推薦的方式是定義返回結(jié)果對(duì)象里的返回值和錯(cuò)誤碼。
基于多維度的限流熔斷策略,構(gòu)建實(shí)時(shí) API 成功率監(jiān)控能力
上文說(shuō)了全鏈路日志和實(shí)時(shí)監(jiān)控,本文就說(shuō)下限流降級(jí),這里都是故事。網(wǎng)關(guān)系統(tǒng),需要對(duì)調(diào)用 API 進(jìn)行實(shí)時(shí)的性能監(jiān)控和錯(cuò)誤碼監(jiān)控,由于是實(shí)時(shí)計(jì)算,所以采用了 NoSQL 來(lái)緩存數(shù)據(jù),因?yàn)槭菍?duì) API 進(jìn)行監(jiān)控,所以將 API 接口名作為緩存 Key,可當(dāng) API 調(diào)用異常猛增時(shí),緩存熱定問(wèn)題就出現(xiàn)了,很快就出現(xiàn)了 failover,然后服務(wù)不可用。所以,在處理數(shù)據(jù)時(shí)一定要考慮好數(shù)據(jù)熱點(diǎn)問(wèn)題,無(wú)論是 NoSQL 還是 MySQL。
4. 總結(jié)
言而總之,本篇文章重點(diǎn)講述了API網(wǎng)關(guān)的統(tǒng)一接入、分層架構(gòu)、高可用架構(gòu)。下篇文章,我將繼續(xù)介紹流量調(diào)度的配置中心、泛化調(diào)用。如果你覺(jué)得有收獲,歡迎你把今天的內(nèi)容分享給更多的朋友。