1. 硬件配置
hwconfig
Summary: Intel S5500WBV, 2 x Xeon E5620 2.40GHz,23.5GB / 24GB 1067MHz
System: Intel S5500WBV
Processors: 2 x Xeon E5620 2.40GHz 133MHz FSB (HTenabled, 8 cores, 16 threads)
Memory: 23.5GB / 24GB 1067MHz == 6 x 4GB, 2 xempty
千兆網卡
2. 測試case
長連接每個連接發送100,000 select 1的請求報文(即請求報文大小爲13,響應報文大小爲56)。建議在測試server前先用netperf測試一下網絡環境,正常的局域網網絡應該發送上面的報文應該可以達到300,000 PPS;併發60直接連接mysql-server qps可以達到90000。CMD:bin/mysql-proxy --daemon --pid-file=log/proxy.pid--proxy-address=:4040 --log-level=debug --log-file=log/proxy.log--proxy-backend-addresses=10.232.64.76:17907 --event-threads=16,即下面的測試不走lua腳本,也沒有連接池 c-cons:b-cons = 1:1。
3. 實驗數據
Event-threads | Concurrent-cons | QPS | CPU | CS |
1 | 1 | 3800 | 30% | |
1 | 10 | 30000 | 96% | |
1 | 30 | 33000 | 100% | 400 |
4 | 1 | 4200 | 48% | |
4 | 10 | 37400 | 300% | |
4 | 30 | 53000 | 360% | 120000 |
4 | 50 | 69131 | 350% | 150000 |
4 | 80 | 67000 | 350% | 150000 |
12 | 1 | 3797 | 50% | 110000 |
12 | 5 | 20000 | 300% | 500000 |
12 | 12 | 37233 | 600% | 600000 |
12 | 30 | 51890 | 800% | 480000 |
12 | 50 | 53255 | 900% | 400000 |
12 | 80 | 53426 | 950% | 350000 |
16 | 10 | 29308 | 850% | 832000 |
16 | 30 | 48390 | 1110% | 580000 |
16 | 50 | 48056 | 1200% | 400000 |
16 | 80 | 47000 | 1350% | 300000 |
在測試的過程中通過mpstat -I SUM -u -P ALL 1,發現所有的網卡中斷都只跑在0 CPU上,之前的網卡SMP是設置好的,然後使用perf導致機器掛了幾次,重啓後之前的SMP設置就無效了。所有上面的所有數據都是沒有開通網卡SMP的數據(網卡SMP見http://jasonwu.me/category/Linux)。後面又試着把網卡的SMP開啓,發現數據還不如上面的,不過差別不大低2000左右,本來以爲開啓了可以將intr及soft分攤到各個CPU上,以此來提高性能,最後攤是攤開了,但qps並沒有上去。看來性能並不在內核網絡處理上,集中式的單CPU處理已經可以達到上面我們的QPS甚至更高的要求,而開通SMP對於這種壓力不夠的影響可能是負的,至於原因超出了我現在的知識範圍(可能是代碼的親緣性上去了,歡迎大牛指點);當然我們這裏並不是說開通網卡的SMP會比不開好,只是我們現在的瓶頸還不在網卡的處理上,當我們上層的處理能力超過單CPU的網卡接收能力的話,那麼此時開通網卡SMP,應該就可以達到更高的性能。
4. Mysql-proxy的網絡模型
之前的文章裏說過mysql-proxy是通過libevent的事件驅動的模型,但缺少介紹多線程在之個模型中的作用及關係:在mysql-proxy上線程是event-thread,即處理event的線程。Mysql-proxy主要有三類event fd:listen fd、socketpair[2]、net-read(write)。第一個很明顯就是mysql的監聽端口默認4040,這個事件只註冊在main-thread線程上;第二個是一個socket對,即兩個socket fd,其中socketpair[0]用於讀(所有的線程都會關心這個event
readable,回調函數chassis_event_handle),socketpair[1]用於寫,當要add一個event(這個event就是第三類的event)的時候(chassis_event_add),會將該event放到一個全局的隊列裏,然後往socketpair[1]寫一個”.”,然後所有線程都會被喚醒一次,喚醒後它們從全局隊列裏取得剛纔保存的event,然後將這個event註冊到自己的event-base上,那麼當該event被觸發的話,將由新的這個取得的線程來處理,另外,被喚醒的線程將盡量多取點event,只要它能夠獲得取隊列的鎖;第三類就是用於實際報文傳送的read/write
socket(它們的回調函數都是network_mysqld_con_handle),這一類與第二類的關係是:當我們要去recv或send socket的時候出現EAGAIN 錯誤,那麼我們就應該去重新註冊一下該事件(所有的第三類事件都是非EV_PERSIST),此時就會調用chassis_event_add函數,這就將觸發新的event可能被別的線程獲得處理。
擴展一下當多個event-threads的時候將會出現什麼情況:①多個線程競爭一個event-queue;②當一個第三類socket出現EAGAIN,那麼將喚醒所有的線程(驚羣);③一個連接的client/severe socket不是固定在一個線程上執行,即任何的第三類event將隨機的在任意線程上運行,在每次交互過程中都會發生變化遷移。簡而言之就是event-thread不關心event是屬於哪個連接的,它只關心這個event可用與否,當出現不可用時,它就該這個event push到全局的event-queue,然後喚醒所有的線程去競爭它。
上面是mysql-proxy的網絡模型下面我們再來看一下核心處理流程。這個處理流程主要包括下面4個部分:報文接收發送event處理(network_mysqld_con_handle),該函數完成報文的讀寫、狀態的判斷轉移;然後調用plugin_call,該函數根據狀態獲得相應狀態下的hook function;再接下來執行狀態的回調函數(proxy_read_auth_result,proxy_send_query_result,proxy_connect_server…);最後就是回調函數再去調用lua
script裏的相應函數,完成定製的任務(如讀寫分離,輸出select報文,結果集等一系列的操作)。注:前面三個都是由代碼編譯後寫死了,但第4個過程我們可以定製,修改,這也是mysql-proxy留着一個擴展的地方。另外即使我們上面沒有指定lua script,但是前面三個過程都是要走的,只是第4個不會被調用。
5. 數據分析
瞭解了網絡模型後,我們再來看一下上面的數據:首先對於1個event-thread的30個連接的時候CPU已經喫滿了,顯然再多個併發連接數也無益了。所以我們可以看到當event-thread增加到4的時候,同樣的併發連接數QPS都上去了,另一個顯著的變化是CS也上去了,這個就是因爲上面我們說的驚羣現象,它把所有的線程都喚醒一次,而且醒來後還得去競爭event-queue,這就無形中增加了多個上下文切換的時機。所以同樣可以解釋當併發越多的時候,CS就越大(在併發連接數相同的情況下),另外一個比較好解釋的現象是在event-thread相同的情況下併發連接數越多,CS上升,然而奇怪的是當併發連接數再大的話CS反而下降,這個可能的原因是因爲chassis_event_handle在取event的時候是儘可能多的取,當併發連接數多的時候,它可能一次會獲得多個event,減少了event的擴散。
當然最令我不解的是爲什麼event-thread增加了QPS卻下降了,我們以4個thread爲參考值,那麼我們系統16CPU,即使不能線性,那麼折半應該不過分吧,那麼多少也得69131 * 2 ~= 100000這個級別。當然有一種可能的解釋是CS太多了,但以我個人的經驗一般CS是果並不是因,也就是有其它真正的原因導致CS高,然後間接影響QPS,或者並不是CS的原因(因爲當解決了這個瓶頸之後CS可能也會跟着下降下去),另外在16個event-threads的時候每個CPU上有user:sys:soft:idle大概=12%:65%:8%:15%;上面的現象MS現在通過代碼不能那麼容易的解釋(個人實力未達),所以只能藉助各種工具。
6. 工具分析
爲了防止某個邏輯CPU單獨執行某一種操作,影響測試結果,所以我們在進行性能排查的時候,我們又把網卡SMP設置爲多個CPU。我們這裏使用的檢測工具是perf。通過perf record -g -p PID的方式,獲得詳細性能數據:
可以看到兩個大頭分別是調用futex_wake、futex_wait(http://hi.baidu.com/luxiaoyi/blog/item/3db9a302ba9a0f074bfb51e3.html);上面我們說到核心處理流程有4個過程,通過上面的perf report可以看到出現了前面兩個過程(network_mysqld_con_handle,plugin_call),我們再回代碼裏:
Plugin_call(…) {… LOCK_LUA(srv->priv->sc); <===> g_mutex_lock(sc->mutex); ret = (*func)(srv, con); UNLOCK_LUA(srv->priv->sc); } |
Perf果然是神器,之前一直在看網絡模型,懷疑的重點也在驚羣上,完全沒注意到這個地方。至於原因也非常的簡單,雖然說在第二個過程plugin_call還沒開始調用lua相關函數,但是在第三個過程裏就要使用到,所有的線程共享相同的一些全局luatable,可見這個鎖粒度還是挺大的。
7. 驗證
這裏只是先說一下理論:一、防止驚羣,將由一對socketpair改爲多對即每個線程一對;二、現在的所有event是隨機的由任何一個線程來處理,是否可以將一對event(client與backend server)固定爲一個線程來處理,減少線程數據遷移?三、減少LOCK_LUA(srv->priv->sc)的鎖粒度,或者去掉lua部分。
歡迎大牛指正。