最近接手项目上需要用到一个http服务。服务每次执行时间过长,所以请求的方式是异步轮询的方式调用服务。项目以前的同事在实现这个项目的时候采用的是单独开启一个线程,使用apache http client库发送请求,然后sleep一段时间再发送请求轮询的方式,这样每次调用服务需要占用一个单独的线程,极大浪费服务器资源,并且并发量有限,所以我改写了部分逻辑。并且手动实现了基于netty的长连接http客户端。有非常高的性能。在性能较低的笔记本上测试,执行简单get请求QPS在10000左右,超越市面上绝大多数http客户端性能。源代码在https://github.com/zhwaaaaaa/rpollingc。
最初在计划实现这个长连接http客户端之前以为非常简单,但是在实现过程中发现不同的服务器对于连接的处理方式还不相同,本文主要讲述在实现思路和遇到的一些坑。
思路:
1.客户端与服务器之前建立多条socket连接(默认8条),发送请求的时候随机选择一条发送出去。发送数据的时候每条连接使用http pipeline的方式(一个请求发送完毕之后无需等待服务器响应这个请求,直接发送下一个请求)。这里就有一个问题了,就是请求源源不断的发送出去,服务器源源不断的把每个请求的响应发送回来,到底请求和响应怎么对应起来。对于使用者来说,他需要知道每个请求的响应到底是什么。使用netty解决这个问题代码如下,rpollingc-core/src/main/java/com/zhw/rpollingc/http/conn/NettyHttpHandler.java。根据http规范,同一个连接上请求是先发送,先响应的,先发送的请求,它的response会先收到。 做法是在netty的handler中,如果一个请求发送成功,把它放到一个队列的尾部,如果收到一个http响应,队列头部的请求对象就是它对应的请求内容。因此在这个handler之前,需要配置解析http response的解码器。为了性能我并没有把http的编码器放到handler中去,因为handler的执行是在netty的io线程中执行的,所以在这个handler中,只是发送请求bytebuf,和接受netty解码后的fullhttpresponse。
2.许多http服务器在一个连接上处理完一定数量的请求后,会把这个连接close掉,比如nginx,默认一个连接只处理100个请求,处理完毕后会强制关闭这个连接,当然可以通过设置keepalive_requests 这个参数取修改它的数量。因为使用了上面说到的pipeline的特性,请求会源源不断的发送出去。当服务器处理完100个请求的时候,客户端可能已经发送了120个请求了,剩下的请求要么在服务器的缓冲区中,要么在客户端的缓冲区中。服务器会不会读取剩下的请求数据了,直接关闭连接,发送一个fin包过来。netty收到这个fin包,知道服务器关闭连接了。这个时候netty的做法是,还在netty缓冲区没有发送出去的请求,会收到closedChannel的异常通知。对于已经发送出去的请求,也被服务器直接丢弃掉了,永远不会接受到服务器的响应了。所以这两部分的请求必须要重新发送。解决这两个问题的方式是:1)对于已经发送出去的请求,因为发送成功之后会把它保存到上文讲的队列中,等待服务器把response 发送回来,当连接关闭的时候队列中的请求就是发送出去了被服务器丢弃的请求,需要重发这部分请求。处理方式是,服务器所有的连接做成一个环形链表,沿着这个环形链表除自己外的所有节点,依次把这些请求发送出去。2)对于在netty缓存中未来得及被netty发送出去的请求,netty会对每个请求出发closedchannelException的异常,所以遇到这种异常也需要把它扔给当前环形链表的下一个节点发送,而不能直接throw给调用者。这部分代码在com/zhw/rpollingc/http/conn/HttpConnection.java中。
3.因为服务器在接受一定数量的请求后会断开连接,所以需要自动重新打开一个新的socketchannel。对于环形链表每个节点保持的连接,可能正处于自动重连的过程中,为提高并发量,当检测到这个节点的连接处于重连过程中的时候,需要把它交给下一个节点继续发送,如果依次往下寻找多个个点后(默认6个),依然处于重连过程中,这时采取的方案是检查连接断开时间,如果连接断开有一段时间了(通过配置设定一个值),说明服务器可能已经连不上了,这时直接抛出异常给调用方即可。如果连接断开时间不长,则需要把它放到一个队列中(跟上文的队列不一样,上文的队列是在handler中的,这个队列是在HttpConnection中的waitingQueue)保存起来,等到连接连上之后发送出去,或者当重连一定次数之后对这部分请求抛出异常处理。为了提高并发量,使用cas写了com/zhw/rpollingc/utils/AtomicArrayCollector.java。
4.当连接上一段时段都没有请求发送的时候,服务器会关闭连接。这里可以采取的做法直接关闭连接重连,或者设一个heartbeat的url,当连接空闲一段时间后发送心跳。下面贴上最核心的HttpConnection.java的代码
package com.zhw.rpollingc.http.conn;
import com.zhw.rpollingc.common.RpcException;
import com.zhw.rpollingc.http.NettyConfig;
import com.zhw.rpollingc.utils.AtomicArrayCollector;
import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.http.HttpObjectAggregator;
import io.netty.handler.codec.http.HttpResponseDecoder;
import io.netty.handler.timeout.IdleStateHandler;
import io.netty.util.HashedWheelTimer;
import io.netty.util.internal.logging.InternalLogger;
import io.netty.util.internal.logging.Slf4JLoggerFactory;
import java.nio.channels.ClosedChannelException;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.TimeUnit;
/**
* auto reconnect http connection. <br/>
* <p>
* {@link NettyHttpHandler} use send request with http1.1 and keep alive the real tcp connection.
* it will send request continuously even if the response of last request not yet received,
* but much server will force closing connection after accepted number of http requests,
* even if using http1.1,such as nginx(max_requests=..),tomcat.
* so auto reconnect to the server and resend request is necessary.
* </p>
* <p>
* during reconnecting this class can receive user http requests and entrust next connect to send.
* so the best way is creating the http connection object and use it to be one of the node of cycle linked list.
* and it will resend the requests on http pipeline when the server closed.
* you can random to pick one connection to send request. this way also can increment the throughput
* </p>
*/
public class HttpConnection implements HttpEndPoint, NettyHttpHandler.Listener {
private static final HashedWheelTimer RECONNECT_TIMER = new HashedWheelTimer();
private static final InternalLogger log = Slf4JLoggerFactory.getInstance(HttpConnection.class);
private final Bootstrap bootstrap;
private final long maxWaitingOpenTime;
private final NettyConfig conf;
private final AtomicArrayCollector<HttpRequest> waitingQueue;
private volatile boolean userClose = false;
HttpConnection next;
private volatile Channel channel;
private long lastCloseTime = -1;
// 128个请求
public HttpConnection(Bootstrap bootstrap, NettyConfig conf) {
this.conf = conf;
waitingQueue = new AtomicArrayCollector<>(conf.getMaxWaitingReSendReq());
maxWaitingOpenTime = conf.getMaxWaitingOpenTimeMs();
bootstrap.handler(new ChannelInitializer<NioSocketChannel>() {
@Override
protected void initChannel(NioSocketChannel ch) throws Exception {
ch.pipeline().addLast("idle-handler", new IdleStateHandler(0,
0,
conf.getIdleHeartbeatInterval()));
ch.pipeline().addLast("http-codec", new HttpResponseDecoder());
ch.pipeline().addLast("response-body-cumulate", new HttpObjectAggregator(conf.getMaxRespBodyLen(),
true));
ch.pipeline().addLast("http-pipeline-handler", new NettyHttpHandler(HttpConnection.this));
}
});
this.bootstrap = bootstrap;
}
@Override
public void send(HttpRequest request) throws RpcException {
send(request, 0);
}
void send(HttpRequest request, int times) {
Channel channel = this.channel;
if (channel == null) {
long lastCloseTime = this.lastCloseTime;
long now = System.currentTimeMillis();
if (userClose) {
request.onErr(new RpcException("user close connection"));
} else if (times < 6) {
// 连接暂时关闭了,发送给下一个节点
next.send(request, times + 1);
} else if (lastCloseTime > 0L) {
if (now - lastCloseTime > maxWaitingOpenTime) {
// 连接 连接已经关闭很长时间了。
request.onErr(new RpcException("closed connection"));
} else {
// 保存到队列等待连接上了之后发送数据
int offer = waitingQueue.offer(request);
// 成功入队
if (offer == 0) {
return;
}
if (offer < 0) {
// 队列不是null,说明队列都满了,就不等了,直接报错
request.onErr(new RpcException("to many request waiting closed connection"));
} else {
// 正在collecting,此时重发就能发出去
send(request, times);
}
}
} else if (lastCloseTime == -2) {
// 因为是先获取channel == null,再判断 lastCloseTime==-2.这也说明连接可能刚刚好了
// 重新走一遍流程即可
send(request, times);
} else {
// lastCloseTime==-1 连接还没打开
request.onErr(new IllegalStateException("sending after calling connect()"));
}
} else {
doWrite0(channel, request);
}
}
private void doWrite0(Channel channel, HttpRequest request) {
ByteBuf reqByteBuf = request.getReqByteBuf();
// 这里必须把引用计数+1,防止发送失败被netty回收
reqByteBuf.retain();
ChannelFuture future = channel.writeAndFlush(request);
future.addListener(f -> {
if (!f.isSuccess()) {
Throwable cause = f.cause();
if (cause instanceof ClosedChannelException) {
// 调用了write 会先交给netty排队。如果排队过程中连接断开了,交接下一个节点发送
// 能够有机会进入netty排队,差点就发出去了,这里把times变成0,让它尽可能快的发出去
next.send(request, 0);
} else if (cause != null) {
request.onErr(new RpcException("connection error", cause));
} else {
request.onErr(new RpcException("send occur unkown error"));
}
}
});
}
@Override
public void connect() {
doConnect(false);
}
private void doConnect(boolean asyncAndReconnect)
throws RpcException {
if (userClose) {
throw new IllegalStateException("user closed");
}
ChannelFuture future = bootstrap.connect();
if (asyncAndReconnect) {
future.addListener(f -> {
if (!f.isSuccess()) {
Throwable cause = f.cause();
String errMsg = cause != null ? cause.getMessage() : "UNKWON ERROR";
log.error("connect error with conf " + conf + " cause:" + errMsg);
long now = System.currentTimeMillis();
if (now - lastCloseTime > maxWaitingOpenTime) {
// 每次重连之前检查一下是否超过了断开连接的最大容忍时间。
// 超过这个时间,就把保存在队列中的请求全部返回错误。
Iterator<HttpRequest> iterator = waitingQueue.collect();
if (iterator.hasNext()) {
// 如果是调用了userClose,这里队列 可能是null
RpcException exp = new RpcException("closed connection and reconnect failed:" + errMsg);
for (; iterator.hasNext(); ) {
HttpRequest req = iterator.next();
req.onErr(exp);
}
}
} else if (userClose) {
Iterator<HttpRequest> iterator = waitingQueue.collect();
if (iterator.hasNext()) {
// 如果是调用了userClose,这里队列 可能是null
RpcException exp = new RpcException("waiting util user closed connection");
for (; iterator.hasNext(); ) {
HttpRequest req = iterator.next();
req.onErr(exp);
}
}
return;
}
scheduleReconnect();
}
});
} else {
future.awaitUninterruptibly();
if (future.isSuccess()) {
return;
}
Throwable cause = future.cause();
if (cause != null) {
throw new RpcException("connect failed with conf" + conf, cause);
} else {
throw new RpcException("unkown reason connect failed with conf" + conf);
}
}
}
@Override
public void close() {
userClose = true;
Channel channel = this.channel;
if (channel != null) {
this.channel = null;
channel.close();
}
}
@Override
public void onOpened(Channel ch) {
if (userClose) {
ch.close();
Iterator<HttpRequest> iterator = waitingQueue.collect();
if (iterator.hasNext()) {
// 如果是调用了userClose,这里队列 可能是null
RpcException exp = new RpcException("user closed connection");
for (; iterator.hasNext(); ) {
HttpRequest req = iterator.next();
req.onErr(exp);
}
}
return;
}
this.channel = ch;
this.lastCloseTime = -2;
Iterator<HttpRequest> iterator = waitingQueue.collect();
for (; iterator.hasNext(); ) {
doWrite0(ch, iterator.next());
}
}
@Override
public void onClosed(Collection<HttpRequest> reqs) {
lastCloseTime = System.currentTimeMillis();
if (userClose) {
return;
}
HttpConnection next = this.next;
if (next == null || next == this) {
Iterator<HttpRequest> iterator = reqs.iterator();
while (iterator.hasNext() && waitingQueue.offer(iterator.next()) < 0) ;
if (iterator.hasNext()) {
RpcException exp = new RpcException("to many req waiting unopened connection");
do {
iterator.next().onErr(exp);
} while (iterator.hasNext());
}
channel = null;
} else {
channel = null;
for (HttpRequest req : reqs) {
next.send(req);
next = next.next;
if (next == this) {
next = next.next;
}
}
}
doConnect(true);
}
private void scheduleReconnect() {
if (userClose) {
return;
}
RECONNECT_TIMER.newTimeout(timeout -> {
if (!timeout.isCancelled()) {
doConnect(true);
}
}, 1000, TimeUnit.MILLISECONDS);
}
}