Cloudpods Golang實踐
Cloudpods是完全自研的一套云平臺,Golang是該平臺的主要后端開發語言。本文介紹我們在平臺開發迭代過程中關于Golang的經驗以及在Golang上積累的框架和庫,包括積累的Golang工具庫,以及基于這些工具庫實現的開發框架。
1、背景介紹
Cloudpods(云聯壹云)從2017年開始迭代開發。當時企業的IT環境已經不僅僅是本地的虛擬機以及裸金屬,不少企業已經逐步采納多云。所以Cloudpods平臺作為新一代的云平臺,需要不僅能管理本地IT環境中的虛擬機和裸金屬,還能管理其他的云平臺的資源,特別是公有云。實現所有的資源在一個平臺上統一運維,操作,起到降低運維復雜度并提高企業IT運維效率的目的。

為了實現一個統一的多云平臺,我們采用了最適合開發云原生應用的 Golang作為后端開發語言。前端則采用Vue框架。整個平臺基于微服務框架。服務之間的認證鑒權基于OpenStack Keystone的框架(我們用Golang重新實現了Keystone)。
2、Cloudpods Golang技術棧
云聯壹云的Golang技術棧包含兩部分:
首先是一個Golang的服務框架,所有的服務組件都基于同一套服務框架來開發,這個服務框架針對云平臺的特點做出優化,適合快速開發云服務的API和異步任務邏輯。
其次是四個主要的Golang工具庫
? jsonutils:JSON序列化和反序列化
https://github.com/yunionio/jsonutils? sqlchemy:模仿python SQLAlchemy的ORM庫
https://github.com/yunionio/sqlchemy? structarg:基于結構體的命令行參數生成和解析工具
https://github.com/yunionio/structarg? pkg:其他一些輔助工具和方法
https://github.com/yunionio/pkg3、Golang框架
大部分Cloudpods服務都基于同一套服務框架開發,此框架的特點是針對云服務開發進行了適配和優化。
Cloudpods的服務框架可以簡單地認為是一套方便CRUD的腳手架。云服務主要是對云資源的操作,比如云資源的創建、刪除、更新等。服務框架內置了云資源的CRUD API的基礎框架,再加上異步任務機制實現對云資源的復雜操作以及資源模型的修改。

除CRUD腳手架外,服務框架把認證,權限、配額等云平臺特有的功能內置集成,使得認證和權限成為云平臺默認的標配,同時簡化相應功能的實現。
首先,服務組件之間的通信基于OpenStack Keystone認證,我們將keystone認證加到框架中,使得服務組件開發者不需關注keystone認證流程如何實現,就能天然支持keystone認證。其次,每一個API請求都受到權限的控制,因此把權限控制也集成到框架中。開發者在實現REST API時,基本不必為權限控制實現相應的代碼,就能夠天然地將權限控制集成到API中。

與此同時,每一個服務都有相應的配置參數,如何方便地管理各個服務的配置,允許對配置更新并同步到相應的組件使其生效,此過程相對復雜。我們將服務配置的功能集成到框架中,開發者采用框架不必考慮配置的存儲、更新、服務器讀取更新并使配置生效,這些復雜事宜已在框架中解決。
另外,還有異步任務的管理功能。平臺是一個分布式的系統,云控制器需要去操作和管理數據計算節點、裸金屬的管理節點。協調組件之間的復雜操作,例如將虛擬機、裸金屬創建起來,這些都是分布式的任務管理,在平臺中也嵌入了異步任務管理框架。如此即可較為方便地實現異步任務。
4、CRUD腳手架原理
在平臺中,每一種資源,例如主機,在底層對應到數據庫MySQL表,資源的狀態、相應的屬性都記錄到了MySQL表中。

用戶通過調用API對數據進行操作,在數據操作的同時也能調用異步的任務去實現相應功能。落到底層代碼中就是一種資源對應到一張MySQL表。
為了比較方便地實現對數據庫MySQL記錄的操作,針對每一個資源,都會對應到一對ModelManager和Model的數據結構。ModelManager數據結構對實現一類資源的集合操作,例如創建資源或者列表,而針對單個資源的操作,則通過Model來實現,實現對單個具體資源的更新、刪除的操作。

