thrift shows CLOSE_WAIL error

常用Thrift搭建WebService,對於線程池的應用,之前一直採用TThreadPoolServer類的實現,本次對垃圾過濾的應用中,在對該類的使用中出現了運行進程的客戶端請求鏈接以增量的方式出現了CLOSE_WAIT狀態,當增長到一定程度後,系統不再處理請求(線程池中已無鏈接可用)。具體錯誤爲:

TThreadPoolServer: TServerTransport died on accept: accept(): Too many open files

Thrift: Fri Feb 22 13:54:46 2013 TServerSocket::acceptImpl() ::accept() Too many open files

該問題已解決,下面列出有處理過程對我有幫助的鏈接地址:

從Socket本身分析了問題的根源:http://blog.csdn.net/hwz119/article/details/1611182

從Thrift出發分析了出現問題的原因,並給出瞭解決問題的建議:http://blog.rushcj.com/2010/12/20/thrift-close-wait/

從代碼上給出了調用方式:http://blog.csdn.net/jianbinhe1012/article/details/7726738

                                                http://wiki.apache.org/thrift/ThriftUsageC%2B%2B

 

 

Thrift常見的服務端類型有以下幾種:

  • TSimpleServer —— 單線程服務器端使用標準的阻塞式 I/O
  • TThreadPoolServer —— 多線程服務器端使用標準的阻塞式 I/O
  • TNonblockingServer —— 多線程服務器端使用非阻塞式 I/O

該問題的出現,是因爲錯誤的選擇了Server的類型,或過於保守的Server Max Connection等。

對於ThreadPoolServer而言,每一個客戶端連接,Server端都需要提供一個固定的線程來維護,在空閒時,線程堵塞在read() 操作,等待客戶端數據的到來。Thrift ThreadPoolServer中使用的默認線程池是定長線程池,意味着Server端能提供的線程池數是有限的。當線程用完時,新的連接將不能得到 Server殷勤的服務,它不會在乎你的生死,你必須等待。

舉個例子:

我有一個用Thrift ThreadPoolServer(使用SimpleThreadPool)實現的Server,最大支持100個連接,現在有100個客戶端連接到我的 Server。實現客戶端的程序員都很在乎TCP連接建立的開銷,因此,他們都維護了一個長連接。這個時候,如果有第101個客戶端連接到Server 了,會發生什麼情況呢?

  1. Server會接受這個連接,連接成功建立;
  2. Server沒有合適的線程來處理這個連接,於是將這個連接放到暫存列表
  3. 如果這個時候有線程空閒了,則一切順利,這個線程將接管這個連接;
  4. 但遺憾的是,我們沒有空閒線程,所以這個連接一直處於空閒狀態,直到客戶端程序timeout(如果設置了timeout的話);
  5. 連接timeout,意味着暫存列表裏的連接已經失效了,此時對應的socket處於CLOSE_WAIT中(出現了本文開頭的情況),遺憾的是,我們依然沒有空閒的線程來處理這個連接,所以它一直處於CLOSE_WAIT中。
  6. 終於,某一個時刻,有一個客戶端關閉了連接,我們有了空閒線程,它去查看暫存列表。發現有一個socket fd,嘗試去接管它,對這個fd執行read(),然後得到一個Connection Reset error,終於,我們可以優雅的關閉它了(CLOSE_WAIT結束)。
  7. 以上就是全部的故事。

那麼,要怎麼辦?

  1. 如果連接數太多,爲什麼不用NonBlockingServer呢?Thrift有基於libevent的實現,雖然它的ThreadPool限制了NonblockingServer的性能,但是,你可以方便的實現一個存/取線程更高效的ThreadPool.
  2. 你可以實現一個TimeoutCachedThreadPool來替代SimpleThreadPool.
  3. 提高Max Connection的值.

我的相關代碼:

Server(C++):

 ...

#include <server/TNonblockingServer.h>

int main(int argc, char **argv) {
    if (argc < 3)
    {
        printf("Usage: %s [port][dict].\n", argv[0]);
        exit(0);
    }
  int port = atoi(argv[1]);
  shared_ptr<ForSharedServiceHandler> handler(new ForSharedServiceHandler(argv[2]));
  shared_ptr<TProcessor> processor(new ForSharedServiceProcessor(handler));
 
//  shared_ptr<TServerTransport> serverTransport(new TServerSocket(port));                   // for TThreadPoolServer
//  shared_ptr<TTransportFactory> transportFactory(new TBufferedTransportFactory());  // for TThreadPoolServer
  shared_ptr<TProtocolFactory> protocolFactory(new TBinaryProtocolFactory());
     
  shared_ptr<ThreadManager> threadManager = ThreadManager::newSimpleThreadManager(100);
  shared_ptr<PosixThreadFactory> threadFactory = shared_ptr<PosixThreadFactory>(new PosixThreadFactory());
  threadManager->threadFactory(threadFactory);
  threadManager->start();

  TNonblockingServer server(processor, protocolFactory, port, threadManager);
//  TThreadPoolServer server(processor, serverTransport, transportFactory, protocolFactory, threadManager);
  server.serve();
  return 0;
}

