拋棄Spring Cloud Gateway,得物 使用Netty架構100Wqps網關

文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :

免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領

免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取


拋棄Spring Cloud Gateway,得物 使用Netty架構100Wqps網關

說在前面

在40歲老架構師 尼恩的讀者交流羣(50+)中,很多小夥伴拿到一線互聯網企業如阿里、網易、有贊、希音、百度、滴滴的面試資格。

最近,尼恩指導一個小夥伴簡歷,寫了一個《高併發網關項目》,此項目幫這個小夥拿到 字節/阿里/微博/汽車之家 面邀, 所以說,這是一個牛逼的項目。

爲了幫助大家拿到更多面試機會,拿到更多大廠offer。尼恩給大家出一章視頻介紹這個項目的架構和實操,《33章:10Wqps 高併發 Netty網關架構與實操》。然後,提供一對一的簡歷指導,讓你簡歷金光閃閃、脫胎換骨。

《第33章:10Wqps 高併發 Netty網關架構與實操》 的視頻介紹在此:

一個頂奢、塔尖的簡歷黃金項目:18Wqps單體Netty API網關的架構與實操

同時,配合《33章:10Wqps 高併發 Netty網關架構與實操》, 尼恩會梳理幾個工業級、生產級網關案例,作爲架構素材、設計的素材。前面梳理了:

除了以上的10個案例,這裏,尼恩給大家介紹:《企業級API網關從入門到精通》。

注意:這些生產案例來自互聯網,並不是尼恩的原創。

這些案例,僅僅是尼恩在《33章:10Wqps 高併發 Netty網關架構與實操》備課的過程中,在互聯網查找資料的時候,收集起來的,供大家學習和交流使用。

最新《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請到公號【技術自由圈】獲取

本文目錄

什麼是API網關?

API 網關是一種服務器,作爲應用程序編程接口 (API) 的入口點,它接收來自外部應用程序的請求,進行處理,並給出恰當的迴應。你可以將它看作一箇中間件,管理API的訪問,並在請求與迴應之間進行轉換、路由、安全檢查等操作。

API網關功能

得物自研API網關(DAG)實踐之路

作者:簌語,原文來自 得物技術公衆號

一、業務背景

我們之前的網關是基於Spring Cloud Gateway(簡稱SCG)技術框架搭建的。SCG基於webflux編程範式,

webflux是一種響應式編程理念,對於提高系統的吞吐量和性能有很大的幫助;

webflux 的底層構建在netty之上, 性能表現優秀;

SCG是spring生態的產物,具有開箱即用的特點,較低的使用成本助力得物早期的業務快速發展;

然而,隨着公司業務的快速發展,流量越來越大,網關的業務邏輯越來越多,以及安全審計需求的不斷升級和穩定性需求的提高,SCG在以下幾個方面逐步暴露了一系列的問題。

SCG的網絡安全問題

從網絡安全角度來講,對公網暴露接口無疑是一件風險極高的事情,

