前言
最近換了一份工作,而新工作是調(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í)行;
- 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;
}
...
}
- 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è):

-
壓測(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)境下該操作將影響很大的性能



