本文來自網易雲社區。

項目背景

蜂巢計費系統為網易雲計算基礎服務(網易蜂巢)提供整體的計費服務,業務範圍涵蓋完整的產品售賣流程,包含定價、訂單、支付、計費、結算、優惠、賬單等主體功能,支持十幾種不同產品的售賣,產品形態上貫穿了IaaS、PaaS和SaaS類別。同時,計費方式還提供了了按量、包年包月、資源包等多種方式。該項目的業務範圍之廣,玩法種類之多,數據要求之嚴註定了它將成為一個燙手的山芋,而且還是一個吃力不討好的工作。

該項目在人員上已經幾經易手,就我所知,已經換過兩撥完整的開發和測試團隊了,而且已經全部離職。不得不說,該項目已經變得令人談之色變,讓人敬而遠之。在這樣的背景下,後期接手的開發和QA不得不硬著頭皮上,踩著雷過河,小心翼翼的應對著不斷湧來的業務需求。隨之而來的是高居不下的bug率,越來越難以維護的代碼,無法擴展的架構問題,我們開始意識到這樣下去是不行的。於是我們從8月份開始了漫漫的架構升級之路。

重新出發

在我們開始優化架構之前,我們重新梳理了計費系統完整的業務,得到了如下圖所示的業務領域:

梳理以後發現,計費系統承載了太多非計費的業務,包含訂單、賬單、結算和代金券等,這些業務代碼散落在各處,沒有嚴格地業務邊界劃分,而是「奇蹟般」的融合在了一個工程裡面。造成這個局面的原因在於計費系統初版設計時,根本沒有考慮到這些問題,當然也不可能考慮到,而在後面逐步地迭代過程中,也未能去及時地調整架構,架構腐化不是一天內完成的。當然,這方面有部分技術的原因,也有部分人為的原因所在,因為當時負責計費系統的開發就只有一人,還是剛畢業的同學。目前看來,也是難為這位同學了。

技術債務的問題不是小事,千里之堤毀於蟻穴。既然我們找到了問題的癥結所在,那麼解決的方式也就顯而易見了,一個字:拆!我們分析了所有的業務,訂單是最大也是最複雜的一個業務,而結算和賬單考慮到後期有可能遷移到雲支付團隊,我們決定優先把訂單系統拆分出去!

拆分的陣痛

訂單拆分說起來容易,做起來難。套用一句業界常說的話,就是開著飛機換輪胎。因為在我們拆分的同時,不斷地有新的業務需求進來,還有一些bug需要處理,所以不太可能讓我們專門進行拆分的工作。因此,為了不影響正常的業務迭代,我們決定拉出獨立分支進行開發。我們分出兩人專門處理拆分的工作。

為了最小化風險,訂單拆分我們分了兩步進行:一,模塊獨立;二:系統獨立。

模塊獨立

模塊獨立是將訂單的代碼首先在工程內部獨立出來,我們採用獨立Module的形式,將訂單獨立成了一個Order的模塊。它擁有完全獨立的服務層、業務層以及持久化層。其他模塊可以依賴Order,而Order不能依賴除公共模塊外的其他業務模塊。整體的模塊劃分如下圖所示。模塊的拆分過程中我們也發現了原先很多不合理的地方,例如:其他服務直接操作訂單的持久化層(DAO)、模塊直接依賴關係混亂、Service所在的Pacakge不合理、存在大量無用的代碼和邏輯、隨意的命名等。我們邊拆分邊重構,雖然進度比預期要緩慢一些,但整體上在向著合理的方向進行。

模塊獨立的過程中我們遇到了業務層級關係的問題。由於訂單模塊不再依賴於其他業務模塊,而又有一些業務邏輯是由訂單觸發的,需要在計費模塊完成,我們又不能直接調用計費模塊的Service。針對這個問題,我們採用了領域事件的方式來解耦,簡單來說就是訂單通過發布事件的方式來與其他模塊進行通信,當時實現的代碼其實也相當簡單。

我們並沒有獨立拆分web層,因為系統還沒有獨立,web層作為統一的打包入口也承載著訂單的流量。而且,Controller層的邏輯相對比較簡單,完全可以在系統獨立時再做。通過大家的努力,8月底訂單已獨立模塊的方式上線了,一切正常。

