自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

奔潰!老板叫我設(shè)計一個億級API網(wǎng)關(guān)

網(wǎng)絡(luò) 通信技術(shù) 開發(fā)工具
網(wǎng)關(guān)是一個比較成熟的產(chǎn)品,基本上各大互聯(lián)網(wǎng)公司都會有網(wǎng)關(guān)這個中間件,來解決一些公有業(yè)務(wù)的上浮,而且能快速的更新迭代。

 [[391992]] 

圖片來自 Pexels

如果沒有網(wǎng)關(guān),要更新一個公有特性,就要推動所有業(yè)務(wù)方都更新和發(fā)布,那是效率極低的事,有網(wǎng)關(guān)后,這一切都變得不是問題。

喜馬拉雅也是一樣,用戶數(shù)增長達到 6 億多的級別,Web 服務(wù)個數(shù)達到500+,目前我們網(wǎng)關(guān)日處理 200 億+次調(diào)用,單機 QPS 高峰達到 4w+。

網(wǎng)關(guān)除了要實現(xiàn)最基本的功能反向代理外,還有公有特性,比如黑白名單,流控,鑒權(quán),熔斷,API 發(fā)布,監(jiān)控和報警等。

我們還根據(jù)業(yè)務(wù)方的需求實現(xiàn)了流量調(diào)度,流量 Copy,預(yù)發(fā)布,智能化升降級,流量預(yù)熱等相關(guān)功能。

 

下面就我們網(wǎng)關(guān)在這些方便的一些實踐經(jīng)驗以及發(fā)展歷程,下面是喜馬拉雅網(wǎng)關(guān)的演化過程。

第一版:Tomcat nio+Async Servlet

網(wǎng)關(guān)在架構(gòu)設(shè)計時最為關(guān)鍵點,就是網(wǎng)關(guān)在接收到請求,調(diào)用后端服務(wù)時不能阻塞 Block,否則網(wǎng)關(guān)的吞吐量很難上去,因為最耗時的就是調(diào)用后端服務(wù)這個遠程調(diào)用過程。

如果這里是阻塞的,Tomcat 的工作線程都 block 住了,在等待后端服務(wù)響應(yīng)的過程中,不能去處理其他的請求,這個地方一定要異步。

架構(gòu)圖如下:

 

這版我們實現(xiàn)單獨的 Push 層,作為網(wǎng)關(guān)收到響應(yīng)后,響應(yīng)客戶端時,通過這層實現(xiàn),和后端服務(wù)的通信是 HttpNioClient,對業(yè)務(wù)的支持黑白名單,流控,鑒權(quán),API 發(fā)布等功能。

但是這版只是功能上達到網(wǎng)關(guān)的要求,處理能力很快就成了瓶頸,單機 QPS 到 5K 的時候,就會不停的 Full GC。

后面通過 Dump 線上的堆分析,發(fā)現(xiàn)全是 Tomcat 緩存了很多 HTTP 的請求,因為 Tomcat 默認會緩存 200 個 requestProcessor,每個 prcessor 都關(guān)聯(lián)了一個 request。

還有就是 Servlet 3.0 Tomcat 的異步實現(xiàn)會出現(xiàn)內(nèi)存泄漏,后面通過減少這個配置,效果明顯。

但性能肯定就下降了,總結(jié)了下,基于 Tomcat 做為接入端,有如下幾個問題。

Tomcat 自身的問題:

  • 緩存太多,Tomcat 用了很多對象池技術(shù),內(nèi)存有限的情況下,流量一高很容易觸發(fā) GC。
  • 內(nèi)存 Copy,Tomcat 的默認是用堆內(nèi)存,所以數(shù)據(jù)需要讀到堆內(nèi),而我們后端服務(wù)是 Netty,有堆外內(nèi)存,需要通過數(shù)次 Copy。
  • Tomcat 還有個問題是讀 body 是阻塞的, Tomcat 的 NIO 模型和 reactor 模型不一樣,讀 body 是 block 的。

這里再分享一張 Tomcat buffer 的關(guān)系圖:

 

通過上面的圖,我們可以看出,Tomcat 對外封裝的很好,內(nèi)部默認的情況下會有三次 copy。

HttpNioClient 的問題:獲取和釋放連接都需要加鎖,對應(yīng)網(wǎng)關(guān)這樣的代理服務(wù)場景,會頻繁的建連和關(guān)閉連接,勢必會影響性能。

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

第二版:Netty+全異步

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

先看下我們基于 Netty 做接入端的架構(gòu)圖:

 

①接入層

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

對 HTTP 協(xié)議的編解碼做了優(yōu)化,對異常,攻擊性請求監(jiān)控可視化。比如我們對 HTTP 的請求行和請求頭大小是有限制的,Tomcat 是請求行和請求加在一起,不超過 8K,Netty 是分別有大小限制。

假如客戶端發(fā)送了超過閥值的請求,帶 cookie 的請求很容易超過,正常情況下,Netty 就直接響應(yīng) 400 給客戶端。

經(jīng)過改造后,我們只取正常大小的部分,同時標記協(xié)議解析失敗,到業(yè)務(wù)層后,就可以判斷出是那個服務(wù)出現(xiàn)這類問題,其他的一些攻擊性的請求,比如只發(fā)請求頭,不發(fā) body 或者發(fā)部分這些都需要監(jiān)控和報警。

②業(yè)務(wù)邏輯層

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

在業(yè)界和一些大廠的網(wǎng)關(guān)設(shè)計中,業(yè)務(wù)邏輯層基本都是設(shè)計成責(zé)任鏈模式,公有的業(yè)務(wù)邏輯也在這層實現(xiàn)。

我們在這層也是相同的套路,支持了:

  • 用戶鑒權(quán)和登陸校驗,支持接口級別配置。
  • 黑白名單,分全局和應(yīng)用,以及 IP 維度,參數(shù)級別。
  • 流量控制,支持自動和手動,自動是對超大流量自動攔截,通過令牌桶算法實現(xiàn)。
  • 智能熔斷,在 Histrix 的基礎(chǔ)上做了改進,支持自動升降級,我們是全部自動的,也支持手動配置立即熔斷,就是發(fā)現(xiàn)服務(wù)異常比例達到閥值,就自動觸發(fā)熔斷。
  • 灰度發(fā)布,我對新啟動的機器的流量支持類似 TCP 的慢啟動機制,給機器一個預(yù)熱的時間窗口。
  • 統(tǒng)一降級,我們對所有轉(zhuǎn)發(fā)失敗的請求都會找統(tǒng)一降級的邏輯,只要業(yè)務(wù)方配了降級規(guī)則,都會降級,我們對降級規(guī)則是支持到參數(shù)級別的,包含請求頭里的值,是非常細粒度的,另外我們還會和 varnish 打通,支持 varnish 的優(yōu)雅降級。
  • 流量調(diào)度,支持業(yè)務(wù)根據(jù)篩選規(guī)則,對流量篩選到對應(yīng)的機器,也支持只讓篩選的流量訪問這臺機器,這在查問題/新功能發(fā)布驗證時非常用,可以先通過小部分流量驗證再大面積發(fā)布上線。
  • 流量 copy,我們支持對線上的原始請求根據(jù)規(guī)則 copy 一份,寫入到 MQ 或者其他的 upstream,來做線上跨機房驗證和壓力測試。
  • 請求日志采樣,我們對所有的失敗的請求都會采樣落盤,提供業(yè)務(wù)方排查問題支持,也支持業(yè)務(wù)方根據(jù)規(guī)則進行個性化采樣,我們采樣了整個生命周期的數(shù)據(jù),包含請求和響應(yīng)相關(guān)的所有數(shù)據(jù)。

上面提到的這么多都是對流量的治理,我們每個功能都是一個 filter,處理失敗都不影響轉(zhuǎn)發(fā)流程,而且所有的這些規(guī)則的元數(shù)據(jù)在網(wǎng)關(guān)啟動時就會全部初始化好。

在執(zhí)行的過程中,不會有 IO 操作,目前有些設(shè)計會對多個 filter 做并發(fā)執(zhí)行,由于我們的都是內(nèi)存操作,開銷并不大,所以我們目前并沒有支持并發(fā)執(zhí)行。

