前言

HTTPClient大家应该都很熟悉,一个很好的抓网页,刷投票或者刷浏览量的工具。但是还有一项非常重要的功能就是外部接口调用,比如说发起微信支付,支付宝退款接口调用等;最近我们在这个工具上栽了一个大跟头,不怕大家笑话,拿出来跟大家分享一下;
过程描述
项目代码比较复杂,我为了直达问题,单独写了程序来说明;
我这里先重复一下导致问题的过程:程序源自于从.NET到Java的重构,开发使用了httpclient来调用微信支付的接口,设置了Httpclient的超时参数,为了提高性能,还遵循httpclient的推荐做法,将httpclient做成了单例;httpclient其他的参数都没有调整,使用的是默认参数;最终这种配置没能扛住网络的抖动,服务发生了雪崩。本篇博客也是“一个隐藏在支付系统很长时间的雷”的续篇;
 

缺陷复现

相信你对这个过程有很多疑点,下面我简化代码说一下这个问题;
我们现在要做的实验(demo)是这样的一个架构(先有架构才能显示出你是一名高级工程师,但是请原谅我简化的有点太简单)。
 
使用httpclient做客户端,然后使用多线程发起HTTP接口调用。为了模拟故障(包括网络故障和服务器服务故障),我们在服务器的接口sleep一段时间,然后观察服务器日志,如果客户端是多并发访问,httpclient是正常的。但如果客户端是一个一个请求过来的,那就说明使用httpclient的方式有问题。
好了,思路就是这样,我们开始通过代码来说明情况;
 
