一個API網(wǎng)關(guān)性能優(yōu)化實踐之路

前言

最近換了一份工作,而新工作是調(diào)研下目前業(yè)界Api網(wǎng)關(guān)的一些性能情況,而在最近過去一年的時間里,我也主要開發(fā)了一個Api網(wǎng)關(guān)來支持協(xié)議適配的需求,但是由于前東家的話整個流量都不大,而相應(yīng)的性能優(yōu)化也沒有很好的去做,借著讓原來在唯品會的同事把網(wǎng)關(guān)推到了OYO在做性能壓測及我剛?cè)肼毿聠挝唤邮值牡谝豁椚蝿?wù),把網(wǎng)關(guān)的性能進(jìn)行了一下優(yōu)化,也踩了一些坑,把這些作為總結(jié)寫下來;本文是https://juejin.im/post/5d19dd5c6fb9a07ec27bbb6e?from=timeline&isappinstalled=0
的補充,主要是介紹整個優(yōu)化步驟;

網(wǎng)關(guān)簡介

Tesla的整個網(wǎng)絡(luò)框架是基于littleproxy[https://github.com/adamfisk/LittleProxy],littleproxy是著名的軟件的后端代理,按照常規(guī)性能應(yīng)該不錯,在此基礎(chǔ)上我們加了些功能,具體代碼在:[https://github.com/spring-avengers/tesla]

  • 刪除UDP代理的功能及SSL的功能;

  • 增加了10多個Filter,而這些Filter由一個最大的Filter包裹來執(zhí)行;

  1. HttpFiltersAdapter的clientToProxyRequest方法,負(fù)責(zé)調(diào)用方到代理方的攔截處理
public HttpResponse clientToProxyRequest(HttpObject httpObject) {
     logStart();
     HttpResponse httpResponse = null;
     try {
         httpResponse = HttpRequestFilterChain.doFilter(serveletRequest, httpObject, ctx);
         if (httpResponse != null) {
             return httpResponse;
         }
     } catch (Throwable e) {
         httpResponse = ProxyUtils.createFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.BAD_GATEWAY);
         HttpUtil.setKeepAlive(httpResponse, false);
         logger.error("Client connectTo proxy request failed", e);
         return httpResponse;
     }
     ...
 }
  1. HttpFiltersAdapter的proxyToClientResponse方法,負(fù)責(zé)從后端服務(wù)拿到請求返回給調(diào)用方的攔截處理
public HttpObject proxyToClientResponse(HttpObject httpObject) {
      if (httpObject instanceof HttpResponse) {
          HttpResponse serverResponse = (HttpResponse)httpObject;
          HttpResponse response = HttpResponseFilterChain.doFilter(serveletRequest, serverResponse, ctx);
          logEnd(serverResponse);
          return response;
      } else {
          return httpObject;
      }
  }
  • 每個Filter的配置數(shù)據(jù)都是定時從數(shù)據(jù)庫里面拉?。ㄓ胔azelcast做了緩存);

  • 為了讓Filter做到熱插拔,大量的使用了反射去構(gòu)造Filter的實例;

