Jack Jiang

          我的最新工程MobileIMSDK:http://git.oschina.net/jackjiang/MobileIMSDK
          posts - 507, comments - 13, trackbacks - 0, articles - 1

          本文由攜程技術Butters分享,原題“干貨 | 日均流量200億,攜程高性能全異步網關實踐”,下文有修訂和重新排版。

          1、引言

          本文分享的是攜程API網關全異步改造的實踐分享,包括從Zuul 1.0同步架構升級為基于Netty的全異步架構,通過RxJava實現業務流程異步化,結合流式轉發、ZGC等技術顯著提升性能,并構建控制面實現多協議統一治理與模塊化編排。

          2、作者介紹

          Butters:攜程軟件技術專家,專注于網絡架構、API網關、負載均衡、Service Mesh等領域。

          3、專題目錄

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

          長連接網關技術專題(一):京東京麥的生產級TCP網關技術實踐總結

          長連接網關技術專題(二):知乎千萬級并發的高性能長連接網關技術實踐

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

          長連接網關技術專題(四):愛奇藝WebSocket實時推送網關技術實踐

          長連接網關技術專題(五):喜馬拉雅自研億級API網關技術實踐

          長連接網關技術專題(六):石墨文檔單機50萬WebSocket長連接架構實踐

          長連接網關技術專題(七):小米小愛單機120萬長連接接入層的架構演進

          長連接網關技術專題(八):B站基于微服務的API網關從0到1的演進之路

          長連接網關技術專題(九):去哪兒網酒店高性能業務網關技術實踐

          長連接網關技術專題(十):百度基于Go的千萬級統一長連接服務架構實踐

          長連接網關技術專題(十一):揭秘騰訊公網TGW網關系統的技術架構演進

          長連接網關技術專題(十二):大模型時代多模型AI網關的架構設計與實現

          長連接網關技術專題(十三):基于Netty的攜程高性能網關異步改造實踐》(* 本文

          4、技術背景

          與許多公司一樣,攜程API網關也是同微服務架構一起引入的基礎設施,最早版本發布于2014年。隨著服務化在公司的快速推進,網關逐漸成為應用暴露到外網的標準方案。后來的“ALL IN無線”、國際化、異地多活等,網關跟隨著公司公共業務與基礎架構共同演進。

          技術方案上,公司微服務早期發展受NetflixOSS影響較深,網關方面最早也是參考了Zuul 1.0進行的二次開發。

          核心可概括為四點:

          • 1)server端:Tomcat NIO + AsyncServlet;
          • 2)業務流程:獨立線程池,分階段的責任鏈模式;
          • 3)client端:Apache HttpClient,同步調用;
          • 4)核心組件:Archaius(動態配置客戶端),Hystrix(熔斷限流),Groovy(熱更新支持)。
          1

          眾所周知,同步調用阻塞線程,系統吞吐受IO影響大。作為行業先驅,Zuul在設計上也考慮到了這點——通過引入Hystrix,資源隔離配合限流,將故障(慢IO)框在一定范圍內;配合熔斷策略,可提前釋放部分線程資源;最終達到局部異常不影響全局的目的。

          但隨著公司業務的發展,上述策略效果逐漸減弱。

          主要原因在于兩方面的變動:

          • 1)業務出海:網關作為海外接入層,部分流量需轉回國內,慢IO成為常態;
          • 2)服務規模增長:局部異常常態化,加上微服務異常擴散的特性,線程池可能長期處于亞健康狀態。
          2

          全異步改造是攜程API網關近年的一項核心工作點,本文也將由此展開,聊一聊我們在網關方面的工作與實踐。重點包括:性能優化、業務形態、技術架構、治理經驗等。

          5、高性能網關核心設計1:異步流程設計

          全異步 = server端異步 + 業務流程異步 + client端異步

          對于server與client端,我們選擇了Netty框架,NIO/Epoll + Eventloop本身就是事件驅動的設計。

          改造核心在于業務流程的異步化,常見異步場景包括:

          • 1)業務IO事件:如請求校驗、身份認證,涉及遠程調用;
          • 2)自身IO事件:如讀取到了報文的前xx字節;
          • 3)請求轉發:包括TCP連接,HTTP請求。

          經驗上,異步編程相比同步在設計、讀寫上都會困難一些。

          所謂的困難,一般包括:

          • 1)流程設計&狀態轉換;
          • 2)異常處理,包括常規異常與超時;
          • 3)上下文傳遞,包括業務上下文與trace log;
          • 4)線程調度;
          • 5)流量控制。

          尤其在Netty上下文內,對ByteBuf生命周期設計的不完善,很容易造成內存泄漏。圍繞這些問題,我們設計了對應外圍框架,最大努力對業務代碼抹平同步/異步差異,方便開發;同時默認兜底與容錯,保證程序整體安全。工具上借助了RxJava,主要流程如下圖所示。

          3

          Maybe:

          • 1)RxJava內置容器類,標識正常結束、有且僅有一個對象返回、異常三種狀態;
          • 2)響應式,方便整體狀態機設計,自帶異常處理、超時、線程調度等封裝;
          • 3)Maybe.empty()/Maybe.just(T),適用同步場景;
          • 4)工具類RxJavaPlugins,方便切面邏輯封裝。

          Filter:

          • 1)代表一塊獨立的業務邏輯,同步&異步業務統一接口,返回Maybe;
          • 2)異步場景(如遠程調用)統一封裝,如涉及線程切換,通過maybe.obesrveOn(eventloop)切回;
          • 3)異步filter默認增加超時,并按弱依賴處理,忽略錯誤。

          public interface Processor<T> {   

              ProcessorType getType();

           

              int getOrder();

           

              boolean shouldProcess(RequestContext context);

           

              //對外統一封裝為Maybe   

              Maybe process(RequestContext context) throws Exception;

          }

          public abstract class AbstractProcessor implements Processor {

              //同步&無響應,繼承此方法

              //場景:常規業務處理

              protected void processSync(RequestContext context) throws Exception {}

           

              //同步&有響應,繼承此方法,健康檢測

              //場景:健康檢測、未通過校驗時的靜態響應

              protected T processSyncAndGetReponse(RequestContext context) throws Exception {

                  process(context);

                  return null;

              };

           

              //異步,繼承此方法

              //場景:認證、鑒權等涉及遠程調用的模塊

              protected Maybe processAsync(RequestContext context) throws Exception

          {

                  T response = processSyncAndGetReponse(context);

                  if (response == null) {

                      return Maybe.empty();

                  } else {

                      return Maybe.just(response);

                  }

              };

           

              @Override

              public Maybe process(RequestContext context) throws Exception {

                  Maybe<T> maybe = processAsync(context);

                  if (maybe instanceof ScalarCallable) {

                      //標識同步方法,無需額外封裝

                      return maybe;

                  } else {

                      //統一加超時,默認忽略錯誤

                      return maybe.timeout(getAsyncTimeout(context), TimeUnit.MILLISECONDS,

                              Schedulers.from(context.getEventloop()), timeoutFallback(context));

                  }

              }

           

              protected long getAsyncTimeout(RequestContext context) {

                  return 2000;

              }

           

              protected Maybe<T> timeoutFallback(RequestContext context) {

                  return Maybe.empty();

              }

          整體流程:

          • 1)沿用責任鏈的設計,分為inbound、outbound、error、log四階段;
          • 2)各階段由一或多個filter組成;
          • 3)filter順序執行,遇到異常則中斷,inbound期間任意filter返回response也觸發中斷。

          public class RxUtil{

                //組合某階段(如Inbound)內的多個filter(即Callable<Maybe<T>>)

                public static  Maybe concat(Iterable

                    Iterator

                    while (sources.hasNext()) {

                        Maybe<T> maybe;

                        try {

                            maybe = sources.next().call();

                        } catch (Exception e) {

                            return Maybe.error(e);

                        }

                        if (maybe != null) {

                            if (maybe instanceof ScalarCallable) {

                                //同步方法

                                T response = ((ScalarCallable<T>)maybe).call();

                                if (response != null) {

                                    //有response,中斷

                                    return maybe;

                                }

                            } else {

                                //異步方法

                                if (sources.hasNext()) {

                                    //將sources傳入回調,后續filter重復此邏輯

                                    return new ConcattedMaybe(maybe, sources);

                                } else {

                                    return maybe;

                                }

                            }

                        }

                    }

                    return Maybe.empty();

                }

            }

          public class ProcessEngine{

                //各個階段,增加默認超時與錯誤處理

              private void process(RequestContext context) {

                    List<Callable<Maybe<Response>>> inboundTask = get(ProcessorType.INBOUND, context);

                    List<Callable<Maybe<Void>>> outboundTask = get(ProcessorType.OUTBOUND, context);

                    List<Callable<Maybe<Response>>> errorTask = get(ProcessorType.ERROR, context);

                    List<Callable<Maybe<Void>>> logTask = get(ProcessorType.LOG, context);

           

                   RxUtil.concat(inboundTask) //inbound階段                   

                        .toSingle() //獲取response                         

                        .flatMapMaybe(response -> {

                            context.setOriginResponse(response);

                            return RxUtil.concat(outboundTask);

                        }) //進入outbound

                        .onErrorResumeNext(e -> {

                            context.setThrowable(e);

                            return RxUtil.concat(errorTask).flatMap(response -> {

                                context.resetResponse(response);

                                return RxUtil.concat(outboundTask);

                            });

                        }) //異常則進入error,并重新進入outbound

                        .flatMap(response -> RxUtil.concat(logTask)) //日志階段

                        .timeout(asyncTimeout.get(), TimeUnit.MILLISECONDS, Schedulers.from(context.getEventloop()),

                                Maybe.error(new ServerException(500, "Async-Timeout-Processing"))

                        ) //全局兜底超時

                        .subscribe( //釋放資源

                                unused -> {

                                    logger.error("this should not happen, " + context);

                                    context.release();

                                },

                                e -> {

                                    logger.error("this should not happen, " + context, e);

                                    context.release();

                                },

                                () -> context.release()

                        );

                }  

            }

          6、高性能網關核心設計2:流式轉發&單線程

          以HTTP為例,報文可劃分為initial line/header/body三個組成部分:

          4

          在攜程,網關層業務不涉及body。因為無需全量存,所以解析完header后可直接進入業務流程。

          于此同時,如果接收到body部分:

          • 1)若已向upstream轉發請求,則直接轉發;
          • 2)否則需要將其暫存,待業務流程處理完畢,同initial line/header一并發送;
          • 3)對upstream端響應的處理方式亦然。

          對比完整解析HTTP報文的方式,這樣處理:

          • 1)更早進入業務流程,意味著upstream更早接收到請求,能有效降低網關這層引入的延遲;
          • 2)body生命周期被壓縮,可降低網關自身的內存開銷。

          雖說提升了性能,但流式的方式也極大提升了整個流程的復雜度

          5

          非流式場景下,Netty Server端編解碼、入向業務邏輯、Netty Cerver端編解碼、出向業務邏輯,各子流程相互獨立,各自處理完整的HTTP對象。

          采取流式后,請求則可能同時處于多流程內,引入的困難可歸納為以下三點:

          • 1)線程安全問題:不同流程若采用不同線程,會涉及上下文的并發修改;
          • 2)多階段聯動:比如Netty Server請求接收一半遇到了連接中斷,此時已經連上了upstream,那么upstream側的協議棧是走不完的,也必須隨之關閉連接;
          • 3)邊緣場景處理:比如upstream在請求未完整發送情況下返回了404/413,是選擇繼續發送、走完協議棧、讓連接能夠復用,還是選擇提前終止流程,節約資源,但同時放棄連接?再比如,upstream已收到請求但未響應,此時Netty Server突然斷開,Netty Client是否也要隨之斷開?等等。

          針對這些場景,我們采用了單線程的方式,核心設計:

          • 1)上線文綁定Eventloop,Netty Server/業務流程/Netty Client在同個eventloop執行;
          • 2)異步filter如因IO庫的關系,必須使用獨立線程池,那在后置處理上必須切回;
          • 3)流程內資源做必要的線程隔離(如連接池)。

          采用單線程的好處:

          • 1)杜絕了并發問題,在多階段聯動、邊緣場景問題處理時,整個系統也處于確定的狀態下,有效降低了開發難度與風險;
          • 2)減少線程切換,一定程度上也能夠提升性能。

          與之相對的,因為worker線程數較少(一般等于CPU核數),eventloop內必須完全杜絕IO操作,否則將對系統吞吐造成毀滅性打擊。

          7、高性能網關核心設計3:其他優化

          內部變量懶加載:針對請求的cookie/query等字段,如無必要,不提前進行字符串解析

          堆外內存&零拷貝:結合前文流式轉發的設計,進一步降低系統內存開銷

          ZGC:項目因TLSv1.3而引入了JDK11(JDK8支持相對較晚,8u261版本,2020.7.14),自然也對新一代的GC算法進行了嘗試,實際表現也確實不負盛名。除CPU占用有少量提升,整體GC耗時下降非常明顯。

          6

          7

          定制的HTTP編解碼:HTTP的悠久歷史,加之協議自身的開放性,催生了許多“壞實踐”,輕則影響成功率,重則威脅網站安全,舉兩個例子:

          流量治理:諸如請求體過大(413)、uri過長(414)、非ASCII字符(400)等問題,一般WebServer會選擇直接拒絕并返回對應狀態碼。由于直接跳過了業務流程,這類問題在統計、服務定為、排障上都會比較麻煩。擴展編解碼,讓問題請求也能夠走完路由流程,可以幫助解決非標流量的治理問題。

          請求過濾:如request smuggling(Netty 4.1.61.Final修復,2021.3.30發布)。擴展編解碼,增加自定義的校驗邏輯,讓安全補丁能夠更快落地。

          8、網關業務形態

          作為獨立、統一的入向流量收口點,網關對公司的價值主要體現在三方面:

          • 1)解耦不同網絡環境:典型場景包括內網&外網、生產環境&辦公區、IDC內部不同安全域、專線等;
          • 2)天然的公共業務切面:包括安全&認證&反爬、路由&灰度、限流&熔斷&降級、監控&告警&排障等;
          • 3)高效、靈活的流量控制。
          8

          9

          這里展開講幾個細分場景。

          私有協議:

          • 1)在收口的客戶端(APP),由框架層攔截用戶發起的HTTP請求,通過私有協議(SOTP)的方式發往服務端;
          • 2)選址方面:①通過服務端下發IP,杜絕DNS劫持;②連接預熱;③自定義的選址策略,可依據網絡質量、環境等自行切換;
          • 3)交互方式上:①更加輕量的協議體;②統一加密&壓縮&多路復用;③協議在入口處由網關統一轉換,對業務透明。

          鏈路優化:核心是引入接入層,讓遠距離用戶就近訪問,緩解握手開銷過大的問題。同時,因為接入層與IDC是可控的兩端,網絡鏈路選擇、協議交互模式上都有更大的優化空間。

          異地多活:區別于按比例分配、就近訪問策略等,異地多活模式下,網關(接入層)需按照業務維度的shardingKey進行分流(如userId),防止底層數據沖突。

          10

          9、網關治理概述

          下圖總結了線上網關的工作狀態:

          11

          橫向對應我們的業務流程:不同渠道(APP、H5、小程序、供應商)、不同協議(HTTP、SOTP)的流量經由負載均衡打到網關,經過系列業務邏輯的處理,最終轉發至后端服務。經歷了第二章的改造后,橫向業務在性能、穩定性上都得到了較好的提升。

          另一方面:由于多渠道/協議的存在,線上網關按業務劃分,進行了獨立集群的部署。業務差異(路由數據、功能模塊)早期通過獨立代碼分支管理,隨著分支數的增加,整體的運維復雜度越來越高。系統設計中,復雜度往往也意味著風險。如何對多協議、多角色的網關實施統一治理,如何以較低的成本,快速為新業務搭建定制化網關,成為了我們后一階段的工作重心。

          解決方案也比較直觀地在圖中畫了出來:

          • 1)是協議上兼容處理,讓線上代碼跑在一套框架下;
          • 2)是引入控制面,對線上網關的差異特性進行統一管理。
          12

          10、網關治理能力1:多協議兼

          協議兼容的做法并不新鮮,整體可以參考Tomcat對HTTP/1.0、HTTP/1.1、HTTP/2.0的抽象。HTTP自身雖然在各個版本內新增了大量feature,但我們在做業務開發時通常感知不到這些,核心在于HttpServletRequest接口的抽象。

          在攜程,網關面對的都是請求—響應模式的無狀態協議,報文組成上也可以劃分為元數據、擴展頭、業務報文三部分,因此可以比較方便地進行類似的嘗試。

          對應工作可以用以下兩點概括:

          • 1)協議適配層:用于屏蔽不同協議的編解碼、交互模式、對TCP連接的處理等;
          • 2)定義通用中間模型與接口:業務面向中間模型與接口編程,更好地聚焦到協議對應的業務屬性上去。
          13

          11、網關治理能力2:路由模塊

          路由模塊是控制面的兩個主要組成部分之一。

          除了管理網關—服務間的映射關系,服務本身可以用以下模型概括:

          {

                //匹配方式

                "type": "uri",

           

                //HTTP默認采用uri前綴匹配,內部通過樹結構尋址;私有協議(SOTP)通過服務唯一標識定位。

                "value": "/hotel/order",

                "matcherType": "prefix",

           

                //標簽與屬性

                //用于portal端權限管理、切面邏輯運行(如按核心/非核心)等

                "tags": [

                    "owner_admin",

                    "org_framework",

                    "appId_123456"

                ],

                "properties": {

                    "core": "true"

                },

           

              //endpoint信息

                "routes": [{

                    //condition用于二級路由,如按app版本劃分、按query重分配等

                    "condition": "true",

                    "conditionParam": {},

                    "zone": "PRO",

           

                    //具體服務地址,權重用于灰度場景

                    "targets": [{

                        "url": "http://test.ctrip.com/hotel",

                        "weight": 100

                    }

                  ]

                }]

            }

          12、網關治理能力3:模塊編排

          模塊編排是控制面的另一項核心部分。

          14

          我們在網關處理流程內預留了多個階段(上圖中用粉色標記)。除開熔斷、限流、日志等通用功能,運行時不同網關所需執行的業務功能由控制面統一下發。功能本身在網關內部有獨立的代碼模塊,控制面額外定義了功能對應的執行條件、參數、灰度比例、錯誤處理方式等。這種編排方式也在側面保證了模塊間的解耦。

          {

                //模塊名稱,對應網關內部某個具體模塊

                "name": "addResponseHeader",

           

                //執行階段

                "stage": "PRE_RESPONSE",

           

                //執行順序

                "ruleOrder": 0,

           

                //灰度比例

                "grayRatio": 100,

           

                //執行條件

                "condition": "true",

                "conditionParam": {},

           

                //執行參數

                //大量${}形式的內置模板,用于獲取運行時數據

                "actionParam": {

                  "connection": "keep-alive",

                  "x-service-call": "${request.func.remoteCost}",

                  "Access-Control-Expose-Headers": "x-service-call",

                  "x-gate-root-id": "${func.catRootMessageId}"

                },

           

                //異常處理方式,可以拋出或忽略

                "exceptionHandle": "return"

             }

          13、本文小結

          網關長期以來都是各類技術交流平臺上的熱點,方案也非常豐富:發展早、易上手的Zuul1.0、高性能的Nginx、集成度高的SpringCloud Gateway、如日中天的Istio等等。最終決定選型的還是各公司自身的業務背景與技術生態。也正因此,在攜程我們選擇了自研的道路。

          技術不斷發展,我們也在持續探索,公共網關同業務網關的關系、新協議的落地(HTTP3)、與ServiceMesh的關系等等,真誠歡迎有興趣的同學一起參與討論。

          14、參考資料

          [1] 京東京麥的生產級TCP網關技術實踐總結

          [2] 手淘億級移動端接入層網關的技術演進之路

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

          [4] 小米小愛單機120萬長連接接入層的架構演進

          [5] B站基于微服務的API網關從0到1的演進之路

          [6] 去哪兒網酒店高性能業務網關技術實踐

          [7] 少啰嗦!一分鐘帶你讀懂Java的NIO和經典IO的區別

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

          [9] Java的BIO和NIO很難懂?用代碼實踐給你看,再不懂我轉行!

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

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

          (本文已同步發布于:http://www.52im.net/thread-4854-1-1.html)



          作者:Jack Jiang (點擊作者姓名進入Github)
          出處:http://www.52im.net/space-uid-1.html
          交流:歡迎加入即時通訊開發交流群 215891622
          討論:http://www.52im.net/
          Jack Jiang同時是【原創Java Swing外觀工程BeautyEye】【輕量級移動端即時通訊框架MobileIMSDK】的作者,可前往下載交流。
          本博文 歡迎轉載,轉載請注明出處(也可前往 我的52im.net 找到我)。


          只有注冊用戶登錄后才能發表評論。


          網站導航:
           
          Jack Jiang的 Mail: jb2011@163.com, 聯系QQ: 413980957, 微信: hellojackjiang
          主站蜘蛛池模板: 信宜市| 色达县| 通化市| 浦北县| 来安县| 揭阳市| 屯昌县| 高平市| 呼伦贝尔市| 巴楚县| 咸阳市| 镇坪县| 武安市| 房产| 伽师县| 静宁县| 平舆县| 建宁县| 承德县| 黎平县| 沂源县| 分宜县| 肃南| 武汉市| 黄浦区| 南充市| 芜湖市| 贵港市| 吴江市| 宁远县| 陇西县| 溧阳市| 商河县| 芦山县| 中宁县| 长宁区| 临江市| 张北县| 大同市| 手机| 通江县|