后端服务开发中经常会有并发请求的需求,比如你需要获取10家供应商的带宽数据(每个都提供不同的url),然后返回一个整合后的数据,你会怎么做呢?
在PHP中,最直观的做法foreach遍历urls,并保存每个请求的结果即可,那么如果供应商提供的接口平均耗时5s,你的这个接口请求耗时就达到了50s,这对于追求速度和性能的网站来说是不可接受的。
这个时候你就需要并发请求了。
PHP请求
PHP是单进程同步模型,一个请求对应一个进程,I/O是同步阻塞的。通过nginx/apache/php-fpm等服务的扩展,才使得PHP提供高并发的服务,原理就是维护一个进程池,每个请求服务时单独起一个新的进程,每个进程独立存在。
PHP不支持多线程模式和回调处理,因此PHP内部脚本都是同步阻塞式的,如果你发起一个5s的请求,那么程序就会I/O阻塞5s,直到请求返回结果,才会继续执行代码。因此做爬虫之类的高并发请求需求很吃力。
那怎么来解决并发请求的问题呢?除了内置的file_get_contents和fsockopen请求方式,PHP也支持cURL扩展来发起请求,它支持常规的单个请求:PHP cURL请求详解,也支持并发请求,其并发原理是cURL扩展使用多线程来管理多请求。
PHP并发请求
我们直接来看代码demo:
1 | // 简单demo,默认支持为GET请求 |
在该并发请求中,先创建一个批处理句柄,然后将url的cURL句柄添加到批处理句柄中,并不断查询批处理句柄的执行状态,当执行完成后,获取返回的结果。
1 | /** 函数作用:返回一个新cURL批处理句柄 |
本例中使用到的预定义常量:
CURLM_CALL_MULTI_PERFORM: (int) -1
CURLM_OK: (int) 0
PHP并发请求耗时对比
第一次请求使用上面的curl_multi_init方法,并发请求105次。
第二次请求使用传统的foreach方法,遍历105次使用curl_init方法请求。
实际的请求耗时结果为:
1 | 方案1: 2.35s |
刨除download的约765ms耗时,单纯的请求耗时优化达到了39.83/1.58达到了25倍,如果继续刨除建连相关的耗时,应该会更高。这其中的耗时:
- 方案1:最慢的一个接口达到了1.58s
- 方案2:105个接口的平均耗时是384ms
这个测试的请求是我的环境的内部接口,所以耗时很短,实际爬虫请求环境优化会更明显。
注意项
并发数限制
curl_multi会消耗很多的系统资源,在并发请求时并发数有一定阈值,一般为512,是由于CURL内部限制,超过最大并发会导致失败。
在我做的测试中,发起2000个相同的请求,并输出每一个请求的响应结果。测试结果2000个请求共有366个成功,前331个均成功,在331-410次序之间共有35个成功的,第410个请求之后全部失败。因此我们一定要注意并发数的限制,不要超过300个,或者你可以自己在自己的机器上做一下测试,来制定你的阈值。
使用之前,请一定要注意并发数限制!!
超时时间
为了防止慢请求影响整个服务,可以设置CURLOPT_TIMEOUT来控制超时时间,防止部分假死的请求无限阻塞进程处理,最后打死机器服务。
CPU负载打满
在代码示例中,如果持续查询并发的执行状态,会导致cpu的负载过高,所以,需要在代码里加上usleep(50000);的语句。
同时,curl_multi_select也可以控制cpu占用,在数据有回应前会一直处于等待状态,新数据一来就会被唤醒并继续执行,减少了CPU的无谓消耗。