系統獨立

模塊拆分完成後,僅接著就是系統獨立,此時我們需要將訂單系統獨立部署。這裡一個關鍵的問題是,獨立部署意味著單獨提供服務,而依賴訂單系統的業務方非常之多,包含前端、主站、大部分的PaaS業務和計費,都有需要直接依賴訂單介面的地方,冒然獨立風險很大。針對這個問題,我們採用使用haproxy七層轉發代理來將流量分發到不同的vip來解決。雖然,在上線過程中遇到了一些坎坷,但最終還是成功了。現在看來這個選擇是非常對的,因為這樣可以在業務方無感知的情況下平滑升級。但長遠來看,最終我們還是以獨立的vip對外保留服務。

訂單和計費直接我們採用RabbitMQ來完成主體通信,關於採用MQ還是HTTP調用我們內部還進行了一番爭論。之所以最終還是採用MQ來進行通信,是因為我們發現很多業務流程並不需要計費系統立即響應(大部分流程都是訂單觸發的),也就是我們常說的弱依賴。另外,職責上計費系統的響應的質量也不應影響到訂單的主體流程,舉個例子:用戶支付了一個雲主機的訂單,如果計費系統此時無法響應,業務上相對來說可以接受過一小會兒計費再處理,而不是把訂單直接退款給用戶。MQ的引入在技術和職責層面都將訂單和計費分的更開了。當然,強依賴的服務是我們無法避免的,其中之一就是結算模塊還留在計費中,訂單需要通過介面調用結算服務來完成支付。

前期,我們在模塊獨立時採用事件解耦的方式,在此時也獲得了收穫。我們通過一個統一的轉化層,將那些事件直接轉化層RabbitMQ可以識別的消息,這樣代碼的改造工作就大大減少了。

系統獨立後一個直接的表象就是每個系統的代碼行數大大降低了。獨立前,整體的代碼行數已經達到了12W行以上(包含配置文件),獨立後,計費系統降低到了10W以下,訂單維持在4W以下。代碼行數的降低將直接提高系統的可維護性。個人認為如果一個工程里的代碼超過10W行,那麼維護性將大大降低,除非是那些有著嚴格自律意識的團隊,否則,我建議還是盡量降低代碼行數。

經過大家一個月的努力,訂單系統終於已獨立的姿態提供服務了。過程很艱辛,但是收穫良多。

拆分的收穫

訂單獨立後,一個直接的好處就是我們能獨立的思考問題了,這在以前是很難做到的一件事情,因為大家不得不小心翼翼的處理那些依賴,做事會畏手畏腳的。另外一個好處就是,我們的工作可以有側重點的進行了。訂單業務可以說是產品最為關注的業務,也是計費對外暴露的主要入口。 下圖就是我們在拆分後規劃訂單的業務架構,大家對後期的訂單規劃充滿期待。

多Region的挑戰

公有雲產商面臨的一大挑戰就是多Region環境的支持。普通的互聯網行業出於高可用的考慮,往往會把核心系統部署到多個機房,然後根據自己的實際應用場景選擇冷備、雙活甚至三活。我們經常聽到的「兩地三中心」、「三地五中心」等等高大上的名詞就是代多機房高可用的縮影。這些行業做多機房部署的主要目的是為了提高系統的可用性,不是其業務的必須屬性。換句話說,他們不做多機房部署也可以,做了當然更好。而公有雲產商不一樣,多Region部署就是其行業屬性之一。如果哪個雲產商不提供多region產品的支持,那麼它肯定是不完整的。不得不承認,我們在這方面的經驗是比較欠缺的,在多Region的支持上走了一些彎路。

摸著石頭過河

今年上半年的時候,蜂巢開始計劃啟動北京Region,預計年中交付,當時對我們橫向業務提出了很大地技術挑戰。一是在於橫向系統設計之初並沒有考慮到對Region環境的支持,我們很被動;二是我們並沒有跨Region系統設計的經驗,我們很著急。計費系統面臨的問題更加嚴重,因為它對數據的一致性要求更高,而且出錯地影響範圍也更大。而且當時計費的技術債務已經很高了,產品的需求列表也拍了很長,套用一句很形象的話說,「留給我們的時間不多了」。