優(yōu)化步驟

  • 由于大量的使用了反射去構(gòu)造實例,但是沒有去緩存這些實例,原本的想法是所有的Filter都是有狀態(tài)的,伴隨每一個HTTP請求的消亡而消亡,但是最終運行下來發(fā)現(xiàn)如果并發(fā)量上來整個GC完全接受不了,大量的對象的產(chǎn)生引起了yong gc的頻率幾乎成倍的增加,進(jìn)而導(dǎo)致qps上不去,而解決這個問題的辦法就是緩存實例,讓每一個Filter無狀態(tài),整個JVM內(nèi)存中僅存在一份實例;
    但是這些做下來,發(fā)現(xiàn)網(wǎng)關(guān)的QPS也沒上去,盡管GC情況解決了,QPS有所上升,但是遠(yuǎn)遠(yuǎn)沒達(dá)到Netty所想要的QPS,那只能繼續(xù)往前走;

  • 線程池的優(yōu)化

    我們都知道Netty的線程池分為boss線程池和work線程池,其中boss線程池負(fù)責(zé)接收網(wǎng)絡(luò)請求,而work線程池負(fù)責(zé)處理io任務(wù)及其他自定義任務(wù),對于網(wǎng)關(guān)這個應(yīng)用來說,boss線程池是必須要的,因為要負(fù)責(zé)請求的接入,但是網(wǎng)關(guān)比較特殊,對于真正的調(diào)用方來說,它是一個服務(wù)端,對于后端服務(wù)來說它是一個客戶端,所以他的線程模型應(yīng)該是如下:


    image.png

    這種線程池模型是典型的netty的線程池模型:
    Acceptor負(fù)責(zé)接收請求,ClientToProxyWorker是負(fù)責(zé)代理服務(wù)器的處理IO請求,而ProxyServerWorker負(fù)責(zé)轉(zhuǎn)發(fā)請求到后端服務(wù),LittleProxy就是使用這種很經(jīng)典的線程模型,其QPS在4核32G的機(jī)器下QPS大概能達(dá)到9000多,但是這種線程模型存在ClientToProxyWorker和ProxyServerWorker線程切換的問題,我們都知道線程切換是要耗費CPU資源的,那我們是不是可以做一個改變呢?換成以下這種:


    image.png

    這種線程池模型是將ClientToProxyWorker和ProxyServerWorker復(fù)用同一個線程池,這種做法在省卻了一個線程切換的時間,也就是對于代理服務(wù)器來說,netty的服務(wù)端及netty的客戶端在線程池傳入時復(fù)用同一個線程池對象;
    做到這一步的話整個代理服務(wù)的性能應(yīng)該能提升不少,但是有沒有更好的線程模型呢?答案是肯定的;
    image.png

這個線程模型的話,整個處理請求及轉(zhuǎn)發(fā)請求都復(fù)用同一個線程,而這種做法的話線程的切換基本沒有;
而相應(yīng)的代碼如下:

  /**
     * Opens the socket connection.
     */
    private ConnectionFlowStep connectChannel = new ConnectionFlowStep(this, CONNECTING) {
        @Override
        public boolean shouldExecuteOnEventLoop() {
            return false;
        }

        @Override
        public Future<?> execute() {
            //復(fù)用整個ClientToProxy的處理IO的線程
            Bootstrap cb = new Bootstrap().group(ProxyToServerConnection.this.clientConnection.channel.eventLoop())
                .channel(NioSocketChannel.class)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, proxyServer.getConnectTimeout())
                .handler(new ChannelInitializer<Channel>() {
                    public void initChannel(Channel ch) throws Exception {
                        Object tracingContext =
                            ProxyToServerConnection.this.clientConnection.channel.attr(KEY_CONTEXT).get();
                        ch.attr(KEY_CONTEXT).set(tracingContext);
                        initChannelPipeline(ch.pipeline(), initialRequest);
                    }

                ;
                });
            if (localAddress != null) {
                return cb.connect(remoteAddress, localAddress);
            } else {
                return cb.connect(remoteAddress);
            }
        }
    };

這種做法zuul2也是如此做,文章可以看看這篇介紹比較詳細(xì):http://www.itdecent.cn/p/cb413fec1632

  • 部署壓測:
image.png
  • 壓測結(jié)果


    image.png

總結(jié)

  • Netty的線程池模型選擇直接決定了性能
  • 在Netty的InboundHandler里不要做任何的加鎖動作,Netty的pipline已經(jīng)保證了是單線程運行,如果要緩存數(shù)據(jù)的話直接用HashMap就好,別用ConcuurentHashMap或者 Collections.synchronizedMap來做加鎖動作
  • 保證Filter是無狀態(tài)的
  • 慎用applicationContext.getEnvironment().getProperty(),在非Web容器環(huán)境下該操作將影響很大的性能
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容