微服務絞殺遺留模組 — — 均一後端軟體工程師 Amy 技術分享

均一 JunyiAcademy
9 min readApr 10, 2020

--

[前言]均一的程式碼基礎 junyiacademy 從 2013 年 fork Khan Academy 原始碼,一直發展到現在,程式碼的複雜度不可同日而語,開發和維護的難度越來越高,很根本而且不好償還的技術債逐漸浮上檯面,有些甚至成為新功能開發的巨大阻力。

於是,我們在去年三月決定從簡化程式碼下手,具體作為是從原本的單體式應用拆出一個負責內容管理的微服務,並將內容管理的邏輯逐步遷移到此服務,直到完全絞殺原始的內容管理模組。

Why we do this: 技術債

均一後端由 Python 撰寫而成,Python 有簡單清晰,語法優雅的優點,但因為極大的自由,例如動態型別與缺乏存取修飾(沒有 private),還有一些腳本語言的特性,例如容許 cycling import,專案長大容易變成一頭無法駕馭的巨獸。

筆者以為,尤其在大型專案,搭配因地制宜的編程規範才能將 Python 的優良部份發揮到最大。

模組化的大敵

先破個梗,從母專案 junyiacademy 拆出的微服務 JunyiContentService_py3 目前的 dependency graph 是這樣的:

Python3 Content Service dependency graph
Python3 Content Service dependency graph

如同普通基於 Flask 的 Web server,右下角的 main 是所有 API 路由的入口,也就是 dependency 最末端的節點。

從右下角開始每個節點順順往上指,越往上面就是越底層的邏輯;除此之外,可以看到圖中間群聚一些節點,是相關性很高的模組(Python module)。這張圖第一眼看到就可以觀察到明顯的秩序,是一個正常的 dependency graph。

而母專案 junyiacademy 是怎樣的呢?

2018 年時志工阿東大大曾幫忙畫過一張 dependency graph,當時是長這樣的:

2018 junyiacademy 全專案的 dependency graph
2018 junyiacademy 全專案的 dependency graph,想要進一步使用互動方式檢視圖片?請參考志工阿東大大的原文

中間有兩個點,一樣是 API route 的入口,從中心往外可以看到內外兩圈節點,但節點之間代表相依性的線錯綜複雜,最多就是觀察到程式有大量的 cycling import。

嗯,沒錯。模組邊界已經從程式碼中消失,且模組之間勉強要定義位階(上層或底層)也很困難。

程式碼同源的 Khan Academy 後來的發展也遭遇類似困境,是 Khan Academy 2018 年發表 Untangling our Python Code 當中的 dependency graph,投入了數個人月重構,仍只有小幅度改善。

2018 Khan Academy 的 dependency graph
2018 Khan Academy 的 dependency graph

高內聚低耦合,最遙遠的距離

程式美學告訴我們,維持軟體的高度內聚和低度耦合乃是一切易讀性和可擴展性的根基;但「編寫乾淨且模組化的程式碼」卻在程式碼基礎逐漸增長的過程淪為道德勸說和精神指標。

於是工程師靈肉分離的重新打造輪子、在模組邊界搞曖昧(搭配 FIXME 和終將沉到甕底的技術債 issue),於是你看到前面佈滿線條,不知所以的兩個圈圈,於是用戶越來越多,我們敢承諾的功能卻越來越小,最後,我們起了殺意⋯⋯

How we do this 1: 絞殺者模式

開玩笑的,怎麼可能因為一點糾結的 dependency 就要殺掉多年的智慧結晶?身為台灣 K-12 線上教育的扛霸子,均一工程團隊做決策是講究科學根據、理性討論、實證導向的。

真正的殺機主因是 Python2 end-of-life,而 GAE (Google App Engine,均一採用的雲端代管服務)Python3 的 SDK 與 Python2 截然不同,需要人工修改大比例的程式才能搬遷至 Python3,我們認為也許長遠來看,把母專案 junyiacademy 絞殺掉遠比就地重構更適切!

當然,絞殺掉均一六年的基業是大事,不能信口開河,貫徹精實創業精神的均一教育平台工程團隊首先要小規模試驗,既然是小規模,好像很適合用一下很潮的微服務架構⋯⋯

How we do this 2: 微服務小試身手

除了很潮,講究理性辯證的均一工程團隊自然要端出一套(冠冕堂皇的)理決策要素。微服務架構的優點很多,下面只列出對我們影響最大的好處:

高內聚低耦合,說到做到!

微服務是一群協同合作的小型自主服務,架構師中的架構師 Martin Fowler 列出微服務架構的第一項優勢即是 Strong Module Boundaries,具體是用服務邊界來規範模組邊界。

由於服務之間通常透過 Web API 之類的方式互相溝通,相對於同一個程式碼基礎中的另一個模組或函式庫,服務邊界顯然更明確,要搞曖昧也不是不行,但是眉來眼去的成本就高得多。

我的技術堆疊我決定

除了明確的模組邊界以外,微服務的自主性是一大關鍵。

