乐趣区

关于edge:EdgeDB-架构简析

与国外不同,我在中文社区碰到的对于 EdgeDB 最多的问题就是——EdgeDB 与 openGauss、OceanBase、TiDB 有什么不同吗?EdgeDB 反对程度伸缩吗?本文将从 EdgeDB 架构设计的角度尝试答复以上问题,以及“EdgeDB 是什么”。

架构
EdgeDB 的整体架构其实非常简单,说白了就是一个封装了 PostgreSQL 的服务器程序:

你的应用程序须要定义一份数据结构 /schema,而后依据这份 schema 来向 EdgeDB 发送 EdgeQL 查问语句。比如说,这是一份用 EdgeQL SDL 语言编写的 schema 定义:

type Person {property name -> str;}

type Team {
    property slogan -> str;
    multi link members -> Person {property title -> str;}
}
复制代码

这是你的 EdgeQL 查问语句:

select Team {
  slogan,
  members: {
    name,
    @title
  } order by @title
}
复制代码

对于 EdgeQL 及 SDL 的细节劣势就不开展说了

接着说这张图,在 EdgeDB 这边,EdgeQL 的查问语句会被编译成 SQL,而后在 PostgreSQL 中执行并将后果原路返回。EdgeDB 自身并不存储任何数据,包含你的 schema 和数据都间接存在 PostgreSQL 中,同时还有万八条 EdgeDB 根底数据,包含内置 schema、数据类型、规范库、用户角色、数据库配置等等。

EdgeDB 对后端的 PostgreSQL 没有任何魔改,就是一般的 PostgreSQL 数据库,版本在 13 或以上即可,甚至能够是一些云平台自带的 PostgreSQL。再加上 EdgeDB 把本人的配置信息都存在 PostgreSQL 里了,所以绝对于 PostgreSQL,EdgeDB 就是一个“无状态”的服务,因而前面的图外面会独自把 EdgeDB Server 和 PostgreSQL 画进去,只管少数状况下 EdgeDB 都是用自带的 PostgreSQL,并且用户并无感知。

性能
如果我通知你,EdgeDB Server 其实是用 Python 写的,你还敢用吗?

实际上,EdgeDB 优化到了能媲美 PostgreSQL 原生性能的境地。这听下来尽管没什么,但我说的是整体效率——比照来看现今大部分解决方案,总体效率会受到连贯资源分配、SQL 编译缓存、ORM 开销、SQL 优化等诸多因素的株连,因而综合来看 EdgeDB 都是名落孙山的,更不要说 EdgeDB 在开发者工作效率上的晋升了。那 EdgeDB 是怎么做到的呢?

从下往上看,首先就是与 PostgreSQL 通信的二进制协定,这里的 EdgeDB 代码脱胎与 asyncpg 我的项目,也是用 Cython 开过光的,所以速度比 Python 中其余通过 psycopg2 连 PostgreSQL 的形式要快得多,甚至比 Go 语言中的两种计划都要快。起因除了是二进制协定和 Cython 的减速外,EdgeDB 大量应用了 pipelining,每个 EdgeQL 查问(是的,无论多简单,EdgeDB 都会编译成一个能够很长但非常高效的 SQL)只会产生一次网络读写(逻辑接口),大大降低了重复往返于网络的工夫开销。

再往上,EdgeDB 会把 PostgreSQL 编译好的 SQL 句柄(prepared statement)缓存下来,因为通常的应用程序总共的查问数量个别都是无限的,比方几十种不同的查问,每次执行只是参数不同而已,因而 EdgeDB 能够跳过 PostgreSQL 每次从新编译 SQL 的步骤,间接进入打算执行阶段。而这在非 EdgeDB 的应用程序或数据库框架中,须要用到高级技巧(比方 SQLAlchemy 的 baked query 性能)能力实现。

