實戰:一個由Java區域性變數引起的血案!

今天接到一個臨時任務,排查一個網站的詭異問題,是這樣的,這個網站訪問量很大,上了一個模組,在頁面服務端發出一個http請求,讀取另一個java網站提供的資料,上線之後發現一旦存在併發,或是比較多的訪問,http請求就會失敗,甚至在伺服器上不能開啟任何頁面,但是伺服器可以被ping通,也可以ping通其它網址(我沒有看到真實的情況,只是聽說有這樣一個情況)。

實戰:一個由Java區域性變數引起的血案!

IT圈不亂

對於一個高併發網站的服務端發起一個http請求我總是覺得很恐怖,在以前的專案中也從來沒這麼做過(寧願讓客戶端ajax方式請求),憑感覺一開始我就懷疑是執行緒池的問題。因為我們知道,asp。net的請求由執行緒池中的工作執行緒處理,如果在這個工作執行緒中同步發起http請求,那麼就好比把一個記憶體級的處理速度一下子拉到了網路級的處理速度(因為需要同步等待網路),我想會不會是網站本身訪問量就很大了(比如執行緒池最大允許工作執行緒800,當前的併發已經需要佔用200),那麼這個速度下來之後(比如從100毫秒處理時間到1秒),一下子執行緒池就爆滿了(200×10=1000>800)。

實戰:一個由Java區域性變數引起的血案!

java與http

於是,我建議開發使用非同步頁和非同步的HttpWebRequest,最簡單的方式是使用WebClient基於事件的非同步模式DownloadStringAync()。在開發人員按照我的建議修改程式碼上線之後,問題還是沒有解決。我也一時很迷茫,想不出還會是哪些原因(我自己線上下測試了一下,使用這種方式即使很大的請求量始終佔用一個工作執行緒,而IOCP則佔用了比較多)。

在拿到了線上伺服器除錯的機會之後,修改頁面程式碼植入了定時器,定時輸出執行緒池的可用工作執行緒和可用IOCP,結果請求過來之後也只佔用了30個工作執行緒(我們的網站30個併發左右),IOCP不佔用(因為後來程式碼回滾了,沒有使用非同步的Http請求)。後來我想看一下tcp連線情況吧,輸入netstat –s後我傻了,看到4000多個tcp連線,當時第一反映就是訪問量太大了。

實戰:一個由Java區域性變數引起的血案!

java

在之前寫程式的時候記得好像要透過登錄檔修改才能為windows server啟用6萬多個tcp埠(預設埠範圍好象是1000多到5000,正好符合之前看到的4000多個連線),還記得tcp的埠關閉之後預設需要等待4分鐘才能繼續使用,查了一下是MaxUserPort和TcpTimedWaitDelay兩個引數,根據msdn把這2個值修改為最大的65534和最小的30(秒)。算了一下,如果每個訪問網站的請求都是新連線,每個請求又要建立一次http請求,那麼一次請求也就佔用2個埠最多了,算它最大能建立6萬個埠,30秒全部釋放,也就是30秒可以處理3萬個請求,那麼我們的處理能力是1000請求/秒,但是我們的網站一般每臺伺服器的併發不超過50,遠遠夠用了。

修改了登錄檔重啟機器之後發現,果然修改生效了,不過建立的tcp連線在壓力上來之後很快達到了6萬多個,伺服器又出現不能訪問外網的問題了,此時斷定根源問題就是tcp連線過多,已經不能再建立連線了(當然也就不能建立http連線),但是已經開啟的頁面確實可以開啟,新的頁面不能開啟,又忽然想到了http的keep alive。

實戰:一個由Java區域性變數引起的血案!

http

有了這個方向之後修改程式碼,為httpwebrequest設定了keep alive(為了確認是不是加上了connection:keep alive的http頭又費了不少周折),為目標的web伺服器也啟用了keep alive,亂七八糟設定了一堆ServicePointManager的屬性,經過幾個小時百般嘗試之後還是不行,總是發現伺服器很容易達到6萬個連線(也就1分鐘不到吧),cpu狂飆,然後就像斷網一樣,一直糾結在為什麼沒有keep alive,為什麼沒有重用tcp連線。後來又單獨製作了一個網站,發現並沒有佔用這麼多埠(難道有其它服務或是模組開了很多tcp埠?)。

突然想到,為什麼不看看到底建立了哪些連線呢?輸入netstat –an –p tcp > c:\a。txt,之後開啟a。txt一看瞬間傻眼,99%的連線都是到memcached的伺服器。第一反應,程式碼裡大量用到了memcached?查了程式碼之後發現,雖然一個請求可能確實進行了10次左右的memcache存取(不能說少,但是也不至於併發大到這個程度),但是我們用的客戶端有連線池,不可能併發大到一下子建立這麼多連線,而且我們的連線池最大的tcp連線也就是500。於是繼續檢視程式碼,在看到MemcachedClient的初始化程式碼後我瞬間石化,居然是每次都宣告一個區域性變數,然後初始化MemcachedClient類,而不是使用static變數來儲存MemcachedClient的例項。

實戰:一個由Java區域性變數引起的血案!

在之前調研memcache客戶端連線池bug問題的時候瞭解到,每一次例項化一個新的MemcachedClient物件都會新建一套連線池,所謂池就是具有一個最小連線,也就是在初始化池的時候就會建立這些最小連線,達到比較好的初始效能,這個值我們配置的是10(每一次請求都要新建10個tcp連線)。寫了一段迴圈測試了一下才執行100次迴圈,建立的tcp連線就到了1000(並且每一次初始化連線池都耗時300毫秒以上,cpu始終是100%,可見建立這麼多連線很耗效能),和猜想完全一致,也想到當時發現頁面重新整理一下連線就加了十幾個是這樣原因啊。在把區域性變數改為全域性的靜態變數之後,網站在100個併發的情況下依然執行良好,遠遠大於原先的期望(50個併發)。

從執行緒池懷疑到埠不夠用,再懷疑到keep alive問題,最後定位最終的原因(和新加的httpwebrequest模組沒關係),費了不少周折。那麼之後的解決方案很簡單了,排查專案中其它memcache的使用方式,然後修改memcache客戶端(我們使用的是Enyim。Caching),修改MemcachedClient的構造方法為private的,並且提供singleton入口即可。

實戰:一個由Java區域性變數引起的血案!

java

對於效能最佳化我的體會是,往往大多數的效能問題來自一到兩個根源問題,如果能找到並解決那麼或許可以有很大的效果。歡迎關注我的百家號:IT圈不亂!有想法的朋友,在評論區交流哦