当我差不多一年前刚加入Tailscale时,我问Crawshaw的第一件事就是:“那么,您使用哪个数据库?知道他喜欢SQLite,是MySQL,PostgreSQL,还是SQLite?”。
“是的,每当发生更改时,我们都会在单进程中获取一个锁并重写文件!”他高兴地笑了起来。
听起来很疯狂。太疯狂了。当然,它易于测试,但无法扩展。我们俩都知道。但这行得通。
即使有了快速的NVMe驱动器并将数据库分成两半(重要数据与临时数据,我们在tmpfs上可能会丢失),事情变得越来越慢。我们知道这一天将会到来。该文件的峰值大小达到了150MB,我们正在以磁盘I / O允许的速度对其进行写入。那不是桃子吗?
Tailscale的协调服务器,即我们的“控制平面”,被称为CONTROL。目前,它是单个VM上的单个Go进程。 CONTROL的最早原型使用SQLite。我们最初的设计与最终设计有很大不同,涉及到与客户端计算机同步的配置数据库以及最终不需要的各种其他概念。通过此过程,我们每周都会对SQL数据模型进行重大重组,这需要大量惊人的键入。 SQL被广泛使用,持久,有效,并且需要消耗大量胶水才能将其引入几乎任何编程语言。 (尝试使用ORM避免这种情况通常用烦人的魔术和效率降低来代替烦人的打字。)
有一天,我厌倦了重构,将其全部扔掉,并建立了一个用于实验的内存数据模型。这使得迭代快得多。几周后,一位客户想试用一下。我还没有准备好提交数据模型并在SQL中正确执行它,所以我采取了一条捷径:保存所有数据的对象被同步包装了.Mutex,所有访问都通过了它,并在编辑时传递了整个结构到json.Marshal并写入磁盘。这使我们的数据模型在Go的约20行中具有持久性。
计划总是要迁移到其他地方,但是,我们忙于其他东西,有点忘了。
显而易见的下一步是转向SQL。我最喜欢的仍然是SQLite,但我无法说服自己为快速增长的服务迁移。它当然可以工作,特别是因为我们控制平面的设计不需要典型Web服务的高可用性:短暂中断的结果是新节点无法登录;工作网络继续工作。
MySQL(或PostgreSQL)将紧随其后。我对1998年以后的MySQL没有特别的了解,但是我敢肯定它会起作用。但是,开放源数据库的HAstory有点令人惊讶:您可以具有传统的滞后副本,也可以提交具有非常令人惊讶的事务语义的tono主副本群集。我对在这些语义之上尝试设计稳定的API或良好的网络图计算并没有感到兴奋。 CockroachDB看起来非常有前途,而且的确如此!但这对于数据库来说是一个相对较新的事物,我有点担心如何在新的DBMS中附加功能,如果需要,这些功能很难迁移。
使我们的控制服务器依赖于MySQL或PostgreSQL,也意味着我们的控制服务器的测试过程变慢了,丑陋。布拉德曾与Perkeep进行过战斗,之前曾写过perkeep.org/pkg/test/dockertest可以正常工作,但我们不想让未来的员工参加。它需要在您的计算机上使用Docker,而且速度不是特别快。
然后有一天,我们看到一份关于Jepsen的报告onetcd。与我们习惯的通常不尽人意的Jepsen报告不同,该报告对etcd表示好话。结合Dave Anderson的一些积极经验,我们开始考虑是否可以直接使用etcd。继续,我们可以将其链接到我们的测试中并直接使用它。没有Docker,没有模拟,没有测试我们实际在生产中使用的东西。
事实证明,我们正在写入磁盘的核心数据模型紧随该模式:
这出奇地映射到了KV商店。因此,这导致我们将etcd视为“最低可行的数据库”。它完成了我们现在需要的关键任务,这是1)将BigLock分解为更类似于sync.RWMutex的事物,以及2)减少我们的I / O以仅写入更改的数据,而不是整个写入。
(我们非常小心,不要使用任何难以映射到CockroachDB的etcd功能。)
不利的一面是etcd虽然在Kubernetes中很流行,但是相对来说数据库系统的用户却很少。作为一家公司,Tailscale在它上面花了一个创新代币。但是数据库在概念上足够小,因此我们不必将其视为黑匣子。当我们在etcd 3.4中遇到一个令人惊讶的缓慢的paginationedge案例时,我能够从源代码中读取自己的方式,并在一小时内为它编写了修复程序。 (然后,我发现等效的修补程序已被应用于下一版的etcd,因此我们将其反向移植了。)
我们用于etcd的客户端在github.com/tailscale/tailetc是开源的。它基于两个概念构建:1)数据库中的总数据量很小,无法容纳到服务器的内存中; 2)读取比写入更为常见。鉴于此,我们希望使读取便宜。
我们这样做的方法是通过在etcd上注册一个手表。 Everychange被发送到客户端,客户端在sync.RWMutex后面维护着巨大的缓存映射[string] interface {}。创建Txand执行Get时,将从该缓存中读取值(该值可能在backetcd中,但通过跟踪modrev:etcd用于定义键-值对的修订版的全局增量ID,在事务上保持一致)。为了避免在缓存中混叠错误,我们复制了对象输出,但是通过对缓存中的对象实施更有效的Clone调用来避免对每个Get进行JSON解码。
这是编写Go的那几次,我在设计包装时就感受到了它的类型系统的局限性。如果我用一种语言处理所有问题,那么我可以在离开缓存的对象上放置某种const限定符,并避免克隆内存。就是说,在我们的服务器上运行配置文件表明复制不是性能问题,所以也许这是我觉得向更复杂的类型系统的牵引力的一个示例,而实际上并不需要它们。通常情况下,假设是危险的,剖析是有启发性的。
选择最小可行的“ nosql”的最大问题是缺少每个标准SQL DBMS提供的出色的索引系统。我们对将索引存储在etcd中或在客户端中管理它们的内存感到困惑。
使用JSONMutexDB,我们可以在内存中生成它们,因为更改数据模型更加容易。 etcd的简单选项将把它们写入数据库,但是那样的话,它们的数据模型就会非常复杂。不幸的是,如果我们要同时运行多个CONTROL进程以实现高可用性和更好的发行版管理,则意味着我们不再只有一个进程来管理数据,因此我们的索引需要了解事务(和回滚)。因此,我们投入了大约两到三周的工程时间来设计事务上一致的内存索引。描述起来有些棘手,所以我们将其保存在以后的博客文章中(希望我们有足够的时间清理代码以便有一天将其开源)。
迁移不是很明显,这总是一件好事。并行运行两个系统一段时间,并在某个时候停止使用旧系统。最令人兴奋的是,当我们关闭JSON写操作时,提交延迟下降了很多。在管理面板中编辑网络时,这一点最为明显。我们将在此处包含漂亮的Grafana图,但是在过渡之前,我们更改了Prometheus配置以保留更多历史记录。无论如何,写入时间从近一秒(有时更糟!)到毫秒。当我们开始时,写作当然不是一秒钟的事情。永远不要低估您的“临时” hack会持续生产多久!
这项工作最令人兴奋的事情,除了确保尾标控制平面可以在可预见的将来进行扩展外,还改善了我们的发布流程。 一个一致的数据库使我们可以轻松地将多个控制平面实例连接起来,这意味着我们可以进行蓝绿色部署。这将使Tailscale工程师能够以有限的最坏情况所带来的信心来尝试部署功能。 目标是使开发速度保持接近JSONMutexDB的早期,那时您可以在一秒钟内重新编译并在本地运行,每天部署十次。