在项目中使用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. Python编程系列---Python中装饰器的几种形式及万能装饰器

    根据函数是否传参  是否有返回值 ,可以分析出装饰器的四种形式: 形式一:无参无返回值 def outer(func): def wrapper(): print("装饰器功能1" ...

  2. Linux Centos7 基于Docker 搭建 Nexus私服搭建

    创建Blob Stores[本地文件存储目录,统一管理] 1.设置名称和工作路径: ps[注意事项]: 1.storage name:自定义名称 2.storage path:存储路径,默认[/nex ...

  3. 详解 Redis 内存管理机制和实现

    Redis是一个基于内存的键值数据库,其内存管理是非常重要的.本文内存管理的内容包括:过期键的懒性删除和过期删除以及内存溢出控制策略. 最大内存限制 Redis使用 maxmemory 参数限制最大可 ...

  4. 个人记账app(一)需求设计

    时间如流水,只能流去不流回. 学历代表你的过去,能力代表你的现在,学习能力代表你的将来. 学无止境,精益求精. 一.开发背景 Android应用市场记账的app那么多,我为什么还要再开发一个呢?重复造 ...

  5. 实用脚本awk

    非常实用的awk 有时候需要去服务器下载几个日志 日志太多,翻滚起来很麻烦,操作又慢又复杂. 可以使用这个下载最新的两个文件 ls -lt | head -3 | awk -F ' ' '{if(NR ...

  6. 一个简单的Post Get请求

    WWW请求 using System; using System.Collections; using System.Collections.Generic; using UnityEngine; u ...

  7. Spring Cloud alibaba网关 sentinel zuul 四 限流熔断

    spring cloud alibaba 集成了 他内部开源的 Sentinel 熔断限流框架 Sentinel 介绍 官方网址 随着微服务的流行,服务和服务之间的稳定性变得越来越重要.Sentine ...

  8. Linux Cannot allocate memory问题

    查找了一下相关文档,发现这个错误的含义其实就是像它自己说的,没法分配内存了. The problem is inherent with the way Java allocates memory wh ...

  9. C语言知识体系

    吾尝终日而思矣,不如须臾之所学也: 吾尝跂而望矣,不如登高之博见也. 登高而招,臂非加长也,而见者远: 顺风而呼,声非加疾也,而闻者彰. 假舆马者,非利足也,而致千里: 假舟楫者,非能水也,而绝江河. ...

  10. 学习笔记52_mongodb增删改查

    使用use db1作为当前数据库 如果没有db1,会自动创建 使用switch db2,当前数据库切换为db2 使用show dbs,显示当前所有数据库 使用show collection ,显示当前 ...