還有個就是規(guī)則會修改,我們修改規(guī)則時,會通知網(wǎng)關(guān)服務(wù),做實時刷新,我們對內(nèi)部自己的這種元數(shù)據(jù)更新的請求,通過獨立的線程處理,防止 IO 在操作時影響業(yè)務(wù)線程。

③服務(wù)調(diào)用層

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

④異步 Push

網(wǎng)關(guān)在發(fā)起服務(wù)調(diào)用后,讓工作線程繼續(xù)處理其他的請求,而不需要等待服務(wù)端返回。

這里的設(shè)計是我們?yōu)槊總€請求都會創(chuàng)建一個上下文,我們在發(fā)完請求后,把該請求的 context 綁定到對應(yīng)的連接上,等 Netty 收到服務(wù)端響應(yīng)時,就會在給連接上執(zhí)行 read 操作。

解碼完后,再從給連接上獲取對應(yīng)的 context,通過 context 可以獲取到接入端的 session。

這樣 push 就通過 session 把響應(yīng)寫回客戶端了,這樣設(shè)計也是基于 HTTP 的連接是獨占的,即連接和請求上下文綁定。

⑤連接池

連接池的原理如下圖:

 

服務(wù)調(diào)用層除了異步發(fā)起遠程調(diào)用外,還需要對后端服務(wù)的連接進行管理。

HTTP 不同于 RPC,HTTP 的連接是獨占的,所以在釋放的時候要特別小心,一定要等服務(wù)端響應(yīng)完了才能釋放,還有就是連接關(guān)閉的處理也要小心。

總結(jié)如下幾點:

  • Connection:close
  • 空閑超時,關(guān)閉連接
  • 讀超時關(guān)閉連接
  • 寫超時,關(guān)閉連接
  • Fin,Reset

上面幾種需要關(guān)閉連接的場景,下面主要說下 Connection:close 和空閑寫超時兩種,其他的應(yīng)該是比較常見的比如讀超時,連接空閑超時,收到 fin,reset 碼這幾個。

⑥Connection:close

后端服務(wù)是 Tomcat,Tomcat 對連接重用的次數(shù)是有限制的,默認是 100 次。

當(dāng)達到 100 次后,Tomcat 會通過在響應(yīng)頭里添加 Connection:close,讓客戶端關(guān)閉該連接,否則如果再用該連接發(fā)送的話,會出現(xiàn) 400。

還有就是如果端上的請求帶了 connection:close,那 Tomcat 就不等這個連接重用到 100 次,即一次就關(guān)閉。

通過在響應(yīng)頭里添加 Connection:close,即成了短連接,這個在和 Tomcat 保持長連接時,需要注意的,如果要利用,就要主動 remove 掉這個 close 頭。

⑦寫超時

首先網(wǎng)關(guān)什么時候開始計算服務(wù)的超時時間,如果從調(diào)用 writeAndFlush 開始就計算,這其實是包含了 Netty 對 HTTP 的 encode 時間和從隊列里把請求發(fā)出去即 flush 的時間,這樣是對后端服務(wù)不公平的。

所以需要在真正 flush 成功后開始計時,這樣是和服務(wù)端最接近的,當(dāng)然還包含了網(wǎng)絡(luò)往返時間和內(nèi)核協(xié)議棧處理的時間,這個不可避免,但基本不變。

所以我們是 flush 成功回調(diào)后開始啟動超時任務(wù),這里就有個注意的地方,如果 flush 不能快速回調(diào),比如來了一個大的 post 請求,body 部分比較大,而 Netty 發(fā)送的時候第一次默認是發(fā) 1k 的大小。

如果還沒有發(fā)完,則增大發(fā)送的大小繼續(xù)發(fā),如果在 Netty 在 16 次后還沒有發(fā)送完成,則不會再繼續(xù)發(fā)送,而是提交一個 flushTask 到任務(wù)隊列,待下次執(zhí)行到后再發(fā)送。

這時 flush 回調(diào)的時間就比較大,導(dǎo)致這樣的請求不能及時關(guān)閉,而且后端服務(wù) Tomcat 會一直阻塞在讀 body 的地方,基于上面的分析,所以我們需要一個寫超時,對大的 body 請求,通過寫超時來及時關(guān)閉。