網關是對外網絡流量的重要橋樑,早期的接口暴露採用泛化路由的模式,即通過正則形式( /api/v1/app/order/** )的路由規則開放接口,單個應用服務往往只配置一個泛化路由,後續上線新接口時外部可以直接訪問;

這種做法帶來了極大的安全風險,很多時候業務開發的接口可能僅僅是內部調用,但是一不小心就被泛化路由開放到了公網,甚至很多時候沒人說得清楚某個服務具體有多少接口屬於對外,多少對內;

另一方面從監控數據來看,黑產勢力也在不斷對我們的接口做滲透試探。

網絡黑產,也稱爲網絡黑色產業鏈,主要是指通過互聯網技術進行網絡攻擊、竊取信息、勒索詐騙、盜竊財產、推廣色情和賭博等非法網絡活動,以及爲這些活動提供工具、資源、平臺等支持和非法獲利變現的渠道和環節。

“黑產勢力”產業具備兩個特點,一是,他們的騙局設計非常周密,模仿了互聯網平臺的客戶服務場景,從宣傳引導到電話和微信等客服交流,精心設計陷阱讓首次接觸的消費者難以區分真僞。二是,他們利用移動支付手段,利用了互聯網信息管理不完善的漏洞。犯罪分子利用他人信息申請虛擬運營商電話卡,隱藏自己的行蹤,使公安機關難以找到犯罪分子的真實信息。此外,二維碼等移動支付方式非常便捷,消費者支付後的資金流向往往難以追蹤。

在中央網信辦、工業和信息化部、公安部、人民銀行的指導下,國家互聯網應急中心支持相關部門開展網絡黑產治理工作。從2019年6月至10月,他們累計治理了561萬個活躍手機黑卡,3萬餘個賭博網站,60個非法網絡金融平臺,1000餘個黑產交流傳播羣組,11個違規廣告聯盟,124個瀏覽器主頁劫持惡意軟件,60個大型賭博洗錢團伙線索,以及28個DDoS攻擊控制端。

SCG的協同效率問題

通過引入接口註冊機制,所有對外開放的接口都必須註冊到網關,未經註冊的接口無法被訪問,從而確保了安全性,

然而,這種方式也帶來了性能問題。SCG採用遍歷方式匹配路由規則,接口註冊模式推廣後路由接口註冊數量迅速提升到3W+,路由匹配性能出現嚴重問題;

在泛化路由時代,每個服務僅需一個路由配置,且變更頻率較低,主要由網關開發人員負責配置,這樣的效率尚可,

但接口註冊模式將路由工作移交給了業務開發人員,這就需要建立一個完整的路由審覈流程,以提高協同效率;

由於早期所有路由信息都存儲在配置中心,這不僅給配置中心帶來了巨大壓力,也增加了穩定性風險。

SCG的性能與維護成本問題

隨着業務迭代的不斷增加,API網關積累了大量業務邏輯,

這些業務邏輯分散在不同的filter中,爲了降低開發成本,網關採用了一套主線分支,不同集羣的代碼完全相同,

然而,不同集羣的業務屬性不同,所需的filter邏輯也不盡相同;

如內網網關集羣幾乎沒什麼業務邏輯,但是App集羣可能需要幾十個filter的邏輯協同工作;

這樣的一套代碼對內網網關而言,存在着大量的性能浪費;因此,如何平衡維護成本和運行效率是一個需要深入思考的問題。

SCG的穩定性風險

API網關作爲基礎服務,承載全站的流量出入,穩定性無疑是第一優先級,

然而,其定位決定了絕不可能是一個簡單的代理層,在穩定運行的同時依然需要承接大量業務需求,

例如,C端用戶登錄下線能力,App強升能力,B端場景下的鑑權能力等;

很難想象較長一段時間以來,網關都保持着雙週一次的發版頻率;

頻繁的發版也帶來了一些問題,實例啓動初期有很多資源需要初始化,此時承接的流量處理時間較長,存在着明顯的接口超時現象;早期的每次發版幾乎都會導致下游服務的接口短時間內超時率大幅提高,而且往往涉及多個服務一起出現類似情況;

爲此甚至拉了一個網關發版公告羣,提前置頂發版公告,讓業務同學和NOC有一個心裏預期;

在發佈升級期間儘可能讓業務服務無感知這是個剛需。

SCG的定製能力問題

流量灰度是網關最常見的功能之一,

對於新版本迭代,業務服務的某個節點發布新版本後希望引入少部分流量試跑觀察,但很遺憾SCG原生並不支持,

需要對負載均衡算法進行手動改寫纔可以,此外基於流量特徵的定向節點路由也需要手動開發,

由於負載均衡算法是SCG的核心模塊,不對外暴露,因此改造成本較高。。

此外,B端和C端業務對接口響應時間的需求不同,SCG也無法滿足這種定製化需求,

B端場景下下載一個報表用戶可以接受等待10s或者1分鐘,但是C端用戶現在沒有這個耐心。

作爲代理層針對以上的場景,我們需要針對不同接口定製不同的超時時間,原生的SCG顯然也不支持。

諸如此類的定製需求還有很多,我們並不寄希望於開源產品能夠開箱即用滿足全部需求,但至少定製性拓展性足夠好。上手改造成本低。

SCG在協同效率、性能、維護成本、穩定性風險和定製能力方面存在一系列問題。這些問題源於接口註冊機制帶來的性能影響、業務邏輯分散導致的維護成本問題、穩定性風險的挑戰、缺乏靈活的定製能力,以及對不同業務需求的不適應。解決這些問題需要重新審視和優化接口註冊機制、路由策略、負載均衡算法,以及提高網關的定製性和擴展性,以確保API網關能夠高效、穩定地支撐業務發展。

二、SCG的技術痛點

SCG主要基於webflux技術構建,其底層依賴於reactor-netty,而reactor-netty又是基於netty的;

這種架構使得SCG能夠與spring cloud的技術組件無縫集成,開箱即用,爲得物早期的業務快速發展提供了便利;

然而,使用webflux技術也帶來了一些成本,

首先它會額外增加編碼人員的心智負擔,他們需要理解流的概念以及常用的操作函數,諸如map, flatmap, defer 等等;

其次,異步非阻塞的編程方式導致代碼中充滿了回調函數,這可能會割裂業務邏輯的順序性,增加代碼的閱讀和理解難度;

進一步評估發現,SCG存在一些缺點:

內存泄露問題

SCG存在多個內存泄漏問題,這些問題的排查困難,且官方遲遲未能修復。長期運行可能導致服務觸發OOM並宕機;

以下爲github上SCG官方開源倉庫的待解決的內存泄漏問題,大約有16個之多。

SCG內存泄漏BUG

下圖可以看到SCG在長期運行的過程中內存使用一直在增長,當增長到機器內存上限時,當前節點將不可用,

聯繫到網關單節點所承接的QPS 在幾千,可想而知節點宕機帶來的危害有多大;

一段時間以來我們需要對SCG網關做定期重啓。

SCG生產實例內存增長趨勢

響應式編程範式複雜

基於webflux的flux和mono,在對request和response信息讀取修改時,編碼複雜度高,代碼理解困難,下圖是對body信息進行修改時的代碼邏輯。

對requestBody 進行修改的方式

多層抽象的性能損耗

儘管相比於傳統的阻塞式網關,SCG的性能已經足夠優秀,

但與原生的netty相比,SCG的性能仍然較低。這是因爲SCG依賴於webflux編程範式,而webflux又是構建在reactor-netty之上的,這導致多層抽象存在較大的性能損耗。

SCG依賴層級

一般認爲, 程序調用棧越深性能越差;

下圖爲只有一個filter的情況下的調用棧,可以看到存在大量的 webflux 中的 subscribe() 和onNext() 方法調用,

這些方法的執行不關聯任何業務邏輯,屬於純粹的框架運行層代碼,

粗略估算下沒有引入任何邏輯的情況下,SCG的調用棧深度在 90+ ,

如果引入多個filter處理不同的業務邏輯,線程棧將進一步加深,

當前網關的業務複雜度實際棧深度會達到120左右,也就是差不多有四分之三的非業務棧損耗,這個比例是有點誇張的。

SCG filter 調用棧深度

路由能力不完善

原生的的SCG並不支持動態路由管理,經過改造後,這些配置數據一般放在諸如Apollo或者ark 這樣的配置中心,SCG路由的配置信息通過大量的KV配置來做,平均一個路由配置需要三到四條KV配置信息來支撐,

即使是添加了新的配置SCG,並不能動態識別,需要引入動態刷新路由配置的能力。

另一方面路由匹配算法通過遍歷所有的路由信息逐一匹配的模式,當接口級別的路由數量急劇膨脹時,性能是個嚴重問題。

SCG路由匹配算法爲On時間複雜度

預熱時間長,冷啓動RT尖刺大

SCG中LoadBalancerClient 會調用choose方法來選擇合適的endpoint 作爲本次RPC發起調用的真實地址,由於是懶加載,只有在有真實流量觸發時纔會加載創建相關資源;

在觸發底層的NamedContextFactory#getContext 方法時存在一個全局鎖導致,woker線程在該鎖上大量等待。

NamedContextFactory#getContext方法存在全局鎖

SCG發佈時超時報錯增多

定製性差,數據流控制耦合

在開發運維過程中,SCG已經出現了較多的針對源碼改造場景,如動態路由、路由匹配性能優化等;

其設計理念老舊,控制流和數據流混合使用,架構不清晰。例如,路由管理操作仍然耦合在filter中,

即使引入了spring mvc方式管理,依然綁定使用webflux編程範式,同時也無法做到控制流端口獨立,存在一定安全風險。

filter中對路由進行管理

SCG在技術上面臨諸多挑戰,包括內存泄漏、響應式編程的複雜性、多層抽象導致的性能損耗、不完善的路由能力、長的預熱時間和冷啓動延遲,以及差的可定製性和數據流控制耦合問題。這些問題影響了SCG的穩定性和性能,亟待解決。

三、方案調研

理想中的網關

綜合業務需求和技術痛點,我們發現理想型的網關應該是這個樣子的:

  • 支持海量接口註冊,並能夠在運行時支持動態添加修改路由信息,具備出色路由匹配性能

  • 編程範式儘可能簡單,降低開發人員心智負擔,同時最好是開發人員較爲熟悉的語言

  • 性能足夠好,至少要等同於目前SCG的性能,RT99線和ART較低

  • 穩定性好,無內存泄漏,能夠長時間持續穩定運行,發佈升級期間要儘可能下游無感

  • 拓展能力強,自定義超時機制、多種網絡協議如HTTP、Dubbo等,並擁有成熟的生態支持

  • 架構設計清晰,數據流與控制流分離,集成UI控制面

開源網關對比

基於以上需求,我們對市面上的常見網關進行了調研,以下幾個開源方案對比。

結合當前團隊的技術棧,我們傾向於選擇Java技術棧的開源產品,

儘管zuul2是我們唯一的選擇,但它在路由註冊和穩定性方面仍無法滿足我們的要求,且沒有實現數據流與控制流的分離架構設計。

因此唯有走上自研之路。

理想的網關應當是業務和技術雙贏的解決方案。它不僅需要具備強大的功能和優秀的性能,還要在穩定性和擴展性上表現出色。在現有開源方案無法完全滿足需求的情況下,自研成爲了我們的選擇。這不僅能夠讓我們根據自身業務和技術需求進行定製化開發,還能夠更好地掌握產品的未來發展,確保長期的適應性和領先性。自研網關的開發將是團隊技術實力和創新精神的體現,也是我們對未來業務支撐能力和技術領先地位的投資。

四、自研架構

在代理網關的開發中,我們通常區分透明代理與非透明代理,兩者的核心差異體現在對流量的干預程度。具體來說,透明代理不對流量內容做任何修改,而非透明代理則會對請求和響應數據進行必要的調整。

鑑於API Gateway的本質功能,它必須對流轉的數據進行適度的處理,

常見的調整主要有

  • 請求修改: 添加或者修改head 信息,加密或者解密 query params head ,

  • 以及 requestbody 或者responseBody 修改,

  • 可以說http請求的每一個部分數據都存在修改的可能性,

這就要求代理層必須深入解讀數據包的細節,而不僅僅是執行簡單的路由轉發操作。

Reactor多線程架構

爲了達到更高的性能,我們需要減少多線程環境下代碼編寫時可能出現的競爭問題,

在處理請求的過程中,從接收請求開始,經過轉發到後端服務,接收後端的響應,再到最終發送給客戶端,這一連串的操作被設計成在單個的workerEventLoop線程中完整執行;

這需要worker線程中執行的IO類型操作全部實現異步非阻塞化,確保worker線程的高速運轉;

這樣的架構和NGINX很類似;我們稱之爲 request-per-thread模式 (單請求閉環)。

API網關組件架構

數據流控制流分離

數據面板專注於流量代理,不處理任何admin 類請求,控制流監聽獨立的端口,接收管理指令。

我們通過深入研究代理網關的架構,對透明與非透明代理的差異有了清晰的認識,並明確了API Gateway在數據處理上的需求。通過採用Reactor多線程模型,我們旨在減少線程競爭,提高處理性能。同時,我們將數據流與控制流分離,確保系統的高效穩定運行。這樣的設計不僅滿足了我們對高性能、高可靠性的需求,而且也爲未來的擴展和維護奠定了堅實的基礎。通過這些技術策略,我們堅信能夠打造出既靈活又強大的API網關,以適應不斷變化的業務需求和技術挑戰。

五、核心設計

請求上下文封裝

在重構API網關的底層架構時,我們保留了基於Netty的堅實基礎,並利用其內置的HTTP協議解析handler,省去了繁瑣的註冊過程。
遵循Netty的編程範式,在初始化階段,我們無需手動註冊每個Handler,因爲Netty的 pipeline 機制會自動處理這一過程。

Client到Proxy鏈路Handler 執行順序

HttpServerCodec 負責HTTP請求的解析;

  • 對於體積較大的Http請求,客戶端可能會拆成多個小的數據包進行發送,因此在服務端需要適當的封裝拼接,避免收到不完整的http請求;

  • HttpObjectAggregator 負責整個請求的拼裝組合。

  • 拿到HTTP請求的全部信息後在業務handler 中進行處理;

  • 如果請求體積過大直接拋棄;

使用ServerWebExchange 對象封裝請求上下文信息,其中包含了

  • client2Proxy的channel,

  • 以及負責處理該channel 的eventLoop 線程等信息,

  • 引入了getAttributes 方法 用於存儲需要傳遞的數據;考慮到整個請求的處理過程中可能在不同階段傳遞一些拓展信息,

爲了最小化從SCG遷移到自研網關的改動,ServerWebExchange 接口在設計上儘量保持了與SCG的一致性,具體實現類也是在此基礎上進行調整的。可以參考如下代碼:

@Getter
  public class DefaultServerWebExchange implements ServerWebExchange {
    private final Channel client2ProxyChannel;
    private final Channel proxy2ClientChannel;
    private final EventLoop executor;
    private ServerHttpRequest request;
    private ServerHttpResponse response;
    private final Map<String, Object> attributes;
 }

DefaultServerWebExchange

Client2ProxyHttpHandler作爲核心的入口handler ,負責將接收到的FullHttpRequest 進行封裝和構建ServerWebExchange 對象,其核心邏輯如下。

@Override
protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest fullHttpRequest) {
    try {
        Channel client2ProxyChannel = ctx.channel();
        DefaultServerHttpRequest serverHttpRequest = new DefaultServerHttpRequest(fullHttpRequest, client2ProxyChannel);
        ServerWebExchange serverWebExchange = new DefaultServerWebExchange(client2ProxyChannel,(EventLoop) ctx.executor(), serverHttpRequest, null);
        // request filter chain
        this.requestFilterChain.filter(serverWebExchange);
    }catch (Throwable t){
        log.error("Exception caused before filters!\n {}",ExceptionUtils.getStackTrace(t));
        ByteBufHelper.safeRelease(fullHttpRequest);
        throw t;
    }
}

可以看到: 數據讀取封裝的邏輯較爲簡單,並沒有植入常見的業務邏輯,封裝完對象後隨即調用 Request filter chain。

這種設計確保了請求處理的靈活性和高效性,同時也爲未來的擴展奠定了基礎。通過這種封裝方式,我們能夠確保在遷移現有業務邏輯時,所需的工作量降至最低,大大提高了開發效率和系統的穩定性。這樣的設計不僅簡化了開發流程,也爲API網關的性能優化和功能增強提供了便利,使得整個系統能夠更好地適應不斷變化的技術環境和業務需求。

FilterChain設計

filter的執行需要定義先後順序,這裏參考了SCG的方案,每個filter返回一個order值。

不同的地方在於DAG的設計不允許 order值重複,因爲在order重複的情況下,很難界定到底哪個Filter 先執行,存在模糊地帶,這不是我們期望看到的;

DAG中的Filter 執行順序爲order值從小到大,且不允許order值重複。

爲了易於理解,這裏將Filter拆分爲兩類:requestFilter和responseFilter,分別對應於請求處理階段和響應處理階段;responseFilter也遵循相同的順序執行規則和唯一性原則。

public interface GatewayFilter extends Ordered {
    void filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

public interface ResponseFilter extends GatewayFilter { }

public interface RequestFilter extends GatewayFilter { }

filter接口設計

在API網關的設計中,過濾鏈的構建是一個關鍵環節。通過引入順序值來確定過濾器的執行順序,我們確保了請求和響應的處理能夠按照預定的邏輯順序進行。DAG的設計避免了執行順序的模糊性,保證了過濾鏈的清晰和可預測性。requestFilter和responseFilter的劃分,不僅使得過濾鏈的邏輯更加清晰,而且也便於管理和維護。這種設計思路體現了我們對系統性能和穩定性的追求,同時也爲API網關未來的擴展留下了空間,使其能夠更好地適應不同的業務場景和技術需求。

路由管理與匹配

以SCG網關注冊的路由數量爲基準,網關節點的需要支撐的路由規則數量是上萬級別的,

按照得物目前的業務量,上限不超過5W,爲了確保高效的匹配性能,將路由規則存儲在分佈式緩存中並不可行,它們需要被保留在節點的內存中。

類似於在nginx中配置上萬條location規則,這種手動維護的方式不僅難度巨大,即便在配置中心管理起來也是一項繁瑣的任務,因此,引入一個獨立的路由管理模塊變得至關重要。

在匹配的效率上也需要進一步優化,

SCG的路由匹配策略爲輪詢,時間效率爲On,

在路由規則膨脹到萬級別後,SCG性能急劇拉胯,

結合得物的接口規範,新網關採用Hash匹配模式,將匹配效率提升到O1;

hash的key爲接口的path,需要強調的是在同一個網關集羣中,path是唯一的,

這裏的path並不等價於業務服務的接口path, 絕大多數時候存在一些剪裁,例如在業務服務的編寫的/order/detail接口,在網關實際註冊的接口可能爲/api/v1/app/order/detail;

由於使用了path作爲key進行hash匹配。

常見的基於path傳參數模式的接口均不支持;

因此,網關保留了類似nginx的前綴匹配支持,但這一功能不對外部開放。這種設計決策旨在確保網關能夠高效地處理大量路由規則,同時簡化維護工作,並保持良好的性能。通過引入獨立的路由管理模塊和採用Hash匹配模式,新的網關不僅提高了匹配效率,也增強了系統的可維護性和穩定性,爲得物未來的業務擴展打下了堅實的基礎。

public class Route implements Ordered {
    private final String id;
    private final int skipCount;
    private final URI uri;
 }

單線程閉環

爲了更好地利用CPU,以及減少不必要的數據競爭,將單個請求的處理全部閉合在一個線程當中

這意味着這個請求的業務邏輯處理,RPC調用,權限驗證,限流token獲取都將始終由某個固定線程處理。

netty中 網絡連接被抽象爲channel,channel 與eventloop線程的對應關係爲 N對1,

一個channel 僅能被一個eventloop 線程所處理,這在處理用戶請求時沒有問題,

但是在接收請求完畢向下遊轉發請求時,我們碰到了一些挑戰:

因爲下游連接通常由連接池管理,而連接池的管理則由另一組eventLoop線程負責。爲了維持閉環處理,我們需要將連接池的線程設置爲與當前請求處理線程相同,這意味着只能有一個線程來處理這個請求;

因此,默認情況下啓動的N個線程(N與機器核心數相同)將分別負責管理一個連接池;

thread-per-core 模式的性能已經在nginx開源組件上得到驗證。

這種模型的核心優勢在於它可以減少線程間切換帶來的開銷,並避免了複雜的數據競爭問題。通過將請求的處理完全侷限於一個線程,我們能夠確保請求的處理流程更加直接和高效。

連接管理優化

爲了滿足單線程閉環,需要將連接池的管理線程設置爲當前的 eventloop 線程,最終我們通過threadlocal 進行線程與連接池的綁定;

在大多數場景下,netty自帶的FixedChannelPool連接池能夠滿足我們的需求,並且它也適用於多線程環境;

由於新網關使用thread-per-core模式並將請求處理的全生命週期閉合在單個線程中,所有爲了線程安全的額外操作不再必要且存在性能浪費;爲此需要對原生連接池做一些優化, 連接的獲取和釋放簡化爲對鏈表結構的簡單getFirst , addLast。

對於RPC 而言,無論是HTTP,還是Dubbo,Redis等最終底層都需要用到TCP連接,將構建在TCP連接上的數據解析協議與連接剝離後,純粹的連接管理是可以複用的,

對於連接池來說,它不需要了解具體連接的用途,只需要保持到特定endpoint的連接穩定即可。因此,即使是RPC服務的連接,也可以放入連接池中進行託管;

最終的連接池設計架構圖。

通過這些優化,API網關能夠更加高效地管理連接,同時減少了資源消耗,爲得物平臺提供了更加穩定和高效的網絡服務。這些改進不僅提高了性能,也爲未來的可擴展性和維護性打下了基礎。

AsyncClient設計

鑑於七層流量幾乎全部基於Http請求,RPC請求中Http協議也佔據了絕大多數,同時還會涉及到少量的dubbo, Redis 等協議通信的場景。

因此,有必要構建一個異步調用框架來提供支持。這個框架需要具備超時處理、回調執行、錯誤報告等功能,並且必須與協議無關,還需要支持鏈式調用以便於使用。

發起一次RPC調用通常可以分爲以下幾步:

  1. 獲取目標地址和使用的協議, 目標服務爲集羣部署時,需要使用loadbalance模塊

  2. 封裝發送的請求,這樣的請求在應用層可以具體化爲某個Request類,網絡層序列化爲二進制數據流

  3. 出於性能考慮選擇非阻塞式發送,發送動作完成後開始計算超時

  4. 接收數據響應,由於採用非阻塞模式,這裏的發送線程並不會以block的方式等待數據

  5. 在超時時間內完成數據處理,或者觸發超時導致連接取消或者關閉

AsyncClient 模塊內容並不複雜,AsyncClient爲抽象類不區分使用的網絡協議;

ConnectionPool 作爲連接的管理者被client所引用,獲取連接的key 使用 protocol+ip+port 再適合不過;

通常在某個具體的連接初始化階段就已經確定了該channel 所使用的協議,因此初始化時會直接綁定協議Handler;當協議爲HTTP請求時,HttpClientCodec 爲HTTP請求的編解碼handler;也可以是構建在TCP協議上的 Dubbo, Mysql ,Redis 等協議的handler。

首先對於一個請求的不同執行階段需要引入狀態定位,這裏引入了 STATE 枚舉:

enum STATE{
        INIT,SENDING,SEND,SEND_SUCCESS,FAILED,TIMEOUT,RECEIVED
}

其次在執行過程中設計了 AsyncContext作爲信息存儲的載體,內部包含request和response信息,作用類似於上文提到的ServerWebExchange;channel資源從連接池中獲取,使用完成後需要自動放回。

public class AsyncContext<Req, Resp> implements Cloneable{
    STATE state = STATE.INIT;
    final Channel usedChannel;
    final ChannelPool usedChannelPool;
    final EventExecutor executor;
    final AsyncClient<Req, Resp> agent;
    
    Req request;
    Resp response;
    
    ResponseCallback<Resp> responseCallback;
    ExceptionCallback exceptionCallback;
    
    int timeout;
    long deadline;
    long sendTimestamp;

    Promise<Resp> responsePromise;
}

AsyncContext

AsyncClient 封裝了基本的網絡通信能力,不拘泥於某個固定的協議,可以是Redis, http,Dubbo 等。

當將數據寫出去之後,該channel的非阻塞調用立即結束,在沒有收到響應之前無法對AsyncContext 封裝的數據做進一步處理,如何在收到數據時將接收到的響應和之前的請求管理起來這是需要面對的問題,channel 對象 的attr 方法可以用於臨時綁定一些信息,以便於上下文切換時傳遞數據,可以在發送數據時將AsyncContext對象綁定到該channel的某個固定key上。

當channel收到響應信息時,在相關的 AsyncClientHandler 裏面取出AsyncContext。

public abstract class AsyncClient<Req, Resp> implements Client {
    private static final int defaultTimeout = 5000;
    private final boolean doTryAgain = false;
    private final ChannelPoolManager channelPoolManager = ChannelPoolManager.getChannelPoolManager();
    protected static AttributeKey<AsyncRequest> ASYNC_REQUEST_KEY = AttributeKey.valueOf("ASYNC_REQUEST");

    public abstract ApplicationProtocol getProtocol();
    
    public AsyncContext<Req, Resp> newRequest(EventExecutor executor, String endpoint, Req request) {
        final ChannelPoolKey poolKey = genPoolKey(endpoint);
        ChannelPool usedChannelPool = channelPoolManager.acquireChannelPool(executor, poolKey);
        return new AsyncContext<>(this,executor,usedChannelPool,request, defaultTimeout, executor.newPromise());
    }

    public void submitSend(AsyncContext<Req, Resp> asyncContext){
        asyncContext.state = AsyncContext.STATE.SENDING;
        asyncContext.deadline = asyncContext.timeout + System.currentTimeMillis();   
        ReferenceCountUtil.retain(asyncContext.request);
        Future<Resp> responseFuture = trySend(asyncContext);
        responseFuture.addListener((GenericFutureListener<Future<Resp>>) future -> {
            if(future.isSuccess()){
                ReferenceCountUtil.release(asyncContext.request);
                Resp response = future.getNow();
                asyncContext.responseCallback.callback(response);
            }
        });
    }
    /**
     * 嘗試從連接池中獲取連接併發送請求,若失敗返回錯誤
     */
    private Promise<Resp> trySend(AsyncContext<Req, Resp> asyncContext){
        Future<Channel> acquireFuture = asyncContext.usedChannelPool.acquire();
        asyncContext.responsePromise = asyncContext.executor.newPromise();
        acquireFuture.addListener(new GenericFutureListener<Future<Channel>>() {
                @Override
                public void operationComplete(Future<Channel> channelFuture) throws Exception {
                    sendNow(asyncContext,channelFuture);
                }
        });
        return asyncContext.responsePromise;
    }

    private void sendNow(AsyncContext<Req, Resp> asyncContext, Future<Channel> acquireFuture){
        boolean released = false;
        try {
            if (acquireFuture.isSuccess()) {
                NioSocketChannel channel = (NioSocketChannel) acquireFuture.getNow();
                released = true;
                assert channel.attr(ASYNC_REQUEST_KEY).get() == null;
                asyncContext.usedChannel = channel;
                asyncContext.state = AsyncContext.STATE.SEND;
                asyncContext.sendTimestamp = System.currentTimeMillis();
                channel.attr(ASYNC_REQUEST_KEY).set(asyncContext);
                ChannelFuture writeFuture = channel.writeAndFlush(asyncContext.request);
                channel.eventLoop().schedule(()-> doTimeout(asyncContext), asyncContext.timeout, TimeUnit.MILLISECONDS);
            } else {
                asyncContext.responsePromise.setFailure(acquireFuture.cause());
            }
        } catch (Exception e){
            throw new Error("Unexpected Exception.............!");
        }finally {
            if(!released) {
                ReferenceCountUtil.safeRelease(asyncContext.request);
            }
        }
    }
}