在這種情況下,我們「膽戰心驚」的給出了第一版的多Region設計方案,主體架構如下所示:

因為當時計費系統還沒有拆分,所有的業務都在一個系統中完成的,就是我們常說的「大泥球」系統。這種情況下我們很難做到多Region部署,訂單和賬單其實只有在一個Region部署就可以了,而計費的數據採集和請求分發是要下沉到各個Region的,而計算過程可以集中完成。採用"雙主"同步複製的方案實則是無奈之舉。資料庫的同步只能基於實例級別,而無法細分到表,我們各Region中計費資料庫中存在資源的計量表,這個數據需要同步到杭州Region來完成。為了避免「腦裂」的問題,我們特別將該表的主鍵採用UUID的形式。存量表因為無法做大規模修改,我們通過限制北京MySQL用戶的許可權來避免寫入和修改全局表。

這個設計很糟糕,但是當時的條件限制,我們也拿不出更好的設計了。雖然上線的過程有些曲折,當這個架構還是成功運行了,這是令我們最為欣慰的事情。因為為了適配這個架構,團隊的小夥伴做了很多工作。不可否認,這個架構存在諸多弊端,其中最大的隱患就在於資料庫的「雙主」同步,這就像一顆隨時會爆的炸彈縈繞在我們心頭。當時專線還沒有搭建好,所有的流量均通過外網隧道代理,糟糕的網路質量無疑放大了這個風險。為此,DBA們向我們吐槽了好久,幸好我們抗打擊能力很強。

涅槃重生

在做完雙Region的支持以後,計費團隊就繼續做產品需求了,因為架構調整導致需求列表已經很長了。而且當時也說的是,短期內(至少今年)不會再有第三個Region了,我們也想著快點做完,多花點精力投入到重構中。但是計劃趕不上變化,9月底我們被通知到第三個Region來了,而且已經被提高到第一優先順序支持了。

有了第一版雙Region的經驗,這一次我們淡定了很多。當然,我們不可能在沿用第一版的設計了,因為DBA就會跟我們拚命的。回過頭來梳理多Region支持面臨的問題時,我發現一開始我們就自己給自己挖了一個坑,然後往裡面跳。橫向支撐系統顯然都需要對所有Region提供支持,但這並不代表其需要在各個Region內部署(我還與團隊其他的小夥伴分享了這方面的想法,網上應該還能找到這一次分享的ppt——《跨Region實踐初探》)。因為公有雲產商經常會提供多個Region的服務,有得甚至達到幾十個Region,如果橫向支持系統每個Region都要全量部署的話,那麼我們花在運維上的精力就可以拖垮我們,更不要說還有最為困難的數據的一致性問題。

其實多Region的支持的問題我們總結出主要表現在一下兩個方面,一是應用層面的介面互通;二是底層資料庫的同步。

我們先說底層資料庫的同步,對計費系統而言,數據的一致性是至關重要的,但多機房部署是在挑戰CAP定律。是不是就沒有了這樣的資料庫方案了呢,有,那就是Google的Spanner,號稱可以在全球做到強一致的資料庫。但是我們沒有這樣的資料庫。其實我們也考慮使用NoSQL資料庫——Cassandra,但是這個資料庫運維起來太複雜,我們也沒有這方面的經驗,也就放棄了。還是回歸到MySQL,受限於傳統關係型資料庫在擴展性方面的問題,我們不可能把整個庫在各個Region都同步一份。但是計費原始數據又必須在各個Region內收集,於是我們決定——拆,把計費拆層兩個部分,分為bill-agent(數據採集)和bill-central(數據計算)兩個部分。

  • Bill-Agent負責Region內日誌的收集和簡單聚合。
  • Bill-Central負責日誌收集外的全局事務處理。

通過這樣的拆分,架構就清晰多了。再多加Region,我們只需要部署Bill-Agent就可以了。Bill-Agent將處理過的計費數據寫入本地庫的一張資源表,利用NDC(馬進在網上分享過關於這個中間件的介紹)將資源表單向同步到Bill-Central的中央庫,然後Bill-Central統一在對計費數據進行處理。有意思的是,這張資源表就是我們在第一版設計中新建的資源表,因為我們將主鍵修改為UUID,所有使用NDC同步表的方案是相當順利的。當然,NDC在我們其他項目的跨Region支持上也發揮了重要作用,比如:跨機房緩存更新的問題。這一版的資料庫方案在技術評審時大家都比較滿意,DBA也肯定了我們的方案。

