房产网站排行榜/软件培训机构哪家好
背景
前段时间测试反馈页面会偶先错误提示,刷新后就没有了。根据测试反馈的时间点查看kibana日志,发现有Connection reset异常。下面通过分析客户端和服务端相关源码进行分析根本原因,给出解决方案。
源码分析
HttpClient分析(version:4.5.13)
- 测试代码如下
@Test public void shouldAnswerWithTrue() throws Exception {HttpClient httpClient = HttpClientBuilder.create().build();HttpGet httpGet = new HttpGet("http://localhost:8080/demo/test");HttpResponse httpResponse = httpClient.execute(httpGet);System.out.println(EntityUtils.toString(httpResponse.getEntity(), "UTF-8"));Thread.sleep(2000);HttpResponse httpResponse1 = httpClient.execute(httpGet);System.out.println(EntityUtils.toString(httpResponse1.getEntity(), "UTF-8")); }
- 连接池获取连接
2.1 调用栈截图如下
2.2 下面展示MainClientExec#execute相关代码public CloseableHttpResponse execute(final HttpRoute route,final HttpRequestWrapper request,final HttpClientContext context,final HttpExecutionAware execAware) throws IOException, HttpException {/*** 1. 从连接池CPool取出Future<CPoolEntry>** ---> AbstractConnPool#lease** 2. 返回ConnectionRequest** 2.1 操作上面future从连接池取连接*/final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);/**** 1. 从连接池取连接:AbstractConnPool#getPoolEntryBlocking1.1 根据路由获取连接池:AbstractConnPool#getPool,没有则新建映射关系:routeToPool1.2 从连接池中获取空闲连接:RouteSpecificPool#getFree1.3 判断连接是否过期:PoolEntry#isExpired (见下面响应头的keep-alive的timeout值)1.4 如果没有,则创建该路由的连接:PoolingHttpClientConnectionManager#create,放入连接池中** 2. 检查连接是否可用:BHttpConnectionBase#isStale2.1 检查间隔2s2.2 连接不是open状态时不需要检查2.3 尝试从socket读取数据,如果有问题重新执行第一步获取连接HttpClientConnection managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);// 如果不是open状态,则建立路由连接:MainClientExec#establishRouteestablishRoute(proxyAuthState, managedConn, route, request, context);/**** 1. 发送请求:HttpRequestExecutor#doSendRequest** 2. 处理响应:HttpRequestExecutor#doReceiveResponse2.1 通过socket读取数据:SocketInputStream#read (异常点)2.2 注意:如果在服务端连接关闭,此时执行SocketInputStream#read是没有异常*/response = requestExecutor.execute(request, managedConn, context);// 如果请求头的Connection为keep-alive则为true:DefaultClientConnectionReuseStrategy#keepAliveif (reuseStrategy.keepAlive(response, context)) {// 从响应头获取Keep-Alive,timeout=60s :DefaultConnectionKeepAliveStrategy#getKeepAliveDurationfinal long duration = keepAliveStrategy.getKeepAliveDuration(response, context);}}
tomcat分析(version:9.0.65)
-
在connector启动时,会开始acceptor线程等待接收请求(下面展示Acceptor#run重要节点逻辑)
public void run() {while (!stopCalled) {// 如果我们已经到达最大连接,等待(最大连接配置:server.tomcat.max-connections=1)endpoint.countUpOrAwaitConnection();/** ** 等待接收请求 NioEndpoint#serverSocketAccept** 1. ServerSocketChannelImpl#serverSocketAccept 如果没有客户端请求则会阻塞*/socket = endpoint.serverSocketAccept();/**** 将socket传递给适当的处理器** 1. 新建一个指定buffer的NioChannel : NioChannel channel = new NioChannel(bufhandler)** 2. 将channel和endipoint包装为NioSocketWrapper,设置读写超时时间(默认1分钟)** 3. NioEndpoint$Poller#register3.1 socket注册SelectionKey.OP_READ事件**/endpoint.setSocketOptions(socket)} }
-
NioEndpoint$Poller在初始化时开启selector,selector循环获取事件(监听读写事件)
public void run() {while (true) {// selector获取注册的事件数int keyCount = selector.select(selectorTimeout);Iterator<SelectionKey> iterator =keyCount > 0 ? selector.selectedKeys().iterator() : null;while (iterator != null && iterator.hasNext()) {SelectionKey sk = iterator.next();// 获取绑定在SelectionKey的socketNioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();/** ** 1. AbstractEndpoint#processSocket** 1.1 新建NioEndpoint$SocketProcessor** 1.2 将SocketProcessor丢入线程池执行*/processKey(sk, socketWrapper);}/**** 超时处理** 1. 计算当前时间到上次socket读数据的时间的间隔:long delta = now - socketWrapper.getLastRead()** 2. socket超时时间:long timeout = socketWrapper.getReadTimeout()** 3. 如果delta>timeout则读超时,重置SocketProcessor的event为SocketEvent.ERRORtimeout(keyCount,hasEvents);} }
-
SocketProcessor最终会调业务的controller方法
3.1 下面展示SocketProcessor#doRun部分代码protected void doRun() {if (event == null) {state = getHandler().process(socketWrapper, SocketEvent.OPEN_READ);} else {/**** AbstractProtocol#process** 1. 如果SocketEvent为SocketEvent.ERROR,则修改SocketState为CLOSED*/state = getHandler().process(socketWrapper, event);}// 超时,则AbstractEndpoint#countDownConnection连接次数减1,重新等待连接(参考上面acceptor线程)if (state == SocketState.CLOSED) {poller.cancelledKey(getSelectionKey(), socketWrapper);} }
3.2 下图展示调用堆栈
问题复现
- 启动springboot的web项目,在socket过期关闭的地方打上断点等待执行
- 在通过socket连接发起http请求地方打上断点(MainClientExec#execute)
2.1 第一个http请求直接跳过
2.2 第二个同一个路由的http请求断点停留,等到服务端sokcet过期关闭该socket连接后执行
2.3 此时执行下一步SocketInputStream#socketRead就会捕获到ConnectionResetException异常
- 最终在RetryExec#execute捕获异常并打印日志
3.1 在捕获异常后,通过DefaultHttpRequestRetryHandler#retryRequest判断是否需要重试
(1)默认连续重试三次
(2)是否包含在不能重试的异常
3.2 打印出连接重置的debug日志
3.3 如果超过重试次数会打印如下日志
束语
虽然本地模拟复现了重置连接的异常,但只是抛出的debug日志,实际遇到异常却是error级别的,所以问题没有完全复现。在网上看到一篇类似的文章《HttpClient遭遇Connection Reset异常,如何正确配置》,对比发现确实自己也没有配空闲连接驱逐器,就死马当作活马医加了一下配置,发到生产查看效果,神奇的是发了之后确实再也没有发现该问题了。