AsyncClient核心源碼

public class AsyncClientHandler extends SimpleChannelInboundHandler {
    @Override
    protected void channelRead0(ChannelHandlerContext ctx, Object msg) throws Exception {
        AsyncContext asyncContext = ctx.attr(AsyncClient.ASYNC_REQUEST_KEY).get();
        try {
            asyncContext.state = AsyncContext.STATE.RECEIVED;
            asyncContext.releaseChannel();
            asyncContext.responsePromise.setSuccess(msg);
        }catch (Throwable t){
            log.error("Exception raised when set Success callback. Exception \n: {}", ExceptionUtils.getFullStackTrace(t));
            ByteBufHelper.safeRelease(msg);
            throw t;
        }
    }
}

AsyncClientHandler

通過上面幾個類的封裝得到了一個易用使用的 AsyncClient,下面的代碼爲調用權限系統的案例:

final FullHttpRequest httpRequest = HttpRequestUtil.getDefaultFullHttpRequest(newAuthReq, serviceInstance, "/auth/newCheckSls");
asyncClient.newRequest(exchange.getExecutor(), endPoint,httpRequest)
        .timeout(timeout)
        .onComplete(response -> {
            String checkResultJson = response.content().toString(CharsetUtil.UTF_8);
            response.release();
            NewAuthResult result = Jsons.parse(checkResultJson,NewAuthResult.class);
            TokenResult tokenResult = this.buildTokenResult(result);
            String body = exchange.getAttribute(DAGApplicationConfig.REQUEST_BODY);

            if (tokenResult.getUserInfoResp() != null) {
                UserInfoResp userInfo = tokenResult.getUserInfoResp();
                headers.set("userid", userInfo.getUserid() == null ? "" : String.valueOf(userInfo.getUserid()));
                headers.set("username", StringUtils.isEmpty(userInfo.getUsername()) ? "" : userInfo.getUsername());
                headers.set("name", StringUtils.isEmpty(userInfo.getName()) ? "" : userInfo.getName());
                chain.filter(exchange);
            } else {
                log.error("{},heads: {},response: {}", path, headers, tokenResult);
                int code = tokenResult.getCode() != null ? tokenResult.getCode().intValue() : ResultCode.UNAUTHO.code;
                ResponseDecorator.failResponse(exchange, code, tokenResult.getMsg());
            }
        })
        .onError(throwable -> {
            log.error("Request service {},occur an exception {}",endPoint, throwable);
            ResponseDecorator.failResponseWithStatus(exchange,HttpResponseStatus.INTERNAL_SERVER_ERROR,"AuthFilter 驗證失敗");
        })
        .sendRequest();

