網(wǎng)易計費系統(tǒng)架構(gòu)升級之路
項目背景
網(wǎng)易蜂巢計費系統(tǒng)為網(wǎng)易云計算基礎(chǔ)服務(wù)提供整體的計費服務(wù),業(yè)務(wù)范圍涵蓋完整的產(chǎn)品售賣流程,包含定價、訂單、支付、計費、結(jié)算、優(yōu)惠、賬單等主體功能,支持十幾種不同產(chǎn)品的售賣,產(chǎn)品形態(tài)上貫穿了IaaS、PaaS和SaaS類別。同時,計費方式還提供了了按量、包年包月、資源包等多種方式。該項目的業(yè)務(wù)范圍之廣,玩法種類之多,數(shù)據(jù)要求之嚴注定了它將成為一個燙手的山芋,而且還是一個吃力不討好的工作。
該項目在人員上已經(jīng)幾經(jīng)易手,就我所知,已經(jīng)換過兩撥完整的開發(fā)和測試團隊了,而且已經(jīng)全部離職。不得不說,該項目已經(jīng)變得令人談之色變,讓人敬而遠之。在這樣的背景下,后期接手的開發(fā)和QA不得不硬著頭皮上,踩著雷過河,小心翼翼的應(yīng)對著不斷涌來的業(yè)務(wù)需求。隨之而來的是高居不下的bug率,越來越難以維護的代碼,無法擴展的架構(gòu)問題,我們開始意識到這樣下去是不行的。于是我們從8月份開始了漫漫的架構(gòu)升級之路。
重新出發(fā)
在我們開始優(yōu)化架構(gòu)之前,我們重新梳理了計費系統(tǒng)完整的業(yè)務(wù),得到了如下圖所示的業(yè)務(wù)領(lǐng)域:
梳理以后發(fā)現(xiàn),計費系統(tǒng)承載了太多非計費的業(yè)務(wù),包含訂單、賬單、結(jié)算和代金券等,這些業(yè)務(wù)代碼散落在各處,沒有嚴格地業(yè)務(wù)邊界劃分,而是“奇跡般”的融合在了一個工程里面。造成這個局面的原因在于計費系統(tǒng)初版設(shè)計時,根本沒有考慮到這些問題,當(dāng)然也不可能考慮到,而在后面逐步地迭代過程中,也未能去及時地調(diào)整架構(gòu),架構(gòu)腐化不是一天內(nèi)完成的。當(dāng)然,這方面有部分技術(shù)的原因,也有部分人為的原因所在,因為當(dāng)時負責(zé)計費系統(tǒng)的開發(fā)就只有一人,還是剛畢業(yè)的同學(xué)。目前看來,也是難為這位同學(xué)了。
技術(shù)債務(wù)的問題不是小事,千里之堤毀于蟻穴。既然我們找到了問題的癥結(jié)所在,那么解決的方式也就顯而易見了,一個字:拆!我們分析了所有的業(yè)務(wù),訂單是最大也是最復(fù)雜的一個業(yè)務(wù),而結(jié)算和賬單考慮到后期有可能遷移到云支付團隊,我們決定優(yōu)先把訂單系統(tǒng)拆分出去!
拆分的陣痛
訂單拆分說起來容易,做起來難。套用一句業(yè)界常說的話,就是開著飛機換輪胎。因為在我們拆分的同時,不斷地有新的業(yè)務(wù)需求進來,還有一些bug需要處理,所以不太可能讓我們專門進行拆分的工作。因此,為了不影響正常的業(yè)務(wù)迭代,我們決定拉出獨立分支進行開發(fā)。我們分出兩人專門處理拆分的工作。
為了最小化風(fēng)險,訂單拆分我們分了兩步進行:一,模塊獨立;二:系統(tǒng)獨立。
模塊獨立
模塊獨立是將訂單的代碼首先在工程內(nèi)部獨立出來,我們采用獨立Module的形式,將訂單獨立成了一個Order的模塊。它擁有完全獨立的服務(wù)層、業(yè)務(wù)層以及持久化層。其他模塊可以依賴Order,而Order不能依賴除公共模塊外的其他業(yè)務(wù)模塊。整體的模塊劃分如下圖所示。模塊的拆分過程中我們也發(fā)現(xiàn)了原先很多不合理的地方,例如:其他服務(wù)直接操作訂單的持久化層(DAO)、模塊直接依賴關(guān)系混亂、Service所在的Pacakge不合理、存在大量無用的代碼和邏輯、隨意的命名等。我們邊拆分邊重構(gòu),雖然進度比預(yù)期要緩慢一些,但整體上在向著合理的方向進行。

模塊獨立的過程中我們遇到了業(yè)務(wù)層級關(guān)系的問題。由于訂單模塊不再依賴于其他業(yè)務(wù)模塊,而又有一些業(yè)務(wù)邏輯是由訂單觸發(fā)的,需要在計費模塊完成,我們又不能直接調(diào)用計費模塊的Service。針對這個問題,我們采用了領(lǐng)域事件的方式來解耦,簡單來說就是訂單通過發(fā)布事件的方式來與其他模塊進行通信,當(dāng)時實現(xiàn)的代碼其實也相當(dāng)簡單。
我們并沒有獨立拆分web層,因為系統(tǒng)還沒有獨立,web層作為統(tǒng)一的打包入口也承載著訂單的流量。而且,Controller層的邏輯相對比較簡單,完全可以在系統(tǒng)獨立時再做。通過大家的努力,8月底訂單已獨立模塊的方式上線了,一切正常。
系統(tǒng)獨立
模塊拆分完成后,緊接著就是系統(tǒng)獨立,此時我們需要將訂單系統(tǒng)獨立部署。這里一個關(guān)鍵的問題是,獨立部署意味著單獨提供服務(wù),而依賴訂單系統(tǒng)的業(yè)務(wù)方非常之多,包含前端、主站、大部分的PaaS業(yè)務(wù)和計費,都有需要直接依賴訂單接口的地方,貿(mào)然獨立風(fēng)險很大。針對這個問題,我們采用使用haproxy七層轉(zhuǎn)發(fā)代理來將流量分發(fā)到不同的vip來解決。雖然,在上線過程中遇到了一些坎坷,但最終還是成功了?,F(xiàn)在看來這個選擇是非常對的,因為這樣可以在業(yè)務(wù)方無感知的情況下平滑升級。但長遠來看,最終我們還是以獨立的vip對外保留服務(wù)。

訂單和計費直接我們采用RabbitMQ來完成主體通信,關(guān)于采用MQ還是HTTP調(diào)用我們內(nèi)部還進行了一番爭論。之所以最終還是采用MQ來進行通信,是因為我們發(fā)現(xiàn)很多業(yè)務(wù)流程并不需要計費系統(tǒng)立即響應(yīng)(大部分流程都是訂單觸發(fā)的),也就是我們常說的弱依賴。另外,職責(zé)上計費系統(tǒng)的響應(yīng)的質(zhì)量也不應(yīng)影響到訂單的主體流程,舉個例子:用戶支付了一個云主機的訂單,如果計費系統(tǒng)此時無法響應(yīng),業(yè)務(wù)上相對來說可以接受過一小會兒計費再處理,而不是把訂單直接退款給用戶。MQ的引入在技術(shù)和職責(zé)層面都將訂單和計費分的更開了。當(dāng)然,強依賴的服務(wù)是我們無法避免的,其中之一就是結(jié)算模塊還留在計費中,訂單需要通過接口調(diào)用結(jié)算服務(wù)來完成支付。
前期,我們在模塊獨立時采用事件解耦的方式,在此時也獲得了收獲。我們通過一個統(tǒng)一的轉(zhuǎn)化層,將那些事件直接轉(zhuǎn)化層RabbitMQ可以識別的消息,這樣代碼的改造工作就大大減少了。
系統(tǒng)獨立后一個直接的表象就是每個系統(tǒng)的代碼行數(shù)大大降低了。獨立前,整體的代碼行數(shù)已經(jīng)達到了12W行以上(包含配置文件),獨立后,計費系統(tǒng)降低到了10W以下,訂單維持在4W以下。代碼行數(shù)的降低將直接提高系統(tǒng)的可維護性。個人認為如果一個工程里的代碼超過10W行,那么維護性將大大降低,除非是那些有著嚴格自律意識的團隊,否則,我建議還是盡量降低代碼行數(shù)。
經(jīng)過大家一個月的努力,訂單系統(tǒng)終于已獨立的姿態(tài)提供服務(wù)了。過程很艱辛,但是收獲良多。
拆分的收獲
訂單獨立后,一個直接的好處就是我們能獨立的思考問題了,這在以前是很難做到的一件事情,因為大家不得不小心翼翼的處理那些依賴,做事會畏手畏腳的。另外一個好處就是,我們的工作可以有側(cè)重點的進行了。訂單業(yè)務(wù)可以說是產(chǎn)品最為關(guān)注的業(yè)務(wù),也是計費對外暴露的主要入口。下圖就是我們在拆分后規(guī)劃訂單的業(yè)務(wù)架構(gòu),大家對后期的訂單規(guī)劃充滿期待。
多Region的挑戰(zhàn)
公有云產(chǎn)商面臨的一大挑戰(zhàn)就是多Region環(huán)境的支持。普通的互聯(lián)網(wǎng)行業(yè)出于高可用的考慮,往往會把核心系統(tǒng)部署到多個機房,然后根據(jù)自己的實際應(yīng)用場景選擇冷備、雙活甚至三活。我們經(jīng)常聽到的“兩地三中心”、“三地五中心”等等高大上的名詞就是多機房高可用的縮影。這些行業(yè)做多機房部署的主要目的是為了提高系統(tǒng)的可用性,不是其業(yè)務(wù)的必須屬性。換句話說,他們不做多機房部署也可以,做了當(dāng)然更好。而公有云產(chǎn)商不一樣,多Region部署就是其行業(yè)屬性之一。如果哪個云產(chǎn)商不提供多region產(chǎn)品的支持,那么它肯定是不完整的。不得不承認,我們在這方面的經(jīng)驗是比較欠缺的,在多Region的支持上走了一些彎路。
摸著石頭過河
今年上半年的時候,蜂巢開始計劃啟動北京Region,預(yù)計年中交付,當(dāng)時對我們橫向業(yè)務(wù)提出了很大的技術(shù)挑戰(zhàn)。一是在于橫向系統(tǒng)設(shè)計之初并沒有考慮到對Region環(huán)境的支持,我們很被動;二是我們并沒有跨Region系統(tǒng)設(shè)計的經(jīng)驗,我們很著急。計費系統(tǒng)面臨的問題更加嚴重,因為它對數(shù)據(jù)的一致性要求更高,而且出錯的影響范圍也更大。而且當(dāng)時計費的技術(shù)債務(wù)已經(jīng)很高了,產(chǎn)品的需求列表也拍了很長,套用一句很形象的話說,“留給我們的時間不多了”。
在這種情況下,我們“膽戰(zhàn)心驚”的給出了第一版的多Region設(shè)計方案,主體架構(gòu)如下所示:

