api.weixin.qq.com:443 failed to respond解决方案

10,014 阅读5分钟
  • 现象

服务器访问微信的时候,零零散散会报微信服务器443端口无响应的错误。异常如下

java.lang.RuntimeException: org.apache.http.NoHttpResponseException: api.weixin.qq.com:443 failed to respond
    at me.chanjar.weixin.mp.api.impl.AbstractWxMpServiceImpl.executeInternal(AbstractWxMpServiceImpl.java:314)
    at me.chanjar.weixin.mp.api.impl.AbstractWxMpServiceImpl.execute(AbstractWxMpServiceImpl.java:251)
    at me.chanjar.weixin.mp.api.impl.AbstractWxMpServiceImpl.post(AbstractWxMpServiceImpl.java:241
    at me.chanjar.weixin.mp.api.impl.WxMpTemplateMsgServiceImpl.sendTemplateMsg(WxMpTemplateMsgServiceImpl.java:34)
    
org.apache.http.NoHttpResponseException: api.weixin.qq.com:443 failed to respond
    at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:143)
    at org.apache.http.impl.conn.DefaultHttpResponseParser.parseHead(DefaultHttpResponseParser.java:57)
  • 原因:

在网上看了很多文章,也和公司内部的其他团队碰了一下,确定原因是是因为微信端的http keepalive时长是20s,当一次http请求完成后,假设下次使用这个连接正好是在20s,微信端其实已经抛弃了这个连接,如果继续使用这个连接发送请求,就会报443的错误。【属于服务端主动抛弃链接】。

当然,有人会问下次使用这个连接的时间超过20s,是不是也会报这个错呢? 答案是,不一定。后面看源码的时候,会说详细的原因。

  • 解决方案:

目标是希望在连接池中的连接被复用的时候,微信端还没有关闭这个连接。当然,指望微信等我们是不靠谱的。我们能做的其实是,在客户端使用连接的时候进行检查,如果连接保持的时间快到20s了,我们就抛弃掉这个连接。不用了,咱们用新连接,微信挑不出理了吧。 因此,我们需要修改http参数来约定keep_alive的时间:

   /**
     * 传输超时毫秒
     */
    private final static int SOCKET_TIMEOUT = 15000;
    
   /**
     * 对连接池进行设置
     */
  
    connManager.setDefaultSocketConfig(socketConfig);
        RequestConfig requestConfig = RequestConfig.custom().setConnectionRequestTimeout(REQUEST_CONNECT_TIMEOUT)
                .setConnectTimeout(CONNECT_TIMEOUT).setSocketTimeout(SOCKET_TIMEOUT).build();
    

网上有些朋友遇到这个问题后说升级了httpClient版本就好了,不知道是不是因为升级以后socketTime的时间落在了20s以前就恰好解决了这个问题。

高版本的HttpClient还有一个检查空闲连接的机制,同样建议设置成15s。不好的地方是会增加检查的频次。就像老版本的isStaleConnectionCheckEnabled()。

connManager = new PoolingHttpClientConnectionManager(registry);
        // 可用空闲连接过期时间,重用空闲连接时会先检查是否空闲时间超过这个时间,如果超过,释放socket重新建立
        connManager.setValidateAfterInactivity(15000);
  • 一些背景知识和源码:

1: http协议建立和销毁都有代价【握手】。

2: 为了减少这种代价,出现了连接池。undertow/tomcat这种web容器都有连接池。 http连接池减少代价的方式为连接请求以后不销毁,下次可以复用。

3:为了保证复用的连接是有效的,不会过期,http在从连接池拿到连接以后会校验对象本身[CPoolProxy]和socket链路都是没问题的,才会复用。过期的【stale】或者是socket不通的连接,客户端是会抛弃掉旧连接,建立新的。

我们通过源码看一下这几个过程:

  1. 从http连接池拿出连接
  2. 检验有效性
  3. 发起请求
  • http使用连接池建立连接

httpClient是通过一系列的executor组成的request executor chain发起连接的。
我们要看的这部分代码在最后一个executor:MainClientExec中。省略无关代码。

 //1 : 创建一个ConnectionRequest,并将获取连接的操作封装在ConnectionRequest中。
        final ConnectionRequest connRequest = connManager.requestConnection(route, userToken);

