我們如何處理大型 Python 單體架構

2023-08-10     InfoQ

原標題:我們如何處理大型 Python 單體架構

作者 | David Seddon

譯者 | 平川

策劃 | Tina

本文最初發布於 EuroPyhon 官方博客。

大家好,我叫 David,是 Kraken Technologies 的一名 Python 開發人員。我從事 Kraken 開發,那是一個 Python 應用程式。據最新統計,它有 27,637 個模塊。是的,你沒看錯:將近 28k 個獨立的 Python 文件,這還不包括測試。我和世界各地的其他 400 名開發人員一起做這件事,不斷地合併代碼。任何人只要在 Github 上得到一個同事的批准,就可以做出變更,開始部署這個運行著 17 家不同的能源和公用事業公司、擁有數百萬客戶的軟體。

現在,你可能覺得這會很混亂。說實話,我也會這麼說。但事實證明,大量的開發人員可以在一個大型的 Python 單體上有效地開展工作,至少在我們工作的領域是如此。這是可能的,原因有很多,很多是文化上的,而不是技術上的。但在這篇博文中,我想介紹一下代碼的組織如何幫助我們實現這一目標。

代碼庫分層

如果你在某個代碼庫上做過一段時間的開發,那麼你肯定感受過那令人不快的複雜性。應用程式的邏輯鏈錯綜複雜,想要獨立地考慮應用程式的各個部分變得越來越困難。我們的代碼庫開始時也是這樣,所以我們決定採用所謂的「分層架構」,對代碼庫層與層之間哪些部分可見做了限制。

分層是一種眾所周知的軟體架構模式。在這種模式中,組件在概念上被組織成一個棧。棧中的組件不能依賴於上層的任何組件。

分層架構,向下依賴

如上圖所示,C 可以依賴 B 和 A,但不能依賴 D。

分層架構的概念很廣泛:它可以用於不同類型的組件。例如,你可以將幾個可獨立部署的服務分層;或者,你的組件可以只是一組原始碼文件。

依賴的構成也很廣泛。一般來說,如果一個組件對另一個組件有直接的了解(即使純粹是在概念層面),那麼它就依賴於另一個組件。間接交互(例如,通過配置)通常不視為依賴。

Python 中的分層

在 Python 代碼庫中,最好將層視為 Python 模塊,將依賴視為 import 語句。

以下面的代碼庫為例:

頂層模塊和子包是層的良好候選。假設我們按下面這樣的順序分層:

那麼,在這個架構中,shopping_cart 不能導入 payments 中的任何模塊。不過,它可以導入 products 的模塊。

層可以嵌套。因此,payments 可以像下面這樣分層:

至於分成幾層以及各層之間的順序,並沒有一種唯一正確的方法,這是一種設計行為。但是像這樣的分層可以減少代碼庫的混亂,使其更容易理解和修改。

Kraken 是如何分層的

在我寫這篇文章時,有 17 家不同的能源和公用事業公司批准了 Kraken 的使用。我們為這些企業客戶中的每一個運行一個單獨的實例。現在,Kraken 的主要特點之一是不同的實例「相同又不同」。換句話說,它們之間有很多共享的行為,但每個實例都有滿足特定客戶需求的定製代碼。在地區層面上也是如此:在英國運營的所有客戶之間存在一些共性(它們與同一能源行業相融合),但與 Octopus Energy Japan 不同。

隨著 Kraken 發展成為一個多客戶平台,我們改進了它的分層結構。總的來說,現在的情況是這樣的:

客戶位於頂層。每個客戶對應該層中的一個子包(例如,oede 對應 Octopus Energy Germany)。客戶的下面是地區,實現特定於國家的行為,同樣,每個地區對應一個子包。最下層是核心層,其中包含所有客戶都使用的代碼。還有一個額外的規則,即客戶子包必須是獨立的(即不能從其他客戶包導入),對於地區同樣如此。

像這樣把 Kraken 分層可以有效限制變更的「爆炸半徑」。由於客戶層位於頂部,所以沒有任何東西直接依賴於它。因此,要修改與特定客戶相關的內容會比較容易,而且不會意外影響其他客戶的行為。同樣,只涉及一個地區的更改也不會影響到另一個地區的任何東西。這使得我們能夠快速獨立地跨團隊開展工作,特別是當我們正在進行的更改僅影響少量 Kraken 實例時。

利用 Import Linter 強制分層

