天天看點

MySQL · 捉蟲動态 · 信号處理機制分析

背景

實際上的關閉動作就是向 mysqld 程序發送一個 kill pid 的信号,也就是 TERM , wait_for_pid 函數中就是不斷檢測 $MYSQL_DATADIR 下面的 pid 檔案是否存在,并且列印 ‘.’,是以上述問題應該是 mysqld 沒有正确處理接收到的信号。

信号處理機制

多線程信号處理

程序中的信号處理是異步的,當信号發送給程序之後,就會中斷程序目前的執行流程,跳到注冊的對應信号處理函數中,執行完畢後再傳回程序的執行流程。在多線程信号進行中,一般采用一個單獨的線程阻塞的等待信号集,然後處理信号,重新阻塞等待。線程的信号處理有以下幾個特點:

每個線程都有自己的信号屏蔽字(單個線程可以屏蔽某些信号)

信号的處理是整個程序中所有線程共享的(某個線程修改信号處理行為後,也會影響其它線程)

程序中的信号是遞送到單個線程的,如果一個信号和硬體故障相關,那麼該信号就會被遞送到引起該事件的線程,否是是發送到任意一個線程。

在程序中使用 sigprocmask 設定信号屏蔽字,線上程中使用 pthread_sigmask,他們的基本相同,pthread_sigmask 工作線上程中,失敗時傳回錯誤碼,而 sigprocmask 會設定 errno 并傳回 -1。參數 how 控制設定屏蔽字的行為,值為 SIG_BLOCK(把信号集添加到現有信号集中,取并集), SIG_SET_MASK(設定信号集為 set), SIG_UNBLOCK(從信号集中移除 set 中的信号)。set 表示需要操縱的信号集合。oset 傳回設定之前的信号屏蔽字,如果設定 set 為 NULL,可以通過 oset 獲得目前的信号屏蔽字。

sigwait 将會挂起調用線程,直到接收到 set 中設定的信号,具體的信号将會通過 sig 傳回,同時會從 set 中删除 sig 信号。 在調用 sigwait 之前,必須阻塞那些它正在等待的信号,否則在調用的時間視窗就可能接收到信号。

發送信号到指定線程,如果 sig 為 0,可以用來判斷線程是否還活着。

man pthread_sigmask 裡面給了一個例子:

執行一下:

測試了一下,把上面代碼的 pthread_sigmask 替換成 sigprocmask ,同樣能夠正确執行,說明線程也能夠繼承原程序的屏蔽字,不過還是盡量使用 pthread_sigmask, 表述清楚點,而且說不定還有其它坑。

MySQL 信号處理

MySQL 是典型的多線程處理,它的信号處理形式和上一小節介紹的差不多,在 mysqld 啟動的時候調用 my_init_signal 初始化信号屏蔽字,把需要信号處理線程處理的信号屏蔽起來,然後啟動信号處理函數,入口是 signal_hand 。

在 my_init_signal 函數中,設定 SIGSEGC, SIGABORT, SIGBUS, SIGILL, SIGFPE 的處理函數為 handle_fatal_signal,把 SIGPIPE,SIGQUIT, SIGHUP, SIGTERM, SIGTSTP 加入到信号屏蔽字裡,調用 sigprocmask 和 pthread_sigmask 設定屏蔽字。這一系列動作是在 mysql 啟動其它輔助線程之前完成的動作,意圖很明顯,就是讓之後的線程都繼承設定的信号屏蔽字,把所有的信号交給信号處理線程去處理。

signal_hand 函數首先把需要處理的信号放到信号集合裡去,然後完成 create_pid_file ,data 目錄下的 pid 檔案實際上是由信号處理線程建立的。接着等待 mysqld 完成啟動,各個線程之間需要同步,核心代碼是一個死循環,通過 my_sigwait 調用 sigwait 阻塞的等待信号的到來。我們目前主要關心 SIGTERM 的處理,和 SIGQUIT, SIGKILL 處理方式相同,都是調用 kill_server 關閉整個資料庫。

Bug Fix

文中開頭的連結中提到 loose-rpl_semi_sync_master_enabled = 0 關閉就不會有問題, 如果為 1 就會出現無法關閉的情況,順着這個線索尋找,rpl_semi_sync_master_enabled 在主備使用 semisync 情況下控制啟動 Master 節點的 Ack Receiver 線程,初始化階段的調用堆棧為:

而 init_common_variables 的調用是在 my_init_signal 之前,也就是 Ack Receiver 線程沒有辦法繼承信号屏蔽字,不會屏蔽 SIGTERM 信号。在 my_init_signal 中還有一段這樣的代碼:

對于信号的修改的作用于整個程序的,也就是說之前啟動的 Ack Receiver 線程沒有信号屏蔽字,而且注冊了信号處理函數。當 SIGTERM 發生後,信号處理線程和 Ack Receiver 線程都可以接收信号處理,信号被随機的分發(測試高機率都是發給 Ack Receiver),print_signal_warning 僅僅列印資訊到 errlog,就出現了無法關閉 mysqld 的情況了。

修改也比較簡單,把 initObject 的操作放到 my_init_signal 之後就好,注意不能把 init_common_variables 整個移到 my_init_signal 之前,因為 my_init_signal 裡面還有要初始化的變量呢。