g++ -o [email protected] $? $(GEN_SRC) -I. -lpthread -O3 -Wall -I${LIB_DIR} -I${BOOST_DIR} -I${THRIFT_DIR} -L${THRIFT_LIB} -lthrift -L${LIB_DIR}-levent -lthriftnb-g

 

Client(PHP):

 ...

require_once $GLOBALS['THRIFT_ROOT'].'/transport/TFramedTransport.php';
function thrift_initial()
{
        global $g_thrift_clinet;
        global $g_transport;
        if ( isset ($argv) && array_search('--http', $argv) ) {
                $socket = new THttpClient('10.210.128.131', 9096, '/php/PhpServer.php');
        } else {
                $socket = new TSocket('10.210.128.131', 9096);
                $socket->setSendTimeout(1000);
                $socket->setRecvTimeout(6000);
        }  
        $g_transport = new TFramedTransport($socket);   // for TNonblockingServer
  //      $g_transport = new TBufferedTransport($socket); // for TThreadPoolServer
        $protocol = new TBinaryProtocol($g_transport); 

        //your class
        $g_thrift_clinet = new ForSharedServiceClient($protocol);

        $g_transport->open();

}

 

Over.

 

知識擴展:

要搞清楚爲什麼會出現CLOSE_WAIT,那麼首先我們必須要清楚CLOSE_WAIT的機制和原理.

 

假設我們有一個client, 一個server.

 

當client主動發起一個socket.close()這個時候對應TCP來說,會發生什麼事情呢?如下圖所示.

 

 

 

client首先發送一個FIN信號給server, 這個時候client變成了FIN_WAIT_1的狀態, server端收到FIN之後,返回ACK,然後server端的狀態變成了CLOSE_WAIT.

接着server端需要發送一個FIN給client,然後server端的狀態變成了LAST_ACK,接着client返回一個ACK,然後server端的socket就被成功的關閉了.

 

從這裏可以看到,如果由客戶端主動關閉一鏈接,那麼客戶端是不會出現CLOSE_WAIT狀態的.客戶端主動關閉鏈接,那麼Server端將會出現CLOSE_WAIT的狀態.

而我們的服務器上,是客戶端socket出現了CLOSE_WAIT,由此可見這個是由於server主動關閉了server上的socket.

 

那麼當server主動發起一個socket.close(),這個時候又發生了一些什麼事情呢.

 

從圖中我們可以看到,如果是server主動關閉鏈接,那麼Client則有可能進入CLOSE_WAIT,如果Client不發送FIN包,那麼client就一直會處在CLOSE_WAIT狀態(後面我們可以看到有參數可以調整這個時間).

 

那麼現在我們要搞清楚的是,在第二中場景中,爲什麼Client不發送FIN包給server.要搞清楚這個問題,我們首先要搞清楚server是怎麼發FIN包給client的,其實server就是調用了

socket.close方法而已,也就是說如果要client發送FIN包,那麼client就必須調用socket.close,否則就client就一直會處在CLOSE_WAIT(但事實上不同操作系統這點的實現還不一樣,

在ahuaxuan(ahuaxuan.iteye.com)的例子中也出現了這樣的case).

 

下面我們來做幾個實驗

實驗一:

環境:

服務器端:win7+tomcat,tomcat的keep-alive的時間爲默認的15s.

客戶端:mac os

實驗步驟:服務器啓動後,客戶端向服務器發送一個get請求,然後客戶端阻塞,等待服務器端的socket超時.通過netstat -np tcp可以看到的情況是發送get請求時,服務器和客戶端鏈接是ESTABLISHED, 15s之後,客戶端變成了CLOSE_WAIT,而服務器端變成了FIN_WAIT_2.這一點也在我們的預料之中,而這個時候由於客戶端線程阻塞,客戶端socket空置在那裏,不做任何操作,2分鐘過後,這個鏈接不管是在win7上,還是在mac os都看不到了.可見,FIN_WAIT_2或者CLOSE_WAIT有一個timeout.在後面的實驗,可以證明,在這個例子中,其實是FIN_WAIT_2有一個超時,一旦過了2分鐘,那麼win7會發一個RST給mac os要求關閉雙方的socket.

 

實驗二