當我們引入分層時,我們很快就發現,僅僅討論分層是不夠的。開發人員經常會不小心違反分層結構。我們需要以某種方式強制執行,為此,我們使用了 Import Linter。

Import Linter 是一個開源工具,可以檢查你是否遵守了分層架構。首先,在 INI 文件中定義一個契約,用於描述分層,像下面這樣:

name = Top level layerstype = layerslayers =kraken.clientskraken.territoriesKraken.core

我們也可以另外添加兩個契約(『independence』契約),強制保持不同客戶和地區的獨立性:

[importlinter:contract:territory-independence]name = Territory independencetype = independencelayers =kraken.territories.deukraken.territories.gbrkraken.territories.jpn...

然後,你可以在命令行上運行 lint-imports,它會告訴你是否有任何導入違反了契約。每次有 pull 請求時都會自動執行這個命令進行檢查,所以如果有人引入了非法導入,就無法通過檢查,也就無法完成合併。

契約不只這些。團隊可以在應用程式中添加更深的分層:例如,kraken.territories.jpn 本身就是分層的。目前,我們有 40 多個契約。

減少技術債務

在引入分層架構時,我們沒法從第一天開始就嚴格遵守。所以我們使用了 Import Linter 的一個特性,在檢查契約之前忽略某些導入。

然後,我們使用被忽略導入的數量作為跟蹤技術債務的指標。這使我們能夠了解改進情況,以及改進速度。

自 2022 年 5 月 1 日起被忽略導入的數量

上圖是在過去一年左右的時間裡,被忽略導入的數量變化情況。我定期向人們進行分享和展示,鼓勵他們朝著完全遵守依賴原則的方向努力。我們在其他幾個技術債務指標中也使用了這種燃盡方法。

缺點,缺點總是不可避免

局部複雜性

在採用分層架構之後的某個時刻,你可能會遇到需要打破分層的情況。實際情況會非常複雜,到處都是相互依賴,比如,你會發現自己想要調用一個更高層的函數。

幸運的是,辦法總比問題多。我們可以利用控制反轉,那在 Python 中很容易實現,所需的只是理念的轉變。但它確實會增加「局部」的複雜性(如代碼庫的一小部分)。然而,為了使系統總體上更簡單,付出這樣的代價是值得的。

較高的層上代碼過多

層越高,越容易更改。我們是有意為之的,讓針對特定客戶或地區的代碼更容易更改。其他所有的層都要依賴於核心代碼,對其進行修改的成本和風險也都更高。

因此,我們面臨的設計壓力部分是由我們選擇的分層結構帶來的,我們需要編寫更多特定於客戶和地區的代碼,而不是在核心代碼中引入更深的層次和更多可供全局使用的代碼。因此,較高的層所擁有的代碼超出了我們的預期。我們仍在研究如何解決這個問題。

我們還沒有完成

還記得那些被忽略的導入嗎?好吧,幾年過去了,我們還是有一些!據最新統計,有 15 個。最後幾項導入是最棘手、讓人最糾結的。

回顧性地對代碼庫進行分層可能需要付出很大的努力。但這件事你做得越早,需要解決的問題就越少。

小 結

Kraken 的分層架構使我們這個非常龐大的代碼庫得以保持健康,並且相對比較容易使用,尤其是在這麼個規模下。如果不對這成千上萬的模塊之間的關係施加約束,我們的代碼庫可能就會變成一大盤義大利面。但是,我們選擇的這個大規模結構——並且隨著業務的發展而發展——幫助我們在單個 Python 代碼庫上做了大量的工作。這似乎是不可能的,但我們確實做到了!

如果你正在處理大型 Python 代碼庫(甚至是相對比較小的代碼庫),不妨試一下分層。這事越早做越簡單。

原文連結:

https://blog.europython.eu/kraken-technologies-how-we-organize-our-very-large-pythonmonolith/

谷歌重磅發布多平台應用開發神器:背靠 AI 編程神器 Codey,支持 React、Vue 等框架,還能補全、解釋代碼

IPv4 開始收費!新的 IT 災難?

愛奇藝VR公司業務停滯,員工或被欠薪;阿里雲開源通義千問 70 億參數模型,免費可商用;華為正式發布鴻蒙 4,接入大模型|Q資訊

年薪超 600 萬,比技術總監還高:電影行業 AI 產品經理的崛起

文章來源: https://twgreatdaily.com/zh-tw/83c3f93c2f7b7c5f9a44e6aa55889e55.html