你看看,母專案 junyiacademy 是 Python2 寫的,新服務當然不可能沿用。微服務是獨立的實體,在 GAE 框架下則是獨立的 service,可以自主決定使用的語言,擁有一組隨著流量自動擴充的虛擬機。

GAE service 還有一點非常適合微服務新手,就是和母專案共用資料庫。

因為微服務架構的成功與否幾乎由服務邊界的品質決定,而服務邊界需要時間淬煉,也就是要反覆調整才會穩定下來;調整的過程如果經常牽涉到折磨人的資料庫搬遷,大概會很快放棄,判微服務架構出局。

不過聊到這裡,有些讀者已經想到,既然可以換語言,Python2 轉 Python3 又因為 SDK 不相容沒有主場優勢,那 GAE 提供好幾個語言的環境都可以考慮啊!

沒錯,其實筆者手寫 Python3,心裡想的卻是 Golang,但為什麼不心手合一,直奔 Golang 呢?

筆者認為,均一工程團隊雖然對前沿技術心神嚮往,卻也懂得墊墊自己斤兩,白話文是資源有限,一次挑戰一個主題,小步前進是我們對專業的尊重。

現階段的主題是分解系統,用小服務取代原大專案的部分功能,我們把焦點放在找到好的服務邊界,確保相關的行為聚集在一起,且盡可能以鬆散的方式做跨邊界溝通。

高大上的東西交代到這裡,下面來談談我們實際做了什麼。

Our progress now 1: Python3 Content Service

前面有提過,直接分析程式碼尋求服務邊界不容易,我們轉向從產品角度切入,配合組織發展的目標,決定一號微服務由教材內容管理擔綱演出,以下稱作 Content Service。

暫態:Python2 Content Service

這個暫時的階段主要工作是解除多餘依賴。為了幫助我們在程式碼中釐清 Content Service 的合理依賴的對象,我們先複製純粹內容的 API 到暫時存在的 Python2 Content Service。

不要小看這一步,均一在線的 API 數目很大,而內容又屬於底層邏輯,切出幾個少數的 API 可以大幅簡化問題。

即使是簡化過的功能,複製出來的程式碼仍然包含了許多不合理的依賴性,例如下面示意圖中,Video(教學影片)需要依賴 User 邏輯即是一例,而 Topic (主題)和 Badge (徽章)之間的 cycling import 則更刺眼。

Content Service 的相依性示意圖
Content Service 的相依性示意圖

解依賴的過程充滿了解和體諒,我們探討 Topic 和 Badge 你來我往的箭頭究竟怎麼出現的?又有哪些箭頭該塗上紅色?然後板起面孔,塗一輪紅色殺一輪。

就這樣迭代了幾次,讓內容的相依性只剩下身份驗證,快取與資料庫操作等基本元件,接下來切回 Python3 主戰場。

Package 依賴關係規範

為了避免重蹈原始專案的覆轍,參考 Khan Academy 的經驗,我們也建立了 package 之間依賴關係的規範:為每個 package 定義層級,依賴關係必須從高階至低階,且同階層之間不能有依賴關係。

下圖是 Python3 Content Service 目前每個 package 的依賴關係,包含了微服務的基本配備與 content 核心邏輯。

Python3 Content Service package 之間的依賴關係與階層
Python3 Content Service package 之間的依賴關係與階層

Our progress now 2: Content Package

在內容主邏輯部分,過去最大的困境是 Topic 和教材 (Exercise, Video, …) 之間互相 import,產生了很多的 cycling import。

重新架構的 content package 引入了工廠模式,透過內容工廠(contnt.factory)做相依注入

所以主題和教材之間不再有互相依賴,細節可以參照下圖的 content.internal.xxx。除此之外,content.xxx 是 package 的入口,也就是讓負責 api routing 的 main.py 呼叫的進入點,目前有 content.topic。

Content package 中 module 的相依關係
Content package 中 module 的相依關係

以上是 2020 Q1 勾勒出的 Python3 Content Service 架構,接下來的幾個月會不斷充實它。

為了避免美好清晰的架構隨著功能豐富而崩解,我們同時也堅持幾項規範,包含單元測試覆蓋率大於八成、pylint 沒有 warning 和 error,以及引入工具檢查前面介紹的 package 依賴關係是否符合規範。

想要了解更多可以參考或直接參與我們的開源專案

均一軟體工程團隊招募中!

你對教育有想法、對全端有些興趣,想要跟艾彌一起充實 Content Service 架構?或是你熟悉前端工程開發,想要透過前端技術,打造更能引導孩子學習的平台介面?

我們正在招募軟體工程師、資深前端工程師,馬上到招募頁面了解詳情、投遞履歷,加入我們

作者|陳艾彌(均一平台教育基金會資深軟體工程師)
編輯|陳又慈(均一平台教育基金會實習生)

--

--

均一 JunyiAcademy

均一相信,每個孩子都學得會,而且是用自己的步調學習!用科技打造線上學校,提供優質免費的線上資源;用智慧助教陪伴家長老師、更省時省力陪伴每個孩子用自己的步調學習。