喜馬拉雅自研網(wǎng)關架構實踐

背景

網(wǎng)關是一個比較成熟了的產(chǎn)品,基本上各大互聯(lián)網(wǎng)公司都會有網(wǎng)關這個中間件痊项,來解決一些公有業(yè)務的上浮锅风,而且能快速的更新迭代,如果沒有網(wǎng)關鞍泉,要更新一個公有特性皱埠,就要推動所有業(yè)務方都更新和發(fā)布,那是效率極低的事咖驮,有網(wǎng)關后边器,這一切都變得不是問題泪姨,喜馬拉雅也是一樣,用戶數(shù)增長達到6億多的級別饰抒,Web服務個數(shù)達到500+,目前我們網(wǎng)關日處理200億加次調(diào)用诀黍,單機QPS高峰達到4w+袋坑。

網(wǎng)關除了要實現(xiàn)最基本的功能反向代理外,還有公有特性眯勾,比如黑白名單枣宫,流控,鑒權吃环,熔斷也颤,API發(fā)布,監(jiān)控和報警等郁轻,我們還根據(jù)業(yè)務方的需求實現(xiàn)了流量調(diào)度翅娶,流量Copy,預發(fā)布好唯,智能化升降級竭沫,流量預熱等相關功能,下面就我們網(wǎng)關在這些方便的一些實踐經(jīng)驗以及發(fā)展歷程骑篙,下面是喜馬拉雅網(wǎng)關的演化過程:

gw-version.png

第一版 Tomcat nio + AsyncServlet

網(wǎng)關在架構設計時最為關鍵點蜕提,就是網(wǎng)關在接收到請求,調(diào)用后端服務時不能阻塞Block靶端,否則網(wǎng)關的吞吐量很難上去谎势,因為最耗時的就是調(diào)用后端服務這個遠程調(diào)用過程,如果這里是阻塞的杨名,那你的tomcat的工作線程都block主了脏榆,在等待后端服務響應的過程中,不能去處理其他的請求,這個地方一定要異步

架構圖如下:


gw-v1.jpg

這版我們實現(xiàn)單獨的Push層台谍,作為網(wǎng)關收到響應后姐霍,響應客戶端時,通過這層實現(xiàn)典唇,和后端服務的通信是HttpNioClient镊折,對業(yè)務的支持黑白名單,流控介衔,鑒權恨胚,API發(fā)布等功能,這版只是功能上達到網(wǎng)關的邀請炎咖,但是處理能力很快就成了瓶頸赃泡,單機qps到5k的時候寒波,就會不停的full gc,后面通過dump 線上的堆分析升熊,發(fā)現(xiàn)全是tomcat緩存了很多http的請求俄烁,因為tomcat默認會緩存200個requestProcessor,每個prcessor都關聯(lián)了一個request级野,還有就是servlet3.0 tomcat的異步實現(xiàn)會出現(xiàn)內(nèi)存泄漏页屠,后面通過減少這個配置,效果明顯蓖柔。但性能肯定就下降了辰企,總結了下,基于tomcat做為接入端况鸣,有如下幾個問題:

Tomcat自身的問題

  • 緩存太多牢贸,tomcat用了很多對象池技術,內(nèi)存有限的情況下镐捧,流量一高很容易觸發(fā)gc潜索。

  • 內(nèi)存copy,tomcat的默認是用堆內(nèi)存懂酱,所以數(shù)據(jù)需要讀到堆內(nèi)帮辟,而我們后端服務是netty,有堆外內(nèi)存玩焰,需要通過數(shù)次copy由驹。

  • tomcat 還有個問題是讀body是阻塞的,tomcat 的nio模型和reactor模型不一樣,讀body是block的昔园。

HttpNioClient的問題

  • 獲取和釋放鏈接都需要加鎖蔓榄,對應網(wǎng)關這樣的代理服務場景,會頻繁的建鏈和關閉鏈接默刚,勢必會影響性能甥郑。

基于tomcat的存在的這些問題,我們后面對接入端做改造荤西,用Netty做接入層和服務調(diào)用層澜搅,也就是我們的第二版,能徹底解決上面的問題邪锌,達到理想的性能勉躺。

第二版 Netty + 全異步

基于Netty的優(yōu)勢,我們實現(xiàn)了全異步觅丰,無鎖饵溅,分層的架構

先看下我們基于Netty做接入端的架構圖


gw-async-pipeline.png

接入層

Netty的io線程,負責http協(xié)議的編解碼工作妇萄,同時對協(xié)議層面的異常做監(jiān)控報警

