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

前言

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

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

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

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

  • 增加了10多個(gè)Filter,而這些Filter由一個(gè)最大的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ù)拿到請(qǐng)求返回給調(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;
      }
  }
  • 每個(gè)Filter的配置數(shù)據(jù)都是定時(shí)從數(shù)據(jù)庫里面拉?。ㄓ胔azelcast做了緩存);

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

優(yōu)化步驟

  • 由于大量的使用了反射去構(gòu)造實(shí)例,但是沒有去緩存這些實(shí)例,原本的想法是所有的Filter都是有狀態(tài)的,伴隨每一個(gè)HTTP請(qǐng)求的消亡而消亡,但是最終運(yùn)行下來發(fā)現(xiàn)如果并發(fā)量上來整個(gè)GC完全接受不了,大量的對(duì)象的產(chǎn)生引起了yong gc的頻率幾乎成倍的增加,進(jìn)而導(dǎo)致qps上不去,而解決這個(gè)問題的辦法就是緩存實(shí)例,讓每一個(gè)Filter無狀態(tài),整個(gè)JVM內(nèi)存中僅存在一份實(shí)例;
    但是這些做下來,發(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ò)請(qǐng)求,而work線程池負(fù)責(zé)處理io任務(wù)及其他自定義任務(wù),對(duì)于網(wǎng)關(guān)這個(gè)應(yīng)用來說,boss線程池是必須要的,因?yàn)橐?fù)責(zé)請(qǐng)求的接入,但是網(wǎng)關(guān)比較特殊,對(duì)于真正的調(diào)用方來說,它是一個(gè)服務(wù)端,對(duì)于后端服務(wù)來說它是一個(gè)客戶端,所以他的線程模型應(yīng)該是如下:


    image.png

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


    image.png

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

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

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

        @Override
        public Future<?> execute() {
            //復(fù)用整個(gè)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

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


    image.png

總結(jié)

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

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