)
我们经历了一个艰难的过程,即犯错会带来灾难性的后果。ORM 的 API 是可链接的,但它并不完全是惰性的。某些声明正在敲定,包括更新。如果您颠倒顺序,更新将应用,但 Where 条件不生效,这意味着您已更新模型中的所有内容。
现在你可以争辩说 SQL 有这种倒退,而 ORM 只是对 API 的真正含义进行了更正。你可以说我们的团队应该更清楚。或者 API 可以有更好的文档记录。无论争论如何,结果都是一样的:我们度过了糟糕的一天。更广泛的观点是:ORM 实际上是 SQL 的包装器。
在我职业生涯的早期,一位工程经理告诉我要对包装器持怀疑态度,因为它们只会给工作量增加认知负担。我反驳说,根据合乎逻辑的结论,他会用汇编编写所有代码。
两个极端都不正确:抽象是关于实现平衡的行为。但随着我职业生涯的发展,我将 ORM 置于不必要的包装阵营中。为什么我们要处理一个与几乎所有开发人员都接受过培训的超级稳定和众所周知的 ANSI SQL API 相反的中间件?每个开发人员都知道如何使用 SQL 更新表中的记录(或者可以轻松地通过 Google 搜索),并且在出错时可能会出现错误。不是每个开发者,实际上是极少数的开发者,都知道 Gorm 的抽象。对于您使用的任何 ORM,(在新员工中)也是如此;您将不断地在堆栈的关键任务部分培训人员。
问题2:性能和过多的内存消耗
我们的后端在内存容量有限的无服务器堆栈 (AWS Fargate) 上运行。随着时间的推移,我们不得不不断增加实例的内存容量,最终达到最大值,然后看着我们的容器死亡。随着数据量的增长,我们看到容器的数量以某种线性方式增长。人们会希望后端改为亚线性扩展。
ORM 是一个自然的罪魁祸首:很容易看出许多 ORM 将使用对象自省来构建 SQL 语句、水合结果或两者兼而有之。Gorm 的内存占用是极端的,但遗憾的是并不少见。Bridge 在后端使用 Python 开始,我们使用 Django 的 ORM 与存在类似问题的数据库进行交互。
直到我们最终将其从堆栈中删除以给我们一个比较点时,我们才意识到问题的严重程度。详细信息包含在下一节中,但作为预览:我们将执行性能提高了约 2 倍,并将内存占用减少了近 10 倍。
问题 3:了解我们的 I/O 配置文件
随着时间的推移,我们注意到自己使用数据库日志工具来了解我们自己的用例,并认为这是不匹配的。我们在 AWS 上设置了RDS Performance Insights并pganalyze来识别数据库中的瓶颈。这些工具很早就证明了它们的价值,我们最终使用它们来了解我们与数据库交互的方式。我们是否过度获取列?我们是否正在运行未索引的查询?
当然,这些问题都有已知的、确定的答案。事实上,我们需要一个外部工具来阐明这个问题,这是代码中一个明显的结构缺陷。对我来说,潜在的问题是 ORM 让与数据库的交互变得太容易了。代码没有集中或模块化到代码库中的中间件层。相反,它自始至终都被意大利面条化了。了解我们的数据库交互性需要对与业务逻辑有关的内容进行广泛的代码审计和审查,而不是读写。
备择方案
使用 ORM 的替代方案似乎相当有限:使用低级数据库驱动程序,在运行时构建 SQL 查询,然后自己将结果映射回对象。当然,ORM 以自动化的方式完成所有这些事情,因此走这条路线将对可维护性做出巨大牺牲。我们的团队得出结论(相当容易且没有太多决策),无论这里有什么好处,成本都太高了,无法考虑。
然而,还有第三条路线:使用代码生成器来自动化这些步骤。我们将 Go 社区中的项目分为两条线:
- 在运行时生成代码的 SQL(例如:squirrel)
- 在编译时生成应用程序代码(示例:jet,sqlc)
生成 SQL 代码是一个有趣的想法,与生成应用程序代码相比,它需要更少的工具和承诺。然而,我们认为这将是我们代码可维护性的横向移动。SQL 生成将需要字符串插值,这意味着在应用数据库迁移时审核代码,这是我们希望结束的一个劳动密集型且耗费精力的过程。
婴儿在洗澡水里?
我们想了很久,想着要不要把婴儿和洗澡水一起倒掉。也许问题不在于 ORM 本身,而在于代码优先子集。在 Go 社区中, sqlboiler是一个有趣的项目,它从您的 DDL 生成模型定义。
我们决定不使用这个项目,原因如下:
- 有太多的代码生成这样的事情。生成的代码需要灵活的配置来控制输出,这是一条好走的路。一方面,您不希望交换代码以进行配置,并在代码库中放置大量的 yaml 或 toml 文件,这些文件需要自己的一组维护问题。在另一个极端,如果您想要控制或自定义配置中未公开的生成代码的某些内容,那么您就不走运了。
- Sqlboiler 很大程度上受到 Active Record 的启发,我们觉得它过于抽象了数据库。我们试图拥抱数据库,因为从文化上讲,我们是一个以数据为中心的组织,希望我们的数据库在我们的应用程序和 API 中更加透明。
选择代码生成器
我们仔细研究了两个代码生成器:jet和sqlc,最终选择了 sqlc。使用 jet,您可以在应用程序中将 SQL 作为 DSL 编写。但因为它生成代码,所以它比 squirrel 等运行时 SQL 生成器提供的功能更进一步。模型和字段是一流的可引用类型,而不是需要字符串插值,这避免了当您想要进行更改时需要在审计过程中通过代码进行 grep。
更吸引人的是,它提供了一种在数据库中聚合或反规范化数据的方法。ORM 的目标是使关系遍历变得容易,而 Jet 的目标是在完整且类型良好的结构中提供数据包,清楚地宣传其中的可用内容。这是一个例子:
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 语言模型.语言
类别 []model.Category
}
}// 执行查询并存储结果
err = stmt.Query(db, &dest)
这里有很多数据聚合。正在建立的应用程序端模型是一个演员,其中包含他们参与过的所有电影、电影所使用的语言以及其所属的类别。
我们最初被这个设计所吸引,但在尝试了一下之后感觉不太对劲。在这个例子中,查询在应用程序中驱动数据模型,而不是相反,我们担心这种方法会导致大量丢弃的聚合模型。我们的目标是推广具有大量业务逻辑和可变性的可重用模型,这些模型在其类型的方法中捕获。
此外,我们的偏好是将 SQL 完全从代码中移出。这里的问题是任何开发人员都可以随心所欲地查询数据库。虽然这是最初的生产力胜利,但其代价是代码和运行时性能的长期可维护性。如果开发人员在不使用索引的情况下以次优方式查询数据库怎么办?随着数据模型变得更大和复杂,这种风险很高,因为它是从 SQL 中删除的一个步骤。虽然 DSL 很受欢迎,但我们仍然觉得它最终像包装器一样。
答案:sqlc
我们决定使用sqlc,一个可配置的可选 sql 编译器。这种方法引起了我们的共鸣;我们喜欢它不会生成您不需要的东西,并且生成的代码可以根据我们定义的类型和标签进行定制。它使代码感觉像我们的,同时提供了迁移我们当前实现的明显路径。我将在以后的文章中详细说明我们如何让 sqlc 为我们工作。
删除 ORM 的好处
这个项目是一项艰巨的任务,不仅需要我们的开发人员,还需要我们的产品团队和整个公司的承诺。我们遇到了功能冻结、将 ORM 与生成的代码并行运行的问题,并且必须仔细规划我们的迁移和部署路径。所有这一切都是在一家资源有限的小型(但不断发展的)公司的背景下进行的。有了所有这些成本,收益最好是显着的,而且确实如此。其中,我们在后端运行时实现了更好的性能和可扩展性,更好的代码库可维护性,更少依赖数据库日志来理解我们的数据 I/O 配置文件,更云原生的实现和后端数据模型的透明度我们所有的开发人员,无论他们每天是否接近堆栈中的数据库。
性能和规模
如果您的 ORM 是动态的,没有使用生成的代码或使用泛型类型或接口,那么它可能在幕后进行了某种程度的反射。在我们的例子中,Gorm 大量使用反射,因为 Go 不支持泛型,而且 Gorm 没有定义很多接口,除了要求您声明对应于应用程序模型的表名。因此,我们期望在这里获得巨大的收益,但是当我们开始对我们的系统进行基准测试时,我们高兴地印象深刻。
性能是关于实现低运行时执行。我们通过识别后端中典型的各种工作负载来对结果进行基准测试,这可能是因为 API 正在执行它们,也可能是由于脱机或批处理进程导致了对数据库的大量 I/O。在下图中,我们在横轴上有用例;蓝色表示我们的 sqlc 驱动的数据交互层,红色表示我们当前使用 Gorm ORM 的延迟。越低越好。
在没有 ORM 的情况下,运行时性能提高了 52%
在我们的工作负载中,我们正在享受大约 2 倍的执行性能加速。令人高兴的是,当工作负载获取更多数据时,这个数字往往会更高。
可扩展性是指消耗尽可能少的内存,这对我们来说尤其重要,因为我们在无服务器后端 (AWS Fargate) 上运行所有工作负载,因此我们更适合横向扩展而不是向上扩展。我们在每个实例上使用的内存越少,意味着需要上线的实例就越少才能达到结果,这意味着成本更低,整体使用率更高。换句话说,如果您需要的实例数量是当前使用数量的一半(预算内),您应该能够处理双倍的数据量,而无需与您的 CFO 交谈。
没有 ORM 的内存消耗减少了 78%
我们平均减少了 78% 的内存消耗。现在你可能会争辩说,也许 Gorm 在这里做的事情效率太低了,而其他 ORM 可能会更好,但从根本上说,大多数映射器都需要类型自省,这将导致糟糕的内存配置文件。
这两项改进都是由每个操作需要发生的分配数量减少驱动的,我们已经将其基准为另一个 80% 的下降:
在没有 ORM 的情况下,每个操作的分配量减少了 80%
代码可维护性
我认为所有与数据层交互的代码都是中间件。当然,如果您使用的是 ORM,您可能没有将这个中间件显式地打包到一个包或一组函数中,我认为这会让您的情况变得更糟:中间件仍然存在,但它不是孤立的。相反,数据库交互性在整个代码库中被意大利面条化了。
当我们想要检索、更新、创建或删除数据时,我们会调用为我们执行此操作的函数:
q.GetAccounts(ctx, ids)// 更复杂的查询采用生成的 Params 类型
q.GetAccountsPage(ctx, db.GetAccountsPageParams{...})
我们的端点甚至不这样做;他们通过调用接口抽象出细节:
结果,错误:= fetch.Page(ctx, fetch.PageParams{
Fetcher: accounts.Fetcher{},
...
})
了解我们的数据 I/O 配置文件
当您拥有 ORM 时,您正在邀请组织中的所有软件开发人员以可能无法解释的方式访问数据库。尽管尽最大努力培训您的团队了解哪些索引可用或设置 DBA 角色,但最终您将拥有无法在没有代码审查的情况下解释的数据库交互代码。这不可避免地导致人们转向数据库日志和监控解决方案,以了解如何访问数据库。这些工具对于任何审查运行时性能和实现 SLA 的流程都是受欢迎的补充,但如果您使用它们来了解如何访问您的数据库,那就太迟了。
我们仍然使用RDS Performance Insights和pganalyze等工具,但我们不再依赖它们来了解一般配置文件,或者担心我们是否正在使用索引。这项工作已转移到我们的集中存储库,它充当我们所有数据库 I/O 的中间件,我们简称为数据存储库。
它不是没有进程,但现在它是一个托管进程。当应用程序开发人员需要一个新查询时,她需要在数据仓库中打开一个 PR,该 PR 将附带代码审查,人们可以在其中询问是否正在使用索引或事务。诚然,这样的代码审查标准应该适用于所有存储库,但数据库 I/O 将只是下游应用程序中的一个要点。我们的数据仓库只关注一件事,而且只关注一件事:托管数据库交互。此外,事后进行代码审计也很容易。DDL 和 SQL 查询都是并排的,因此很容易知道查询是否正确地使用了索引。
更多云原生实现
您的里程可能会有所不同,但我们使用的两个 ORM(Gorm 和 Django)都包装了数据库连接,导致了两个问题。首先,在这两种情况下,包装对象暴露的功能都少于底层驱动程序中可用的功能。随着数据库和驱动程序更新以满足特定需求,这可能会变得非常令人沮丧。
其次,特别是在 Django 的情况下,它让我们远离了云原生设计。我们特别努力的一个方面是从我们的 Lambda 函数中访问数据。诸如 Lambda 之类的函数即服务平台将希望您将数据库连接定义为全局变量,以便它是可冻结的。这个任务在 Django 上基本上是不可能的。尽管我们在 Gorm 中解决这个问题的麻烦较少,但我们在获得我们想要的连接池特性方面遇到了其他问题,即使在云中长期存在的计算层上也是如此。
最终能否实现云原生设计取决于您选择的数据库驱动程序,并且您需要确保您的 ORM 支持该驱动程序。我们很幸运使用了 Postgres,更幸运的是 Go 社区有专门的驱动程序:jackc/pgx。能够在没有 ORM 的情况下直接使用此驱动程序,使我们在云原生设计方面具有更大的灵活性,并能够利用 Postgres 特定的功能,这些功能通常被其他优先考虑广泛的跨数据库支持的驱动程序所遗漏。
数据模型透明度
最后,也许是最重要的一点,放弃我们的 ORM 通过提高数据模型的透明度改变了我们的工程文化,使其更加以数据为中心。Bridge 是一家数据处理公司。我们为注册投资顾问、企业和其他金融科技平台做标准化和丰富金融数据的工作。
我们重视数据的完整性、准确性和一致性以实现这些目标,除非每个人都认为他们了解数据模型,否则我们无法做到这一点。许多 ORM 在哲学上是围绕在开发过程中隐藏或抽象数据库而构建的,这最终将导致您的团队密切关注“O”并降低“R”的优先级。并且“O”被锁定在单个存储库中,任何一个人都可能知道也可能不知道。但是每个人都可以了解数据库的结构安排:组织成模式、DDL、E/R 图等。
对我们来说,我们的数据库不仅仅是我们读取和写入的信息容器。这是我们思想的表达;我们如何简化和模拟行业挑战的复杂性。删除 ORM 将所有这些细节置于人们脑海中的前端中心,创造了对数据模型和 Postgres 的更多所有权,减少了“越界”的心态。这也许是最好的收获。
当前文章:Golang中的ORM编程杂谈
分享链接:http://www.shufengxianlan.com/qtweb/news8/908.html
网站建设、网络推广公司-创新互联,是专注品牌与效果的网站制作,网络营销seo公司;服务项目有等
广告
声明:本网站发布的内容(图片、视频和文字)以用户投稿、用户转载内容为主,如果涉及侵权请尽快告知,我们将会在第一时间删除。文章观点不代表本网站立场,如需处理请联系客服。电话:028-86922220;邮箱:631063699@qq.com。内容未经允许不得转载,或转载时需注明来源:
创新互联