最近由于網(wǎng)站在大量訪問后偶爾遇到數(shù)據(jù)庫連接失敗的情況,為了解決這個問題,做了一些分析。然后逐步挖掘,獲得了一些有意思的東西。記錄一下。
網(wǎng)站訪問量大,在某些時刻提示數(shù)據(jù)庫連接失敗。但從后端看數(shù)據(jù)庫狀態(tài)一切正常,當(dāng)前連接數(shù)也遠(yuǎn)小于最大連接數(shù)。
在日志中發(fā)現(xiàn)以下提示:
SQLSTATE[HY000] [2002] Cannot assign requested address
使用netstat 查看,發(fā)現(xiàn)大量到mysql的連接處于 TIME_WAIT 狀態(tài)。
netstat -anlpt | grep 3306 |grep TIME_WAIT
按道理說,php使用pdo連接數(shù)據(jù)庫后很快就完成了操作,并釋放了連接。為什么還會有這么多等待呢。
后面發(fā)現(xiàn),雖然pdo執(zhí)行完操作,然后php頁面也結(jié)束后,資源應(yīng)該也釋放了。這體現(xiàn)在mysql中的當(dāng)前實際連接上是對的,頁面執(zhí)行完成后,當(dāng)前實際連接會減少。但是該端口監(jiān)聽并不會馬上斷開,和mysql的 wait_timeout參數(shù)有關(guān)。而默認(rèn)的wait_timeout 是28800,8小時。太長了。根據(jù)實際需要將 wait_timeout 調(diào)整至100秒足夠業(yè)務(wù)使用。
這是第一步,減少了TIME_WAIT 的回收時間。
另外,網(wǎng)上提到,如果確實產(chǎn)生大量的TIME_WAIT,可以修改系統(tǒng)參數(shù),啟用端口重用。
修改 /etc/sysctl.conf
net.ipv4.ip_local_port_range = 10000 61000
net.ipv4.tcp_tw_recycle=1
net.ipv4.tcp_tw_reuse = 1
第一行增大本地端口可用范圍,后面兩行啟用端口重用。sysctl -p 使修改生效。`
這樣改后,確實效果明顯,TIME_WAIT一下子少了很多。
不過這里有一個巨坑。真的是巨坑。網(wǎng)上幾乎很少提及這個問題。那就是在nat網(wǎng)絡(luò)環(huán)境下,如果使用了這個設(shè)置,會導(dǎo)致nat網(wǎng)絡(luò)下無法訪問該服務(wù)器。
看了幾十篇文章,只有一篇提到了這個。 https://www.cnblogs.com/billyxp/p/3683559.html
我也遇到了這樣的問題,修改生效后。公司內(nèi)網(wǎng)的同事由于在一個網(wǎng)段內(nèi),都無法訪問網(wǎng)站了,但是外面其他網(wǎng)絡(luò)沒有任何問題,手機也能正常訪問。
于是將以下參數(shù)改回,然后內(nèi)網(wǎng)訪問恢復(fù)正常。
net.ipv4.tcp_tw_recycle=0
net.ipv4.tcp_tw_reuse = 0
這似乎又回到了原點。還是會有大量的TIME_WAIT產(chǎn)生,只是說給了更多的可用端口范圍,和更快的回收時間而已。
思來想去,還是只有從根源上解決問題。
觀察php連接Mysql的代碼發(fā)現(xiàn),代碼都是使用短連接來完成,用完后系統(tǒng)自動回收資源。由于訪問量巨大,所以產(chǎn)生了很多連接。
修改連接代碼使用長連接連接Mysql,本次使用完畢后,下次連接數(shù)據(jù)庫時該連接可以重用,避免建立新的連接。修改后觀察,隨著原來臨時連接達到timeout時限,time_wait的情況越來越少,后面幾乎沒有了?;謴?fù)正常。目前只有由php-fpm創(chuàng)建的長連接穩(wěn)定保持在一定數(shù)據(jù),當(dāng)然這和配置的php-fpm的線程數(shù)有關(guān)系。
自此,該問題基本解決。最終,還是要把代碼寫好才行啊,不然再好的硬件配置都是枉然。
解決掉這個,然后順手看了下redis,似乎也存在同樣的問題。
按照同樣的思路,將redis改成長連接,不過似乎沒有什么效果。不知道是不是我的phpredis版本問題。由于切換這個有可能會影響系統(tǒng)穩(wěn)定性,暫時未作進一步挖掘。目前redis的TIME_WAIT只有幾百個,還屬于可接受范圍,后續(xù)再做處理。
在這個過程中,發(fā)現(xiàn)了一個新的東西。用unix socket連接redis比用ip地址連接更快。由于redis本來就放在本機,所以用unix socket也是沒什么問題的。立即做了一個簡單測試。
redis-normal.php
$start_time = microtime(1);
$redis = new Redis();
$redis->connect('127.0.0.1');
for($i=1;$i<10000;$i++){
$list_len = $redis->llen("visit_list");
$data = $redis->lrange("visit_list",0,$list_len);
}
$redis->close();
echo microtime(1) - $start_time ;
0.69s
redis-socket.php
$start_time = microtime(1);
$redis = new Redis();
$redis->connect('/var/myredis.sock');
for($i=1;$i<10000;$i++){
$list_len = $redis->llen("visit_list");
$data = $redis->lrange("visit_list",0,$list_len);
}
$redis->close();
echo microtime(1) - $start_time ;
0.38s
使用socket連接的速度是完勝使用ip連接的,速度幾乎快了一倍。
如果把redis的connect放在循環(huán)內(nèi),進行一萬次的連接和關(guān)閉。也是socket方式更快,也是幾乎快了一倍。不過整體會消耗更多的時間。
所以,socket連接看來確實比ip連接優(yōu)秀不少,這在本機redis環(huán)境下很實用。
另外,順手看了些關(guān)于unix socket的資料,對一些細(xì)節(jié)更了解了一些。本次查資料還涉及到了使用tcpdump,以及tcp的三次握手,四次再見等等,發(fā)現(xiàn)抽絲剝繭的感覺很爽,不過要花很多時間去一步步深入。
以下是一些參考:
What's the difference between Unix socket and TCP/IP socket?
A UNIX socket is an inter-process communication mechanism that allows bidirectional data exchange between processes running on the same machine.
IP sockets (especially TCP/IP sockets) are a mechanism allowing communication between processes over the network. In some cases, you can use TCP/IP sockets to talk with processes running on the same computer (by using the loopback interface).
UNIX domain sockets know that they’re executing on the same system, so they can avoid some checks and operations (like routing); which makes them faster and lighter than IP sockets. So if you plan to communicate with processes on the same host, this is a better option than IP sockets.
Edit: As per Nils Toedtmann's comment: UNIX domain sockets are subject to file system permissions, while TCP sockets can be controlled only on the packet filter level.
Windows 10 has support for Unix sockets. There are some limitations, but it's available: blogs.msdn.microsoft.com/commandline/2017/12/19/… – Tyson Jul 30 '18 at 2:45
Unix Socket Tutorial
https://www.tutorialspoint.com/unix_sockets/index.htm