相似地,EdgeDB 也会把 EdgeQL 到 SQL 的编译后果缓存下来,缓存命中就间接执行。只不过,这里的缓存索引不是字符串哈希,而是通过语法语义解析失去的 AST(形象语法树)。这样做的益处是,即便你的 EdgeQL 语句里有一些字面量,EdgeDB 也能够通过 AST 剖析出句子的骨干,不会影响缓存的命中。因为每个查问都要在查缓存前做解析,因而 EdgeDB 用 Rust 写了一个解析器 Python 插件,解析一条语句的用时大略是 50-70 微秒,也就是 0.05 毫秒,或者 0.00005 秒。

在左边,就是 EdgeQL 编译过程池。因为编译器自身是 Python 写的,所以执行起来特耗 CPU,于是 EdgeDB 就做了一个过程池(为了绕开 GIL 所以不是线程池),专门用来编译 EdgeQL,而后 pickle 了用 UNIX domain socket 来传数据。但个别状况下,如果缓存充沛预热了,编译过程池没什么太大工作量的。

最初最下面是客户端连 EdgeDB 的二进制协定,这个协定特意模拟了 PostgreSQL 的二进制协定,一方面团队做过 asyncpg 教训最丰盛,另一方面也是最重要的,就是继承了 PostgreSQL 协定里数据的格局。也就是说,从二进制传输层来看,EdgeDB Server 并不需要解包从 PostgreSQL 服务器传过来的查问后果数据,间接换成 EdgeDB 本人的二进制协定外包装,传给客户端即可。也就是说,对于理论的用户数据,EdgeDB 简直就是一个架在 PostgreSQL 后面的通明代理,但却应用了齐全不同的查询语言和类型零碎。

连贯
当 EdgeDB 进入一个实在的高并发环境之后,事件就变得更有意思了:

首先,EdgeDB Server 与客户端之间的连贯是非常轻量级的,实现了鉴权之后就齐全无状态了(除了在数据库事务中必须绑定同一个后端连贯),因为所有的前端连贯都共享同一个 EdgeQL 编译缓存和后端 PostgreSQL 连接池,只有当客户端发动一个申请的时候,EdgeDB 才会给这个客户端连贯调配一个后端 PostgreSQL 数据库的连贯,并且查问实现了之后就立马还给连接池,供其余前端连贯应用。这里的原理相似于 pgBouncer,用了 EdgeDB 之后你就不再须要这些中间件了,也不须要放心前端连贯会占用无限的 PostgreSQL 连接数资源。因为有 uvloop 的加持,目前单机并发反对个几万、几十万前端连贯还是没问题的,只有你的后端 PostgreSQL 能撑得住。

因为特地的轻,所以前端连贯在 EdgeDB Server 端并没有设置“连接池”,只有一个最大连接数的限度,更多的作用是防攻打而不是因为 PostgreSQL 资源无限。同时,EdgeDB Server 会被动毙掉长时间(默认 30 秒)没有流动的前端连贯——客户端从新连就好了。

EdgeDB 目前的网络并发 I/O 是由 uvloop 承载的,也就是那个传说中最快的 Python 异步网络框架。其实 uvloop 和 asyncpg——甚至肯定水平上能够包含 Python 里的 async/await 语法——都是为了做 EdgeDB 才搞进去的。所以目前 EdgeDB 在 I/O 并发上是 Python 里目前能做到的最优解,但选 Python 更多还是因为晚期 EdgeDB 的迭代次数十分多,须要这种灵活性,接下来稳固之后,会思考用比方 Rust 来重写 I/O 层。

其次,你可能留神到了图中有两波客户端,他们用的 schema 不一样。这对应了 PostgreSQL 里的一个“逻辑库”的概念,也就是一个数据库实例下面能够有多个逻辑子库。EdgeDB 同样反对这一性能,并且比 PostgreSQL 的反对更成熟,因为 PostgreSQL 无奈帮你均衡不同逻辑库之间的压力,总共就那几百上千个数据库连贯,你给 db1 多调配一个连贯就要给 db2 少调配一个,而同一个连贯又没有方法零老本的换库(pg 的锅)。EdgeDB 因为有架构设计上的劣势,所以能够看到前端连贯的应用比重,所以咱们在 EdgeDB 的后端连接池里写了一个简单的算法,用来调度不同逻辑库应调配的数据库连贯资源数,以达到主动均衡出最优的服务质量(QoS),也就是大家不至于旱的旱死涝的涝死,从而彻底解放了“前端”开发人员的脑力,不用再为此事放心。

