在项目中使用HttpClient可能是很普遍,尤其在当下微服务大火形势下,如果服务之间是http调用就少不了跟http客户端找交道.由于项目用户规模不同以及应用场景不同,很多时候可能不需要特别处理也.然而在一些高并发场景下必须要做一些优化.

项目是快递公司的快件轨迹查询项目,目前平均每小时调用量千万级别.轨迹查询以Oracle为主要数据源,Mongodb为备用,当Oracle不可用时,数据源切换到Mongodb.今年菜鸟团队加入后,主要数据迁移到了阿里云上,以Hbase为主要存储.其中Hbase数据查询服务由数据解析组以Http方式提供.原有Mongodb弃用,云上数据源变为主数据源,Oracle作为备用.当数据源切换以后,主要的调用方式也就变成了http方式.在第10月初第一轮双11压测试跑上,qps不达标.当然这个问题很好定位,因为十一假之间轨迹域组内已经进行过试跑,当时查的是oracle.十一假期回来后,只有这一处明显的改动,很容易定位到问题出现在调用上.但具体是云上Hbase慢,还是网络传输问题(Hbase是阿里云上的服务,轨迹查询项目部署在IDC机房).通过云服务,解析组和网络运维的配合,确定问题出现在应用程序上.在Http服务调用处打日志记录,发现以下问题:

可以看到每隔一段时间,就会有不少请求的耗时明显比其它的要高.

导致这种情况可能可能是HttpClient反复创建销毁造成引起来销,首先凭经验可能是对HttpClient进行了Dispose操作(Using(HttpClient client=new HttpClient){...})

如果你装了一些第三方插件,当你写的HttpClient没有被Using包围的时候会给出重构建议,建议加上Using或者手动dispose.然而实际中是否要dispose还要视情况而定,对于一般项目大家的感觉可能是不加也没有大问题,加了也还ok.但是实际项目中,一般不建议反复重新创建这个对象,关于HttpClient是否需要Dispose请看这里

在对这个问题的答案里,提问者指出微软的一些示例也是没有使用using的.下面一个比较热的回答指出HttpClient生命周期应该和应用程序生命周期一致,只要应用程序需要Http请求,就不应用把它Dispose掉.下面的一个仍然相对比较热的回答指出一般地,带有Dispose方法的对象都应当被dispose掉,但是HttpClient是一个例外.

当然以上只是结合自己的经验对一个大家都可能比较困惑的问题给出一个建议,实际上对于一般的项目,用还是不用Dispose都不会造成很大问题.本文中上面提到的问题跟HttpClient也没有关系,因为程序中使用的Http客户端是基于HttpWebRequest封装的.

问题排查及优化

经过查询相关资料以及同行的经验分享(这篇文章给了很大启发)

查看代码,request.KeepAlive = true;查询微软相关文档,这个属性其实是设置一个'Keep-alive'请求header,当时同事封装Http客户端的场景现场无从得知,然而对于本文中提到的场景,由于每次请求的都是同一个接口,因此保持持续连接显然能够减少反复创建tcp连接的开销.因此注释掉这一行再发布测试,以上问题便不复出现了!

