一个API网关性能优化实践之路

前言

最近换了一份工作,而新工作是调研下目前业界Api网关的一些性能情况,而在最近过去一年的时间里,我也主要开发了一个Api网关来支持协议适配的需求,但是由于前东家的话整个流量都不大,而相应的性能优化也没有很好的去做,借着让原来在唯品会的同事把网关推到了OYO在做性能压测及我刚入职新单位接手的第一项任务,把网关的性能进行了一下优化,也踩了一些坑,把这些作为总结写下来;本文是https://juejin.im/post/5d19dd5c6fb9a07ec27bbb6e?from=timeline&isappinstalled=0
的补充,主要是介绍整个优化步骤;

网关简介

Tesla的整个网络框架是基于littleproxy[https://github.com/adamfisk/LittleProxy],littleproxy是著名的软件的后端代理,按照常规性能应该不错,在此基础上我们加了些功能,具体代码在:[https://github.com/spring-avengers/tesla]

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

  • 增加了10多个Filter,而这些Filter由一个最大的Filter包裹来执行;

  1. HttpFiltersAdapter的clientToProxyRequest方法,负责调用方到代理方的拦截处理
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方法,负责从后端服务拿到请求返回给调用方的拦截处理
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的配置数据都是定时从数据库里面拉取(用hazelcast做了缓存);

  • 为了让Filter做到热插拔,大量的使用了反射去构造Filter的实例;

优化步骤

  • 由于大量的使用了反射去构造实例,但是没有去缓存这些实例,原本的想法是所有的Filter都是有状态的,伴随每一个HTTP请求的消亡而消亡,但是最终运行下来发现如果并发量上来整个GC完全接受不了,大量的对象的产生引起了yong gc的频率几乎成倍的增加,进而导致qps上不去,而解决这个问题的办法就是缓存实例,让每一个Filter无状态,整个JVM内存中仅存在一份实例;
    但是这些做下来,发现网关的QPS也没上去,尽管GC情况解决了,QPS有所上升,但是远远没达到Netty所想要的QPS,那只能继续往前走;

  • 线程池的优化

    我们都知道Netty的线程池分为boss线程池和work线程池,其中boss线程池负责接收网络请求,而work线程池负责处理io任务及其他自定义任务,对于网关这个应用来说,boss线程池是必须要的,因为要负责请求的接入,但是网关比较特殊,对于真正的调用方来说,它是一个服务端,对于后端服务来说它是一个客户端,所以他的线程模型应该是如下:


    image.png

    这种线程池模型是典型的netty的线程池模型:
    Acceptor负责接收请求,ClientToProxyWorker是负责代理服务器的处理IO请求,而ProxyServerWorker负责转发请求到后端服务,LittleProxy就是使用这种很经典的线程模型,其QPS在4核32G的机器下QPS大概能达到9000多,但是这种线程模型存在ClientToProxyWorker和ProxyServerWorker线程切换的问题,我们都知道线程切换是要耗费CPU资源的,那我们是不是可以做一个改变呢?换成以下这种:


    image.png

    这种线程池模型是将ClientToProxyWorker和ProxyServerWorker复用同一个线程池,这种做法在省却了一个线程切换的时间,也就是对于代理服务器来说,netty的服务端及netty的客户端在线程池传入时复用同一个线程池对象;
    做到这一步的话整个代理服务的性能应该能提升不少,但是有没有更好的线程模型呢?答案是肯定的;
    image.png

这个线程模型的话,整个处理请求及转发请求都复用同一个线程,而这种做法的话线程的切换基本没有;
而相应的代码如下:

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

        @Override
        public Future<?> execute() {
            //复用整个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也是如此做,文章可以看看这篇介绍比较详细:https://www.jianshu.com/p/cb413fec1632

  • 部署压测:
image.png
  • 压测结果


    image.png

总结

  • Netty的线程池模型选择直接决定了性能
  • 在Netty的InboundHandler里不要做任何的加锁动作,Netty的pipline已经保证了是单线程运行,如果要缓存数据的话直接用HashMap就好,别用ConcuurentHashMap或者 Collections.synchronizedMap来做加锁动作
  • 保证Filter是无状态的
  • 慎用applicationContext.getEnvironment().getProperty(),在非Web容器环境下该操作将影响很大的性能
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345