asyncClient的使用

通過AsyncContext的管理,異步客戶端能夠在接收到響應時有效地處理請求和響應之間的關係。這種設計不僅提高了網絡通信的效率,也增強了系統的可擴展性和靈活性,爲得物平臺的網絡通信提供了強大的支持。

請求超時管理

一個請求的處理時間不能無限期拉長, 超過某個閾值的情況下App的頁面會被取消 ,長時間的加載卡頓不如快速報錯帶來的體驗良好;

因此,網關必須對接口調用實施超時管理,特別是在向後端服務發起調用的過程中。通常,我們會設定一個默認的超時閾值,比如3秒。若超過此閾值,網關將向客戶端返回超時失敗的信息。考慮到網關下游服務多樣性,包括對響應時間敏感的C端業務、邏輯複雜的B端服務接口,以及涉及大量計算的監控接口,不同接口對超時時間的需求各不相同。因此,應爲每個接口單獨設置超時時間,而非採用單一的配置值。

asyncClient.newRequest(exchange.getExecutor(), endPoint,httpRequest)
        .timeout(timeout)
        .onComplete(response -> {
            String checkResultJson = response.content().toString(CharsetUtil.UTF_8);
            //..........
        })
        .onError(throwable -> {
            log.error("Request service {},occur an exception {}",endPoint, throwable);
            ResponseDecorator.failResponseWithStatus(exchange,HttpResponseStatus.INTERNAL_SERVER_ERROR,"AuthFilter 驗證失敗");
        })
        .sendRequest();

