高并发 HTTP 请求实践

当今,正处于互联网高速发展的时代,每个人的生活都离不开互联网,互联网已经影响了每个人生活的方方面面。我们使用淘宝、京东进行购物,使用微信进行沟通,使用美图秀秀进行拍照美化等等。而这些每一步的操作下面,都离不开一个技术概念HTTP(Hypertext Transfer Protocol,超文本传输协议)。

举个:chestnut:,当我们打开京东APP的时候,首先进入的是开屏页面,然后进入首页。在开屏一般是广告,而首页是内容相关,包括秒杀,商品推荐以及各个tag页面,而各个tag也有其对应的内容。当我们在进入开屏之前或者开屏之后(这块依赖于各个app的技术实现),会向后端服务发送一个http请求,这个请求会带上该页面广告位信息,向后端要内容,后端根据广告位的配置,挑选一个合适的广告或者推荐商品返回给APP端进行展示。在这里,为了描述方便,后端当做一个简单的整体,实际上,后端会有非常复杂的业务调度,比如获取用户画像,广告定向,获取素材,计算坐标,返回APP,APP端根据坐标信息,下载素材,然后进行渲染,从而在用户端进行展示,这一切都是秒级甚至毫秒级响应,一个高效的HTTP Client在这里就显得尤为重要,本文主要从业务场景来分析,如何实现一个高效的HTTP Client。

一、概念

当我们需要模拟发送一个http请求的时候,往往有两种方式:

1、通过浏览器

2、通过 curl 命令进行发送请求

如果我们在大规模高并发的业务中,如果使用curl来进行http请求,其效果以及性能是不能满足业务需求的,这就引入了另外一个概念 libcurl。

二、实现

在开始实现client发送http请求之前,我们先理解两个概念:

同步请求

异步请求

客户端把请求发送给服务器之后,不会等待服务器返回,而是去做其他事情,待服务器处理完成之后,通知客户端该事件已经完成,客户端在获取到通知后,将服务器处理后的结果返回给调用方。

通过这俩概念,就能看出,异步在实现上,要比同步复杂的多。同步,即我们简单的等待处理结果,待处理结果完成之后,再返回调用方。而对于异步,往往在实现上,需要各种回调机制,各种通知机制,即在处理完成的时候,需要知道是哪个任务完成了,从而通知客户端去处理该任务完成后剩下的逻辑。

下面,我们将从代码实现的角度,来更深一步的理解libcurl在实现同步和异步请求操作上的区别,从而更近异步的了解同步和异步的实现原理。

同步

使用libcurl完成同步http请求,原理和代码都比较简单,主要是分位以下几个步骤:

1、初始化easy handle

2、在该easy handle上设置相关参数,在本例中主要有以下几个参数

  • CURLOPT_URL,即请求的url

  • CURLOPT_WRITEFUNCTION,即回调函数,将http server返回数据写入对应的地方

  • CURLOPT_FOLLOWLOCATION,是否获取302跳转后的内容

  • CURLOPT_POSTFIELDSIZE,此次发送的数据大小

  • CURLOPT_POSTFIELDS,此次发送的数据内容

  • 更多的参数设置,请参考libcurl官网

3、 curl_easy_perform,调用该函数发送http请求,并同步等待返回结果

4、 curl_easy_cleanup,释放步骤一中申请的easy handle资源

代码实现(easy_curl.cc)

编译

结果

异步

接 触过网络编程的读者,都或多或少的了解多路复用的原理。 IO多路复用在Linux下包括了三种, select 、 poll 、 epoll ,抽象来看,他们功能是类似的,但具体细节各有不同:首先都会对一组文件描述符进行相关事件的注册,然后阻塞等待某些事件的发生或等待超时。

在使用Libcurl进行异步请求,从上层结构来看,简单来说,就是对easy handle 和 multi 接口的结合使用。其中,easy handle底层也是一个socket,multi接口,其底层实现也用的是epoll,那么我们如何使用easy handle和multi接口,来实现一个高性能的异步http 请求client呢?下面我们将使用代码的形式,使得读者能够进一步了解其实现机制。

multi 接口的使用是在easy 接口的基础之上,将easy handle放到一个队列中(multi handle),然后并发发送请求。与easy 接口相比,multi接口是一个异步的,非阻塞的传输方式。

multi接口的使用,主要有以下几个步骤:

  • curl_multi _init初始化一个multi handler对象

  • 初始化多个easy handler对象,使用curl_easy_setopt进行相关设置

  • 调用curl_multi _add_handle把easy handler添加到multi curl对象中

  • 添加完毕后执行curl_multi_perform方法进行并发的访问

  • 访问结束后curl_multi_remove_handle移除相关easy curl对象,先用curl_easy_cleanup清除easy handler对象,最后curl_multi_cleanup清除multi handler对象。

http_request.h

http_request.cc

main.cc

至此,我们已经可以使用libcurl来实现并发发送http请求,当然这个只是一个简单异步实现功能,更多的功能,还需要读者去使用libcurl中的其他功能去实现,此处留给读者一个问题(这个问题,也是笔者项目中使用的一个功能,该项目已经线上稳定运行4年,日请求量在20E ),业务需要,某一个请求需要并发发送给指定的几家,即该请求,需要并发发送给几个http server,在一个特定的超时时间内,获取这几个http server的返回内容,并进行处理,那么这种功能应该如何使用libcurl来实现呢?透露下,可以使用libcurl的另外一个参数CURLOPT_PRIVATE。

三、性能对比

至此,我们已经基本完成了 高性能http 并发功能的设计,那么到底性能如何呢?笔者从 以下几个角度来做了测试:

1、串行发送同步请求

2、多线程情况下,发送同步请求(此处线程为4个,笔者测试的服务器为4C)

3、使用multi接口

4、使用multi接口,并复用其对应的easy handle

5、使用dns cache(对easy handle设置CURLOPT_DNS_CACHE_TIMEOUT),即不用每次都进行dns解析

方法

平均耗时(ms)

最大耗时(ms)

串行 同步

21.381

30.617

多线程同步

4.331

16.751

multi接口

1.376

11.974

multi接口 连接复用

0.352

0.748

multi 接口使用dns cache

0 .381

0.731

四、一点心得

libcurl是一个高性能,较易用的HTTP client,在使用其直接,一定要对其接口功能进行详细的了解,否则很容易入坑,犹记得在18年中的时候,上 线了某一个功能,会偶现coredump(在上线之前,也进行了大量的性能测试,都没有出现过一次coredump),为了分析这个原因,笔者将服务的代码一直精简精简,然后模拟测试,缩小coredump定位范围,最终发现,只有在超时的时候,才会导致coredump,这就说明了为什么测试环境没有coredump,而线上会产生coredump,这是因为线上的超时时间设置的是5ms,而测试环境超时时间是20ms,这就基本把原因定位到超时导致的coredump。

然后,分析libcurl源码,发送时一个libcurl的参数设置导致coredump,至此,笔者耗费了23个小时,问题才得以解决。

上一篇:Uncaught (in promise) TypeError: handle is not a function,监听函数中是不是不能写函数呀


下一篇:MFC多线程同步