先說說背景:一個LAMP在線測試網站,日均PV1萬左右,比賽時一小時就好幾萬吧。目前數據庫大約有30萬條記錄。服務器配置很高。近期出現性能問題,當訪問量增大的時候,數據庫服務器的壓力非常大,mysql的內存占用率通常能到400%,這時候基本不能提供服務了,連網站頁面都打不開。理論上說現在的數據量還不算大,訪問量也不是很大,服務器的配置也很高,出現這種狀況是不正常的。這個問題究結了很久,昨天終於找到問題的原因了,記錄一下,以后遇到類似的問題可以有個參考。先看一張圖片,這是glances監控軟件的截圖,可以看到紅色的496.6就是mysql的CPU占用率
之前遇到這個問題,只能重啟mysql服務,但這只是暫時的,重啟之后不到1分鍾CPU占用率接着就上來了,不是長久之計。
然后考慮修改數據庫和apache的配置文件,加大緩存空間,但效果還是不明顯。
后來又考慮是不是路由器的問題,因為目前是兩台服務器通過一個小路由器連起來,組成一個小局域網,一個放apache(壓力很小),另一個服務器放數據庫(壓力很大),交換數據都要先經過路由器。於是開始懷疑路由器的性能問題,干脆直接用一根網線把兩個服務器連起來(服務器有兩個網卡)。這樣兩個服務器就能直接通信了。事實證明,這樣還是不能解決數據庫壓力大的問題。
接下來,還是各種糾結,想過各種方法,比如換nginx,換其他數據庫,弄個數據庫集群什么的,都比較麻煩,還需要修改php代碼,比較費勁,關鍵是php這東西看着就惡心……
有一天,我發現了memcached,memcached是一個緩存系統,這么說吧,假如有一個查詢語句,很費時間,如果沒有緩存的話,每次刷新頁面就要訪問數據庫查詢一次,這樣數據庫壓力就比較大。 如果有緩存就不一樣了,如果要查詢的數據在緩存中存在的話,就直接從緩存中取出來,這樣就不用去數據庫查詢了,如果訪問量大的話可以明顯減少數據庫的查詢次數,當然也減輕了數據庫的壓力。先貼一段沒有緩存的代碼:
$list = $statics->getFirstTenList();
下面是加上緩存的代碼:
$list = null; if (!($list = $cache->get('top_ten_list'))) { $list = $statics->getFirstTenList(); $cache->set('top_ten_list', $list, 7200); }
沒有緩存的代碼,每次都要調用查詢函數從數據庫獲取數據。下面的有緩存的就不一樣了。當需要查詢一個數據之前,首先看看緩存里面有沒有,就是代碼中$cache->get(),如果從緩存取得了,就不用執行那條查詢數據庫的語句了。如果緩存服務器沒有,就要從數據庫里面查詢,查詢以后,用$cache->set()方法就剛才的查詢結果保存到緩存服務器,並設置這個緩存存在的時間,這里是7200秒,7200秒之后,這個緩存就失效了,就要從新更新。還有一點比較重要,就是要給每一個緩存取一個名字,不同的緩存名字不能相同,緩存服務器是根據緩存的名字來區分緩存,如果名字相同的話,從緩存取出來的數據就不是我們需要的數據。舉個例子來說:有兩個比賽,比賽的排名頁面做一個緩存,如果兩個緩存名字都叫ranklist,首先刷新一下第一個比賽的排名頁面,那么第一個比賽的排名數據就存在緩存服務器了。如果這時再刷新一下第二個比賽的排名頁面,因為ranklist在緩存中存在,直接就從緩存中取出來了,但是取出來的數據並不是我們需要的,這種問題還是比較小的。如果是不同類型的數據,那么網站就亂了。
緩存是個好東西,然后打算通過大量增加緩存來減輕服務器的壓力。但后來我發現,這樣也不行。因為我們的網站是一個在線測評系統,是一個實時動態的系統,一個用戶提交一個答案以后,想立即看到評判結果,當答案正確的時候,要立即更新排名信息。如果增加緩存的話,從緩存中取出來的信息就不是最新的,不能顯示實時的排名等信息。這就好比你訂了一張票,但是系統好久才提示你訂票成功,中間的等待時間是最難熬的。還有一個原因也限制了緩存的使用,就是對於大量的頁面,比如說服務器上有2000個題,一個題一個頁面,如果一個頁面一個緩存的話肯定會降低緩存的性能。但是對於一些數據,長時間變動不大,比如說總做題數的排名,就可以考慮增加一個緩存,可以設置半天或者一天的生存時間,也就是半天或者一天更新一次。
由於大量緩存並不適用與我們這種實時性很高的系統,因此我並沒有增加很多緩存。數據庫壓力還是那么大。還是沒找到問題的根源。
找到一本mysql性能優化的書,然后就開始研究,首先想到的還是加索引,后來用show index from table_name來查看,發現里面已經對常用的查詢詞加索引了,我甚至開始考慮是不是因為索引太多加重了數據庫的壓力?經過一些測試(刪除某些索引),發現還是不行,看來通過增加索引已經不能很好得解決OJ問題。
后來發現了一個語句,是 show processlist,這條命令的作用是顯示哪些線程正在執行,執行了多長時間,還有一些其他信息,看截圖:
上面的圖片是在負載不是很大的時候截的,從上圖我們能看到有兩條sql查詢語句已經執行了7秒了,還有一個正在往臨時表拷貝數據的操作。真正比賽的時候比這個問題嚴重多了!有幾個sql語句執行時間超過10秒,總的sql查詢語句個數有200多個,當然大部分線程是被阻塞的。 在這里我們就能很明顯看出來到底是哪一條sql語句在浪費時間,接下來,我們就要找到底這條sql查詢語句是從哪發起的,從php源嗎中找這條語句,如果一條一條找就費勁了,這里我們用強大的grep命令來幫助我們查找。
很明顯,這幾條語句就在一個文件里面,讓我們打開這個文件看看,下面是其中的一個函數,包含上面的查詢語句
這個函數是從哪調用的?再找
看到了吧,這里有八條類似的語句,這8條語句的作用是什么?其中第一條的作用是從30萬的表里找到今天提交的並且AC的題目總數,第二條就是統計今天的提交總數,下面幾條分別是統計這周,這月,這年的AC和提交總數。顯示提交狀態的solution表中目前大約有30萬條信息,並且隨時都會更新,只要有人提交就會插入新數據並更新一些數據。在數據庫理論里面將,統計和插入更新是互相沖突的,這里就需要加鎖,當執行查詢語句的時候,將這個表鎖住,防止其他數據更新而影響查詢結果。也就是說,當執行上面的查詢語句時,數據庫里面的其他語句就被阻塞了,就被迫去sleep。在提交高峰期,數據庫里面被阻塞的語句能達到200多條甚至更多,mysql的CPU占用率達到能達到500%,肯定不能對外提供服務了!
接下來,我們要找找到底是在哪個頁面里面有這個統計,哪個頁面調用了這個函數,(強大的grep)
當看到上面的結果的時候,我的心凌亂了~~靠,怎么回事?這個多頁面都有這個統計,然后打開頁面文件找找,找到了
(上圖是我修改之后的查詢結果,大部分都被我用//注釋掉了,原來是沒有注釋的~)
在一個不起眼的邊欄里,我看到了上面的一個統計。我調查了一些用戶,你們注意過這個統計信息嗎?你們關注過這個統計信息嗎?答案和我想的一樣,沒有!誰都不關注那這個統計還有什么意義?不知道當初設計者怎么想到的這個功能。當初數據量小的時候沒有這個問題,隨着數據量增加,問題就暴露出來了。就是這8個數字,拖慢了整個網站的速度!
接下來解決方案也很清晰了,兩種方法:1.刪除這個統計,2.給這個統計增加緩存。綜合各方面的意見,我決定只在總的排名頁面保留這個統計,當然是要增加緩存的,6個小時更新一次。其他頁面全部去掉!
解決這個問題之后,再試試網站的訪問速度,那叫一個快啊!淚奔啊!糾結了這么久的性能問題終於解決啦!orz