因為當(dāng)時計費系統(tǒng)還沒有拆分,所有的業(yè)務(wù)都在一個系統(tǒng)中完成的,就是我們常說的“大泥球”系統(tǒng)。這種情況下我們很難做到多Region部署,訂單和賬單其實只有在一個Region部署就可以了,而計費的數(shù)據(jù)采集和請求分發(fā)是要下沉到各個Region的,而計算過程可以集中完成。采用"雙主"同步復(fù)制的方案實則是無奈之舉。數(shù)據(jù)庫的同步只能基于實例級別,而無法細分到表,我們各Region中計費數(shù)據(jù)庫中存在資源的計量表,這個數(shù)據(jù)需要同步到杭州Region來完成。為了避免“腦裂”的問題,我們特別將該表的主鍵采用UUID的形式。存量表因為無法做大規(guī)模修改,我們通過限制北京MySQL用戶的權(quán)限來避免寫入和修改全局表。
這個設(shè)計很糟糕,但是當(dāng)時的條件限制,我們也拿不出更好的設(shè)計了。雖然上線的過程有些曲折,這個架構(gòu)還是成功運行了,這是令我們最為欣慰的事情。因為為了適配這個架構(gòu),團隊的小伙伴做了很多工作。不可否認,這個架構(gòu)存在諸多弊端,其中最大的隱患就在于數(shù)據(jù)庫的“雙主”同步,這就像一顆隨時會爆的炸彈縈繞在我們心頭。當(dāng)時專線還沒有搭建好,所有的流量均通過外網(wǎng)隧道代理,糟糕的網(wǎng)絡(luò)質(zhì)量無疑放大了這個風(fēng)險。為此,DBA們向我們吐槽了好久,幸好我們抗打擊能力很強。
涅槃重生
在做完雙Region的支持以后,計費團隊就繼續(xù)做產(chǎn)品需求了,因為架構(gòu)調(diào)整導(dǎo)致需求列表已經(jīng)很長了。而且當(dāng)時也說的是,短期內(nèi)(至少今年)不會再有第三個Region了,我們也想著快點做完,多花點精力投入到重構(gòu)中。但是計劃趕不上變化,9月底我們被通知到第三個Region來了,而且已經(jīng)被提高到第一優(yōu)先級支持了。
有了第一版雙Region的經(jīng)驗,這一次我們淡定了很多。當(dāng)然,我們不可能在沿用第一版的設(shè)計了,因為DBA就會跟我們拼命的?;剡^頭來梳理多Region支持面臨的問題時,我發(fā)現(xiàn)一開始我們就自己給自己挖了一個坑,然后往里面跳。橫向支撐系統(tǒng)顯然都需要對所有Region提供支持,但這并不代表其需要在各個Region內(nèi)部署(我還與團隊其他的小伙伴分享了這方面的想法,網(wǎng)上應(yīng)該還能找到這一次分享的ppt——《跨Region實踐初探》)。因為公有云產(chǎn)商經(jīng)常會提供多個Region的服務(wù),有得甚至達到幾十個Region,如果橫向支持系統(tǒng)每個Region都要全量部署的話,那么我們花在運維上的精力就可以拖垮我們,更不要說還有最為困難的數(shù)據(jù)的一致性問題。
其實多Region的支持的問題我們總結(jié)出主要表現(xiàn)在一下兩個方面,一是應(yīng)用層面的接口互通;二是底層數(shù)據(jù)庫的同步。

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

