一个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线程池是必须要的,因为要负责请求的接入,但是网关比较特殊,对于真正的调用方来说,它是一个服务端,对于后端服务来说它是一个客户端,所以他的线程模型应该是如下:


    一个API网关性能优化实践之路_第1张图片
    image.png

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


    一个API网关性能优化实践之路_第2张图片
    image.png

    这种线程池模型是将ClientToProxyWorker和ProxyServerWorker复用同一个线程池,这种做法在省却了一个线程切换的时间,也就是对于代理服务器来说,netty的服务端及netty的客户端在线程池传入时复用同一个线程池对象;
    做到这一步的话整个代理服务的性能应该能提升不少,但是有没有更好的线程模型呢?答案是肯定的;
    一个API网关性能优化实践之路_第3张图片
    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() {
                    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

  • 部署压测:
一个API网关性能优化实践之路_第4张图片
image.png
  • 压测结果


    一个API网关性能优化实践之路_第5张图片
    image.png

总结

  • Netty的线程池模型选择直接决定了性能
  • 在Netty的InboundHandler里不要做任何的加锁动作,Netty的pipline已经保证了是单线程运行,如果要缓存数据的话直接用HashMap就好,别用ConcuurentHashMap或者 Collections.synchronizedMap来做加锁动作
  • 保证Filter是无状态的
  • 慎用applicationContext.getEnvironment().getProperty(),在非Web容器环境下该操作将影响很大的性能

你可能感兴趣的:(一个API网关性能优化实践之路)