NSQ 最佳實踐
目前,全新的非同步任務服務每天高效穩定的為唱吧提供數億次的呼叫。伺服器團隊用全新的方式重新定義了非同步任務實現方式,以為雲端計算而生的NSQ、成熟的PHP執行者PHP-FPM、自主開發的中介軟體NSQProxy以及admin管理後臺共同組成了非同步任務的佇列服務。
唱吧非同步任務的前世
開啟唱吧伺服器程式碼,一股歷史的厚重感撲面而來,“這塊程式碼是歷史原因”成了同學們的口頭禪。
為什麼要用非同步任務
為了提高響應,減少使用者等待,和線上使用者無直接關係的程式碼挪到非同步後臺執行,這樣可以讓前臺業務邏輯程式碼更加簡潔,執行速度更快。比如:使用者購買禮物時,餘額檢查、付款、禮物進入使用者揹包等邏輯放在同步執行,統計等放在非同步執行。
流程
-
同步執行的程式碼將資料插入佇列(MemcacheQ)
-
以crontab的方式在後臺啟程序,程序開頭程式碼就是while(true)
-
從佇列中去取出資料消費,然後sleep,一直死迴圈下去。
弊端
-
MemcacheQ遠不如Nginx等足夠成熟穩定,偶爾會莫名其妙的卡主自己好幾秒。
-
消費者自由散漫,分散在各個機器上,開幾個程序也都是隨心所欲。
-
不支援訂閱釋出(推送),只能死迴圈裡去get,get不到就sleep。出現了1次set對應136次get(135次get到空),白白浪費伺服器資源。
-
消費者以死迴圈的方式常住記憶體,導致程式碼更新不及時,生成端的資料消費端不認識,必須上伺服器手動kill程序,這期間會造成消費失敗的資料丟失。
-
每一個環節都是單點,無法避免單點故障,壞一個洞全船都要沉。
鳥哥: 不要拿PHP做常駐記憶體的事情,因為我沒給PHP寫優雅的GC
唱吧非同步任務的今身
NSQ替換MemcacheQ
Golang編寫,雲端計算時代的產物,MQ領域的新星,為分散式訊息佇列而生,效能強勁,部署及其方便,二次開發難度低,有Go、Python、PHP等多語言客戶端。NSQ相關不是重點,不在贅述。
NSQ優勢
-
訊息可靠性高:有ack/requeue機制。
-
磁碟落地:積壓的訊息可以磁碟落地。
-
擴充套件性:優勢明顯,新增節點極其方便。
-
易用性:部署簡單,甚至安裝不需要編譯。
-
分散式:為分散式而生,去中心化,官方推薦的拓撲結構沒有單點故障。
-
延遲投遞。
-
支援訂閱釋出。
-
bitly、有贊、docker、digg等大規模部署,並且有贊開源了二次開發版本。
唱吧村特殊村情及主要矛盾
歷史上,我們的非同步任務入隊的資料是:PHP的類名(string)、PHP方法名和該方法的引數拼接成一個字串。消費者需要new一個類,然後以執行字串的方式(eval())來執行,由於方法和引數拼接成字串,帶來轉義風險,最主要的是,這樣就決定了我們的最終消費者,必須是PHP。
全新的架構
-
該拉取為推送。有資料就執行,沒資料就阻塞,避免空輪詢。
-
引入PHP-FPM。PHP-FPM作為非常成熟的PHP執行者,有完善的程序管理、垃圾回收、效能優化,並且常駐記憶體,非常適合作為最終消費者。並且PHP-FPM常駐記憶體並監聽9000埠,也非常適合承擔訂閱者的角色,程式碼實時更新。
-
將上述兩點融合,開發一箇中間件,從NSQ訂閱資料,再以FAST-CGI協議推送給PHP-FPM。
-
開發一個管理頁面,可以方便的配置和管理。
-
舊版是線上請求直接寫入單點的MemcacheQ機器上,先在是以HTTP寫入Nginx,Nginx做負載均衡,轉發到NSQ節點上。
中介軟體NSQProxy
NSQProxy是唱吧伺服器部門自住開發的輕量級中介軟體,Golang編寫,效能強勁。
實現方式
-
啟動第一步主備檢測:
-
如果是主機,則一個監聽4140埠,此時新啟兩個協程,該協程等待備機發PING並回PONG,一直阻塞等待accept。另一個協程(主協程)繼續向下走。
-
如果是備機,則死迴圈向主機發PING,如果收到PONG,則sleep,程式會一直阻塞在這裡。如果主機未回覆,否則備機轉為主機啟動。
-
主備角色等資訊由配置檔案決定。
-
訊號監聽:
-
新啟二個協程,一個是走主流程的(下面第3點),一個是走動態消費流程(下面第4點)。
-
主協程監聽SIGINT(2), SIGTERM(15), SIGTRAP(5),如果是SIGINT(2)和SIGTERM(15)則程式退出,如果是SIGTRAP(5)忽略不管。主協程阻塞在這裡。
-
主流程:
-
協程從Mysql獲取管理後臺的配置,每個佇列都與NSQ建立一個連結,並根據配置的併發量(N個),啟動N個協程,以FAST-CGI協議向PHP-FPM傳送消費資料。
-
在剛進入主流程時,上一小步執行之前,會啟一個新協程,以定時器的方式,定時更新系統配置建和管理後臺的配置資料。
-
動態消費:
-
有些運營活動、push推送等突發性的入隊,造成原本的併發量消費能力不足,這時候需要新增協程來幫助消費。
-
以定時器的方式,定時掃描NSQ中積壓的佇列,若某佇列達到積壓閾值,則會啟動與NSQ建立新的連線,啟動新的協程來增加消費能力。再以定時器的方式,達到設定時間(如300秒),就會關閉連結並協程協程。
效能測試
入隊
藉助apache-ab工具,訊息長度:356(簡訊驗證碼的標準長度)。
-
Golang TCP協議:10個程序,每個程序入隊10000,總入隊100000
-
執行時間:8s
-
單次平均時間:8s / 10w = 0.08ms
-
單次真實時間: 0.08ms * 10 = 0.8ms
-
QPS: 10w/8s = 12500 個
-
CPU * 20核 ≈ 30%
-
Golang HTTP協議:10個程序,每個程序入隊10000,總入隊100000
-
執行時間:2.9s
-
單次平均時間:2.9s / 10w = 0.029ms
-
單次真實時間: 0.029ms * 10 = 0.29ms
-
QPS: 10w/2.9s = 34482 個
-
CPU * 20核 ≈ 30%
-
PHP HTTP協議:10個程序,每個程序入隊10000,總入隊100000
-
執行時間:3.4s
-
單次平均時間:3.4s / 10w = 0.034ms
-
單次真實時間: 0.034ms * c = 0.34ms
-
QPS: 10w/3.4s = 29412 個
-
CPU * 4核 ≈ 30%,16個核是0%
出隊
訊息長度:356(簡訊驗證碼的標準長度),消費操作就是打日誌。速度是根據打日誌的時間戳來統計。
-
20併發,最多推送40
-
QPS:2177
-
PHP-FPM:起初CPU * 20核 > 90%, 後來就長期維持100%。
-
NSQD:CPU會有個別一兩個核,在個別時刻閃現到1%~2%,然後恢復0%,網路讀寫210k/960k
-
NSQProxy:CPU一個核30%,一個核15%,其餘都是4%。網路讀寫2400k/3320k,磁碟每30秒寫30M(每條消費log)
-
10併發,最多推送20
-
QPS:同上
-
起初CPU * 20核 > 60%, 後來就所有核長期維持100%。
-
NSQD:CPU會有個別一兩個核,最多2個核閃現到1%,然後恢復0%,網路讀寫210k/960k
-
NSQProxy:CPU一個核30%,一個核13%,其餘都是4%。網路讀寫2400k/3320k,磁碟每30秒寫30M(每條消費log)
誰還不是寫BUG的咋滴
PHP-FPM的特性決定了它的瓶頸
PHP-FPM就像HTTP協議一樣,每次請求相互獨立,不儲存上次請求的狀態和上下文。拋開優化點不談,一次請求結束,刪除變數,刪除引用,釋放記憶體,一切成空。下次個請求到來時,從頭再來!
這就造成了一個問題:如敏感詞檢測的類,在new Class()時,會載入十幾萬行詞庫檔案並解析,這一步大約耗時700ms,那麼PHP-FPM每次消費前,都要重複這一步驟,執行完成後再銷燬,這就會使消費速度大幅下降,佇列積壓嚴重。如果是Golang、JAVA等語言,則可以一次載入解析,永久使用。
PHP-FPM程序數限制
我們的非同步任務幾乎都是IO密集型,沒有CPU密集型。所以可以開數百個程序跑,反正都在Sleep等待網路返回。而PHP-FPM卻不可以,
在PHP7下,PHP-FPM開到200以上效能CPU的佔用就開始飆升,同時每個PHP-FPM是同步執行,程序不可複用。那麼在PHP-FPM程序數固定的大前提下,如果有一個任務執行特別慢,那麼就會佔用PHP-FPM程序不釋放,這樣的任務多來幾個,很快就會把伺服器上PHP-FPM的程序全部佔光,導致其他消費快的佇列卻無可用的PHP-FPM。
二期暢想
一期工程所暴露的問題,在二期中會逐一解決。
針對以上的坑,列出幾點解決方案,待探討論證。
方式一
維持現狀,NSQ和MemcacheQ共存,大多數佇列在NSQ + PHP-FPM的方式,個別特殊佇列仍然使用MemcacheQ + 死迴圈的方式。
方式二
安裝NSQ-PHP客戶端擴充套件,取代PHP-FPM。
方式三
簡單粗暴執行:消費快的佇列在PHP-FPM中執行,消費慢的佇列仍然在while(true)中執行。NSQProxy不僅僅提供一個訊息轉發到PHP-FPM的功能(訂閱釋出的推送模式),同時還要監聽一個埠可供消費者主動獲取(拉取資料模式)。但是這樣,既不優雅,也不統一和規範,儘管它可以快速解決當前問題。
方式四
PHP實現一個常駐記憶體的,監聽某埠的功能,取代PHP-FPM。我小時候寫的PHP-Socket專案有了使用者之地:MeepoPS是Nginx + PHP-FPM的結合體,即對外監聽埠,也可以直接執行PHP程式碼,壓測資料表明比較穩定。