asyncClient 的鏈式調用設計了 timeout方法,用於傳遞超時時間,我們可以通過一個全局Map來配置這樣的信息。

Map<String,Integer> 其key爲全路徑的path 信息,V爲設定的超時時間,單位爲ms, 至於Map的信息在實際配置過程中如何承載,使用ARK配置或者Mysql 都很容易實現。

處於併發安全和性能的極致追求,超時事件的設定和調度最好能夠在與當前channel綁定的線程中執行,慶幸的是 EventLoop線程自帶schedule 方法。具體來看上文的 AsyncClient 的56行。

schedule 方法內部以堆結構的方式實現了對超時時間進行管理,整體性能尚可。

通過使用異步客戶端的鏈式調用和全局映射表,可以方便地管理接口超時時間。同時,爲了保證併發安全和性能,超時事件的處理應當在EventLoop線程中進行。這種設計不僅提高了系統的響應速度,也增強了用戶體驗,確保了得物平臺在處理網絡請求時的穩定性和效率。

堆外內存管理優化

常見的堆外內存手動管理方式,是引用計數,堆外內存在回收的時候條件只有一個,就是RC值爲0 ,釋放的時候對 RC (引用計數) 的值做調整,

然而,隨着業務複雜性的增加,在處理業務的某個環節後,已經不記得當前的引用計數值是多少了,甚至是前面的RC增加了,後面的RC忘記減少了;

