写点什么

一次几乎不可能的数据库迁移

Brad Fitzpatrick、David Crawshaw

  • 2021-03-07
  • 本文字数:3693 字

    阅读完需:约 12 分钟

一次几乎不可能的数据库迁移

最初,我们把一个文件当作数据库,将数据转化为 JSON 大对象写入进去,后来,它的速度越来越慢,我们决定进行数据库的迁移,这个过程中我们遇到了一些问题和障碍,但最终我们成功完成了这一次不太可能的数据库迁移。

Brad 加入一家初创公司

大约一年前,当我刚加入 Tailscale(https://tailscale.com/)时,我问 Crawshaw(https://github.com/crawshaw)的第一件事是:“嗯……你们使用的是什么数据库呢?MySQL、PostgreSQL、SQLite?“我知道他喜欢 SQLite。

 

“一个文本文件,”他回答道。

“嗯?”

“是的,我们将一个大型 JSON 对象写入一个文本文件。”

“怎么写?什么时候写?写什么?”

“嗯,无论什么时候,只要有什么东西一变,我们都会在我们的单进程中获取一个锁,然后重写这个文件!”他高兴地笑着说。


听起来很疯狂。其实就是很疯狂。的确,它很容易测试,但是,不能扩展。这些,我们都知道。但是,当时它还行得通。

 

后来,它不行了。


即使使用高速 NVMe 驱动器,并且将数据库分成两部分(重要数据和 tmpfs 上的可能丢失的临时数据),有些事情也会变得越来越慢。我们知道这一天终将到来。文件达到了 150MB 的峰值,我们正在以磁盘 I/O 允许的最快速度写入它。这已经到了极限。

 

那么,迁移到 MySQL 或 PostgreSQL,如何?或者是 SQLite?


不,Crawshaw 另有主意。

聊聊大卫的背景故事

Tailscale 的协调服务器(我们的“控制面板”)以控制 CONTROL(https://getsmart.fandom.com/wiki/CONTROL闻名遐迩。它目前是单个 VM 上的单个 Go 进程。它最早的原型使用的是 SQLite。我们最初的设计与最终的设计有非常大的差异,包括同步到客户端机器上的配置数据库,以及所有我们最终不再需要的其他概念。在这个过程中,我们每周都要对 SQL 数据模型进行非常大规模的重组,这需要大量的键盘输入工作。SQL 已经得到了广泛使用,它持久、有效,但将其引入到任何编程语言中几乎都需要做大量的粘合。(通常大家都试图用 ORM 来避免这种情况,用令人生厌的大量魔法字和效率损失来取代那些同样令人生厌的键盘输入工作。)


一天,我厌倦了重构,就把它彻底丢在一边,建了一个内存数据模型进行实验。这样,迭代速度更快了。几周后,一位客户想要试用一下。我还没有做好提交数据模型和用 SQL 来完成它的准备,所以我选择了一条捷径:将持有所有数据的对象包装在一个 sync.Mutex 中。所有访问都要经过它,在编辑时,将整个结构传递给 json.Marshal,然后写入磁盘。这就是我们用大约 20 行 Go 实现的数据模型持久层。


我们本来计划要迁移为别的语言的,但忙着忙着就给忘记了。

JSONMutexDB 的后面是什么?

下一步显然是迁移到 SQL。我最喜欢的仍然是 SQLite,但是我不能说服自己把一个快速增长的服务迁移到它上面。它当然是可行,尤其是我们的控制面板的设计并不需要典型的 web 服务高可用性:短时间停机的无非就是使新节点无法登录而已,正在工作的网络可以保持正常工作。


其后是 MySQL(或 PostgreSQL)。我对 1998 年以后的 MySQL 不是特别熟悉,但我确信它是可以的。不过,开源数据库的 HA 情况有些令人惊讶:您可以使用传统的滞后副本,也可以提交到具有令人非常惊讶的事务语义的无主副本集群。我对试图在这些语义之上设计一个稳定的 API 或良好的网络图计算并不感兴趣。CockroachDBhttps://github.com/cockroachdb/cockroach#what-is-cockroachdb 曾经看起来很有前途,而实际上现在仍然很有前途!但对于一个数据库来说,它还是相对比较新的,我不太放心把一些特性附着在一个新的 DBMS 上,因为如果我们需要将这些特性中迁移出来时,可能很难做得到。


让我们的控制服务器依赖于 MySQL 或 PostgreSQL 还意味着我们对控制服务器的测试将变得缓慢和丑陋。Brad 与 Perkeephttps://perkeep.org/)曾就此有过争论,他之前写过perkeep.org/pkg/test/dockertest,它的确可行,但我们不想要求未来的员工都这么做。它需要在你的机器上部署 Docker 环境,速度不是特别快。


后来有一天我们看到一份 Jepsen 写的 etcd 报告(https://jepsen.io/analyses/etcd-3.4.3)。这篇报告不似 Jepsen 之前那种满篇吐槽的风格,里面还指出了一些 etcd(https://etcd.io/)的优点。结合 Dave Anderson(https://github.com/danderson)的一些正面体验,我们开始考虑是否可以直接使用 etcd。由于是它用 Go 编写的,我们可以直接将它连接到我们的测试中,并直接使用它。无需 Docker,无需 mock,就可以测试我们在生产环境中实际使用的东西。


事实上,我们写入到磁盘的核心数据模型严格遵循了以下模式:

type AllTheData struct {	BigLock    sync.Mutex	Somethings map[string]Something	Widgets    map[string]Widget	Gadgets    map[string]Gadget}
复制代码

这很好地映射到了 KV-store 上。因此,我们将 etcd 作为一个“最小可行的数据库”。它做了我们当前所需要的最关键的事情,那就是 1)将 BigLock 拆解成更类似于 sync.RWMutex 的东西。2)减少 I/O,只写改变的数据,而不是整体都写。


(我们会谨慎避免使用任何难以映射到 CockroachDB 的 etcd 特性。)


这样做的缺点是,etcd 虽然在 Kubernetes 中很流行,但是数据库系统的用户相对较少。作为一家公司,Tailscale 正致力于在其上打造一款创新代币(https://mcfunley.com/choose-boring-technology)。但这款数据库从概念上讲非常小,以致于我们不必把它当作一个黑盒。当我们在 etcd 3.4 中遇到一个异常缓慢的主键分页的极端情况时,我能够阅读它的源代码并在一个小时内编写出一个修复程序。(后来,我发现 etcd 的下一个版本也已经做了一样的修复(https://github.com/etcd-io/etcd/commit/26c930f27d46776da5fedae69267ba0b69c31185),所以我们将其反向移植了过来。)

我们的 etcd 客户端包装器

我们用于 etcd 的客户端是开放源码的,网址是 github.com/tailscale/tailetc(https://github.com/tailscale/tailetc)。它围绕了两个概念:1)DB 中的总数据量足够小,可以放入服务器的内存中;2)读比写更常见。鉴于这一点,我们希望降低读取成本。

我们的方法是对 etcd 注册一个监控。每次更改都被发送到这个客户端,这个客户端在一个 sync.RWMutex 后面维护一个庞大的缓存 map[string] interface{}。当你创建一个 Tx 并且做一次 Get 时,这个值从这个缓存中读出(这个缓存可能在 etcd 之后,但是通过跟踪 modrev 来保持事务一致性:即一个全局递增的 ID, etcd 使用它来界定键-值对的修订)。为了避免缓存中的混叠错误,我们将对象复制出来,但是通过对缓存中的对象实现更有效的克隆调用,避免了每次 Get 时的 JSON 解码。


最终结果是,从 etcd 获取一个值不需要任何网络流量。


当我在设计一个包时,我感受到了编写 Go 时它的类型系统的局限性,这样的感受并不多,它是其中之一。如果我使用的是一种具有各种花哨功能的语言,那么我可以在离开缓存的对象上放置某种 const 限定符,从而避免对内存进行克隆。即便如此,在我们的服务器上执行的性能分析却表明,复制并不是一个性能问题,所以该例可能说明,我实际上并不需要那些心心念念的更复杂的类型系统。通常情况下,假设很可能并不正确,性能分析才更具启发意义。

一个障碍:索引

选择最小可行的“nosql”的最大问题是缺乏每个标准 SQL DBMS 所提供的出色的索引系统。我们要么在 etcd 中存储索引,要么在客户端的内存中管理索引。

我们使用 JSONMutexDB 在内存中生成它们,因为更改数据模型要容易得多。使用 etcd 的一个简单做法是将它们写入数据库,但这将产生非常复杂的数据模型。不幸的是,如果我们想要同时运行多个控制进程以实现高可用性和更好的发布管理,就意味着我们不再只有一个管理数据的进程,因此我们的索引需要支持事务(以及回滚)。因此,我们投入了大约两到三周的工程时间来设计事务一致的内存索引。这一点描述起来有些复杂,所以笔者将在后续的博客文章中专题解释,敬请期待。

迁移

而迁徙本身却没什么特别值得注意的,这其实件好事。我们这两个系统并行运行了一段时间,并在某个时间点停止了旧系统的使用。最令人兴奋的是,当我们关闭 JSON 写入时,提交延迟降低了很多。在管理面板中编辑网络时这一点尤为明显。我们有漂亮的 Grafana 图表,在切换之前我们就调整了 Prometheus 配置以保持更多的历史纪录。不论在哪种情况下,写操作都能从几乎一秒(有时更糟!)的时间缩短到毫秒级。刚开始的时候,写入并不是我们的第二目标。永远不要低估“临时”起意会产生多么长久的影响!

未来

在这项工作中,除了确保 Tailscale 控制面板可以在可预见的未来扩展外,最令人兴奋的事情是我们发布过程的改进。我们可以轻松地将多个控制面板实例附加到一个一致的数据库中,这意味着我们可以切换为蓝绿部署(https://en.wikipedia.org/wiki/Blue-green_deployment)。这将让 Tailscale 的工程师们有信心去尝试部署特性,因为变更所能造成的最差结果是有限的。我们的目标是将开发速度保持在接近 JSONMutexDB 早期的水平,当时我们可以在不到一秒的时间内重新编译并在本地运行,每天部署上 10 几次。


原文链接:An unlikely database migration


译者简介:冬雨,小小技术宅一枚,从事研发过程改进及质量改进方面的工作,关注编程、软件工程、敏捷、DevOps、云计算等领域,非常乐意将国外新鲜的 IT 资讯和深度技术文章翻译分享给大家,已翻译出版《深入敏捷测试》、《持续交付实战》。

2021-03-07 12:003503

评论 1 条评论

发布
用户头像
原型数据库和生产数据库的选型要慎重
2021-03-15 08:34
回复
没有更多了
发现更多内容

数据结构与算法之树与二叉树(理论篇)

未见花闻

7月月更

Node.js基础入门

bo

前端 Node 7月月更

新星计划Day10【数据结构与算法】 排序算法

京与旧铺

7月月更

前端值得一看的ArkUI框架进度指示器

坚果

开源 HarmonyOS OpenHarmony Open Harmony 7月月更

【愚公系列】2022年07月 Java教学课程 06-常量

愚公搬代码

7月月更

当转转严选订单遇到状态机

转转技术团队

架构 状态机

闭会算法好题之2020ICPC澳门站签到A题

KEY.L

7月月更

Iptables的基本原理

阿泽🧸

iptables 7月月更

TOGAF中的本手和妙手

涛哥 数字产品和业务架构

企业架构 TOGAF

zookeeper-zookeeper的应用场景

zarmnosaj

7月月更

【MySql项目实战优化】复杂触发器案例分享

安逸的咸鱼

MySQL 实战 7月月更

在线随机抛硬币正反面统计工具

入门小站

工具

linux中的内部命令和外部命令

工程师日月

Linux 7月月更

运营商的时代之旅:种下5.5G的魔豆,攀上数字化的天空花园

脑极体

Docker安装Redis!!!(含每一步详细图解)实战

宁在春

redis Docker 7月月更

技术解析|Doris Connector 结合 Flink CDC 实现 MySQL 分库分表 Exactly Once精准接入

SelectDB

数据库 flink 数据仓库 Doris

微服务间的通信协议和消息格式

穿过生命散发芬芳

7月月更 微服务通信

100 条 Linux vim 命令备忘单,收藏起来随时备用!

wljslmz

vim Linux 运维、 7月月更

用 Flutter 给小姐姐的照片调个颜色滤镜

岛上码农

flutter ios 前端 安卓开发 7月月更

在线XML转JSON工具

入门小站

工具

jQuery 基础动画

Jason199

js JQuery框架 7月月更

Spring全家通之SpringMVC如何传递参数以及返回值的类型

Java学术趴

7月月更

使用 PixCap 和 ReadyPlayerMe 快速制作3D 模型动画

devpoint

3D 7月月更 pixcap readyplayerme

云原生指南之什么是云原生基础架构

宇宙之一粟

云原生 7月月更

【算法社区】十大排序算法详解

小明Java问道之路

数据结构 算法 排序 排序算法 7月月更

CnosDB 涅槃重生:弃用Go, 全面拥抱Rust

CnosDB

rust 物联网 时序数据库 开源社区 Go 语言

数据要素

奔向架构师

数据资产 7月月更

历史的坑,只能尽量填平

技术小生

事故复盘 7月月更

SDL键盘事件

柒号华仔

7月月更

使用pytorch自己构建网络模型实战

秃头小苏

PyTorch 7月月更

AWS Support Plan

冯亮

云计算 DevOps AWS 云计算架构师

一次几乎不可能的数据库迁移_语言 & 开发_InfoQ精选文章