這一周處理了幾個(gè)之前一直出現(xiàn)一直沒有正視的坑,總結(jié)一下。
sqlalchemy連接池
我寫了一個(gè)簡(jiǎn)單的內(nèi)網(wǎng)dns管理系統(tǒng),用到flask和sqlalchemy。主要就是通過web頁(yè)面和api對(duì)dns記錄進(jìn)行增刪改查,所有這些操作都寫入數(shù)據(jù)庫(kù),每次更改后產(chǎn)生一個(gè)celery任務(wù)來異步的重新生成dns的配置文件。
看起來一個(gè)簡(jiǎn)單的問題,但是在celery中讀取數(shù)據(jù)庫(kù)的時(shí)候出現(xiàn)了問題,每天早上都會(huì)報(bào)錯(cuò)Mysql has gone away,我重啟服務(wù)恢復(fù)正常,第二天早上又會(huì)出現(xiàn)同樣的問題,但是在flask的路由下調(diào)用的所有數(shù)據(jù)庫(kù)操作從未出現(xiàn)任何問題。咨詢DBA,DBA說數(shù)據(jù)庫(kù)是正常的,所以一定是我的程序處理有問題了。我在網(wǎng)上各種資料查詢,最后找到結(jié)果。
flask_sqlalchemy默認(rèn)使用數(shù)據(jù)庫(kù)連接池來對(duì)理數(shù)據(jù)庫(kù)連接進(jìn)行復(fù)用以減少建立連接的開銷,他默認(rèn)會(huì)對(duì)連接池中的每個(gè)連接隔2小時(shí)檢查一次是否可用,如果不可用就銷毀掉,創(chuàng)建新的連接放到連接池中,我們使用db.session的時(shí)候就需要從連接池中拿到一個(gè)連接,使用完后調(diào)用db.session.close來將連接放回到連接池(close并不是關(guān)閉連接,關(guān)閉連接的操作由連接池來管理)。但是我在路由函數(shù)中從來沒有使用過db.session.close,也從來沒有出現(xiàn)過問題,是因?yàn)?code>flask_sqlachemy會(huì)自動(dòng)在請(qǐng)求上下文結(jié)束時(shí)自動(dòng)將連接放回連接池,這樣放回連接池的連接就會(huì)每隔兩小時(shí)接受一次檢查。但是在celery中查詢的時(shí)候問題就會(huì)出現(xiàn),celery中并沒有請(qǐng)求上下文,所有查詢完以后并不會(huì)將連接放回連接池,也就是說celery中一直使用同一個(gè)連接,從來沒有放回過連接池,也就從來不會(huì)被檢查連接的有效性,所以連接斷開的時(shí)候進(jìn)行查詢,導(dǎo)致報(bào)錯(cuò)。
連接為什么會(huì)斷開呢?原來mysql會(huì)把閑置不用超過8小時(shí)的連接主動(dòng)關(guān)掉,而dns變更大多是白天上班的時(shí)候變更,晚上基本不會(huì)變更,這樣超過8小時(shí)以后mysql的連接已經(jīng)被斷開了,而celery中使用的連接還是前一天早上重啟拿到的連接,肯定失效了,拿去查詢數(shù)據(jù)失敗,拋出異常。
所以我后來在celery中每次查詢結(jié)束后都會(huì)調(diào)用db.session.close,這樣會(huì)將連接放回連接池,下次查詢會(huì)重新從連接池中獲取新的連接,這個(gè)連接每個(gè)兩個(gè)小時(shí)會(huì)被檢查,因此再也沒有出現(xiàn)過這個(gè)問題。
supervisord 與 celery
這個(gè)問題出現(xiàn)在發(fā)布系統(tǒng)中,也是讓我頭疼了好久的一個(gè)問題,這一周終于得到解決。我們的發(fā)布系統(tǒng)采用celery異步的執(zhí)行ansible腳本來進(jìn)行發(fā)布的,每次發(fā)布大約需要1.5到5分鐘不等的時(shí)間。我們使用supervisord來管理web端和celery端的啟動(dòng),每次修改完代碼之后上線重啟發(fā)布系統(tǒng)之后就會(huì)出現(xiàn)很多發(fā)布任務(wù)卡死不再繼續(xù)執(zhí)行,每次都需要手動(dòng)把卡死的發(fā)布任務(wù)kill掉,然后重發(fā),這周終于找到問題根源了。
眾所周知celery有一個(gè)warm shutdown的機(jī)制,就是給celery master發(fā)送一個(gè) SIG_TERM信號(hào),celery master就會(huì)通知其管理的workers停止接受新的任務(wù),然后等待手頭的任務(wù)結(jié)束,等到所有workers均完成任務(wù)后,master會(huì)kill掉所有workers,然后結(jié)束運(yùn)行。我們的發(fā)布系統(tǒng)需要的就是這個(gè)功能,但是我一直感覺supervisord停止celery的時(shí)候并沒有采取warm shutdown的方式,因?yàn)槊看味紩?huì)又一些celery worker進(jìn)程未被停止,而且永遠(yuǎn)不會(huì)結(jié)束運(yùn)行,這些celery worker中執(zhí)行的發(fā)布任務(wù)就會(huì)卡死不動(dòng),這個(gè)現(xiàn)象叫我百思不得其解。
于是我去查supervisord的文檔,發(fā)現(xiàn)他停止進(jìn)程的時(shí)候確實(shí)默認(rèn)就發(fā)送SIG_TERM信號(hào),而且我也沒有這方面的配置,說明應(yīng)該是使用了warm shutdown的,看celery worker打印出來的日志也確實(shí)是有warm shutdown,但是為什么會(huì)出現(xiàn)這么詭異的事情呢?我再看supervisord的文檔,發(fā)現(xiàn)每個(gè)由superivsord托管的程序都有一個(gè)配置項(xiàng)stopwaitsecs,表示supervisord對(duì)這個(gè)程序發(fā)出SIG_TERM信號(hào)(用來殺死該程序)到他收到SIG_CHLD信號(hào)(標(biāo)志著這個(gè)程序確實(shí)被殺死)之間他愿意等待的時(shí)間,如果超過這個(gè)時(shí)間,supervisord會(huì)再次向該程序發(fā)出SIG_KILL信號(hào),這個(gè)信號(hào)是一定會(huì)殺死其管理的程序的。也就是說,supervisord會(huì)先對(duì)celery的master進(jìn)行warm shutdown的操作,完了之后等待stopwaitsecs秒之后會(huì)大開殺戒,直接干掉celery master。而這個(gè)stopwaitsecs默認(rèn)為10s,對(duì)于我們一個(gè)平均發(fā)布時(shí)間為3分鐘左右的發(fā)布任務(wù)來說顯然等不到warm shutdown真正完成,supervisord就會(huì)粗暴的干掉celery master。而這樣粗暴的干掉celery master之后,celery worker貌似也出現(xiàn)了異常,再不干活了,因?yàn)樗欠浅R蕾噈aster的,而且他干完活之后還需要想master發(fā)送ack確認(rèn)信息的,故此導(dǎo)致這些workers中的發(fā)布任務(wù)卡死。
當(dāng)我把stopwaitsecs調(diào)整至10分鐘之后,再?zèng)]有出現(xiàn)過類似問題, 因?yàn)檫@個(gè)時(shí)候,supervisord對(duì)celery master進(jìn)行warm shutdown 操作之后會(huì)等待最多10分鐘才會(huì)粗暴的干掉celery master,而我們的發(fā)布任務(wù)最長(zhǎng)不會(huì)超過7分鐘,因此10分鐘以內(nèi)warm shutdown一定會(huì)成功,supervisord一定會(huì)受到SIG_CHILD信號(hào)。
總之,以后還是要仔細(xì)看文檔啊。