「高併發」一直是你們感興趣的話題。2010年~2012年,我曾在Qunar供職,主要負責機票搜索相關的業務,當時咱們的搜索系統最高天天承載了億級用戶的高併發訪問。那段日子,很苦很累,Qunar的發展很快,我也見證了搜索系統的技術演變歷程,本文就來給你們講講機票高併發的故事。前端
業務背景數據庫
Qunar成立於2005年,那時候你們還習慣打電話或者去代理商買機票。隨着在線旅遊快速發展,機票業務逐步來到線上。在「在線旅遊」的大浪潮下,Qunar的核心業務主要是線上機票搜索和機票銷售。根據2014年9月艾瑞監測數據,在旅行類網站月度獨立訪問量統計中,去哪兒網以4474萬人名列前茅。截至2015年3月31日,去哪兒網可實時搜索約9000家旅遊代理商網站,搜索範圍覆蓋全球範圍內超過28萬條國內及國際航線。json
Qunar由機票起家,核心產品包括機票搜索比價系統、機票銷售OTA系統等。後來一度成爲國內最大旅遊搜索引擎,因此最開始你們知道Qunar都是從機票開始。後端
在Qunar,我主要工做負責機票搜索系統。當時,搜索業務達到了日均十億量級PV,月均上億UV的規模。而整個搜索主系統設計上是比較複雜的,大概包含了7、八個子系統。那時線上服務器壓力很大,時常出現一些高併發的問題。有時候爲了解決線上問題,通宵達旦連續一兩週是常有的事。儘管如此,咱們仍是對整個搜索系統作到了高可用、可擴展。緩存
爲了你們瞭解機票搜索的具體業務,咱們從用戶角度看一下搜索的過程,以下圖:安全
根據上面的圖片,簡單解釋下:服務器
首頁:用戶按出發城市、到達城市、出發日期開始搜索機票,進入列表頁。數據結構
列表頁:展現第一次搜索結果,通常用戶會屢次搜索,直到找到適合他的航班,而後進入詳情頁。多線程
產品詳情頁:用戶填入我的信息,開始準備下單支付。架構
從上面的介紹能夠看出,過程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,好比機票低價系統、推薦系統。
搜索系統按架構圖,主要定義成前臺搜索、後臺搜索兩大模塊,分別用二、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掛了1、兩天,技術們想聯繫服務商溝通服務器問題。由於是免費,就沒有所謂的服務商一說,最後產品總監(算兼職商務吧)給了一個國外的網址,打開是這家服務商的工單頁面,全英文,沒有留任何郵箱。提交工單後,不知道何時回覆。能夠想一想當時個人心情......
雖然有那麼多困難,咱們仍是找到一些技術方案,具體以下。
引入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充分利用了起來(包含付費的一種黑屏終端,咱們把它用成了免費模式,這裏用到了某些黑科技,政策緣由不方便透露),同時知足了前臺上億次搜索查詢!
監控系統
鑑於機票系統的複雜度和大業務量,完備監控是很必要的:
一、整個Qunar系統架構層級複雜,第三方服務調用較多(譬如GDS),早期監控系統基於CACTI+NAGIOS ,CACTI有很豐富的DashBoard,能夠多維度的展現監控數據。除此之外,公司爲了保證核心業務快速響應,埋了不少報警閾值。並且Qunar還有一個NOC小組,是專門24小時處理線上報警:記得當時手機天天會有各類系統上百條的報警短信。
固然,我仍是比較淡定了。由於系統太多,報警信息也不滿是系統bug,它多是某些潛在的問題預警,因此,系統監控很是相當重要。
二、複雜系統來源於複雜的業務,Qunar除了對服務器CPU、內存、IO系統監控之外,遠遠是不夠的。咱們更關心,或者說更容易出問題是業務的功能缺陷。因此,爲了知足業務須要,咱們當時研發了一套業務監控的插件,它的核心原理以下圖:
它把監控數據先保存到內存中,內部定時程序每分鐘上傳數據到監控平臺。同時它做爲一個Plugin,能夠即插即用。接入既有的監控系統,它幾乎實時作到監控,設計上也避免了性能問題。後期,產品、運營還基於此係統,作數據分析和預測:好比統計出票正態分佈等。由於它支持自定義統計,有很方便DashBoard實時展現。對於整個公司業務是一個頗有力的支撐。
到今天,這種設計思路還在不少監控系統上看到類似的影子。
機票銷售系統
機票另外一個重要系統TTS:TTS(Total Solution)模式,是去哪兒網自主研發的交易平臺,是爲航空公司、酒店在線旅遊產品銷售系統。
TTS有大量商家入駐,商家會批量錄入航班價格信息。
爲了減小大量商家同時錄入海量數據帶來的數據庫併發讀寫的問題,咱們會依據每一個商家規模,經過數據庫動態保存服務器IP,靈活的切換服務器達到負載均衡的效果。這裏再也不細說了。
最後,回顧整個搜索架構的設計,核心思想體現了服務的一種解耦化。設計的系統雖然數量看起來不少,可是咱們出發點都是把複雜的業務拆解成簡單的單元,讓每個單元專一本身的任務。這樣,每一個系統的性能調優和擴展性變得容易。同時,服務的解耦使整個系統更好維護,更好支撐了業務。