大廠報價查詢系統性能優化之道!

0 前言

機票查詢系統,日均億級流量,要求高吞吐,低延遲架構設計。提升緩存的效率以及實時計算模塊長尾延遲,成爲制約機票查詢系統性能關鍵。本文介紹機票查詢系統在緩存和實時計算兩個領域的架構提升。

1 機票搜索服務概述

1.1 機票搜索的業務特點

機票搜索業務:輸入目的地,然後點擊搜索,後臺就開始捲了。基本1~2s將最優結果反給用戶。這個業務存在以下業務特點。

1.1.1 高流量、低延時、高成功率

超高流量,同時,對搜索結果要求也很高——成功率要高,不能查詢失敗或強說成功,希望能反給用戶最優最新數據。

1.1.2 多引擎聚合,SLA不一

機票搜索數據來源哪?很大一部分來源自己的機票運價引擎。爲補充產品豐富性,還引入國際一些GDS、SLA,如聯航。將外部引擎和自己引擎結果聚合後發給用戶。

1.1.3 計算密集&IO密集

大家可能意識到,我說到我們自己的引擎就是基於一些運價的數據、倉位的數據,還有其他一些航班的信息,我們會計算、比對、聚合,這是一個非常技術密計算密集型的這麼一個服務。同時呢,外部的GDS提供的查詢接口或者查詢引擎,對我們來說又是一個IO密集型的子系統。我們的搜索服務要將這兩種不同的引擎結果很好地聚合起來。

1.1.4 不同業務場景的搜索結果不同要求

作爲OTA巨頭,還支持不同應用場景。如同樣北京飛上海,由於搜索條件或搜索渠道不一,返回結果有所不同。如客戶是學生,可能搜到學生特價票,其他用戶就看不到。

2 公司基建

爲應對如此業務,有哪些利器?

2.1 三個獨立IDC

互相做災備,實現其中一個IDC完全宕機,業務也不受影響。

2.2 DataCenter技術棧

SpringCloud+K8s+雲服務(海外),感謝Netflix開源支撐國內互聯網極速發展。

2.3 基於開源的DevOps

我們基於開源做了整套的DevOps工具和框架。

2.4 多種存儲方案

公司內部有比較完善可用度比較高的存儲方案,包括MySQL,Redis,MangoDB……

2.5 網絡可靠性

注重網絡可靠性,做了很多DR開發,SRE實踐,廣泛推動熔斷,限流等,以保證用戶得到高質量服務。

3 機票搜索服務架構

3.1 架構圖

IDC有三個,先引入GateWay分流前端服務,前端服務通過服務治理,和後端聚合服務交互。聚合服務再調用很多引擎服務。

聚合服務結果,通過Kafka推到AI數據平臺,做大數據分析、流量回放等數據操作。雲上部署數據的過濾服務,使傳回數據減少90%。

4 緩存架構

4.1 緩存的挑戰和策略

4.1.1 爲啥大量使用緩存應對流量高峯?

提高效率、速度的首選技術手段。

雖使用很多開源技術,但還有瓶頸。如數據庫是分片、高可用的MySQL,但和一些雲存儲、雲數據庫比,其帶寬、存儲量、可用性有差距,通常需用緩存保護我們的數據庫,不然頻繁讀取會使數據庫很快超載。

很多外部依賴,提供給我們的帶寬,QPS有限。而公司整體業務量是快速增長的,而外部的業務夥伴給我們的帶寬,要麼已達技術瓶頸,要麼開始收高費用。此時,使用緩存就可保護外部的一些合作伙伴,不至於擊穿他們的系統,也可幫我們降本。

4.1.2 本地緩存 VS 分佈式緩存

整個架構的演進過程,一開始本地緩存較多,後來部分用到分佈式緩存。

本地緩存的主要問題:

  • 啓動時,有個冷啓動過程,對快速部署不利
  • 與分佈式緩存相比,本地緩存命中率太低

對海量的數據而言,單機提供命中率非常低,5%甚至更低。對此,現已全面切向Redis分佈式緩存。本着對戰failure的理念,不得不考慮失敗場景。萬一集羣掛掉或一部分分片掛掉,這時需要通過限流客戶端、熔斷等,防止雪崩效應。

4.1.3 TTL

  • 命中率
  • 新鮮度
  • 動態更新