每個資源的Model對應到Golang的一個結構體,該結構體有若干字段,每個字段代表資源的屬性,例如有一個用戶的資源包含用戶的id、extra屬性,用戶是否enabled,用戶何時創建,歸屬的域等。一個屬性就是Golang結構體的一個字段,通過結構體字段的Tag屬性定義每個字段在MySQL的數據庫中對應的schema的定義。
例如Id這個字段 ,屬性中有width:"64" charset:"ascii" nullable:"false" primary:"true"
這些tags定義了Id這個字段是數據庫里面的一個VARCHAR(64)的字段,并且他的字符集是ascii碼,不能為空,并且是主鍵。所以通過tag將結構體的字段映射到了MySQL的schema的字段中,如此,每一個model通過字段的定義就能夠清晰地映射到MySQL的數據表中。這樣實現了Model的字段和MySQL的數據表定義的嚴格同步,每次程序啟動時都會進行schema的同步檢查,如果Model 的定義和數據表的定義不一致,就會執行相應的SQL的變更操作,將表的schema定義和Model的定義變更為一致。
例如我們將Id的寬度從64改成80,在程序重啟時就能夠發現這個變化,然后將數據表該知道的寬度變更成80。如此實現通過代碼定義的Model 和數據庫中的表定義的嚴格同步。(另外,也支持離線變更數據庫Schema,即只檢測數據庫Schema的變更,并且輸出對應的SQL語句,通過專門的離線數據庫schema變更工具實現數據庫的變更。)
與此同時,每類資源都會提供一系列的API,此處列出了對一個資源會實現的九類API,包括創建、刪除、更新、執行操作、獲取詳情、列表等操作。

每一個操作對應一個REST API,每一個REST API對應到后端代碼中就對應到了每一個資源對應的ModalManager或者Model的方法。
例如獲取資源詳情的REST API是: GET /resources/
調用這個REST API其實就映射到相應的Model的GetDetails方法。為了實現獲取資源的詳情只需要去實現Model中的GetDetails的方法。
通過框架簡化了實現云資源REST API的流程,只需要把相應的Model和ModelManager的方法根據輸入實現相應邏輯,然后把正確的輸出返回回去,這個REST API的功能即可實現。
如此諸如鑒權 、認證、配置同步等周邊的工作在框架中實現,從而大大提升了開發效率并降低在開發過程中犯錯的幾率。
5、Golang 工具庫
下面介紹在Cloudpods開發過程中積累的Golang工具庫。
jsonutils是一個JSON序列化和反序列的工具庫。

Golang 的標準庫中帶的JSON庫是encoding/json,encoding/json是一個非常強大、非常高效的JSON序列化和反序列的工具庫。encoding/json實現的是Golang的數據結構和對應json的字符串之間的相互轉換。可以把Golang中的結構體通過Marshal的方式生成一個Json的字符串,或者把Json的字符串通過Unmarshal放到相應的結構體中的各個字段,這樣即可訪問結構體去獲得json中的這些值。
jsonutils與encoding/json相比的明顯區別是中間增加了一個中間態,在jsonutils庫里面實現一個中間態的通用類型的數據類型JSONObject。我們可以把數據結構Marshal(s)成JSONObject,JSONObject是Golang的interface,該interface可以進一步地序列化成json字符串。

這個中間態就是使用jsonutils的重要原因,通過通用類型的JSONObject就可以實現任意的結構體都可以Marshal(s)成JSONObject然后可以把JSONObject作為函數參數進行傳遞。
Golang是一個嚴格類型檢查的靜態語言,它的每一個變量都有相應的類型,我們的框架能夠處理任意API的輸入輸出,如果沒有中間的結構體,在處理API的輸入輸出時,輸入是json字符串, 為了在程序上訪問它, 就必須把它反序列化成嚴格的有類型結構體,這樣一來就無法將框架變成通用框架。
如果引入通用的JSONObject,在框架中輸入了json字符串, 先把它反序列化成JSONObject,這個JSONObject是通用類型的,這樣就可以將JSONObject作為參數再進一步的向下傳遞,直到傳遞到具體相應的Model或者ModelManager相應的方法中,然后進一步把它轉換成相應的結構體。這樣就允許框架中使用已經反序列化好的JSONObject 并進行操作,可以實現比較通用的框架。
平臺采用jsonutils最主要的原因是方便實現通用的框架。同時jsonutils還有其它特別之處,JSONObject 不僅能夠轉換成JSON字符串,也可以轉換成QueryString 或者是把QueryString 反序列化成JSONObject 或者可以序列化成YAML的字符串。
這樣可以實現更方便的功能,比如對于列表 GET這種讀取的這種API,它的參數通常作為QueryString 嵌入到URL中。如此我們可以將QueryString參數在框架中反序列化成JSONObject,把它作為JSONObject輸入參數傳入到框架中。
對于其他類型的請求,可以把請求參數放在HTTP請求的body中的JSON字符串,我們同樣可以把請求參數解析成JSONObject。這樣,可以以同樣的邏輯去處理嵌入到URL中的QueryString的參數以及嵌入到body 中的JSON的參數,可以做到統一處理出入參數的邏輯。
另外jsonutils 還有還有一些針對云平臺API做的一些特別的一些處理。較為特別的一點是支持結構體字段的版本變更。
以下為舉例說明:

