“高并發”一直是大家感興趣的話題。本文就來給大家講講機票高并發的故事。
背景介紹
Qunar是2005年成立的,那時候人們還不習慣通過打電話或者到代理商那里去買機票。近幾年隨著旅游業的發展,機票業務也由傳統的線下來到線上。在“在線旅游”的大浪潮下,Qunar的核心業務主要是線上機票搜索和機票銷售。根據2014年9月艾瑞監測數據,在旅行類網站月度獨立訪問量統計中,去哪兒網以4474萬人名列前茅。截至2015年3月31日,去哪兒網可實時搜索約9000家旅游代理商網站,搜索范圍覆蓋全球范圍內超過28萬條國內及國際航線。
Qunar由機票起家,核心產品包括機票搜索比價系統、機票銷售OTA系統等。后來一度成為國內最大旅游搜索引擎,所以最開始大家知道Qunar都是從機票開始。
為了方便大家了解機票搜索的具體業務,我們從用戶熟悉角度看一下搜索的過程,如下圖:
根據上面的圖片,簡單解釋下:
金喜正規買球:用戶按出發城市、到達城市、出發日期開始搜索機票,進入列表頁。
列表頁:展示第一次搜索結果,一般用戶會多次搜索,直到找到適合他的航班,然后進入詳情頁。
產品詳情頁:用戶填入個人信息,開始準備下單支付。
從上面的介紹可以看出,過程1和2是個用戶高頻的入口。用戶訪問流量一大,必然有高并發的情況。所以在金喜正規買球和列表頁會做一些優化:
前端做靜態文件的壓縮,優化Http請求連接數,以減小帶寬,讓頁面更快加載出來。
前后端做了數據分離,讓搜索服務解耦,在高并發情況下更靈活做負載均衡。
后端數據(航班數據)99%以上來自緩存,加載快,給用戶更快的體驗。而我們的緩存是 異步刷新的機制,后面會提及到。
在過億級UV的搜索業務,其搜索結果核心指標:一是保證時間夠快,二是保證結果實時最新。
為了達到這個指標,搜索結果就要盡量走緩存,我們會預先把航班數據放到緩存,當航班數據變化時,增量更新緩存系統。 所以,Qunar機票技術部就有一個全年很關鍵的一個指標:搜索緩存命中率,當時已經做到了>99.7%。再往后,每提高0.1%,優化難度成指數級增長了。哪怕是千分之一,也直接影響用戶體驗,影響每天上萬張機票的銷售額。
這里還有幾個值得關注的指標:
每臺搜索實例的QPS(搜索有50~60臺虛擬機實例,按最大并發量,每臺請求吞吐量>1000)。
搜索結果的 Average-Time : 一般從C端用戶體驗來說,Average-Time 不能超過3秒的。
了解完機票搜索大概的流程,下面就來看看Qunar搜索的架構。
搜索系統設計架構
Qunar搜索架構圖
上面提到搜索的航班數據都是存儲在緩存系統里面。最早使用Memcached,通過一致Hash建立集群,印象大概有20臺左右實例。 存儲的粒度就是出發地和到達地全部航班數據。隨著當時Redis并發讀寫性能穩步提高,部分系統開始逐步遷移到Redis,比如機票低價系統、推薦系統。
搜索系統按架構圖,主要定義成前臺搜索、后臺搜索兩大模塊,分別用2、3標示,下面我也會重點解釋。
前臺搜索
主要讀取緩存,解析,合并航班數據返回給用戶端。
前臺搜索是基于Web服務,高峰期時候最大啟動了50臺左右的Tomcat實例。搜索的URL規則是:出發城市+到達城市+出發日期,這和緩存系統存儲最小單元:出發城市+到達城市+出發日期是一致的。
Tomcat服務我們是通過Nginx來做負載均衡,用Lua腳本區分是國際航線還是國內航線,基于航線類型,Nginx會跳轉不同搜索服務器:主要是國際搜索、國內搜索(基于業務、數據模型、商業模式,完全分開部署)。不光如此,Lua還用來敏捷開發一些基本服務:比如維護城市列表、機場列表等。
航班數據
上文我一直提到航班數據,接下來簡單介紹下航班的概念和基本類型,讓大家有個印象,明白的同學可以跳過:
單程航班:也叫直達航班,比如BJ(北京)飛NY(紐約)。
往返航班:比如BJ飛NY,然后又從NY返回BJ。
帶中轉:有單程中轉、往返中轉;往返中轉可以一段直達,一段中轉。也可以兩段都有中轉,如下圖:
其實,還有更復雜的情況:
如果哪天在BJ(北京)的你想來一次說走就走的旅行,想要去NY(紐約)。你選擇了BJ直飛NY的單程航班。后來,你覺得去趟米國老不容易,想順便去LA玩。那你可以先BJ飛到LA,玩幾天,然后LA再飛NY。
不過,去了米國要回來吧,你也許:
NY直接飛回BJ。
突然玩性大發,中途順便去日本,從NY飛東京,再從東京飛BJ。
還沒玩夠?還要從NY飛夏威夷玩,然后夏威夷飛東京,再東京飛首爾,最后首爾返回北京。
…… 有點復雜吧,這是去程中轉、回程多次中轉的航班路線。
對應國際航班還算非常正常的場景,比如從中國去肯尼亞、阿根廷,因為沒有直達航班,就會遇到多次中轉。所以,飛國外有時候是蠻有意思、蠻麻煩的一件事。
通過上面例子,大家了解到了機票中航線的復雜程度。但是,我們的緩存其實是有限的,它只保存了兩個地方的航班信息。這樣簡單的設計也是有必然出發點:考慮用最簡單的兩點一線,才能最大限度上組合復雜的線路。
所以在前臺搜索,還有大量工作要做,總而言之就是:
按照最終出發地、目的地,根據一定規則搜索出用戶想要的航班路線。這些規則可能是:飛行時間最短、機票價格最便宜(一般中轉就會便宜)、航班中轉最少、最宜飛行時間。
你看,機票里面的航線是不是變成了數據結構里面的有向圖,而搜索就等于在這個有向圖中,按照一定的權重求出最優路線的過程!
高并發下多線程應用
我們后端技術棧基于Java。為了搜索變得更快,我們大量把Java多線程特性用到了并行運算上。這樣,充分利用CPU資源,讓計算航線變得更快。 比如下面這樣中轉航線,就會以多線程方式并行先處理每一段航班。類似這樣場景很多:
Java的多線程對于高并發系統有下面的優勢:
Java Executor框架提供了完善線程池管理機制:譬如newCachedThreadPool、 SingleThreadExecutor 等線程池。
FutureTask類靈活實現多線程的并行、串行計算。
在高并發場景下,提供了保證線程安全的對象、方法。比如經典的ConcurrentHashMap,它比起HashMap,有更小粒度的鎖,并發讀寫性能更好。線程安全的StringBuilder取代String、StringBuffer等等(Java在多線程這塊實現是非常優秀和成熟的)。
高并發下數據傳輸
因為每次搜索機票,返回的航班數據是很多的:
包含各種航線組合:單程、單程一次中轉、單程多次中轉,往返更不用說了。
航線上又區分上百種航空公司的組合。比如北京到紐約,有美國航空,國航,大韓, 東京等等各個國家的各大航空公司,琳瑯滿目。
那么,最早航班數據用標準的XML、JSON存儲,不過隨著搜索量不斷飆升,CPU和帶寬壓力很大了。后來采取自己定義一種txt格式來傳輸數據:一方面數據壓縮到原來30%~40%,極大的節約了帶寬。同時CPU的運算量大大減低,服務器數量也隨之減小。
在大用戶量、高并發的情況下,是特別能看出開源系統的特點:比如機票的數據解析用到了很多第三方庫,當時我們也用了Fastjson。在正常情況下,Fastjson 確實解析很快,一旦并發量上來,就會越來越吃內存,甚至JVM很快出現內存溢出。原因呢,很簡單,Fastjson設計初衷是:先把整個數據裝載到內存,然后解析,所以執行很快,但很費內存。
當然,這不能說Fastjson不優秀,現在看 GitHub上有8000多star。只是它不適應剛才的業務場景。
這里順便說到聯想到一個事:互聯網公司因為快速發展,需要新技術來支撐業務。 那么,應用新的技術應該注意些什么呢?我的體會是:
好的技術要大膽嘗試,謹慎使用。
優秀開源項目,注意是優秀。使用前一定弄清他的使用場景,多做做壓力測試。
高并發的用戶系統要做A/B測試,然后逐步導流,最后上線后還要有個觀察期。
后臺搜索
后臺搜索系統的核心任務是從外部的GDS系統抓取航班數據,然后異步寫入緩存。
首先說一個概念GDS(Global Distribution System)即“全球分銷系統”,是應用于民用航空運輸及整個旅游業的大型計算機信息服務系統。通過GDS,遍及全球的旅游銷售機構可以及時地從航空公司、旅館、租車公司、旅游公司獲取大量的與旅游相關的信息。
機票的源數據都來自于各種GDS系統,但每個GDS卻千差萬別:
服務器遍布全球各地:國內GDS主要有中航信的IBE系統、黑屏數據(去機場、火車站看到售票員輸入的電腦終端系統),國際GDS遍布于東南亞、北美、歐洲等等。
通訊協議不一樣,HTTP(API、Webservice)、Socket等等。
服務不穩定,尤其國外的GDS,受網路鏈路影響,訪問很慢(十幾分鐘長連接很常見),服務白天經常性掛掉。
更麻煩的是:GDS一般付費按次查詢,在大搜索量下,實時付費用它,估計哪家公司都得破產。而且就算有錢 , 各種歷史悠久的GDS是無法承載任何的高并發查詢。更苦的是,因為是創業公司,我們大都只能用免費的GDS,它們都是極其不穩定的。
所謂便宜沒好貨,最搞笑的一次是:曾經在米國的GDS掛了一、兩天,技術們想聯系服務商溝通服務器問題。因為是免費,就沒有所謂的服務商一說,最后產品總監(算兼職商務吧)給了一個國外的網址,打開是這家服務商的工單頁面,全英文,沒有留任何郵箱。提交工單后,不知道什么時候回復。可以想想當時我的心情......
雖然有那么多困難,我們還是找到一些技術方案,具體如下。
引入NIO框架
考慮GDS訪問慢,不穩定,導致很多長連接。我們大量使用NIO技術:
NIO,是為了彌補傳統I/O工作模式的不足而研發的,NIO的工具包提出了基于Selector(選擇器)、Buffer(緩沖區)、Channel(通道)的新模式;Selector(選擇器)、可選擇的Channel(通道)和SelectionKey(選擇鍵)配合起來使用,可以實現并發的非阻塞型I/O能力。
NIO并不是一下就憑空出來的,那是因為 Epoll 在Linux2.6內核中正式引入,有了I/O多路復用技術,它可以處理更多的并發連接。這才出現了各種應用層的NIO框架。
HTTP、Socket 都支持了NIO方式,在和GDS通信過程中,和過去相比:
通信從同步變成異步模式:CPU的開銷、內存的占用都減低了一個數量級。
長連接可以支持更長超時時間,對國外GDS通信要可靠多了。
提高了后臺搜索服務器的穩定性。
消息隊列
為了異步完成航班數據更新到緩存,我們采用消息隊列方式(主備AMQ)來管理這些異步任務。具體實現如下。
有一個問題,如何判斷緩存過期呢?這里面有一個復雜的系統來設置的,它叫Router。資深運營會用它設置可以細化到具體一個航段的緩存有效期:比如說北京—NY,一般來說買機票的人不多的,航班信息緩存幾天都沒有問題。但如果北京—上海,那可能就最多5分鐘了。
Router還有一個復雜工作,我叫它“去偽存真”。我們長期發現(真是便宜無好貨),某些GDS返回航班數據不全是準確的,所以我們會把某些航線、甚至航班指定具體的GDS數據源,比如北京—新加坡:直達航班數據 來自于ABAQUS,但是中轉數據,北京—上海—新加坡, 或者北京—臺北—新加坡 從IBE來會精準些。
因此Router路由規則設計要很靈活。通過消息隊列,也其實采用異步化方式讓服務解耦,進行了很好的讀寫分離。
GDS服務抽象虛擬Node
為了管理好不同GDS資源,最大的利用它們。我們把GDS服務器抽象成一組Node節點來便于管理,像下面這樣:
具體原理:按照每個GDS服務器穩定性(通過輪休方式,不斷Check它們的可用性)和查詢性能,我們算出一個合理的權重,給它分配對應的一組虛擬的Node節點,這些Node節點由一個Node池統一管理。這樣,不同的GDS系統都抽象成了資源池里面的一組相同的Node節點。
那么它具體如何運轉的呢?
當緩存系統相關航班數據過期后,前臺搜索告知MQ有實時搜索任務,MQ統一把異步任務交給Router,這個時候Router并不會直接請求GDS數據,而是去找Node池。Node池會動態分配一個Node節點給Router,最后Router查找Node節點映射的GDS,然后去請求數據,最后異步更新對應的緩存數據。通過技術的實現,我們把哪些不穩定的,甚至半癱瘓的GDS充分利用了起來(包含付費的一種黑屏終端,我們把它用成了免費模式,這里用到了某些黑科技,政策原因不方便透露),同時滿足了前臺上億次搜索查詢!
監控系統
鑒于機票系統的復雜度和大業務量,完備監控是很必要的:
1、整個Qunar系統架構層級復雜,第三方服務調用較多(譬如GDS),早期監控系統基于CACTI+NAGIOS ,CACTI有很豐富的DashBoard,可以多維度的展示監控數據。除此以外,公司為了保證核心業務快速響應,埋了很多報警閾值。而且Qunar還有一個NOC小組,是專門24小時處理線上報警:記得當時手機每天會有各種系統上百條的報警短信。
當然,我還是比較淡定了。因為系統太多,報警信息也不盡是系統bug,它可能是某些潛在的問題預警,所以,系統監控非常至關重要。
2、復雜系統來源于復雜的業務,Qunar除了對服務器CPU、內存、IO系統監控以外,遠遠是不夠的。我們更關心,或者說更容易出問題是業務的功能缺陷。所以,為了滿足業務需要,我們當時研發了一套業務監控的插件,它的核心原理如下圖:
它把監控數據先保存到內存中,內部定時程序每分鐘上傳數據到監控平臺。同時它作為一個Plugin,可以即插即用。接入既有的監控系統,它幾乎實時做到監控,設計上也避免了性能問題。后期,產品、運營還基于此系統,做數據分析和預測:比如統計出票正態分布等。因為它支持自定義統計,有很方便DashBoard實時展示。對于整個公司業務是一個很有力的支撐。
到今天,這種設計思路還在很多監控系統上看到相似的影子。
機票銷售系統
機票另一個重要系統TTS:TTS(Total Solution)模式,是去哪兒網自主研發的交易平臺,是為航空公司、酒店在線旅游產品銷售系統。
TTS有大量商家入駐,商家會批量錄入航班價格信息。
為了減少大量商家同時錄入海量數據帶來的數據庫并發讀寫的問題,我們會依據每個商家規模,通過數據庫動態保存服務器IP,靈活的切換服務器達到負載均衡的效果。這里不再細說了。
最后,回顧整個搜索架構的設計,核心思想體現了服務的一種解耦化。設計的系統雖然數量看起來很多,但是出發點都是把復雜的業務拆解成簡單的單元,讓每一個單元專注自己的任務。這樣,每個系統的性能調優和擴展性變得容易。同時,服務的解耦使整個系統更好維護,更好支撐了業務。
重大喜訊,慧都學院將在3月底有一堂《基于圖的大數據分析》免費公開課,點擊立即報名。

標簽:
本站文章除注明轉載外,均為本站原創或翻譯。歡迎任何形式的轉載,但請務必注明出處、不得修改原文相關鏈接,如果存在內容上的異議請郵件反饋至chenjj@fc6vip.cn