我們引入一個safeRelase的思路 , 在數據回寫給客戶端後,把這個請求整個生命週期所申請的堆外內存全部釋放掉,也就是在最終的release的時候,如果當前的RC>0 就不停的 release ,直至爲0;

public static void safeRelease(Object msg){
    if(msg instanceof ReferenceCounted){
        ReferenceCounted ref = (ReferenceCounted) msg;
        int refCount = ref.refCnt();
        for(int i=0; i<refCount; i++){
            ref.release();
        }
    }
}

因此只要把這樣的邏輯放在netty的最後一個Handler中即可保證內存得到有效釋放。

傳統的引用計數法在業務複雜時可能會出現問題,因此我們提出了在數據回寫後一次性釋放整個生命週期堆外內存的策略。通過在Netty的最終處理器中實現這一邏輯,可以確保堆外內存被有效管理,避免了內存泄漏的風險,提高了系統的穩定性和可靠性。

集羣限流改造優化

首先來看DAG 啓動後sentinel相關線程,類似的問題,線程數量非常多,需要針對性優化。

Sentinel 線程數

sentinel線程分析優化:

最終優化後的線程數量爲4個

sentinel原生限流源碼分析如下,進一步分析SphU#entry方法發現其底調用 FlowRuleCheck#passClusterCheck;

在passClusterCheck方法中發現底層網絡IO調用爲阻塞式, 由於該方法的執行線程爲workerEventLoop,因此需要使用上文提到的AsyncClient 進行優化。

private void doSentinelFlowControl(ServerWebExchange exchange, GatewayFilterChain chain, String resource){
    Entry urlEntry = null;
    try {
        if (!StringUtil.isEmpty(resource)) {
            //1. 檢測是否限流
            urlEntry = SphU.entry(resource, ResourceTypeConstants.COMMON_WEB, EntryType.IN);
        }
       //2. 通過,走業務邏輯
        chain.filter(exchange);
    } catch (BlockException e) {
        //3. 攔截,直接返回503
        ResponseDecorator.failResponseWithStatus(exchange, HttpResponseStatus.SERVICE_UNAVAILABLE, ResultCode.SERVICE_UNAVAILABLE.message);
    } catch (RuntimeException e2) {
        Tracer.traceEntry(e2, urlEntry);
        log.error(ExceptionUtils.getFullStackTrace(e2));
        ResponseDecorator.failResponseWithStatus(exchange, HttpResponseStatus.INTERNAL_SERVER_ERROR,HttpResponseStatus.INTERNAL_SERVER_ERROR.reasonPhrase());
    } finally {
        if (urlEntry != null) {
            urlEntry.exit();
        }
        ContextUtil.exit();
    }
}