全鏈路超時機制

 

上圖是我們在整個鏈路超時處理的機制:

  • 協(xié)議解析超時
  • 等待隊列超時
  • 建連超時
  • 等待連接超時
  • 寫前檢查是否超時
  • 寫超時
  • 響應(yīng)超時

監(jiān)控報警

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

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

協(xié)議層:

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

應(yīng)用層:

  • 耗時監(jiān)控,有慢請求,超時請求,以及 tp99,tp999 等。
  • OPS 監(jiān)控和報警。
  • 帶寬監(jiān)控和報警,支持對請求和響應(yīng)的行,頭,body 單獨監(jiān)控。
  • 響應(yīng)碼監(jiān)控,特別是 400,和 404。
  • 連接監(jiān)控,我們對接入端的連接,以及和后端服務(wù)的連接,后端服務(wù)連接上待發(fā)送字節(jié)大小也都做了監(jiān)控。
  • 失敗請求監(jiān)控。
  • 流量抖動報警,這是非常有必要的,流量抖動要么是出了問題,要么就是出問題的前兆。

總體架構(gòu):

 

性能優(yōu)化實踐

①對象池技術(shù)

對于高并發(fā)系統(tǒng),頻繁的創(chuàng)建對象不僅有分配內(nèi)存的開銷外,還有對 GC 會造成壓力。

我們在實現(xiàn)時會對頻繁使用的比如線程池的任務(wù) task,StringBuffer 等會做寫重用,減少頻繁的申請內(nèi)存的開銷。

②上下文切換

高并發(fā)系統(tǒng),通常都采用異步設(shè)計,異步化后,不得不考慮線程上下文切換的問題。

我們的線程模型如下:

 

我們整個網(wǎng)關(guān)沒有涉及到 IO 操作,但我們在業(yè)務(wù)邏輯這塊還是和 Netty 的 IO 編解碼線程異步。

有兩個原因:

  • 防止開發(fā)寫的代碼有阻塞。
  • 業(yè)務(wù)邏輯打日志可能會比較多,在突發(fā)的情況下,在 push 線程時,支持用 Netty 的 IO 線程替代,這里做的工作比較少。

這里有異步修改為同步后(通過修改配置調(diào)整),CPU 的上下文切換減少 20%,進而提高了整體的吞吐量,就是不能為了異步而異步,zull2 的設(shè)計和我們的類似。

③GC 優(yōu)化

在高并發(fā)系統(tǒng),GC 的優(yōu)化不可避免,我們在用了對象池技術(shù)和堆外內(nèi)存時,對象很少進入老年代。

另外我們年輕代會設(shè)置的比較大,而且 SurvivorRatio=2,晉升年齡設(shè)置最大 15,盡量對象在年輕代就回收掉, 但監(jiān)控發(fā)現(xiàn)老年代的內(nèi)存還是會緩慢增長。

通過 dump 分析,我們每個后端服務(wù)創(chuàng)建一個連接,都時有一個 socket,socket 的 AbstractPlainSocketImpl。

而 AbstractPlainSocketImpl 就重寫了 Object 類的 finalize 方法,實現(xiàn)如下:

  1. /** 
  2.      * Cleans up if the user forgets to close it. 
  3.      */ 
  4.     protected void finalize() throws IOException { 
  5.         close(); 
  6.     } 

是為了我們沒有主動關(guān)閉連接,做的一個兜底,在 GC 回收的時候,先把對應(yīng)的連接資源給釋放了。

由于 finalize 的機制是通過 JVM 的 Finalizer線程來處理的,而且 Finalizer 線程的優(yōu)先級不高,默認是 8,需要等到 Finalizer 線程把 ReferenceQueue 的對象對于的 finalize 方法執(zhí)行完。

還要等到下次 GC 時,才能把該對象回收,導(dǎo)致創(chuàng)建連接的這些對象在年輕代不能立即回收,從而進入了老年代,這也是為啥老年代會一直緩慢增長的問題。

④日志

