日日操夜夜添-日日操影院-日日草夜夜操-日日干干-精品一区二区三区波多野结衣-精品一区二区三区高清免费不卡

公告:魔扣目錄網為廣大站長提供免費收錄網站服務,提交前請做好本站友鏈:【 網站目錄:http://www.ylptlb.cn 】, 免友鏈快審服務(50元/站),

點擊這里在線咨詢客服
新站提交
  • 網站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會員:747

本文目錄

- 說在前面

- 喜馬拉雅自研億級API網關技術實踐

- 1、第1版:Tomcat NIO+Async Servlet

- 2、第2版?.NETty+全異步

  - 2.1 接入層

  - 2.2 業務邏輯層

  - 2.3 服務調用層

    - 2.3.1 異步 Push

    - 2.3.2 連接池

    - 2.3.3 Connection:close

    - 2.3.4 寫超時

- 3、全鏈路超時機制

- 4、監控報警

- 5、性能優化實踐

  - 5.1 對象池技術

  - 5.2 上下文切換

  - 5.3 GC優化

  - 5.4 日志

- 6、未來規劃

- 說在最后:有問題可以找老架構取經

- 部分歷史案例

?

喜馬拉雅自研億級API網關技術實踐

網關作為一種發展較為完善的產品,各大互聯網公司普遍采用它作為中間件,以應對公共業務需求的不斷浮現,并能迅速迭代更新。

如果沒有網關,要更新一個公共特性,就得推動所有業務方都進行更新和發布,這無疑是效率極低的。然而,有了網關之后,這一切都不再是問題。

喜馬拉雅也如此,用戶數量已增長到 6 億級別,Web 服務數量超過 500 個,目前我們的網關每天處理超過 200 億次的調用,單機 QPS 峰值可達 4w+。

除了實現基本的反向代理功能,網關還具備許多公共特性,如黑白名單、流量控制、身份驗證、熔斷、API 發布、監控和報警等。根據業務方的需求,我們還實現了流量調度、流量復制、預發布、智能升級、流量預熱等相關功能。

從技術上來說,喜馬拉雅API網關的技術演進路線圖大致如下

注意:請點擊圖像以查看清晰的視圖!

本文將介紹在喜馬拉雅 API 網關面臨億級流量的情況下,我們如何進行技術演進,以及我們的實踐經驗總結。

1、第1版:Tomcat NIO+Async Servlet

在架構設計中,網關的關鍵之處在于接收到請求并調用后端服務時,不能發生阻塞(Block),否則網關的處理能力將受到限制。

這是因為最耗時的操作就是遠程調用后端服務這個過程。

如果此處發生阻塞,Tomcat 的工作線程會被全部 block 住了,等待后端服務響應的過程中無法處理其他請求,因此這里必須采用異步處理。

架構圖如下

注意:請點擊圖像以查看清晰的視圖!

在這個版本中,我們實現了一個單獨的 Push 層,用于在網關接收到響應后,響應客戶端,并通過此層實現與后端服務的通信。

該層使用的是 HttpNioClient,支持業務功能包括黑白名單、流量控制、身份驗證、API 發布等。

然而,這個版本僅在功能上滿足了網關的要求,處理能力很快成為瓶頸。當單機 QPS 達到 5K 時,會頻繁發生 Full GC。

通過分析線上堆,我們發現問題在于 Tomcat 緩存了大量 HTTP 請求。因為 Tomcat 默認會緩存 200 個 requestProcessor,每個處理器都關聯一個 request。

另外,Servlet 3.0 的 Tomcat 異步實現可能會導致內存泄漏。后來我們通過減少這個配置,效果明顯。

然而,這種調整會導致性能下降。總結一下,基于 Tomcat 作為接入端存在以下問題:

Tomcat 自身的問題

  • 1)緩存過多,Tomcat 使用了許多對象池技術,在有限內存的情況下,流量增大時很容易觸發 GC;

  • 2)內存 Copy,Tomcat 的默認內存使用堆內存,因此數據需要從堆內讀取,而后端服務是 Netty,使用堆外內存,需要經過多次 Copy;

  • 3)Tomcat 還有個問題是讀 body 是阻塞的, Tomcat 的 NIO 模型和 reactor 模型不同,讀 body 是 block 的。

這里再分享一張 Tomcat buffer 的關系圖

注意:請點擊圖像以查看清晰的視圖!

從上圖中,我們能夠明顯觀察到,Tomcat 的封裝功能相當完善,但在內部默認設置下,會有三次 copy。

HttpNioClient 的問題:在獲取和釋放連接的過程中都需要進行加鎖,針對類似網關這樣的代理服務場景,會導致頻繁地建立和關閉連接,這無疑會對性能產生負面影響。