例如有一個輸入參數的結構體稱為Input,有一個字段是TenantId,用來標識用戶的租戶ID。
隨著版本的升級,希望將TenantId統一改名為ProjectId,這種升級如果不做任何處理,將可能出現接口兼容性的問題,在變更之前這個字段是TenantId,變更之后這個字段就只能是ProjectId。升級后,使用TenantId的客戶端就不能正確訪問這個接口。
在這個結構體中,我們增加了特別的tag :yunion-deprecated-by,把這個結構體input升級為新的input的結構之后,增加了ProjectId的字段,用它來代表新的TenantId的屬性。
舊的TenantId仍然保留,但是在結構體的tag中加了名稱為yunion-deprecated-by的tag,這個Tag的值是ProjectId。表明TenantId的字段已經被ProjectId這個字段deprecated。
代碼在處理時,如果舊客戶端的參數中只有TenantId,此時框架就會將TenantId的值根據yunion-deprecated-by這個tag 指引同時copy到ProjectId的字段中。
這樣,如果舊的客戶端去訪問新的接口,在新的接口中同樣可以用ProjectId這個字段去獲取這個值。這樣保證了舊的客戶端可以訪問升級后的接口。
Sqlchemy是前面介紹的Golang服務框架中實現Model和數據庫表映射的底層實現。

Sqlchemy實現了Golang的數據結構到MySQL表單向同步,能夠根據結構體字段的定義以及字段中tag的定義,生成精確的MySQL的schema。
Sqlchemy能保證數據庫中schema總是嚴格地和結構體定義保持一致,如果不一致,能夠自動地變更數據庫,將數據庫Schema和結構體的定義變更為一致。
Sqlchemy另一個重要特性是能夠實現結構化的數據庫查詢語句。

這里舉一個簡單的例子。如上圖所示,我們要對UserTable表進行一個查詢。首先實例化UserTable實例,然后調用它的Query接口返回一個SQuery實例,再調用Equals方法,進行過濾。這樣就表示在查詢user表,并且要求domain_id 這個字段要等于指定的值。Sqlchemy在執行時會把結構體的結構化查詢變成SQL的查詢語句,并送到MySQL進行執行。
使用這種代碼化的數據庫查詢方式的優點是避免了人工拼湊SQL容易出現的問題,并且Golang是一個嚴格的靜態語言。可以通過Golang的語法檢查保證查詢語句的正確性。
另一個好處是因為把SQL的查詢代碼化,這樣就可以把數據庫查詢的邏輯進行一定程度的代碼復用,這樣就不用重復同樣的查詢語句,而是調用一個方法,這個方法中就嵌入了SQL的查詢邏輯。
框架中所有數據庫的操作查詢都用到sqlchemy,在一定程度上保證框架代碼中SQL語句的正確性以及執行效率。
還有一個比較重要一個庫是structarg,這個庫的作用是將程序的命令行參數或者配置文件信息和代碼中的結構體做嚴格的映射。

這樣可以基于結構體自動生成程序輸入參數的提示,并且能夠根據命令行參數將參數中的值反序列化放到結構體中, 或者是將對應配置文件中的參數反序列化到結構體中,在程序中就可以通過訪問結構體的字段去訪問相應參數的值。
如此即可比較方便地在程序中使用配置的信息,這里舉個例子。
假設定義了UserOption的結構體,這個結構體tag中包含一個help的tag,這個tag的定義了結構體的每個字段的幫助信息。將其初始化并進行程序編譯之后,如果命令行中執行帶help的參數的話,其將會把這些幫助信息展現出來。
同時,用戶也可以在命令行參數中去用這些參數把值傳遞到程序中,在程序代碼中即可通過訪問結構體去訪問這些參數。
同時,也支持配置文件的輸入,而且配置文件同時支持Key=Value格式和YAML格式。
這樣可以將程序的配置文件的配置信息通過結構體定義下來 ,程序就能夠自動地識別配置文件的信息,然后將信息放到結構體中訪問。
框架中對配置管理的基礎就是structarg的功能,利用該功能將每一個服務的配置用結構體來定義,并且配置的信息會存到數據庫中,在數據庫的信息進行修改之后,框架會把數據庫中的配置信息拉取下來,然后把它反序列化到結構體中,程序中能夠感知到配置的變更,并做出相應的配置變更處理。
除去三個比較重要的庫之外, 還有一些其他輔助的工具庫在pkg這個庫中。包括模仿python的prettytable的格式化輸出數據的庫,錯誤處理庫,保證key順序的map的實現等。這里就不一一介紹,感興趣的讀者可以閱讀相關源碼。