品质
提到服务质量(QoS),就不得不说一下 EdgeDB 在官网客户端里为 QoS 所做的优化。

当你的应用程序调用了 EdgeDB 官网客户端(简称 client 吧,因为对象名个别就是 client)的 query() 办法之后,client 并不是单纯的把申请转发进来了事,而是做了一系列通常是由利用开发者实现的、可能进步利用服务质量的事件。

每个 client 都封装了一个连接池,初始为空,仅在须要的时候才会创立连贯,所以 client 是妥妥的懒加载模式。创立连贯时,兴许是断网了,兴许 EdgeDB Server 所在的 Docker 容器还没启动起来,兴许是云服务正在重启或者故障转移,反正就是一下没连上。怎么办?client 报错给利用开发者之前,会先本人尝试重连,万一连上了就继续执行呗,反正这个重试工夫是能够配置的。

拿到连贯之后,client 会先查看本地查问缓存,如果曾经有了这个查问的类型信息,就会间接应用该信息对输出参数数据进行编码,而后间接用一次 optimistic_execute 服务器交互来实现查问;否则,就要先 prepare 拿到参数类型等相干信息,再进行 execute 调用,须要两次往返服务器。

再往后,服务器如果胜利返回后果诚然是好,但有些状况下就是会出错。再一次地,当 client 把问题埋怨给利用开发者之前,会先尝试自行解决。如果到 EdgeDB 数据库的连贯尚在,问题仅限于比如说隔离级别导致的数据抵触,或者后端 PostgreSQL 临时掉线了等“可重试”的问题,那么 client 会间接尝试从新发送执行曾经拼装好的申请数据。但如果连 EdgeDB 数据库连贯都没了,那么在重试规定容许的状况下,client 会间接尝试重连,除非这条 EdgeQL 查问语句不是只读的(因为网络不稳固这事儿谁也说不准,兴许曾经执行胜利了呢,所以还是只重试只读查问最为稳当。你问我 client 怎么晓得语句是不是只读的?EdgeDB Server 晓得呀,prepare result 外面就有只读信息。要是这个数据都没来得及传回来,那还是间接报错好了)。

对于应用数据库事务的代码,这个过程仍然是一样的,只不过对利用开发者更为通明了而已。比方上面这段 Python 代码:

async for tx in client.transaction():
    async with tx:
        await tx.execute("insert ...")
复制代码

或是上面这段 JavaScript 代码:

await client.transaction(async tx => {await tx.execute(`insert ...`);
});
复制代码

EdgeDB 的官网客户端从接口上强制要求,利用开发者必须思考到整段事务代码如果产生重试应该怎么办。这其实才是正确的数据库事务写法(因为 SERIALIZABLE 隔离级别下,解决 SerializationError 的最佳实际就是无意识地从新执行整段事务代码),不能因为其余数据库驱动没提供这种写法,你就能够让用户看大白页而后成为“重试”的一环。有了强制的重试事务接口,你才不会把一些本不应放在事务中的代码误写进去,比方操作一个 Redis 里的计数器。

有了各种保障 QoS 的机制,当比方 client 连接池里的连贯放太久被 Server 给毙了的时候,利用开发者齐全不须要放心因而而导致出错,同时 EdgeDB 也能够加重一些并发的压力,已达到整体服务质量的晋升。

周边
从另一个角度来看 EdgeDB 的话,它不仅仅只是一个数据库服务器:

体验

最初补充一点开发者应用相干的体验。

应用 EdgeDB 进行开发的第一步就是装置 EdgeDB CLI,在 Linux/macOS 下就是一个命令:

$ curl --proto '=https' --tlsv1.2 -sSf https://sh.edgedb.com | sh
复制代码

在 Windows 下也是一个命令(服务器运行时应用 WSL):

PS> iwr https://ps1.edgedb.com -useb | iex
复制代码

实现之后,你须要在你的我的项目文件夹下,初始化 EdgeDB 的我的项目(如果你是新我的项目,能够用空文件夹):