鑒于 Tomcat 存在的這些難題,我們在后續對接入端進行了優化,采用 Netty 作為接入層和服務調用層,也就是我們的第二版,成功地解決了上述問題,實現了理想的性能。

2、第2版:Netty+全異步

基于 Netty 的優勢,我們構建了全異步、無鎖、分層的架構。

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

注意:請點擊圖像以查看清晰的視圖!

2.1 接入層

Netty 的 IO 線程主要負責 HTTP 協議的編解碼工作,同時也監控并報警協議層面的異常情況。

我們對 HTTP 協議的編解碼進行了優化,并對異常和攻擊性請求進行了監控和可視化處理。

例如,我們對 HTTP 請求行和請求頭的大小都有限制,而 Tomcat 是將請求行和請求頭一起計算,總大小不超過 8K,而 Netty 是分別對兩者設置大小限制。

如果客戶端發送的請求超過了設定的閥值,帶有 cookie 的請求很容易超過這個限制,一般情況下,Netty 會直接響應 400 給客戶端。

在優化后,我們只取正常大小的部分,并標記協議解析失敗,這樣在業務層就可以判斷出是哪個服務出現了這類問題。

對于其他攻擊性的請求,例如只發送請求頭而不發送 body 或者只發送部分內容,都需要進行監控和報警。

2.2 業務邏輯層

這一層負責實現一系列支持業務的公共邏輯,包括 API 路由、流量調度等,采用責任鏈模式,這一層不會進行 IO 操作。

在業界和大型企業的網關設計中,業務邏輯層通常都被設計成責任鏈模式,公共的業務邏輯也在這一層實現。

在這一層,我們也執行了相似的操作,并支持以下功能

  • 1)用戶認證和登錄驗證,支持接口級別的配置;

  • 2)黑白名單:包括全局和應用的黑白名單,以及 IP 和參數級別的限制;

  • 3)流量控制:提供自動和手動控制,自動控制可攔截過大流量,通過令牌桶算法實現;

  • 4)智能熔斷:在 Histrix 的基礎上進行改進,支持自動升降級,我們采用全自動方式,也支持手動配置立即熔斷,即當服務異常比例達到設定值時,自動觸發熔斷;

  • 5)灰度發布:對于新啟動的機器的流量,我們支持類似于 TCP 的慢啟動機制,為機器提供一段預熱時間;

  • 6)統一降級:我們對所有轉發失敗的請求都會執行統一降級操作,只要業務方配置了降級規則,都會進行降級,我們支持將降級規則細化到參數級別,包括請求頭中的值,非常細粒度,此外,我們還會與 varnish 集成,支持 varnish 的優雅降級;

  • 7)流量調度:支持業務根據篩選規則,將流量分配到對應的機器,也支持僅讓篩選的流量訪問該機器,這在排查問題/新功能發布驗證時非常有用,可以先通過小部分流量驗證,再大面積發布上線;

  • 8)流量 copy:我們支持根據規則對線上原始請求 copy 一份,將其寫入 MQ 或其他 upstream,用于線上跨機房驗證和壓力測試;

  • 9)請求日志采樣:我們對所有失敗的請求都會進行采樣并保存到磁盤,以供業務方排查問題,同時也支持業務方根據規則進行個性化采樣,我們采樣了整個生命周期的數據,包括請求和響應相關的所有數據。

上述提到的所有功能都是對流量進行管理,我們每個功能都作為一個 filter,處理失敗都不會影響轉發流程,而且所有這些規則的元數據在網關啟動時就會全部初始化好。

在執行過程中,不會進行 IO 操作,目前有些設計會對多個 filter 進行并發執行,由于我們的操作都是在內存中進行,開銷并不大,所以我們目前并未支持并發執行。

另外,規則可能會發生變化,所有需要進行規則的動態刷新。

我們在修改規則時,會通知網關服務,進行實時刷新,我們對內部自己的這種元數據更新請求,通過獨立的線程處理,防止 IO 操作時影響業務線程。

2.3 服務調用層

服務調用對于代理網關服務非常關鍵,這個環節,性能必須很高:必須采用異步方式,

我們利用 Netty 實現了這一目標,同時也充分利用了 Netty 提供的連接池,實現了獲取和釋放的無鎖操作。

2.3.1 異步 Push

在發起服務調用后,網關允許工作線程繼續處理其他請求,而無需等待服務端返回。

在這個設計中,我們為每個請求創建一個上下文,發送請求后,將該請求的 context 綁定到相應的連接上,當 Netty 收到服務端響應時,會在連接上執行 read 操作。

