至瑞網

nginx 正向代理超時分析

在sae,fetchurl是所有http請求的出口(正向代理),fetchurl基於nginx開發,以下稱做nginx。這次碰到的問題是這樣的,某個uri,假設為xuri,使用nginx抓取(使用curl請求nginx抓取xuri),返回了部分數據,然後超時。使用curl直接抓取,總是正常。curl行,nginx不行,則可能是nginx的問題。把該xuri對應的資源放到測試的apache上,記做yuri,則nginx可以正常抓取。這樣,又可能不是nginx的問題,而是xuri所在服務器的問題,但是卻解釋不了為什麼curl直接抓取行。為了還nginx一個清白,我拿perl模擬nginx(主要是模擬請求的http header)抓取xuri,xuri抓取正常。為了還xuri服務器一個清白,我拿perl模擬xuri服務器(主要是模擬響應的http header),nginx抓取正常。既然都清白,為什麼nginx抓取xuri不行,一定是這中間的模擬邏輯有問題,但是我找不出原因。我認為http協議的所有語義都在請求header和響應header中,而這兩者我都模擬了。


苦悶中,想起nginx的一個參數proxy_buffering,它的含義是:nginx不是立即轉發收到的請求,而是等buffer滿之後再轉發。默認是on。把它改成off,使用curl請求nginx抓取xuri就行了,數據是完整的,沒有超時。我找到問題的所在了,我把它重新設為off,並且修改perl模擬xuri服務器的邏輯,發送完xuri的內容後,不是立即關閉連接,而是sleep。這下模擬出來了,nginx抓取失敗。


由此推測,整個事情是這樣的。nginx抓取時,已經讀取了所有的xuri的內容,因為proxy_buffering on的緣故,它沒有立即把抓取的內容發送給客戶端(這裡是curl),它在等待xuri服務器關閉連接,注意,此時它已經讀到了完整的數據,但是很遺憾,xuri服務器沒有關閉連接,它一直等到了超時,xuri服務器都沒有關閉,此時nginx關閉了和客戶端的連接,丟棄了buffer中的數據。修改proxy_buffering為off為什麼就可以了呢?這要從curl的實現說起,curl通過http響應頭Content-Length,知道http響應體的字節數,讀夠了這些字節數後,curl主動斷開了連接。在這裡因為nginx不緩存了,所以curl讀取到了完整的數據,它斷開了和nginx的連接,所以看起來既沒有超時,又讀取了完整的數據。curl能直接正常抓取xuri也是因為如此。另外,nginx可以正常抓取位於apache的yuri,是因為apache在發送完yuri對應的資源後,關閉了和nginx的連接。


原因找到了,解決有兩個辦法,一是關閉nginx的proxy_buffering,二是xuri服務器發送完數據,立即關閉連接。nginx和xuri服務器通信用的是http/1.0,不知道協議是怎麼規定的,是xuri服務器發送完數據立即關閉連接,還是客戶端(這裡是curl)根據Content-Length判斷數據發送結束,主動關閉連接,還是客戶端(這裡是nginx)不管Content-Length,根據服務器端關閉了連接,判斷傳輸結束,然後關閉連接。不管標準如何,事實如此,也只能相機行事。


附perl的模擬代碼,模擬失敗的原因是,準確模擬了傳輸的字節流,卻沒有準確模擬客戶端/服務端對收到/發送字節流後的處理。


客戶端,準確的模擬了nginx發送http頭,卻沒有準確模擬nginx處理http響應,這裡模擬的是curl如何處理響應。準確模擬應該是阻塞在recv,直到服務器端斷開連接,而不是根據Content-Length主動斷開連接。

#!/usr/bin/perl


use strict;

use warnings;


use IO::Socket::INET;


my $socket = IO::Socket::INET->new(PeerAddr => "t0.qlogo.cn",

    PeerPort => 80, 

    Proto => "tcp")

    or die "can't connect to host";


my $request = "GET /mbloghead/87aa5f74cd34e94596a0/100 HTTP/1.0\r\nHost: t0.qlogo.cn\r\nUser-Agent: SAE/fetchurl-omy123xojx\r\nConnection: close\r\nAccept: */*\r\n\r\n";

$socket->send($request, 0); 


my $response;

$socket->recv($response, 9000, 0); 

my ($header, $body) = split /\r\n\r\n/, $response, 2;

if ($header =~ /Content-Length:\s*(\d+)/) {

    my $len = int($1);

    $len -= length($body);

    while ($len > 0) {

        $socket->recv($response, 9000, 0); 

        $len -= length($response);

        $body .= $response;

    }   

    print $body;

}


模擬xuri服務端,它準確的模擬了yuri所在的apache,卻沒有準確模擬xuri所在的服務器(是hphttp)。準確模擬應該是在$client->send($c, 0),之後調用sleep。

#!/usr/bin/perl


use strict;

use warnings;


use IO::Socket::INET;


open(IN, "<x.jpg");

my ($c, $off) = ("", 0); 

while (my $n = read(IN, $c, 1024, $off)) {

    $off += $n; 

}

close(IN);

my $size = int(-s "x.jpg");


my $server = IO::Socket::INET->new(Listen => 10, 

    LocalPort => 8080,

    Proto => "tcp",

    ReuseAddr => 1)

    or die "can't listen at 8080";


while (my $client = $server->accept()) {

    my $request;

    $client->recv($request, 1024, 0); 

    my $response = "HTTP/1.0 200 OK\r\nContent-Length: $size\r\nConnection:Close\r\n\r\n";

    $client->send($response, 0); 

    $client->send($c, 0);

    $client->close();

}