TTL生命週期跟業務強相關。買機票經常遇到:剛在報價列表頁看到一個低價機票,點進報價詳情頁就沒了,why?航空公司低價艙位票,一次可能只放幾張,若在熱門航線,可能同時幾百人在查,它們都可能看到這幾張票,它就會出現在緩存裏。若已有10人訂了票,其他人看到緩存再點進去,運價就已失效。對此,就要求權衡,不能片面追求高命中率,還得兼顧數據新鮮度。爲保證新鮮度、數據準確性,還有大量定時任務去做更新和清理。

4.2 緩存架構演進

4.2.1 多級緩存

架構圖的三處緩存:

  • 引擎級緩存
  • L1分佈式聚合緩存,基本上就是用戶看到的最終查詢結果
  • L2二級緩存,分佈式的子引擎結果

若聚合服務需多個返回結果,很大程度都是先讀一級緩存,一級緩存沒有命中的話,再從二級緩存裏面去讀中間結果,這樣可以快速聚合出一個大家所需要的結果返回。

4.2.2 引擎緩存

引擎緩存:

  • 查詢結果緩存
  • 中間產品緩存
  • 基礎數據緩存

使用一個多級緩存模式。如下圖,最頂部是指引前的結果緩存,儲存在Redis,引擎內部根據產品、供應商,有多個渠道的中間結果,所以對子引擎來說會有個中間緩存。

這些中間結果計算,需要數據,這數據就來自最基礎的一級緩存。

4.2.3 基於Redis的一級緩存

Pros:

  • 讀寫性能高
  • 水平擴展

Cons:

  • 固定TTL
  • 命中率和新鮮度的平衡

