本篇内容是根据2019年8月份Graph databases[1]音频录制内容的整理与翻译,
Mat、Johnny 和 Jaana[2] 与 Francesc Campoy[3] 一起讨论图形数据库。我们提出了所有重要的问题---
什么是图形数据库(以及我们为什么需要它们)?它们与关系数据库相比有哪些优势?图形数据库是否更擅长回答您未曾预料到的问题?数据是如何构造的?查询如何工作?它们擅长解决哪些问题?它们不适合解决哪些问题?而且……由于 Francesc 正处于困境之中,我们向他询问了 Just for Func[4] 以及它何时回归。
过程中为符合中文惯用表达有适当删改, 版权归原作者所有.
Mat Ryer: 你好,欢迎收听 Go Time。我是 Mat Ryer,今天我们要讨论的是图数据库。和我一同主持的是 Johnny Boursiquot。你好,Johnny!
Johnny Boursiquot: 你好,Mat。
Mat Ryer: 我们今天还有 Jaana B. Dogan。你好,Jaana。
Jaana Dogan: 你好!
Mat Ryer: 你好吗?
Jaana Dogan: 很好!
Mat Ryer: 欢迎回来,好久没见了。
Jaana Dogan: 的确有一段时间了...
Mat Ryer: 是啊,很高兴你能回到节目。今天我们还有一位特别的嘉宾,那就是 Francesc Campoy。你好,Francesc!
Francesc Campoy: 嘿,大家好!
Mat Ryer: 你怎么样?
Francesc Campoy: 很不错。你刚刚的发音还挺标准的。 [笑]
Mat Ryer: 谢谢你,这对我来说很重要。好吧,我先问一个每次提到你都会有人问的问题---
这是来自 Slack 的 Pontus 的提问:“JustForFunc 什么时候回归?”
Francesc Campoy: 呃,总有一天吧...?[笑声]
Mat Ryer: 好的,酷。
Francesc Campoy: 是的,总会回归的。也许明年吧。可能。
Jaana Dogan: 你需要帮助吗?
Francesc Campoy: 是的,我需要更多时间。这是我唯一需要的。[笑] 不过,我刚搬进了新家,一切都在安顿中,我会有一个小工作室,所以我应该能重新开始。现在我只需要一点精力。所以,可能再休息几个月,然后再回来。
Mat Ryer: 很好。对于那些不太了解的人来说,Francesc 之前做了一个非常棒的视频系列叫做 JustForFunc。我觉得这个名字不错,不过我建议的名字是“Go Func Yourself” [笑声],但那被拒绝了。你不喜欢那个名字。
Francesc Campoy: 是的... 当时我还在 Google 工作,不确定那名字会不会被接受。
Johnny Boursiquot: 哦,Func 那个... [笑声]
Mat Ryer: 那也被拒绝了... 好了,今天我们要讨论的是图数据库,这是一个非常有趣的领域,感觉还很新。我们稍后会深入探讨。但也许我们可以先从 Francesc 介绍一下他现在的工作开始,然后我们再聊什么是图数据库。
Francesc Campoy: 好的,我可以简单讲一下我是怎么开始接触图数据库的。在我离开 Google 后,我加入了一家公司 source{d}[5],在那里我开始做代码分析... 当你思考代码解析时,你最后会得到一棵语法树... 而一旦你开始在上面做注释,比如“这个函数调用了那里”或者“这个变量是在这里声明的”或者“这是一个类型”等等,你就会开始形成一个图。然后你会想,你要怎样存储这些信息,于是你会去看关系型数据库,但你会发现这并不适合,操作起来非常困难,一旦你开始添加更多东西,它很快就会崩溃;它的结构过于严格。
所以你转而去看 NoSQL 数据库,但是你会遇到问题,比如“你要复制多少数据?为了性能你是否要多次复制相同的信息,但这需要考虑一致性问题,或者你是否想将所有信息拆分成小块,但这样速度会非常慢?”这时我开始关注图数据库。你说得对,它听起来好像是个新东西。不过,你猜 Neo4j 有多老了?
Mat Ryer: 哦,这是个好问题。Johnny,你觉得呢?
Johnny Boursiquot: 我猜至少有十年了吧...
Francesc Campoy: 如果我没记错的话,它已经有19年历史了。
Mat Ryer: 哇!
Francesc Campoy: 是的,他们已经做了很久了。
Mat Ryer: 很快就能合法喝酒了。 [笑声]
Francesc Campoy: 嗯,在西班牙它已经可以合法喝酒了。 [笑声] 是的。所以这并不是一个完全新的技术,它是逐渐演变的,我开始注意到其实并没有太多纯粹的图数据库。而那些有图层的数据库有很多竞争者,但问题是以图的形式存储数据并不容易,这种方式带来了很多灵活性。因为它的理念是,你不需要像传统数据库那样去思考如何建模,而是可以像在白板上画图一样存储数据。你存储节点和关系,基本上就是这样。
一旦你需要开始思考“我要如何建模以适应我的数据库”,你就给开发者增加了复杂性。你需要考虑这些问题,而这些复杂性也是一种成本。人们往往没有意识到这一点,但这种复杂性意味着当你调试时,它会更难调试,因为你引入了额外的抽象层。
另一方面,从性能角度来看也是如此。如果你需要进行转换操作... 如果你考虑关系型数据库---
比如有人关注其他人---
如果我让你找出“我关注的所有人,然后他们的关注者,再然后他们的关注者”,你会进行很多层次的查询,这就是人们常说的 N+1 问题。你获取一些数据集,然后为其中的每一项再做一次查询,这个过程会不断重复。所以它无法很好地扩展。图数据库的理念就是为了解决这个问题。
我开始研究这个领域,并决定加入 Dgraph[6],部分原因是他们用 Go 编写,我想大家可能会喜欢 Go... 还有就是它是完全开源的,分布式的,且我不想频繁出差,而 Dgraph 总部在旧金山[7]。
Mat Ryer: 这很有趣。你刚提到它是分布式的。我的理解是,图数据库速度快的原因之一是它的数据存储格式几乎不需要太多转换。它基本上是以你使用它的方式存储的。这样理解对吗?
Francesc Campoy: 是的。整个理念是,有一个术语叫做“无索引邻接”(index-free adjacency)
,虽然大多数人不太理解它的意思。无索引邻接的意思是,当你... 我有一个很好的比喻。假设我是 Francesc,我想去某人的家,我知道他们的地址,我直接过去。这就是无索引邻接。你从一开始就拥有所有的信息。
而在普通数据库中,流程是我有一个密钥,先用它去查找信息的位置,然后再去获取。所以更像是我先去查电话簿,找到某人的名字,然后找到他们的地址,再去他们家。这个额外的步骤就是存储方式的主要区别。但最终,我们存储数据的方式还是键值对(key-value)。它是一个非常高级的键值存储,带有很多附加功能,比如 Raft 共识协议等。但从根本上说,数据的存储方式是键值对。分布式部分则是很多数据库---
例如 Neo4j---
为了高效运行,它们需要将所有数据存储在同一台机器上。
在 Dgraph,我们是按照谓词来分区数据。谓词比如名字、年龄或朋友等,每种信息是独立存储的,因此我们可以将它们存储在不同的机器上。当然,也可以只存储在一台机器上,但潜在地可以分布在多台机器上。通过这样分割数据,无论你获取多少数据,发送的网络请求数量与你获取的谓词数量成正比,而不是数据量的大小。所以它在扩展方面表现很好。
否则,如果你按其他方式分区,随着数据量的增加,你会需要向所有相关的机器发送查询,这样会严重影响性能。
Jaana Dogan: 我可以问一个更基础的问题吗?因为我觉得自己不太适合在这个播客里... [笑声] 相比于列式数据库,图数据库的底层存储结构是什么样的?假设所有数据都在同一台机器上。
Francesc Campoy: 这是个很酷的问题。我们使用了一个开源项目,叫做 Badger[8],它也是由 Dgraph 创建的。我们的吉祥物“挖掘獾”(diggy the badger)就是以它命名的。它是一个键值存储,你可以将它想象成 SST(有序字符串表)等类似的东西,我们在 Google 里也使用这种技术... 但从高层次来看,它就是键和值的存储。我们内部使用的是 LSM 树(日志结构合并树)。
Mat Ryer: 合并树。
Francesc Campoy: 是的,合并树。我总是把它和 LSTM(长短时记忆神经网络)混淆,这是 AI 领域的术语。所以我经常搞混。但这种结构允许你在存储和检索时都能有很好的响应速度。所以这只是一种不同类型的树结构。如果你听说过 RocksDB[9],它和 RocksDB 有些类似。两个最大的区别是:1)值不存储在树内,这使得它在存储效率上更高,而且当你遍历键时速度更快,因为你不需要获取值。2)更重要的是,它是用 Go 编写的。对于我们来说这非常重要,否则我们就得用 cgo,而 cgo 很好,但在性能上不一定理想。所以对我们来说,拥有一个用 Go 原生编写的存储系统是非常重要的。但归根结底,它就是一个高级的键值存储。
Johnny Boursiquot: 我们刚才提到的例子是社交图,因为这比较容易理解。我想听听一些不太常见的例子,Dgraph 的客户群中有哪些使用这种技术的场景?除了社交图,还有哪些方面它表现得非常好?
Francesc Campoy: 是的,社交图是一个传统的例子,因为一提到图数据库,人们就会想到 Facebook、Twitter 等等... 这些确实是很好的应用场景。不过,广义上来说,这是知识图谱的一部分; 你存储信息的图。比如,Dgraph 的技术实际上来自 Google 内部开发的技术。我记得内部项目也叫 Dgraph... 有点奇怪,但还好,因为 Google 从来不会在外部使用内部名字,所以还是挺酷的...
但这确实是相同的技术,理念是当你存储电影、演员等信息时,如果你考虑搜索引擎中右侧显示的那些额外信息,比如演员的名字和他们出演的电影等,这些信息实际上是由一个图数据库提供的。
这种知识图谱可以非常迅速地变化。我们看到有人在 Kubernetes 上构建了一个可视化图层。你可以在图数据库中将 Kubernetes 中的服务、Pods 等相关信息可视化,并查询“如果这个服务挂了,哪些东西会受到影响?”这就是一个图数据库的问题。
VMware 创建了一个开源项目叫 Purser[10],还有很多其他类似的项目。地理图也是一个例子。我们有地理定位功能,你可以查询“找到距离旧金山市中心50英里以内的所有酒店”,然后在此基础上进行更深层的图查询。你可以深入挖掘你的数据集,而这在其他数据库中很难做到。
我不知道你是否用过 BigQuery[11]。使用 BigQuery 时,你会编写需要处理大量数据的查询... 它不仅仅是写查询更简单,而是开发者的反馈循环更短,你可以快速得到结果并继续操作。而如果你需要等待五分钟,那就会非常慢,非常痛苦。
另一个常见的应用场景是数据集成。由于不需要模式,你可以非常轻松地将不同的数据集整合在一起。比如,一家大型电信公司收购了多家公司,每家公司都有自己的用户数据库。如果你想将这些系统整合成一个,图数据库非常适合完成这项任务。
如果你用关系型数据库来做这件事,外键的数量会让你头疼。而图数据库在处理这种集成时非常简单,效果也很好。
Mat Ryer: 我们之前聊到过索引问题,而图数据库与我们常见的模式不太一样……最近我在玩 Firestore[12],它是 Firebase 无模式的数据存储。如果你尝试执行从未进行过的查询,系统会报错提示“无法提供服务,因为没有匹配这些字段顺序的索引”或者类似你的过滤条件。所以你必须提前知道你要向数据库提出什么样的请求。图数据库的情况有所不同吗,基于它的特性?
Francesc Campoy: 不完全是。整个概念是,遍历关系时不需要索引。如果我从某个节点开始,绕着这个节点遍历是不需要索引的。你可能需要索引的地方是找到那个起始节点。也就是说,虽然遍历关系不需要索引,但找到你要用来开始遍历的初始节点可能需要索引。有很多不同的情况……
例如,如果你想找到某个用户下的所有订单或产品,这些都不需要索引,但找到该用户的用户名则需要索引。所以确实存在索引,并且这些索引需要建立。它们不会在你请求之前自动建立,但如果你使用 Dgraph---
我对其他图数据库不太确定,但至少 Dgraph 的工作方式是,你开始插入数据时不需要关心模式……但实际上随着你不断插入数据,系统会逐步生成一个模式。如果你尝试进行一个需要索引的查询,而该索引不存在,它确实会失败并告诉你“你需要建立这个索引”。你只需点击一个按钮,索引就生成了……开发过程中,模式会随着时间演变。
最后,你会将这个模式放入你的源代码中,声明“好吧,这是我要使用的模式”。然后当你上线到生产环境时,我通常建议启用严格模式;也就是说,当你插入数据时,如果数据不符合现有的模式,它会被拒绝。这样更安全。在开发阶段,无模式数据库非常棒,但一旦进入生产环境,我会更谨慎一些。
Mat Ryer: 我也有过类似的经历---
以前我做过 MongoDB 项目,那时我无法相信自己不需要考虑模式;我可以随意插入数据,甚至文档之间可以完全不相似……当时这种自由让我觉得非常强大。但当我构建了一些东西后,我意识到“我现在真希望能有一些错误提示。”
Francesc Campoy: 是的。我们的理念是,你可以完全不需要模式开始,但我们也有一个类型系统……你可以开始添加数据,然后会说“这个节点有这种类型,这种类型应该有这些字段……”这并不意味着这些字段一定存在,只是它们应该存在。比如,如果你有一个人(Person),你可以说“应该有一个名字和年龄。”这样当你检索数据时,你可以说“获取所有类型为‘person’的字段”或类似的操作。
所以你可以在上面使用一个类型系统,这完全是可选的。你可以从不关心存储在数据库中的数据开始,看看结果,最后得到一个模式。然后这个模式包含了所有的谓词或你存储的所有类型信息---
比如名字、年龄、朋友等等---
接着你可以说“好吧,这就是我想要的类型吗?我是否需要在这个字段上添加索引,比如用户名?我要精确查找,还是使用哈希来查找这些内容……”然后你可以将它们分组为类型,从而进行更高级的操作。
酷的是,当你达到这个阶段后,我们还有一个 GraphQL 层,这样你可以开始向图数据库发送 GraphQL 查询,这使得前端工程师非常开心,因为他们不需要学习新的语言。
Jaana Dogan: 完全不依赖模式会产生额外的成本吗?因为索引会受到影响……如果你更改了模式,索引是如何重建的?这是否也是生产环境中需要锁定模式的原因之一?
Francesc Campoy: 不一定。你可以随时更改模式,这完全没问题。如果你想为某个之前没有索引的字段添加索引,它会即时计算出来……而且速度很快。不过,如果你有几TB的数据,可能需要几分钟。在这几分钟内,你无法修改数据,只能读取数据……这没什么问题。在开发阶段,你通常使用较小的数据集,这时点击一下几乎是瞬时完成的。
所以我非常喜欢无模式开发的体验。这有点像你用 Python 编写代码时,你可以随意组合东西,它就能工作---
这非常棒。但同时,当我进入生产时,我希望有更多的约束,确保模式对我有利……因此我会添加一些机制,以便在我做错事情时---
这完全可能发生---
能收到通知。一些错误提示,哪怕只是日志或警告,比静默失败要好得多。
Jaana Dogan: 完全同意。
Francesc Campoy: 这就是我喜欢无模式数据库的原因,它非常有用,但一旦进入生产,我宁愿用带有合理类型系统的 Go 来写代码,而不是用 C 并在做错事时出现奇怪的行为。我对自己没有信心。
Jaana Dogan: 我也有过类似的经历,当我使用文档数据库时……因为没有模式,我最终会有许多嵌套类型、嵌套对象。如果引用的是大型对象,或者其他内容,整个数据库里可能会充斥着很多不该出现的东西。所以有一个模式选项来限制这些内容其实是很不错的。
Francesc Campoy: 是的,我曾经遇到过调试了十分钟后才发现,存储的数据字段名有个拼写错误,这让我痛苦了好一阵子……所以我当时想,“这就是我不想再次经历的事情。”如果能避免它,那就再好不过了。所以有一个可选的强制执行的模式非常合理。
Jaana Dogan: 是的。
Johnny Boursiquot: 我一直在试图理解如何在没有模式的情况下建模关系,这对我来说有点难……像我有一张发票和发票项,它们是父子关系。发票项不能独立存在,它依赖发票作为父节点,这种关系在无模式的世界里该如何建模?还是说你进入图数据库和无模式的世界时,必须抛弃这种传统的关系建模方式?
Francesc Campoy: 其实你不需要完全抛弃你之前的所有概念,尤其是如果你来自文档数据库的背景……你可以想象一下,不再是将整个文档存为一个大 JSON,而是将其分解成小的事实。例如,不再是一个对象包含名字和年龄,并且朋友字段指向一个有名字和年龄的数组,而是将它们分解为三元组。三元组的结构是主语、谓语和宾语。你可以说“这个 ID 的名字是 Francesc,年龄是某某,朋友是某某的列表,这些朋友的 ID 也有他们的名字和年龄。”所以你存储的只是这些三元组。
一旦你理解了这一点,你会发现之前你习惯的那种顺序感已经消失了。所以如果你有一个订单和其项,并且你想按某种特定顺序存储这些项,那么你需要显式存储这个顺序。你有几种方法可以做到这一点……你可以像传统关系型数据库那样创建一个表,这个表同时指向订单和产品,还包含一个项号。你也可以把这个项本身当作一个节点,类似关系型数据库中的处理方式,或者你可以将数据附加到关系本身上。例如,你可以说“这个项属于这个订单”,并在这个关系上标注“这是订单中的第一个项”。
有很多地方可以存储这种信息,整个概念非常灵活,你可以根据需要做任何事情,这意味着你需要更加严格地思考该如何存储数据……这也是为什么有一个模式会对你有所帮助。但最终,我们发现大多数人倾向于用最直接、最简单的方式建模数据。然后他们会发送查询,并观察性能问题在哪里……因为数据的存储方式决定了性能的优劣。这时你开始对数据库进行调优。
但总的来说,我建议用最自然的方式来存储数据。就像写 Go 代码时,你不应该一开始就考虑缓存局部性。如果你一开始就这样做,你的代码可能会非常丑陋。编写符合直觉、易于维护的代码,等到需要优化时再考虑性能问题……
对于数据建模也是同样的道理。找到一个清晰、直观的方式来存储数据。如果有问题,首先提交问题报告……因为可能是系统的问题……然后我们可以开始调优和调试,看看我们读取了多少数据……有时问题很简单,比如“哦,你其实没有使用这个索引,结果加载了整个数据库,而不是一个项目。”这显然不好。所以,写代码时,建模数据应该尽量保持简单和自然……毕竟你需要长期维护它。如果你一开始就耍聪明,那你以后也必须保持同样聪明,这一点我不太敢保证。[笑声]
Jaana Dogan: 说得好。
Johnny Boursiquot: 根据我的经验,你可以用关系型数据库走得很远,直到你意识到“哦,我没有为正确的列添加合适的索引……”在你意识到需要做点什么之前,你可以走得很远。但听起来对于键值存储或图数据库,你似乎需要更多地提前思考如何构建数据,如何分片等问题。
Francesc Campoy: 实际上,情况正好相反。我认为图数据库(以及文档数据库)由于模式的灵活性,使得你可以从最简单的方式出发,一旦你发现有些东西没有提前考虑到,你可以很容易地迁移到别的方式。数据转换和其他操作都非常简单。相比之下,考虑关系型数据库……
我有一个例子:你有电影和导演的关系,你可能会说“一部电影有一个导演,所以我要在电影表里存一个指向导演的外键。”你觉得这很好,直到你发现“哦,某些电影有多个导演。”现在你需要重构数据库,这是很困难的……因为你存储数据的方式使得将其拆分为不同部分变得更加复杂。
你可能需要引入一个新的概念,比如“电影导演”,因为你从一对多关系转变为多对多。这种情况下,处理起来要困难得多。而对于图数据库,如果你一开始说“这是一个一对多的关系”,然后后来发现“哦,不对,是多对多”,那只需要在某个地方修改一下谓词的类型,问题就解决了。
因此,数据迁移在图数据库中要简单得多。关键是,你可以在拥有足够信息后再做决定,而不是在一开始就试图做出正确的决定。
Mat Ryer: 这给开发带来了很大的优势,因为我认为我们常常自欺欺人,以为可以在一开始就完美设计系统,然后进入实现阶段……但从来不是这样。你在开发过程中学到的东西非常多,能够灵活适应和迁移数据显得尤为重要。Slack 频道里的 Andy Walker 提问---
Andy Walker 是我新的 Twitter 敌人……他问开发模式和生产模式的切换是怎么回事。他问“我是否需要一个代表所有关心内容的数据集种子,然后以某种方式清除它并快照等?”这个过程到底是什么样的?
Francesc Campoy: 我通常的做法---
有不同的选项---
但基本思路是,当你往数据库插入数据时,默认你可以插入任何数据,系统会接受一切。因此,你开始存储数据,系统会随着你输入的数据自动生成模式。比如我存储了“我的名字是 Francesc”---
系统会生成一个东西说“哦,有个叫做 Name 的字段,它指向某种类型……”如果你以 JSON 格式发送,它可能会猜测这是一个字符串;如果你以 rtf 格式发送,它会说“这是默认值。我们不知道它是什么类型,但你可以存储。”
随着你不断输入数据,你会逐渐调整模式,声明“实际上这个 Name 字段需要是字符串。而且我们还需要一个索引。”或者你添加年龄字段,并继续添加更多数据。最终,你拥有了所有你需要的数据和一个完整的模式……并且数据库中还有所有需要的索引,来支持你要进行的查询。这时,自动生成的模式可以直接复制粘贴到你的代码中,叫做 Dgraph.schema
,然后当你启动程序时,如果数据库不存在---
是一个全新的空数据库---
你会发送这个模式并锁定它……从此以后,模式不能再更改。如果你发送的数据符合模式,那没问题。但如果你尝试发送数据,字段名不是 Name 而是 First Name,并且这个字段不在模式中,它会直接拒绝。
当你启动数据库时,你有一个选项---
服务模式。服务模式有标准模式或严格模式。还有一种模式是完全不可变的---
你不能修改任何东西,不能更改数据。那是一个只读数据库。所以严格模式介于两者之间。你可以写入数据,但不能更改模式。
Jaana Dogan: 既然你提到了只读数据库,是否有数据复制的机制?我可以有多个只读节点,比如一个写节点吗?
Francesc Campoy: 是的,这一点非常重要。Dgraph 中的 D 代表“分布式”。所以你应该运行多个服务器来执行每个功能;它的设计是没有单点故障。如果你了解 Kubernetes 的工作原理---
你有多个副本,这些副本在一个组中存储相同的信息,执行相同的任务,我们使用 Raft 共识算法决定哪个是领导者。如果其中一个副本崩溃了,我们不在乎。如果你有三个副本,其中一个崩溃了,你仍然有两个副本,数据库会继续工作。当它恢复时,它会被通知“嘿,你是这个组的一部分。这是你应该服务的数据”,然后它会赶上进度,数据会被复制。
除此之外,如果你的数据量太大,单台机器无法容纳,你可以添加更多的组,数据会在这些组中分片。但每个组都有自己的副本,你可以有多个 Alpha(持有数据的服务器)……每个组通常有 3 或 5 个副本,具体取决于你的可用性要求。Zero 节点也是如此。Zero 节点类似 Kubernetes 的 Master 节点或控制器,管理整个集群,你也可以将它复制成三个副本,这样即使一个节点挂了,集群仍能正常工作。
Mat Ryer: 你之前提到 GraphQL,我见过一些 GraphQL 实现,后来惊讶地发现它其实是由 Postgres 支撑的。
Francesc Campoy: 是的。
Mat Ryer: 但接口是 GraphQL。就像你说的,JavaScript 开发者很喜欢这种方式……我认为能够精确指定你想要的数据,并且只返回这些数据,这一点非常好……而且这种方式非常自然,因为 GraphQL 本质上是类似 JSON 的格式。
Francesc Campoy: 是的。
Mat Ryer: 那你怎么看待用图接口操作传统数据库的方式?
Francesc Campoy: 我认为 GraphQL 是 REST 的一个很好的替代方案。我真的很喜欢它。它有一个模式,所以当你请求数据时,你知道自己可以请求什么……但同时,它不仅仅是数据,还有操作。你可以执行变更(mutation)、查询等操作。所以可以说它是 gRPC 的现代版。如果你用过 gRPC,你会知道它有数据部分(用 protocol buffers 表示),但同时还有各种……
Francesc Campoy: 我认为 GraphQL 是 REST 的一个很好的替代方案。我真的非常喜欢它。它有一个 schema(模式),所以当你请求数据时,你知道你可以请求什么东西……但与此同时,你不仅仅可以获取数据,还可以执行操作。你有 mutations(更新操作)和 queries(查询操作)之类的东西。所以它有点像一种现代化的 gRPC。如果你曾经使用过 gRPC,你会知道它的数据是通过 protocol buffers(协议缓冲区)来表示的,但同时你也有各种不同的---
我想它们被称为……
Mat Ryer: 服务。
Francesc Campoy: 对,服务……各种不同的服务,对吧?所以 GraphQL 的理念也有点类似于此。所以我非常喜欢它。不过,问题是,这是一个很美好的抽象,但要让它不出现漏洞是非常困难的。如果你有一个非常美好的抽象,比如“哦,一切都是一个图,你只需要把一切都当作图来使用”---
好的,第一个问题就是认证。嘿,真有趣。[笑声] 如果你开始考虑这些问题,你会发现其实在实现这些东西时有很多问题,无法自然而然地解决。
有很多适配器,比如 Prisma、Apollo 和 Hasura,它们是不同公司提供的,用在现有数据库之上的适配器。这很棒,因为这意味着你可以在你的旧系统之上创建一个 GraphQL 层,然后逐步地、慢慢地开始替换其中的部分,比如“哦,这些我打算迁移到一个更加原生的 GraphQL 环境中”或者“我要将这个单体架构拆分成微服务,然后在上面实现联邦架构……”所有这些方式都可行,也非常有用。
不过,每次我参加 GraphQL 峰会,人们总是在讨论同样的问题,比如缓存、N+1 问题……比如,如果你要从关系数据库上获取大量信息,不管怎样你基本上都在获取表中的所有数据。你会获取到那些信息,然后在发送之前,“哦,猜猜看---
除了客户信息之外,现在我还想获取所有的订单。”你需要获取那些 ID,然后再去获取订单的信息,接着再深入。所以这就像 N+1 问题。为了解决这个问题,有一些方法,比如预取一些东西,或者通过玩转缓存来解决,事情变得非常复杂。虽然这些方法有效,这很好,但理解起来非常困难,调试起来更是难上加难。
这也是为什么对我们来说,Dgraph 的 GraphQL 适配非常容易,因为我们之前支持的语言,我们还会继续支持很长时间的,是 GraphQL+-,因为它基本上就是 GraphQL。它基本是 GraphQL,但我们增加了一些额外的东西,让它成为一个更适合数据库的查询语言,去掉了一些不太有意义的东西。
所以适配 GraphQL 非常容易---
“基本上是同样的语言---
现在我们正在研究的是,‘我们如何把那些让语言与 GraphQL 不兼容的额外功能,再以兼容的方式加回 GraphQL 中?’”其实有办法的……这真的非常有趣,涉及很多语言设计和 API 的问题,但都是在 GraphQL 里面,非常酷。
Jaana Dogan: 你提到了调试,就像在关系数据库中有一个巨大的文化是围绕分析工具的……它们可以分析你的查询等等。Dgraph 有类似的工具吗?
Francesc Campoy: 目前还没有。我们实际上正在开发一个查询规划器。现在的情况是,数据的结构方式使得查询规划器并不那么有意义,除非是你在使用索引。因此我们正从这个方面开始着手工作……因为有时候使用索引并不一定是最好的方式。基本上,如果你在关系数据库中连接两个表,如果其中一个表比另一个表小得多,你可以采用比全连接更好的操作方法。这有点类似的想法,取决于数据集的大小,有时候使用索引更好,而有时候直接遍历所有项可能更快……这就是我们正在研究的方向。
话虽如此,我们在最新发布的版本中有一个新功能,可以让你知道获取了多少节点,从磁盘上读取了多少数据,经历了多少次网络调用……然后在此基础上---
Jaana,我确信你会喜欢---
我们在整个系统中都应用了 OpenCensus。
Jaana Dogan: [笑] 恭喜。
Francesc Campoy: 是的,谢谢。它的效果出人意料地好。当你遇到“好的,这个运行得很慢,发生了什么?”这样的情况,你只需要打开 OpenCensus,你就能看到所有的调用轨迹,访问不同的机器,访问磁盘,然后你可以看到“这个和这个地方真的很慢,发生了什么?”这使得我们正在开发的工具不仅对终端用户有用,对于任何已经了解 OpenCensus 的技术人员来说,开源社区已经有很多相关的东西可以使用。
Jaana Dogan: 它也是一个知识工具。我非常喜欢分布式跟踪作为一种知识工具。团队中的每个人要理解整个系统的端到端流程可能非常困难,所以他们可以通过分布式跟踪工具进一步了解他们的项目,对吗?
Francesc Campoy: 是的。这也是我喜欢掌控一切的原因之一……假设你正在管理一个 GraphQL 层,你有一个 GraphQL 层,它会转换为发给不同 alpha 节点的请求,alpha 节点再与 zero 节点通信,最后去 Badger 从 SSD 上获取数据……我们拥有所有这些代码,所以我们能够在整个过程中使用 OpenCensus,并且对发生了什么有一个非常清晰的视图。
Jaana Dogan: 这很棒。
Francesc Campoy: 如果你试图在一个适配器中做类似的事情,并且你有一个适配器覆盖在另一个数据库上,那么这种集成将会非常困难。
Jaana Dogan: 对。
Francesc Campoy: 所以我们简化了所有事情,一切都是我们自己构建的,所有东西都用 Go 编写,所有东西都集成了 OpenCensus,所有东西---
我想用“云原生”这个词;一切都应该是持续运行的,如果某个部分崩溃了,没关系---
只要重启它然后继续运行。所有这些因素使得最终使用起来更简单,因为移动的部分更少。
Mat Ryer: 有人曾在 Twitter 上说 Kubernetes 基本上就是在大规模地关闭和重启东西。
Francesc Campoy: 是的,而且它有效。
Mat Ryer: 我们还应该聊聊你不会使用图数据库的场景……
Francesc Campoy: 嗯。
Mat Ryer: 我想很多听众---
当然包括我自己---
我打算去玩一下这个,因为我真的想看看……因为到目前为止我对我们讨论的内容还没有完全理解它到底能做什么,以及使用它时的感觉。但是否有一些问题你根本不会考虑使用图数据库来解决?
Francesc Campoy: 我们构建 Dgraph 的方式是考虑到通用的使用场景。你在存储数据……就像你使用文档数据库一样---
你可以用文档数据库几乎处理任何东西。这是一个非常通用的领域。我们正在努力使得你能够使用图算法和一些高级功能,但同时我们也在努力确保普通的用例,比如只存储数据并确保一切稳定且高效---
这是我们追求的目标。
不过,有些场景,比如你在处理时间序列数据---
这个领域有一些效率更高的解决方案,因为它是一个非常特定的领域,而且有一些非常棒的解决方案。所以时间序列是其中之一……我会说,分析是我们可以做的事情之一,具体取决于你处理的数据量。
如果你在做实时分析,我认为 Dgraph 实际上非常适合。如果你在处理大批量数据的分析,并且你基本上是在一次性遍历所有数据,那么 BigQuery 很棒。要想重新实现这一点会非常困难。所以有些工具在某些特定用例上会更好。我确实认为人们仍然倾向于把关系数据库用于一切操作,而关系数据库在你需要获取大量信息时表现得非常好,特别是当数据总是相同的 schema 时,效果尤其好。但遗憾的是,这种情况并不常见。关系数据库在存储关系数据方面表现得相当糟糕,尽管它们名字中有“关系”这个词……它们实际上并不擅长处理关系数据。
Mat Ryer: 朝鲜叫做“朝鲜民主主义人民共和国”……(笑声)
Francesc Campoy: 我是说,嗯……(笑声)
Jaana Dogan: 我们在朝鲜被封杀了。哎呀……(笑声)
Johnny Boursiquot: 谢谢你,Mat……(笑声)
Francesc Campoy: 至少朝鲜的某个人有幽默感……(笑声)不,实际上在关系数据库中管理关系数据确实很难,这也是为什么人们喜欢文档数据库。它让事情变得更简单;你只需要存储你的数据,就这样。而我认为图数据库可以在某种程度上替代文档数据库,而且它的可扩展性要好得多。所以我认为它可能会朝这个方向发展。
Mat Ryer: 你认为人们最终会在某些情况下运行不同类型的数据库吗?比如在过渡中,或者如果某个团队认为“这是一个非常适合使用图数据库的用例,但我们仍然有现有的系统必须继续使用……”因为有时你不能简单地删除一切然后重新开始。你是否认为会有这样的情况,你的关系数据库(或现有的数据存储)仍然在运行,同时你还维护图数据库来处理那些特别的、特定的用例?
Francesc Campoy: 是的,这实际上是我们经常看到的情况,你已经有了一个非常大的数据集,而你现在想要做的是读取这些数据,因为你已经有很多程序和系统在使用这个数据库……所以替换这个数据库意味着你需要替换所有在其上运行的软件,而这是一项非常大的工作,几乎是不可能完成的。但我们在这些情况下的做法是,将图数据库与传统数据库放在一起,并将它们同步在一起。许多数据库能够发送 Kafka 的变更流,告诉你“嘿,这个数据变了,这个数据变了……”这样你就可以让图数据库完全保持与关系数据库同步,然后在图数据库擅长的事情上使用图数据库。
一个传统的例子,如果你有一个博客系统,并且你想要获取某篇博客的所有评论,而那是大数据---
如果你把这些评论存储为某种关系数据,可能是评论或者帖子,这实际上是非常困难的。你需要额外的索引,但你基本上是在倒回到关系数据库的工作方式中去。所以这会非常困难,性能也不会很好。你可以用图数据库来存储所有索引的元数据,然后获取所有节点或你想要的东西的 ID,保持结构,然后标签、图片等元数据仍然存储在你的关系数据库中。
所以你可以把它作为一个索引数据库来增强你可以构建的东西,而不是必须替换整个系统。这完全是一个正常的场景,因为你知道,如果你去找 CTO 说:“嘿,我有一个全新的数据库,用这个替换你现在的数据库吧”,他们会说:“不,我们不会这么做。”
Mat Ryer: “是的,我们只需要 Ctrl+A 然后删除我们所有的数据,没问题吧……” [笑声]
Francesc Campoy: “只要把你的数据存储在这张软盘上,然后我们再继续。” [笑声]
Mat Ryer: 那么如果我们查看 Dgraph 存储的文件,实际上它们是什么样子的?当它存储在磁盘上时,具体是什么样子?
Francesc Campoy: 你会看到有三个目录---
P、W 和 ZW。P 是存储所有数据的地方。实际上你会看到 LSTM 文件,基本上是 Badger 存储的东西。很多不同的键值存储以 LSTM 形式存储在磁盘上,所以你会有很多这样的文件……然后你有 W,这是一个预写日志。每次有 demo 操作发送到数据库,我们都会将其存储在这里,以防万一系统崩溃。如果崩溃了,我们就可以知道“哦,我们当时执行到这里了,这些操作需要继续应用。”
然后还有一个文件夹是 ZW。ZW 是用于管理集群的 zero 节点的预写日志。所以你会有这三个文件夹,里面的所有东西都是二进制文件。你可以把这些文件复制到其他地方,启动一个新的数据库,但问题是这些文件实际上包含了集群机器的数量等信息……所以随着时间的推移,如果你更改了这些设置,可能会导致崩溃,这并不是正确的方式。不过我们也有功能可以将所有数据导出为 JSON、rtf,或者你甚至可以使用 protocol buffers 存储二进制备份。
Jaana Dogan: 文件系统是存储数据的唯一方式吗?还是我可以编写一个适配器将数据发送到某个 blob 存储中?
Francesc Campoy: 实际上 Badger 这个键值存储是为 SSD 设计的。如果你不是在 SSD 上运行,它仍然能够工作,但速度会慢很多。所以 SSD 是必须的。当你进行备份时,你完全可以将其存储在云存储或其他地方,这完全没问题。
有意思的是,实际上有人问过如何把所有数据存储在内存中,而我们正在为 Badger 开发一个无盘模式,在这个模式下,所有数据都存储在内存中,然后你可以在 RAM 上运行所有东西……对于一个图数据库来说,这并不是一个好主意,因为它应该存储大量数据,但如果你有一些非常小的东西,并且你想要极高的性能,那绝对是可以尝试的。
Jaana Dogan: 可以使用内存文件系统吗?还是必须编写一个新的适配器?
Francesc Campoy: 哦,这已经内置了……我们做的不是添加很多东西,而是基本上移除了一些检查。我们有一些块是存储在磁盘上的,还有一些我们需要移除的小功能。默认情况下---
是的,我们已经有适配器可以将所有东西直接写入内存。你不需要做任何额外的事情。
Jaana Dogan: 很酷。
Mat Ryer: 如果数据集---
抱歉,Johnny。如果数据集---
其实我不知道为什么要向你道歉,因为听众不知道你举手了,对吧……
Johnny Boursiquot: 我不认为……不,他们不知道。
Mat Ryer: 这工作真的难啊。[笑声] 更糟糕的是,我忘记我刚才要说什么了,所以……轮到你了,Johnny。 [笑声]
Francesc Campoy: 多棒的主持人啊。 [笑声]
Johnny Boursiquot: 我跟你说…… [笑声]
Mat Ryer: 来自伦敦的呼叫……好像有很大的延迟,虽然实际上没有。这真是太神奇了,对不起。我们会把这段剪掉;有时候我真的很惊讶技术居然能正常运作,这是其中一次。Johnny……
Johnny Boursiquot: [56:05] 不,这段一定会留下来,我保证…… [笑声]
Mat Ryer: 只要是让我看起来蠢的部分,都会被留下。这似乎是个惯例。
Johnny Boursiquot: [笑声] 在第 103 期节目中,我们请到了 Dgraph 的一些人上节目;我们请了 Manish Jain 和 Karl McGuire……我们讨论了很多事情,包括 Ristretto。我肯定我发音不对……你可以在回答时纠正我,不过……我理解的意思是,他们想要引入一个非常好的缓存系统来满足 Dgraph 的需求。我很想知道这项转变的现状,是否已经完成了,或者目前的进展如何,性能如何……我很好奇这些。
Francesc Campoy: 嗯,严格来说,如果你想用意大利语发音,那应该是 "Ristretto",但我们通常念成 "Ristretto",这样更简单。 [笑声] Ristretto 是我们在多个层面上集成的缓存机制。一个层面是在键值存储中。我们正在直接与 Badger 进行集成……我们的想法是,我们已经为键值存储增加了加密和压缩功能,这会对 CPU 造成更大的负担。所以,当你需要在读取数据之前解密时,CPU 会消耗额外的计算周期……因此,通过引入缓存,我们希望通过使用更多的内存,使得系统和以前一样快。
除此之外,我们还在 Dgraph 层上添加缓存,甚至正在考虑在 GraphQL 层使用它……这样我们就可以在多个层面上进行缓存,并尝试找到最具性能的选项。关于 Ristretto 有趣的一点是,在 Go 中编写一个缓存其实非常难。它出乎意料的难。比如,如果我给你一个指向包含许多内容的结构体的指针,那么它在内存中到底占用了多少空间?这个问题非常难回答。
Mat Ryer: 是指针本身的大小吗?
Francesc Campoy: 不,是指针指向的内容。你有一个结构体,但当然,它是字段的总和,没错。再加上一些对齐操作,诸如此类的事情……那么一个 map 的大小是多少呢?它是所有键的大小加上一些额外的东西……一个 slice 的大小是多少呢?它是数组的大小加上一些额外的东西……所以你会在很多地方得到“加上一些额外的东西”,因此如果你有一个 JSON 文件并且你想解析它,并且精确地说出“把它加载到内存中后会占用多大空间”,这实际上非常困难。
在处理缓存时,如果你没有搞清楚这一点,问题是你无法判断这些新对象是否能适应内存。所以在加载新对象之前是否应该丢弃一些旧的呢?这很难。即使是 gRPC---
gRPC 运行得非常好,但 gRPC 在 Go 中也没有做这个。在 C++ 中有个 arenas(内存分配区)的概念,你可以将对象直接分配到某个内存空间中……但 Go 不支持这个。所以所有这些,像内存管理之类的事情,在 Go 中都出乎意料地复杂……这很有趣,但也让人感到棘手。我遇到过一些问题,我真的不知道该怎么处理,只能说“祝你好运”。
Mat Ryer: 而且,当然,你以前的 Google 朋友现在也不理你了,所以你甚至没法去问他们……
Jaana Dogan: 我为什么会在这个节目里? [笑声]
Francesc Campoy: 这是个好问题…… [笑声] 我的天哪。不过,对 Ristretto 来说,整个想法是让 Badger 和 Dgraph 更加出色,并能够通过更好地利用内存来弥补 CPU 的额外开销……同时我们也希望它是开源的,这样任何人都可以用它做他们想做的事情。而且它非常成功。很多人都在使用它,提交问题和功能请求之类的,这非常不错。
Mat Ryer: 是啊,我现在就在一个项目里用它。
Francesc Campoy: 哦,真棒。
Mat Ryer: 是的。它真的很好。而且它的 API 也非常简单。真的很容易就能插入。而且当你的应用程序突然变得非常快时,那种满足感是极大的。我并不关注内存的使用;如果我需要更多内存,我就买更多内存,这就是我的方法……但其他人可能喜欢更科学一点…… [笑声]
所以,如果有人感兴趣,我拼写一下这个名字……我们也会在节目笔记里放上这些链接,不过是“Ristrrrrrreto”,对吧?Ristretto……
Francesc Campoy: 至少你知道什么是 Ristretto 吗?
Mat Ryer: 是一种意面吗?我不是开玩笑…… [笑声] 我真的---
它让我有点饿。
Francesc Campoy: 因为它听起来像意大利语,所以你觉得它是意面? [笑声]
Mat Ryer: 嗯,我以为是……
Jaana Dogan: 是一种咖啡,对吧?
Francesc Campoy: 不,Ristretto 是一种非常非常短的浓缩咖啡。它是一种非常小的咖啡。
Jaana Dogan: 哦,对。
Mat Ryer: 哦。浓缩咖啡本来就很小了,不是吗?
Francesc Campoy: 是的,所以 Ristretto 更小。它只是一两滴咖啡。
Mat Ryer: 就像是小孩子喝的浓缩咖啡。
Francesc Campoy: 我觉得如果你给孩子喝 Ristretto,最后他们会变成像你这样的人。所以我不会给孩子喝 Ristretto…… [笑声] 这似乎是个坏主意。
Jaana Dogan: 是啊,虽然它的量更少,但咖啡的浓度要高得多,对吧?
Francesc Campoy: 是的。
Jaana Dogan: 类似于这样……
Francesc Campoy: 它是压缩的咖啡,对。
Mat Ryer: 对不起,我刚才说它是意面被批评了,但结果它是咖啡……拜托! [笑声]
Francesc Campoy: 不过是的,整个想法就是做一些非常小但是有效的东西,所以 Ristretto 是个不错的名字。
Mat Ryer: 是的。它真的很好。我推荐大家试试。而且它确实很容易集成到现有的 Go 项目中。谢谢你,Francesc,给我们带来这些教育。对于那些想了解更多关于你们在做什么的人,有什么网址可以访问吗?
Francesc Campoy: Dgraph.io。另外,在结束之前,我想提到一些事情……下个月有 FOSDEM,你们可能听说过---
这是比利时的一个开放会议,将在 2 月 1 日和 2 日举行,如果我没记错的话……今年我又和 Women Who Go 的 Maartje Eyskens 一起负责管理这个会议。现在征集演讲稿件已经开始了,所以请大家踊跃投稿。我们希望能收到尽可能多的提案,这样最终我们就能有许多高质量的演讲。尽管会场很小,会议总是人满为患,但非常有趣,最后我们还会得到非常棒的视频。所以即使这是你第一次演讲,这也是一个很好的尝试机会。而且我们会有人帮助你,给你指导,预览你的幻灯片,帮助你排练,提供你所需要的任何帮助。所以我们非常欢迎并鼓励第一次演讲的人来投稿。
Mat Ryer: 太棒了,非常感谢。我们会在节目笔记里发布所有这些链接,所以不用担心……最后,我要做的就是跟大家说再见。感谢大家的收听。我们下次再见!
Graph databases: https://changelog.com/gotime/108
[2]Jaana: https://github.com/rakyll
[3]Francesc Campoy: https://github.com/campoy
[4]Just for Func: https://www.youtube.com/c/justforfunc
[5]source{d}: https://github.com/src-d
[6]Dgraph: https://github.com/dgraph-io/dgraph
[7]Dgraph 总部在旧金山: https://dgraph.io/
[8]Badger: https://github.com/dgraph-io/badger
[9]RocksDB: https://github.com/facebook/rocksdb
[10]Purser: https://github.com/vmware-archive/purser
[11]BigQuery: https://cloud.google.com/bigquery
[12]Firestore: https://firebase.google.com/docs/firestore?hl=zh-cn