現在再來看跨Region調用的問題。在多Region的橫向系統中,我們發現或多或少的存在著機房間的介面調用問題。這些問題有可能是某些Region的庫不能寫需要路由到主庫來寫導致的,也有可能是全局緩存的問題,還有就是Global業務向Region內服務發送指令。計費屬於最後一種場景,我們有一些業務場景需要由杭州Region觸發,然後調用各個Region內的服務的介面。在第一版的實現中,計費系統自己實現了跨Region代理部分,但是實現的不是很好,代碼的可維護性比較差,加重了調試的難度。這一版的設計中,我們決定把跨Region介面代理單獨拿出來重新做,結合多Region的應用場景,然後封裝一些非功能性的特性,這就成了後面我們很重要的一個組件——RegionProxy。

RegionProxy最開始是為了解決跨Region調用的非功能性問題,簡化應用系統處理的成本。但是設計上經歷了比較大的調整。最開始的設計我們是希望Region內所有跨Region的HTTP調用都能通過RegionProxy來代理,RegionProxy之間能夠發現對方並且相互通信,那麼Region內的應用系統就只需要與本Region的RegionProxy通信就可以調到任意一個Region的應用系統了。但是在方案評審的過程中,我們發現如果都用RegionProxy代理,可能會導致跨Region調用多出一跳或者兩跳,調試可能會比較困難。後來,我們放棄了這個方案。再後來,我們發現ServiceMesh的方案和我們最初RegionProxy的方案是十分相似的。

在RegionProxy的設計上我們進行了簡化處理,我們將所有Region的業務系統錄入到一個全局的配置中心(我們自己開發的ConfigCenter)中,然後通過一個自己開發的一個HttpProxy的Java庫來與ConfigCenter通信來完成跨Region的調用。這樣做的好處就是使用方用起來比較輕量,但是在網路連通性方面我們需要與所有Region的系統做到互通。在開發Proxy庫的時候,我們不僅對跨Region的HTTP調用進行了封裝,而且對普通的HTTP調用也加入了非功能性的封裝,這樣系統可以通過Proxy庫完成所有的HTTP調用請求,極大的簡化了代碼的維護成本。後面,我們使用RegionProxy來代理請求後,確實刪除了很多以前的無用代碼,整體流程上也清晰了許多。

多Region的感悟

經過兩版多Region的改造,我們確實收貨了很多寶貴的經驗,非常難得。實際上,在多Region的支持上,大家需要清晰地認識到為什麼要支持多Region,以何種方式去支持多Region,多Region支持與高可用的關係等基本問題。如果這些問題回到不好,或者不清楚,那麼很容易就會掉到陷阱中去。另外一個感悟就是結合業務的實際場景,第二版的多Region架構我們之所以能夠這麼設計,就在於計費系統不需要實時出賬,我們完全可以把數據保存下來,離線計算以後再出賬,這是可以接受的。但這並不適用與所用情況,有些性能要求很高的橫向業務就不適合這種場景。

拿來主義

前面提過幾次技術債務的問題,有些問題是可以通過工具來解決了,有些只能通過內部重構來解決。左耳朵耗子曾經說過一句話對我感觸很大,大意是說有些公司在解決問題時偏流程,有些公司偏技術。我想我們既然是技術團隊,在解決問題時能通過技術方式解決的就應該盡量用技術解決,流程和人都是不可靠的。

難以管理的配置文件

計費項目面臨的諸多問題之一就有配置文件的管理,因為業務流程的原因,計費系統有著大量的各種各樣的配置。以前我們把配置文件放到工程裡面,通過自動化部署平台來指定使用不同的配置文件。這樣做的一個顯著問題就是代碼和配置耦合起來了,每次修改什麼配置都得提交代碼,而我們提交又有著一套嚴格地流程,導致整體效率不高。另外一個問題就是可視化的問題。往往QA在線下環境測試都是通過的,而上線以後出了問題,基本上都是配置導致的問題。針對這幾個的問題,我們決定使用Apollo來管理我們的配置,通過整合Apollo,我們的兩個項目(訂單和計費)都做到了工程零配置,所有的配置都放到Apollo上進行管理,好處良多。