高并發(fā)下,特別是 Netty 的 IO 線程除了要執(zhí)行該線程上的 IO 讀寫操作,還有執(zhí)行異步任務(wù)和定時任務(wù),如果 IO 線程處理不過來隊列里的任務(wù),很有可能導(dǎo)致新進來異步任務(wù)出現(xiàn)被拒絕的情況。

那什么情況下可能呢?IO 是異步讀寫的問題不大,就是多耗點 CPU,最有可能 block 住 IO 線程的是我們打的日志。

目前 Log4j 的 ConsoleAppender 日志 immediateFlush 屬性默認為 true,即每次打 log 都是同步寫 flush 到磁盤的,這個對于內(nèi)存操作來說,慢了很多。

同時 AsyncAppender 的日志隊列滿了也會 block 住線程,log4j 默認的 buffer 大小是 128,而且是 block 的。

即如果 buffer 的大小達到 128,就阻塞了寫日志的線程,在并發(fā)寫日志量大的的情況下,特別是堆棧很多時,log4j 的 Dispatcher 線程會出現(xiàn)變慢要刷盤。

這樣 buffer 就不能快速消費,很容易寫滿日志事件,導(dǎo)致 Netty IO 線程 block 住,所以我們在打日志時,也要注意精簡。

未來規(guī)劃

現(xiàn)在我們都是基于 HTTP/1,現(xiàn)在 HTTP/2 相對于 HTTP/1 關(guān)鍵實現(xiàn)了在連接層面的服務(wù),即一個連接上可以發(fā)送多個 HTTP 請求。

即 HTTP 連接也能和 RPC 連接一樣,建幾個連接就可以了,徹底解決了 HTTP/1 連接不能復(fù)用導(dǎo)致每次都建連和慢啟動的開銷。

我們也在基于 Netty 升級到 HTTP/2,除了技術(shù)升級外,我們對監(jiān)控報警也一直在持續(xù)優(yōu)化,怎么提供給業(yè)務(wù)方準確無誤的報警,也是一直在努力。

還有一個就是降級,作為統(tǒng)一接入網(wǎng)關(guān),和業(yè)務(wù)方做好全方位的降級措施,也是一直在完善的點,保證全站任何故障都能通過網(wǎng)關(guān)第一時間降級,也是我們的重點。

總結(jié)

網(wǎng)關(guān)已經(jīng)是一個互聯(lián)網(wǎng)公司的標配,這里總結(jié)實踐過程中的一些心得和體會,希望給大家一些參考以及一些問題的解決思路,歡迎交流。

作者:彭榮新

編輯:陶家龍

出處:轉(zhuǎn)載自公眾號喜馬拉雅技術(shù)博客(ID:xmly_tech)

 

責(zé)任編輯:武曉燕 來源: 喜馬拉雅技術(shù)博客
相關(guān)推薦

2018-11-26 08:06:24

API網(wǎng)關(guān)億級

2018-11-01 13:23:02

網(wǎng)關(guān)APIHTTP

2020-03-03 07:59:29

設(shè)計秒殺系統(tǒng)

2018-12-10 13:50:16

網(wǎng)絡(luò)安全網(wǎng)絡(luò)安全技術(shù)周刊

2021-03-02 07:54:18

流量網(wǎng)關(guān)設(shè)計

2021-03-05 07:47:07

工作流引擎節(jié)點

2019-09-18 09:41:25

億級流量網(wǎng)站

2021-10-14 09:51:17

架構(gòu)運維技術(shù)

2019-11-26 09:42:36

代碼開發(fā)API

2021-06-28 10:09:59

架構(gòu)網(wǎng)關(guān)技術(shù)

2020-02-09 16:52:02

睡醒公司倒閉

2021-02-09 09:50:21

SQLOracle應(yīng)用

2013-07-01 11:01:22

API設(shè)計API

2023-03-27 08:33:32

2021-07-26 05:03:44

OpenFeign系統(tǒng)組件

2013-06-19 09:59:07

2020-09-22 07:50:23

API接口業(yè)務(wù)

2020-08-05 07:37:29

任務(wù)系統(tǒng)定時

2021-03-16 16:35:39

網(wǎng)關(guān)Java代碼

2024-09-06 11:02:15

API網(wǎng)關(guān)插件
點贊
收藏

51CTO技術(shù)棧公眾號