作者:凹凸曼 Manjiz Atom 是什麼?Atom 是集結業內各色資深電商行業設計師,提供一站式專業智能頁面和小程式設計服務的平臺。經過 2 年緊湊迭代,項目越來越龐大,需求不斷變更優化,內部邏輯錯綜複雜,維護成本急劇拉升。同時,Atom 將要承載的業務越來越多,要向更多的內部用戶和商家提供服務 ...
作者:凹凸曼 - Manjiz
Atom 是什麼?Atom 是集結業內各色資深電商行業設計師,提供一站式專業智能頁面和小程式設計服務的平臺。經過 2 年緊湊迭代,項目越來越龐大,需求不斷變更優化,內部邏輯錯綜複雜,維護成本急劇拉升。同時,Atom 將要承載的業務越來越多,要向更多的內部用戶和商家提供服務,為了適應這些變化,架構升級成為當時緊迫的事項,我們將解構服務端模塊,讓服務輕量化、模塊化,更便捷地拓展業務場景。
Atom 服務端經歷了三個版本的迭代,本文著重剖析第三個版本。
架構 1.0
這是 Atom 最古老的一個版本,在這一版本中,只規划了頻道頁的功能,目的是把開發人員從繁複的頻道頁開發中解放出來,因為功能目的純粹,所以系統複雜度較低,服務端直接使用了 Koa 框架上手開發,這是一個單體架構的服務,所有的代碼都在一個進程中運行。
在部署方面,運用的是非常原始的手工操作:開發人員登入機器,拉取代碼後進行類似本地環境的安裝啟動,然後在不同機器重覆這個過程。
另外,Quark 的舊版本使用的是具名組件,具名組件一定程度限制了 Quark 自身的擴展性,這裡不作展開。
架構 2.0
從頻道頁搭建平臺到多場景頁面搭建平臺,Atom 用了不到一年時間,更豐富的組件,更多的模板,更多的場景,更多參與進來的設計師,更多的用戶,產品開發逐漸專業化,簡單的手工運維已經不再適用,於是前端和服務端都進行了一次大換血,服務端用 Salak 重構,Salak 是個非常好上手的服務端框架,同時為我們帶來了介面文檔的自動化生成功能,前端和服務端都改為依靠 Talos(一容器式部署內部平臺)來部署。服務端逐漸邁入工業時代。
然而,這個階段仍然沒解決粗放的開發方式,缺乏巨集觀上的規劃,日益暴露了以下這些問題:
-
高度集中
90% 以上服務集中於一個單體架構中,業務越來越複雜,代碼量越來越大,代碼的可讀性、可維護性和可擴展性下降,開發人員接入成本劇增,業務擴展的代價成指數上升,持續交付能力難以維持。隨著用戶越來越多,程式承受的併發越來越高,單體架構的應用的併發能力有限。由於系統複雜度的提高,測試的難度也越來越大。
-
耦合度高
單體中的各個模塊間互相依賴,互相影響,互相掣肘,導致代碼重用性低,新功能開發往往由於忌憚耦合邏輯中的隱藏彩蛋,而選擇重新編寫,這不是我們希望看到的!
-
邏輯混亂
除了耦合導致的邏輯混亂,Atom 作為一個從零成長起來的平臺,本身就淤積了大量的歷史需求,有些是不再使用的,有些是幾乎不被使用的,這些代碼邏輯給開發人員一個極大的挑戰:在進行代碼維護的時候不敢輕易改動代碼。另外在迭代中需要向下相容,讓服務端有沉重的歷史包袱。
-
代碼冗餘
由於框架在前期沒有定義好規範標準,在開發過程比較嚴格遵守代碼校驗,代碼的邏輯、常量等等重覆定義,這也同時讓項目變得難以維護,比如修改一個常量需要在保證沒有遺漏的前提同時修改多處。
新架構目標
根據原有架構的優劣,我們設置了本次架構升級的目標:
- 服務模塊化
- 服務通用化
- 插拔式站點
- 插拔式場景
- 標準與規範
名詞解釋:
- 站點:即把服務端與平臺解耦,從原來的服務即平臺,到可以為互相隔離的多個平臺提供相同的服務。
- 場景:為應對不同業務類型而設定的概念,不同場景有不同的管理方式和流程等。
整體架構
整體架構分為 Web 應用層、介面層、服務層 和 數據層 4 部分,這樣拆分能做到入口統一,在部署上的單點部署讓發佈更加的便捷,獨立部署則降低對服務整體的影響:
- Web 應用層:包括 Atom 平臺及其他的平臺應用
- 介面層:提供網關服務,應用層的請求經由網關作許可權控制及請求轉發
- 服務層:
- 服務通信:非同步通信使用 MQ,RPC 通信使用 HTTP
- 業務模塊:核心代碼,拆解眾多小模塊應用
- 基礎服務:統一把控用戶與許可權
- 服務管理:提升服務的穩定性、健壯性、靈活性
- 數據層:核心數據存儲
其中網關作為整個服務端的流量入口,對所有流量進行處理,攔截非法請求,解析登錄態並傳遞到下游,校驗介面許可權以及超時響應等,統一把控,同時減輕下游的壓力。
實施
計劃/籌備/評估
在正式進入升級開發前,小組通過會議探討架構升級的必要性和可行性,促使我們進行升級的直接原因是平臺新增的站點需求和場景需求,如要在原有架構上實現這個需求,勢必會在原已混亂的邏輯上增添更多的耦合邏輯,而間接原因,亦即升級必要性,則是要讓系統模塊化、標準化、通用化,讓系統的邏輯更加清晰,提升整個系統的可維護性。
經過我們反覆的探討,對原系統按照功能進行分割,在功能的基礎上再按照通用性進行進一步拆分,附加新架構的支撐性工作,評估這些工作的工作量和預計用時,最後對任務進行分配下達。
實施
模塊化
為什麼要模塊化?隨著平臺越做越大,我們想要讓各個部分的功能更加獨立、明確、清晰,把各部分之間的影響降到最低,對各部分單獨運維,避免牽一發而動全身的情況。
這次升級按照功能和通用性把項目劃分為 10+ 個模塊:如專門負責編譯的模塊,專門負責模板管理的模塊,負責定時任務的模塊,作為入口的網關等等。
其中拆分出來若幹通用服務,通用服務作為獨立於 Atom 系統之外的服務,可以為 Atom 以及其他系統提供服務。
對項目進行模塊拆解,最為頭疼的是斬斷關聯邏輯,模塊的剝離和修複必然會導致一個問題——相同的代碼在不同的模塊重覆出現。為瞭解決這個問題,我們把部分這些代碼放到工具 npm 包中,這些代碼包括了:常量、TypeScript 類型定義、許可權映射、Mongoose Schema 定義、Salak 插件和工具方法等等。
另一個問題,在原架構中,模塊間可以通過代碼直接調用,那新架構中如何“還原”這個功能?為了保證解耦度,新架構中僅有少數需要即時調用的功能在模塊間通過介面進行直接調用,其他的都是通過 MQ 消息隊列和資料庫進行互通。
對於 MQ 通信,這裡舉個例子:編譯。服務端編譯通常需要的時間比較長,長時間占用連接對服務性能有所影響,而且編譯結果並不需要同步響應,對編譯模塊來說,如果來者不拒,對服務有不小的壓力,於是我們決定使用消息隊列來完成各個模塊之間的通信:
- 由項目模塊通過介面直接調用發佈模塊發起發佈操作;
- 發佈模塊向消息池推送一條“我要編譯”;
- 編譯模塊接收到消息後由自身情況判斷是否可以進入編譯,否則先不予以響應;
- 編譯的各個狀態也通過消息推送;
- 最後項目模塊在接收到編譯狀態的消息後作各種處理。
通用化
前面提到在模塊化的工作中,我們拆出了 4 個通用的服務模塊,通用服務獨立於 Atom 系統之外,可以為 Atom 以及其他系統提供服務。模塊的通用化是出於兩點考慮:
- 豐富部門的服務,減少重覆開發功能
- 排除 Atom 非核心代碼,讓系統瘦身
伴隨而來的一個問題值得我們思考,如何考量一個功能是否值得抽離通用化?我們應該儘量避免陷入一個誤區:系統模塊化就是把系統拆得越細越好。如果拆分過細,勢必增加運維工作量。在拆分模塊的時候,我們考量的是一個模塊內的功能是否完整且獨立,以及部門或公司對這個通用服務的需求度,真正地做到低耦合高內聚。
標準化
代碼層面,下麵做了個簡單的對比:
對比項 | 舊架構 | 新架構 |
---|---|---|
主要語言 | JavaScript | TypeScript |
代碼檢測 | 未遵守 | 必需 |
介面名稱 | 花樣百出 | 統一形式 |
介面輸出 | 百花齊放 | 統一形式 |
TypeScript 的好,前端人都知道,它為我們帶來了自動補全、可選的類型系統,使我們能夠用上更加新的 JavaScript 特性等等,更多可以參考《為什麼選擇 TypeScript》。出現後面三點的原因是什麼?舊架構經歷了從零到一的過程,項目在最初規劃欠缺以及中後期沒有足夠的時間對系統進行修正,時間和需求的變更的雙重作用導致代碼淤積。
為此,我們在新架構的開發中就強調代碼的標準化,對每次提交都要經過代碼檢測,然後是對五花八門的介面進行統一:
- 介面路徑統一:舊架構中,一個列表介面的路徑可能是
/xxx/list
,也可能是/xxx/xxxes
等等,我們在新架構中基於 RESTful API 規則,用資源名片語成的路徑和語義化的 HTTP 協議統一介面的定義; - 參數名統一:比如列表入參中每頁數量可能叫
pageSize
也可能叫count
,於是我們把它統一成一個名字,要求在開發中遵守這個約定; - 輸出統一:在數據輸出到前端前對數據進行處理篩選,剔除包括
_id
和__v
等無關數據,在輸出形式上也做了統一,要求輸出中所有的 _id 都替換以 id 的名字出現等等。
代碼標準化的好處是讓代碼更加好維護,開發人員很快就能定位到對應的介面代碼,對前端而言則減少對介面的識別記憶。
插拔式站點
前面提到,這次架構升級的直接原因是站點需求和場景需求。如果在舊架構下迭代站點需求,只會進一步增加耦合度。為此,我們增加了站點管理模塊,在幾乎所有的數據項中增加了站點欄位,給幾乎所有的資料庫查詢都帶上了站點參數。通過這些努力,現在新增站點只需要通過站點模塊新增站點,再做一些初始化配置即可完成。
站點概念除對 Atom 功能有了更高要求,也對原來的許可權體系形成了新的挑戰。在升級前的版本中用戶的許可權僅有一個集合,要實現每個站點擁有不同的許可權只能從兩個角度出發:
- 許可權含義拆分(為每個站點分別提供一套獨立的許可權)
- 用戶許可權增加一層抽象(用戶的許可權改變為多個集合根據站點進行切換)
在比較了兩種修改形式後,拆分許可權含義雖然在理解上比較容易代碼也改動不多。但卻大大提升了維護許可權表的難度,相當於新增場景就需要增加一套許可權,無法做到可插拔。最後在網關層增加了根據用戶訪問站點
切換許可權集合的邏輯。
插拔式場景
場景是站點下麵一個緯度,現有活動、頻道、心理學測試、SNS、店鋪幾大場景,如果在舊架構下新增一個場景,需要排期進行開發,而且代碼上恐怕也會增加不少針對不同場景的 if-else
。為了更便捷省心地擴展和維護場景,我們對場景相關的代碼從資源管理的角度做了拆解。
ATOM下每個場景擁有的資源主要有 模板/項目/標簽/許可權
四種:
標簽 頁面
| |
模板------>項目
許可權
首先介紹項目模塊目錄的結構,項目模塊的代碼基於 策略模式 組織,每個場景的業務邏輯拆分到單獨文件,由調度器直接調用,避免不同場景間邏輯摻雜。
- 調度器文件命名為
base_資源_service
- 場景策略文件命名為
場景小寫_資源_service
- 通用策略文件命名為
common_資源_service
當用戶查詢進來時,調度器根據查詢的條件直接調用對應策略文件中的方法(一般不允許直接調用指定場景的策略除非確認不會關聯到其他場景的數據),當調度器沒有沒有找到對應場景下的策略時,預設會調用 common_service
的邏輯,所以各場景需要繼承 common_service
。以頁面管理服務為例,調度器為 src/service/page
目錄下的 base_page_service
,通用邏輯為 common_page_service
,頻道頁場景邏輯為 ch_page_service
。
出於對場景下公有方法的統一抽象,服務中常用的 CRUD 方法介面 放置在 AbstractServiceClass
文件中
├── src
│ ├── service
│ │ └── {resource}
│ │ ├── base_{resource}_service 策略文件調用器,controller/mq 直接調用
│ │ ├── common_{resource}_service 通用策略文件,例如列表查詢共用的參數處理
│ │ └── {scene}_{resource}_service 場景策略文件,場景特殊的
部署
數據遷移
鑒於這次升級的巨變,在新舊版本間的切換務必慎重,除了前端與服務端為此做的大量的聯調外,我們還對數據進行了相容性遷移,主要做法是通過遷移腳本把舊數據根據新架構的需要做多重處理,爾後寫入新資料庫中。
不中斷部署
在單體架構中,每一次服務的發佈部署都會造成幾分鐘的空窗。
為避免這種情況,在生產環境,我們保證每個模塊至少擁有兩個容器,在部署的時候,把部分容器從負載均衡摘除,然後迴圈檢測容器是否還有流量,直至沒有流量進來才進行更新操作,服務啟動後重新添加到負載均衡,然後對剩下的容器進行同樣的操作,這樣做的好處是,保證了整個部署過程,服務是不中斷的,避免了部署過程中的空檔情況。
運維
為避免再重蹈舊架構下糟糕的運維體驗及項目代碼管理,我們為新架構梳理了一個運維文檔,包括快速接入、開發、調試、部署方方面面的細節都儘可能詳盡地記錄下來。
為系統增加了監控,監控每個介面的性能和可用性。
效果
經過這次升級,基本達成計劃中的效果:
- 清晰:邏輯梳理、去除冗餘、TS 重構、ESNext
- 模塊化:解耦 10+ 模塊,獨立運作;HTTP、MQ、數據層等多通信方式
- 標準化:強代碼規範;介面統一;響應統一
- 通用化:4+ 通用模塊,平臺無關;抽取公共庫、配置、插件、中間件等
- 易遷移:一鍵初始化;一鍵、單點、獨立部署;入口統一
- 易擴展:+新增站點拓展能力;調整場景拓展;節省人力時間成本 95%+
- 易維護:追加日誌;一鍵部署;不中斷部署
- 易對接:完備的 Joi 文檔;詳盡的介面變更記錄;儘可能的向上相容
工具/方法/協作
工具對項目的順利進行有非常重要的影響,因此在這次升級中,我們嘗試了多種工具。
為了保證項目成員對自己負責模塊有清晰的瞭解以及對模塊的改造有明確的圖樣,團隊引入流程圖工具用於梳理舊架構的模塊並分工,梳理勾畫新架構各個模塊內部的邏輯等等。
在排期方面,我們實踐使用到了甘特圖,用甘特圖按照模塊對任務進行拆分,然後指派給對應的負責人並設置計劃的進行時間,每天同步整體的進度,從甘特圖可以清晰地瞭解項目的資源分配與排期,也能看到項目計劃與實際的對照,有助於項目整體的進度把控。
甘特圖對項目升級的任務進行了初步的劃分,對於更細化的劃分,我們放到了 IssueBoard,IssueBoard 像是一個簡化版的任務看板,但對我們來說已經綽綽有餘了,另外,選擇它的理由還包括:它支持跟 git 提交進行聯動,適合開發人員使用,可以通過每次提交來關閉相應的 Issue。
總結反思
在這次升級過程中,也暴露了一些不足,主要體現在排期與預期以及在前期的溝通上。
-
排期與預期
在升級籌劃初期的排期過於樂觀,而且在升級過程中沒有再進行修正,當然這是客觀原因造成的,團隊要在有限的需求空窗期內完成升級以避免同時維護兩個版本,這導致的後果是團隊必須每天比計劃花更多的時間。
-
溝通
在服務端進行升級時,沒有跟前端溝通具體的細節,而這次升級又是非完全向下相容的,所以在聯調的時候給造成前端一定的困擾和不便。
參考
- Atom:https://ling.jd.com/atom
- Salak:https://salakjs.github.io/docs/docs/zh-cn/introduction.html
- RESTful API:http://www.ruanyifeng.com/blog/2014/05/restful_api.html
原文地址:https://aotu.io/notes/2020/04/21/atom-services-upgrade/
歡迎關註凹凸實驗室博客:aotu.io
或者關註凹凸實驗室公眾號(AOTULabs),不定時推送文章: