馬蜂窩火車票系統服務化改造初探

交通方式是用戶旅行前要考慮的核心要素之一。爲了幫助用戶更好地完成消費決策閉環,馬蜂窩上線了大交通業務。如今,用戶在馬蜂窩也能夠完成購買機票、火車票等操做。php

與大多數業務系統相同,咱們同樣經歷着從無到有,再到快速發展的過程。本文將以火車票業務系統爲例,主要從技術的角度,和你們分享在一個新興業務發展的不一樣階段背後,系統建設與架構演變方面的一些經驗。前端

第一階段:從無到有

在這個階段,快速支撐起業務,填補業務空白是第一目標。基於這樣的考慮,當時的火車票業務從模式上選擇的是供應商代購;從技術的角度須要優先實現用戶在馬蜂窩 App 查詢車次餘票信息、購票、支付,以及取消、退票退款等核心功能的開發。redis

圖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 應用上。程序內部主要有四個入口,分別是:api

  1. 供其餘二方系統調用的 facade 模塊
  2. 運營後臺調用的 admin 模塊
  3. 處理 App 和 H5 請求的 train 核心模塊
  4. 處理外部供應商回調模塊

四個入口會依賴於下層 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 做爲分佈式微服務框架,主要有如下因素考慮:

  1. 成熟的高性能分佈式框架。目前不少公司都在使用,已經經受住各方面性能考驗,比較穩定;
  2. 能夠和 Spring 框架無縫集成。咱們的架構正是基於 Spring 搭建,而且接入 Dubbo 時能夠作到代碼無侵入,接入也很是方便;
  3. 具有服務註冊、發現、路由、負載均衡、服務降級、權重調節等能力;
  4. 代碼開源。能夠根據需求進行個性化定製,擴展功能,進行自研發

圖 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 電子票同步訂單信息,若是不作冪等性控制,後果是很是嚴重的。

保證冪等性的方法有不少,以佔座消息爲例,咱們有兩個措施來保證冪等:

  1. 佔座消息都帶有協議約束的惟一 serialNo,推送服務能夠判斷該消息是否已被正常處理。
  2. 業務側的修改實現 CAS(Compare And Swap),簡單來講就是數據庫樂觀鎖,如 update order set order_status = 2 where order_id = 『1234' and order_status = 1 and pay_status = 2 。

小結

服務化的實施具有必定的成本,須要人員和基礎設施都有必定的基礎。初始階段,從相對獨立的新業務着手,作好和舊系統的融合複用,能較快的獲取成果。搶票系統在不到一個月的時間內完成產品設計,開發聯調,測試上線,也很好地印證了這一點。

第三階段:服務化推動和系統能力提高

搶票系統建設的完成,表明咱們邁出了一小步,也只是邁出了一小步,畢竟搶票是週期性的業務。更多的時間裏,電子票是咱們業務量的主要支撐。在新老系統的並行期,主要有如下痛點:

  1. 原有電子票系統因爲當時因素影響,與特定供應商綁定緊密,受供應商制約較大;
  2. 因爲和搶票系統及大交通其餘系統之間的兼容成本較高,致使咱們統一鏈路追蹤、環境隔離、監控告警等工做實施難度很大;
  3. PHP 和 Java 橋接層承接太多業務,性能沒法保證

所以,卸下歷史包袱,儘快完成舊系統的服務化遷移,統一技術棧,使主要業務獲得更加有力的系統支撐,是咱們接下來的目標。

與業務同行:電子票流程改造

咱們但願經過對電子票流程的改造,重塑以前應急模式下創建的火車票項目,最終實現如下幾個目標:

  1. 創建馬蜂窩火車票的業務規則,改變以前業務功能和流程上受制於供應側規則的局面;
  2. 完善用戶體驗和功能,增長在線選座功能,優化搜索下單流程,優化退款速度,提高用戶體驗;
  3. 提高數據指標和穩定性,引入新的供應側服務,提升可靠性;供應商分單體系,提高佔座成功率和出票率;
  4. 技術上完成到 Java 服務化的遷移,爲後續業務打下基礎

咱們要完成的不只是技術上的重構,而是結合新的業務訴求,去不斷豐富新的系統,力求達到業務和技術的目標一致性,所以咱們將服務化遷移和業務系統建設結合在一塊兒推動。下圖是電子票流程改造後火車票總體架構:

圖11-電子票改造後的火車票架構

圖中淺藍色部分爲搶票期間已經建好的功能,深藍色模塊爲電子票流程改造新加入的部分。除了和搶票系統相似的供應商接入、正向交易、逆向交易之外,還包括搜索與基礎數據系統,在供應側也增長了電子票的業務功能。同時咱們新的運營後臺也已經創建,保證了運營支撐的延續性。

項目實施過程當中,除了搶票所說的一些問題以外,也着重解決如下幾個問題。

搜索優化

先來看用戶一次站站搜索可能穿過的系統:

圖12-站站查詢調用流程

請求先到 twl api 層,再到 tsearch 查詢服務,tsearch 到 tjs 接入服務再到供應側,整個調用鏈路仍是比較長的,若是每次調用都是全鏈路調用,那麼結果是不太樂觀的。所以 tsearch 對於查詢結果有 redis 緩存,緩存也是縮短鏈路、提升性能的關鍵。站站查詢要緩存有幾個難點:

  1. 對於數據實時性要求很高。核心是餘票數量,若是數據不實時,那麼用戶再下單佔座成功率會很低
  2. 數據比較分散。好比出發站,到達站,出發日期,緩存命中率不高
  3. 供應側接口不穩定。平均在 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. 灰度多版本發佈:目前咱們的灰度策略只能支持新老版本的並行,將來除了會進行多版本並行驗證,還要結合業務定製化需求,使灰度策略更加靈活。

寫在最後

業務的發展離不開技術的發展,一樣,技術的發展也要充分考慮當時場景下的業務現狀和條件,兩者相輔相成。比起設計不足而言,咱們更要規避過分設計。

技術架構是演變出來的,不是一開始設計出來的。咱們須要根據業務發展規律,將長期技術方案進行階段性分解,逐步達成目標。同時,也要考慮服務化會帶來不少新問題,如複雜度驟增、業務拆分、一致性、服務粒度、鏈路過長、冪等性、性能等等。

比服務支撐更難的是服務治理,這也是咱們你們要深刻思考和去作的事情。

本文做者:李戰平,馬蜂窩大交通業務研發技術專家。

(題圖來源:網絡)

關注馬蜂窩技術,找到更多你想要的內容

相關文章
相關標籤/搜索