結果:

  • 命中率<20%
  • 高新鮮度(TTL<5m,動態刷新
  • 讀寫延遲<3ms

一級緩存使用Redis,是考慮其讀寫性能好,快速,水平擴展性能,能提高存儲量及帶寬。但當前設計侷限性:

  • 爲簡單,使用固定TTL,這是爲保證返回結果的相對新鮮
  • 爲命中率和新鮮度,還在不斷提高

目前解決方案還不能完美解決這倆問題。

分析下返回結果,一級緩存命中率小於20%,某些場景更低,就是爲保證更高準確度和新鮮度。高優先度,一級緩存的TTL肯定低於5min,有些場景可能只有幾十s;支持動態刷新,整體延遲小於3ms。整個運行過程可用性較好。

4.2.4 基於Redis的二級緩存(架構升級)

Pros:

  • 讀寫性能進一步提升
  • 服務可靠性提升
  • 成本消減

Cons:

  • 增加複雜性替代二級索引

結果:

  • 成本降低90%
  • 讀寫性能提升30%

4.2.5 基於MongoDB的二級緩存

二級緩存一開始用MongoDB:

  • 高讀寫性能
  • 支持二級緩存,方便數據清理
  • 多渠道共用子引擎緩存
  • TTL通過ML配置

會計算相對較優TTL,保證特定數據:

  • 有的可緩存久點
  • 有的可快速更新迭代

二級緩存基於MongoDB,也有侷限性:

  • 架構是越簡單越好,多引入一種存儲會增加維護代價(強依賴)
  • 由於MongoDB的license模式,使得費用非常高(Ops)

結果:

  • 但二級緩存使查詢整體吞吐量提高3倍
  • 通過機器學習設定的TTL,使命中率提升27%
  • 各引擎平均延時降低20%

都是可喜變化。在一個成熟的流量非常大的系統,能有個10%提升,就是個顯著技術特點。

針對MongoDB也做了提升,最後將其切成Redis,通過設計方案,雖增加部分複雜性,但替代了二級索引,改進結果就是成本降低90%,讀寫性能提升30%。

5 LB演進

  • 系統首要目標滿足高可用
  • 其次是高流量支撐

可通過多層的均衡路由實現,把這些流量均勻分配到多個IDC的多個集羣。

5.1 目標

  • 高可用

  • 高流量支撐

  • 低事故影響範圍

  • 提升資源利用率

  • 優化系統性能(長尾優化)

    如個別查詢時間特長,需要我們找到調度算法問題,一步步解決。

5.2 LB架構

負載均衡

  • Gateway,LB,IP直連

  • DC路由規則

  • IP直連+Pooling

    • 計算密集型任務
    • 計算時長&權重
    • 部分依賴外部查詢
  • Set化

LB的演進:

公司的路由和負載均衡的架構,非常典型,有GateWay、load、balance、IP直連,在IP基礎上實現了一項新的Pooling技術。也實現了Set化,在同一IDC,所有的服務都只和該數據中心的節點打交道,儘量減少跨地區網絡互動。

5.3 Pooling

爲啥做 Pooling?有些計算密集的引擎,存在耗時長,耗CPU資源多的子任務,這些子任務可能夾雜一些實時請求,所以這些任務可能會留在線程裏邊,阻塞整個流程。

Pooling就負責:我們把這些子任務放在queue裏邊,將節點作爲worker,總是動態的去取,每次只取一個,計算完了要麼把結果返回,要麼把中間結果再放回queue。這樣的話如果有任何實時的外部調用,我們就可以把它分成多次,放進queue進行task的整個提交執行和應用結果的返回。

5.4 過載保護

有過載保護

  • 扔掉排隊時間超過T的請求(T爲超時時間),所有請求均超時,系統整體不可用
  • 扔到排隊時間超過X的請求(X爲小於T的時間),平均響應時間爲X+m,系統整體可用。m爲平均處理時間

Pooling設計需要一個過載保護,當流量實在太高,可用簡單的過載保護,把等待時間超過某閾值的請求全扔掉。當然該閾值肯定小於會話時間,就能保證整個Pooling服務高可用。

雖可能過濾掉一些請求,但若沒有過載保護,易發生滾雪球效應,queue裏任務越來越多,當系統取到一個任務時,實際上它的原請求可能早已timeout。

img

壓測結果可見:達到系統極限值前,有無Pooling兩種case的負載均衡差異。如80%負載下,不採用Pooling的排隊時間比有Pooling高10倍:

所以一些面臨相同流量問題廠家,可考慮把 Pooling 作爲一個動態調度或一個control plan的改進措施。

如下圖,實現Pooling後平均響應時間基本沒大變化,還是單層查詢計算普遍60~70ms。但實現Pooling後,顯著的鍵值變少,鍵值範圍也都明顯控制在平均時間兩倍內。這對大體量服務來說,比較平順曲線正是所需。

6 AI賦能

6.1 應用場景

6.1.1 反爬

在前端,我們設定了智能反爬,能幫助屏蔽掉9%的流量。

6.1.2 查詢篩選

在聚合服務中,我們並會把所有請求都壓到子系統,而是會進行一定的模式運營,找出價值最高實際用戶,然後把他們的請求發到引擎當中。對於一些實際價值沒有那麼高的,更多的是用緩存,或者屏蔽掉一些比較昂貴的引擎。

6.1.3 TTL智能設定

整個TTL設定使用ML技術。

6.2 ML技術棧和流程

ML技術棧和模型訓練流程:

6.3 過濾請求

開銷非常大的子引擎多票,會拼接多個不同航空公司的出票,返給用戶。但拼接計算昂貴,只對一部分產品開放。通過機器學習找到哪些查詢可通過多票引擎得到最好結果,然後只對這一部分查詢用戶開放,結果也很不錯。

看右上角圖片,整個引擎能過濾掉超過80%請求,流量高峯時能把曲線變得平滑,效果顯著。整個對於查詢結果、訂單數,都沒太大影響,且節省80%產品資源。這種線上模型運算時間也短,普遍低於1ms。

7 總結

使用了多層靈活緩存,從而能很好的應對高流量的衝擊,提高反應速度。

使用可靠的調度和負載均衡,這樣就使我們的服務保持高可用狀態,並且解決了長尾的查詢延遲問題。最後內部嘗試了很多技術革新,將適度的AI技術推向生產,從目前來看,機器學習發揮了很好的效果。帶來了ROI的提升,節省了效率,另外在流量高峯中,它能夠起到很好的削峯作用。以上就是我們爲應對高流量洪峯所採取了一系列有針對性的架構改善。

  • 多層,靈活的緩存 -> 流量,速度
  • 可靠的調度和負載均衡 -> 高可用
  • 適度的AI -> ROI,削峯

8 Q&A

Q:啥場景用緩存?

A:所有場景都要考慮緩存。高流量時,每級緩存都能帶來很好的保護系統,提高性能的效果,但要考慮到緩存失效的應對措施。

Q:緩存迭代過程咋樣的?

A:先有L1,又加L2,主要因爲流量越來越大,引擎外部依賴逐漸撐不住,不得不把中間結果也高效緩存,此即L1到L2的演進。二級緩存用Redis替代MongoDB,是出於高可用考慮,費用節省也是一個因素,但更主要是發現自運維的MongoDB比Redis,整體可用性要差很多,所以最後決定切換。

Q:分佈式緩存的設計方式?

A:分佈式緩存的關鍵在於它的KV怎麼設定?須根據業務場景,如有的KV里加入IP地址,即Pooling,基於Redis建立了它的隊列,所以我們queue當中是把這種請求方的IP作爲建設的一部分放了進去,就能保證子任務能知道到哪查詢它相應的返回結果。

Q:爲什麼redis的讀寫延遲能做到3ms以內呢?

A:讀寫延時低其實主要指讀延時,讀延時3ms內沒問題。

Q:這隊列是內存隊列?還是MQ?

A:互聯隊列用Redis,主要是爲保證其高可用性。

Q:緩存失效咋刷新,涉及分佈式鎖?

A:文章所提緩存失效,並非指它裏邊存的數據失效,主要指整個緩存機制失效。無需分佈式鎖,因爲都是單獨的KV存儲。

Q:緩存數據一致性咋保證?

A:非常難保證,常用技巧:緩存超過閾值,強行清除。然後若有更精確內容進來,要動態刷新。如本可存5min,但第2min有位用戶查詢並下單,這時肯定是要做一次實時查詢,順便把還沒過期的內容也刷新一遍。

Q:熱key,大key咋監控?

A:對我們熱區沒那麼明顯,因爲一般我們的一個key對應一個點,一個出發地和一個目的地,中間再加上各種渠道引擎的限制。而不像分片,你分成16或32片,可能某一分片邏輯設計不合理,導致那片過熱,然後相應硬件直接到瓶頸。

Q:詳解Pooling?

A:原理:子任務耗時間不一,若完全基於SOA進行動態隨機分,肯定有的計算節點分到的子任務較重,有的較輕,加入Pooling,就好像加入一個排隊策略,特別是對中間還會實時調用離開幾s的case,排隊策略能極大節省計算資源。

Q:監控咋做的?

A:基於原來用了時序數據庫,如ClickHouse,和Grafana,然後現在兼容Promeneus的數據收集和API。

Q:二級緩存採用Redis的啥數據類型?

A:二級緩存存儲中間結果,應該是分類型的數據類型。

Q;TTL計算應該考慮啥?

A:最害怕數據異常,如系統總返回用戶一個已過期低票價,用戶體驗很差,所以這方面犧牲命中率,然後縮短TTL,只不過TTL控制在5min內,有時還要微調,所以還得用機器學習模型。

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:爲追求更高讀寫速度,其他中間件如內存隊列,很難用在分佈式調度。若用message queue,由於它存在明顯順序性,不能基於KV去讀到你所寫的,如你發了個子任務,這時你要定時取其結果,但你基於MQ或內存隊列沒法拿到,這也是一個限制。

Q:多級緩存預熱咋保證MySQL不崩?

A:冷啓動問題更多作用在本地緩存,因爲本地緩存發佈有其他的情況,需要預熱,在這之間不能接受生產流量。對多級緩存、分佈式緩存,預熱不是問題,因爲它本就是分佈式的,可能有部分節點要下線之類,但對整個緩存機制影響很小,然後這一部分請求又分散到我們的多個服務器,不會產生太大抖動。但若整個緩存機制失效如緩存集羣完全下掉,還是要通過熔斷或限流對實時系統作過載保護。

Q:Redis對集合類QPS不高,咋辦?

A:Redis多加些節點,減少它的存儲使用率,把整體throughput提上即可。若你對雲業務有了解,就知道每個節點都有throughput限制。若單節點throughput成爲瓶頸,那就降低節點使用率。

關注我,緊跟本系列專欄文章,咱們下篇再續!

作者簡介:魔都技術專家兼架構,多家大廠後端一線研發經驗,各大技術社區頭部專家博主,編程嚴選網創始人。具有豐富的引領團隊經驗,深厚業務架構和解決方案的積累。

負責:

  • 中央/分銷預訂系統性能優化
  • 活動&優惠券等營銷中臺建設
  • 交易平臺及數據中臺等架構和開發設計

目前主攻降低軟件複雜性設計、構建高可用系統方向。

參考:

本文由博客一文多發平臺 OpenWrite 發佈!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章