長連線閘道器技術專題(五):喜馬拉雅自研億級API閘道器技術實踐

語言: CN / TW / HK

本文由喜馬拉雅技術團隊原創分享,原題《喜馬拉雅自研閘道器架構實踐》,有改動。

1、引言

閘道器是一個比較成熟的產品,基本上各大網際網路公司都會有閘道器這個中介軟體,來解決一些公有業務的上浮,而且能快速的更新迭代。如果沒有閘道器,要更新一個公有特性,就要推動所有業務方都更新和釋出,那是效率極低的事,有閘道器後,這一切都變得不是問題。

喜馬拉雅也是一樣,使用者數增長達到 6 億多的級別,Web 服務個數達到500+,目前我們閘道器日處理 200 億+次呼叫,單機 QPS 高峰達到 4w+。

閘道器除了要實現最基本的功能反向代理外,還有公有特性,比如黑白名單,流控,鑑權,熔斷,API 釋出,監控和報警等。我們還根據業務方的需求實現了流量排程,流量 Copy,預釋出,智慧化升降級,流量預熱等相關功能。

從技術上來說,喜馬拉雅API閘道器的技術演進路線圖大致如下:

本文將分享在喜馬拉雅API閘道器在億級流量前提下,進行的技術演進發展歷程和實踐經驗總結。

學習交流:

- 移動端IM開發入門文章:《新手入門一篇就夠:從零開發移動端IM

- 開源IM框架原始碼:https://github.com/JackJiang2011/MobileIMSDK

本文同步釋出於:http://www.52im.net/thread-3564-1-1.html

2、專題目錄

本文是系列文章的第5篇,總目錄如下:

長連線閘道器技術專題(一):京東京麥的生產級TCP閘道器技術實踐總結

長連線閘道器技術專題(二):知乎千萬級併發的高效能長連線閘道器技術實踐

長連線閘道器技術專題(三):手淘億級移動端接入層閘道器的技術演進之路

長連線閘道器技術專題(四):愛奇藝WebSocket實時推送閘道器技術實踐

長連線閘道器技術專題(五):喜馬拉雅自研億級API閘道器技術實踐》(* 本文)

3、第1版:Tomcat NIO+Async Servlet

閘道器在架構設計時最為關鍵點,就是閘道器在接收到請求,呼叫後端服務時不能阻塞 Block,否則閘道器的吞吐量很難上去,因為最耗時的就是呼叫後端服務這個遠端呼叫過程。

如果這裡是阻塞的,Tomcat 的工作執行緒都 block 住了,在等待後端服務響應的過程中,不能去處理其他的請求,這個地方一定要非同步。

架構圖如下:

這版我們實現單獨的 Push 層,作為閘道器收到響應後,響應客戶端時,通過這層實現,和後端服務的通訊是 HttpNioClient,對業務的支援黑白名單,流控,鑑權,API 釋出等功能。

但是這版只是功能上達到閘道器的要求,處理能力很快就成了瓶頸,單機 QPS 到 5K 的時候,就會不停的 Full GC。

後面通過 Dump 線上的堆分析,發現全是 Tomcat 快取了很多 HTTP 的請求,因為 Tomcat 預設會快取 200 個 requestProcessor,每個 prcessor 都關聯了一個 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 做接入層和服務呼叫層,也就是我們的第二版,能徹底解決上面的問題,達到理想的效能。

4、第2版:Netty+全非同步

基於 Netty 的優勢,我們實現了全非同步,無鎖,分層的架構。

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

PS:如果你對Netty和Java NIO瞭解太少,下面幾篇資料請務必閱讀:

少囉嗦!一分鐘帶你讀懂Java的NIO和經典IO的區別

Java的BIO和NIO很難懂?用程式碼實踐給你看,再不懂我轉行!

史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!

寫給初學者:Java高效能NIO框架Netty的學習方法和進階策略

新手入門:目前為止最透徹的的Netty高效能原理和框架架構解析

史上最通俗Netty框架入門長文:基本介紹、環境搭建、動手實戰

4.1 接入層

Netty 的 IO 執行緒,負責 HTTP 協議的編解碼工作,同時對協議層面的異常做監控報警。

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

假如客戶端傳送了超過閥值的請求,帶 cookie 的請求很容易超過,正常情況下,Netty 就直接響應 400 給客戶端。

經過改造後,我們只取正常大小的部分,同時標記協議解析失敗,到業務層後,就可以判斷出是那個服務出現這類問題,其他的一些攻擊性的請求,比如只發請求頭,不發 body 或者發部分這些都需要監控和報警。

