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


 
让我们的控制服务器依赖于 MySQL 或 PostgreSQL 还意味着我们对控制服务器的测试将变得缓慢和丑陋 。Brad 与 Perkeep(https://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 { BigLocksync.Mutex Somethings map[string]Something Widgetsmap[string]Widget Gadgetsmap[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 的一个简单做法是将它们写入数据库 , 但这将产生非常复杂的数据模型 。不幸的是 , 如果我们想要同时运行多个控制进程以实现高可用性和更好的发布管理 , 就意味着我们不再只有一个管理数据的进程 , 因此我们的索引需要支持事务(以及回滚) 。因此 , 我们投入了大约两到三周的工程时间来设计事务一致的内存索引 。这一点描述起来有些复杂 , 所以笔者将在后续的博客文章中专题解释 , 敬请期待 。


推荐阅读