對http協(xié)議的編解碼做了優(yōu)化蜕企,對異常咬荷,攻擊性請求監(jiān)控可視化,比如我們對http的請求行和請求頭大小是有限制的轻掩,tomcat是請求行和請求加在一起幸乒,不超過8k,netty是分別有大小限制唇牧,假如客戶端發(fā)送了超過閥值的請求罕扎,帶cookie的請求很容易超過,正常情況下奋构,netty就直接響應400給客戶端,經(jīng)過改造后拱层,我們只取正常大小的部分弥臼,同時標記協(xié)議解析失敗,到業(yè)務層后根灯,就可以判斷出是那個服務出現(xiàn)這類問題径缅,其他的一些攻擊性的請求,比如只發(fā)請求頭烙肺,不發(fā)body/或者發(fā)部分這些都需要監(jiān)控和報警纳猪。

業(yè)務邏輯層

負責對API路由,流量調(diào)度等一序列的支持業(yè)務的公有邏輯桃笙,都在這層實現(xiàn)氏堤,采樣責任鏈模式,這層不會有io操作搏明。

在業(yè)界和一些大廠的網(wǎng)關設計中鼠锈,業(yè)務邏輯層基本都是設計成責任鏈模式,公有的業(yè)務邏輯也在這層實現(xiàn)星著,我們在這層也是相同的套路购笆,支持了:

  • 用戶鑒權和登陸校驗,支持接口級別配置
  • 黑白明單虚循,分全局和應用同欠,以及ip維度,參數(shù)級別
  • 流量控制横缔,支持自動和手動铺遂,自動是對超大流量自動攔截,通過令牌桶算法實現(xiàn)
  • 智能熔斷茎刚,在histrix的基礎上做了改進娃循,支持自動升降級,我們是全部自動的斗蒋,也支持手動配置立即熔斷捌斧,就是發(fā)現(xiàn)服務異常比例達到閥值笛质,就自動觸發(fā)熔斷
  • 灰度發(fā)布,我對新啟動的機器的流量支持類似tcp的慢啟動機制捞蚂,給
    機器一個預熱的時間窗口
  • 統(tǒng)一降級妇押,我們對所有轉(zhuǎn)發(fā)失敗的請求都會找統(tǒng)一降級的邏輯,只要業(yè)務方配了降級規(guī)則姓迅,都會降級敲霍,我們對降級規(guī)則是支持到參數(shù)級別的,包含請求頭里的值丁存,是非常細粒度的肩杈,另外我們還會和varnish打通,支持varish的優(yōu)雅降級
  • 流量調(diào)度解寝,支持業(yè)務根據(jù)篩選規(guī)則扩然,對流量篩選到對應的機器,也支持只讓篩選的流量訪問這臺機器聋伦,這在查問題/新功能發(fā)布驗證時非常用夫偶,可以先通過小部分流量驗證再大面積發(fā)布上線。
  • 流量copy觉增,我們支持對線上的原始請求根據(jù)規(guī)則copy一份兵拢,寫入到mq或者其他的upstream,來做線上跨機房驗證和壓力測試逾礁。
  • 請求日志采樣说铃,我們對所有的失敗的請求都會采樣落盤,提供業(yè)務方排查問題支持嘹履,也支持業(yè)務方根據(jù)規(guī)則進行個性化采樣截汪,我們采樣了整個生命周期的數(shù)據(jù),包含請求和響應相關的所有數(shù)據(jù)植捎。

上面提到的這么多都是對流量的治理衙解,我們每個功能都是一個filter,處理失敗都不影響轉(zhuǎn)發(fā)流程焰枢,而且所有的這些規(guī)則的元數(shù)據(jù)在網(wǎng)關啟動時就會全部初始化好蚓峦,在執(zhí)行的過程中,不會有IO操作济锄,目前有些設計會對多個filter做并發(fā)執(zhí)行暑椰,由于我們的都是內(nèi)存操作,開銷并不大荐绝,所以我們目前并沒有支持并發(fā)執(zhí)行一汽,還有個就是規(guī)則會修改,我們修改規(guī)則時,會通知網(wǎng)關服務召夹,做實時刷新岩喷,我們對內(nèi)部自己的這種元數(shù)據(jù)更新的請求,通過獨立的線程處理监憎,防止io在操作時影響業(yè)務線程纱意。

服務調(diào)用層

服務調(diào)用對于代理網(wǎng)關服務是關鍵的地方,一定需要異步鲸阔,我們通過netty實現(xiàn),同時也很好的利用了netty提供的鏈接池偷霉,做到了獲取和釋放都是無鎖操作

異步Push