4.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 在操作時影響業務執行緒。

4.3 服務呼叫層

服務呼叫對於代理閘道器服務是關鍵的地方,一定需要非同步,我們通過 Netty 實現,同時也很好的利用了 Netty 提供的連線池,做到了獲取和釋放都是無鎖操作。

4.3.1)非同步 Push:

閘道器在發起服務呼叫後,讓工作執行緒繼續處理其他的請求,而不需要等待服務端返回。

這裡的設計是我們為每個請求都會建立一個上下文,我們在發完請求後,把該請求的 context 繫結到對應的連線上,等 Netty 收到服務端響應時,就會在給連線上執行 read 操作。

解碼完後,再從給連線上獲取對應的 context,通過 context 可以獲取到接入端的 session。

這樣 push 就通過 session 把響應寫回客戶端了,這樣設計也是基於 HTTP 的連線是獨佔的,即連線和請求上下文繫結。

4.3.2)連線池:

連線池的原理如下圖:

服務呼叫層除了非同步發起遠端呼叫外,還需要對後端服務的連線進行管理。

HTTP 不同於 RPC,HTTP 的連線是獨佔的,所以在釋放的時候要特別小心,一定要等服務端響應完了才能釋放,還有就是連線關閉的處理也要小心。

總結如下幾點:

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

上面幾種需要關閉連線的場景,下面主要說下 Connection:close 和空閒寫超時兩種,其他的應該是比較常見的比如讀超時,連線空閒超時,收到 fin,reset 碼這幾個。

4.3.3)Connection:close:

後端服務是 Tomcat,Tomcat 對連線重用的次數是有限制的,預設是 100 次。

當達到 100 次後,Tomcat 會通過在響應頭裡新增 Connection:close,讓客戶端關閉該連線,否則如果再用該連線傳送的話,會出現 400。

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

通過在響應頭裡新增 Connection:close,即成了短連線,這個在和 Tomcat 保持長連線時,需要注意的,如果要利用,就要主動 remove 掉這個 close 頭。

4.3.4)寫超時:

首先閘道器什麼時候開始計算服務的超時時間,如果從呼叫 writeAndFlush 開始就計算,這其實是包含了 Netty 對 HTTP 的 encode 時間和從佇列裡把請求發出去即 flush 的時間,這樣是對後端服務不公平的。

所以需要在真正 flush 成功後開始計時,這樣是和服務端最接近的,當然還包含了網路往返時間和核心協議棧處理的時間,這個不可避免,但基本不變。

所以我們是 flush 成功回撥後開始啟動超時任務。

這裡就有個注意的地方:如果 flush 不能快速回調,比如來了一個大的 post 請求,body 部分比較大,而 Netty 傳送的時候第一次預設是發 1k 的大小。

如果還沒有發完,則增大發送的大小繼續發,如果在 Netty 在 16 次後還沒有傳送完成,則不會再繼續傳送,而是提交一個 flushTask 到任務佇列,待下次執行到後再發送。

這時 flush 回撥的時間就比較大,導致這樣的請求不能及時關閉,而且後端服務 Tomcat 會一直阻塞在讀 body 的地方,基於上面的分析,所以我們需要一個寫超時,對大的 body 請求,通過寫超時來及時關閉。

5、全鏈路超時機制

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

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

6、監控報警

閘道器業務方能看到的是監控和報警,我們是實現秒級別報警和秒級別的監控,監控資料定時上報給我們的管理系統,由管理系統負責聚合統計,落盤到 influxdb。

我們對 HTTP 協議做了全面的監控和報警,無論是協議層的還是服務層的。

協議層:

  • 1)攻擊性請求,只發頭,不發/發部分 body,取樣落盤,還原現場,並報警;
  • 2)Line or Head or Body 過大的請求,取樣落盤,還原現場,並報警。

應用層:

  • 1)耗時監控:有慢請求,超時請求,以及 tp99,tp999 等;
  • 2)OPS 監控和報警;
  • 3)頻寬監控和報警:支援對請求和響應的行,頭,body 單獨監控;
  • 4)響應碼監控:特別是 400,和 404;
  • 5)連線監控:我們對接入端的連線,以及和後端服務的連線,後端服務連線上待發送位元組大小也都做了監控;
  • 6)失敗請求監控;
  • 7)流量抖動報警:這是非常有必要的,流量抖動要麼是出了問題,要麼就是出問題的前兆。

總體架構:

7、效能優化實踐

7.1 物件池技術