// 2: 连接HttpClientConnection对象 最后发请求execute的参数
        final HttpClientConnection managedConn;
        try {
            final int timeout = config.getConnectionRequestTimeout();
            managedConn = connRequest.get(timeout > 0 ? timeout : 0, TimeUnit.MILLISECONDS);
        } catch(final InterruptedException interrupted) {
            Thread.currentThread().interrupt();
            throw new RequestAbortedException("Request aborted", interrupted);
        } catch(final ExecutionException ex) {
            Throwable cause = ex.getCause();
            if (cause == null) {
                cause = ex;
            }
            throw new RequestAbortedException("Request execution failed", cause);
        }

         //3: 检查是否过期 stale==过期  新版本httpClient默认关闭
           if (config.isStaleConnectionCheckEnabled()) {
            // validate connection
            if (managedConn.isOpen()) {
                this.log.debug("Stale connection check");
                if (managedConn.isStale()) {
                    this.log.debug("Stale connection detected");
                    managedConn.close();
                }
            }
        }

        final ConnectionHolder connHolder = new ConnectionHolder(this.log, this.connManager, managedConn);
        
         //4: 如果连接不可用 重新建立连接 establishRoute
         if (!managedConn.isOpen()) {
                    this.log.debug("Opening connection " + route);
                    try {
                        establishRoute(proxyAuthState, managedConn, route, request, context);
                    } catch (final TunnelRefusedException ex) {
                        if (this.log.isDebugEnabled()) {
                            this.log.debug(ex.getMessage());
                        }
                        response = ex.getResponse();
                        break;
                    }
                }
         
         //5: 为连接设置soTimeOut      
         final int timeout = config.getSocketTimeout();
                if (timeout >= 0) {
                    managedConn.setSocketTimeout(timeout);
                }
                
         //6: 发起http请求       
        response = requestExecutor.execute(request, managedConn, context);

  • 从连接池中拿出连接并校验连接的有效性

判断有效性需要判断两步。首先判断连接本身是否有效【CPoolEntry】,其次是连接本身是否能通。

  • 检查连接本身是否有效
try {
            //每一个route都有一个连接池,这里获取指定route的连接池
            final RouteSpecificPool<T, C, E> pool = getPool(route);
            E entry = null;
            //循环取,直到超时
            while (entry == null) {
                Asserts.check(!this.isShutDown, "Connection pool shut down");
                for (;;) {
                    //从连接池中去一个空闲的连接,优先取state相同的。state默认是null
                    entry = pool.getFree(state);
                    //如果没有符合的连接,则调出,创建一个新连接
                    if (entry == null) {
                        break;
                    }
                    //如果连接超时,则关闭
                    if (entry.isExpired(System.currentTimeMillis())) {
                        entry.close();
                    //如果是永久连接,且最近周期内没有检验,则校验连接是否可用。不可用的连接需要关闭    
                    } else if (this.validateAfterInactivity > 0) {
                        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;
                    }
                }
                //entry不为空,则修改连接池的参数,并返回。
                if (entry != null) {
                    this.available.remove(entry);
                    this.leased.add(entry);
                    onReuse(entry);
                    return entry;
                }

                // New connection is needed
                //获取池子的最大连接数,如果池子已经超过容量了,需要把超过的资源回收
                //如果池子中连接数没有超,空闲的连接还比较多,就先从别人的池子里借一个来用
                 ......

                //不能借,就自己动手了。新建并返回。
                final C conn = this.connFactory.create(route);
                entry = pool.add(conn);
                this.leased.add(entry);
                return entry;  
            }
            throw new TimeoutException("Timeout waiting for connection");
        } finally {
            this.lock.unlock();
        }
    }


  • 检查连接是否能通
   //org.apache.http.impl.conn.CPool
    protected boolean validate(final CPoolEntry entry) {
        return !entry.getConnection().isStale();
    }

	//org.apache.http.impl.BHttpConnectionBase
	//判断连接是否不可用(go down)
    public boolean isStale() {
	    //没有打开,即socket为空,则不可用
        if (!isOpen()) {
            return true;
        }
        try {
        //socket链路有了,测试链路是否可用
        //这里的测试方法是查看很短的时间内(这里是1ms),是否可以从输入流中读到数据
        //如果测试结果返回-1说明不可用
            final int bytesRead = fillInputBuffer(1);
            return bytesRead < 0;
        } catch (final SocketTimeoutException ex) {
	        //注意这里SocketTimeoutException时,认为是可用的
            return false;
        } catch (final IOException ex) {
            //有I/O异常,不可用
            return true;
        }
    }
  • 发起请求
 response = requestExecutor.execute(request, managedConn, context);
  • 参考:

1: http连接池+MainClientExec相关源码

2: 443原因分析