解碼完成后,再從連接上獲取相應的 context,通過 context 可以獲取到接入端的 session。

這樣,push 通過 session 將響應寫回客戶端,這個設計基于 HTTP 連接的獨占性,即連接和請求上下文綁定。

2.3.2 連接池

連接池的原理如下圖

注意:請點擊圖像以查看清晰的視圖!

服務調用層除了異步發起遠程調用外,還需要管理后端服務的連接。

HTTP 與 RPC 不同,HTTP 連接是獨占的,所以在釋放連接時需要特別小心,必須等待服務端響應完成后才能釋放,此外,連接關閉的處理也需要謹慎。

總結如下幾點

  • 1)Connection:close;
  • 2)空閑超時,關閉連接;
  • 3)讀超時關閉連接;
  • 4)寫超時,關閉連接;
  • 5)Fin、Reset。

上面幾種需要關閉連接的場景,下面主要說下 Connection:close 和空閑寫超時兩種,其他情況如讀超時、連接空閑超時、收到 fin、reset 碼等都比較常見。

2.3.3 Connection:close

后端服務采用的是 Tomcat,它對連接的重用次數有規定,默認為 100 次。

當達到 100 次限制時,Tomcat 會在響應頭中添加 Connection:close,要求客戶端關閉該連接,否則再次使用該連接發送請求會出現 400 錯誤。

還有就是如果前端的請求帶了 connection:close,那 Tomcat 就不會等待該連接重用滿 100 次,即一次就關閉連接。

在響應頭中添加 Connection:close 后,連接變為短連接。

在與 Tomcat 保持長連接時,需要注意這一點,如果要利用該連接,需要主動移除 close 頭。

2.3.4 寫超時

首先,網關在何時開始計算服務的超時時間?

如果從調用 writeAndFlush 開始計算,實際上包含了 Netty 對 HTTP 的編碼時間和從隊列中發送請求即 flush 的時間,這樣對后端服務不公平。

因此,需要在真正 flush 成功后開始計時,這樣最接近服務端,當然還包含了網絡往返時間和內核協議棧處理時間,這是無法避免的,但基本穩定。

因此,我們在 flush 成功回調后啟動超時任務。

需要注意的是:如果 flush 不能快速回調,例如遇到一個大的 POST 請求,body 部分較大,而 Netty 發送時默認第一次只發送 1k 大小。

如果尚未發送完畢,會增大發送大小繼續發送,如果在 Netty 發送 16 次后仍未發送完成,將不再繼續發送,而是提交一個 flushTask 到任務隊列,待下次執行后再發送。

此時,flush 回調時間較長,導致此類請求無法及時關閉,后端服務 Tomcat 會一直阻塞在讀取 body 部分,基于上述分析,我們需要設置寫超時,對于大的 body 請求,通過寫超時及時關閉連接。

3、全鏈路超時機制

注意:請點擊圖像以查看清晰的視圖!

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

  • 1)協議解析超時;
  • 2)等待隊列超時;
  • 3)建連超時;
  • 4)等待連接超時;
  • 5)寫前檢查是否超時;
  • 6)寫超時;
  • 7)響應超時。
 

4、監控報警

對于網關的業務方來說,他們能看到的是監控和警報功能,我們能夠實現秒級的報警和監控,將監控數據定時上傳到我們的管理系統,由管理系統負責匯總統計并存儲到 InfluxDB 中。

我們對 HTTP 協議進行了全面的監控和警報,涵蓋了協議層和服務層的問題。

協議層

  • 1)針對攻擊性請求,只發送頭部,不發送或只發送部分 body,我們會進行采樣并記錄,還原現場,并觸發警報;
  • 2)對于 Line 或 Head 或 Body 過大的請求,我們會進行采樣記錄,還原現場,并及時發出警報。

應用層

  • 1)監控耗時:包括慢請求,超時請求,以及 tp99,tp999 等;

  • 2)監控 OPS:并及時發出警報;

  • 3)帶寬監控和報警:支持對請求和響應的行,頭,body 單獨監控;

  • 4)響應碼監控:特別是 400,和 404;

  • 5)連接監控:我們對接入端的連接,以及與后端服務的連接,以及后端服務連接上待發送字節大小都進行了監控;

  • 6)失敗請求監控

  • 7)流量抖動報警:這是非常必要的,流量抖動可能是出現問題,或者是問題即將出現的預兆。

總體架構

注意:請點擊圖像以查看清晰的視圖!

5、性能優化實踐

5.1 對象池技術

針對高并發系統,不斷地創建對象不僅會占用內存資源,還會對垃圾回收過程產生壓力。

