前幾天看到一篇博客,提到php所在服務器在大併發狀況下,頻繁建立TCP短鏈接,而其所在服務器的2MSL時間過長,致使沒有端口可用,系統沒法建立TCP socket,而大量報錯。博主在後面給的解決方案是減小2MSL的時間,儘快清除TIME_WAIT狀態的TCP鏈接,回收端口。同時,文章結尾寫了不用長鏈接的理由,但這真的是最好的解決辦法嗎?有其餘辦法能夠更好的作法嗎?php
相似經歷
之因此多這篇文章興趣這麼高,是由於在前段時間,末學也經歷了一件相似的優化歷程,先簡短的描述下咱們的服務器架構。如圖
html
一款webgame 粗略架構圖mysql
想必看到這幅架構圖的同窗,都比較熟悉吧,也比較簡單。「最前面」的nginx 反代負責與玩家的http請求通信,這裏是長鏈接。在其與後端遊戲大區通信時,使用了短鏈接,也就是意味着,每處理用戶的一個http請求,都要從新與後端的nginx創建一次TCP(http)請求,後端nginx處理完以後就關閉。後端的nginx與php在同一臺服務器上,通信配置以下:linux
03 |
server 10.10.10.1 max_fails=2 fail_timeout=30s; #app1 |
04 |
server 10.10.10.2 max_fails=2 fail_timeout=30s; #app2 |
08 |
// Nginx 默認配置 http://trac.nginx.org/nginx/browser/nginx/trunk/conf/nginx.conf |
11 |
fastcgi_pass 127.0.0.1:9000; |
12 |
fastcgi_index index.php; |
13 |
fastcgi_param SCRIPT_FILENAME /scripts $fastcgi_script_name ; |
14 |
include fastcgi_params; |
18 |
//PHP-FPM 默認配置 https://github.com/php/php-src/blob/master/sapi/fpm/php-fpm.conf.in |
19 |
; The address on which to accept FastCGI requests. |
21 |
; 'ip.add.re.ss:port' - to listen on a TCP socket to a specific address on a specific port; |
22 |
; 'port' - to listen on a TCP socket to all addresses on a specific port; |
23 |
; '/path/to/unix/socket' - to listen on a unix socket. |
24 |
; Note: This value is mandatory. |
25 |
listen = 127.0.0.1:9000 |
在這個架構圖中,反代承擔這承上啓下的做用,前面是用戶,後面是web app服務器,長鏈接用戶,短鏈接後端。若用戶數在1W人,1W人與反代的鏈接,在反代服務器上都用80端口。但這1W人的每個請求,反代都要從新使用服務器的端口與後端nginx的80建立tcp socket,來處理請求。若2MSL時間爲1分鐘以上,同時,玩家兩次請求間隔很短,想咱們遊戲的服務器,兩次請求間隔大約5秒(甚至2-3秒)。那麼反代的端口,5秒用1W個,10秒2W,15秒3W,20秒4W,25秒5W,30秒6W!!!,前面已經用掉的端口還沒被回收掉。那麼將會有不少的用戶請求被nginx反代接收以後,因沒有端口資源而沒法與後端nginx建立tcp鏈接。。。以遊戲的其中一個大區爲例:
nginx
關於這個問題,末學在新浪微博上請教過其餘大牛,其中tengine負責人淘叔度前輩以及@120斤的大青蛙前輩告訴我,nginx將在1.1.4版本開始支持ngx_http_upstream_keepalive,3月1號左右,是1.1.16 dev版本,咱們的運維團隊稍微因爲幾天,以後在部分小區的服務器上適用了1.1.16,效果不錯,Nginx反代上與目標服務器的80端口的TCP鏈接中處於ESTABLISHED狀態的很是多,TIME_WAIT的幾乎沒有,很穩定。咱們幾乎計劃爲了這個性能的提高,決定用dev版了。接着nginx1.2Stable version也出來了,那麼也就用這個版本了。至於反代上nginx與後端app server創建TCP鏈接個數,也就是端口數(滿載狀況下,維持ESTABLISHED狀態)的爲nginx worker_processes * ((upstream1 server * keepalive參數數量) + (upstream2 server * keepalive參數數量) + …),並很少。git
如上,端口占用數少了,那麼還有其餘有點嗎?先看一副 socket建立,監聽、數據接收發送、關閉與客戶端鏈接的socket流程圖:
如上圖,nginx反代跟後端nginx的形式跟php-fpm與mysqld socket的模型同樣的。
nginx反代跟php-fpm 相對來講,就是客戶端,這種作法,他們的每一個新socket 請求的發起,都會跟着左邊的流程走一遍,一遍又一遍。。。。每次的socket建立,端口等資源申請,與服務端的三次握手,關閉時的四次握手,端口的回收。。。。
改用長鏈接以後,以下圖:
只有php-fpm(或者nginx反代)的子進程分別與mysqld建立1次TCP鏈接,以後都是send、recv數據的事情了,端口占用也是爲數很少的幾個(每一個fpm子進程將與mysqld維持一個TCP長鏈接)。程序員
一樣,後端服務器的nginx與php-fpm的通信也是如此,只是請求兩沒有反代那麼大。區別就是IP地址是迴環地址127.0.0.1。
既然是迴環地址,那麼兩個服務都是在同一臺機器上跑的,既然是同一臺機器,爲什麼不用進程間通信的socket–unix domain socket呢?
socket是神馬?摘抄一段描述:github
Socket 能夠被定義描述爲兩個應用通訊通道的端點。一個 Socket 端點能夠用 Socket 地址來描述, Socket 地址結構由 IP 地址,端口和使用協議組成( TCP or UDP )。http協議能夠經過socket實現,socket在傳輸層上實現。從這個角度來講,socket介於應用層和傳輸層之間。可是socket做爲一種進程通訊機制,操做系統分配惟一一個socket號,是依賴於通訊協議的,可是這個通訊協議不只僅是 tcp或udp,也能夠是其它協議。web
在同一臺服務器上,用tcp socket與unix domain socket有什麼區別?
如圖所示,對於進程間通信的兩個程序,unix domain socket的流程不會走到TCP 那層,直接以文件形式,以stream socket通信。若是是TCP socket,則須要走到IP層。sql
對於非同一臺服務器上,TCP socket走的就更多了。
至於localhost\127.0.0.1以及網絡IP他們之間的區別,無心中找到一篇博客寫的是以mysql做爲驗證,來講明localhost不走TCP/IP層,跟127.0.0.1不同。末學認爲他理解錯了。他的理由以下
(如下截圖均在linux上,windows的沒有unix domain socket)
mysql鏈接本機時,不加-h參數:
mysql鏈接本機時,加-h參數且值是localhost:
mysql鏈接本機時,加-h參數且值是127.0.0.1:
那位同窗從mysql工具的使用方法、與結果的區別,來理解推導localhost與127.0.0.1的區別,這從方向上就存在問題,我更相信,這是mysql這個程序本身的行爲,遇到-h參數沒加,或者-h參數的值不是IP形式,且my.cnf裏指定mysql的socket路徑時,則直接使用unix domain socket來鏈接服務器,固然,這也是個人猜想,沒有去驗證,你們聽聽就好,別相信。
鑑於末學對以上的理解,將服務器的架構配置變動以下
04 |
//參見nginx官方wiki,記得看E文版,中文版的還沒更新 http://wiki.nginx.org/NginxHttpUpstreamModule |
05 |
server 10.10.8.97 max_fails=2 fail_timeout=30s; #app1 |
06 |
server 10.10.8.99 max_fails=2 fail_timeout=30s; #app2 |
07 |
server 10.10.8.85 max_fails=2 fail_timeout=30s; #app3 |
12 |
location ~ ^([^.]+\.php)($|/.*) { |
13 |
fastcgi_pass unix:/ var /run/php5-fpm.sock; |
14 |
fastcgi_index index.php; |
15 |
include fastcgi_params; |
19 |
; Note: This value is mandatory. |
20 |
listen = / var /run/php5-fpm.sock //與nginx 的fastcgi_pass的路徑一致便可,目錄要有相應讀寫權限 |
至此,優化還爲完畢,若php-fpm與mysql使用mysql_pconnect的話,那麼php-fpm的子進程生成模式最好用static模式,若爲dynamic模式,可能會出現mysql鏈接數被佔滿的狀況,這也跟mysql服務的鏈接超時時間有關,適當調整也容易避免。
不過,咱們目前還沒用mysql_pconnect,主要緣由是咱們的代碼中,有些事務處理開啓以後,對於代碼的失敗處理,忘記寫回滾語句,在短鏈接的狀況下,這個鏈接的銷燬,哪怕客戶端沒提交ROLLBACK或者COMMIT指令,mysql會自動回滾以前的事務。但使用長鏈接以後,不一樣請求會使用同一個MYSQL鏈接句柄,每一個事務開啓都會禁用MYSQL的自動提交,即SET AUTOCOMMIT=0語句,這語句會提交以前的事務。對於咱們代碼忘記寫回滾,而直接返回結果的狀況下,這是會出大問題的,也是咱們目前惟一沒有使用MYSQL_pconnect的緣由。(計劃近期找到沒有寫回滾語句的代碼,修復,繼續使用mysql_pconnect)
其實還有,咱們php-fpm使用了APC來緩存php file,以及 變量數據等,這些也是有優化的地方(若是有時間的話,則待續)。
回過頭來再理解下文章開頭那位同窗給的解決辦法,我仍不能從他給的理由中,理解長鏈接的缺點,哪怕是解決了TIME_WAIT的問題,但每次建立TCP socket ,鏈接到服務器時三次握手,關閉TCP socket時的四次握手 這些也是開銷。固然,縮短2MSL的時間,也是更好利用服務器資源的一個好方法。
最後,咱們調整優化的服務器架構圖以下:
好像有點偏離這篇文章的標題了,其實我更想說我不能理解爲啥nginx跟php-fpm給的默認配置中,都是TCP socket通信的,爲啥不默認給unix domain socket的默認配置呢?若是說爲了方便非同一臺服務器時的狀況,但給的默認IP也是迴環地址呀。
並且,nginx給默認配置中,對於uri請求中的php文件的處理,匹配規則仍是老的,以前發生由於NGINX與PHP的配置而致使的安全問題,雖然不是nginx的錯,但nginx也可給出更嚴謹的範例,但仍沒有。
值得欣慰的是,在UBUNTU 12.4中,nginx的默認配置有了很大的改進,不論是匹配uri的規則,仍是nginx與php-fpm的交互方式:
02 |
# fastcgi_split_path_info ^(.+\.php)(/.+)$; //贊1 |
03 |
# # NOTE: You should have "cgi.fix_pathinfo = 0;" in php.ini |
05 |
# # With php5-cgi alone: |
06 |
# fastcgi_pass 127.0.0.1:9000; //贊3 |
08 |
# fastcgi_pass unix:/ var /run/php5-fpm.sock; //贊3 |
09 |
# fastcgi_index index.php; |
10 |
# include fastcgi_params; |
PS:末學只是個web程序員,這些只是末學學習研究如上知識的總結,理解上不免有錯,各位請海涵。