【转载】 一次生产环境的NOHTTPRESPONSEEXCEPTION异常的排查记录
https://www.freesion.com/article/41531004212/
环境:
jdk1.8+tomcat8+httpclient4.5.2
主要现象:
项目偶发出现org.apache.http.NoHttpResponseException: The target server failed to respond异常
定位原因:
查阅资料,此异常属于长连接keep-Alive的一种异常现象。当服务端某连接闲置超过keep-Alive超时时间后,服务端会关闭连接,进行四次挥手。如果此时,客户端再次拿此连接来访问服务端就会报NoHttpResponseException错误。
解决过程:
既然已经知道错误导致的原因,就可对症下药。主要解决思路有两种:
方案一:延长务端keep-Alive超时时间,拿tomcat举例,可以配置Connector 元素中的keepAliveTimeout参数;
方案二:降低客户端的keep-Alive时间,在服务端关闭闲置连接前关闭客户端连接。
方案一只能优化问题,但是并不能解决问题。因为keep-Alive超时时间不能设置为-1(永久),如果设置一直保持连接会极大的影响到服务端性能。
下面主要说一下方案二的解决方案,以httpClient4.5.2版本为例:
先贴最终的代码:
- SSLContext sslcontext = SslUtils.createIgnoreVerifySSL();
- //设置协议http和https对应的处理socket链接工厂的对象
- Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create()
- .register("http", PlainConnectionSocketFactory.INSTANCE)
- .register("https", new SSLConnectionSocketFactory(sslcontext))
- .build();
- ConnectionKeepAliveStrategy connectionKeepAliveStrategy = (final HttpResponse response, final HttpContext context) -> {
- Args.notNull(response, "HTTP response");
- final HeaderElementIterator it = new BasicHeaderElementIterator(
- response.headerIterator(HTTP.CONN_KEEP_ALIVE));
- while (it.hasNext()) {
- final HeaderElement he = it.nextElement();
- final String param = he.getName();
- final String value = he.getValue();
- if (value != null && param.equalsIgnoreCase("timeout")) {
- try {
- return Long.parseLong(value) * 1000;
- } catch (final NumberFormatException ignore) {
- }
- }
- }
- // keep alive 3秒 客户端维护这个连接最多3秒的有效期 在获取环节超过3秒就会关闭此连接org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking() entry.isExpired(System.currentTimeMillis())
- return 3 * 1000;
- };
- PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
- connManager.setDefaultMaxPerRoute(10);
- connManager.setMaxTotal(100);
- //获取连接后 再次校验是否空闲超时org.apache.http.pool.AbstractConnPool.getPoolEntryBlocking() entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()
- connManager.setValidateAfterInactivity(3000);
- //evictIdleConnections 超时之前 定期回收空闲连接 并发setMaxConnPerRoute=10 最多setMaxConnTotal=100个;注意,evictIdleConnections会在启动时线程sleep一个maxIdle时间
- //创建自定义的httpclient对象
- CloseableHttpClient client = HttpClients.custom()
- // 注意:HttpClients的setDefaultMaxPerRoute和setMaxTotal不会覆盖connManager的值
- .setConnectionManager(connManager)
- .setConnectionManagerShared(false).evictIdleConnections(3000, TimeUnit.MILLISECONDS)
- .setKeepAliveStrategy(connectionKeepAliveStrategy)
- // 接口幂等 允许重试 注释掉disableAutomaticRetries 默认重试3次 会从连接池中获取 不会直接创建新的连接
- .disableAutomaticRetries()
- .build();
主要配置参数说明:
- org.apache.http.impl.conn.PoolingHttpClientConnectionManager#setValidateAfterInactivity
- org.apache.http.impl.client.HttpClientBuilder#setConnectionManagerShared
- org.apache.http.impl.client.HttpClientBuilder#evictIdleConnections(long, java.util.concurrent.TimeUnit)
- org.apache.http.impl.client.HttpClientBuilder#setKeepAliveStrategy
- org.apache.http.impl.client.HttpClientBuilder#disableAutomaticRetries
ORG.APACHE.HTTP.IMPL.CONN.POOLINGHTTPCLIENTCONNECTIONMANAGER#SETVALIDATEAFTERINACTIVITY
从连接池中获取到空闲连接后,在使用之前校验空闲时间是否超过指定的时间,单位毫秒;注意,如果你像楼主一样,使用了
PoolingHttpClientConnectionManager connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);
这块代码,那么请注意此处会把时间设置为2000ms.(ps:楼主在本地环境一直复现不了NoHttpResponseException的罪魁祸首)
方法路径:
org.apache.http.impl.conn.PoolingHttpClientConnectionManager#PoolingHttpClientConnectionManager(org.apache.http.conn.HttpClientConnectionOperator, org.apache.http.conn.HttpConnectionFactory<org.apache.http.conn.routing.HttpRoute,org.apache.http.conn.ManagedHttpClientConnection>, long, java.util.concurrent.TimeUnit)
逻辑上只要配置此处,即可保证连接在超时后关闭并重新从池子中获取(如果还超时,继续关闭连接并重新拿),无论哪一种配置,一定要配置此处,否则都会安装默认2秒过期时间来回收连接。感兴趣的可以看下源码:
- private E getPoolEntryBlocking(
- final T route, final Object state,
- final long timeout, final TimeUnit tunit,
- final PoolEntryFuture<E> future)
- throws IOException, InterruptedException, TimeoutException {
- Date deadline = null;
- if (timeout > 0) {
- deadline = new Date
- (System.currentTimeMillis() + tunit.toMillis(timeout));
- }
- this.lock.lock();
- try {
- final RouteSpecificPool<T, C, E> pool = getPool(route);
- E entry = null;
- while (entry == null) {
- Asserts.check(!this.isShutDown, "Connection pool shut down");
- for (;;) {
- entry = pool.getFree(state);
- if (entry == null) {
- break;
- }
- if (entry.isExpired(System.currentTimeMillis())) {
- entry.close();
- } else if (this.validateAfterInactivity > 0) {
- //看这里 连接最后修改时间+超时时间是否小于now
- if (entry.getUpdated() + this.validateAfterInactivity <= System.currentTimeMillis()) {
- if (!validate(entry)) {
- entry.close();
- }
- }
- }
- if (entry.isClosed()) {
- this.available.remove(entry);
- pool.free(entry, false);
- } else {
- break;
- }
- }
- if (entry != null) {
- this.available.remove(entry);
- this.leased.add(entry);
- onReuse(entry);
- return entry;
- }
- // New connection is needed
- final int maxPerRoute = getMax(route);
- // Shrink the pool prior to allocating a new connection
- final int excess = Math.max(0, pool.getAllocatedCount() + 1 - maxPerRoute);
- if (excess > 0) {
- for (int i = 0; i < excess; i++) {
- final E lastUsed = pool.getLastUsed();
- if (lastUsed == null) {
- break;
- }
- lastUsed.close();
- this.available.remove(lastUsed);
- pool.remove(lastUsed);
- }
- }
- if (pool.getAllocatedCount() < maxPerRoute) {
- final int totalUsed = this.leased.size();
- final int freeCapacity = Math.max(this.maxTotal - totalUsed, 0);
- if (freeCapacity > 0) {
- final int totalAvailable = this.available.size();
- if (totalAvailable > freeCapacity - 1) {
- if (!this.available.isEmpty()) {
- final E lastUsed = this.available.removeLast();
- lastUsed.close();
- final RouteSpecificPool<T, C, E> otherpool = getPool(lastUsed.getRoute());
- otherpool.remove(lastUsed);
- }
- }
- final C conn = this.connFactory.create(route);
- entry = pool.add(conn);
- this.leased.add(entry);
- return entry;
- }
- }
- boolean success = false;
- try {
- pool.queue(future);
- this.pending.add(future);
- success = future.await(deadline);
- } finally {
- // In case of 'success', we were woken up by the
- // connection pool and should now have a connection
- // waiting for us, or else we're shutting down.
- // Just continue in the loop, both cases are checked.
- pool.unqueue(future);
- this.pending.remove(future);
- }
- // check for spurious wakeup vs. timeout
- if (!success && (deadline != null) &&
- (deadline.getTime() <= System.currentTimeMillis())) {
- break;
- }
- }
- throw new TimeoutException("Timeout waiting for connection");
- } finally {
- this.lock.unlock();
- }
- }
方法路径:
org.apache.http.pool.AbstractConnPool#getPoolEntryBlocking
ORG.APACHE.HTTP.IMPL.CLIENT.HTTPCLIENTBUILDER#SETCONNECTIONMANAGERSHARED和ORG.APACHE.HTTP.IMPL.CLIENT.HTTPCLIENTBUILDER#EVICTIDLECONNECTIONS(LONG, JAVA.UTIL.CONCURRENT.TIMEUNIT)
启动异步定时线程,关闭回收指定超时时间的空闲连接。如果在获取空闲连接前已经回收就没问题了,但是极端情况下也会出现NoHttpResponseException问题。比如:keep-Alive超时时间是20秒,然后定时配置15秒,假设第一次使用连接并释放时间为x,定时上次结束时间为y,y+15<x+20,也就是定时下次处理时,连接空闲时间还没有超过20秒,那么此处定时不会回收此连接,但是如果5秒后获取这个连接使用,肯定会报NoHttpResponseException异常。
evictIdleConnections需要配合setConnectionManagerShared=false使用,ConnectionManagerShared默认false。关键代码如下:
- if (!this.connManagerShared) {
- if (closeablesCopy == null) {
- closeablesCopy = new ArrayList<Closeable>(1);
- }
- final HttpClientConnectionManager cm = connManagerCopy;
- if (evictExpiredConnections || evictIdleConnections) {
- final IdleConnectionEvictor connectionEvictor = new IdleConnectionEvictor(cm,
- maxIdleTime > 0 ? maxIdleTime : 10, maxIdleTimeUnit != null ? maxIdleTimeUnit : TimeUnit.SECONDS);
- closeablesCopy.add(new Closeable() {
- @Override
- public void close() throws IOException {
- connectionEvictor.shutdown();
- }
- });
- connectionEvictor.start();
- }
- closeablesCopy.add(new Closeable() {
- @Override
- public void close() throws IOException {
- cm.shutdown();
- }
- });
- }
注意:evictIdleConnections会在启动时,线程sleep一个maxIdle时间。源码如下:
- public IdleConnectionEvictor(
- final HttpClientConnectionManager connectionManager,
- final ThreadFactory threadFactory,
- final long sleepTime, final TimeUnit sleepTimeUnit,
- final long maxIdleTime, final TimeUnit maxIdleTimeUnit) {
- this.connectionManager = Args.notNull(connectionManager, "Connection manager");
- this.threadFactory = threadFactory != null ? threadFactory : new DefaultThreadFactory();
- this.sleepTimeMs = sleepTimeUnit != null ? sleepTimeUnit.toMillis(sleepTime) : sleepTime;
- this.maxIdleTimeMs = maxIdleTimeUnit != null ? maxIdleTimeUnit.toMillis(maxIdleTime) : maxIdleTime;
- this.thread = this.threadFactory.newThread(new Runnable() {
- @Override
- public void run() {
- try {
- while (!Thread.currentThread().isInterrupted()) {
- //此处休眠一个sleepTimeMs时间 可追溯代码发现sleepTimeMs来源于maxIdleTime
- Thread.sleep(sleepTimeMs);
- connectionManager.closeExpiredConnections();
- if (maxIdleTimeMs > 0) {
- connectionManager.closeIdleConnections(maxIdleTimeMs, TimeUnit.MILLISECONDS);
- }
- }
- } catch (final Exception ex) {
- exception = ex;
- }
- }
- });
- }
方法路径:
org.apache.http.impl.client.IdleConnectionEvictor#IdleConnectionEvictor(org.apache.http.conn.HttpClientConnectionManager, java.util.concurrent.ThreadFactory, long, java.util.concurrent.TimeUnit, long, java.util.concurrent.TimeUnit)
ORG.APACHE.HTTP.IMPL.CLIENT.HTTPCLIENTBUILDER#SETKEEPALIVESTRATEGY
此方法是设置客户端连接池维护的连接的keep-Alive时间。如果连接空闲时间超过设置的时间,则会关闭此连接并重新获取。主要相关源码如下:在初始化线程池时设置了过期时间expiry是创建时间+keep-Alive时间,已经过期时间在updateExpiry(连接池回收会调接方法)中被修改成最新时间。
- //方法路径:org.apache.http.pool.PoolEntry#PoolEntry(java.lang.String, T, C, long, java.util.concurrent.TimeUnit) 此处的timeToLive 就是设置的keep-Alive时间
- public PoolEntry(final String id, final T route, final C conn,
- final long timeToLive, final TimeUnit tunit) {
- super();
- Args.notNull(route, "Route");
- Args.notNull(conn, "Connection");
- Args.notNull(tunit, "Time unit");
- this.id = id;
- this.route = route;
- this.conn = conn;
- this.created = System.currentTimeMillis();
- if (timeToLive > 0) {
- this.validityDeadline = this.created + tunit.toMillis(timeToLive);
- } else {
- this.validityDeadline = Long.MAX_VALUE;
- }
- this.expiry = this.validityDeadline;
- }
- //方法路径:org.apache.http.pool.PoolEntry#updateExpiry
- public synchronized void updateExpiry(final long time, final TimeUnit tunit) {
- Args.notNull(tunit, "Time unit");
- this.updated = System.currentTimeMillis();
- final long newExpiry;
- if (time > 0) {
- newExpiry = this.updated + tunit.toMillis(time);
- } else {
- newExpiry = Long.MAX_VALUE;
- }
- this.expiry = Math.min(newExpiry, this.validityDeadline);
- }
以上配置不是互斥也不少都需要配置,楼主亲自验证发现,只配置setValidateAfterInactivity或只配置setKeepAliveStrategy都可以。evictIdleConnections极端情况会有问题。
ORG.APACHE.HTTP.IMPL.CLIENT.HTTPCLIENTBUILDER#DISABLEAUTOMATICRETRIES
随便一提,httpclient默认会重试3次。如果接口不支持幂等,请注意不要使用重试。
OK,为了解决个问题,把源码看了一遍,特写博客以备以后注意使用。
【转载】 一次生产环境的NOHTTPRESPONSEEXCEPTION异常的排查记录的更多相关文章
- 教你50招提升ASP.NET性能(十二):在生产环境,仔细考虑你需要记录哪些日志
(18)When in production, carefully consider what you need to log 招数18: 在生产环境,仔细考虑你需要记录哪些日志 Many peopl ...
- 一次生产环境CPU占用高的排查
1. 项目背景 甲方是保密级别非常高的政府部门.所以我们全程拿不到任何测试数据,只能是自己模拟数据进行测试. 项目部署的时候,公司派了一人到甲方现场,在甲方客户全程监督下,进行部署,调试,导入数据等工 ...
- 记一次 MySQL 主从同步异常的排查记录,百转千回
你好,我是悟空. 这是悟空的第 183 篇原创文章 官网:www.passjava.cn 本文主要内容如下: 一.现象 最近项目的测试环境遇到一个主备同步的问题: 备库的同步线程停止了,无法同步主库的 ...
- wordpress后台加载速度异常缓慢排查记录(原创)
原因在于在function.php函数中加入了下面的代码导致了缓慢: //停用版本更新通知remove_action('load-update-core.php', 'wp_update_themes ...
- SpringBoot+ShardingSphere彻底解决生产环境数据库字段加解密问题
前言 互联网行业公司,对于数据库的敏感字段是一定要进行加密的,方案有很多,最直接的比如写个加解密的工具类,然后在每个业务逻辑中手动处理,在稍微有点规模的项目中这种方式显然是不现实的,不仅工作量大而 ...
- vite项目生产环境去掉console信息【转载】
环境变量引入 通常去掉console为生产环境,即需要引入环境变量.具体请看这篇文章: vite项目初始化之~环境变量 注意 与webpacak相比,vite已经将这个功能内置到了,所以我们只需要配置 ...
- 分布式日志框架Exceptionless之生产环境部署步骤
Exceptionless 是一个开源的实时的日志收集框架,它将日志收集变得简单易用并且不需要了解太多的相关技术细节及配置.本篇基于我的上一篇<基于Exceptionless实现分布式日志> ...
- Dubbo Mesh 在闲鱼生产环境中的落地实践
本文作者至简曾在 2018 QCon 上海站以<Service Mesh 的本质.价值和应用探索>为题做了一次分享,其中谈到了 Dubbo Mesh 的整体发展思路是“借力开源.反哺开源” ...
- 如何在生产环境使用Btrace进行调试
占小狼 转载请注明原创出处,谢谢! 背景 记得前几天有人问我:在生产环境中可能经常遇到各种问题,你们一般是如何进行调试的? 很惭愧,没有经验.因为平时碰不到生产环境的服务器,定位问题需要各种数据,所以 ...
- Flink 实战:如何解决生产环境中的技术难题?
大数据作为未来技术的基石已成为国家基础性战略资源,挖掘数据无穷潜力,将算力推至极致是整个社会面临的挑战与难题. Apache Flink 作为业界公认为最好的流计算引擎,不仅仅局限于做流处理,而是一套 ...
随机推荐
- C#中测试普通方法和对象类型以及泛型所需要的时间
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- 放大招!青云企业级容器平台 QKCP 迎来重磅升级
青云企业级容器平台 QKCP 3.2 重磅发布.QKCP(QingCloud KubeSphere Container Platform)是青云科技基于 KubeSphere 开源容器平台打造的企业级 ...
- php运行redis测试
在今天将官方的redis教程看完之后,想自己来一个测试. 按照官方给出的代码: 1 <?php 2 //连接本地的 Redis 服务 3 $redis = new Redis(); 4 $red ...
- linux 基础(5)文件的打包和压缩
文件的压缩是非常重要和常见的操作. 在 Windows 下,zip 和 rar 经常使用的压缩软件,框选一堆文件,右键用 WinRAR 就可以完成压缩.不过在 linux 下通常不这么做.一是因为 l ...
- OAS常见错误
body { font-family: Arial, sans-serif; line-height: 1.6; margin: 20px } h1, h2 { color: rgba(51, 51, ...
- 3.12 Linux创建文件及修改文件时间戳(touch命令)
既然知道了如何在 Linux 系统中创建目录,接下来你可能会想在这些目录中创建一些文件,可以使用 touch 命令. 需要注意的是,touch 命令不光可以用来创建文件(当指定操作文件不存在时,该命令 ...
- debian大便系统配置国内软件源
本例在debian:buster-slim docker镜像中实验通过 1.启动docker实例 docker run -it --name debian debian:buster-slim bas ...
- CubeIDE 主题美化与颜色设置
一.主题美化 搜索引擎里很多,这里不必多说. 二.颜色设置 2.1.关于控制台 菜单栏里:window→preference→输入"console"并回车,然后按照下图指示来: 2 ...
- 洛谷P4913【深基16.例3】二叉树深度题解
很简单的二叉树遍历问题,可以用dfs(深度优先搜索)解决. 看到数据范围,最大不超过 \(10^6\) ,可以不开 long long (但我还是习惯性地开了) 接下来上代码: #include< ...
- 高性能计算-雅可比算法MPI通信优化(5)
雅可比算法原理:如下图对方阵非边界元素求上下左右元素的均值,全部计算元素的数值计算完成后更新矩阵,进行下一次迭代. 测试目标:用MPI实现对8*8方阵雅可比算法迭代并行计算,用重复非阻塞的通信方式 # ...