前言
最近换了一份工作,而新工作是调研下目前业界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包裹来执行;
- 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;
}
...
}
- 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线程池是必须要的,因为要负责请求的接入,但是网关比较特殊,对于真正的调用方来说,它是一个服务端,对于后端服务来说它是一个客户端,所以他的线程模型应该是如下:
这种线程池模型是典型的netty的线程池模型:
Acceptor负责接收请求,ClientToProxyWorker是负责代理服务器的处理IO请求,而ProxyServerWorker负责转发请求到后端服务,LittleProxy就是使用这种很经典的线程模型,其QPS在4核32G的机器下QPS大概能达到9000多,但是这种线程模型存在ClientToProxyWorker和ProxyServerWorker线程切换的问题,我们都知道线程切换是要耗费CPU资源的,那我们是不是可以做一个改变呢?换成以下这种:
这种线程池模型是将ClientToProxyWorker和ProxyServerWorker复用同一个线程池,这种做法在省却了一个线程切换的时间,也就是对于代理服务器来说,netty的服务端及netty的客户端在线程池传入时复用同一个线程池对象;
做到这一步的话整个代理服务的性能应该能提升不少,但是有没有更好的线程模型呢?答案是肯定的;
这个线程模型的话,整个处理请求及转发请求都复用同一个线程,而这种做法的话线程的切换基本没有;
而相应的代码如下:
/**
* 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
- 部署压测:
-
压测结果
总结
- Netty的线程池模型选择直接决定了性能
- 在Netty的InboundHandler里不要做任何的加锁动作,Netty的pipline已经保证了是单线程运行,如果要缓存数据的话直接用HashMap就好,别用ConcuurentHashMap或者 Collections.synchronizedMap来做加锁动作
- 保证Filter是无状态的
- 慎用applicationContext.getEnvironment().getProperty(),在非Web容器环境下该操作将影响很大的性能