$ edgedb project init
No `edgedb.toml` found in this repo or above.
Do you want to initialize a new project? [Y/n]
> Y
How would you like to run EdgeDB for this project?
1. Local (native package)
2. Docker
> 1
Checking EdgeDB versions...
Specify the version of EdgeDB to use with this project [1-rc3]:
> # left blank for default
Specify the name of EdgeDB instance to use with this project:
> my_instance
Initializing EdgeDB instance...
Bootstrap complete. Server is up and running now.
Project initialialized.
复制代码

此时,CLI 程序会下载 EdgeDB Server 并创立一个数据库实例(内带 PostgreSQL 实例),而后在以后文件夹下创立以下文件(夹):

edgedb.toml – EdgeDB 我的项目文件,蕴含版本号什么的;
dbschema/default.esdl – 一个空的 schema 定义,供你后续编辑 schema 用;
dbschema/migrations/*.edgeql – 主动生成的数据库 migration,不要手动编辑,由 CLI 命令治理。
这些文件都应该增加到版本控制如 Git 中,接下来你就能够正式进入开发了。跟 EdgeDB 无关的行为大略有:

连到数据库里,手动执行一些查问:间接 edgedb + 回车;
批改 schema:间接批改 esdl 文件,实现之后先执行 edgedb migration create 创立 migration 脚本,而后执行 edgedb migrate 实现 migration;
代码连数据库:import edgedb + edgedb.create_client(),不同语言或环境略有不同,但只有在有 edgedb.toml 的(子)文件夹中执行代码,就不须要额定的连贯参数;生产环境用环境变量来设置连贯参数。
能看进去,EdgeDB 在日常开发中的应用体验非常简略暴力,因为客户端库和命令行工具都是自家产的,所以咱们把能省的都帮开发者省了,并且一致性极高。比方我不说你甚至都不会意识到,开发环境也是启用了 TLS 的,因为 EdgeDB Server 会主动创立开发证书,CLI 记住信赖的证书,客户端库就能畅通无阻的连服务器了,不须要开发者的任何干涉。未来的托管 EdgeDB 云端实例也会做到一样的体验。

总结
通过零碎架构能够看出,目前 EdgeDB 的关注点在于:

EdgeQL,实践上有可能成为 SQL 的“接班人”
单机数据库效率,作为根底数据库,先服务好大部分中小规模的利用场景
开发体验与工作效率,为用起来“爽”做了大量工作
云生态适配,有作为 serverless 数据库的后劲
然而,EdgeDB 与 NewSQL 并没有什么关系,目前在程度伸缩方面也并没有提供额定的反对,就是定位为一款新型根底通用 OLTP 数据库。诚然,你能够提供本人的可伸缩魔改 PostgreSQL 后端,EdgeDB 也内置反对肯定水平的高可用,也能够改进去只读正本什么的,但那并不是以后 EdgeDB 的关注重点。如果 EdgeDB Server 自身变成了零碎瓶颈,那么就在同一个 PostgreSQL 后端上多加一个 EdgeDB Server 实例,一个不行,就两个(之后 I/O 改良了,间接减少 I/O 线 / 过程数即可)。

然而,EdgeDB 的定位更偏差于相似 PostgreSQL 这样的根底数据库,大神们能够在根底数据库之上玩出花来,但通用数据库自身则会偏向于先把根底打牢。EdgeDB 以 EdgeQL 为招牌,在兼顾性能的同时将开发者体验和效率列在第一位,并一举突破了许多现有技术栈——比方 ORM——的解放,为古代利用开发带来了申明式 schema、蕴含 migration 的工作流、transaction 重试等最佳实际,能够说是一种全新的数据库物种。

最初
如果你感觉此文对你有一丁点帮忙,点个赞。或者能够退出我的开发交换群:1025263163 互相学习,咱们会有业余的技术答疑解惑

如果你感觉这篇文章对你有点用的话,麻烦请给咱们的开源我的项目点点 star:http://github.crmeb.net/u/defu 不胜感激!

PHP 学习手册:https://doc.crmeb.com
技术交换论坛:https://q.crmeb.com

退出移动版