当然实际中做的优化绝不仅仅是这一点,如果仅仅是这样,一句话就能够说完了,大家都记住以后就这样做就Ok了.实际上还参考了不少大家在实际项目中的经验或者坑.下面把整个HttpClient代码贴出来,下面再对关键部分进行说明.

 public static string Request(string requestUrl, string requestData, out bool isSuccess, string contentType = "application/x-www-form-urlencoded;charset=utf8")
{
string apiResult = "";
isSuccess = false;
if (string.IsNullOrEmpty(requestData))
{
return apiResult;
}
HttpWebRequest request = null;
HttpWebResponse response = null;
try
{
byte[] buffer = Encoding.UTF8.GetBytes(requestData);
request = WebRequest.Create($"{requestUrl}") as HttpWebRequest;
request.ContentType = "application/json";
request.Method = "POST";
request.ContentLength = buffer.Length;
request.Timeout =200;
request.ReadWriteTimeout = Const.HttpClientReadWriteTimeout
request.ServicePoint.Expect100Continue = false;
request.ServicePoint.UseNagleAlgorithm = false;
request.ServicePoint.ConnectionLimit = 2000
request.AllowWriteStreamBuffering = false;
request.Proxy = null; using (var stream = request.GetRequestStream())
{
stream.Write(buffer, 0, buffer.Length);
}
using (response = (HttpWebResponse)request.GetResponse())
{
string encoding = response.ContentEncoding;
using (var stream = response.GetResponseStream())
{
if (string.IsNullOrEmpty(encoding) || encoding.Length < 1)
{
encoding = "UTF-8"; //默认编码
} if (stream != null)
{
using (StreamReader reader = new StreamReader(stream, Encoding.GetEncoding(encoding)))
{
apiResult = reader.ReadToEnd();
//byte[] bty = stream.ReadBytes();
//apiResult = Encoding.UTF8.GetString(bty);
}
}
else
{
throw new Exception("响应流为null!");
}
}
}
isSuccess = true;
}
catch (Exception err)
{
isSuccess = false;
LogUtilities.WriteException(err);
}
finally
{
response?.Close();
request?.Abort();
} return apiResult;
}
  • 首先是TimeOut问题,不仅仅是在高并发场景下,实际项目中建议不管是任何场景都要设置它的值.在HttpWebRequest对象中,它的默认值是100000毫秒,也就是100秒.如果服务端出现问题,默认设置将会造成严重阻塞,对于普通项目也会严重影响用户体验.返回失败让用户重试也比这样长时间等待体验要好.

  • ReadWriteTimeout很多朋友可能没有接触过这个属性,尤其是使用.net 4.5里HttpClient对象的朋友.有过Socket编程经验的朋友可能会知道,socket连接有连接超时时间和传输超时时间,这里的ReadWriteTimeout类似于Socket编程里的传输超时时间.从字面意思上看,就是读写超时时间,防止数据量过大或者网络问题导致流传入很长时间都无法完成.当然在一般场景下大家可以完全不理会它,如果由于网络原因造成读写超时也很有可能造成连接超时.这里之所以设置这个值是由于实际业务场景决定的.大家可能已经看到,以上代码对于ReadWriteTimeout的设置并不像Timeout一样设置为一个固定值,而是放在了一个Const类中,它实际上是读取一个配置,根据配置动态决定值的大小.实际中的场景是这样的,由于压测环境无法完全模拟真实的用户访问场景.压测的时候都是使用单个单号进行轨迹查询,但是实际业务中允许用户一次请求查询最多多达数百个单号.一个单号的轨迹记录一般都是几十KB大小,如果请求的单号数量过多数量量就会极大增加长,查询时间和传输时间都会极大增加,为了保证双11期间大多数用户能正常访问,必要时会把这个时间设置的很小(默认是3000毫秒),让单次查询量大的用户快速失败.

以上只是一种备用方案,不得不承认,既然系统允许一次查询多个单号,因此在用户在没有达到上限之前所有的请求都是合法的,也是应该予以支持的,因此以上做法实际上有损用户体验的,然而系统的资源是有限的,要必要的时候只能牺牲特殊用户的利益,保证绝大多数用户的利益.双11已经渡过,实际中双11也没有改动以上配置的值,但是做为风险防范增加动态配置是必要的.

这里再多差一下嘴,就是关于ContentLength它的值是可以不设置的,不设置时程序会自动计算,但是设置的时候一定要设置字节数组的长度,而不是字符串的长度,因为包含中文时,根据编码规则的不同,中文字符可能占用两个字节或者更长字节长度.

最为重要的是,文档中说将 AllowWriteStreamBuffering 设置为 true 可能会在上传大型数据集时导致性能问题,因为数据缓冲区可能会使用所有可用内存。由于发送的请求仅仅是单号,数据量很小,并且很少有用户一个单号反复查询的需求.加上可能会有副作用.这里设置为false.

  • request.Proxy = null;这里是参考了一个一位网友的文章,里面提到默认的Proxy导致超时怪异行为.由于解决问题是在10月份,据写这篇文章已经有一段时间了,因此再寻找时便找不到这篇文章了.有兴趣的朋友可以自己搜索一下.

很多朋友可能会关心,通过以上配置到底有没有解决问题.实际中以上配置后已经经历了双11峰值qps过万的考验.下面给出写本文时候请求耗时的监控

可以看到,整体上请求耗时比较平稳.

可能看了这个图,有些朋友还是会有疑问,通过上面日志截图可以看到,除了耗时在100ms以上的请求外,普通的耗时在四五十毫秒的还是有很多的,但是下面这个截图里都是在10到20区间浮动,最高的也就30ms.这其实是由于在压测的过程中,发现Hbase本身也有不稳定的因素(大部分请求响应耗时都很平稳,但是偶尔会有个别请求娄千甚至数万毫秒(在监控图上表现为一个很突兀的线,一般习惯称为毛刺),这在高并发场景下是不能接受的,问题反馈以后阿里云对Hbase进行了优化,优化以后耗时也有所下降.)

