遇到一個因socket未關閉引發的檔案控制代碼用完問題
“愛提踢斯”專案最近遇到一個問題,當FTP伺服器磁碟沒有空間時,裝置會不斷復位——這是測試人員反饋的。我們拿到log後,看到一個通訊所用的檔案開啟失敗。不斷列印Too many open file,然後超時裝置復位。同時我們看到資料庫檔案開啟失敗,無法寫入資料。一個現象,看到好幾處問題。還是從最初的表現來入手。雖然把bug指派給別人,但從時間、進度上考慮,週末還是去加班。而最後,解決了問題。根據老夫目測,是FTP的socket未關閉引起的。
linux系統的檔案控制代碼(我很想說“檔案描述符”,但“控制代碼”似乎更多人使用)是有限制的。檢視系統支援控制代碼最大值用如下命令:
# ulimit -n
1024
可見,預設是1024。
下面用程式碼來說明一下問題。程式碼如下:
/** 系統一次允許最大的檔案控制代碼為1024。 open foobar file for: 340 open bar file for: 340 open foo file for: 341 open foobar file failed: : Too many open files open BAR file failed: : Too many open files # ulimit -a ... open files (-n) 1024 ... */ #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <stdio.h> #include <stdlib.h> int foo(void) { static int cnt = 1; int ret = 0; int fd =-1; char buf[5] = {0}; char read_buf[5] = {0}; fd = open("/tmp/FOO.txt",O_RDWR|O_CREAT,0666); if(fd < 0) { perror("open FOO file failed: "); return -1; } printf("open foo file for: %d\n", cnt++); return 0; } int bar(void) { static int cnt = 1; int ret = 0; int fd =-1; char buf[5] = {0}; char read_buf[5] = {0}; fd = open("/tmp/BAR.txt",O_RDWR|O_CREAT,0666); if(fd < 0) { perror("open BAR file failed: "); return -1; } printf("open bar file for: %d\n", cnt++); return 0; } int foobar(void) { static int cnt = 1; int ret = 0; int fd =-1; char buf[5] = {0}; char read_buf[5] = {0}; fd = open("/tmp/foobar.txt",O_RDWR|O_CREAT,0666); if(fd < 0) { perror("open foobar file failed: "); return -1; } printf("open foobar file for: %d\n", cnt++); return 0; } int main(void) { int ret = 0; while (1) { foo();usleep(1); foobar();usleep(1); ret = bar(); // if (ret < 0) break; // 不讓其退出 usleep(100); } return 0; }
要檢視這個程序佔用檔案控制代碼數,先獲取該程序PID:
# ps -ef | grep a.out
root 17835 7615 17 23:01 pts/1 00:00:00 ./a.out
我們檢視該程序檔案控制代碼最大值:
# cat /proc/17835/limits | grep "files"
Max open files 1024 4096 files
和系統預設值一樣。檢視已佔用值:
# ll /proc/17835/fd | wc -l
1027
我們看到,已經佔用上千個控制代碼,一直未釋放。當達到系統最大值時,就會報Too many open files的錯誤。
匆匆已然四載,不曾想到,當年剛入職搞FTP的我,竟然會埋下地雷,讓今天的我不幸踩中。在沒有離職情況下,只好義無反顧地去解決bug。——而這個bug,正是因為未關閉socket造成的。
FTP客戶端的實現,是需要開啟2個socket的,一個是命令通道,一個是資料通道。在伺服器沒有磁碟空間情況下,write資料是返回錯誤的,使用FTP模組者認為是無法登陸,下次會再次嘗試登陸——在這種情況下,登陸是正常的,只是無法寫資料。但每次登陸,都會建立命令通道的socket,這導致了socket的洩漏,不會關閉,因為使用者並沒有呼叫logout函式退出登陸。另一個問題是最主要的,每次寫資料時,要建立資料通道的socket,但在出錯時,並沒有關閉資料通道的socket,而是直接返回。這再次導致了socket洩漏。找到了原因,解決起來就好辦了。
自從知道可以檢視某個程序佔用的檔案控制代碼,我去看看以前的裝置,發現有個別檔案佔用控制代碼較多——也就幾十個。目測是隻開啟但未關閉。出於熱心,我把情況在部門群裡說了,至於後續的事,因為那些不是我的職責範圍,不敢越趄代庖。
2015.8.1 李遲