TL;DR
PHP5/7加上7.19的libcurl,设置低于1s的超时时间时,curl_exec
仍会执行超过1s以上。原因在于此版本的libcurl实现逻辑上以1000ms作为curl_exec
中poll
系统调用的超时值。
问题
某些HTTP接口响应时间可能因为种种原因会引起响应时间延长的问题,在这种情况下,从性能、体验角度考虑,设置调用方的超时时间成为必要。
从手册上得知:
选项 | 设置value为 | 备注 |
---|---|---|
CURLOPT_TIMEOUT_MS | 设置cURL允许执行的最长毫秒数。 如果 libcurl 编译时使用系统标准的名称解析器( standard system name resolver),那部分的连接仍旧使用以秒计的超时解决方案,最小超时时间还是一秒钟。 | 在 cURL 7.16.2 中被加入。从 PHP 5.2.3 起可使用。 |
在符合版本条件的服务器上用PHP内置的HTTP服务器模拟一个时长超过1s的接口:
1 | [root@vm curl_test]# cat slow.php |
通过如下代码访问这一接口:
1 | <?php |
通过如下命令,设定了200ms的超时时间,同时还有2s的接口延迟响应时间。
1 | [root@vm curl_test]# php curl_test.php 200 2 |
预料中,客户端应该会在200ms左右的时间结束程序,然而实际执行输出让人感到困惑:
1 | [root@vm curl_test]# php curl_test.php 200 2 |
客户端运行了1000ms+的时间。
分析
查询手册
从PHP手册中查询到的结果,字面意思并没有太多的歧义,使用方法上也并没有明显错误,下面可以从运行时的状态开始分析。
Strace跟踪运行过程
通过strace
查看运行过程中的系统调用:
1 | strace -o curl_strace.log -ttt php curl_test.php 200 1 |
为了方便分析,将输出记录到curl_strace.log
文件之中,并记录各个系统调用产生时的时间戳。
关键的网络请求部分系统调用记录如下:
1 | 1 1497665724.892491 connect(3, {sa_family=AF_INET, sin_port=htons(8000), sin_addr=inet_addr("127.0.0.1")}, 16) = -1 EINPROGRESS (Operation now in progress) |
fd=3
的文件描述符即用于处理网络请求。在上述步骤的第10行发出请求之后,调用了poll
系统调用来检验fd是否可以读写。查阅手册得知,poll系统调用的声明为:
1 | int poll(struct pollfd *fds, nfds_t nfds, int timeout); |
第三个参数即以毫秒为单位的等待超时时间。
请注意16行的系统poll
调用,超时时间足足设定了1000ms,之后程序执行结束退出,那么这个1000ms的超时设定,应该就是程序没有在200ms左右的时间退出的主要原因。
PHP curl扩展
PHP中的curl函数在curl的扩展中定义,如果怀疑对运行的方式产生了怀疑,可以查看扩展源码。
在PHP源码的ext/curl
目录下的interface.c
文件中,可以看到curl_exec
方法的定义:
可以看到,实际上PHP中的curl_exec
调用的是libcurl
中的curl_easy_perform
这一函数执行的网络请求。
对于超时的问题,需要深入到libcurl之中了。
libcurl 7.19
通过curl --version
命令可以获取cUrl的版本,之后在GitHub是哪个clone源码repo,checkout对应版本(7.19.x
)即可。
可以看到curl_easy_perform
在libcurl源码目录中lib/easy.c
中被定义了。主要执行步骤如下:
1.执行Curl_perform
函数
2.在Curl_perform
中完成连接(connect_host
函数)与传输数据操作(Transfer
函数)
3.在Transfer
中对第一次poll操作设定0ms的超时,之后均设定为1000ms的超时,参见代码:
1 | if(first && |
4.Curl_socket_ready
方法中会根据传入的超时时间即变量timeout_ms
的值设定poll
系统调用的超时值,结合strace日志,即11
与16
行的poll
操作。
5.第一次超时为0ms时,poll
会立刻返回,显然没有socket可以读写,会走入switch
语句的case 0
的分支,调用Curl_readwrite
函数;在第二次以及之后的执行中,超时会恒定在1000ms,在可读写之后,会走入switch
语句的default
的分支,也会调用Curl_readwrite
函数。
6.在Curl_readwrite
函数中会再次调用Curl_socket_ready
方法,但是这次传入的超时时间则固定为0ms了:
1 | /* |
这里的Curl_socket_ready
就是strace日志中12
与17
行两次poll
操作记录的来源。
7.在Curl_readwrite
中会判断当前已执行时间是否已经超过了设定的超时时间:
1 | if(data->set.timeout && |
如果已超过则,返回已超时。
小结
综上,老版本curl设定了ms级别超时但是仍然会执行s级别的问题在于curl的poll
超时设定被固定在1000ms之上,造成了这一问题。
解决
可以考虑升级libcurl,或者在编译的PHP中使用高版本的libcurl。
1 | [root@vm curl_test]# curl --version |
以上。