Filedescriptor out of range in select

今天晚上,有同學給我報了一個內部網路協議包 stpclient 的 bug,如下:

File "/data/apps/column-web/eggs/snow-3.2.1-py2.7.egg/snow/client.py", line 146, in service stp_response = self._client.call(stp_request.argv)File "/data/apps/column-web/eggs/snow-3.2.1-py2.7.egg/snow/client.py", line 51, in call self.send_request(request)File "/data/apps/column-web/eggs/stpclient2-0.0.15-py2.7.egg/stpclient2/client.py", line 221, in send_request self._timeout)ValueError: filedescriptor out of range in select()

這個異常已經發生過好幾次了,所以我決定弄清楚原因。

尋找原因

首先,坊間流傳的版本是,當 select 中監聽的 fd 個數超過 1024 的時候,就會出現這個異常,我分析了一下,這個異常是 stp 的客戶端拋出的,因為這個版本的客戶端是我寫的,所以我清楚的知道,不可能有超過1024個fd,同時被 select,那麼問題就來了,到底是因為啥呢?

這個時候,能想到的問題就是,肯定就是坊間的版本有問題,於是首先我看了一下,select 系統調用中的說明,如下:

An fd_set is a fixed size buffer. Executing FD_CLR() or FD_SET() with a value of fd that is negative or is equal to or larger than FD_SETSIZE will result in undefined behavior. Moreover, POSIX requires fd to be a valid file descriptor.

select 的 notes 中寫到:

fd_set 是一個固定大小的 buffer,當設置的 fd 為負或者是超過 FD_SETSIZE 的時候,就會產生 undefined behavior,POSIX 要求 fd 是一個合法的 fd

從這面來看,和坊間的傳聞是有差別的,並不是同時監聽的 fd 超過 FD_SETSIZE 就會有問題,而是 fd 大小超過 FD_SETSIZE 就會有問題,FD_SETSIZE 是定義在 select.h 中的,大小為1024(出問題的機器上),所以應該是 fd 大小超過 1024 就會有問題.

/usr/include/linux/posix_types.h

#undef __FD_SETSIZE#define __FD_SETSIZE 1024

但是,到這裡,並沒有知道我們看到的那行異常是怎麼拋出來的

ValueError: filedescriptor out of range in select()

明顯,這是 python 拋出來的異常,系統調用只是說會產生 undefined behavior 而已。

這個時候,順著 python 的 socket 模塊,找到了 select 模塊 的 python 源代碼,我們發現了如下幾行:

Modules/selectmodule.c

#if defined(_MSC_VER) max = 0; /* not used for Win32 */#else /* !_MSC_VER */ if (!_PyIsSelectable_fd(v)) { PyErr_SetString(PyExc_ValueError, "filedescriptor out of range in select()"); goto finally; } if (v >= max) max = v;#endif /* _MSC_VER */

然後我們發現,當調用 _PyIsSelectable_fd 返回 false 的時候,會產生我們上面的那個異常,接下來我們看一下 _PyIsSelectable_fd 的實現:

Include/fileobject.h

#ifdef HAVE_SELECT #define _PyIsSelectable_fd(FD) (((FD) >= 0) && ((FD) < FD_SETSIZE))#else #define _PyIsSelectable_fd(FD) (1)#endif /* HAVE_SELECT */

因為 POSIX 裡面僅僅規定了 select 系統調用傳入的 fd 需要合法,那就需要調用方檢測 fd,所以在 python 裡面,當 fd 為負,或者是超過 FD_SERSIZE 就會被當做不合法,拋出 ValueError

那麼現在問題明確了,肯定是因為執行 select 調用的時候,傳入了大於 1024 或者是小於 0 的fd,導致了上面異常的出現,接下來的問題就是需要找到為啥會出現大於1024的 fd 了

解釋現象

通過基礎架構的同學了解到,我們對於進程最大能開啟的進程數做了調整,supervisor 裡面每個進程最大能開啟的 fd 個數是 655360,所以完全有可能出現 fd 超出 1024 的情況.

這個異常是出現在 web 機器上,因為一部分 zone 的服務使用了短鏈接,所以會導致單進程擁有的 fd 個數很高,超過 1024.

至此,這個問題算是得到了一個合理的解釋,因為大量使用短鏈接,所以導致單進程的 fd 個數升高,超出了 1024 限制,出現了最開始的異常

解決方法

  • 因為這個值是定義在內核裡面,所以如果在維持目前方案不變的前提下,解決這個問題就需要重新編譯 Linux-kernel,將這個值提高
  • 修改 stpclient 的客戶端,使用epoll,代替比較老舊的 select,當時使用 select 的原因是,fd 個數很少,性能上沒有問題,同時 select 在其他平台上也可以得到支持

推薦閱讀:

如何實現feed流
Rebol/Red,讓編程回歸人性
一晚上糊出一個語言「前端」
理論上最好的編程語言: 起點動機篇
php 與C/C++ 集成的方法有哪些?

TAG:編程語言 | Python | Linux內核 |