Golang 中的 ORM 編程雜談
我們最近決定在Bridge Financial Technology放棄我們的對(duì)象關(guān)系映射器 (ORM) ,并且一直很喜歡這個(gè)決定。放棄你的 ORM 需要挑戰(zhàn)和承諾,但利潤(rùn)是真實(shí)的。我將介紹:
- 背景:描述軟件開(kāi)發(fā)中所謂的越南問(wèn)題,理解和分類(lèi)映射器。
- 我們的旅程:我們轉(zhuǎn)型的原因和方式以及經(jīng)驗(yàn)教訓(xùn)。
- 解決的問(wèn)題和收獲:性能、可擴(kuò)展性、可維護(hù)性和更多云原生代碼的改進(jìn)以及以數(shù)據(jù)為中心的透明文化。
越南問(wèn)題的背景與思考
在2010 年左右的DjangoCon上,有人告訴我使用 ORM 就像美國(guó)在越南開(kāi)戰(zhàn)一樣。該評(píng)論指的是Ted Neward在他 2006 年題為“計(jì)算機(jī)科學(xué)的越南”的博文中創(chuàng)造了這個(gè)詞并將他的比較形式化。
這種比較引起了我的共鳴,但我從未考慮過(guò)放棄 ORM。從類(lèi)比中吸取了完全錯(cuò)誤的教訓(xùn),我花了幾年時(shí)間尋找完美的 ORM,無(wú)論是通過(guò)我自己的決策還是其他人的決策,我都接觸到了名副其實(shí)的環(huán)法自行車(chē)賽:Python、Active Record、Linq 中的 Django 和 SQLAlchemy, Hibernate、實(shí)體框架,以及最近在 Bridge Financial Technology 的 Golang 支持的后端中的 Gorm。
當(dāng)我們?cè)谲浖_(kāi)發(fā)中發(fā)現(xiàn)痛苦時(shí),傾其所有是很重要的。讓它痛到你無(wú)法忍受為止。疼痛越嚴(yán)重,您就越能更好地描述和確定疼痛的來(lái)源。我們的痛苦讓我重新評(píng)估了所謂的越南問(wèn)題,并質(zhì)疑 ORM 是否有意義。在對(duì)面向?qū)ο蠛完P(guān)系世界之間的不匹配進(jìn)行全面分析之前,我不會(huì)去。為此,我建議閱讀 Ted Neward 的永恒帖子。但我將總結(jié)重要的要點(diǎn)。
越南:對(duì)象-關(guān)系不匹配
我首先以大多數(shù)歷史學(xué)家今天所看到的方式過(guò)度簡(jiǎn)化了越南戰(zhàn)爭(zhēng)。越南人正在打一場(chǎng)內(nèi)戰(zhàn)以統(tǒng)一他們的國(guó)家。美國(guó)與蘇聯(lián)和共產(chǎn)主義本身進(jìn)行了代理人戰(zhàn)爭(zhēng),以防止其蔓延。也就是說(shuō),越南人將其視為南北問(wèn)題,而美國(guó)則將其視為東/西問(wèn)題。
整件事都不值得。越南今天是一個(gè)統(tǒng)一的共產(chǎn)主義國(guó)家(越共得到了它想要的東西),而沒(méi)有目睹共產(chǎn)主義學(xué)說(shuō)在亞洲以外的中國(guó)傳播,中國(guó)于 1949 年成為共產(chǎn)主義國(guó)家(美國(guó)得到了它想要的東西)。然而,它跨越了近 15 年,3 屆總統(tǒng)政府和超過(guò) 100 萬(wàn)人傷亡(橫跨美國(guó)、越南北部和南部、雙方的盟友以及軍事和文職人員)。從表面上看,更好的策略是完全脫離接觸。
轉(zhuǎn)向軟件開(kāi)發(fā):對(duì)象和關(guān)系同樣是根本不同的東西。他們來(lái)自不同的地方,有著不同的目標(biāo),沒(méi)有錯(cuò)也沒(méi)有壞處。當(dāng)然,技術(shù)可以在這些世界中進(jìn)行堆棧排名,使用您的團(tuán)隊(duì)知道、尊重和信任的堆棧,您將獲得更多里程。對(duì)我們來(lái)說(shuō),我們相信 Postgres 是關(guān)系數(shù)據(jù)庫(kù)(比 MySQL 或 SQL Server 更好)的同類(lèi)最佳實(shí)現(xiàn)。同樣,我們喜歡并欣賞 Go,特別是它沒(méi)有通過(guò)繼承實(shí)現(xiàn)面向?qū)ο蟮脑O(shè)計(jì)目標(biāo)。如果您在數(shù)據(jù)庫(kù)或編程語(yǔ)言方面遇到困難,我建議您先解決該問(wèn)題,然后再解決交叉路口的問(wèn)題,或者至少是我們的軌跡,我們對(duì)結(jié)果感到滿(mǎn)意。充實(shí)下面的類(lèi)比是對(duì)這些“方面”的概述:
面向?qū)ο笤瓌t
面向?qū)ο蟮南到y(tǒng)旨在提供:
- 身份管理:將狀態(tài)的等價(jià)性與對(duì)象本身分開(kāi)。當(dāng)兩個(gè)對(duì)象的值相同時(shí)發(fā)生對(duì)象等價(jià),而當(dāng)兩個(gè)對(duì)象相同時(shí)發(fā)生身份等價(jià),也就是說(shuō),它們都指向內(nèi)存中的相同位置。
- 狀態(tài)管理:將幾個(gè)原語(yǔ)關(guān)聯(lián)成一個(gè)更大的捆綁包的能力,該捆綁包代表有關(guān)世界或問(wèn)題的某些東西。
- 行為:操作所述狀態(tài)的操作集合。
- 封裝:將對(duì)象的簡(jiǎn)化的、導(dǎo)出的表面區(qū)域定義到系統(tǒng)的其余部分的能力,從而隱藏不必要的細(xì)節(jié)。
- 多態(tài)性:同質(zhì)處理不同對(duì)象的能力,盡管它們是不同的東西,但它們可以以相似的方式做出反應(yīng)。
大多數(shù)編程語(yǔ)言通過(guò)繼承模型來(lái)實(shí)現(xiàn)這些原則。但值得注意的是,許多非常成功的語(yǔ)言確實(shí)是沒(méi)有繼承的面向?qū)ο蟮模禾貏e是 Go、Erlang 和 Rust。
關(guān)系原則
關(guān)系存儲(chǔ)引擎旨在規(guī)范化數(shù)據(jù)并記錄世界的事實(shí)。SQL 提供操作以與圍繞集合論建立的數(shù)據(jù)進(jìn)行交互,以確保數(shù)學(xué)正確性并在數(shù)據(jù)可變性期間實(shí)現(xiàn)屬性;即ACID 合規(guī)性。在事務(wù)中執(zhí)行的 SQL 操作是原子的、一致的、隔離的和持久的。規(guī)范化通常是通過(guò)適當(dāng)?shù)脑O(shè)計(jì)來(lái)實(shí)現(xiàn)的,黃金標(biāo)準(zhǔn)是第三正?;?Boyce-Codd 形式。
(一些)差異
這些是完全不同的系統(tǒng)。他們的一些區(qū)別包括:
- 對(duì)象對(duì)聚合狀態(tài)有意義,而關(guān)系試圖將其分割成多個(gè)表。
- 對(duì)象世界中的數(shù)據(jù)可變性問(wèn)題圍繞并發(fā)系統(tǒng)中的意外覆蓋問(wèn)題,并通過(guò)封裝進(jìn)行保護(hù)。關(guān)系世界中的可變性需要交易來(lái)保證狀態(tài)改變時(shí)的正確性。在數(shù)據(jù)被持久化之前,不太需要在改變對(duì)象狀態(tài)時(shí)獲得正確性。
- 存在對(duì)象集合以實(shí)現(xiàn)跨這些對(duì)象的行為,而存在關(guān)系集合以建立關(guān)于世界的事實(shí)。
概念問(wèn)題
以下是由這些差異引起的問(wèn)題列表。
映射問(wèn)題。由于以下問(wèn)題,使用任何映射器都很難將表映射到對(duì)象:
- 對(duì)象關(guān)系將通過(guò)將一個(gè)對(duì)象與另一個(gè)對(duì)象組合來(lái)表示。但是,相關(guān)對(duì)象依賴(lài)JOIN于 SQL 中的隱式操作,否則相關(guān)對(duì)象將不會(huì)被數(shù)據(jù)初始化。
- 關(guān)系引擎中的多對(duì)多表涉及 3 個(gè)表:兩個(gè)數(shù)據(jù)源和第三個(gè)連接表。然而,這種關(guān)系只需要兩個(gè)對(duì)象以及另一個(gè)對(duì)象的列表。
如果您的編程語(yǔ)言支持繼承,那么以這種方式對(duì)對(duì)象建??赡芎苷T人,但數(shù)據(jù)庫(kù)中沒(méi)有 IS-A 類(lèi)型的關(guān)系。
誰(shuí)/什么擁有架構(gòu)定義?編程語(yǔ)言,以及應(yīng)用程序開(kāi)發(fā)人員,還是數(shù)據(jù)庫(kù)的 DDL,以及 DBA?即使您在組織中沒(méi)有區(qū)分這些角色,您仍然會(huì)遇到這個(gè)問(wèn)題,即所謂的“O”受制于“R”,反之亦然。
元數(shù)據(jù)去哪兒了?大多數(shù)字段自然會(huì)有 1:1 的對(duì)應(yīng)關(guān)系。例如,整數(shù)、布爾值等將在數(shù)據(jù)庫(kù)和您的編程語(yǔ)言中具有眾所周知的類(lèi)型。但是枚舉呢?很可能這些是帶有有限數(shù)量選項(xiàng)的字符串或整數(shù),并且數(shù)據(jù)庫(kù)和編程語(yǔ)言都可以強(qiáng)制執(zhí)行約束。那么你把這些元數(shù)據(jù)放在哪里呢?應(yīng)用程序?數(shù)據(jù)庫(kù)?兩個(gè)都?
所有這些問(wèn)題都需要通過(guò)將一方(對(duì)象或關(guān)系)聲明為權(quán)威來(lái)解決。這導(dǎo)致了 ORM 的分類(lèi)——它對(duì)這個(gè)權(quán)威有什么看法?對(duì)象還是關(guān)系?
理解和分類(lèi) ORM
實(shí)際上,您不能簡(jiǎn)單地“退出”并擺脫問(wèn)題。這篇文章的標(biāo)題有點(diǎn)營(yíng)銷(xiāo)動(dòng)機(jī)。畢竟,您不會(huì)將對(duì)象持久保存到平面文件中并將其稱(chēng)為“數(shù)據(jù)庫(kù)”,也不會(huì)使用 SQL 構(gòu)建應(yīng)用程序。這些事情必須在某個(gè)時(shí)候相遇。但我認(rèn)為,許多 ORM 采用的通用方法是有限的,主要是出于方便,通常以犧牲一方為代價(jià)。
從廣義上講,有兩種類(lèi)型的 ORM:代碼優(yōu)先和數(shù)據(jù)庫(kù)優(yōu)先。
- 在代碼優(yōu)先方法中,您將對(duì)象模型定義定義為將映射到數(shù)據(jù)庫(kù)實(shí)體(一個(gè)或多個(gè)表)的類(lèi)或類(lèi)型。這種方法即時(shí)生成 SQL 并大量使用反射。Go 社區(qū)中的一個(gè)例子是gorm。
- 數(shù)據(jù)庫(kù)優(yōu)先方法通常依賴(lài)于從您的數(shù)據(jù)庫(kù)定義語(yǔ)言 (DDL) 生成對(duì)象定義代碼。Golang 示例是SQLBoiler。
您可能會(huì)對(duì)其中之一產(chǎn)生直觀(guān)和直覺(jué)的反應(yīng)。這種觀(guān)點(diǎn)可以頑固地持有。就個(gè)人而言,我總是優(yōu)先考慮對(duì)象而不是關(guān)系,并使用代碼優(yōu)先的 ORM。但最終我將想法轉(zhuǎn)變?yōu)閮?yōu)先考慮數(shù)據(jù)庫(kù)。該決定通過(guò)生成用于與數(shù)據(jù)庫(kù)交互的代碼,促使將 ORM 全部丟棄。
我們放棄 ORM 的旅程
以上幾點(diǎn)都是理論上的。我們的旅程從我們從 ORM Gorm 開(kāi)始感受到的實(shí)際痛苦和問(wèn)題開(kāi)始。
問(wèn)題 1:封裝的 API
更新數(shù)據(jù)庫(kù)中的一些記錄是一個(gè)早期的臉面時(shí)刻。執(zhí)行此操作的 SQL 是:
UPDATE <table>
SET <values>
WHERE <conditions>
但是 Gorm API 改變了接受值和條件的順序。
// 預(yù)期
db.Model(<table>).Where(<conditions>).Updates(<values>)// 更新整個(gè)表,忽略條件
db.Model(<table>).Updates(<values>).Where(<conditions>)
我們經(jīng)歷了一個(gè)艱難的過(guò)程,即犯錯(cuò)會(huì)帶來(lái)災(zāi)難性的后果。ORM 的 API 是可鏈接的,但它并不完全是惰性的。某些聲明正在敲定,包括更新。如果您顛倒順序,更新將應(yīng)用,但 Where 條件不生效,這意味著您已更新模型中的所有內(nèi)容。
現(xiàn)在你可以爭(zhēng)辯說(shuō) SQL 有這種倒退,而 ORM 只是對(duì) API 的真正含義進(jìn)行了更正。你可以說(shuō)我們的團(tuán)隊(duì)?wèi)?yīng)該更清楚。或者 API 可以有更好的文檔記錄。無(wú)論爭(zhēng)論如何,結(jié)果都是一樣的:我們度過(guò)了糟糕的一天。更廣泛的觀(guān)點(diǎn)是:ORM 實(shí)際上是 SQL 的包裝器。
在我職業(yè)生涯的早期,一位工程經(jīng)理告訴我要對(duì)包裝器持懷疑態(tài)度,因?yàn)樗鼈冎粫?huì)給工作量增加認(rèn)知負(fù)擔(dān)。我反駁說(shuō),根據(jù)合乎邏輯的結(jié)論,他會(huì)用匯編編寫(xiě)所有代碼。
兩個(gè)極端都不正確:抽象是關(guān)于實(shí)現(xiàn)平衡的行為。但隨著我職業(yè)生涯的發(fā)展,我將 ORM 置于不必要的包裝陣營(yíng)中。為什么我們要處理一個(gè)與幾乎所有開(kāi)發(fā)人員都接受過(guò)培訓(xùn)的超級(jí)穩(wěn)定和眾所周知的 ANSI SQL API 相反的中間件?每個(gè)開(kāi)發(fā)人員都知道如何使用 SQL 更新表中的記錄(或者可以輕松地通過(guò) Google 搜索),并且在出錯(cuò)時(shí)可能會(huì)出現(xiàn)錯(cuò)誤。不是每個(gè)開(kāi)發(fā)者,實(shí)際上是極少數(shù)的開(kāi)發(fā)者,都知道 Gorm 的抽象。對(duì)于您使用的任何 ORM,(在新員工中)也是如此;您將不斷地在堆棧的關(guān)鍵任務(wù)部分培訓(xùn)人員。
問(wèn)題2:性能和過(guò)多的內(nèi)存消耗
我們的后端在內(nèi)存容量有限的無(wú)服務(wù)器堆棧 (AWS Fargate) 上運(yùn)行。隨著時(shí)間的推移,我們不得不不斷增加實(shí)例的內(nèi)存容量,最終達(dá)到最大值,然后看著我們的容器死亡。隨著數(shù)據(jù)量的增長(zhǎng),我們看到容器的數(shù)量以某種線(xiàn)性方式增長(zhǎng)。人們會(huì)希望后端改為亞線(xiàn)性擴(kuò)展。
ORM 是一個(gè)自然的罪魁禍?zhǔn)祝汉苋菀卓闯鲈S多 ORM 將使用對(duì)象自省來(lái)構(gòu)建 SQL 語(yǔ)句、水合結(jié)果或兩者兼而有之。Gorm 的內(nèi)存占用是極端的,但遺憾的是并不少見(jiàn)。Bridge 在后端使用 Python 開(kāi)始,我們使用 Django 的 ORM 與存在類(lèi)似問(wèn)題的數(shù)據(jù)庫(kù)進(jìn)行交互。
直到我們最終將其從堆棧中刪除以給我們一個(gè)比較點(diǎn)時(shí),我們才意識(shí)到問(wèn)題的嚴(yán)重程度。詳細(xì)信息包含在下一節(jié)中,但作為預(yù)覽:我們將執(zhí)行性能提高了約 2 倍,并將內(nèi)存占用減少了近 10 倍。
問(wèn)題 3:了解我們的 I/O 配置文件
隨著時(shí)間的推移,我們注意到自己使用數(shù)據(jù)庫(kù)日志工具來(lái)了解我們自己的用例,并認(rèn)為這是不匹配的。我們?cè)?AWS 上設(shè)置了RDS Performance Insights并pganalyze來(lái)識(shí)別數(shù)據(jù)庫(kù)中的瓶頸。這些工具很早就證明了它們的價(jià)值,我們最終使用它們來(lái)了解我們與數(shù)據(jù)庫(kù)交互的方式。我們是否過(guò)度獲取列?我們是否正在運(yùn)行未索引的查詢(xún)?
當(dāng)然,這些問(wèn)題都有已知的、確定的答案。事實(shí)上,我們需要一個(gè)外部工具來(lái)闡明這個(gè)問(wèn)題,這是代碼中一個(gè)明顯的結(jié)構(gòu)缺陷。對(duì)我來(lái)說(shuō),潛在的問(wèn)題是 ORM 讓與數(shù)據(jù)庫(kù)的交互變得太容易了。代碼沒(méi)有集中或模塊化到代碼庫(kù)中的中間件層。相反,它自始至終都被意大利面條化了。了解我們的數(shù)據(jù)庫(kù)交互性需要對(duì)與業(yè)務(wù)邏輯有關(guān)的內(nèi)容進(jìn)行廣泛的代碼審計(jì)和審查,而不是讀寫(xiě)。
備擇方案
使用 ORM 的替代方案似乎相當(dāng)有限:使用低級(jí)數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序,在運(yùn)行時(shí)構(gòu)建 SQL 查詢(xún),然后自己將結(jié)果映射回對(duì)象。當(dāng)然,ORM 以自動(dòng)化的方式完成所有這些事情,因此走這條路線(xiàn)將對(duì)可維護(hù)性做出巨大犧牲。我們的團(tuán)隊(duì)得出結(jié)論(相當(dāng)容易且沒(méi)有太多決策),無(wú)論這里有什么好處,成本都太高了,無(wú)法考慮。
然而,還有第三條路線(xiàn):使用代碼生成器來(lái)自動(dòng)化這些步驟。我們將 Go 社區(qū)中的項(xiàng)目分為兩條線(xiàn):
- 在運(yùn)行時(shí)生成代碼的 SQL(例如:squirrel)
- 在編譯時(shí)生成應(yīng)用程序代碼(示例:jet,sqlc)
生成 SQL 代碼是一個(gè)有趣的想法,與生成應(yīng)用程序代碼相比,它需要更少的工具和承諾。然而,我們認(rèn)為這將是我們代碼可維護(hù)性的橫向移動(dòng)。SQL 生成將需要字符串插值,這意味著在應(yīng)用數(shù)據(jù)庫(kù)遷移時(shí)審核代碼,這是我們希望結(jié)束的一個(gè)勞動(dòng)密集型且耗費(fèi)精力的過(guò)程。
嬰兒在洗澡水里?
我們想了很久,想著要不要把嬰兒和洗澡水一起倒掉。也許問(wèn)題不在于 ORM 本身,而在于代碼優(yōu)先子集。在 Go 社區(qū)中, sqlboiler是一個(gè)有趣的項(xiàng)目,它從您的 DDL 生成模型定義。
我們決定不使用這個(gè)項(xiàng)目,原因如下:
- 有太多的代碼生成這樣的事情。生成的代碼需要靈活的配置來(lái)控制輸出,這是一條好走的路。一方面,您不希望交換代碼以進(jìn)行配置,并在代碼庫(kù)中放置大量的 yaml 或 toml 文件,這些文件需要自己的一組維護(hù)問(wèn)題。在另一個(gè)極端,如果您想要控制或自定義配置中未公開(kāi)的生成代碼的某些內(nèi)容,那么您就不走運(yùn)了。
- Sqlboiler 很大程度上受到 Active Record 的啟發(fā),我們覺(jué)得它過(guò)于抽象了數(shù)據(jù)庫(kù)。我們?cè)噲D擁抱數(shù)據(jù)庫(kù),因?yàn)閺奈幕现v,我們是一個(gè)以數(shù)據(jù)為中心的組織,希望我們的數(shù)據(jù)庫(kù)在我們的應(yīng)用程序和 API 中更加透明。
選擇代碼生成器
我們仔細(xì)研究了兩個(gè)代碼生成器:jet和sqlc,最終選擇了 sqlc。使用 jet,您可以在應(yīng)用程序中將 SQL 作為 DSL 編寫(xiě)。但因?yàn)樗纱a,所以它比 squirrel 等運(yùn)行時(shí) SQL 生成器提供的功能更進(jìn)一步。模型和字段是一流的可引用類(lèi)型,而不是需要字符串插值,這避免了當(dāng)您想要進(jìn)行更改時(shí)需要在審計(jì)過(guò)程中通過(guò)代碼進(jìn)行 grep。
更吸引人的是,它提供了一種在數(shù)據(jù)庫(kù)中聚合或反規(guī)范化數(shù)據(jù)的方法。ORM 的目標(biāo)是使關(guān)系遍歷變得容易,而 Jet 的目標(biāo)是在完整且類(lèi)型良好的結(jié)構(gòu)中提供數(shù)據(jù)包,清楚地宣傳其中的可用內(nèi)容。這是一個(gè)例子:
stmt := SELECT(
Actor.ActorID, Actor.FirstName, Actor.LastName, Actor.LastUpdate,
Film.AllColumns,
Language.AllColumns,
Category.AllColumns,
).FROM(
Actor.INNER_JOIN
(FilmActor, Actor.ActorID.EQ(FilmActor .ActorID)).
INNER_JOIN(Film, Film.FilmID.EQ(FilmActor.FilmID)).
INNER_JOIN(Language, Language.LanguageID.EQ(Film.LanguageID)).
INNER_JOIN(FilmCategory, FilmCategory.FilmID.EQ(Film.FilmID) )).
INNER_JOIN(Category, Category.CategoryID.EQ(FilmCategory.CategoryID)),
).WHERE(
Language.Name.EQ(String("English")).
AND(Category.Name.NOT_EQ(String("Action" ))).
AND(Film.Length.GT(Int(180))),
).ORDER_BY(
Actor.ActorID.ASC(),
Film.FilmID.ASC(),
)var dest []struct {
model.Actor 電影 []struct {
model.Film 語(yǔ)言模型.語(yǔ)言
類(lèi)別 []model.Category
}
}// 執(zhí)行查詢(xún)并存儲(chǔ)結(jié)果
err = stmt.Query(db, &dest)
這里有很多數(shù)據(jù)聚合。正在建立的應(yīng)用程序端模型是一個(gè)演員,其中包含他們參與過(guò)的所有電影、電影所使用的語(yǔ)言以及其所屬的類(lèi)別。
我們最初被這個(gè)設(shè)計(jì)所吸引,但在嘗試了一下之后感覺(jué)不太對(duì)勁。在這個(gè)例子中,查詢(xún)?cè)趹?yīng)用程序中驅(qū)動(dòng)數(shù)據(jù)模型,而不是相反,我們擔(dān)心這種方法會(huì)導(dǎo)致大量丟棄的聚合模型。我們的目標(biāo)是推廣具有大量業(yè)務(wù)邏輯和可變性的可重用模型,這些模型在其類(lèi)型的方法中捕獲。
此外,我們的偏好是將 SQL 完全從代碼中移出。這里的問(wèn)題是任何開(kāi)發(fā)人員都可以隨心所欲地查詢(xún)數(shù)據(jù)庫(kù)。雖然這是最初的生產(chǎn)力勝利,但其代價(jià)是代碼和運(yùn)行時(shí)性能的長(zhǎng)期可維護(hù)性。如果開(kāi)發(fā)人員在不使用索引的情況下以次優(yōu)方式查詢(xún)數(shù)據(jù)庫(kù)怎么辦?隨著數(shù)據(jù)模型變得更大和復(fù)雜,這種風(fēng)險(xiǎn)很高,因?yàn)樗菑? SQL 中刪除的一個(gè)步驟。雖然 DSL 很受歡迎,但我們?nèi)匀挥X(jué)得它最終像包裝器一樣。
答案:sqlc
我們決定使用sqlc,一個(gè)可配置的可選 sql 編譯器。這種方法引起了我們的共鳴;我們喜歡它不會(huì)生成您不需要的東西,并且生成的代碼可以根據(jù)我們定義的類(lèi)型和標(biāo)簽進(jìn)行定制。它使代碼感覺(jué)像我們的,同時(shí)提供了遷移我們當(dāng)前實(shí)現(xiàn)的明顯路徑。我將在以后的文章中詳細(xì)說(shuō)明我們?nèi)绾巫? sqlc 為我們工作。
刪除 ORM 的好處
這個(gè)項(xiàng)目是一項(xiàng)艱巨的任務(wù),不僅需要我們的開(kāi)發(fā)人員,還需要我們的產(chǎn)品團(tuán)隊(duì)和整個(gè)公司的承諾。我們遇到了功能凍結(jié)、將 ORM 與生成的代碼并行運(yùn)行的問(wèn)題,并且必須仔細(xì)規(guī)劃我們的遷移和部署路徑。所有這一切都是在一家資源有限的小型(但不斷發(fā)展的)公司的背景下進(jìn)行的。有了所有這些成本,收益最好是顯著的,而且確實(shí)如此。其中,我們?cè)诤蠖诉\(yùn)行時(shí)實(shí)現(xiàn)了更好的性能和可擴(kuò)展性,更好的代碼庫(kù)可維護(hù)性,更少依賴(lài)數(shù)據(jù)庫(kù)日志來(lái)理解我們的數(shù)據(jù) I/O 配置文件,更云原生的實(shí)現(xiàn)和后端數(shù)據(jù)模型的透明度我們所有的開(kāi)發(fā)人員,無(wú)論他們每天是否接近堆棧中的數(shù)據(jù)庫(kù)。
性能和規(guī)模
如果您的 ORM 是動(dòng)態(tài)的,沒(méi)有使用生成的代碼或使用泛型類(lèi)型或接口,那么它可能在幕后進(jìn)行了某種程度的反射。在我們的例子中,Gorm 大量使用反射,因?yàn)? Go 不支持泛型,而且 Gorm 沒(méi)有定義很多接口,除了要求您聲明對(duì)應(yīng)于應(yīng)用程序模型的表名。因此,我們期望在這里獲得巨大的收益,但是當(dāng)我們開(kāi)始對(duì)我們的系統(tǒng)進(jìn)行基準(zhǔn)測(cè)試時(shí),我們高興地印象深刻。
性能是關(guān)于實(shí)現(xiàn)低運(yùn)行時(shí)執(zhí)行。我們通過(guò)識(shí)別后端中典型的各種工作負(fù)載來(lái)對(duì)結(jié)果進(jìn)行基準(zhǔn)測(cè)試,這可能是因?yàn)?API 正在執(zhí)行它們,也可能是由于脫機(jī)或批處理進(jìn)程導(dǎo)致了對(duì)數(shù)據(jù)庫(kù)的大量 I/O。在下圖中,我們?cè)跈M軸上有用例;藍(lán)色表示我們的 sqlc 驅(qū)動(dòng)的數(shù)據(jù)交互層,紅色表示我們當(dāng)前使用 Gorm ORM 的延遲。越低越好。
在沒(méi)有 ORM 的情況下,運(yùn)行時(shí)性能提高了 52%
在我們的工作負(fù)載中,我們正在享受大約 2 倍的執(zhí)行性能加速。令人高興的是,當(dāng)工作負(fù)載獲取更多數(shù)據(jù)時(shí),這個(gè)數(shù)字往往會(huì)更高。
可擴(kuò)展性是指消耗盡可能少的內(nèi)存,這對(duì)我們來(lái)說(shuō)尤其重要,因?yàn)槲覀冊(cè)跓o(wú)服務(wù)器后端 (AWS Fargate) 上運(yùn)行所有工作負(fù)載,因此我們更適合橫向擴(kuò)展而不是向上擴(kuò)展。我們?cè)诿總€(gè)實(shí)例上使用的內(nèi)存越少,意味著需要上線(xiàn)的實(shí)例就越少才能達(dá)到結(jié)果,這意味著成本更低,整體使用率更高。換句話(huà)說(shuō),如果您需要的實(shí)例數(shù)量是當(dāng)前使用數(shù)量的一半(預(yù)算內(nèi)),您應(yīng)該能夠處理雙倍的數(shù)據(jù)量,而無(wú)需與您的 CFO 交談。
沒(méi)有 ORM 的內(nèi)存消耗減少了 78%
我們平均減少了 78% 的內(nèi)存消耗?,F(xiàn)在你可能會(huì)爭(zhēng)辯說(shuō),也許 Gorm 在這里做的事情效率太低了,而其他 ORM 可能會(huì)更好,但從根本上說(shuō),大多數(shù)映射器都需要類(lèi)型自省,這將導(dǎo)致糟糕的內(nèi)存配置文件。
這兩項(xiàng)改進(jìn)都是由每個(gè)操作需要發(fā)生的分配數(shù)量減少驅(qū)動(dòng)的,我們已經(jīng)將其基準(zhǔn)為另一個(gè) 80% 的下降:
在沒(méi)有 ORM 的情況下,每個(gè)操作的分配量減少了 80%
代碼可維護(hù)性
我認(rèn)為所有與數(shù)據(jù)層交互的代碼都是中間件。當(dāng)然,如果您使用的是 ORM,您可能沒(méi)有將這個(gè)中間件顯式地打包到一個(gè)包或一組函數(shù)中,我認(rèn)為這會(huì)讓您的情況變得更糟:中間件仍然存在,但它不是孤立的。相反,數(shù)據(jù)庫(kù)交互性在整個(gè)代碼庫(kù)中被意大利面條化了。
當(dāng)我們想要檢索、更新、創(chuàng)建或刪除數(shù)據(jù)時(shí),我們會(huì)調(diào)用為我們執(zhí)行此操作的函數(shù):
q.GetAccounts(ctx, ids)// 更復(fù)雜的查詢(xún)采用生成的 Params 類(lèi)型
q.GetAccountsPage(ctx, db.GetAccountsPageParams{ })
我們的端點(diǎn)甚至不這樣做;他們通過(guò)調(diào)用接口抽象出細(xì)節(jié):
結(jié)果,錯(cuò)誤:= fetch.Page(ctx, fetch.PageParams{
Fetcher: accounts.Fetcher{},
})
了解我們的數(shù)據(jù) I/O 配置文件
當(dāng)您擁有 ORM 時(shí),您正在邀請(qǐng)組織中的所有軟件開(kāi)發(fā)人員以可能無(wú)法解釋的方式訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)。盡管盡最大努力培訓(xùn)您的團(tuán)隊(duì)了解哪些索引可用或設(shè)置 DBA 角色,但最終您將擁有無(wú)法在沒(méi)有代碼審查的情況下解釋的數(shù)據(jù)庫(kù)交互代碼。這不可避免地導(dǎo)致人們轉(zhuǎn)向數(shù)據(jù)庫(kù)日志和監(jiān)控解決方案,以了解如何訪(fǎng)問(wèn)數(shù)據(jù)庫(kù)。這些工具對(duì)于任何審查運(yùn)行時(shí)性能和實(shí)現(xiàn) SLA 的流程都是受歡迎的補(bǔ)充,但如果您使用它們來(lái)了解如何訪(fǎng)問(wèn)您的數(shù)據(jù)庫(kù),那就太遲了。
我們?nèi)匀皇褂肦DS Performance Insights和pganalyze等工具,但我們不再依賴(lài)它們來(lái)了解一般配置文件,或者擔(dān)心我們是否正在使用索引。這項(xiàng)工作已轉(zhuǎn)移到我們的集中存儲(chǔ)庫(kù),它充當(dāng)我們所有數(shù)據(jù)庫(kù) I/O 的中間件,我們簡(jiǎn)稱(chēng)為數(shù)據(jù)存儲(chǔ)庫(kù)。
它不是沒(méi)有進(jìn)程,但現(xiàn)在它是一個(gè)托管進(jìn)程。當(dāng)應(yīng)用程序開(kāi)發(fā)人員需要一個(gè)新查詢(xún)時(shí),她需要在數(shù)據(jù)倉(cāng)庫(kù)中打開(kāi)一個(gè) PR,該 PR 將附帶代碼審查,人們可以在其中詢(xún)問(wèn)是否正在使用索引或事務(wù)。誠(chéng)然,這樣的代碼審查標(biāo)準(zhǔn)應(yīng)該適用于所有存儲(chǔ)庫(kù),但數(shù)據(jù)庫(kù) I/O 將只是下游應(yīng)用程序中的一個(gè)要點(diǎn)。我們的數(shù)據(jù)倉(cāng)庫(kù)只關(guān)注一件事,而且只關(guān)注一件事:托管數(shù)據(jù)庫(kù)交互。此外,事后進(jìn)行代碼審計(jì)也很容易。DDL 和 SQL 查詢(xún)都是并排的,因此很容易知道查詢(xún)是否正確地使用了索引。
更多云原生實(shí)現(xiàn)
您的里程可能會(huì)有所不同,但我們使用的兩個(gè) ORM(Gorm 和 Django)都包裝了數(shù)據(jù)庫(kù)連接,導(dǎo)致了兩個(gè)問(wèn)題。首先,在這兩種情況下,包裝對(duì)象暴露的功能都少于底層驅(qū)動(dòng)程序中可用的功能。隨著數(shù)據(jù)庫(kù)和驅(qū)動(dòng)程序更新以滿(mǎn)足特定需求,這可能會(huì)變得非常令人沮喪。
其次,特別是在 Django 的情況下,它讓我們遠(yuǎn)離了云原生設(shè)計(jì)。我們特別努力的一個(gè)方面是從我們的 Lambda 函數(shù)中訪(fǎng)問(wèn)數(shù)據(jù)。諸如 Lambda 之類(lèi)的函數(shù)即服務(wù)平臺(tái)將希望您將數(shù)據(jù)庫(kù)連接定義為全局變量,以便它是可凍結(jié)的。這個(gè)任務(wù)在 Django 上基本上是不可能的。盡管我們?cè)?Gorm 中解決這個(gè)問(wèn)題的麻煩較少,但我們?cè)讷@得我們想要的連接池特性方面遇到了其他問(wèn)題,即使在云中長(zhǎng)期存在的計(jì)算層上也是如此。
最終能否實(shí)現(xiàn)云原生設(shè)計(jì)取決于您選擇的數(shù)據(jù)庫(kù)驅(qū)動(dòng)程序,并且您需要確保您的 ORM 支持該驅(qū)動(dòng)程序。我們很幸運(yùn)使用了 Postgres,更幸運(yùn)的是 Go 社區(qū)有專(zhuān)門(mén)的驅(qū)動(dòng)程序:jackc/pgx。能夠在沒(méi)有 ORM 的情況下直接使用此驅(qū)動(dòng)程序,使我們?cè)谠圃O(shè)計(jì)方面具有更大的靈活性,并能夠利用 Postgres 特定的功能,這些功能通常被其他優(yōu)先考慮廣泛的跨數(shù)據(jù)庫(kù)支持的驅(qū)動(dòng)程序所遺漏。
數(shù)據(jù)模型透明度
最后,也許是最重要的一點(diǎn),放棄我們的 ORM 通過(guò)提高數(shù)據(jù)模型的透明度改變了我們的工程文化,使其更加以數(shù)據(jù)為中心。Bridge 是一家數(shù)據(jù)處理公司。我們?yōu)樽?cè)投資顧問(wèn)、企業(yè)和其他金融科技平臺(tái)做標(biāo)準(zhǔn)化和豐富金融數(shù)據(jù)的工作。
我們重視數(shù)據(jù)的完整性、準(zhǔn)確性和一致性以實(shí)現(xiàn)這些目標(biāo),除非每個(gè)人都認(rèn)為他們了解數(shù)據(jù)模型,否則我們無(wú)法做到這一點(diǎn)。許多 ORM 在哲學(xué)上是圍繞在開(kāi)發(fā)過(guò)程中隱藏或抽象數(shù)據(jù)庫(kù)而構(gòu)建的,這最終將導(dǎo)致您的團(tuán)隊(duì)密切關(guān)注“O”并降低“R”的優(yōu)先級(jí)。并且“O”被鎖定在單個(gè)存儲(chǔ)庫(kù)中,任何一個(gè)人都可能知道也可能不知道。但是每個(gè)人都可以了解數(shù)據(jù)庫(kù)的結(jié)構(gòu)安排:組織成模式、DDL、E/R 圖等。
對(duì)我們來(lái)說(shuō),我們的數(shù)據(jù)庫(kù)不僅僅是我們讀取和寫(xiě)入的信息容器。這是我們思想的表達(dá);我們?nèi)绾魏?jiǎn)化和模擬行業(yè)挑戰(zhàn)的復(fù)雜性。刪除 ORM 將所有這些細(xì)節(jié)置于人們腦海中的前端中心,創(chuàng)造了對(duì)數(shù)據(jù)模型和 Postgres 的更多所有權(quán),減少了“越界”的心態(tài)。這也許是最好的收獲。