智慧可視化搭建系統 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

總結反思

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

  • 排期與預期

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

  • 溝通

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

參考

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


歡迎關注凹凸實驗室部落格:aotu.io

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

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