15 个版本 (4 个重大更改)
0.6.4 | 2022年2月23日 |
---|---|
0.5.0 | 2022年2月15日 |
0.1.3 | 2021年12月7日 |
#505 in 数据库接口
每月下载 719 次
9MB
165K SLoC
Verneuil: SQLite 的流式复制
Verneuil[^verneuil-process] [vɛʁnœj] 是一个 VFS (操作系统抽象层),用于 SQLite,它像默认的 unix VFS 一样访问本地数据库文件,同时异步地将快照复制到 S3-兼容的 blob 存储中。我们编写它是为了提高那些适合 SQLite(至少对于单节点部署)的现有服务的可扩展性和可用性。
Backtrace 依赖于 Verneuil 备份和复制数千个 SQLite 数据库,大小从 100KB 到几 GB 不等,其中一些数据库每秒都会更新... S3 成本低于每天 40 美元。
[^verneuil-process]: Verneuil 方法 是第一种商业合成宝石制造方法... DRH 坚持将 SQLite 读作一种矿物,当然是一种珍贵的矿物(
Verneuil 的主要设计目标是向工作单节点系统添加异步读取复制,而不引入新的灾难性故障模式。避免新的故障模式优于其他所有考虑因素,包括复制延迟:没有尝试限制或最小化读取副本的陈旧性。Verneuil 读取副本应在可接受陈旧数据时使用。
与这种保守的复制方法一致,磁盘上的本地数据库文件仍然是真相的来源,VFS 与 SQLite 的默认 unix
VFS 完全兼容,即使是并发(带文件锁定)访问。Verneuil 将必须跨 SQLite 事务持久化的所有状态存储在磁盘上,因此多个进程仍然可以使用 Verneuil 访问和复制相同的数据库。
Verneuil 还对所有的 API 调用进行了节流(当前硬编码的限制为每秒 30 个调用/进程)以避免“意外”的云费用,并且通过一个用于将数据上传到远程 blob 存储的崩溃安全缓冲目录将 SQLite VFS 与复制工作线程解耦,该目录将最坏情况的磁盘占用限制为源数据库文件大小的四倍左右。因此,始终可以安全地禁用对 blob 存储的访问:缓冲的复制数据可能会随着时间的推移而增长,但始终在限制范围内。
将默认的Unix VFS替换为Verneuil会对本地SQLite操作产生影响,当然:写入必须更慢,以便排队更新以进行复制。然而,这种减速通常与执行写入本身所需的时间成比例,通常受SQLite事务提交回滚模式下产生的两个两个fsync
的影响。此外,额外的复制逻辑在写入锁降级为读取锁的情况下运行,因此后续事务只有在准备提交时才会阻塞在新复制步骤上。
这项工作与litestream不可比:Verneuil旨在进行异步读取复制,并将流式备份作为良好的副作用。因此,复制方法完全不同。特别是,litestream仅适用于WAL模式的SQLite数据库,而Verneuil仅支持回滚日志记录。请参阅doc/DESIGN.md
以获取详细信息。
这个仓库里有什么
-
一个“Linux”VFS(
c/linuxvfs.c
),实现了SQLite对非WAL数据库所需的所有功能,而不需要SQLite的Unix VFS中所有向后兼容的冗余。新VFS的行为与上游的Unix VFS完全兼容!它是新(仅限Linux)SQLite VFS的一个更简单的起点。 -
一个带有C接口的Rust crate(见
include/verneuil.h
)用于配置和注册-
verneuil
VFS,它挂钩到Linux VFS以跟踪更改,在轮转目录中生成快照,并将轮转数据异步上传到远程对象存储(如S3)。此VFS仅与SQLite的回滚日志模式兼容。可以直接作为Rust程序调用,也可以通过其C接口调用。 -
verneuil_snapshot
VFS,允许SQLite访问存储在S3兼容的blob存储中的快照。
-
-
一个运行时加载的SQLite扩展
libverneuil_vfs
,允许SQLite使用verneuil
VFS(将数据库复制到远程存储)或使用verneuil_snapshot
VFS(访问复制的快照)打开数据库。 -
verneuilctl
命令行工具,用于恢复快照,强制上传轮转数据,将数据库文件同步到远程存储,以及执行其他特定管理任务。
快速入门
有关更详细的设置信息,包括如何将verneuil crate直接链接而不是作为SQLite扩展加载,请参阅doc/VFS.md
和doc/SNAPSHOT_VFS.md
。rusqlite_integration
示例显示了它是如何适用于Rust crate的。
对于快速修改和测试,最简单的方法是将Verneuil构建为SQLite的运行时加载扩展(libverneuil_vfs
)。
cargobuild --release --examples --features='dynamic_vfs'
verneuilctl
工具也将非常有用。
cargobuild --release --examples --features='vendor_sqlite'
Verneuil需要额外的配置才能知道在哪里轮转复制数据,以及在哪里上传或从远程存储获取数据。这些配置数据必须以JSON编码,并将反序列化为一个verneuil::Options
结构(在src/lib.rs
中)。
一个最小配置字符串如下。有关详细信息,请参阅doc/VFS.md
和doc/SNAPSHOT_VFS.md
。
{
// "make_default": true, to use the replicating VFS by default
// "tempdir": "/my/tmp/", to override the location of temporary files
"replication_spooling_dir": "/tmp/verneuil/",
"replication_targets": [
{
"s3": {
"region": "us-east-1",
// "endpoint": "http://127.0.0.1:9000", //for non-standard regions
"chunk_bucket": "verneuil_chunks",
"manifest_bucket": "verneuil_manifests",
"domain_addressing": true // or false for the legacy bucket-as-path interface
// "create_buckets_on_demand": true // to create private buckets as needed
}
}
]
}
这是一个要传递给sqlite3_open_v2
作为查询字符串参数的庞大数据,因此Verneuil目前正在VERNEUIL_CONFIG
环境变量中查找该配置字符串。如果该变量的值以一个符号开始,例如“@/path/to/config.json”,Verneuil将在该文件中查找配置JSON。
配置文件不包含任何凭证:Verneuil从环境中获取这些凭证,要么通过调用本地EC2凭证守护进程,要么通过读取AWS_ACCESS_KEY_ID
和AWS_SECRET_ACCESS_KEY
环境变量。
环境设置完成后,我们可以在SQLite中加载扩展,并开始将我们的写入复制到S3或其他任何兼容的blob服务器(我们使用minio进行测试)。
$ RUST_LOG=warn [email protected] sqlite3
SQLite version 3.22.0 2018-01-22 18:45:57
Enter ".help" for usage hints.
Connected to a transient in-memory database.
Use ".open FILENAME" to reopen on a persistent database.
sqlite> .load ./libverneuil_vfs -- Load the Verneuil VFS extension.
sqlite> .open file:source.db?vfs=verneuil
-- The contents of source.db will now be spooled for replication before
-- letting each transaction close.
sqlite> .open file:verneuil://source.host.name/path/to/replicated.db?vfs=verneuil_snapshot
-- opens a read replica for the most current snapshot replicated to s3 by `source.host.name`
-- for the database at `/path/to/replicated.db`.
在SQLite外壳之外,必须启用扩展加载,以便允许访问load_extension
SQL函数。
还必须启用URI文件名,以便在连接字符串中指定VFS;还可以将VFS参数传递给sqlite3_open_v2
。
复制数据在每次SQLite事务结束时同步地缓冲到replication_spooling_dir
,实际上将数据上传到远程存储是在异步发生的:我们不想在网络调用上阻塞事务提交。
退出外壳或关闭应用程序后,我们可以使用verneuilctl flush $REPLICATION_SPOOLING_DIR
确保所有缓冲数据都刷新到远程存储:这个命令将尝试同步上传缓冲目录中的所有挂起数据,并在失败时详细记录/报错。
使用verneuilctl help
查找其他verneuilctl
子命令的文档。
$ ./verneuilctl --help
verneuilctl 0.1.0
utilities to interact with Verneuil snapshots
USAGE:
verneuilctl [OPTIONS] <SUBCOMMAND>
FLAGS:
-h, --help
Prints help information
-V, --version
Prints version information
OPTIONS:
-c, --config <config>
The Verneuil JSON configuration used when originally copying the database to remote storage.
A value of the form "@/path/to/json.file" refers to the contents of that file; otherwise, the argument
itself is the configuration string.
This parameter is optional, and defaults to the value of the `VERNEUIL_CONFIG` environment variable.
-l, --log <log>
Log level, in the same format as `RUST_LOG`. Defaults to only logging errors to stderr; `--log=info`
increases the verbosity to also log info and warning to stderr.
To fully disable logging, pass `--log=off`.
SUBCOMMANDS:
flush The verneuilctl flush utility accepts the path to a spooling directory, (i.e., a value for
`verneuil::Options::replication_spooling_dir`), and attempts to upload all the files pending
replication in that directory
help Prints this message or the help of the given subcommand(s)
manifest The verneuilctl manifest utility accepts the path to a source replicated file and an optional
hostname, and outputs the contents of the corresponding manifest file to `--out`, or stdout by
default
manifest-name The verneuilctl manifest-name utility accepts the path to a source replicated file and an
optional hostname, and prints the name of the corresponding manifest file to stdout
restore The verneuilctl restore utility accepts the path to a verneuil manifest file, and reconstructs
its contents to the `--out` argument (or stdout by default)
sync The verneuilctl sync utility accepts the path to a sqlite db, and uploads a fresh snapshot to
the configured replication targets
$ ./verneuilctl restore --help
verneuilctl-restore 0.1.0
The verneuilctl restore utility accepts the path to a verneuil manifest file, and reconstructs its contents to the
`--out` argument (or stdout by default)
USAGE:
verneuilctl restore [OPTIONS]
FLAGS:
--help
Prints help information
-V, --version
Prints version information
OPTIONS:
-h, --hostname <hostname>
The hostname of the machine that generated the snapshot.
Defaults to the current machine's hostname.
-m, --manifest <manifest>
The manifest file that describes the snapshot to restore.
These are typically stored as objects in versioned buckets; it is up to the invoker to fish out the relevant
version.
If missing, verneuilctl restore will attempt to download it from remote storage, based on `--hostname` and
`--source_path`.
As special cases, an `http://` or `https://` prefix will be downloaded over HTTP(S), an
`s3://bucket.region[.endpoint]/path/to/blob` URI will be loaded via HTTPS domain-addressed S3,
`verneuil://machine-host-name/path/to/sqlite.db` will be loaded based on that hostname (or the current
machine's hostname if empty) and source path, and a `file://` prefix will always be read as a local path.
-o, --out <out>
The path to the reconstructed output file.
Defaults to stdout.
-s, --source-path <source-path>
The path to the source file that was replicated by Verneuil, when it ran on `--hostname`
但是为什么?
Backtraces将后端大多数元数据分片成数千个小的(1-2 MB)到中等大小(高达1-2 GB)的SQLite数据库,平均聚合写入速率为每秒几十次写入事务(有几个热点数据库和许多冷点数据库)。在Verneuil之前,这种方法提供了足够的性能和可用性。然而,事情可以变得更好,这就是我们编写Verneuil的原因:为了分布可以与稍微过时的只读副本一起工作的逻辑,并简化我们的灾难恢复剧本,同时不引入在单节点代码中已经足够好的新故障模式。
实际上,确保副本是最新的不是我们的目标。尽管如此,我们发现一旦后端达到稳定状态,不到0.1%的写入事务复制时间超过5秒,并且检测到超过一分钟的复制延迟的频率不到每百万次写入一次。当然,这所有的一切都取决于写入负载和机器或进程上复制的数据库数量。例如,每当服务重启并快速连续写入几个数据库时,我们会经历复制延迟的临时峰值。
数据新鲜度不是目标,因为Verneuil优先考虑灾难预防。这就是我们在快照更新逻辑和将快照数据上传到远程blob存储的复制工作线程之间插入无等待的崩溃安全复制缓冲区(作为磁盘上的文件实现)的原因。我们信任这个缓冲区作为一个“数据二极管”在架构上关闭从复制工作线程返回到SQLite VFS(即返回到应用程序)的反馈循环。关键的是,给定SQLite数据库的缓冲数据量限制为该数据库文件大小的倍数,即使复制器完全卡住也是如此。即使blob存储不可访问或目标存储桶配置错误,本地操作也不会因不断增长的复制队列而中断。缓冲区还无需fsync
调用更新,这些调用可能会轻易影响整个存储子系统;Verneuil通过在重新启动后丢弃所有复制状态来实现崩溃安全。
分布式解决方案在可扩展性和可用性方面经常引入新的灾难性故障模式,结果是一个可能对罕见(一年一次或更少)事件如硬件故障或断电具有弹性的系统,但代价是增加了复杂性,使得正常运行的代码部件之间的不可预见交互经常导致长时间影响客户的严重问题。Verneuil的保守方法让我们有信心,我们可以用它来提高我们现有的集中式系统的可扩展性和可用性,而不会降低现有系统的可靠性。
灾难避免包括限制云成本。Verneuil可以保证广泛更新率下的成本效益,因为它始终能够限制更新数据的API调用:复制缓冲区将简单地压缩快照,并始终将复制数据的足迹限制为源数据库文件大小的四倍。
无论更新模式(频率和数据库数量)如何,我们都可以依赖Verneuil保持在我们的复制预算内:它每秒平均不会超过30[^size-limit]个API调用/复制目标/进程。每次调用上传一个块(不可压缩数据为64 KB,如果使用zstd则更少),或者一个清单(源数据库文件中每个64 KB块为16字节,因此2 GB文件为512 KB)。
[^size-limit]:这个硬编码的限制,加上每天“接触”一次每个现有块的巡逻逻辑,限制了单个进程中复制的数据库的总大小:复制逻辑可能在20-30 GB左右崩溃,但本地操作不应受到影响,除了缓冲复制数据的有限增长。对我们来说这不是问题,因为我们只存储在SQLite中的元数据,这些元数据通常比数据小得多。
可以通过保留规则回收块,该规则在块一周不活动后删除它们(Verneuil每天尝试“接触”一次有用的块),因此,即使有很多变化,将块上传到美国东部标准桶的成本最多为$5e-6 + 64 K/1 GB * $0.023 / 4(每月周数)< $6e-6。
多GB数据库的清单可以更大,但清单更新被限制为每个数据库每秒不到一个,并且可以更积极地删除清单blob(例如,一旦版本变得过时)。使用24小时保留规则,上传2 GB数据库的清单的成本低于$6e-6,包括API调用和存储。
我们还可以将复制的数据库永久足迹的存储成本(美国东部标准桶的$0.023/GB/月)考虑到这个上限中,但这通常受API成本支配。
以平均30次上传/复制目标/秒/进程的速率,因此,冗余数据的成本累计不超过$15.55/复制目标/天/进程。通常每台机器只有一个复制目标和复制进程,因此这相当于每天$15.55/机器(相当于c5.4xlarge)。实际上,Backtrace后端车队(每天数百万条写入,分布在一千多个数据库中)的平均每天成本约为$40。
它是如何测试的?
除了在生产环境中简单地运行以确认常规单节点操作仍然正常以及代码正确地调整API调用外,我们使用SQLite的开源回归测试套件,在将默认的Unix VFS替换为Verneuil之后。不幸的是,一些测试假设WAL数据库受支持,因此我们必须禁用它们;一些其他测试注入故障以测试SQLite的故障处理逻辑,这些测试也必须禁用。生成的测试套件位于https://github.com/pkhuong/sqlite/tree/pkhuong/no-wal-baseline
从损坏的测试套件中配置一个SQLite build
目录,然后运行 verneuil/t/test.sh
来构建加载Verneuil VFS的测试可执行文件,并将其设置为新的默认值。测试脚本还会启动一个本地minio容器用于Verneuil VFS。
在测试模式下,VFS执行内部一致性检查代码,并在检测到轮转或复制失败时崩溃。
读取副本的逻辑不能像SQLite测试套件那样轻松地依赖。相反,它需要接受经典的单元测试和手动烟雾测试。
1.0版本缺少的功能
- 可配置性:大多数管道都可用于配置单个SQLite连接,但当前实现侧重于直接链接到libverneuil的程序,并通过C调用来配置它。我们已经在SQLite中通过配置环境变量(与当前的全局配置结构相匹配)加载了Verneuil,但我们应该添加从连接字符串(SQLite查询字符串参数)读取配置数据的功能。
1.0版本之后我们应该做的事情
-
我们目前总是以
0644
的权限创建日志文件。umask适用,但实现与SQLite的Unix VFS相同的逻辑并继承主数据库文件的权限是有意义的。 -
现在许多文件系统都支持写时复制;我们应该考虑在提交步骤中使用它,而不是日志文件!
-
S3客户端库非常原始。我们应该重用HTTPS连接。
-
考虑一种在不通过S3间接的方式获取块的方法。可以在提前发出有希望的块,或者简单地像HTTP一样按需在请求-响应中提供服务。
依赖项
~30–46MB
~875K SLoC