SentinelGatewayFilter(sentinel 適配SCG的邏輯)

public class RedisTokenService implements InitializingBean {
    private final RedisAsyncClient client = new RedisAsyncClient();
    private final RedisChannelPoolKey connectionKey;
    
    public RedisTokenService(String host, int port, String password, int database, boolean ssl){
        connectionKey = new RedisChannelPoolKey(String host, int port, String password, int database, boolean ssl);
    }
    //請求token
    public Future<TokenResult> asyncRequestToken(ClusterFlowRule rule){
        ....
        sendMessage(redisReqMsg,this.connectionKey)
    }
    
    private Future<TokenResult> sendMessage(RedisMessage requestMessage, EventExecutor executor, RedisChannelPoolKey poolKey){
        AsyncRequest<RedisMessage,RedisMessage> request = client.newRequest(executor, poolKey,requestMessage);
        DefaultPromise<TokenResult> tokenResultFuture = new DefaultPromise<>(request.getExecutor());

        request.timeout(timeout)
                .onComplete(response -> {
                    ...
                    tokenResultFuture.setSuccess(response);
                })
                .onError(throwable -> {
                    ...
                    tokenResultFuture.setFailure(throwable);
                }).sendRequest();

        return tokenResultFuture;
    }
}

RedisTokenService

最終的限流Filter代碼如下:

public class SentinelGatewayFilter implements RequestFilter {
    @Resource
    RedisTokenService tokenService;
    
    @Override
    public void filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        //當前爲 netty NioEventloop 線程
        ServerHttpRequest request = exchange.getRequest();
        String resource = request.getPath() != null ? request.getPath() : "";
  
        //判斷是否有集羣限流規則
        ClusterFlowRule rule = ClusterFlowManager.getClusterFlowRule(resource);
        if (rule != null) {
           //異步非阻塞請求token
            tokenService.asyncRequestToken(rule,exchange.getExecutor())
                    .addListener(future -> {
                        TokenResult tokenResult;
                        if (future.isSuccess()) {
                            tokenResult = (TokenResult) future.getNow();
                        } else {
                            tokenResult = RedisTokenService.FAIL;
                        }
                        if(tokenResult == RedisTokenService.FAIL || tokenResult == RedisTokenService.ERROR){
                            log.error("Request cluster token failed, will back to local flowRule check");
                        }
                        ClusterFlowManager.setTokenResult(rule.getRuleId(), tokenResult);
                        doSentinelFlowControl(exchange, chain, resource);
                    });
        } else {
            doSentinelFlowControl(exchange, chain, resource);
        }
    }
}

改造後適配DAG的SentinelGatewayFilter

六、壓測性能

DAG高壓表現

wrk -t32 -c1000 -d60s -s param-delay1ms.lua --latency http://a.b.c.d:xxxxx

DAG網關的QPS、實時RT、錯誤率、CPU、內存監控圖;

在CPU佔用80% 情況下,能夠支撐的QPS在4.5W。

DAG網關的QPS、RT 折線圖

DAG在CPU佔用80% 情況下,能夠支撐的QPS在4.5W,ART 19ms

SCG高壓表現

wrk -t32 -c1000 -d60s -s param-delay1ms.lua --latency http://a.b.c.d:xxxxx

SCG網關的QPS、實時RT、錯誤率、CPU、內存監控圖:

SCG網關的QPS、RT 折線圖:

SCG在CPU佔用95% 情況下,能夠支撐的QPS在1.1W,ART 54.1ms

DAG低壓表現

wrk -t5 -c20 -d120s -s param-delay1ms.lua --latency http://a.b.c.d:xxxxx

DAG網關的QPS、實時RT、錯誤率、CPU、內存:

DAG網關的QPS、RT 折線圖:

DAG在QPS 1.1W情況下,CPU佔用30%,ART 1.56ms

數據對比

DAG和SCG的對比結論

滿負載情況下,DAG要比SCG的吞吐量高很多,QPS幾乎是4倍,RT反而消耗更低,SCG在CPU被打滿後,RT表現出現嚴重性能劣化。

DAG的吞吐控制和SCG一樣情況下,CPU和RT損耗下降了更多。

DAG在最大壓力下,內存消耗比較高,達到了75%左右,不過到峯值後,就不再會有大幅變動了。對比壓測結果,結論令人欣喜,++SCG作爲Java生態當前使用最廣泛的網關,其性能屬於一線水準,DAG的性能達到其4倍以上也是遠超意料,這樣的結果給與研發同學極大的鼓舞

七、投產收益

安全性提升

完善的接口級路由管理

基於接口註冊模式的全新路由上線,包含了接口註冊的申請人,申請時間,接口場景備註信息等,接口管理更加嚴謹規範;

結合路由組功能可以方便的查詢當前服務的所有對外接口信息,某種程度上具備一定的API查詢管理能力;同時爲了緩解用戶需要檢索的接口太多的尷尬,引入了一鍵收藏功能,大部分時候用戶只需要切換到已關注列表即可。

註冊接口列表

接口收藏

防滲透能力極大增強

早期的泛化路由,給黑產的滲透帶來了極大的想象空間和安全隱患,甚至可以在外網直接訪問某些業務的配置信息。

黑產接口滲透

接口註冊模式啓用後,所有未註冊的接口均無法訪問,防滲透能力提升一個臺階,同時自動推送異常接口訪問信息。

404接口訪問異常推送

穩定性增強

內存泄漏問題解決

通過一系列手段改進優化和嚴格的測試,新網關的內存使用更加穩健,內存增長曲線直接拉平,徹底解決了泄漏問題。

老網關內存增長趨勢

新網關內存增長趨勢

降本增效

資源佔用下降50% +

SCG平均CPU佔用

DAG資源佔用

得益於ZGC的優秀算法,JVM17 在GC暫停時間上取得了出色的成果,網關作爲延遲敏感型應用對GC的暫停時間尤爲看重,爲此我們組織升級了JDK17 版本;下面爲同等流量壓力情況下的配置不同GC的效果對比,++可以看到GC的暫停時間從平均70ms 降低到1ms 內,RT99線得到大幅度提升;吞吐量不再受流量波動而大幅度變化,性能表現更加穩定;同時網關的平均響應時間損耗降低5%。