服務器端:ubuntu9.10+tomcat,tomcat的keep-alive的時間爲默認的15s.

客戶端:mac os

實驗步驟:服務器啓動後,客戶端向服務器發送一個get請求,然後客戶端阻塞,等待服務器端的socket超時.通過netstat -np tcp(ubuntu使用netstat -np|grep tcp)可以看到的情況是發送get請求時,服務器和客戶端鏈接是ESTABLISHED, 15s之後,客戶端變成了CLOSE_WAIT,而服務器端變成了FIN_WAIT_2.這一點也也在我們的預料之中,而這個時候由於客戶端線程阻塞,客戶端socket空置在那裏,不做任何操作,1分鐘過後,ubuntu上的那個socket不見了,但是mac os上的socket還在,而且還是CLOSE_WAIT,這說明,FIN_WAIT_2確實有一個超時時間,win7上的超時操作可以關閉mac os上的socket,而ubuntu上的FIN_WAIT_2超時操作卻不能關閉mac os上的socket(其狀一直是CLOSE_WAIT).

 

實驗三

服務器端:mac os+tomcat,tomcat的keep-alive的時間爲默認的15s.

客戶端:mac os

實驗步驟:服務器啓動後,客戶端向服務器發送一個get請求,然後客戶端阻塞,等待服務器端的socket超時.通過netstat -np tcp可以看到的情況是發送get請求時,服務器和客戶端鏈接是ESTABLISHED, 15s之後,客戶端變成了CLOSE_WAIT,而服務器端變成了FIN_WAIT_2.這一點也在我們的預料之中,而這個時候由於客戶端線程阻塞,客戶端socket空置在那裏,不做任何操作,4分鐘過後,mac os服務器端上的那個socket不見了,但是mac os客戶端上的socket還在,而且還是CLOSE_WAIT,這說明,FIN_WAIT_2確實有一個超時時間,win7上的超時操作可以關閉mac os上的socket,而ubuntu和mac os上的FIN_WAIT_2超時操作卻不能關閉mac os上的socket.

 

 

 

總結, 當服務器的內核不一樣上FIN_WAIT_2的超時時間和操作是不一樣的.

經查:控制FIN_WAIT_2的參數爲:

/proc/sys/net/ipv4/tcp_fin_timeout

如 果套接字由本端要求關閉,這個參數決定了它保持在FIN-WAIT-2狀態的時間。對端可以出錯並永遠不關閉連接,甚至意外當機。缺省值是60秒。2.2 內核的通常值是180秒,你可以按這個設置,但要記住的是,即使你的機器是一個輕載的WEB服務器,也有因爲大量的死套接字而內存溢出的風險,FIN- WAIT-2的危險性比FIN-WAIT-1要小,因爲它最多隻能吃掉1.5K內存,但是它們的生存期長些。參見tcp_max_orphans。

 

實驗四

服務器端:ubuntu9.10+tomcat,tomcat的keep-alive的時間爲默認的15s.

客戶端:mac os

實驗步驟:服務器啓動後,客戶端向服務器發送一個get請求,然後關閉客戶端關閉socket.通過netstat -np tcp可以看到的情況是發送get請求時,服務器和客戶端鏈接是ESTABLISHED, 客戶端拿到數據之後,客戶端變成了TIME_WAIT,而服務器端變成了已經看不到這個socket了.這一點也也在我們的預料之中,誰主動關閉鏈接,那麼誰就需要進入TIME_WAIT狀態(除非他的FIN_WAIT_2超時了),大約1分鐘之後這個socket在客戶端也消失了.

 

實驗證明TIME_WAIT的狀態會存在一段時間,而且在這個時間端裏,這個FD是不能被回收的.

 

但是我們的問題是客戶端有很多CLOSE_WAIT,而且我們的服務器不是windows,而是linux,所以CLOSE_WAIT有沒有超時時間呢,肯定有,而且默認情況下這個超時時間應該是比較大的.否則不會一下子看到兩百個CLOSE_WAIT的狀態.

 

客戶端解決方案:

 

1.由於socket.close()會導致FIN信號,而client的socket CLOSE_WAIT就是因爲該socket該關的時候,我們沒有關,所以我們需要一個線程池來檢查空閒連接中哪些進入了超時狀態(idleTIME),但進入超時

的socket未必是CLOSE_WAIT的狀態的.不過如果我們把空閒超時的socket關閉,那麼CLOSE_WAIT的狀態就會消失.(問題:像HttpClient這樣的工具包中,如果要檢查鏈接池,那麼則需要鎖定整個池,而這個時候,用戶請求獲取connection的操作只能等待,在高併發的時候會造成程序響應速度下降,具體參考IdleConnectionTimeoutThread.java(HttpClient3.1))

 

