tkmybatis 子查询_日均20亿流量:携程机票查询系统的架构升级
一、 機票搜索服務概述
1. 攜程機票搜索的業務特點
首先簡單介紹一下機票的搜索業務:大家可能都用過攜程,當你去輸入目的地,然后點擊搜索的時候,我們的后臺就開始了緊張的工作。我們基本上會在一兩秒的時間,將最優的結果反饋給用戶。這個業務存在以下業務特點。
(1)高流量、低延時、高成功率
首先,我們不得不面對非常高的流量,同時,我們對搜索結果要求也很高——成功率要高,不能說查詢失敗,或者強說成功,我們希望能夠反饋給用戶最優質最新鮮的數據。
(2)多引擎聚合,SLA不一
攜程機票搜索的數據來源于哪兒呢?有很大一部分結果來源于我們自己的機票運價引擎。除此之外,為了補充產品豐富性,我們還引入了國際上的一些GDS、SLA,比如我們說的聯航。我們將外部的引擎,和我們自己的引擎結果聚合之后發送給大家。
(3)計算密集&IO密集
大家可能會意識到,我說到我們自己的引擎就是基于一些運價的數據、倉位的數據,還有其他一些航班的信息,我們會計算、比對、聚合,這是一個非常技術密計算密集型的這么一個服務。同時呢,外部的GDS提供的查詢接口或者查詢引擎,對我們來說又是一個IO密集型的子系統。我們的搜索服務要將這兩種不同的引擎結果很好地聚合起來。
(4)不同業務場景的搜索結果不同要求
攜程作為一個非常大的OTA,還會支持不同的應用場景。例如,同樣是北京飛往上海,由于設定的搜索條件或搜索渠道不一樣,返回的結果會有一些不同。比如,有的客戶是學生,可能就搜到學生的特價票,而其他的用戶則看不到這個信息。
總體來說,每天的查詢量是20億次,這是一個平均的查詢量。其中經過鑒定,9%的查詢量來自于爬蟲,這其中有一些惡意爬蟲,也有一些是出于獲取信息目的的爬蟲,可能來自于其他的互聯網廠商。對于不同的爬蟲,我們會有不同的應對策略。在有效查詢當中,大概有28%是來自國際客戶,然后有63%屬于中文客戶。攜程的國際客戶,特別是機票業務,所占有的比重越來越多。
2. 攜程基礎設施(Infrastructure)建設情況
好,接下來就簡單介紹一下,就是為了應對這樣的業務特點,我們有哪些武器呢?
(1)三個獨立的數據中心
攜程目前有三個獨立的數據中心。他們是可以互相做災備的,就在兩天前,我們有一個比較盛大的慶祝會,經過差不多一年的系統提升和演練。我們實現了其中一個數據中心完全宕機的情況下,攜程的業務不會受到影響。這個我們給他起了一個很fashion的名字叫流浪地球。我也代表我們機票業務領到了一個團體貢獻獎。
(2)技術棧
講一下我們的DataCenter大概的技術棧。可能跟很多的互聯網廠商一樣,我們是用了SpringCloud+K8s+云服務(海外),這里感謝Netflix無私的開源項目,其實支撐了很多互聯網的基礎設施部門,不然,大家可能還要摸索很長時間才能達到同樣的效果。
(3)基于開源的DevOps
我們基于開源做了整套的DevOps工具和框架。
(4)多種存儲方案
在攜程內部有比較完善可用度比較高的存儲方案,包括MySQL,Redis,MangoDB……
(5)網絡可靠性
攜程非常注重網絡的可靠性,做了很多DR的開發,做了很多SRE實踐,廣泛推動了熔斷,限流等等,以保證我們的用戶得到最高質量的服務。
3. 攜程搜索服務的架構
這里我簡單畫了一下攜程機票搜索服務的架構,如下圖所示。
我們的數據中心有三個,中間部分可以看到,我們首先引入了GateWay分流前端的服務,前端的服務通過服務治理,可以和后端聚合服務進行交互。聚合服務再調用很多的引擎服務,在這兒大家看出可以看到非常熟悉的Redis的圖標,這就是我們廣泛使用的分布式緩存。
緩存的具體細節下文會講到。我們聚合服務的結果,可以通過Kafka推送到我們的AI數據平臺,會做一些大數據的分析、流量回放,還有其他的一些數據相關的操作。在圖中http://trip.com框的右邊,我們還專門在云上部署了數據的過濾服務,使得傳回的數據減少了90%,這是我們的data Center的介紹。
二、緩存架構的演進
1. 緩存的挑戰和策略
(1)為什么大量使用緩存應對流量高峰?
在流量高下為什么要使用緩存?其實有過實戰經驗的同學都知道,緩存是提高效率、提升速度,首先需要考慮的一種技術手段。
對于我們來說,為什么要大量使用緩存?首先,我們雖然使用了很多比較流行的開源技術,但是我們還是有一些瓶頸的。比如,雖然我們的數據庫是分片的、高可用的的MySQL,但是它跟一些比較流行的云存儲、云數據庫相比,它的帶寬、存儲量、可用性還是有一定差距,所以我們通常情況下需要使用緩存來保護我們的數據庫,不然頻繁的讀取會使得數據庫很快超載。
另外,我們有比較多的外部依賴,它們提供給我們的帶寬,QPS也是很有限的。攜程的整整業務量是快速增長的,而外部的業務伙伴給我們的帶寬,要么已經達到了他們的技術瓶頸,要么開始收非常高的費用。在這種情況下,使用緩存就可以保護外部的一些合作伙伴,不至于把他們系統給擊穿,另外也可以幫我們節省一些費用。
(2)本地緩存 VS 分布式緩存
在整個攜程架構的演進的過程當中,一開始本地緩存比較多的,后來部分用到分布式緩存,然后占比越來越高。
本地緩存主要有兩個問題:一個啟動的時候,它會有一個冷啟動的過程,這對快速部署是非常不利的。另外一個問題是,與分布式緩存相比,本地緩存的命中率實在是太低了。對于我們海量的數據而言,單機所能提供的命中率非常低,低到5%甚至更低。在這種情況下,我們現在已經幾乎全面切向了分布式緩存。
現在我們的分布式緩存解決方案是Redis分布式緩存,總體而言,現在攜程可用性和容錯性都是比較高的。我們在設計當中,本著對戰failure的這么一個理念,我們也不得不考慮失敗的場景。萬一集群掛掉了,或者它的一部分分片掛掉了,這時候需要通過限流客戶端、熔斷等方式,防止它的雪崩效應,這是在我們設計當中需要注意到的。
(3)TTL設置
還有一點需要強調的,TTL生命周期設置的時候需要花一點心思,這也是跟業務密切相關的。買機票經常有這種場景:剛剛看到一個低價機票,點進去就沒有了。這種情況出現的原因可能是什么呢?
大家知道,航空公司的低價艙位票,一次可能就只放出來幾張,如果是熱門航線,可能同時有幾百人在查詢。所以,幾百人都可能會看到這幾張票,它就會出現在緩存里邊。如果已經有10個人去訂了票,其他人看到緩存再點進去,運價就已經失效了。這種情況就要求有一個權衡,不能片面追求高命中率,還要兼顧數據新鮮度。所以,為了保證新鮮度、數據準確性,我們還會有大量的定時工作去做更新和清理。
2. 緩存架構演進
接下來講一下緩存架構的演進。
(1)多級緩存
這里我舉了三處緩存:
- 子引擎級別的緩存。
- L1分布式聚合緩存,L1聚合緩存基本上就是我們用戶看到的最終查詢結果。
- L2二級緩存,二級緩存是分布式的子引擎的結果。
如果聚合服務需要多個返回結果的話,那么很大程度上都是先讀一級緩存,一級緩存沒有命中的話,再從二級緩存里面去讀中間結果,這樣可以快速聚合出一個大家所需要的結果返回。
(2)引擎緩存
我們使用了一個多級的緩存模式。如下右圖所示,最頂部的是我們指引前的結果緩存,儲存在Redis中,在引擎內部,往往根據產品、供應商,會有多個渠道的中間結果,所以對我們的子引擎來說會有一個中間緩存。這些中間結果的計算,需要數據,這個數據就來自上文提到的最基礎的一級緩存。
(3)基于Redis的一級緩存
一級緩存使用了Redis,主要考慮到它讀寫性能好,快速,水平擴展性能,能夠提高存儲量以及帶寬。在當前設計當中有兩個局限性:首先為了簡單起見,使用了固定的TTL,這是為了保證返回結果的相對新鮮。第二,為了命中率和新鮮度,我們還在不斷地去提高。總之,目前的解決方案還不能完美地解決這兩方面的問題。
返回結果我們分析了一下,在一級緩存當中,它的命中率是小于20%的,在某些場景下甚至比20%還要低,就是為了保證更高的準確度和新鮮度。高優先度,一級緩存的TTL肯定是低于5分鐘的,有一些場景下可能只有幾十秒;然后我們還支持動態的刷新機制,整體的延遲是小于3毫秒的。在整個的運行過程中,可用性一直比較好。
(4)二級緩存
二級緩存一開始是采用了MongoDB,主要考慮到幾個因素:首先,它的讀寫性能比較好,另外有一個比較重要因素是,它支持二級緩存。大家知道Redis其實就是一個KV這樣的一個存儲了。而在設計二級緩存的時候,為了支持多一點的功能,比如說為了能夠很方便地做數據清理,就需要用到二級索引的功能。我們會計算出來一個相對較優的TTL,保證特定的數據有的可以緩存時間長一點,有的可以快速更新迭代。
二級緩存基于MongoDB,也有一些局限性。首先,架構是越簡單越好,多引入一種存儲方式會增加維護的代價。其次,由于MongoDB整個的license的模式,會使得費用非常高的。但是,二級緩存使得查詢的整體吞吐量提高了三倍,通過機器學習設定的TTL,使得命中率提升了27%,各個引擎的平均延時降低了20%——這些對我們來說,是非常可喜的變化。在一個比較成熟的流量非常大的系統中,能有一個10%以上的提升,就是一個比較顯著的技術特點。
針對MongoDB,我們也做了一個提升,最后把它切成Redis,通過我們的設計方案,雖然增加了一部分復雜性,但是替代了二級索引,這樣一個改進的結果,成本降低了90%,讀寫性能提升了30%。
三、負載均衡的演進
系統的首要目標是要滿足高可用,其次是高流量支撐。我們可以通過多層的均衡路由實現把這些流量均勻分配到多個數據中心的多個集群中。
1. 目標
第三,需要降低事故影響范圍,即使是在穩定的系統里也不能避免事故,但是當事故發生時,我們要使事故的影響范圍盡可能的小。第四,我們需要提升硬件資源的利用率,另外還會有一些長尾問題,比如個別查詢的時間會特別的長,需要我們找到調度算法上的問題,然后一步步解決。
2. 負載均衡架構
上圖所示的是攜程路由和負載均衡的架構,非常典型,有GateWay、load、balance、IP直連,在IP的基礎上,我們實現了一項新的Pooling技術。我們也實現了Set化,在同一個數據中心里,所有的服務都只和該數據中心的節點打交道,盡量減少跨地區的網絡互動。
3. Pooling
為什么要做 Pooling 呢?因為前文提到了我們有一些計算非常密集的引擎,存在一些耗時長,耗費CPU資源比較多的子任務,同時這些子任務中可能夾雜著一些實時請求,所以這些任務可能會留在線程里邊,阻塞整個流程。
Pooling 要做的事情就是:我們把這些子任務放在queue里邊,將節點作為worker,總是動態的去取,每次只取一個,計算完了要么把結果返回,要么把中間結果再放回queue。這樣的話如果有任何實時的外部調用,我們就可以把它分成多次,放進queue進行task的整個提交執行和應用結果的返回。
4. 過載保護
在 Pooling 的設計當中,我們需要設計一個過載保護,當流量實在太高的情況下,可以采用一個簡單的過載保護,把等待時間超過某一個閾值的請求全都扔掉。當然這個閾值肯定是小于談話時間的,這樣就能保證我們整個的 Pooling 服務是高可用的。
雖然我們可能會過濾掉一些請求,但是大家可以想象一下,如果沒有過載保護,很容易就會發生滾雪球效應,queue里面的任務越來越多,當系統取到一個任務的時候,實際上它的原請求可能早就已經timeout了。
下圖所示的是壓測結果,大家可以看到,在達到我們系統的極限值之前,有Pooling 和沒Pooling兩種情形下的負載均衡差異。比如在80%負載下,不采用Pooling的排隊時間會比有Pooling的情況下高出10倍。
所以對于一些面臨相同流量問題的互聯網廠家,可以考慮把 Pooling 作為自己的一個動態調度,或者作為一個control plan的改進措施。
如下圖所示,我們實現了 Pooling 之后平均響應時間基本沒有大的變化,還是單層查詢計算普遍需要六七十毫秒。但是實現了 Pooling 之后,有一個顯著的變化是鍵值變少了,鍵值的范圍也都明顯控制在平均時間的兩倍以內。這對于我們這樣大體量的服務來說,比較平順曲線正是我們所需要的。
四、AI的應用
這里我列出了使用效果比較好的幾個AI應用場景。
1. 應用場景
(1)反爬
在前端,我們設定了智能反爬,能幫助屏蔽掉9%的流量。
(2)查詢篩選
在聚合服務中,我們并會把所有請求都壓到子系統,而是會進行一定的模式運營,找出價值最高實際用戶,然后把他們的請求發到引擎當中。對于一些實際價值沒有那么高的,更多的是用緩存,或者屏蔽掉一些比較昂貴的引擎。
(3)TTL智能設定
之前已經提到,整個TTL的設定,使用了機器學習技術。以上就是我們AI應用在搜索場景中效果最好的三個。
2. ML技術棧和流程
ML的整個技術棧,還有我們的模型訓練流程,如下圖所示。
3. 具體場景
這里講一下AI的一個具體場景,就是過濾請求。
我們有一個非常開銷非常大的子引擎,叫多票。它會把多個不同航空公司的出票拼接起來,返回給最終用戶。但是,它的拼接計算非常昂貴,所以只對一部分產品開放。我們通過機器學習找到了哪些查詢可以通過多票引擎得到最好的結果,然后只對這一部分查詢用戶開放,結果顯示非常好。
大家可以看PPT右上角的圖片,我們整個引擎能夠過濾掉超過80%的請求,在流量高峰的時候,它能把曲線變得平滑起來,非常顯著。整個對于查詢結果、訂單數,都沒有太大的影響,而且節省了80%的產品資源。同時,可以看到這種線上模型,它的運算時間也是非常短的,普遍低于1毫秒。
五、總結
最后對這次的分享做總結,希望能夠給大家帶來一點點啟發。我們使用了多層靈活的緩存,從而能很好的應對高流量的沖擊,提高反應速度。
另外我們使用了比較可靠的調度和負載均衡,這樣就使我們的服務保持高可用狀態,并且解決了長尾的查詢延遲問題。最后我們在攜程內部嘗試了很多技術革新,將適度的AI技術推向生產,從目前來看,機器學習發揮了很好的效果。帶來了ROI的提升,節省了效率,另外在流量高峰中,它能夠起到很好的削峰作用。以上就是我們為應對高流量洪峰所采取了一系列有針對性的架構改善。
六、Q&A
Q:在哪些場景下使用緩存?
A:所有的場景都要考慮使用緩存。在高流量的情況下,每一級緩存都能帶來很好的保護系統,提高性能的效果,但是一定要考慮到緩存失效時的應對措施。
Q:緩存的迭代過程是怎樣的?
A:如前文所述,我們先有L1,然后又加了L2,主要是因為我們的流量越來越大,引擎的外部依賴逐漸撐不住了,所以我們不得不把中間結果也高效的緩存起來,這就是我們L1到L2的演進過程。在二級緩存我們用Redis替代了MongoDB,是出于高可用性的考慮,當然費用的節省也是當時考慮的一個因素,但是更主要的我們發現了自運維的MongoDB比Redis,它的整體可用性要差很多,所以最后決定做了切換。
Q:分布式緩存的設計方式?
A:分布式緩存的關鍵在于它的鍵值怎么設定?必須要根據特定的業務場景,比如說我們有的鍵值里加入了IP地址,也就是我們的Pooling,基于Redis建立了它的隊列,所以我們queue當中是把這種請求方的IP作為建設的一部分放了進去,這樣就能保證子任務能夠知道到哪兒去查詢它相應的返回結果。
Q:為什么redis的讀寫延遲能做到3ms以內呢?
A:我首先要解釋一下,所謂讀寫延時低,其實主要指的是讀延時,讀延時可以做到三毫秒以內,應該是沒有什么問題的。
Q:這個隊列是內存隊列?還是MQ?
A:互聯隊列我們是用的Redis實現的,而不是用的消息隊列或者內存隊列,主要是為了保證它的高可用性。
Q:緩存失效怎么刷新的,這里涉及分布式鎖吧?
A:前文提到的緩存失效,并不是指它里邊存的數據失效,我主要指的是整個緩存機制失效了。我們不需要分布式鎖,因為我們都是單獨的key-value存儲。
Q:緩存數據一致性怎么保證的?
A:這個是非常難保證的,我們常用的技巧是,首先緩存超過我們預設的閾值的話,我們會強行清除。然后如果有更精確的內容進來,我們是要動態刷新的。比如本來可以存5分鐘,但是在第2分鐘有一位用戶查詢并且下單,這時候肯定是要做一次實時查詢,順便把還沒有過期的內容也刷新一遍。
Q:熱key,大key怎么監控的?
A:其實對我們來說,熱區沒有那么明顯,因為一般來說我們的一個key基本上對應一個點,一個出發地和一個目的地,中間再加上各種渠道引擎的限制。而不像分片那樣,你分成16片或者32片,有可能某一分片邏輯設計不合理,導致那一片過熱,然后相應的硬件直接到了瓶頸。
Q:老師能詳細講一下Pooling嗎?
A:我先講一下原理:子任務它們所用的時間長短是不一樣的,如果完全基于我們的SOA進行動態隨機分的話,肯定有的計算節點分到的子任務比較重,有的節點分到的比較輕,加入Pooling,就好像加入了一個排隊策略,特別是對于中間還會實時調用離開幾秒鐘的情況,那么排隊策略能夠極大的節省我們的計算資源。
Q:監控是怎么做的?
A:監控我們現在是基于原來用了時序數據庫,如ClickHouse,和Grafana,然后現在兼容了Promeneus的數據收集和API。
Q:二級緩存采用Redis的哪種數據類型?
A:二級緩存存的是我們的中間結果,應該是分類型的數據類型。
Q;TTL計算應該考慮哪些問題?
A:我們首先最害怕數據出現問題,比如系統總是返回用戶一個已經過期的低票價,用戶體驗肯定會很差,所以這方面我們會犧牲命中率,然后會縮短TTL,只不過TTL控制在5分鐘之內,有時候還需要微調一下,所以用了機器學習模型。
Q:IP直連和Pooling沒太聽明白,是說AGG中,涉及到的計算進行拆分,將中間結果進行存儲,其他請求里如果也需要這個中間計算,可以直接獲取?
A:IP直連和PoolingIP直連,其實把負載均勻的分到各個節點Pooling,只不過你要計算的子任務加入隊列,然后每一個運算節點每次取一個,計算完了再放回去。這樣計算的效率更高。中間結果沒有共享,中間結果存回去是因為有的子任務需要中間離開,再去查其他的實時系統,所以就相當于把它分成了兩個運算子任務,中間的任務要重新放回隊列。
Q:下單類似秒殺了吧,發現一瞬間票光了,相應的緩存是怎么更新的?
A:如果我們有第1個用戶選擇了一個運價,沒有通過,我們是要把緩存數據都給殺掉的,然后盡量防止第2個用戶還會陷入同樣的問題。
Q:多級緩存數據怎么保證一致?
A:因為我們一級緩存存的是最終的結果,二級緩存是中間結果,所以不需要保持一致。
Q:請教一下,一級、二級、三級緩存,請求過來了,怎么提高吞吐量,按理說,每個查詢過程需要消耗時間的,吞吐量應該會下降?
A:是這樣的,如果你沒有這些緩存的話,那幾乎所有都要走一遍。實時計算這種情況下,時間長,而且我們部署的集群能夠響應的數很有限,然后一、二、三級每一級都能夠屏蔽掉很多的請求。我們一級大概能夠屏蔽20%,二級可以屏蔽掉差不多40%~50%。然后在這種情況下,同樣的集群吞吐量顯然是有明顯增加。
Q:如何防止緩存過期時刻產生的擊穿問題,目前攜程是定時任務主動緩存,還是根據用戶請求進行被動的緩存?
A:對于緩存清除,我們既有定時任務,也有被動的更新。比如說用戶又取了一次或者購票失敗這些情況,我們都是會刷新或者清除緩存的。
Q:搜索結果會根據用戶特征重新計算運價和票種么?
A:我要做一個小的澄清,用戶有時候會奇怪:為什么我的運價跟別人的運價不一致,是不是有殺熟或者其他的情況?我可以負責任的告訴大家,我們不利用大數據殺熟!那為什么同樣的查詢返回的結果不一樣?有一定的比例是因為我們緩存數據出現了問題,比如前面緩存的到后面票賣光了,然后又推給了不幸的用戶。另外一點是,我們有很多的引擎,比如說國外的供應商,特別是聯航,他們的系統帶寬不夠寬,可用性沒有那么高,延時也高,所以有一部分這種低價的票不能夠及時返回到我們最終結果里邊,就會出現這種情況,這不是我們算法特意去做的,只不過是我們系統的局限性。
Q:Pooling 為什么使用 Redis?
A:Redis是為了追求更高的讀寫速度,其他中間件,比如說內存隊列是很難用在我們這種分布式的調度當中。我們如果用message queue的話,由于它存在很明顯的順序性,不能夠基于鍵值去讀到你所寫的,比如你發送了一個子任務,這時候你要定時去拿這個結果,但是你基于其他的消息隊列或者內存隊列是沒法拿到的,這也是我們的一個限制。
Q:多級緩存預熱如何保證MySQL不崩?
A:冷啟動的問題更多是作用在是本地緩存,因為本地緩存發布有其他的情況,需要有一個預熱的環節,在這之間是不能接受生產流量的。對于多級緩存、分布式緩存、預熱問題很小,因為本來它就是一個分布式的,可能有一小部分節點,比如要下線或者什么,但是對整個緩存機制來說影響很小,然后這一部分請求又分散到我們的多個服務器上,幾乎不會產生太大的抖動的。但是還要重復一點,如果整個緩存機制失效,比如緩存集群由于某種情況完全下掉了,這個時候還是要通過熔斷或者限流來對我們的實時系統作過載保護。
Q:Redis對集合類的QPS并不高,這個怎么解決。
A:其實你Redis多加一些節點,然后減少它的存儲使用率,把整體的throughput提上去就可以了。如果你對云業務有了解的話,就會知道每一個節點,它都是有throughput的限制的。如果就單節點的throughput成為瓶頸的話,就降低節點它的使用率。
講師簡介
宋濤,攜程集團機票業務技術總監。北京大學計算機系畢業,加州大學計算機博士。先后從事微軟公司Windows團隊,智能搜索團隊的架構和技術管理工作,Amazon公司云服務部門云存儲管理服務技術主管。擁有多年大型項目架構經驗,是攜程技術領軍人物。
總結
以上是生活随笔為你收集整理的tkmybatis 子查询_日均20亿流量:携程机票查询系统的架构升级的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Python如何实现孤立随机森林算法
- 下一篇: JSON文件是什么意思