替換定時任務框架

計費系統嚴重依賴於定時任務,有許多流程需要通過定時任務來推動。以前我們使用QUARTZ+MYSQL來作為我們分散式定時任務框架,但是這種做法的可維護性太差,而且對資料庫侵入很高,對測試也不友好。在QA的不斷吐槽中,我們決定替換掉現有的定時任務框架。在調研開源的定時任務框架後我們決定使用Elastic-Job來作為我們的分散式定時任務框架。目前,我們的兩個項目的所有定時任務(除bill-agent外)都已遷移到Elastic-Job上來了。

抽象化設計

如果你要問我做蜂巢計費最困難的地方是什麼?我的回答肯定是業務太複雜了。這種複雜性不是因為我們架構設計的不好導致的複雜,而是業務本身就是十分複雜的。現在計費系統需要支持十幾種產品的售賣形式,涵蓋IaaS、PaaS和SaaS的絕大部分產品,同時各個產品的售賣和計費模式都存在或多或少的差異,這讓我們很難通過一個統一的模型就涵蓋所有的場景。我們找到了一條緩解這個問題的方式——抽象化。

橫向系統或者支持系統如果需要服務多個產品,那麼抽象化設計是不可或缺的一個緩解。如果越早進行抽象化,那麼後期對接和維護的成本也就會越低,還能把系統的邊界劃分得更清晰。計費系統早期的設計在抽象化方面沒有過多的規劃,在後期的對接方面又處於比較弱勢的一方,導致計費系統出現了大量的特化代碼。這些特化代碼對一個服務十幾個產品的支持系統無疑是傷害巨大的。現在我們已經意識到了問題的嚴重性,也著手在做這方面的重構工作了。但是挑戰依然很大,因為業務的複雜性是無法通過技術手段就能降低的,這方面我們只有和產品、運營和銷售各方面一起努力,打造一個合理、靈活、穩定的新計費。

抽象化設計因為我們還在進行中,後期有機會再分享。

總結

從八月底加入計費團隊以來,收穫良多,無論是在技術上,還是在對業務的理解上,都有了許多新的認識。最為給力的還是團隊的小夥伴們,因為計費本身的需求非常多,處理這些需求的人都只剛剛夠。後來我們又做了兩版跨Region改造、訂單拆分、框架替換、抽象化等優化工作,迭代周期從兩周一次壓縮到了一周一次,開發和QA的小夥伴也都是任勞任怨。當然,大家能在這個過程中有所收穫才是最關鍵的。

計費系統可以說是我接觸過的最為複雜的一個系統,越是複雜的系統越需要清晰的頭腦和良好的設計。雲計算產商的博弈已經到了白熱化階段了,大家拼的不光光是每個產品的質量和體驗,還有整個雲平台的內功。公有雲平台本身就是一個龐大、複雜的系統,如何把這個系統建設好,用戶體驗做好、服務質量提高、穩定性得到保障,這本身就是極為有難度的一件事情。計費系統作為公有雲平台一個重要的組成部分,可以說扮演著一個極為關鍵的角色。做得好可以對整個平台提供助力,而做的差則會拖慢整體的發展進程。我們已經找到了適合自己的一條道路,相信會走上正軌!

網易雲計算基礎服務深度整合了 IaaS、PaaS 及容器技術,提供彈性計算、DevOps 工具鏈及微服務基礎設施等服務,幫助企業解決 IT、架構及運維等問題,使企業更聚焦於業務,是新一代的雲計算平台。

本文來自網易雲社區,經作者蔣文康授權發布。

原文:蜂巢計費系統架構升級之路

了解 網易雲 :

網易雲官網:https://www.163yun.com

網易雲社區:sq.163yun.com/blog

新用戶大禮包:https://www.163yun.com/gift

更多網易研發、產品、運營經驗分享請訪問網易雲社區。


推薦閱讀:
相关文章