首先這個問題,我只是其中參與者之一。但這個問題很有參考意義,特記錄下來。
還有我第一次用“徹底”這個詞,不知道會不會有人噴?其實,還有一些問題,也不是特別清楚。比如說什么是CPU流水(我又不是硬件工程師)。
問題現象
MySQL現網數據庫切換到新的物理服務器時,出現了業務查詢超時異常問題。
詳細過程不再熬述了,總之對比新舊硬件環境的不同。初步懷疑是新服務器CPU的問題。
定位過程
現網肯定不能不停重試,於是在本地服務器用sysbench壓測。
查看CPU占比,sys占比特別高。vmstat顯示context switch高。
通過perf top查看調用棧。
調用棧如下。
問題原因
如上,可以看到調用棧,spin_lock占用了很大比例。
與美團的CPU原因類似。因為某些你懂的原因,具體細節就不多說了。因為我主要是講解一下何為spin lock。
而且看完全篇,你就會發現其實內容遠比你想象中的多。
https://dev.mysql.com/doc/refman/8.0/en/innodb-parameters.html#sysvar_innodb_spin_wait_pause_multiplier
什么是自旋鎖
多線程中,對共享資源進行訪問,為了防止並發引起的相關問題,通常都是引入鎖的機制來處理並發問題。
獲取到資源的線程A對這個資源加鎖,其他線程比如B要訪問這個資源首先要獲得鎖,而此時A持有這個資源的鎖,只有等待線程A邏輯執行完,釋放鎖,這個時候B才能獲取到資源的鎖進而獲取到該資源。
這個過程中,A一直持有着資源的鎖,那么沒有獲取到鎖的其他線程比如B怎么辦?通常就會有兩種方式:
1. 一種是沒有獲得鎖的進程就直接進入阻塞(BLOCKING),這種就是互斥鎖
2. 另外一種就是沒有獲得鎖的進程,不進入阻塞,而是一直循環着,看是否能夠等到A釋放了資源的鎖。
自旋鎖(spin lock)是一種非阻塞鎖,也就是說,如果某線程需要獲取鎖,但該鎖已經被其他線程占用時,該線程不會被掛起,而是在不斷的消耗CPU的時間,不停的試圖獲取鎖。
自旋鎖避免了進程上下文的調度開銷,因此對於線程只會阻塞很短時間的場合是有效的。因此操作系統的實現在很多地方往往用自旋鎖。
為什么要使用自旋鎖
互斥鎖有一個缺點,他的執行流程是這樣的 托管代碼 - 用戶態代碼 - 內核態代碼、上下文切換開銷與損耗,假如獲取到資源鎖的線程A立馬處理完邏輯釋放掉資源鎖,如果是采取互斥的方式,那么線程B從沒有獲取鎖到獲取鎖這個過程中,就要用戶態和內核態調度、上下文切換的開銷和損耗。所以就有了自旋鎖的模式,讓線程B就在用戶態循環等着,減少消耗。
自旋鎖的本質
Critical Section Integration (CSI)
本質上自旋鎖產生的效果就是一個CPU core 按順序逐一執行關鍵區域的代碼,所以在我們的優化代碼中將關鍵區域的代碼以函數的形式表現出來,當線程搶鎖的時候,如果發現有沖突,那么就將自己的函數掛在鎖擁有者的隊列上,然后使用MCS進入spinning 狀態,而鎖擁有者在執行完自己的關鍵區域之后,會檢測是否還有其他鎖的請求,如果有那么依次執行並且通知申請者,然后返回。可以看到通過這個方法所有的共享數據更新都是在CPU私用緩存內完成,能夠大幅度減少共享數據的遷移,由於減少了遷移時間,那么加快了關鍵區域運行時間最終也減少了沖突可能性。
提升自旋鎖spinlock的性能-pause指令
自旋鎖 pause版權看源碼的時候get的一個新的知識點,可以提升自旋鎖spinlock的性能-pause指令,看到的源碼如下:
# define UT_RELAX_CPU() asm ("pause" )
# define UT_RELAX_CPU() __asm__ __volatile__ ("pause")
經過上網查找資料pause指令。當spinlock執行lock()獲得鎖失敗后會進行busy loop,不斷檢測鎖狀態,嘗試獲得鎖。這么做有一個缺陷:頻繁的檢測會讓流水線上充滿了讀操作。另外一個線程往流水線上丟入一個鎖變量寫操作的時候,必須對流水線進行重排,因為CPU必須保證所有讀操作讀到正確的值。流水線重排十分耗時,影響lock()的性能。
自旋鎖spinlock剖析與改進Pause指令解釋(from intel):Description Improves the performance of spin-wait loops. When executing a “spin-wait loop,” a Pentium 4 or Intel Xeon processor suffers a severe performance penalty when exiting the loop because it detects a possible memory order violation. The PAUSE instruction provides a hint to the processor that the code sequence is a spin-wait loop. The processor uses this hint to avoid the memory order violation in most situations, which greatly improves processor performance. For this reason, it is recommended that a PAUSE instruction be placed in all spin-wait loops.
MySQL spin lock處理代碼
MySQL關於spin lock的部分代碼。如下代碼可以看到MySQL默認作了30次(innodb_sync_spin_loops=30)mutex檢查后,才放棄占用CPU資源。
rw_lock_sx_lock_func( // 加sx鎖函數 { /* Spin waiting for the lock_word to become free */ os_rmb; while (i < srv_n_spin_wait_rounds && lock->lock_word <= X_LOCK_HALF_DECR) { if (srv_spin_wait_delay) { ut_delay(ut_rnd_interval( 0, srv_spin_wait_delay)); // 加鎖失敗,調用ut_delay } i++; } spin_count += i; if (i >= srv_n_spin_wait_rounds) { os_thread_yield(); //暫停當前正在執行的線程對象(及放棄當前擁有的cup資源) } else { goto lock_loop; //MySQL關於spin lock的部分代碼。如下代碼可以看到MySQL默認作了30次(innodb_sync_spin_loops=30)mutex檢查后,才放棄占用CPU資源。 os_thread_yield(); //暫停當前正在執行的線程對象(及放棄當前擁有的cup資源) } ... ulong srv_n_spin_wait_rounds = 30; ulong srv_spin_wait_delay = 6;
注:上面代碼,線程中的yield()方法說明
yield 多線程版權Thread.yield()方法作用是:暫停當前正在執行的線程對象(及放棄當前擁有的cup資源),並執行其他線程。yield()做的是讓當前運行線程回到可運行狀態,以允許具有相同優先級的其他線程獲得運行機會。因此,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。
每次ut_delay默認執行pause指令300次( innodb_spin_wait_delay=6*50)
ut_delay( /*=====*/ ulint delay) /*!< in: delay in microseconds on 100 MHz Pentium */ { ulint i, j; UT_LOW_PRIORITY_CPU(); j = 0; for (i = 0; i < delay * 50; i++) { j += i; UT_RELAX_CPU(); } UT_RESUME_PRIORITY_CPU(); return(j); } # define UT_RELAX_CPU() asm ("pause" ) # define UT_RELAX_CPU() __asm__ __volatile__ ("pause")
操作系統中,SYS和USER這兩個不同的利用率代表着什么?
操作系統中,SYS和USER這兩個不同的利用率代表着什么?或者說二者有什么區別?
簡單來說,CPU利用率中的SYS部分,指的是操作系統內核(Kernel)使用的CPU部分,也就是運行在內核態的代碼所消耗的CPU,最常見的就是系統調用(SYS CALL)時消耗的CPU。而USER部分則是應用軟件自己的代碼使用的CPU部分,也就是運行在用戶態的代碼所消耗的CPU。比如ORACLE在執行SQL時,從磁盤讀數據到db buffer cache,需要發起read調用,這個read調用主要是由操作系統內核包括設備驅動程序的代碼在運行,因此消耗CPU計算到SYS部分;而ORACLE在解析從磁盤中讀到的數據時,則只是ORACLE自己的代碼在運行,因此消耗的CPU計算到USER部分。
那么SYS部分的CPU主要會由哪些操作或是系統調用產生呢?具體如下所示。
1> I/O操作。比如讀寫文件、訪問外設、通過網絡傳輸數據等。這部分操作一般不會消耗太多的CPU,因為主要的時間消耗會在1/O操作的設備上。比如從磁盤讀文件時,主要的時間在磁盤內部的操作上,而消耗的CPU時間只占I/O操作響應時間的一少部分。只有在過高的並發I/O時才可能會使得SYS CPU 有所增加。
2> 內存管理。比如應用程序向操作系統申請內存,操作系統維護系統可用內存,交換空間換頁等。其實與ORACLE類似,越大的內存,越頻繁的內存管理操作,CPU的消耗會越高。
3> 進程調度。這部分CPU的使用,在於操作系統中運行隊列的長短,越長的運行隊列,表明越多的進程需要調度,那么內核的負擔就越高。
4> 其他,包括進程間通信、信號量處理、設備驅動程序內部的一些活動等等。
什么是用戶態?什么是內核態?如何區分?
一般現代CPU都有幾種不同的指令執行級別。
在高執行級別下,代碼可以執行特權指令,訪問任意的物理地址,這種CPU執行級別就對應着內核態。
而在相應的低級別執行狀態下,代碼的掌控范圍會受到限制。只能在對應級別允許的范圍內活動。
舉例:
intel x86 CPU有四種不同的執行級別0-3,linux只使用了其中的0級和3級分別來表示內核態和用戶態。
系統調用與context switch
進程上下文切換,是指從一個進程切換到另一個進程運行。而系統調用過程中一直是同一個進程在運行
系統調用過程通常稱為特權模式切換,而不是上下文切換。當進程調用系統調用或者發生中斷時,CPU從用戶模式(用戶態)切換成內核模式(內核態),此時,無論是系統調用程序還是中斷服務程序,都處於當前進程的上下文中,並沒有發生進程上下文切換。
當系統調用或中斷處理程序返回時,CPU要從內核模式切換回用戶模式,此時會執行操作系統的調用程序。如果發現就需隊列中有比當前進程更高的優先級的進程,則會發生進程切換:當前進程信息被保存,切換到就緒隊列中的那個高優先級進程;否則,直接返回當前進程的用戶模式,不會發生上下文切換。
system call
System calls in most Unix-like systems are processed in kernel mode, which is accomplished by changing the processor execution mode to a more privileged one, but no process context switch is necessary
context switch
Some operating systems(Not include Linux) also require a context switch to move between user mode and kernel mode tasks. The process of context switching can have a negative impact on system performance
通過vmstat查看context switch
一般vmstat工具的使用是通過兩個數字參數來完成的,第一個參數是采樣的時間間隔數,單位是秒,第二個參數是采樣的次數,如:
root@local:~# vmstat 2 1
procs -----------memory---------- ---swap-- -----io---- -system-- ----cpu----
r b swpd free buff cache si so bi bo in cs us sy id wa
1 0 0 3498472 315836 3819540 0 0 0 1 2 0 0 0 100 0
context switch 高,導致的爭用其它案例
有很多種情況都會導致 context switch。MySQL 中的 mutex 和 RWlock 在獲取不成功后,短暫spin,還不成功,就會發生 context switch,sleep,等待喚醒。
在 MySQL中,mutex 和 RWlock導致的 context switch,一般在show global status,show engine innodb mutex,show engine innodb status,performance_schema等中會體現出來,針對不同的mutex和RWlock等待,可以采取不同的優化措施。
除了MySQL的mutex和RWlock,還發現一種情況,是MySQL外的mutex競爭導致context switch高。
典型症狀:
MySQL running 高,但系統 qps、tps 低
系統context switch很高,每秒超過200K
在 MySQL 內存查不到mutex和RWlock競爭信息
SYS CPU 高,USER CPU 低
並發執行的SQL中出現timestamp字段,MySQL的time_zone設置為system
分析
對於使用 timestamp 的場景,MySQL 在訪問 timestamp 字段時會做時區轉換,當 time_zone 設置為 system 時,MySQL 訪問每一行的 timestamp 字段時,都會通過 libc 的時區函數,獲取 Linux 設置的時區,在這個函數中會持有mutex,當大量並發SQL需要訪問 timestamp 字段時,會出現 mutex 競爭。
MySQL 訪問每一行都會做這個時區轉換,轉換完后釋放mutex,所有等待這個 mutex 的線程全部喚醒,結果又會只有一個線程會成功持有 mutex,其余又會再次sleep,這樣就會導致 context switch 非常高但 qps 很低,系統吞吐量急劇下降。
解決辦法:設置time_zone=’+8:00’,這樣就不會訪問 Linux 系統時區,直接轉換,避免了mutex問題。
問題解決對策
通過修改spin lock相應參數,問題現象得到了緩解。
至於CPU硬件本身是不是有可能存在問題,這個是留待他人解決吧。
可不能走自己的路,讓他人無路可走。
總結
spin lock通過pause指令強制占有CPU,而使自己不被換出CPU,減少context switch發生的頻率。從而實現系統的高效運行。
此例問題的原因是因為新的物理服務器的CPU PAUSE指令周期遠小於舊的物理服務器。導致CPU context switch顯著高於舊的服務器,從而影響user的運行(表象為查詢超時)。
一兩句話,能說清楚的問題,我居然說了這么多。看來,能把簡單的事情,說復雜也是一種本事。哈哈。
參考資料
實在是太多了,就不列出來了。在此感謝那些提供了信息分享的朋友們。如引用了您的原文,但沒有指出出處,還請見諒。
附加信息--CPU pause指令說明
內核中的嵌入式匯編代碼”rep;nop”會被編譯為PAUSE指令,Intel Pentium4以后的CPU支持,之前的就相當於NOP。為啥rep;nop不是指循環執行nop同時遞減ecx的值呢,這還真是之前困擾過我的問題,有篇文章用代碼解釋了這個問題-鏈接-。而博主自己也在虛擬機里面將cpu_relax()中的rep;nop替換為nop,前后均在host上抓取vmexit和trace kvm_exit數據,結果顯示,PAUSE_INSTRUCTION造成的退出消失了。
上文中還翻譯了Intel的spec,大概說了下pause指令有兩種功能,一方面是可以解決memory order violation問題,另一方面可以降低循環等待的能耗。
什么是memory order violation?看這里。簡單說下就是cpu的pipeline會根據執行代碼的情況來預測即將執行的指令,提前將這些指令放入流水線中,達到一定的並行計算優化性能的目的,但是總會事與願違,在spinlock代碼實現中,如果不加入pause指令,很容易造成pipeline被“讀入lockvar,比較lockvar是否為0”這樣指令刷屏了,這就造成了即使在lockvar已經被別的CPU更新為非0值的時候,pipeline中出現了無效指令,這種情況就是memory order violation,即本應在寫內存后讀取內存值的動作發生在了寫入之前,於是cpu就暴力的把pipeline全部flush掉,這樣就造成了性能損失,因為這里是在等待一個lockvar被改變,只要及時的對這個動作做出反應就可以了。
pause指令的出現可以給cpu一個提示,這里不要給我緩存指令,等前面的執行完再看后面的,於是大大的減少了出現無效指令的可能性(此時出現這個情況的時間窗口為:讀取了lockvar,但是cmp還沒有執行;與之前比起來,時間窗口大大大的縮小了,之前是預讀取了很多次的“讀取lockvar,與0比較,跳入再一次的讀取比較的分支”這三個動作,最后一個跳轉也是預測的)。
節能的效果更好理解,其實這里就是在原地踏步,不是要求一定時間內踏的次數多,而是要對出現的情況及時處理。所以加入pause可以讓處理更及時,而且不必把能量浪費在多出來的無意義的指令上。