網(wǎng)關在發(fā)起服務調(diào)用后,讓工作線程繼續(xù)處理其他的請求褐筛,而不需要等待服務端返回类少,這里的設計是我們?yōu)槊總€請求都會創(chuàng)建一個上下文,我們在發(fā)完請求后渔扎,把該請求的context 綁定到對應的鏈接上硫狞,等netty收到服務端響應時,就會在給鏈接上執(zhí)行read操作赞警,解碼完后妓忍,再從給鏈接上獲取對應的context虏两,通過context可以獲取到接入端的session愧旦,這樣push就通過session把響應寫回客戶端了,這樣設計也是基于http的鏈接是獨占的定罢,即鏈接可以和請求上下文綁定笤虫。

鏈接池

鏈接池的原理如下圖:

southgate_pool.png

服務調(diào)用層除了異步發(fā)起遠程調(diào)用外,還需要對后端服務的鏈接進行管理祖凫,http不同于rpc琼蚯,http的鏈接是獨占的,所以在釋放的時候要特別小心惠况,一定要等服務端響應完了才能釋放遭庶,還有就是鏈接關閉的處理也要小心,總結如下幾點:

  • Connection:close
  • 空閑超時稠屠,關閉鏈接
  • 讀超時關閉鏈接
  • 寫超時峦睡,關閉鏈接
  • Fin,Reset

上面幾種需要關閉鏈接的場景,下面主要說下Connection:close和空閑寫超時兩種权埠,其他的應該是比較常見的比如讀超時榨了,鏈接空閑超時,收到fin攘蔽,reset碼這幾個龙屉。

Connection:close

后端服務是tomcat,tomcat對鏈接重用的次數(shù)是有限制的满俗,默認是100次转捕,當達到100次后作岖,tomcat會通過在響應頭里添加Connection:close,讓客戶端關閉該鏈接瓜富,否則如果再用該鏈接發(fā)送的話鳍咱,會出現(xiàn)400。

還有就是如果端上的請求帶了connection:close,那tomcat就不等這個鏈接重用到100次与柑,即一次就關閉谤辜,通過在響應頭里添加Connection:close,即成了短鏈接价捧,
這個在和tomcat保持長鏈接時丑念,需要注意的,如果要利用结蟋,就要主動remove掉這個close頭脯倚。

寫超時

首先網(wǎng)關什么時候開始計算服務的超時時間,如果從調(diào)用writeAndFlush開始就計算嵌屎,這其實是包含了netty對http的encode時間和從隊列里把請求發(fā)出去即flush的時間推正,這樣是對后端服務不公平的,所以需要在真正flush成功后開始計時宝惰,這樣是和服務端最接近的植榕,當然還包含了網(wǎng)絡往返時間和內(nèi)核協(xié)議棧處理的時間,這個不可避免尼夺,但基本不變尊残。

所以我們是flush成功回調(diào)后開始啟動超時任務,這里就有個注意的地方淤堵,如果flush不能快速回調(diào)寝衫,比如來了一個大的post請求,body部分比較大拐邪,而netty發(fā)送的時候第一次默認是發(fā)1k的大小慰毅,如果還沒有發(fā)完,則增大發(fā)送的大小繼續(xù)發(fā)扎阶,如果在netty在16次后還沒有發(fā)送完成汹胃,則不會再繼續(xù)發(fā)送,而是提交一個flushTask到任務隊列乘陪,待下次執(zhí)行到后再發(fā)送统台,這時flush回調(diào)的時間就比較大,導致這樣的請求不能及時關閉啡邑,而且后端服務tomcat會一直阻塞在讀body的地方贱勃,基于上面的分析,所以我們需要一個寫超時,對大的body請求贵扰,通過寫超時來及時關閉仇穗。

全鏈路超時機制

下面是我們在整個鏈路種一個超時處理的機制筝野。

gw-timeout.png
  • 協(xié)議解析超時
  • 等待隊列超時
  • 建鏈超時
  • 等待鏈接超時
  • 寫前檢查是否超時
  • 寫超時
  • 響應超時

監(jiān)控報警

網(wǎng)關業(yè)務方能看到的是監(jiān)控和報警宁赤,我們是實現(xiàn)秒級別報警和秒級別的監(jiān)控,監(jiān)控數(shù)據(jù)定時上報給我們的管理系統(tǒng)痊夭,由管理系統(tǒng)負責聚合統(tǒng)計舞丛,落盤到influxdb

我們對http協(xié)議做了全面的監(jiān)控和報警耘子,無論是協(xié)議層的還是服務層的

