背景
NJet在做動態化能力設計時,曾利用了基於mqtt消息的event框架,整體實現為利用CoPilot框架實現了一個消息的broker,同時CoPilot ctrl進程作為api server,接收http請求,轉化為消息後,發送給沙箱進程做配置驗證,驗證後,廣播給作為消費者的所有的worker進程應用配置變更。
當時的設計初衷是event框架僅僅做簡單的api配置變更,假設了這類配置動態變更數量比較少,所以僅僅是功能上的考慮,性能沒有做太多要求,所以上圖所顯示的client發送http請求後,這一套標序號的流程走下來,完成過程是10ms這個級別,平均約50ms。
隨着NJet在不同場景的應用,有兩個關鍵性的需求對這套event框架的性能需求提出了嚴重挑戰,第一是實際生產環境的超大規模配置數據;第二是某些業務場景需要快速的worker間數據交換。
- 先説下大規模配置數據的挑戰。由於NJet支持server及location級別的動態化,以及可以動態配置上游成員,實際生產中頻繁碰到了過百個,甚至1000+上游成員, 而server也有近100級別,因此通過動態api下發配置,常常耗時超過10s,最大的一個客户場景報告超過43s。
- 進程間數據交換來源於NJet作為消息服務器的需求,由於NJet在前期提煉出了一套動態協議框架,可以簡單的增加某種協議的解析庫,結合tcc腳本實現業務邏輯,就可以快速的實現某種特定協議的應用服務器。以聯邦制的消息服務器為例,某client發送一條消息到消息服務器,如果接收者在消息服務器註冊,消息服務器會直接處理,否則就需要根據目的地址,轉發到其他的消息服務器(注:客户註冊到不同的消息服務器,但仍然可以相互通信,稱為聯邦,比較流行的matrix,以及古老的irc、郵件都是這種模型)。消息服務器間的通信,出於安全等方面考慮,僅僅會維持1條長鏈接,對NJet這種繼承自NGINX的多進程模式就提出了很大挑戰,即client發送到消息服務器的消息,可能會被worker1接收,但根據目的選擇,需要被worker2發送,因為只有worker2才建立了到合適目的消息服務的連接。
上圖所顯示的worker1 接收到消息,轉發到worker2,以及回包worker2轉發到worker1,就採用了NJet內置的event框架,但10ms級別的處理性能,遠不能滿足消息服務器的處理性能指標要求,甚至由於加入了跨進程通信,出現了多worker處理能力遠小於單worker處理能力的荒謬情況
瓶頸分析
njet中集成的mqtt broker來源於mosquitto,NJet基於此做了定製,做了對Copilot框架的適配,從而保證NJet能夠管理該broker。在優化了client端代碼後(主要是把消息緩衝發送,修改為立即發送),平均的處理時間降低到了2ms(見下圖),但這個處理速度,仍然不能滿足進程間通信的處理要求。
訪問單獨的mosquitto不存在性能問題,以及tcpdump抓包發現嵌入CoPilot框架後,broker處理慢,確認了CoPilot框架阻塞了broker對於消息的處理。
根據NJet的Copilot開發指南,獨立的Copilot進程應實現自己的事件處理循環(實現函數njt_helper_run),並在該循環中,調用check_cmd_fp,獲得NJet的當前狀態,如停止,reload等,從而終止事件循環,保證NJet對Copilot進程的管理,對check_cmd_fp的調用分析發現其最終調用了ngx_process_events_and_timers,該函數調用會被IO事件觸發,否則會定時一段時間返回,如下面代碼顯示:
voidngx_process_events_and_timers(ngx_cycle_t *cycle){
ngx_uint_t flags;ngx_msec_t timer, delta;
// 計算最近的定時器超時時間
timer = ngx_event_find_timer();
// 核心阻塞調用
(void) ngx_process_events(cycle, timer, flags);
// 處理定時器ngx_event_expire_timers();}
所以為了保證CoPilot事件循環不被阻塞,就需要保證Copilot進程中特定的ngx_process_events_and_timers調用立即返回(通過設置timer為0)
修復
無獨有偶,在我們考慮如何傳遞參數進ngx_process_events_and_timers時,我們發現了openresty的特定patch
// openrestry patch
if (!njt_queue_empty(&njt_posted_delayed_events)) {
njt_log_debug0(NJT_LOG_DEBUG_EVENT, cycle->log, 0,
"posted delayed event queue not empty"
" making poll timeout 0");
timer = 0;
}
// openresty patch end
通過讀Openresty的註釋,我們發現了openresty在提供定時器上也存在了被ngx_process_events_and_timers阻塞,定時器性能不高的問題,其解析方案是添加了全局的事件隊列,通過向其臨時post一條消息,從而保證了接下來的調用可以不被阻塞,立即返回。
所以我們的解決方案就很簡單了,在check_cmd_fp向該隊列設置一條消息,
......
njt_post_event(param->ev, &njt_posted_delayed_events);
int cmd = param->check_cmd_fp(cycle);
......
感謝openresty!
成效
原api調用壓測結果:20條/s
Running 1m test @ http://localhost:8081/api/v1/config
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 53.89ms 6.59ms 169.60ms 98.13%
Req/Sec 18.65 3.44 20.00 86.62%
1117 requests in 1.00m, 244.34KB read
Requests/sec: 18.60
Transfer/sec: 4.07KB
改進後的調用結果:10000條/s
Running 1m test @ http://localhost:8081/api/v1/config
1 threads and 1 connections
Thread Stats Avg Stdev Max +/- Stdev
Latency 119.09us 212.75us 7.30ms 98.51%
Req/Sec 10.12k 1.12k 12.62k 67.05%
604889 requests in 1.00m, 101.53MB read
Requests/sec: 10064.84
Transfer/sec: 1.69MB
本地測試提高了約500倍,在虛擬機等各種不同的環境也有百倍以上的性能提升,滿足了消息服務器等各種跨進程通信的性能需求
備註
- CoPilot開發指南參考:https://docs.njet.org.cn/docs/v4.0.0/development/copilot/index.html
- 本優化在NJet4.0.1版本實現,並backport到長期支持版本3.3.x,請升級到對應的版本3.3.1.2
- NJet會定期從nginx和openresty合併,目前合併nginx到1.27.4