JDK8-G1 暫停時間表現

JDK17-ZGC暫停時間表現

吞吐量方面,G1伴隨流量的變化呈現出一定的波動趨勢,均線在99.3%左右。ZGC的吞吐量則比較穩定,維持在無限接近100%的水平。

JDK8-G1 吞吐量

JDK17-ZGC吞吐量

對於實際業務接口的影響,從下圖中可以看到平均響應時間有所下降,這裏的RT差值表示接口經過網關層的損耗時間;不同接口的RT差值損耗是不同的,這可能和請求響應體的大小,是否經過登錄驗證,風控驗證等業務邏輯有關。

JDK17與JDK8 ART對比

需要指出的是ZGC對於一般的RT敏感型應用有很大提升, 服務的RT 99線得到顯著改善。但是如果當前應用大量使用了堆外內存的方式,則提升相對較弱,如大量使用netty框架的應用, 因爲這些應用的大部分數據都是通過手動釋放的方式進行管理。

八、思考總結

架構演進

API網關的自研並非一蹴而就,而是經歷了多次業務迭代循序漸進的過程;從早期的泛化路由引發的安全問題處理,到後面的大量路由註冊,帶來的匹配性能下降 ,以及最終壓垮老網關最後一根稻草的內存泄漏問題;在不同階段需要使用不同的應對策略,早期業務快速迭代,大量的需求堆積,最快的時候一個功能點的改動需要三四天內上線 ,我們很難有足夠的精力去做一些深層次的改造,這個時候需求導向爲優先,功能性建設完善優先,是一個快速奔跑的建設期;伴隨體量的增長安全和穩定性的重視程度逐步拔高,繼而推進了這些方面的大量建設;從拓展SCG的原有功能到改進框架源碼,以及最終的自研重寫,可以說新的API網關是一個業務推進而演化出來的產物,也只有這樣 ”生長“ 出來的架構產品才能更好的契合業務發展的需要。

穩定性把控

自研基礎組件是一項浩大的工程,可以預見代碼量會極爲龐大,如何有效管理新項目的代碼質量是個棘手的問題; 原有業務邏輯的改造也需要回歸測試;

現實的情況是中間件團隊沒有專職的測試,質量保證完全依賴開發人員;這就對開發人員的代碼質量提出了極高的要求,一方面我們通過與老網關適配相同的代理引擎接口,降低遷移成本和業務邏輯出現bug的概率;另一方面還對編碼質量提出了高標準,平均每週兩到三次的CodeReview;80%的單元測試行覆蓋率要求。

網關作爲流量入口,承接全司最高流量,對穩定性的要求極爲苛刻。最理想的狀態是在業務服務沒有任何感知的情況下,我們將新網關逐步替換上去;爲此我們對新網關上線的過程做了充分的準備,嚴格控制上線過程;具體來看整個上線流程分爲以下幾個階段:

第一階段

我們在壓測環境長時間高負載壓測,持續運行時間24小時以上,以檢測內存泄漏等穩定性問題。同時利用性能檢測工具抓取熱點火焰圖,做針對性優化。

第二階段

發佈測試環境試跑,採用並行試跑的方式,新老網關同時對外提供服務(流量比例1 :1,初期新網關承接流量可能只有十分之一),一旦用戶反饋的問題可能跟新網關有關,或者發現異常case,立即關停新網關的流量。待查明原因並確認修復後,重新引流。

第三階段

上線預發,小得物環境試跑,由於這些環境流量不大,依然可以並行長時間試跑,發現問題解決問題。

第四階段

生產引流,單節點從萬分之一比例開始灰度,逐步引流放大,每個階段停留24小時以上,觀察修正後再放大,循環此過程;基於單節點承擔正常比例流量後,再次抓取火焰圖,基於真實流量場景下的性能熱點做針對性優化。

自研過程是一個逐步完善的過程,從解決安全問題到提高性能,再到解決內存泄漏,每個階段都有其特定的問題和要求。在穩定性控制方面,我們依靠開發人員的代碼質量和對新網關上線流程的嚴格控制,確保了網關的高可用性和穩定性。通過逐步替換和灰度測試,我們確保了新網關能夠在不影響現有業務的情況下平穩上線。這個過程中,我們學到了很多,也取得了顯著的成果,爲得物平臺的發展奠定了堅實的基礎。

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

架構之路,充滿了坎坷

架構和高級開發不一樣 , 架構問題是open/開放式的,架構問題是沒有標準答案的

正由於這樣,很多小夥伴,儘管耗費很多精力,耗費很多金錢,但是,遺憾的是,一生都沒有完成架構升級

所以,在架構升級/轉型過程中,確實找不到有效的方案,可以來找40歲老架構尼恩求助.

前段時間一個小夥伴,他是跨專業來做Java,現在面臨轉架構的難題,但是經過尼恩幾輪指導,順利拿到了Java架構師+大數據架構師offer 。所以,如果遇到職業不順,找老架構師幫忙一下,就順利多了。

技術自由的實現路徑:

實現你的 架構自由:

喫透8圖1模板,人人可以做架構

10Wqps評論中臺,如何架構?B站是這麼做的!!!

阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了

峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?

100億級訂單怎麼調度,來一個大廠的極品方案

2個大廠 100億級 超大流量 紅包 架構方案

… 更多架構文章,正在添加中

實現你的 響應式 自由:

響應式聖經:10W字,實現Spring響應式編程自由

這是老版本 《Flux、Mono、Reactor 實戰(史上最全)

實現你的 spring cloud 自由:

Spring cloud Alibaba 學習聖經》 PDF

分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)

一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)

實現你的 linux 自由:

Linux命令大全:2W多字,一次實現Linux自由

實現你的 網絡 自由:

TCP協議詳解 (史上最全)

網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!

實現你的 分佈式鎖 自由:

Redis分佈式鎖(圖解 - 秒懂 - 史上最全)

Zookeeper 分佈式鎖 - 圖解 - 秒懂

實現你的 王者組件 自由:

隊列之王: Disruptor 原理、架構、源碼 一文穿透

緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)

緩存之王:Caffeine 的使用(史上最全)

Java Agent 探針、字節碼增強 ByteBuddy(史上最全)

實現你的 面試題 自由:

4800頁《尼恩Java面試寶典 》 40個專題

免費獲取11個技術聖經PDF:

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章