4年前PHP Curl毫秒超時問題現在怎麼樣了?

轉載自:https://www.jianshu.com/p/857e664aef49

先上結論:

  • PHP 7和libcurl 7.60.0對curl毫秒超時理論都是支持的,但如果DNS使用同步解析模式(部分Linux發行版缺省爲依賴alram信號的同步模式),則無法支持毫秒
  • 通過設置CURLOPT_NOSIGNAL(Guzzle毫秒的缺省處理),會設置DNS解析不超時,一旦DNS異常將導致服務阻塞,簡單粗暴風險高
  • 建議方案:配置curl時啓用異步DNS解析特性,兩種異步DNS解析特性均可
    • ./configure --enable-ares:開啓c-ares
    • ./configure --enable-threaded-resolver:開啓threaded-resolver
    • 二者對比說明:libcurls-name-resolving
  • CentOS 7中缺省編譯的Curl已配置異步DNS解析,也就說CentOS7下PHP 7 完美支持毫秒超時

鳥哥在14年拋出的問題

問題起源:Laruence:21 Jan 14 Curl的毫秒超時的一個”Bug”

問題描述:

libcurl新版本支持毫秒級超時,升級PHP後,設置毫秒超時直接返回超時錯誤(Timeout reached 28

解決方案:

  • 方案1:
    • curl_setopt($ch, CURLOPT_NOSIGNAL, 1);禁用信號,直接跳過DNS解析超時校驗
    • 缺點:DNS解析不受控制,如果DNS服務異常,且無超時,導致整個應用大面積阻塞
  • 方案2:
    • libcurl編譯時啓用c-ares進行DNS解析,如此配置./configure --enable-ares[=PATH]
    • 缺點:依賴libcurl編譯時配置

PHP文檔中關於TIMEOUT_MS說明

看下PHP文檔裏的說明:http://php.net/manual/zh/function.curl-setopt.php

  • CURLOPT_CONNECTTIMEOUT_MS
  • 嘗試連接等待的時間,以毫秒爲單位。設置爲0,則無限等待。 如果 libcurl 編譯時使用系統標準的名稱解析器( standard system name resolver),那部分的連接仍舊使用以秒計的超時解決方案,最小超時時間還是一秒鐘。
  • 在 cURL 7.16.2 中被加入。從 PHP 5.2.3 開始可用。
  • The number of milliseconds to wait while trying to connect. Use 0 to wait indefinitely. If libcurl is built to use the standard system name resolver, that portion of the connect will still use full-second resolution for timeouts with a minimum timeout allowed of one second.
  • Added in cURL 7.16.2. Available since PHP 5.2.3.

2018年的今天這個問題如何

結論:

  • curl-7.60.0中同樣保持此段代碼,原因在於使用Linux默認的SIGALARM進行DNS解析時,alarm()最小時間爲1秒
  • 因此缺省情況下,直接使用毫秒仍然會有問題
  • curl-7.60.0.tar.gz中相關部分代碼如下:
int Curl_resolv_timeout(struct connectdata *conn,
                        const char *hostname,
                        int port,
                        struct Curl_dns_entry **entry,
                        time_t timeoutms)
{
...
...
#ifdef USE_ALARM_TIMEOUT
  if(data->set.no_signal)  // 設置no_signal,則timeout爲0,忽略超時
    /* Ignore the timeout when signals are disabled */
    timeout = 0;
  else
    timeout = (timeoutms > LONG_MAX) ? LONG_MAX : (long)timeoutms;

  if(!timeout)  // timeout=0,則無超時解析
    /* USE_ALARM_TIMEOUT defined, but no timeout actually requested */
    return Curl_resolv(conn, hostname, port, entry);

  if(timeout < 1000) { // 如果timeout < 1000,則直接返回CURLRESOLV_TIMEDOUT超時,增加了日誌輸出
    /* The alarm() function only provides integer second resolution, so if
       we want to wait less than one second we must bail out already now. */
    failf(data,
        "remaining timeout of %ld too small to resolve via SIGALRM method",
        timeout);
    return CURLRESOLV_TIMEDOUT;
  }

Guzzle Http中是否有規避策略

  • guzzlehttp/guzzle 6.2
    • 如果設置timeout時間小於1,則guzzle會設置CURLOPT_NOSIGNAL=true
    • 代碼如下:
$timeoutRequiresNoSignal = false;
if (isset($options['timeout'])) {
    $timeoutRequiresNoSignal |= $options['timeout'] < 1;  // 超時時間小於1,則設置NoSignal=1
    $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
}
// CURL default value is CURL_IPRESOLVE_WHATEVER
if (isset($options['force_ip_resolve'])) {
    if ('v4' === $options['force_ip_resolve']) {
        $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V4;
    } else if ('v6' === $options['force_ip_resolve']) {
        $conf[CURLOPT_IPRESOLVE] = CURL_IPRESOLVE_V6;
    }
}
if (isset($options['connect_timeout'])) {
    $timeoutRequiresNoSignal |= $options['connect_timeout'] < 1; // 超時時間小於1,則設置NoSignal=1
    $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
}
if ($timeoutRequiresNoSignal && strtoupper(substr(PHP_OS, 0, 3)) !== 'WIN') {
    $conf[CURLOPT_NOSIGNAL] = true;
}
  • 在某個老版本guzzle中發現並未兼容的老代碼:
if (isset($options['timeout'])) {
    $conf[CURLOPT_TIMEOUT_MS] = $options['timeout'] * 1000;
}
if (isset($options['connect_timeout'])) {
    $conf[CURLOPT_CONNECTTIMEOUT_MS] = $options['connect_timeout'] * 1000;
}

CentOS 7中curl

CentOS 7中缺省攜帶的curl編譯已配置threaded-resolver,也就是說CentOS 7 + PHP 7組合使用毫秒超時很安全。

補充一點curl支持異步DNS的方式有兩種:c-ares和threaded-resolver(centos 7中缺省啓用threaded-resolver)

附帶兩個查看本機curl編譯配置的命令:

  • curl --version:AsynchDNS表示支持異步DNS特性
  • curl-config --configure:查看編譯時配置

附錄不同CentOS下curl缺省編譯配置:

  • CentOS 6.5
>curl --version

curl 7.37.0 (x86_64-unknown-linux-gnu) libcurl/7.37.0 OpenSSL/1.0.1e zlib/1.2.3
Protocols: dict file ftp ftps gopher http https imap imaps pop3 pop3s rtsp smtp smtps telnet tftp 
Features: Largefile NTLM NTLM_WB SSL libz

> curl-config --configure

'--with-ssl' '--with-ipv6' '--enable-ldap' '--enable-ldaps'
  • CentOS 7
> cat /etc/redhat-release
CentOS Linux release 7.0.1406 (Core) 

> curl --version
curl 7.29.0 (x86_64-redhat-linux-gnu) libcurl/7.29.0 NSS/3.19.1 Basic ECC zlib/1.2.7 libidn/1.28 libssh2/1.4.3
Protocols: dict file ftp ftps gopher http https imap imaps ldap ldaps pop3 pop3s rtsp scp sftp smtp smtps telnet tftp 
Features: AsynchDNS GSS-Negotiate IDN IPv6 Largefile NTLM NTLM_WB SSL libz

> curl-config --configure

'--build=x86_64-redhat-linux-gnu' '--host=x86_64-redhat-linux-gnu' '--program-prefix=' '--disable-dependency-tracking' '--prefix=/usr' '--exec-prefix=/usr' '--bindir=/usr/bin' '--sbindir=/usr/sbin' '--sysconfdir=/etc' '--datadir=/usr/share' '--includedir=/usr/include' '--libdir=/usr/lib64' '--libexecdir=/usr/libexec' '--localstatedir=/var' '--sharedstatedir=/var/lib' '--mandir=/usr/share/man' '--infodir=/usr/share/info' '--disable-static' '--enable-hidden-symbols' '--enable-ipv6' '--enable-ldaps' '--enable-manual' '--enable-threaded-resolver' '--with-ca-bundle=/etc/pki/tls/certs/ca-bundle.crt' '--with-gssapi' '--with-libidn' '--with-libssh2' '--without-ssl' '--with-nss' 'build_alias=x86_64-redhat-linux-gnu' 'host_alias=x86_64-redhat-linux-gnu' 'CFLAGS=-O2 -g -pipe -Wall -Wp,-D_FORTIFY_SOURCE=2 -fexceptions -fstack-protector-strong --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=generic' 'LDFLAGS=-Wl,-z,relro '

參考文章

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章