協(xié)議層

  • 攻擊性請求,只發(fā)頭球切,不發(fā)/發(fā)部分body谷誓,采樣落盤,還原現(xiàn)場吨凑,并報警
  • Line or Head or Body過大的請求捍歪,采樣落盤,還原現(xiàn)場鸵钝,并報警

應用層

  • 耗時監(jiān)控糙臼,有慢請求,超時請求恩商,以及tp99变逃,tp999等
  • qps監(jiān)控和報警
  • 帶寬監(jiān)控和報警,支持對請求和響應的行痕届,頭韧献,body單獨監(jiān)控末患。
  • 響應碼監(jiān)控研叫,特別是400,和404
  • 鏈接監(jiān)控,我們對接入端的鏈接璧针,以及和后端服務的鏈接嚷炉,后端服務鏈接上待發(fā)送字節(jié)大小也都做了監(jiān)控
  • 失敗請求監(jiān)控
  • 流量抖動報警,這是非常有必要的探橱,流量抖動要么是出了問題申屹,要么就是出問題的前兆。

總體架構

soutgate-all-arch.png

性能優(yōu)化實踐

對象池技術

對于高并發(fā)系統(tǒng)隧膏,頻繁的創(chuàng)建對象不僅有分配內(nèi)存的開銷外哗讥,還有對gc會造成壓力,我們在實現(xiàn)時會對頻繁使用的比如線程池的任務task胞枕,StringBuffer等會做寫重用杆煞,減少頻繁的申請內(nèi)存的開銷。

上下文切換

高并發(fā)系統(tǒng),通常都采用異步設計决乎,異步化后队询,不得不考慮線程上下文切換的問題,我們的線程模型如下:

southgate-thread-mode.jpg

我們整個網(wǎng)關沒有涉及到io操作构诚,但我們在業(yè)務邏輯這塊還是和netty的io編解碼線程異步蚌斩,是有兩個原因,1是防止開發(fā)寫的代碼有阻塞范嘱,2是業(yè)務邏輯打日志可能會比較多送膳,在突發(fā)的情況下,但是我們在push線程時丑蛤,支持用netty的io線程替代肠缨,這里做的工作比較少,這里有異步修改為同步后(通過修改配置調(diào)整)盏阶,cpu的上下文切換減少20%晒奕,進而提高了整體的吞吐量,就是不能為了異步而異步名斟,zull2的設計和我們的類似脑慧,

GC優(yōu)化

在高并發(fā)系統(tǒng),gc的優(yōu)化不可避免砰盐,我們在用了對象池技術和堆外內(nèi)存時闷袒,對象很少進入老年代,另外我們年輕代會設置的比較大岩梳,而且SurvivorRatio=2囊骤,晉升年齡設置最大15,盡量對象在年輕代就回收掉冀值, 但監(jiān)控發(fā)現(xiàn)老年代的內(nèi)存還是會緩慢增長也物,通過dump分析,我們每個后端服務創(chuàng)建一個鏈接列疗,都時有一個socket滑蚯,socket的AbstractPlainSocketImpl,而AbstractPlainSocketImpl就重寫了Object類的finalize方法抵栈,實現(xiàn)如下:

/**
 * Cleans up if the user forgets to close it.
 */
protected void finalize() throws IOException {
    close();
}

是為了我們沒有主動關閉鏈接告材,做的一個兜底,在gc回收的時候古劲,先把對應的鏈接資源給釋放了,由于finalize的機制是通過jvm的Finalizer線程來處理的斥赋,而且Finalizer線程的優(yōu)先級不高,默認是8产艾,需要等到Finalizer線程把ReferenceQueue的對象對于的finalize方法執(zhí)行完疤剑,還要等到下次gc時洛波,才能把該對象回收,導致創(chuàng)建鏈接的這些對象在年輕代不能立即回收骚露,從而進入了老年代蹬挤,這也是為啥老年代會一直緩慢增長的問題。

日志

高并發(fā)下棘幸,特別是netty的io線程除了要執(zhí)行該線程上的io讀寫操作焰扳,還有執(zhí)行異步任務和定時任務,如果io線程處理不過來隊列里的任務误续,很有可能導致新進來異步任務出現(xiàn)被拒絕的情況吨悍,那什么情況下可能呢,io是異步讀寫的問題不大蹋嵌,就是多耗點cpu育瓜,最有可能block住io線程的是我們打的日志,目前l(fā)og4j的ConsoleAppender日志immediateFlush屬性默認為true,即每次打log都是同步寫flush到磁盤的栽烂,這個對于內(nèi)存操作來說躏仇,慢了很多,同時AsyncAppender的日志隊列滿了也會block住線程,log4j默認的buffer大小是128腺办,而且是block的焰手,即如果buffer的大小達到128,就阻塞了寫日志的線程怀喉,在并發(fā)寫日志量大的的情況下书妻,特別是堆棧很多時,log4j的Dispatcher線程會出現(xiàn)變慢要刷盤躬拢,這樣buffer就不能快速消費躲履,很容易寫滿日志事件,導致netty io線程block住聊闯,所以我們在打日志時工猜,也要注意精簡。