2.經查,其實有參數可以調整CLOSE_WAIT的持續時間,如果我們改變這個時間,那麼可以讓CLOSE_WAIT只保持很短的時間(當然這個參數不只作用在CLOSE_WAIT上,縮短這個時間可能會帶來其他的影響).在客戶端機器上修改如下:

sysctl -w net.ipv4.tcp_keepalive_time=60(缺省是2小時,現在改成了60秒)

sysctl -w net.ipv4.tcp_keepalive_probes=2

sysctl -w net.ipv4.tcp_keepalive_intvl=2

我們將CLOSE_WAIT的檢查時間設置爲30s,這樣一個CLOSE_WAIT只會存在30S.

 

3. 當然,最重要的是我們要檢查客戶端鏈接的空閒時間,空閒時間可以由客戶端自行定義,比如idleTimeout,也可由服務器來決定,服務器只需要每次在response.header中加入一個頭信息,比如說名字叫做timeout頭,當然一般情況下我們會用keep-alive這個頭字段, 如果服務器設置了該字段,那麼客戶端拿到這個屬性之後,就知道自己的connection最大的空閒時間,這樣不會由於服務器關閉socket,而導致客戶端socket一直close_wait在那裏.

 

服務器端解決方案

 

4.前面講到客戶端出現CLOSE_WAIT是由於服務器端Socket的讀超時,也是TOMCAT中的keep-alive參數.那麼如果我們把這個超時時間設置的長點,會有什麼影響?

如果我們的tomcat既服務於瀏覽器,又服務於其他的APP,而且我們把connection的keep-alive時間設置爲10分鐘,那麼帶來的後果是瀏覽器打開一個頁面,然後這個頁面一直不關閉,那麼服務器上的socket也不能關閉,它所佔用的FD也不能服務於其他請求.如果併發一高,很快服務器的資源將會被耗盡.新的請求再也進不來. 那麼如果把keep-alive的時間設置的短一點呢,比如15s? 那麼其他的APP來訪問這個服務器的時候,一旦這個socket, 15s之內沒有新的請求,那麼客戶端APP的socket將出現大量的CLOSE_WAIT狀態.

所以如果出現這種情況,建議將你的server分開部署,服務於browser的部署到單獨的JVM實例上,保持keep-alive爲15s,而服務於架構中其他應用的功能部署到另外的JVM實例中,並且將keep-alive的時間設置的更

長,比如說1個小時.這樣客戶端APP建立的connection,如果在一個小時之內都沒有重用這條connection,那麼客戶端的socket纔會進入CLOSE_WAIT的狀態.針對不同的應用場景來設置不同的keep-alive時間,可以幫助我們提高程序的性能.

 

5.如果我們的應用既服務於瀏覽器,又服務於其他的APP,那麼我們還有一個終極解決方案.

那就是配置多個connector, 如下:

<!-- for browser -->

 <Connector port="8080" protocol="HTTP/1.1" 

               connectionTimeout="20000" 

               redirectPort="8443" />

 

<!-- for other APP -->

<Connector port="8081" protocol="HTTP/1.1" 

               connectionTimeout="20000" 

               redirectPort="8443" keepAliveTimeout="330000" />

 

訪問的時候,瀏覽器使用8080端口,其他的APP使用8081端口.這樣可以保證瀏覽器請求的socket在15s之內如果沒有再次使用,那麼tomcat會主動關閉該socket,而其他APP請求的socket在330s之內沒有使用,才關閉該socket,這樣做可以大大減少其他APP上出現CLOSE_WAIT的機率.

 

你一定會問,如果我不設置keepAliveTimeout又怎麼樣呢,反正客戶端有idleTimeout,客戶端的close_wait不會持續太長時間,請注意看上圖中標紅的地方,一個是close_wait,還有一個是time_wait狀態,也就是說誰主動發起請求,那麼它將會最終進入time_wait狀態,據說windows上這個time_wait將持續4分鐘,我在linux上的測試表明,linux上它大概是60s左右,也就是說高併發下,也就是服務器也需要過60s左右才能真正的釋放這個FD.所以我們如果提供http服務給其他APP,那麼我們最好讓客戶端優先關閉socket,也就是將客戶端的idleTimeout設置的比server的keepalivetimeout小一點.這樣保證time_wait出現在客戶端. 而不是資源較爲緊張的服務器端.

 

總結:

       本文中ahuaxuan給大家揭示了TCP層client和server端socket關閉的一般流程,並且指出異常情況下client和server端各自會發生的情況,包含了在不同平臺上出現了的不同情況, 同時說明了在應用層上我們可以做什麼樣的邏輯來保證socket關閉時對server端帶來最小的影響.


 

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