CURL: CURLE_COULDNT_CONNECT问题探究
摘自::存儲系統研究:socket connect error 99(Cannot assign request address)
這是最近使用libcurl寫http服務的壓力測試的時候遇到的一個問題,其直接表象是客戶端在發送http請求時失敗,最終原因是客戶端的TIME_WAIT狀態的socket進程過多,導致端口被占滿。下面看整個分析過程:
(1) 首先看產生錯誤的源碼:
/* get it! */
res = curl_easy_perform(curl_handle);
long http_code = 0;
curl_easy_getinfo(curl_handle, CURLINFO_RESPONSE_CODE, &http_code);
/* cleanup curl stuff */
curl_easy_cleanup(curl_handle);
if (res != CURLE_OK || http_code != 200) {
cout << uri << ", res = " << res << ", http_code = " << http_code << endl;
}
return (res == CURLE_OK && http_code == 200);
錯誤日志如下:
http://10.237.92.30:8746/thumbnail/jpeg/l820/AppStore/b262b95f-95b8-4e0e-b4e0-edc3b76e3c81, res = 7, http_code = 0 http://10.237.92.30:8746/thumbnail/jpeg/l820/AppStore/a4c37951-d8b5-40ff-af27-4efcd1a58e71, res = 7, http_code = 0 http://10.237.92.30:8746/thumbnail/jpeg/l820/AppStore/abab08ff-75e1-40da-a113-053789e93686, res = 7, http_code = 0
查看curllib的錯誤代碼,如下,錯誤代碼為CURLE_COULDNT_CONNECT
CURLE_OK = 0,
CURLE_UNSUPPORTED_PROTOCOL, /* 1 */
CURLE_FAILED_INIT, /* 2 */
CURLE_URL_MALFORMAT, /* 3 */
CURLE_NOT_BUILT_IN, /* 4 - [was obsoleted in August 2007 for
7.17.0, reused in April 2011 for 7.21.5] */
CURLE_COULDNT_RESOLVE_PROXY, /* 5 */
CURLE_COULDNT_RESOLVE_HOST, /* 6 */
CURLE_COULDNT_CONNECT, /* 7 */
CURLE_FTP_WEIRD_SERVER_REPLY, /* 8 */
CURLE_REMOTE_ACCESS_DENIED, /* 9 a service was denied by the server
(2) 分析curl_easy_perform返回錯誤的原因
最直接的辦法采用gdb跟蹤客戶端的運行情況,發現客戶端在connect的時候返回錯誤,在源文件curl-7.28.1/lib/connect.c的singleipconnect函數中,于是加入日志在connect之后打印errno,代碼如下:
if(!isconnected && (conn->socktype == SOCK_STREAM)) {
rc = connect(sockfd, &addr.sa_addr, addr.addrlen);
if(-1 == rc) {
error = SOCKERRNO;
printf("connect failed with errno = %d", errno);
}
conn->connecttime = Curl_tvnow();
if(conn->num_addr > 1)
Curl_expire(data, conn->timeoutms_per_addr);
再次運行測試程序,得到如下輸出:
connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/f8913ca1- ae5f-4fcc-abc5-cbe9ada1a67d, ret_code: 0, res: 7 connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/3726a1e2- 057e-402d-b347-61c5a5136cd9, ret_code: 0, res: 7 connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/c19bad67- 6b7d-4dc6-a17a-f74ea525c32a, ret_code: 0, res: 7 connect failed with errno = 99 http://127.0.0.1:8902/thumbnail/jpeg/l820/AppStore/5d778568- d873-46a7-9651-ad8ac3810bf4, ret_code: 0, res: 7
可以看到errno = 99,在內核的include/asm-generic/errno.h文件中可以查看errno = 99的解釋為” Cannot assign requested address”。
#define EAFNOSUPPORT 97 /* Address family not supported by protocol */ #define EADDRINUSE 98 /* Address already in use */ #define EADDRNOTAVAIL 99 /* Cannot assign requested address */ #define ENETDOWN 100 /* Network is down */
(3) errno = 99的原因;
至于connect系統調用為什么返回失敗,就只能看系統調用的實現了。
a) connect系統調用
connect系統調用在net/socket.c中實現,Sys_connect系統調用的調用棧如下:
Sys_connect--->
sock->ops->connect // inet_stream_connect
sk->sk_prot->connect // tcp_v4_connect
tcp_v4_connect的作用主要是完成TCP連接三次握手中的第一個握手,即向服務端發送SYNC = 1和一個32位的序號的連接請求包。要發送SYNC請求包,按照TCP/IP協議,就必須有源IP地址和端口,源IP地址的選擇和路由相關,需要查詢路由表,在ip_route_connect中實現,源端口的選擇在__inet_hash_connect中實現,而且如果找不到一個可用的端口,這個函數會返回-EADDRNOTAVAIL,因此基本上可以確定是這個函數返回錯誤導致connect失敗;
b) __inet_hash_connect
這個函數的主要作用是選擇一個可用的端口,其主要的實現步驟如下:
i. 調用inet_get_local_port_range(&low, &high);獲取可用的端口鏈表;
調用read_seqbegin(&sysctl_local_ports.lock);得到順序鎖;
得到可用端口的low和high:
*low = sysctl_local_ports.range[0];
*high = sysctl_local_ports.range[1];
ii. 對于每一個端口,進行下面的步驟:
在inet_hashinfo *hinfo中查找這個端口inet_hashinfo用于保存已經使用的端口信息,每個使用的端口在這個hash表中有一個entry;
對端口做hash得到鏈表頭(使用鏈表解決hash沖突)
遍歷鏈表中的每一個entry:
a) 判斷是否與這個要使用的端口相同,如果相同轉到步驟b,如果不相同則遍歷下一個entry
b) 找到這個端口,調用check_established(__inet_check_established)判斷這個端口是否可以重用(TIME_WAIT狀態下的端口并且net.ipv4.tcp_tw_recycle = 1是端口可以重用)
如果在鏈表中沒有找到這個端口,表示端口沒有被使用,調用inet_bind_bucket_create在hash表中插入一個entry;
iii. 如果到最后都沒有找到一個可用的端口就返回EADDRNOTAVAIL;
從這個函數的實現可以看出,主要是由于可用的端口被占滿了,所以找不到一個可用的端口,導致連接失敗。運行netstat可以發現確實存在很多TIME_WAIT狀態的socket,這些socket將可用端口占滿了。
[root@test miuistorage-dev]# netstat -n | awk '/^tcp/ {++state[$NF]} END {for(key in state)
print key," ",state[key]}'
TIME_WAIT 26837
ESTABLISHED 30
(4) 解決辦法:
要解決端口被TIME_WAIT狀態的socket占滿的問題,可以有以下的解決辦法:
a) 修改可用端口范圍
查看當前的端口范圍:
root@guojun8-desktop:/linux-2.6.34# sysctl net.ipv4.ip_local_port_range net.ipv4.ip_local_port_range = 32768 61000
修改端口范圍:
root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.ip_local_port_range="32768 62000" net.ipv4.ip_local_port_range = 32768 62000
這種辦法可能不能解決根本問題,因為如果使用短連接,即使增加可用端口還是會被占滿的。
b) 設置net.ipv4.tcp_tw_recycle = 1
這個參數表示系統的TIME-WAIT sockets是否可以快速回收
root@guojun8-desktop:linux-2.6.34# sysctl net.ipv4.tcp_tw_recycle=1 net.ipv4.tcp_tw_recycle = 1
c) 設置net.ipv4.tcp_tw_recycle = 1
這個參數表示是否可以重用TIME_WAIT狀態的端口;
root@guojun8-desktop:linux-2.6.34# [root@test thumbnail]# sysctl net.ipv4.tcp_tw_reuse=1 net.ipv4.tcp_tw_reuse = 1
(5) 更深入的探討:sysctl做了什么
可以用strace跟蹤一下sysctl的系統調用:
root@guojun8-desktop:linux-2.6.34# strace sysctl net.ipv4.tcp_tw_recycle=1
execve("/sbin/sysctl", ["sysctl", "net.ipv4.tcp_tw_recycle=1"], [/* 20 vars */]) = 0
brk(0) = 0x952f000
…..
open("/proc/sys/net/ipv4/tcp_tw_recycle", O_WRONLY|O_CREAT|O_TRUNC, 0666) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=0, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000
write(3, "1
", 2) = 2
close(3) = 0
munmap(0xb788e000, 4096) = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 8), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb788e000
write(1, "net.ipv4.tcp_tw_recycle = 1
", 28net.ipv4.tcp_tw_recycle = 1
) = 28
exit_group(0) = ?
可以看到這個程序打開/proc/sys/net/ipv4/tcp_tw_recycle并向文件中寫入1,但是這個設置時怎樣其作用的呢?在內核中對/proc/sys目錄下的文件的i_fop做了特殊的處理,在proc_sys_make_inode 中設置:inode->i_fop = &proc_sys_file_operationsproc_sys_file_operations的定義如下:
static const struct file_operations proc_sys_file_operations = {
.read = proc_sys_read,
.write = proc_sys_write,
};
proc_sys_write中會修改對應的文件,并且修改內存中的內容,不同的文件有不同的proc_handler,如tcp_tw_recycle對應的處理函數是proc_dointvec,這個函數會修改下面的變量:
tcp_death_row.sysctl_tw_recycle
這個變量在內核中表示TIME_WIAT狀態的socket是否可以被快速回收。
總結
以上是生活随笔為你收集整理的CURL: CURLE_COULDNT_CONNECT问题探究的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL 数据库名字 区分大小写
- 下一篇: 猴面包树