摘要:為了幫助用戶更好地完成消費決策閉環,馬蜂窩上線了大交通業務。現在,用戶在馬蜂窩也可以完成購買機票火車票等操作。第二階段架構轉變及服務化初探從年開始,整個大交通業務開始從架構向服務化演變。
交通方式是用戶旅行前要考慮的核心要素之一。為了幫助用戶更好地完成消費決策閉環,馬蜂窩上線了大交通業務。現在,用戶在馬蜂窩也可以完成購買機票、火車票等操作。
與大多數業務系統相同,我們一樣經歷著從無到有,再到快速發展的過程。本文將以火車票業務系統為例,主要從技術的角度,和大家分享在一個新興業務發展的不同階段背后,系統建設與架構演變方面的一些經驗。
第一階段:從無到有在這個階段,快速支撐起業務,填補業務空白是第一目標。基于這樣的考慮,當時的火車票業務從模式上選擇的是供應商代購;從技術的角度需要優先實現用戶在馬蜂窩 App 查詢車次余票信息、購票、支付,以及取消、退票退款等核心功能的開發。
圖1-核心功能與流程
技術架構綜合考慮項目目標、時間、成本、人力等因素,當時網站服務架構我們選擇的是 LNMP(Linux 系統下 Nginx+MySQL+PHP)。整個系統從物理層劃分為訪問層 ( App,H5,PC 運營后臺),接入層 (Nginx),應用層 (PHP 程序),中間件層 (MQ,ElasticSearch),存儲層 (MySQL,Redis)。
對外部系統依賴主要包括公司內部支付、對賬、訂單中心等二方系統,和外部供應商系統。
圖 2-火車票系統 V1.0 技術架構
如圖所示,對外展現功能主要分為兩大塊,一塊是 C 端 App 和 H5,另外是運營后臺。二者分別經過外網 Nginx 和內網 Nginx 統一打到 php train 應用上。程序內部主要有四個入口,分別是:
供其他二方系統調用的 facade 模塊
運營后臺調用的 admin 模塊
處理 App 和 H5 請求的 train 核心模塊
處理外部供應商回調模塊
四個入口會依賴于下層 modules 模塊實現各自功能。對外調用上分兩種情況,一種是調用二方系統的 facade 模塊滿足其公司內部依賴;一種是調用外部供應商。基礎設施上依賴于搜索、消息中間件、數據庫、緩存等。
這是典型的單體架構模式,部署簡單,分層結構清晰,容易快速實現,可以滿足初期產品快速迭代的要求,而且建立在公司已經比較成熟的 PHP 技術基礎之上,不必過多擔心穩定性和可靠性的問題。
該架構支撐火車票業務發展了將近一年的時間,簡單、易維護的架構為火車票業務的快速發展做出了很大的貢獻。然而隨著業務的推進,單體架構的缺陷逐漸暴露出來:
所有功能聚合在一起,代碼修改和重構成本增大
研發團隊規模逐漸擴大,一個系統多人開發協作難度增加
交付效率低,變動范圍難以評估。在自動化測試機制不完善的情況下,易導致「修復越多,缺陷越多」的惡性循環
伸縮性差,只能橫向擴展,無法按模塊垂直擴展
可靠性差,一個 Bug 可能引起系統崩潰
阻礙技術創新,升級困難,牽一發而動全身
為了解決單體架構所帶來的一系列問題,我們開始嘗試向微服務架構演進,并將其作為后續系統建設的方向。
第二階段:架構轉變及服務化初探從 2018 年開始,整個大交通業務開始從 LNMP 架構向服務化演變。
架構轉變——從單體應用到服務化「工欲善其事,必先利其器」,首先簡單介紹一下大交通在實施服務化過程中積累的一些核心設施和組件。
我們最主要的轉變是將開發語言從 PHP 轉為 Java,因此技術選型主要圍繞 Java 生態圈來展開。
開發框架與組件圖3-大交通基礎組件
如上圖所示,整體開發框架與組件從下到上分為四層。這里主要介紹最上層大交通業務場景下的封裝框架和組件層:
mlang:大交通內部工具包
mes-client-starter:大交通 MES 技術埋點采集上報
dubbo-filter:對 Dubbo 調用的 tracing 追蹤
mratelimit:API 限流保護組件
deploy-starter:部署流量摘除工具
tul:統一登錄組件
cat-client:對 CAT 調用增強封裝的統一組件
基礎設施體系服務化的實施離不開基礎設施體系的支持。在公司已有基礎之上,我們陸續建設了:
敏捷基礎設施:基于 Kubernetes 和 Docker
基礎設施監控告警:Zabbix,Prometheus,Grafana
業務告警:基于 ES 日志,MES 埋點 + TAlarm
日志系統 :ELK
CI/CD 系統:基于 Gitlab+Jekins+Docker+Kubernetes
配置中心:Apollo
服務化支撐 :Springboot2.x+Dubbo
服務治理:Dubbo-admin,
灰度控制
TOMPS :大交通應用管理平臺
MPC 消息中心:基于 RocketMQ
定時任務:基于 Elastic-Job
APM 系統 :PinPoint,CAT
PHP 和 Java 雙向互通支持
如上所述,初步構筑了較為完整的 DevOps + 微服務開發體系。整體架構如下:
圖4-大交通基礎設施體系
從上至下依次分為:
訪問層——目前有 App,H5 和微信小程序;
接入層——走公司公共的 Nginx,OpenResty;
業務層的應用——包括無線 API,Dubbo 服務,消
消息中心和定時任務——部署在 Kubernetes+Docker 中
中間件層——包括 ElasticSearch,RocketMQ 等
存儲層——MySQL,Redis,FastDFS,HBase 等
此外,外圍支撐系統包括 CI/CD、服務治理與配置、APM 系統、日志系統、監控告警系統。
CI/CD?系統CI 基于 Sonar + Maven(依賴檢查,版本檢查,編譯打包) + Jekins
CD 基于 Jekins+Docker+Kubernetes
我們目前還沒有放開 Prod 的 OPS 權限給開發,計劃在新版 CD 系統中會逐步開放。
圖5-CI/CD 體系
服務化框架?Dubbo我們選擇 Dubbo 作為分布式微服務框架,主要有以下因素考慮:
成熟的高性能分布式框架。目前很多公司都在使用,已經經受住各方面性能考驗,比較穩定;
可以和 Spring 框架無縫集成。我們的架構正是基于 Spring 搭建,并且接入 Dubbo 時可以做到代碼無侵入,接入也非常方便;
具備服務注冊、發現、路由、負載均衡、服務降級、權重調節等能力;
代碼開源。可以根據需求進行個性化定制,擴展功能,進行自研發
圖 6-Dubbo 架構
除了保持和 Dubbo 官方以及社區的密切聯系外,我們也在不斷對 Dubbo 進行增強與改進,比如基于 dubbo-fitler 的日志追蹤,基于大交通統一應用管理中心的 Dubbo 統一配置管理、服務治理體系建設等。
服務化初探——搶票系統向服務化的演進決不能是一個大躍進運動,那樣只會把應用拆分得七零八落,最終不但大大增加運維成本,而且看不到收益。
為了保證整個系統的服務化演進過程更加平滑,我們首先選擇了搶票系統進行實踐探索。搶票是火車票業務中的一個重要版塊,而且搶票業務相對獨立,與已有的 PHP 電子票業務沖突較少,是我們實施服務化的較佳場景。
在對搶票系統進行服務拆分和設計時,我們積累了一些心得和經驗,主要和大家分享以下幾點。
功能與邊界簡單來說,搶票就是實現用戶提前下搶票單,系統在正式開售之后不斷嘗試為用戶購票的過程。從本質上來說,搶票是一種手段,通過不斷檢測所選日期和車次的余票信息,以在有余票時為用戶發起占座為目的。至于占座成功以后的處理,和正常電子票是沒有什么區別的。理解了這個過程之后,在盡量不改動原有 PHP 系統的前提下,我們這樣劃分它們之間的功能邊界:
圖7-搶票功能劃分
也就是說,用戶下搶票單支付成功,待搶票占座成功后,后續出票的事情我們會交給 PHP 電子票系統去完成。同理,在搶票的逆向方面,只需實現「未搶到票全額退」以及「搶到票的差額退」功能,已出票的線上退和線下退票都由 PHP 系統完成,這就在很大程度上減少了搶票的開發任務。
服務設計服務的設計原則包括隔離、自治性、單一職責、有界上下文、異步通信、獨立部署等。其他部分都比較容易把控,而有界上下文通俗來說反應的就是服務的粒度問題,這也是做服務拆分繞不過去的話題。粒度太大會導致和單體架構類似的問題,粒度太細又會受制于業務和團隊規模。結合實際情況,我們對搶票系統從兩個維度進行拆分:
1. 從業務角度系統劃分為供應商服務 (同步和推送)、正向交易服務、逆向交易服務、活動服務。
圖8-搶票服務設計
正向交易服務:包括用戶下搶票單、支付、取消、出票、查詢、通知等功能
逆向交易服務:包括逆向訂單、退票、退款、查詢、通知等
供應側:去請求資源方完成對應業務操作、下搶票單、取消、占座、出票等
活動服務:包括日常活動、分享、活動排名統計等
2. 從系統層級分為前端 H5 層前后分離、API 接入層、RPC 服務層,和 PHP 之間的橋接層、異步消息處理、定時任務、供應商對外調用和推送網關。
圖9-搶票系統分層
展示層:H5 和小程序,前后端分離
API 層:對 H5 和小程序提供的統一 API 入口網關,負責對后臺服務的聚合,以 HTTP REST 風格對外暴露
服務層:包括上節所提到的業務服務,對外提供 RPC 服務
橋接層:包括調用 PHP 的代理服務,對 Java 側提供 Dubbo RPC 服務,以 HTTP 形式調用統一的 PHP 內部網關;對 PHP 提供的統一 GW,PHP 以 HTTP 形式通過 GW 來調用 Java 服務
消息層:異步的消息處理程序,包括訂單狀態變更通知,優惠券等處理
定時任務層:提供各種補償任務,或者業務輪詢處理
數據要素對于交易系統而言,不管是用何種語言,何種架構,要考慮的最核心部分終歸是數據。數據結構基本反應了業務模型,也左右著程序的設計、開發、擴展與升級維護。我們簡單梳理一下搶票系統所涉及的核心數據表:
1. 創單環節:用戶選擇車次進入填單頁以后,要選擇乘車人、添加聯系人,所以首先會涉及到乘車人表,這塊復用的 PHP 電子票功能
2. 用戶提交創單申請后,將會涉及到以下數據表:
訂單快照表——首先將用戶的創單請求要素存儲起來
搶票訂單表 (order):為用戶創建搶票單
區段表(segment):用于一個訂單中可能存在的連續乘車而產生的多個車次情況 (類似機票航段)
乘車人表 (passenger):搶票單中包含乘車人信息
活動表 (activity):反映訂單中可能包含的活動信息
物品表 (item):反映包含的車票,保險等信息
履約表:用戶購買車票、保險后,最終會做票號回填,我們也叫票號信息表
3. 產生占座結果后:用戶占座失敗涉及全額退款,占座成功可能涉及差額退款,因此我們會有退票訂單表 (refund_order);雖然只涉及到退款,但同樣會有 refund_item 表來記錄退款明細。
訂單狀態訂單系統的核心要點是訂單狀態的定義和流轉,這兩個要素貫穿著整個訂單的生命周期。
我們從之前的系統經驗中總結出兩大痛點,一是訂單狀態定義復雜,我們試圖用一個狀態字段來打通前臺展示和后端邏輯處理,結果導致單一訂單狀態多達 18 種;二是狀態流轉邏輯復雜,流轉的前置因素判斷和后置方向上太多的 if else 判斷,代碼維護成本高。
因此,我們采用有限狀態機來梳理搶票正向訂單的狀態和狀態流轉,關于狀態機的應用,可以參照之前發過的一篇文章《狀態機在馬蜂窩機票交易系統中的應用與優化》,下圖是搶票訂單的狀態流轉圖:
圖10-搶票訂單狀態流轉
我們將狀態分為訂單狀態和支付狀態,通過事件機制來推進狀態的流轉。到達目標態有兩個前提:一是原狀態,二是觸發事件。狀態按照預設的條件和路線進行流轉,將業務邏輯的執行和事件觸發與狀態流轉拆分開,達到解耦和便于擴展維護的目的。
如上圖所示,將訂單狀態定義為:初始化→下單成功→交易成功→關閉。給支付狀態定義為:初始化→待支付→已支付→關閉。以正常 case 來說,用戶下單成功后,會進入下單成功和待支付;用戶通過收銀臺支付后,訂單狀態不變,支付狀態為已支付;之后系統會開始幫用戶占座,占座成功以后,訂單會進入交易成功,支付狀態不變。
如果僅僅是上面的雙狀態,那么業務程序執行倒是簡單了,但無法滿足前臺給用戶豐富的單一狀態展示,因此我們還會記錄關單原因。關單原因目前有 7 種:未關閉、創單失敗、用戶取消、支付超時、運營關單、訂單過期、搶票失敗。我們會根據訂單狀態、支付狀態、關單原因,計算出一個訂單對外展示狀態。
冪等性設計所謂冪等性,是指對一個接口進行一次調用和多次調用,產生的結果應該是一致的。冪等性是系統設計中高可用和容錯性的一個有效保證,并不只存在于分布式系統中。我們知道,在 HTTP 中,GET 接口是天生冪等的,多次執行一個 GET?操作,并不會對系統數據產生不一致的影響,但是 POST,PUT,DELETE 如果重復調用,就可能產生不一致的結果。
具體到我們的訂單狀態來說,前面提到狀態機的流轉是需要事件觸發的,目前搶票正向的觸發事件有:下單成功、支付成功、占座成功、關閉訂單、關閉支付單等等。我們的事件一般由用戶操作或者異步消息推送觸發,其中任意一種都無法避免產生重復請求的可能。以占座成功事件來說,除了修改自身表狀態,還要向訂單中心同步狀態,向 PHP 電子票同步訂單信息,如果不做冪等性控制,后果是非常嚴重的。
保證冪等性的方法有很多,以占座消息為例,我們有兩個措施來保證冪等:
占座消息都帶有協議約束的唯一 serialNo,推送服務可以判斷該消息是否已被正常處理。
業務側的修改實現 CAS(Compare And Swap),簡單來說就是數據庫樂觀鎖,如 update order set order_status = 2 where order_id = 『1234" and order_status = 1 and pay_status = 2 。
小結服務化的實施具備一定的成本,需要人員和基礎設施都有一定的基礎。初始階段,從相對獨立的新業務著手,做好和舊系統的融合復用,能較快的獲取成果。搶票系統在不到一個月的時間內完成產品設計,開發聯調,測試上線,也很好地印證了這一點。
第三階段:服務化推進和系統能力提升搶票系統建設的完成,代表我們邁出了一小步,也只是邁出了一小步,畢竟搶票是周期性的業務。更多的時間里,電子票是我們業務量的主要支撐。在新老系統的并行期,主要有以下痛點:
原有電子票系統由于當時因素影響,與特定供應商綁定緊密,受供應商制約較大;
由于和搶票系統及大交通其他系統之間的兼容成本較高,導致我們統一鏈路追蹤、環境隔離、監控告警等工作實施難度很大;
PHP 和 Java 橋接層承接太多業務,性能無法保證
因此,卸下歷史包袱,盡快完成舊系統的服務化遷移,統一技術棧,使主要業務得到更加有力的系統支撐,是我們接下來的目標。
與業務同行:電子票流程改造我們希望通過對電子票流程的改造,重塑之前應急模式下建立的火車票項目,最終實現以下幾個目標:
建立馬蜂窩火車票的業務規則,改變之前業務功能和流程上受制于供應側規則的局面;
完善用戶體驗和功能,增加在線選座功能,優化搜索下單流程,優化退款速度,提升用戶體驗;
提升數據指標和穩定性,引入新的供應側服務,提高可靠性;供應商分單體系,提升占座成功率和出票率;
技術上完成到 Java 服務化的遷移,為后續業務打下基礎
我們要完成的不僅是技術上的重構,而是結合新的業務訴求,去不斷豐富新的系統,力求達到業務和技術的目標一致性,因此我們將服務化遷移和業務系統建設結合在一起推進。下圖是電子票流程改造后火車票整體架構:
圖11-電子票改造后的火車票架構
圖中淺藍色部分為搶票期間已經建好的功能,深藍色模塊為電子票流程改造新加入的部分。除了和搶票系統類似的供應商接入、正向交易、逆向交易以外,還包括搜索與基礎數據系統,在供應側也增加了電子票的業務功能。同時我們新的運營后臺也已經建立,保證了運營支撐的延續性。
項目實施過程中,除了搶票所說的一些問題之外,也著重解決以下幾個問題。
搜索優化先來看用戶一次站站搜索可能穿過的系統:
圖12-站站查詢調用流程
請求先到 twl api 層,再到 tsearch 查詢服務,tsearch 到 tjs 接入服務再到供應側,整個調用鏈路還是比較長的,如果每次調用都是全鏈路調用,那么結果是不太樂觀的。因此 tsearch 對于查詢結果有 redis 緩存,緩存也是縮短鏈路、提高性能的關鍵。站站查詢要緩存有幾個難點:
對于數據實時性要求很高。核心是余票數量,如果數據不實時,那么用戶再下單占座成功率會很低
數據比較分散。比如出發站,到達站,出發日期,緩存命中率不高
供應側接口不穩定。平均在 1000ms 以上
綜合以上因素考慮,我們設計 tsearch 站站搜索流程如下:
圖13-搜索設計流程
如圖所示,首先對于一個查詢條件,我們會緩存多個渠道的結果,一方面是因為要去對比哪個渠道結果更加準確,另一方面可以增加系統可靠性和緩存命中率。
我們將 Redis 的過期時間設為 10min,對緩存結果定義的有效期為 10s,首先取有效;如果有效為空,則取失效;如果失效也為空,則同步限時 3s 去調用渠道獲取,同時將失效和不存在的緩存渠道交給異步任務去更新。需要注意通過分布式鎖來防止并發更新一個渠道結果。最終的緩存結果如下:
緩存的命中率會在 96% 以上,RT 平均在 500ms 左右,能夠在保證用戶體驗良好的同時,做到及時的數據更新。
消息的消費我們有大量業務是通過異步消息方式來處理的,比如訂單狀態變更消息、占座通知消息、支付消息等。除了正常的消息消費以外,還有一些特殊的場景,如順序消費、事務消費、重復消費等,主要基于 RocketMQ 來實現。
順序消費
主要應用于對消息有先后依賴的場景,比如創單消息必須先于占座消息被處理。RocketMQ 本身支持消息順序消費,我們基于它來實現這種業務場景。從原理上來說也很簡單,RocketMQ 限定生產者只能將消息發往一個隊列,同時限定消費端只能有一個線程來讀取,這樣全局單隊列,單消費者就保證了消息的順序消費。
重復消費
RocketMQ 保障的是 At Least Once,并不能保證 Exactly Only Once,前面搶票我們也提過,一是通過要求業務側保持冪等性,另外通過數據庫表 message_produce_record 和 message_consume_record 來保證精準一次投遞和消費結果確認。
事務消費
基于 RocketMQ 的事務消息功能。它支持二階段提交,會先發送一條預處理消息,然后回調執行本地事務,最終提交或者回滾,幫助保證修改數據庫的信息和發送異步消息的一致。
灰度運行殲十戰機的總設計師曾說過一句話:「造一架飛機不是最難的,難的是讓它上天」,對我們來說同樣如此。3 月是春游季的高峰,業務量與日俱增,在此期間完成系統重大切換,我們需要完備的方案來保障業務的順利切換。
方案設計
灰度分為白名單部分和百分比灰度部分,我們首先在內部進行白名單灰度,穩定后進入 20% 流量灰度期。
灰度的核心是入口問題,由于本次前端也進行了完整改版,因此我們從站站搜索入口將用戶引入到不同的頁面,用戶分別會在新舊系統中完成業務。
圖14-灰度運行方案
搜索下單流程App 在站站搜索入口調用灰度接口獲取跳轉地址,實現入口分流。
圖15-搜索下單分流
效果對比 近期規劃我們只是初步實現了服務化在火車票業務線的落地實施,與此同時,還有一些事情是未來我們要去持續推進和改進的:
1. 服務粒度細化:目前的服務粒度仍然比較粗糙。隨著功能的不斷增多,粒度的細化是我們要去改進的重點,比如將交易服務拆分為訂單查詢服務,創單服務,處理占座的服務和處理出票的服務。這也是 DevOps 的必然趨勢。細粒度的服務,才能最大限度滿足我們快速開發、快速部署,以及風險可控的要求。
2. 服務資源隔離:只在服務粒度實現隔離是不夠的。DB 隔離、緩存隔離、MQ 隔離也非常必要。隨著系統的不斷擴展與數據量的增長,對資源進行細粒度的隔離是另一大重點。
3. 灰度多版本發布:目前我們的灰度策略只能支持新老版本的并行,未來除了會進行多版本并行驗證,還要結合業務定制化需求,使灰度策略更加靈活。
寫在最后業務的發展離不開技術的發展,同樣,技術的發展也要充分考慮當時場景下的業務現狀和條件,二者相輔相成。比起設計不足而言,我們更要規避過度設計。
技術架構是演變出來的,不是一開始設計出來的。我們需要根據業務發展規律,將長期技術方案進行階段性分解,逐步達成目標。同時,也要考慮服務化會帶來很多新問題,如復雜度驟增、業務拆分、一致性、服務粒度、鏈路過長、冪等性、性能等等。
比服務支撐更難的是服務治理,這也是我們大家要深入思考和去做的事情。
本文作者:李戰平,馬蜂窩大交通業務研發技術專家。
(題圖來源:網絡)
關注馬蜂窩技術,找到更多你想要的內容
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/40597.html
摘要:為了幫助用戶更好地完成消費決策閉環,馬蜂窩上線了大交通業務。現在,用戶在馬蜂窩也可以完成購買機票火車票等操作。第二階段架構轉變及服務化初探從年開始,整個大交通業務開始從架構向服務化演變。 交通方式是用戶旅行前要考慮的核心要素之一。為了幫助用戶更好地完成消費決策閉環,馬蜂窩上線了大交通業務。現在,用戶在馬蜂窩也可以完成購買機票、火車票等操作。 與大多數業務系統相同,我們一樣經歷著從無到有...
摘要:大交通研發質量體系建設為了幫助用戶更好地完成消費決策閉環,馬蜂窩上線了大交通業務,為用戶提供購買機票火車票等服務。 質量是決定產品能否成功、企業能否持續發展的關鍵因素之一。如何做好質量體系建設,這是個比較大的話題,包含的范圍很廣,也沒有固定的衡量標準。 打開一個互聯網公司招聘網站,搜索「測試工程師」崗位時,你會發現幾乎全部 JD 都包含一條要求「建設或者參與建設所負責業務的質量體系」。...
閱讀 1309·2021-11-16 11:45
閱讀 2232·2021-11-02 14:40
閱讀 3871·2021-09-24 10:25
閱讀 3028·2019-08-30 12:45
閱讀 1253·2019-08-29 18:39
閱讀 2468·2019-08-29 12:32
閱讀 1587·2019-08-26 10:45
閱讀 1916·2019-08-23 17:01