HttpClient在高并发场景下的优化实战的更多相关文章

  1. Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%。再往后,每提高0.1%,优化难度成指数级增长了。哪怕是千分之一,也直接影响用户体验,影响每天上万张机票的销售额。 在高并发场景下,提供了保证线程安全的对象、方法。比如经典的ConcurrentHashMap,它比起HashMap,有更小粒度的锁,并发读写性能更好。线程安全的StringBuilder取代S

    Qunar机票技术部就有一个全年很关键的一个指标:搜索缓存命中率,当时已经做到了>99.7%.再往后,每提高0.1%,优化难度成指数级增长了.哪怕是千分之一,也直接影响用户体验,影响每天上万张机 ...

  2. 高并发场景下System.currentTimeMillis()的性能问题的优化 以及SnowFlakeIdWorker高性能ID生成器

    package xxx; import java.sql.Timestamp; import java.util.concurrent.*; import java.util.concurrent.a ...

  3. 高并发场景下System.currentTimeMillis()的性能问题的优化

    高并发场景下System.currentTimeMillis()的性能问题的优化 package cn.ucaner.alpaca.common.util.key; import java.sql.T ...

  4. C++高并发场景下读多写少的优化方案

    概述 一谈到高并发的优化方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也能很大的影响整体性能,本文从单模块下读 ...

  5. 【转】记录PHP、MySQL在高并发场景下产生的一次事故

    看了一篇网友日志,感觉工作中值得借鉴,原文如下: 事故描述 在一次项目中,上线了一新功能之后,陆陆续续的有客服向我们反应,有用户的个别道具数量高达42亿,但是当时一直没有到证据表示这是,确实存在,并且 ...

  6. C++高并发场景下读多写少的解决方案

    C++高并发场景下读多写少的解决方案 概述 一谈到高并发的解决方案,往往能想到模块水平拆分.数据库读写分离.分库分表,加缓存.加mq等,这些都是从系统架构上解决.单模块作为系统的组成单元,其性能好坏也 ...

  7. MySQL在大数据、高并发场景下的SQL语句优化和"最佳实践"

    本文主要针对中小型应用或网站,重点探讨日常程序开发中SQL语句的优化问题,所谓“大数据”.“高并发”仅针对中小型应用而言,专业的数据库运维大神请无视.以下实践为个人在实际开发工作中,针对相对“大数据” ...

  8. 高并发场景下System.currentTimeMillis()的性能优化

    一.前言 System.currentTimeMillis()的调用比new一个普通对象要耗时的多(具体耗时高出多少我也不知道,不过听说在100倍左右),然而该方法又是一个常用方法, 有时不得不使用, ...

  9. 【高并发】高并发环境下如何优化Tomcat配置?看完我懂了!

    写在前面 Tomcat作为最常用的Java Web服务器,随着并发量越来越高,Tomcat的性能会急剧下降,那有没有什么方法来优化Tomcat在高并发环境下的性能呢? Tomcat运行模式 Tomca ...

随机推荐

  1. 在Spring Boot项目中使用Spock测试框架

    本文首发于个人网站:在Spring Boot项目中使用Spock测试框架 Spock框架是基于Groovy语言的测试框架,Groovy与Java具备良好的互操作性,因此可以在Spring Boot项目 ...

  2. 玩转 RTC时钟库 DS3231

    1.前言     接着博主的上一篇 玩转 RTC时钟库 + DS1302,这一篇我们重点讲解DS3231时钟模块.没有看过上一篇的同学,麻烦先去阅读一下,因为很多理论基础已经在上一篇做了详细讲解,这里 ...

  3. 深入理解JavaScript中的作用域、作用域链和闭包

    版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明.本文链接:https://blog.csdn.net/qappleh/article/detai ...

  4. Leetcode Tags(8)Binary Search

    一.475. Heaters 输入: [1,2,3],[2] 输出: 1 解释: 仅在位置2上有一个供暖器.如果我们将加热半径设为1,那么所有房屋就都能得到供暖. 输入: [1,2,3,4],[1,4 ...

  5. unity单例封装

    # 1.前言本篇主要针对Unity单例模式,完成一个可以重复继承使用的抽象类,减少重复的工作与代码.同时,对存在的多种单例进行优劣分析.# 2.Unity单例问题分析## 2.1 单例原则单例要满足以 ...

  6. django-数据库之连接数据库

    1.连接数据库出现的一些问题 基本目录如下: 首先我们pip install pymysql 然后在项目中,进行配置settings.py: 然后在__init__.py中进行输入: 启动服务器: 报 ...

  7. SpringBoot整合RabbitMq(二)

           本文序列化和添加package参考:https://www.jianshu.com/p/13fd9ff0648d RabbitMq安装 [root@topcheer ~]# docker ...

  8. 基于jquery,php实现AJAX长轮询(LongPoll),类似推送机制

    HTTP是无状态.单向的协议,用户只能够通过客服端向服务器发送请求并由服务器处理发回一个响应.若要实现聊天室.WEBQQ.在线客服.邮箱等这些即时通讯的应用,就要用到“ 服务器推送技术(Comet)” ...

  9. Charles抓取HTTPS数据包方法

    设置代理端口8888 ssl代理设置 允许所有地址连接 手机获取证书之前,先在电脑安装证书,需要信任.help-->ssl-proxying-->Install Charles Root ...

  10. HashMap 中的容量与扩容实现,细致入微,值的一品!

    前言 开心一刻 巴闭,你的脚怎么会有味道,我要闻闻看是不是好吃的,嗯~~爸比你的脚臭死啦!! …… 高手过招,招招致命 JDK1.8 中 HashMap 的底层实现,我相信大家都能说上来个 一二,底层 ...