攜程國際 BU 的 SEO 重構實踐
作者簡介
熊聘,攜程國際事業部公共研發團隊Leader,目前主要負責國際化相關的基礎元件和市場相關專案的研發。開源社群愛好者,喜歡閱讀優秀的開源專案原始碼,對新技術有著深厚的興趣。
最近一段時間一直在對原有的SEO專案進行重構,目前已經進入重構的後期階段,想和大家一起分享一下整個重構的細節,希望對大家有所幫助。
一、什麼是SEO專案
SEO (Search EngineOptimization),搜尋引擎優化,利用搜索引擎(目前主要指Google)的規則提高網站在搜尋引擎內的自然排名和網站的品牌影響力。
使用者在搜尋引擎上搜索相應的關鍵字,點選搜尋結果直接跳轉到SEO的著陸頁(Landing Pages),然後通過Landing Pages將流量引到需要推廣的網站,從而將這些流量轉化成訂單 。
SEO專案主要是根據不同的推廣維度設計相應的Landing Pages,併為這些Landing Pages提供相應的資料,目前該專案主要涵蓋攜程酒店和機票兩大產線,後續可能會接入更多的產線。
二、為什麼要重構
原SEO專案主要存在以下幾個方面的問題:
程式碼耦合 :前端程式碼和服務端程式碼全部耦合在同個專案裡面,在開發過程中相互依賴,頁面資訊模組化,可配置化,支援AB測試等一系列的功能都很難實現。
資料儲存 :SEO專案的資料和之前的其它系統儲存在同一個DB中,並且部分資料表是共用的,必然導致某些表中的欄位從SEO專案的角度來看是無用的但又不能去掉。
資料更新 :資料全部更新完一次約2-3天,整個過程需要人工干預,如果更新過程中出現了任何問題需要重新進行全量更新,並且還存在髒資料 ,主要分為兩類:一類是資料表中某個欄位的值部分是正確的,部分是不正確的;另一類是資料不完整,比如:如果某個城市沒有任何酒店或者機場,則這條城市資料是沒有意義的,因為在做城市維度推廣的時候,這個城市下面是沒有任何酒店或者機場的資料的。
需求無法滿足 :在SEO頁面的底部需要根據一定的規則計算相關的連結資訊,計算某一個站點的某一個產線在某一種特定語種下需要的時間約為4小時,現有16個站點,每個站點有15種語種和有3個產線,計算出所有站點下所有語種和產線的連結資訊需要的時間為16*15*3*4=2880小時(120天),顯然目前的實現方案是無法滿足業務需求的。
為什麼會存在以上這些問題?主要原因如下:
需求迭代太快 :IBU一直處於高速發展的狀態,很多需求都是需要在很短的時間,快速的完成,並且對未來需求的變化很難把握,在做需求的過程中難免會選擇一些短期的,嘗試性的,快速見效的方案。
開發人員少 :在重構之前整個SEO專案僅1-2個人來完成所有的開發工作,當需求源源不斷湧現時,開發人員難免會措手不及,應接不暇。
資料複雜 :目前SEO幾乎需要與機票和酒店相關的所有資料,而這些資料的收集過程又是極其分散、複雜和繁鎖的,收集某條資料時可能需要採集多個數據源的資料才能將這條資料中的所有欄位補全,並且資料量大導致更新時間長,資料之間的關聯性高從而導致資料更新過程中加大了保證資料完整性的難度。
三、技術選型
專案的技術選型對整個專案來說至關重要,這裡主要表述的是服務端的技術選型。
開發語言 :去掉了之前的部分程式碼採用Java實現,部分程式碼採用PHP實現的方案,將開發語言統一定為Java。主要原因是公司主推Java語言,團隊中所有成員最熟練掌握的程式語言都是Java。
資料儲存 :在資料儲存方面主要採用SQL/">MySQL資料庫,去掉了之前一部分資料採用ES儲存,另一部分資料採用MySQL儲存的方案。SEO專案中酒店的資料量最大,也僅千萬級,對於MySQL來說是完全沒有問題的。
RPC 框架 :公司提供了兩種對外暴露服務的方式,一種是通過Baiji契約實現的,另一種是CDubbo。未選擇後者的主要原因是當時剛推出來,在穩定性上可能會略差於前者,同時整個團隊對前者的理解更深入,使用的也多一些,降低學習成本。
四、設計方案
SEO 專案的整體架構如下圖所示:
1 、Vampire
主要是用來採集資料並轉換成格式化的資料。採集資料的方式主要有增量和全量兩種,資料來源可以是MQ、DB和API(後面還會接入更多的資料來源)。其核心思想是通過併發的方式拉取來自不同資料來源的資料並將這些資料進行轉換成格式化的資料,然後呼叫Faba的Write介面將資料寫入DB中。
由於全量資料的資料量較大,所以在整個過程中拉取全量資料最為複雜。
從目前來看更新全量資料絕大多數情況是採用呼叫API的方式,需要考慮被呼叫API的QPS、響應時間、更新一次的時間間隔、API的返回報文大小(有些情況需要考慮分頁)、API的超時時間、Gateway超時時間、網路頻寬、資料之間的依賴關係等,從而確定Vampire在呼叫API時的執行緒數 、呼叫頻次、呼叫週期、呼叫時間(一般在非高峰期呼叫)、部署時的機器數量、虛擬機器的CPU核數和記憶體大小等,針對呼叫不同的API需要對不同引數進行優化。
增量更新相對而言簡單一些,主要採用MQ的對接方式,需要考慮先發送的訊息後到達,後傳送的訊息先到達的情況、重複訊息、訊息丟失、MQ中佇列的大小等。在整個拉取資料的過程中還需要考慮資料提供方可能出現髒資料或者無法支撐Vampire帶來的流量,因此還需要支援暫停、恢復、強制更新等功能。
無論是增量還是全量的方式拉取資料,最後都需要轉換成格式化的資料並寫入DB,這個轉換過程的處理速度至關重要,因為Vampire從整體上來看其實是一個生產者和消費者模型,生產者是接入的各種不同資料來源,而消費者則是將拉取的資料進行轉化然後呼叫Faba提供的寫介面,快速完成資料的轉換工作。
理想情況下應該是生產者的生產速度等於消費者的消費速度,當生產速度大於消費速度時,生產出來未被來得及消費的資料就會囤積在記憶體中,容易造成OOM,所以在實際使用的時候一般是消費速度大於生產速度。
而對於Vampire而言,生產者的速度是接入各資料來源的流量之和,隨著資料來源的增加而增加,但是消費者的消費能力是固定的,所以要想提高整個資料採集和轉化的吞吐量,本質上是要提高消費者的速度,也就是提高Faba的Write介面的速度(後面會詳細講解Faba處理資料的機制)。
目前生產環境部署了4臺8核8G的虛擬機器,Vampire的處理能力可以達到每秒10K+,處理1000W條資料耗時約30min。
2 、Faba
該子專案主要是為整個SEO專案提供資料Read和Write操作。其中Write介面主要由Vampire呼叫,用來補充資料,Vampire將採集到並轉換好的資料通過呼叫Faba的Write介面將資料寫入DB,Read介面主要是對外提供訪問資料的方式,由Service來呼叫。
Write 介面主要是採用非同步的方式實現的,Vampire在呼叫時會將資料先暫存到一個訊息佇列中,然後再來消費這些資料,這樣處理的好處在於:首先提高了Write介面的QPS和響應時間,其次可以將一些相同的操作進行合併成批量的操作,從而儘量減少DB連線數的消耗,最後可以在寫入時儘可能的對一個批次的寫入的資料進行去重,減少不必要的寫入操作。
Write介面的設計需要考慮三個方面的因素:
第一、 支援冪等。 因為寫入的資料來源於訊息佇列,訊息佇列會有重試的機制,所以在寫入的時候需要支援冪等。
其實訊息佇列也不能保證資料是有序到達的,資料是否有序到達僅對增量拉取資料有影響,對於全量拉取資料沒有影響,因為在全量拉取資料時,每條資料當且僅當只會被拉取一次,所以對每條資料的更新操作是相互獨立的無需考慮先後順序。
對於增量拉取資料而言,假設一條城市資料在同一時刻先後將城市名稱從A修改到B,再從B修改到C,這兩條更新的操作會被有序的推送到Vampire,然後再由Vampire轉換成格式化資料後呼叫Faba的Write介面,從訊息佇列中消費這兩條資料時可能會先收到城市名稱從B修改到C的資料,後收到從A修改到B的資料,這時會以兩條資料發生修改的時間做為時間戳,在DB中更新資料時只更新當前時間戳大於這條資料在DB中的更新時間,其餘的全部過濾掉,也就是城市名稱從B修改到C的資料會被更新到DB,從A修改到B的資料會被過濾掉。
第二、 消費速率。 很容易看出在整個寫入的過程中的瓶頸是DB的寫操作,公司DB的連線池大小是100,也就是說通過多執行緒來消費訊息佇列中的資料,執行緒池的大小不要超過100,確定了消費者的消費能力,生產者的生產能力只需要通過簡單的計算就可以確定了,理論上只需要將生產者單位時間內生產資料的總量等於消費者執行緒數100*每個批次內資料平均條數,這只是一個理想的情況。
實際情況可能還需要考慮三個因素:
1) 訊息佇列的大小 ,也就是囤積資料的能力,這個與機器記憶體有關;
2) 可接受的資料延時時間 ,也就是一條資料從進入訊息佇列到寫入DB的時間;
3) IO的處理能力 ,往DB中寫資料會產生大量的IO操作,特別是在進行批量寫入操作時,之前由於這個因素沒有考慮到,導致和SEO的DB在同一臺物理機器上的其它DB的以前正常的讀寫操作出現大量的超時告警。
第三、 資料優先順序 ,Vampire會從不同的資料來源來拉取資料,不同的資料來源會提供某一條資料中的若干個欄位,不同的資料來源的資料質量也會有所不一樣,也就是不同資料來源對同一條資料中的若干個欄位有不同的優先順序,優先順序高的資料質量高,這個優先順序是在接入資料來源時定義的,所以在更新資料時還需要根據資料的優先順序來判斷資料是否更新,目前同一條資料的同一個欄位的資料來源只有一個,所以可以先不考慮這方面的問題。
Write介面的效能
Read 介面目前主要是從DB中讀取資料,其效能主要取決於以下兩個方面的因素:
第一、 資料庫表結構的設計 ,在設計時儘量減少資料的冗餘,將原來的每張資料表垂直拆分成多張資料表,根據業務需求建立好索引,讓每一條查詢的SQL語句都走索引,對於複雜的SQL查詢,拆分成多條簡單的SQL,然後讓每條簡單的SQL都命中索引,並且將這些簡單的SQL儘可能的複用,如果某一條SQL查詢出來的結果會比較大需要分頁,這時會通過對SQL的執行進行解析,確定出合理的頁大小,對於複雜查詢和分頁查詢多資料情況下都是通過執行多條簡單的SQL將返回的結果通過程式組裝的方式完成的。
第二、 介面的設計 ,對外的Read介面在設計時也是儘量的簡單,這裡的簡單包括入參簡單和返回值簡單。
入參簡單指的是呼叫介面傳入的引數儘可能的少且傳入的每個引數都是必要的,例如:某一個介面有A、B和C三個引數,假設通過A和C這兩個引數可以間接推匯出B這個引數,這時B這個引數就是沒有必要的,應該去掉;
返回值簡單指的是返回的報文不要太多,在設計時一般小於4KB,同時返回的報文中的資料欄位都是有用的。
在整套介面拆分的過程中還需要考慮兩個重要的因素:
1) 所有介面通過若干次的組合呼叫是否可以獲取DB中的所有有用資料 ;
2) 完成一個特定的功能需要呼叫多個簡單介面的次數儘可能的少,儘量多呼叫響應快的介面,少呼叫響應略慢的介面 。
在單機4核4G,Tomcat連線數200,DB連線數100的環境下,資料量為1KW+時,Read介面直接訪問資料庫,不走快取,對於簡單的查詢QPS最高可以達到1400+,對於複雜的多條件分頁查詢QPS最低可達到400+
Faba 中的快取分為本地快取和分散式快取兩種。
對於 本地快取 主要儲存一些資料體量小,訪問頻次高,資料不一致性要求低的資料; 分散式快取 主要是通過Redis作為載體來實現的,儲存一些資料體量相對較大,value小,訪問頻次高的資料。
同時在快取資料時對資料量小的資料儘量做到全量快取,定期更新,對於資料量大的資料採用LRU淘汰策略來更新快取,在快取空間固定的情況下,提高快取命中率。由於根據目前的需求來看僅通過直連DB的方式達到的QPS已經可以滿足了,所以開發快取的優先順序較低,目前還在開發過程,介面效能方面的資料暫時還不能給出。
3 、Service
根據對業務需求的分析發現,每個產線的SEO頁面都是由若干套頁面組成的,每套頁面都是從不同的角度來推廣,每個頁面由若干個Module組成,一個Module對應一個介面。
以機票為例:機票的SEO頁面會包含出發地和機場這兩套頁面,出發地這套頁面由A、B和C這三個Module組成,機場這套頁面由B、C和D這三個Module組成,這時僅需要開發4個介面分別實現A、B、C和D這4個Module對應的功能即可,可以很好的提高介面的複用性。同時,也可以通過配置讓一個頁面中的某個Module在不同的語種、幣種、城市等維度中展示不同的資料。
4 、Page
該專案主要由前端團隊負責,這裡不做詳細描述。
5 、Portal
主要由4個模組組成,其中Config模組可以根據不同的語種、幣種等條件進行配置來控制Service中的各介面在不同引數情況下的返回結果、排序方式等;Log模組主要用來記錄Vampire資料更新的進度、更新時長和日誌等;
AB Test模組主要是配合Config模組實現不同配置之間的對比,從而幫助業務人員更好的做出抉擇;Statistic模組主要用來統計Faba中快取的命中率等效能方面的資料。
四、總結
SEO 專案的核心在於資料,如何採集資料,更新資料,將質量較好的資料在每次的更新中逐漸沉澱下來是整個專案的關鍵; 介面、資料表設計的儘量簡單是提高整個專案效能的根本。
本文只是大致描述了一下SEO專案重構的整體方案,對於設計方案中的具體實現細節並未做過多的描述,同時有些非核心功能還在開發中,對此感興趣的同學可以留言,也歡迎大家拍磚。
【推薦閱讀】