Bill-Agent負責(zé)Region內(nèi)日志的收集和簡單聚合。
Bill-Central負責(zé)日志收集外的全局事務(wù)處理。
通過這樣的拆分,架構(gòu)就清晰多了。再多加Region,我們只需要部署B(yǎng)ill-Agent就可以了。Bill-Agent將處理過的計費數(shù)據(jù)寫入本地庫的一張資源表,利用NDC(馬進在網(wǎng)上分享過關(guān)于這個中間件的介紹)將資源表單向同步到Bill-Central的中央庫,然后Bill-Central統(tǒng)一在對計費數(shù)據(jù)進行處理。有意思的是,這張資源表就是我們在第一版設(shè)計中新建的資源表,因為我們將主鍵修改為UUID,所有使用NDC同步表的方案是相當(dāng)順利的。當(dāng)然,NDC在我們其他項目的跨Region支持上也發(fā)揮了重要作用,比如:跨機房緩存更新的問題。這一版的數(shù)據(jù)庫方案在技術(shù)評審時大家都比較滿意,DBA也肯定了我們的方案。
現(xiàn)在再來看跨Region調(diào)用的問題。在多Region的橫向系統(tǒng)中,我們發(fā)現(xiàn)或多或少的存在著機房間的接口調(diào)用問題。這些問題有可能是某些Region的庫不能寫需要路由到主庫來寫導(dǎo)致的,也有可能是全局緩存的問題,還有就是Global業(yè)務(wù)向Region內(nèi)服務(wù)發(fā)送指令。計費屬于最后一種場景,我們有一些業(yè)務(wù)場景需要由杭州Region觸發(fā),然后調(diào)用各個Region內(nèi)的服務(wù)的接口。在第一版的實現(xiàn)中,計費系統(tǒng)自己實現(xiàn)了跨Region代理部分,但是實現(xiàn)的不是很好,代碼的可維護性比較差,加重了調(diào)試的難度。這一版的設(shè)計中,我們決定把跨Region接口代理單獨拿出來重新做,結(jié)合多Region的應(yīng)用場景,然后封裝一些非功能性的特性,這就成了后面我們很重要的一個組件——RegionProxy。
RegionProxy最開始是為了解決跨Region調(diào)用的非功能性問題,簡化應(yīng)用系統(tǒng)處理的成本。但是設(shè)計上經(jīng)歷了比較大的調(diào)整。最開始的設(shè)計我們是希望Region內(nèi)所有跨Region的HTTP調(diào)用都能通過RegionProxy來代理,RegionProxy之間能夠發(fā)現(xiàn)對方并且相互通信,那么Region內(nèi)的應(yīng)用系統(tǒng)就只需要與本Region的RegionProxy通信就可以調(diào)到任意一個Region的應(yīng)用系統(tǒng)了。但是在方案評審的過程中,我們發(fā)現(xiàn)如果都用RegionProxy代理,可能會導(dǎo)致跨Region調(diào)用多出一跳或者兩跳,調(diào)試可能會比較困難。后來,我們放棄了這個方案。再后來,我們發(fā)現(xiàn)ServiceMesh的方案和我們最初RegionProxy的方案是十分相似的。
在RegionProxy的設(shè)計上我們進行了簡化處理,我們將所有Region的業(yè)務(wù)系統(tǒng)錄入到一個全局的配置中心(我們自己開發(fā)的ConfigCenter)中,然后通過一個自己開發(fā)的一個HttpProxy的Java庫來與ConfigCenter通信來完成跨Region的調(diào)用。這樣做的好處就是使用方用起來比較輕量,但是在網(wǎng)絡(luò)連通性方面我們需要與所有Region的系統(tǒng)做到互通。在開發(fā)Proxy庫的時候,我們不僅對跨Region的HTTP調(diào)用進行了封裝,而且對普通的HTTP調(diào)用也加入了非功能性的封裝,這樣系統(tǒng)可以通過Proxy庫完成所有的HTTP調(diào)用請求,極大的簡化了代碼的維護成本。后面,我們使用RegionProxy來代理請求后,確實刪除了很多以前的無用代碼,整體流程上也清晰了許多。
多Region的感悟
經(jīng)過兩版多Region的改造,我們確實收貨了很多寶貴的經(jīng)驗,非常難得。實際上,在多Region的支持上,大家需要清晰地認識到為什么要支持多Region,以何種方式去支持多Region,多Region支持與高可用的關(guān)系等基本問題。如果這些問題回到不好,或者不清楚,那么很容易就會掉到陷阱中去。另外一個感悟就是結(jié)合業(yè)務(wù)的實際場景,第二版的多Region架構(gòu)我們之所以能夠這么設(shè)計,就在于計費系統(tǒng)不需要實時出賬,我們完全可以把數(shù)據(jù)保存下來,離線計算以后再出賬,這是可以接受的。但這并不適用于所有情況,有些性能要求很高的橫向業(yè)務(wù)就不適合這種場景。
拿來主義
前面提過幾次技術(shù)債務(wù)的問題,有些問題是可以通過工具來解決了,有些只能通過內(nèi)部重構(gòu)來解決。左耳朵耗子曾經(jīng)說過一句話對我感觸很大,大意是說有些公司在解決問題時偏流程,有些公司偏技術(shù)。我想我們既然是技術(shù)團隊,在解決問題時能通過技術(shù)方式解決的就應(yīng)該盡量用技術(shù)解決,流程和人都是不可靠的。
難以管理的配置文件
計費項目面臨的諸多問題之一就有配置文件的管理,因為業(yè)務(wù)流程的原因,計費系統(tǒng)有著大量的各種各樣的配置。以前我們把配置文件放到工程里面,通過自動化部署平臺來指定使用不同的配置文件。這樣做的一個顯著問題就是代碼和配置耦合起來了,每次修改什么配置都得提交代碼,而我們提交又有著一套嚴格的流程,導(dǎo)致整體效率不高。另外一個問題就是可視化的問題。往往QA在線下環(huán)境測試都是通過的,而上線以后出了問題,基本上都是配置導(dǎo)致的問題。針對這幾個的問題,我們決定使用Apollo來管理我們的配置,