對於高併發系統,頻繁的建立物件不僅有分配記憶體的開銷外,還有對gc會造成壓力,我們在實現時會對頻繁使用的比如執行緒池的任務task,StringBuffer等會做寫重用,減少頻繁的申請記憶體的開銷。

7.2 上下文切換

高併發系統,通常都採用非同步設計,非同步化後,不得不考慮執行緒上下文切換的問題。

我們的執行緒模型如下:

我們整個閘道器沒有涉及到io操作,但我們在業務邏輯這塊還是和netty的io編解碼執行緒非同步。

是有兩個原因:

  • 1)是防止開發寫的程式碼有阻塞;
  • 2)是業務邏輯打日誌可能會比較多,在突發的情況下,但是我們在push執行緒時,支援用netty的io執行緒替代,這裡做的工作比較少,這裡有非同步修改為同步後(通過修改配置調整),cpu的上下文切換減少20%,進而提高了整體的吞吐量,就是不能為了非同步而非同步,zull2的設計和我們的類似。

7.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執行緒來處理的,而且Finalizer執行緒的優先順序不高,預設是8,需要等到Finalizer執行緒把ReferenceQueue的物件對於的finalize方法執行完,還要等到下次gc時,才能把該物件回收,導致建立連結的這些物件在年輕代不能立即回收,從而進入了老年代,這也是為啥老年代會一直緩慢增長的問題。

7.4 日誌

高併發下,特別是 Netty 的 IO 執行緒除了要執行該執行緒上的 IO 讀寫操作,還有執行非同步任務和定時任務,如果 IO 執行緒處理不過來佇列裡的任務,很有可能導致新進來非同步任務出現被拒絕的情況。

那什麼情況下可能呢?IO 是非同步讀寫的問題不大,就是多耗點 CPU,最有可能 block 住 IO 執行緒的是我們打的日誌。

目前 Log4j 的 ConsoleAppender 日誌 immediateFlush 屬性預設為 true,即每次打 log 都是同步寫 flush 到磁碟的,這個對於記憶體操作來說,慢了很多。

同時 AsyncAppender 的日誌佇列滿了也會 block 住執行緒,log4j 預設的 buffer 大小是 128,而且是 block 的。

即如果 buffer 的大小達到 128,就阻塞了寫日誌的執行緒,在併發寫日誌量大的的情況下,特別是堆疊很多時,log4j 的 Dispatcher 執行緒會出現變慢要刷盤。

這樣 buffer 就不能快速消費,很容易寫滿日誌事件,導致 Netty IO 執行緒 block 住,所以我們在打日誌時,也要注意精簡。

8、未來規劃

現在我們都是基於 HTTP/1,現在 HTTP/2 相對於 HTTP/1 關鍵實現了在連線層面的服務,即一個連線上可以傳送多個 HTTP 請求。

即 HTTP 連線也能和 RPC 連線一樣,建幾個連線就可以了,徹底解決了 HTTP/1 連線不能複用導致每次都建連和慢啟動的開銷。

我們也在基於 Netty 升級到 HTTP/2,除了技術升級外,我們對監控報警也一直在持續優化,怎麼提供給業務方準確無誤的報警,也是一直在努力。

還有一個就是降級,作為統一接入閘道器,和業務方做好全方位的降級措施,也是一直在完善的點,保證全站任何故障都能通過閘道器第一時間降級,也是我們的重點。

9、寫在最後

閘道器已經是一個網際網路公司的標配,這裡總結實踐過程中的一些心得和體會,希望給大家一些參考以及一些問題的解決思路,我們也還在不斷完善中,同時我們也在做多活的專案,歡迎交流。

附錄:更多相關資料

[1] NIO非同步網路程式設計資料:

Java新一代網路程式設計模型AIO原理及Linux系統AIO介紹

有關“為何選擇Netty”的11個疑問及解答

MINA、Netty的原始碼(線上閱讀版)已整理髮布

詳解Netty的安全性:原理介紹、程式碼演示(上篇)

詳解Netty的安全性:原理介紹、程式碼演示(下篇)

詳解Netty的優雅退出機制和原理

NIO框架詳解:Netty的高效能之道

Twitter:如何使用Netty 4來減少JVM的GC開銷(譯文)

絕對乾貨:基於Netty實現海量接入的推送服務技術要點

新手入門:目前為止最透徹的的Netty高效能原理和框架架構解析

寫給初學者:Java高效能NIO框架Netty的學習方法和進階策略

少囉嗦!一分鐘帶你讀懂Java的NIO和經典IO的區別

史上最強Java NIO入門:擔心從入門到放棄的,請讀這篇!