未來規(guī)劃

現(xiàn)在我們都是基于http1馅袁,現(xiàn)在http2相對于http1關鍵實現(xiàn)了在鏈接層面的服務域慷,即一個鏈接上可以發(fā)送多個http請求荒辕,即http的鏈接也能和rpc的鏈接一樣汗销,建幾個鏈接就可以了,徹底解決了http1鏈接不能復用導致每次都建鏈和慢啟動的開銷抵窒,我們也在基于netty升級到http2,除了技術升級外弛针,我們對監(jiān)控報警也一直在持續(xù)優(yōu)化,怎么提供給業(yè)務方準確無誤的報警李皇,也是一直在努力削茁,還有一個就是降級宙枷,作為統(tǒng)一接入網(wǎng)關,和業(yè)務方做好全方位的降級措施茧跋,也是一直在完善的點慰丛,保證全站任何故障都能通過網(wǎng)關第一時間降級,也是我們的重點瘾杭。

總結

網(wǎng)關已經(jīng)是一個互聯(lián)網(wǎng)公司的標配诅病,這里總結實踐過程中的一些心得和體會,希望給大家一些參考以及一些問題的解決思路粥烁,我們也還在不斷完善中贤笆,同時我們也在做多活的項目,感興趣的同學可以加入我們讨阻。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末芥永,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子钝吮,更是在濱河造成了極大的恐慌埋涧,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件奇瘦,死亡現(xiàn)場離奇詭異飞袋,居然都是意外死亡,警方通過查閱死者的電腦和手機链患,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評論 3 392
  • 文/潘曉璐 我一進店門巧鸭,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人麻捻,你說我怎么就攤上這事纲仍。” “怎么了贸毕?”我有些...
    開封第一講書人閱讀 162,483評論 0 353
  • 文/不壞的土叔 我叫張陵郑叠,是天一觀的道長。 經(jīng)常有香客問我明棍,道長乡革,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,165評論 1 292
  • 正文 為了忘掉前任摊腋,我火速辦了婚禮沸版,結果婚禮上,老公的妹妹穿的比我還像新娘兴蒸。我一直安慰自己视粮,他們只是感情好,可當我...
    茶點故事閱讀 67,176評論 6 388
  • 文/花漫 我一把揭開白布橙凳。 她就那樣靜靜地躺著蕾殴,像睡著了一般笑撞。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上钓觉,一...
    開封第一講書人閱讀 51,146評論 1 297
  • 那天茴肥,我揣著相機與錄音,去河邊找鬼荡灾。 笑死炉爆,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的卧晓。 我是一名探鬼主播芬首,決...
    沈念sama閱讀 40,032評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼逼裆!你這毒婦竟也來了郁稍?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,896評論 0 274
  • 序言:老撾萬榮一對情侶失蹤胜宇,失蹤者是張志新(化名)和其女友劉穎耀怜,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體桐愉,經(jīng)...
    沈念sama閱讀 45,311評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡财破,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,536評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了从诲。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片左痢。...
    茶點故事閱讀 39,696評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖系洛,靈堂內(nèi)的尸體忽然破棺而出俊性,到底是詐尸還是另有隱情,我是刑警寧澤描扯,帶...
    沈念sama閱讀 35,413評論 5 343
  • 正文 年R本政府宣布定页,位于F島的核電站,受9級特大地震影響绽诚,放射性物質(zhì)發(fā)生泄漏典徊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,008評論 3 325
  • 文/蒙蒙 一恩够、第九天 我趴在偏房一處隱蔽的房頂上張望卒落。 院中可真熱鬧,春花似錦玫鸟、人聲如沸导绷。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽妥曲。三九已至,卻和暖如春钦购,著一層夾襖步出監(jiān)牢的瞬間檐盟,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,815評論 1 269
  • 我被黑心中介騙來泰國打工押桃, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留葵萎,地道東北人。 一個月前我還...
    沈念sama閱讀 47,698評論 2 368
  • 正文 我出身青樓唱凯,卻偏偏與公主長得像羡忘,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子磕昼,可洞房花燭夜當晚...
    茶點故事閱讀 44,592評論 2 353

推薦閱讀更多精彩內(nèi)容