Press "Enter" to skip to content

Curl的毫秒超时的一个"Bug"

最近我们的服务在升级php使用的libcurl, 期望新版本的libcurl支持毫秒级的超时, 从而可以更加精细的控制后端的接口超时, 从而提高整体响应时间.
但是, 我们却发现, 在我们的CentOS服务器上, 当你设置了小于1000ms的超时以后, curl不会发起任何请求, 而直接返回超时错误(Timeout reached 28).
原来, 这里面有一个坑, CURL默认的, 在Linux系统上, 如果使用了系统标准的DNS解析, 则会使用SIGALARM来提供控制域名解析超时的功能, 但是SIGALARM不支持小于1s的超时, 于是在libcurl 7.28.1的代码中(注意中文注释行):

int Curl_resolv_timeout(struct connectdata *conn,
                        const char *hostname,
                        int port,
                        struct Curl_dns_entry **entry,
                        long timeoutms)
{
.......
.......
#ifdef USE_ALARM_TIMEOUT
  if(data->set.no_signal)
    /* Ignore the timeout when signals are disabled */
    timeout = 0;
  else
    timeout = timeoutms;
  if(!timeout)
    /* USE_ALARM_TIMEOUT defined, but no timeout actually requested */
    return Curl_resolv(conn, hostname, port, entry);
  if(timeout < 1000) //如果小于1000, 直接超时返回
    /* The alarm() function only provides integer second resolution, so if
       we want to wait less than one second we must bail out already now. */
    return CURLRESOLV_TIMEDOUT;
  ....
  ....

可见, 当你的超时时间小于1000ms的时候, name解析会直接返回CURLRESOLV_TIMEOUT, 最后会导致CURLE_OPERATION_TIMEDOUT, 然后就Error, Timeout reached了...
这....太坑爹了吧? 难道说, 我们就不能使用毫秒超时么? 那你提供这功能干啥?
还是看代码, 还是刚才那段代码, 注意这个(中文注释行):

#ifdef USE_ALARM_TIMEOUT
  if(data->set.no_signal)  //注意这行
    /* Ignore the timeout when signals are disabled */
    timeout = 0;
  else
    timeout = timeoutms;
  if(!timeout)
    /* USE_ALARM_TIMEOUT defined, but no timeout actually requested */
    return Curl_resolv(conn, hostname, port, entry);
  if(timeout < 1000)
    /* The alarm() function only provides integer second resolution, so if
       we want to wait less than one second we must bail out already now. */
    return CURLRESOLV_TIMEDOUT;

看起来, 只要set.no_signal 这个东西为1, 就可以绕过了... 那这个玩意是啥呢?
这就简单了, grep一下代码, 发现:

  case CURLOPT_NOSIGNAL:
    /*
     * The application asks not to set any signal() or alarm() handlers,
     * even when using a timeout.
     */
    data->set.no_signal = (0 != va_arg(param, long))?TRUE:FALSE;
    break;

哈哈, 原来是这货:

<?php
   curl_setopt($ch, CURLOPT_NOSIGNAL, 1);
?>

加上这个OPT以后, 一切终于正常了!
后记:
这样一来, 就会有一个隐患, 那就是DNS解析将不受超时限制了, 这在于公司内部来说, 一般没什么问题, 但是万一DNS服务器hang住了, 那就可能会造成应用超时.
那么还有其他办法么?
有, 那就是Mike提醒的, 我们可以让libcurl使用c-ares(C library for asynchronous DNS requests)来做名字解析. 具体的可以在config curl的时候:

./configure --enable-ares[=PATH]

这样就可以不用设置NOSIGNAL了 🙂
PS, 为什么冠以"Bug", 我只是好奇, 他们为什么不用setitimer?
参考: http://stackoverflow.com/questions/7987584/curl-timeout-less-than-1000ms-always-fails

53 Comments

  1. 莫斯科
    莫斯科 December 10, 2019

    这一句要添加到所有文件里面么
    我用的wordpress还是添加到head就可以了?

  2. wuyou
    wuyou June 1, 2019

    我说我设置总超时时间是3s,结果DNS解析耗了10几秒,都没超时,我还以为dns的耗时需要另外设置,原来是之前的代码设置了CURLOPT_NOSIGNAL这个玩意,导致dns解析不受超时限制了。。这个太蛋疼了

  3. 千与琥珀
    千与琥珀 December 22, 2018

    搜答案搜到鸟哥这里来了,我就说CURL怎么老是不支持1S以下超时,并发量一大了有地方卡住超蛋疼

  4. busk
    busk April 20, 2018

    setitimer也是alarm机制

  5. 西顿照明
    西顿照明 April 3, 2018

    我就在想一个问题,有没有谁能开发个系统,是中文做的代码,让我们不懂英文的也能进入这一行业http://www.zwdeng.cn/news.html/

  6. 三月荒川
    三月荒川 December 12, 2017

    17年底遇到这个问题月余,今天静心搜寻答案,原来14年laruence就发现了并撰写了原委,感慨自己真是个懒惰的程序员啊。

  7. 无名
    无名 September 7, 2017

    真心是个坑,今天踩到了,搞了半天都没发现它是哪里的问题,最后到鸟哥这里找到答案了,感谢

  8. jscklulu
    jscklulu August 8, 2017

    使用yar ,windows环境,客户端调用出现 curl exec failed ‘Timeout was reached’,鸟哥,请赐教

  9. […] 其中CURLOPT_NOSIGNAL这个设置是为了消除curl毫秒级的bug。这个bug的详细信息在鸟哥的博客里也有提到:Curl的毫秒超时的一个”Bug” […]

  10. Joleen
    Joleen September 1, 2016

    If you are interested in topic: can you make money creating apps
    – you should read about Bucksflooder first

  11. rednife
    rednife July 14, 2016

    是的,这个bug,在项目中,我也遇到过!

  12. 张蒙
    张蒙 September 6, 2014

    嘿嘿之前也发现这个问题了,同样的解决方法

  13. Anglea
    Anglea August 28, 2014

    I read a lot of interesting content here. Probably you
    spend a lot of time writing, i know how to save you a lot of work,
    there is an online tool that creates readable, google friendly posts in minutes, just search
    in google – laranitas free content source

  14. Erna
    Erna July 4, 2014

    I see a lot of interesting articles on your page. You have to spend a lot
    of time writing, i know how to save you a lot of time, there is a
    tool that creates unique, SEO friendly articles in couple
    of minutes, just type in google – laranita’s free content source

  15. marker
    marker June 17, 2014

    前几天刚遇到这个问题,忽略掉了,几天看到,获益良多

  16. 儿童玩具
    儿童玩具 May 17, 2014

    先前一直没发现有这个问题。今天才明白原来是这样。

  17. maque
    maque April 18, 2014

    博客勤更新哦

  18. shenguanpu
    shenguanpu April 10, 2014

    设置了CURLOPT_NOSIGNAL 也没有效果啊 curl版本 7.31.0
    超时设置到0.5 结果就没有超时限制了
    鸟哥使用的是curl_multi_exec去处理并发吗? 是否会有性能问题呢?

  19. 都市网达
    都市网达 March 11, 2014

    这个问题,还不能捕获异常,烦死人了,后来换java了。

  20. 雪候鸟
    雪候鸟 March 4, 2014

    @tang 用github主干的代码, 这个在windows上的bug已经fixed了

  21. tang
    tang March 2, 2014

    忘记说了,刚才说的那个错误是指用鸟哥的yar时,客户端的方法调用出现的。

  22. tang
    tang March 2, 2014

    鸟哥,你好,我的是IIS上运行的,发现curl exec failed ‘Timeout was reached’这样的错误也较为常见.
    已经调用SetOpt(YAR_OPT_CONNECT_TIMEOUT, 5000);
    SetOpt(YAR_OPT_TIMEOUT, 1000*60*5);
    按理说我们是服务器直连,速度还可以,服务器的远程方法5分钟足够了的。请鸟哥指点?

  23. luffy
    luffy February 4, 2014

    按照鸟哥的做法,经过多轮测试,似乎毫秒的依然无法生效,求解

  24. 一个堕落的程序员
    一个堕落的程序员 January 30, 2014

    If you want cURL to timeout in less than one second, you can use CURLOPT_TIMEOUT_MS, although there is a bug/”feature” on “Unix-like systems” that causes libcurl to timeout immediately if the value is < 1000 ms with the error "cURL Error (28): Timeout was reached". The explanation for this behavior is:
    "If libcurl is built to use the standard system name resolver, that portion of the transfer will still use full-second resolution for timeouts with a minimum timeout allowed of one second."
    What this means to PHP developers is "You can use this function without testing it first, because you can't tell if libcurl is using the standard system name resolver (but you can be pretty sure it is)"
    The problem is that on (Li|U)nix, when libcurl uses the standard name resolver, a SIGALRM is raised during name resolution which libcurl thinks is the timeout alarm.
    The solution is to disable signals using CURLOPT_NOSIGNAL. Here's an example script that requests itself causing a 10-second delay so you can test timeouts:
    0) {
    echo “cURL Error ($curl_errno): $curl_error\n”;
    } else {
    echo “Data received: $data\n”;
    }
    } else {
    // Server
    sleep(10);
    echo “Done.”;
    }
    ?>

  25. James Tang
    James Tang January 23, 2014

    我们现在调用外链是采用的这种方式支持毫秒级超时,遇到的问题是(极少)部分请求时间远大于设置的超时时间:如设置100ms但有时请求时间能高达5000ms.我说的“请求时间”是指开始调用curl到curl执行完成的时间,所以这种>100ms的时间会不会是DNS解析消耗的时间?

  26. 雪候鸟
    雪候鸟 January 22, 2014

    @Mike thanks, i will update my post about it 🙂

  27. wiwi
    wiwi January 22, 2014

    准确的说,这样就没有域名解析超时了吧?
    这样设的毫秒超时还有用吗?
    PS:在c语言等多线程中使用CURL时,也必须设CURLOPT_NOSIGNAL为1,不然会报错

  28. wiwi
    wiwi January 22, 2014

    但这样就没用到超时了吧?

  29. xuanskyer
    xuanskyer January 21, 2014

    好久没看到鸟哥更新了啊!

  30. 傲雪星枫
    傲雪星枫 January 21, 2014

    學習,CURLOPT_NOSIGNAL是启用时忽略所有的curl传递给php进行的信号。

  31. 23lalala
    23lalala January 21, 2014

    学习了 还以为高版本的curl毫秒超时就没问题了。

  32. Lynn
    Lynn January 21, 2014

    真心是个坑

  33. sky
    sky January 21, 2014

    这个仅仅是忽略timeout,鸟哥就是为了普及这个bug呗

  34. 起司猫
    起司猫 January 21, 2014

    学习了!

  35. 惊奇
    惊奇 January 21, 2014

    前排占座 点我名字有惊奇

  36. 莱登堡
    莱登堡 January 21, 2014

    CURLOPT_NOSIGNAL这个参数原来这么有用,以前还没关注过!

  37. 花生2
    花生2 January 21, 2014

    占位了

Leave a Reply

Your email address will not be published. Required fields are marked *