智能可視化搭建系統 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 通信,這里舉個例子:編譯。服務端編譯通常需要的時間比較長,長時間占用連接對服務性能有所影響,而且編譯結果並不需要同步響應,對編譯模塊來說,如果來者不拒,對服務有不小的壓力,於是我們決定使用消息隊列來完成各個模塊之間的通信:

  1. 由項目模塊通過接口直接調用發布模塊發起發布操作;
  2. 發布模塊向消息池推送一條“我要編譯”;
  3. 編譯模塊接收到消息后由自身情況判斷是否可以進入編譯,否則先不予以響應;
  4. 編譯的各個狀態也通過消息推送;
  5. 最后項目模塊在接收到編譯狀態的消息后作各種處理。

編譯消息

通用化

前面提到在模塊化的工作中,我們拆出了 4 個通用的服務模塊,通用服務獨立於 Atom 系統之外,可以為 Atom 以及其他系統提供服務。模塊的通用化是出於兩點考慮:

  1. 豐富部門的服務,減少重復開發功能
  2. 排除 Atom 非核心代碼,讓系統瘦身

伴隨而來的一個問題值得我們思考,如何考量一個功能是否值得抽離通用化?我們應該盡量避免陷入一個誤區:系統模塊化就是把系統拆得越細越好。如果拆分過細,勢必增加運維工作量。在拆分模塊的時候,我們考量的是一個模塊內的功能是否完整且獨立,以及部門或公司對這個通用服務的需求度,真正地做到低耦合高內聚

標准化

代碼層面,下面做了個簡單的對比:

對比項 舊架構 新架構
主要語言 JavaScript TypeScript
代碼檢測 未遵守 必需
接口名稱 花樣百出 統一形式
接口輸出 百花齊放 統一形式

TypeScript 的好,前端人都知道,它為我們帶來了自動補全、可選的類型系統,使我們能夠用上更加新的 JavaScript 特性等等,更多可以參考《為什么選擇 TypeScript》。出現后面三點的原因是什么?舊架構經歷了從零到一的過程,項目在最初規划欠缺以及中后期沒有足夠的時間對系統進行修正,時間和需求的變更的雙重作用導致代碼淤積。

為此,我們在新架構的開發中就強調代碼的標准化,對每次提交都要經過代碼檢測,然后是對五花八門的接口進行統一:

  • 接口路徑統一:舊架構中,一個列表接口的路徑可能是 /xxx/list,也可能是 /xxx/xxxes 等等,我們在新架構中基於 RESTful API 規則,用資源名詞組成的路徑和語義化的 HTTP 協議統一接口的定義;
  • 參數名統一:比如列表入參中每頁數量可能叫 pageSize 也可能叫 count,於是我們把它統一成一個名字,要求在開發中遵守這個約定;
  • 輸出統一:在數據輸出到前端前對數據進行處理篩選,剔除包括 _id__v 等無關數據,在輸出形式上也做了統一,要求輸出中所有的 _id 都替換以 id 的名字出現等等。

Behavior

代碼標准化的好處是讓代碼更加好維護,開發人員很快就能定位到對應的接口代碼,對前端而言則減少對接口的識別記憶。

插拔式站點

前面提到,這次架構升級的直接原因是站點需求和場景需求。如果在舊架構下迭代站點需求,只會進一步增加耦合度。為此,我們增加了站點管理模塊,在幾乎所有的數據項中增加了站點字段,給幾乎所有的數據庫查詢都帶上了站點參數。通過這些努力,現在新增站點只需要通過站點模塊新增站點,再做一些初始化配置即可完成。

站點概念除對 Atom 功能有了更高要求,也對原來的權限體系形成了新的挑戰。在升級前的版本中用戶的權限僅有一個集合,要實現每個站點擁有不同的權限只能從兩個角度出發:

  1. 權限含義拆分(為每個站點分別提供一套獨立的權限)
  2. 用戶權限增加一層抽象(用戶的權限改變為多個集合根據站點進行切換)

在比較了兩種修改形式后,拆分權限含義雖然在理解上比較容易代碼也改動不多。但卻大大提升了維護權限表的難度,相當於新增場景就需要增加一套權限,無法做到可插拔。最后在網關層增加了根據用戶訪問站點
切換權限集合的邏輯。

插拔式場景

場景是站點下面一個緯度,現有活動、頻道、心理學測試、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。

IssueBoard

總結反思

在這次升級過程中,也暴露了一些不足,主要體現在排期與預期以及在前期的溝通上。

  • 排期與預期

    在升級籌划初期的排期過於樂觀,而且在升級過程中沒有再進行修正,當然這是客觀原因造成的,團隊要在有限的需求空窗期內完成升級以避免同時維護兩個版本,這導致的后果是團隊必須每天比計划花更多的時間。

  • 溝通

    在服務端進行升級時,沒有跟前端溝通具體的細節,而這次升級又是非完全向下兼容的,所以在聯調的時候給造成前端一定的困擾和不便。

參考

原文地址:https://aotu.io/notes/2020/04/21/atom-services-upgrade/


歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公眾號(AOTULabs),不定時推送文章:

歡迎關注凹凸實驗室公眾號


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM