HarmonyOS开发者限时福利来啦!最高10w+现金激励等你拿~ 了解详情
写点什么

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

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:003620

评论 1 条评论

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

Demo分享丨看ModelArts与HiLens是如何让车自己跑起来的

华为云开发者联盟

人工智能 智能车 hilens

二十多岁的年纪是怎么成功四面字节跳动,最终拿到offer的?

Java架构之路

Java 程序员 架构 面试 编程语言

突破2.8万美元关口,比特币为何“疯涨”? ​

CECBC

比特币 比特币数字货币

GitHub标星力推!我掏空了各大搜索引擎,给你整理了188道Java面试题,满满干货记得收藏

Java架构之路

Java 程序员 架构 面试 编程语言

SpringBoot,来实现MySQL读写分离技术

Java架构师迁哥

测开之函数进阶· 第4篇《匿名函数》

清菡软件测试

测试开发

在wildfly 21中搭建cluster集群

程序那些事

程序那些事 wildfly wildfly21 集群部署 集群架构

速来围观!阿里P8大牛写出的JDK源码剖析及大型网站技术架构与业务架构融合之道

Java架构之路

Java 程序员 架构 面试 编程语言

自研ARM芯片,亲手拆掉Wintel联盟,微软这次是认真的吗?

脑极体

Spring Cloud 2020.0.0 正式发布,对开发者来说意味着什么?

阿里巴巴云原生

阿里云 容器 开发者 云原生 架构师

扫地阿姨看完都学会了!万字长文总结Android多进程,满满干货指导

欢喜学安卓

android 程序员 面试 移动开发

架构师训练营第五周”技术选型一“作业

随秋

极客大学架构师训练营

Java岗四面字节跳动成功之前,我都刷了那些面试题以及做了那些准备!

Java架构之路

Java 程序员 架构 面试 编程语言

K8S 资源可视化利器:Kubectl-Graph

郭旭东

Kubernetes Kubernetes Plugin

支持 gRPC 长链接,深度解读 Nacos 2.0 架构设计及新模型

阿里巴巴云原生

云计算 阿里云 开源 微服务 云原生

架构师训练营 - 大作业1

阿甘

架构大作业二

Geek_michael

极客大学架构师训练营

云上可靠性测试:让我们一起给开发找点事儿

华为云开发者联盟

安全 云服务 可靠性

CAP 原理 <笔记>

raox

极客大学架构师训练营

JAVA并发编程原理与实战

Geek_53983e

原理 java 并发 实战

冰河又一MySQL力作出版(文末送书)!!

冰河

MySQL 高可用 高并发 高性能 MySQL架构

LeetCode题解:剑指 Offer 40. 最小的k个数,快速排序,JavaScript,详细注释

Lee Chen

算法 大前端 LeetCode

架构师训练营 - 大作业 2

阿甘

手把手教你写!2021年Android工作或更难找,最全的BAT大厂面试题整理

欢喜学安卓

android 程序员 面试 移动开发

与前端训练营的日子 --Week09

SamGo

学习

甲方日常 76

句子

工作 随笔杂谈 日常

姐夫半夜不睡觉,竟躲在厕所看这“57道Redis面试题”?

Java架构之路

Java 程序员 架构 面试 编程语言

7. JDK拍了拍你:字符串拼接一定记得用MessageFormat#format

YourBatman

Spring Framework 类型转换 MessageFormat DateFormat

架构大作业一

Geek_michael

极客大学架构师训练营

面试官:Android事件分发机制及设计思路,跳槽薪资翻倍

欢喜学安卓

android 程序员 面试 移动开发

为移动应用产业开辟出海新航路,华为应用市场是如何“破冰”的?

脑极体

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