為了解決這個問題,我們在實現過程中會對諸如線程池的任務、StringBuffer 等頻繁使用的對象進行重用,從而降低內存分配的開銷。

5.2 上下文切換

在高并發系統中,通常會采用異步設計。異步化后,線程上下文切換的問題必須得到關注。

我們的線程模型如下

注意:請點擊圖像以查看清晰的視圖!

我們的網關沒有涉及 I/O 操作,但在業務邏輯處理方面仍然采用了 Netty 的 I/O 編解碼線程異步方式。

這主要有兩個原因

  • 1)防止開發人員編寫的代碼出現阻塞現象;

  • 2)在突發情況下,業務邏輯可能會產生大量的日志記錄,我們允許在推送線程時使用 Netty 的 I/O 線程作為替代。這種做法可以減少 CPU 上下文切換的次數,從而提高整體吞吐量。我們不能僅僅為了異步而異步,Zuul2 的設計理念與我們的做法相似。

5.3 GC優化

在高并發系統中,垃圾回收GC的優化是必不可少的。

我們采用了對象池技術和堆外內存,使得對象很少進入老年代,同時年輕代的設置較大,SurvivorRatio 設置為 2,晉升年齡設置最大為 15,以盡量讓對象在年輕代就被回收。

但監控發現老年代的內存仍在緩慢增長。通過dump分析,我們每個后端服務創建一個鏈接,都時有一個socket,socket的AbstractPlAInSocketImpl,而AbstractPlainSocketImpl就重寫了Object類的finalize方法。

實現如下

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

是為了我們沒有主動關閉鏈接,做的一個兜底,在gc回收的時候,先把對應的鏈接資源給釋放了。

由于finalize 的機制是通過 JVM 的 Finalizer 線程處理的,其優先級不高,默認為 8。它需要等待 Finalizer 線程把 ReferenceQueue 的對象對應的 finalize 方法執行完,并等到下次垃圾回收時,才能回收該對象。這導致創建鏈接的這些對象在年輕代不能立即回收,從而進入了老年代,這也是老年代持續緩慢增長的原因。

5.4 日志

在高并發系統中,尤其是 Netty 的 I/O 線程,除了執行 I/O 讀寫操作外,還需執行異步任務和定時任務。如果 I/O 線程處理不過隊列中的任務,可能會導致新進來的異步任務被拒絕。

在什么情況下可能會出現這種情況呢?異步讀寫問題不大,主要是多耗點 CPU。最有可能阻塞 I/O 線程的是日志記錄。目前 Log4j 的 ConsoleAppender 日志 immediateFlush 屬性默認為 true,即每次記錄日志都是同步寫入磁盤,這對于內存操作來說,速度較慢。

同時,AsyncAppender 的日志隊列滿了也會阻塞線程。Log4j 默認的 buffer 大小是 128,而且是阻塞的。即當 buffer 大小達到 128 時,會阻塞寫日志的線程。在并發寫日志量較大且堆棧較深的情況下,Log4j 的 Dispatcher 線程可能會變慢,需要刷盤。這樣 buffer 就不能快速消費,很容易寫滿日志事件,導致 Netty I/O 線程被阻塞。因此,在記錄日志時,我們需要注意精簡。

6、未來規劃

目前,我們都在使用基于 HTTP/1 的協議。

相對于 HTTP/1,HTTP/2 在連接層面實現了服務,即在一個連接上可以發送多個 HTTP 請求。

這就意味著 HTTP 連接可以像 RPC 連接一樣,建立幾個連接即可,完全解決了 HTTP/1 連接無法復用導致的重復建連和慢啟動的開銷。

我們正在基于 Netty 升級到 HTTP/2,除了技術升級外,我們還在不斷優化監控報警,以便為業務方提供準確無誤的報警。此外,我們還在作為統一接入網關與業務方實施全面的降級措施,以確保全站任何故障都能通過網關第一時間降級,這也是我們的重點工作。

分享到:
標簽:架構 設計
用戶無頭像

網友整理

注冊時間:

網站:5 個   小程序:0 個  文章:12 篇

  • 51998

    網站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會員

趕快注冊賬號,推廣您的網站吧!
最新入駐小程序

數獨大挑戰2018-06-03

數獨一種數學游戲,玩家需要根據9

答題星2018-06-03

您可以通過答題星輕松地創建試卷

全階人生考試2018-06-03

各種考試題,題庫,初中,高中,大學四六

運動步數有氧達人2018-06-03

記錄運動步數,積累氧氣值。還可偷

每日養生app2018-06-03

每日養生,天天健康

體育訓練成績評定2018-06-03

通用課目體育訓練成績評定