step1 服务器端程序
为了避免配置tomcat,我直接使用embed jetty,来启动一个8888端口的服务,这个服务什么都不做,就打印一下日志,然后sleep一下,出去时,再打印一次日志;一共两个类(如何引入maven依赖我就不写了);
public class JettyServerMain {
public static void main(String[] args) throws Exception {
Server server = new Server(8888); server.setHandler(new HelloHandler()); server.start();
server.join();
}
} class HelloHandler extends AbstractHandler { /**
* 作为测试,在这个方法故意sleep 3秒,然后返回hello;
*/
@Override
public void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response)
throws IOException, ServletException {
long threadId = Thread.currentThread().getId();
Log.getLogger(this.getClass()).info("threadId="+threadId+" come in");
try {
Thread.sleep(3000);
}
catch(Exception e) {
e.printStackTrace();
} response.setStatus(HttpServletResponse.SC_OK);
PrintWriter out = response.getWriter(); out.println("hello+"+threadId); baseRequest.setHandled(true);
Log.getLogger(this.getClass()).info("threadId="+threadId+" finish");
}
}

  

 
step2 简化版httpclient(V1)
我们先写第一版的httpclient,即先通过httpclient调用一下刚才的程序,看是否好用;代码如下:
public class HTTPClientV1 {
public static void main(String argvs[]){
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
// 创建Get请求
HttpGet httpGet = new HttpGet("http://localhost:8888");
// 响应模型
CloseableHttpResponse response = null;
try {
// 由客户端执行(发送)Get请求
response = httpClient.execute(httpGet);
// 从响应模型中获取响应实体
HttpEntity responseEntity = response.getEntity(); if (responseEntity != null) {
System.out.println("响应内容为:" + EntityUtils.toString(responseEntity));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (httpClient != null) {
httpClient.close();
}
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

  

step3 复用httpclient(V2)
我们从httpclient官方看到,推荐多线程复用httpclient;
 
因此,多线程复用httpclient单例,模拟同时发起10个请求;
public static void main(String argvs[]){
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
for(int i=0;i<10;i++) {
new Thread(new Runnable() {
@Override
public void run() {
GetRequest(httpClient);
}
}).start();
}
}

  

此时,应该允许一下看看效果;首选启动jetty,运行JettyServerMain
22:48:46.618 INFO  log: Logging initialized @897ms
22:48:46.655 INFO Server: jetty-9.2.14.v20151106
22:48:47.051 INFO ServerConnector: Started ServerConnector@5136ac92{HTTP/1.1}{0.0.0.0:8888}
22:48:47.052 INFO Server: Started @1346ms

  

运行多线程请求HTTPClientV2,服务器端打印日志如下:
22:49:59.056 INFO  HelloHandler: threadId=15 come in
22:49:59.057 INFO HelloHandler: threadId=14 come in
22:50:02.080 INFO HelloHandler: threadId=14 finish
22:50:02.080 INFO HelloHandler: threadId=15 finish
22:50:02.144 INFO HelloHandler: threadId=15 come in
22:50:02.144 INFO HelloHandler: threadId=19 come in
22:50:05.144 INFO HelloHandler: threadId=19 finish
22:50:05.144 INFO HelloHandler: threadId=15 finish
22:50:05.148 INFO HelloHandler: threadId=19 come in
22:50:05.148 INFO HelloHandler: threadId=14 come in
22:50:08.149 INFO HelloHandler: threadId=19 finish
22:50:08.149 INFO HelloHandler: threadId=14 finish
22:50:08.153 INFO HelloHandler: threadId=15 come in
22:50:08.153 INFO HelloHandler: threadId=19 come in
22:50:11.153 INFO HelloHandler: threadId=19 finish
22:50:11.153 INFO HelloHandler: threadId=15 finish
22:50:11.158 INFO HelloHandler: threadId=14 come in
22:50:11.158 INFO HelloHandler: threadId=19 come in
22:50:14.158 INFO HelloHandler: threadId=19 finish
22:50:14.158 INFO HelloHandler: threadId=14 finish

  

是不是感觉到有点惊奇?但从服务器端看,客户端在同一时间,只有2个请求过来,这两个请求完事之后,才会发下面的两个请求;如果服务器端sleep的不是3秒,而是10秒或者好几分钟,客户端会怎样?
step4 增加超时设置(V3)
能够想到超时,说明你一定是有一定技术储备的程序员了。核心代码如下:
// 创建Get请求
HttpGet httpGet = new HttpGet("http://localhost:8888");
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(2000)
.setConnectTimeout(2000)
.build();
httpGet.setConfig(requestConfig);

  

再跑一次,看看服务器端的输出
22:55:32.751 INFO  HelloHandler: threadId=15 come in
22:55:32.751 INFO HelloHandler: threadId=14 come in
22:55:34.758 INFO HelloHandler: threadId=19 come in
22:55:34.759 INFO HelloHandler: threadId=21 come in
22:55:35.751 INFO HelloHandler: threadId=15 finish
22:55:35.751 INFO HelloHandler: threadId=14 finish
22:55:36.761 INFO HelloHandler: threadId=23 come in
22:55:36.767 INFO HelloHandler: threadId=14 come in
22:55:37.760 INFO HelloHandler: threadId=19 finish
22:55:37.761 INFO HelloHandler: threadId=21 finish
22:55:38.764 INFO HelloHandler: threadId=15 come in
22:55:38.769 INFO HelloHandler: threadId=19 come in
22:55:39.761 INFO HelloHandler: threadId=23 finish
22:55:39.767 INFO HelloHandler: threadId=14 finish
22:55:40.766 INFO HelloHandler: threadId=21 come in
22:55:40.771 INFO HelloHandler: threadId=23 come in
22:55:41.764 INFO HelloHandler: threadId=15 finish
22:55:41.770 INFO HelloHandler: threadId=19 finish
22:55:43.766 INFO HelloHandler: threadId=21 finish
22:55:43.771 INFO HelloHandler: threadId=23 finish

  

可以看到,因为有2秒的超时,所以在发起请求2秒后,服务器接收到后来的2个请求,此时服务器同时处理的请求有4个;为什么同时发起的有10个请求,服务器却做多同时只接收到4个请求呢?V3完整代码如下:
import java.io.IOException;

import org.apache.http.HttpEntity;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils; /**
* Date: 2019/5/22
* TIME: 21:25
* HTTPClient
* 1、共享httpclient
* 2、增加超时时间
* @author donlianli
*/
public class HTTPClientV3 {
public static void main(String argvs[]){
// 获得Http客户端(可以理解为:你得先有一个浏览器;注意:实际上HttpClient与浏览器是不一样的)
CloseableHttpClient httpClient = HttpClientBuilder.create().build(); for(int i=0;i<10;i++) {
new Thread(new Runnable() {
@Override
public void run() {
GetRequest(httpClient);
}
}).start();
}
} private static void GetRequest(CloseableHttpClient httpClient) {
// 创建Get请求
HttpGet httpGet = new HttpGet("http://localhost:8888");
RequestConfig requestConfig = RequestConfig.custom()
.setSocketTimeout(2000)
.setConnectTimeout(2000)
.build();
httpGet.setConfig(requestConfig);
// 响应模型
CloseableHttpResponse response = null;
try {
// 由客户端执行(发送)Get请求
response = httpClient.execute(httpGet);
// 从响应模型中获取响应实体
HttpEntity responseEntity = response.getEntity(); if (responseEntity != null) {
System.out.println("响应内容为:" + EntityUtils.toString(responseEntity));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
} }
}

  

这就是httpclient没有设置默认线程池的后果,赶快看看你们的代码是不是也有这个问题;
说到这边,有人说是因为连接池没有更改大小导致,其实是错误的,这个单独更改MaxTotal是不管用的,必须同时更改DefaultMaxPerRoute这个默认配置;
我们可以这样理解这两个参数,如果你访问的是一个域名,比如访问的是微信支付域名api.mch.weixin.qq.com,那么此时可以同时发起的请求受这两个参数影响。httpclient首先会从检查请求数是否超过DefaultMaxPerRoute,如果没有,则会再检查连接池中总连接数是否会超过MaxTotal大小。这两项都没有超过,才会新建立一个连接,反之则会等待连接池中其他线程释放。因此,同一时间向同一域名发起的总请求数<=DefaultMaxPerRoute<=MaxTotal;如果你使用httpclient不止向一个域名发起连接请求,那maxTotal会作为一个总的开关,来控制所有已经建立的网络连接数量;
还是上面的代码,如果想同时发起超过10个请求,就应该设置DefaultMaxPerRoute>10。代码(V5)如下:

 public static void main(String argvs[]){
PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
// 总连接数
cm.setMaxTotal(200);
// 这个至少要大于10
cm.setDefaultMaxPerRoute(20);
CloseableHttpClient httpClient = HttpClientBuilder.create()
.setConnectionManager(cm).build(); for(int i=0;i<10;i++) {
new Thread(new Runnable() {
@Override
public void run() {
GetRequest(httpClient);
}
}).start();
}
}

  

扩展延伸

一、httpclient默认采用了连接池来管理连接,所以,如果采用这种策略,那么connect_timeout参数一般没什么用,因为本身连接是之前已经建立好的,如果你本身没有设置等待从连接池中获取连接的超时时间(RequestConfig.ConnectionRequestTimeout),那么你设置的超时时间是根本不管用的,因为那个SocketTimeout是获取网络连接之后请求发出之后才会生效的参数;
二、其实httpclient是使用了池管理技术,连接数据库使用的dbcp,c3p0,阿里的druid,连接redis使用的jedis都采用了池技术,这3个参数在使用了池管理的组件中都存在。如果这些组件,没有设置这几个参数,一样会存在类似的问题;关于池管理技术,如果有空,我会再单独写一篇文章;
 
好了,整个过程已经复现完毕,三个重要参数也都解释的应该清楚;更多的参数设置及其含义,其实还能讲好几篇,我这里就不再细讲了,大家可以参考:https://blog.csdn.net/lovomap151/article/details/78879904
如果仍然有疑问,可以公众号(猿界汪汪队)私信我;所有用到的代码,可以在https://github.com/donlianli/easydig/tree/master/src/main/java/com/donlian/httpclient/defaultRoute 找到;
 
PS:其实在我们的支付项目中,这个问题隐藏的更深,支付和退款的超时不一样并且公用了同一个httpclient,退款把所有httpclient的连接都占用完毕导致用户无法支付;我们访问微信使用的https协议,https协议是构建在http协议之上的,微信的退款是双向认证,不同的商户证书是不一样的。太复杂,至今不敢相信我们竟然在没有现场的情况下发现这个缺陷;
 
其他故障总结案例:
 
更多最新案例分析,请关注猿界汪汪队

做支付遇到的HttpClient大坑的更多相关文章

  1. 使用 ping++做支付的流程

    获取支付凭据 /// <summary> /// 获取支付凭据 /// </summary> /// <param name="model">& ...

  2. 干货 | 高耦合场景下,Trip.com如何做支付设计与落地

    干货 | 高耦合场景下,Trip.com如何做支付设计与落地 https://mp.weixin.qq.com/s/VR9NTR3RpKVfmUPcwgMABg 原创 Ryann Liu 携程技术 2 ...

  3. HttpClient学习(四)—— 关于Http

    一.Http状态码 状态码分类 100 ~ 199 信息提示 200 ~ 299 成功 300 ~ 399 重定向 400 ~ 499 客户端错误 500 ~ 599 服务端错误 常见状态码 200 ...

  4. 微信支付开发h5调用

    这两天做微信支付开发.碰到大坑.纠结死我了.好不容做完. 后台java:直接上代码:注意区分前后端的变量大小写... @RequestMapping(value = "/index" ...

  5. APP支付报错ALI40247处理方案!

    简直日狗!这里要吐槽支付宝: 1.支付宝文档太复杂,分类虽然详细,但是我找不到app支付 对应服务端的demo 2.提供下载的sdk都是全整合的 用下来都是一条龙服务,还有一些客户端(app)的请求也 ...

  6. Phonegap 之 iOS银联在线支付(js调用ios端银联支付控件)

    Phonegap项目,做支付的时候,当把网站打包到ios或android端成app后,在app上通过wap调用银联在线存在一个问题: 就是当从银联支付成功后,再从服务器返回到app客户端就很难实现. ...

  7. JAVA微信支付接口开发——支付

    微信支付接口开发--支付 这几天在做支付服务,系统接入了支付宝.微信.银联三方支付接口.个人感觉支付宝的接口开发较为简单,并且易于测试. 关于数据传输,微信是用xml,所以需要对xml进行解析. 1. ...

  8. 微信 JS API 支付教程

    最近一个项目中用到了微信开发,之前没有做过支付相关的东西,算是拿这个来练练手,刚开始接触支付时候很懵逼,加上微信支付开发文档本来就讲得不清楚,我是彻底蒙圈了,参考了很多代码之后,算是有一点思路了. 用 ...

  9. 【SSH网上商城项目实战26】完成订单支付后的短信发送功能

     转自: https://blog.csdn.net/eson_15/article/details/51475431 上一节我们使用了Java mail完成了给买家发送邮件的功能,还遗留一个功能,就 ...

随机推荐

  1. Java程序猿的JavaScript学习笔记(9—— jQuery工具方法)

    计划按例如以下顺序完毕这篇笔记: Java程序猿的JavaScript学习笔记(1--理念) Java程序猿的JavaScript学习笔记(2--属性复制和继承) Java程序猿的JavaScript ...

  2. WINDOWS XP 系统显示乱码的解决方法(修改注册表,使用正常字体)

            一位同事的计算机进入WINDOWS XP系统后,电脑里的所有汉字全部显示乱码,很多办公文档无法打开而影响工作.因为第一次遇到这种问题,当然首先是百度解决了,搜索了相关的信息后找到了答案 ...

  3. Getting started with ASP.NET Core MVC and Visual Studio

    This tutorial will teach you the basics of building an ASP.NET Core MVC web app using Visual Studio ...

  4. 大数据DDos检测——DDos攻击本质上是时间序列数据,t+1时刻的数据特点和t时刻强相关,因此用HMM或者CRF来做检测是必然! 和一个句子的分词算法CRF没有区别!

    DDos攻击本质上是时间序列数据,t+1时刻的数据特点和t时刻强相关,因此用HMM或者CRF来做检测是必然!——和一个句子的分词算法CRF没有区别!注:传统DDos检测直接基于IP数据发送流量来识别, ...

  5. 负载均衡获得真实源IP的6种方法

    除了X-FORWARD-FOR,负载均衡中获得真实源IP的方法还有很多种. 本文抛砖引玉,主要介绍获得真实源IP的多种方法,而不是具体配置. 负载均衡获得真实IP的方法有很多种,将形成专题文章. 本文 ...

  6. Meson使用

    安装 Meson是基于python3实现,至少需要python3.5才能运行,默认采用ninja作为后端.在Ubuntu下最简单的是通过pip3安装 $ sudo apt-get install py ...

  7. 大数据查询——HBase读写设计与实践--转

    背景介绍 本项目主要解决 check 和 opinion2 张历史数据表(历史数据是指当业务发生过程中的完整中间流程和结果数据)的在线查询.原实现基于 Oracle 提供存储查询服务,随着数据量的不断 ...

  8. MD5三种方法的学习总结

    MD5百度百科 MD5即Message-Digest Algorithm 5(信息-摘要算法5),用于确保信息传输完整一致.是计算机广泛使用的杂凑算法之一(又译摘要算法.哈希算法),主流编程语言普遍已 ...

  9. Several Ideas on Perl List Context

    According to Beginning Perl Book published by Tsinghua Pub., the list context appears when you are t ...

  10. 基于jQuery封装一个瀑布流插件

    /*封装一个瀑布流插件*/ (function($){ $.fn.WaterFall = function(){ /*这是你初始化 调用这个方法的时候的 那个jquery选着到的dom对象 this* ...