手把手教你用Netty實現網路通訊程式的心跳機制、斷線重連機制

Java的BIO和NIO很難懂?用程式碼實踐給你看,再不懂我轉行!

史上最通俗Netty框架入門長文:基本介紹、環境搭建、動手實戰

長連線閘道器技術專題(一):京東京麥的生產級TCP閘道器技術實踐總結

長連線閘道器技術專題(五):喜馬拉雅自研億級API閘道器技術實踐

>> 更多同類文章 ……

[2] 有關IM架構設計的文章:

淺談IM系統的架構設計

簡述移動端IM開發的那些坑:架構設計、通訊協議和客戶端

一套海量線上使用者的移動端IM架構設計實踐分享(含詳細圖文)

一套原創分散式即時通訊(IM)系統理論架構方案

從零到卓越:京東客服即時通訊系統的技術架構演進歷程

蘑菇街即時通訊/IM伺服器開發之架構選擇

騰訊QQ1.4億線上使用者的技術挑戰和架構演進之路PPT

如何解讀《微信技術總監談架構:微信之道——大道至簡》

快速裂變:見證微信強大後臺架構從0到1的演進歷程(一)

移動端IM中大規模群訊息的推送如何保證效率、實時性?

現代IM系統中聊天訊息的同步和儲存方案探討

微信朋友圈千億訪問量背後的技術挑戰和實踐總結

騰訊資深架構師乾貨總結:一文讀懂大型分散式系統設計的方方面面

以微博類應用場景為例,總結海量社交系統的架構設計步驟

子彈簡訊光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐

一套高可用、易伸縮、高併發的IM群聊、單聊架構方案設計實踐

社交軟體紅包技術解密(一):全面解密QQ紅包技術方案——架構、技術實現等

即時通訊新手入門:一文讀懂什麼是Nginx?它能否實現IM的負載均衡?

從游擊隊到正規軍(一):馬蜂窩旅遊網的IM系統架構演進之路

從游擊隊到正規軍(二):馬蜂窩旅遊網的IM客戶端架構演進和實踐總結

從游擊隊到正規軍(三):基於Go的馬蜂窩旅遊網分散式IM系統技術實踐

瓜子IM智慧客服系統的資料架構設計(整理自現場演講,有配套PPT)

阿里釘釘技術分享:企業級IM王者——釘釘在後端架構上的過人之處

微信後臺基於時間序的新一代海量資料儲存架構的設計實踐

IM開發基礎知識補課(九):想開發IM叢集?先搞懂什麼是RPC!

阿里技術分享:電商IM訊息平臺,在群聊、直播場景下的技術實踐

一套億級使用者的IM架構技術乾貨(上篇):整體架構、服務拆分等

一套億級使用者的IM架構技術乾貨(下篇):可靠性、有序性、弱網優化等

從新手到專家:如何設計一套億級訊息量的分散式IM系統

>> 更多同類文章 ……

[3] 更多其它架構設計相關文章:

騰訊資深架構師乾貨總結:一文讀懂大型分散式系統設計的方方面面

快速理解高效能HTTP服務端的負載均衡技術原理

子彈簡訊光鮮的背後:網易雲信首席架構師分享億級IM平臺的技術實踐

知乎技術分享:從單機到2000萬QPS併發的Redis高效能快取實踐之路

新手入門:零基礎理解大型分散式架構的演進歷史、技術原理、最佳實踐

阿里技術分享:深度揭祕阿里資料庫技術方案的10年變遷史

阿里技術分享:阿里自研金融級資料庫OceanBase的艱辛成長之路

達達O2O後臺架構演進實踐:從0到4000高併發請求背後的努力

優秀後端架構師必會知識:史上最全MySQL大表優化方案總結

小米技術分享:解密小米搶購系統千萬高併發架構的演進和實踐

一篇讀懂分散式架構下的負載均衡技術:分類、原理、演算法、常見方案等

通俗易懂:如何設計能支撐百萬併發的資料庫架構?

多維度對比5款主流分散式MQ訊息佇列,媽媽再也不擔心我的技術選型了

從新手到架構師,一篇就夠:從100到1000萬高併發的架構演進之路

美團技術分享:深度解密美團的分散式ID生成演算法

12306搶票帶來的啟示:看我如何用Go實現百萬QPS的秒殺系統(含原始碼)

>> 更多同類文章 ……

本文已同步釋出於“即時通訊技術圈”公眾號。

▲ 本文在公眾號上的連結是:點此進入。同步釋出連結是:http://www.52im.net/thread-3564-1-1.html

「其他文章」