替換定時任務(wù)框架
計費系統(tǒng)嚴重依賴于定時任務(wù),有許多流程需要通過定時任務(wù)來推動。以前我們使用QUARTZ+MYSQL來作為我們分布式定時任務(wù)框架,但是這種做法的可維護性太差,而且對數(shù)據(jù)庫侵入很高,對測試也不友好。在QA的不斷吐槽中,我們決定替換掉現(xiàn)有的定時任務(wù)框架。在調(diào)研開源的定時任務(wù)框架后我們決定使用Elastic-Job來作為我們的分布式定時任務(wù)框架。目前,我們的兩個項目的所有定時任務(wù)(除bill-agent外)都已遷移到Elastic-Job上來了。
抽象化設(shè)計
如果你要問我做蜂巢計費最困難的地方是什么?我的回答肯定是業(yè)務(wù)太復(fù)雜了。這種復(fù)雜性不是因為我們架構(gòu)設(shè)計的不好導(dǎo)致的復(fù)雜,而是業(yè)務(wù)本身就是十分復(fù)雜的。現(xiàn)在計費系統(tǒng)需要支持十幾種產(chǎn)品的售賣形式,涵蓋IaaS、PaaS和SaaS的絕大部分產(chǎn)品,同時各個產(chǎn)品的售賣和計費模式都存在或多或少的差異,這讓我們很難通過一個統(tǒng)一的模型就涵蓋所有的場景。我們找到了一條緩解這個問題的方式——抽象化。
橫向系統(tǒng)或者支持系統(tǒng)如果需要服務(wù)多個產(chǎn)品,那么抽象化設(shè)計是不可或缺的緩解。如果越早進行抽象化,那么后期對接和維護的成本也就會越低,還能把系統(tǒng)的邊界劃分得更清晰。計費系統(tǒng)早期的設(shè)計在抽象化方面沒有過多的規(guī)劃,在后期的對接方面又處于比較弱勢的一方,導(dǎo)致計費系統(tǒng)出現(xiàn)了大量的特化代碼。這些特化代碼對一個服務(wù)十幾個產(chǎn)品的支持系統(tǒng)無疑是傷害巨大的?,F(xiàn)在我們已經(jīng)意識到了問題的嚴重性,也著手在做這方面的重構(gòu)工作了。但是挑戰(zhàn)依然很大,因為業(yè)務(wù)的復(fù)雜性是無法通過技術(shù)手段就能降低的,這方面我們只有和產(chǎn)品、運營和銷售各方面一起努力,打造一個合理、靈活、穩(wěn)定的新計費。
推薦閱讀:
不是你需要中臺,而是一名合格的架構(gòu)師(附各大廠中臺建設(shè)PPT)
企業(yè)IT技術(shù)架構(gòu)規(guī)劃方案
論數(shù)字化轉(zhuǎn)型——轉(zhuǎn)什么,如何轉(zhuǎn)?
企業(yè)10大管理流程圖,數(shù)字化轉(zhuǎn)型從業(yè)者必備!
