【编者的话】2015 年 5 月,闻名全球的在线音乐平台 Spotify 将他们的存储系统从 Postgres 升级至 Cassandra,整个过程中完全没有停机时间。Spotify 为何要升级他们的存储系统,促进他们升级的导火线是什么,他们如何做到无停机时间的在线升级,以及在升级过程中遇到了哪些困难?负责 Spotify 登录服务的团队成员 Marcus Vesterlund 在博客中介绍了这次升级的整个过程。
介绍
Spotify 的所有用户信息目前已经从 Postgres 迁移至 Cassandra 数据库,最后的切换过程是在 5 月 11 日完成的。作为负责 Spotify 用户登录功能的团队,我们希望让读者了解一下我们所做的努力。
PostgreSQL 赞歌
仅仅去年,Spotify 的新增活跃用户就超过了 3 千 5 百万(详见 20 Million Reasons to Say Thanks 一文)。全部用户的详细信息都保存在 user 数据库中,包括用户名、国家及邮件等等。用户每次登录时都必须查询该数据库,同样,每次创建新的用户、升级为 Premium 用户、接受某个许可或是连接到 Facebook 时,也都要访问这个 user 数据库。这意味着 user 数据库非常繁忙,它担负着整个 Spotify 基础设施中的核心功能。
我们当时所采用的 Postgres 数据库是值得信任的,它已经为我们服务了许多年,但如今它所处理的数据量比设计时的目标已经高出了几倍,并且数据集的增长速度每天都在继续上升。
对这样的核心基础设施进行改动是一件令人畏惧的任务,但同时,我们确实不清楚 Postgres 还能坚持运行多久。
单点故障
我们的 Postgres 环境还受到另外一个问题的困扰:虽然读操作分布在所有的数据中心,但所有的写操作都发生在位于伦敦的一台孤零零的机器上。对此我们的一位网络工程师 Loke Berne 有一句名言:“从网络运维的角度来说,如果能把那个恐怖的机柜彻底干掉,那实在太美妙了。”他所指的机柜正是包含了 user 数据库写入功能的主节点。让仅仅这么一台机器作为 user 数据库的主节点可不是什么有趣的事,对于用户信息的所有更新,例如新创建的帐号、或者是让 7 千 5 百万活跃用户升级为 Premium 用户,这些操作都是由这台可怜的机器所处理的。一旦这台机器产生故障,以上这些操作都会失败,必须等到某台热备机器升级为主节点,并将访问量发送到新的主节点为止。
Postgres 的淘汰已不可避免
作为负责登录服务的团队,我们深知现有的解决方案将无法跟上用户的增长速度。它就像是一匹年迈的老马,虽然也许能够捱过这个冬天,但今后的日子将越来越难捱。我们心里都明白,现在是时候让它停下了,但扣住扳机的手却在不停地颤抖。
一切都开始于鲨鱼的一咬
2013 年 9 月,连接于伦敦与阿什伯恩数据中心之间的大西洋海底光缆突然断开了。有传言说是鲨鱼咬断了光缆,不论事实真相如何,它所造成的结果是我们的新用户数量在一周内大幅下降。如果我们能够在网络的另一边创建新用户,那么这次网络异常所带来的问题就不会那么严重了。
我们其实已经知道伦敦的单点故障是有问题的,但直到 9 月份的那一周,我们才清楚地知道,这种设计上的失败不仅仅停留在理论上而已。单点故障给我们造成了实实在在的商业损失,这种损失能够很容易地换算成欧元和美金。我们很久之前就开始考虑以 Cassandra 作为解决方案,但一直没有机会专注于这方面的工作。而现在情况已经很明显了,我们必须找到一种新的解决方案。
为行驶中的汽车更换引擎
“如果某件事你做的足够出色,那么人们甚至不会感觉到它。”
– 动画“飞出个未来”其中关于“Godfellas”的一集中, 上帝的实体的名言。
这条格言非常适用于基础设施的改动,虽然这一目标并非总是可行的,但确实是我们的努力方向。数据的迁移是个非常棘手的任务,但我们不希望在迁移时停下整个系统,这会影响用户登录以及新用户的创建。因此,我们必须进行一次无缝的切换。这就意味着我们需要让这两个存储系统同时运行一段时间,以 Postgres 作为主存储机制,同时隐蔽地运行 Cassandra 存储系统。所谓隐蔽就是指并行地进行实际请求的处理,但忽略其结果。
这种方式能够带来多种益处:
- 我们能够确保新的存储方案其能力足以处理现有的负载,对于 Postgres 的能力需求我们已经很了解了,但 Cassandra 是一种不同的系统,因此必须对其进行实际评估。
- 与 Cassandra 存储相关的代码也在实际运行中,因此我们能够在最终切换之前找到所有的 bug,以及健壮性和可伸缩性方面的问题。
- 因为我们隐蔽地运行着 Cassandra,因此即使它发生了故障也不要紧。主进程将记录下这次错误的信息,但忽略它的错误结果。
- 我们可以在新旧存储系统之间保持数据的同步。
迁移现有帐号
除了隐蔽地处理写入操作,我们还必须对所有用户进行迁移。为此,我们通过一个后台作业,让它将 Postgres 中的所有用户逐个复制到 Cassandra 中。我们必需确保将竞态条件最小化,因为隐蔽的写入操作有可能与帐号的迁移过程同时进行。
由于 Postgres 工作方式的限制(在进行一个长时间运行的查询时,复制过程会中止),我们不得不以一种特殊的方式进行迁移。我们必须保证在运行迁移脚本之前,所有的写入操作,包括新建帐号与帐号更新,都已经进行了适当的隐蔽式处理。
- 对一个只读的从节点进行一个长时间的查询,以得到大量的用户名。
- 对于每个用户名: A. 如果该用户名已经存在于 Cassandra 中,那么就无需进行迁移。还记得吧,我们假设隐蔽的写入操作有接近 100% 的覆盖率。 B. 如果该用户名不存在,那么就准备进行迁移。 C. 对另一个只读的从节点进行查询,以获得对应这个用户名的用户数据(请记住,在长时间的查询过程中,复制过程会中止。因此,如果我们还是对第一个从节点进行查询,万一用户在这个长时间运行的查询启动后修改了个人信息,那我们就有可能会获取到过期的数据)。 D. 从第二个从节点中获取数据,并插入 Cassandra 数据库(从这一刻开始,如果这个用户产生了任何数据变更,都会同时反映在 Postgres 与 Cassandra 数据库中)。
正确性验证
我们还需要一个脚本以验证这两个存储系统是否已经完全同步了。这个脚本也会以类似的方式逐个处理每个用户,从两个存储系统中同时获取用户数据并进行对比。它还能够为我们生成有用的统计数据,告诉我们比较结果的差别,以及差别的频度。这种方式已经证实对于 bug 的排查非常有帮助。
开始切换
在进行实际切换过程中,Spotify 后台所采用的微服务架构帮了我们一个大忙。它的思想是将对 user 数据库的实际调用封装在一个 RESTful 的服务中,这种方式确保只有一个服务了解存储层的细节。实际的切换过程其实仅包括:
- 将配置中的主节点与隐蔽式节点的角色进行切换。
- 确保新的配置在所有服务机器上都已生效。
- 同时重启所有服务的实例。
同时重启所有服务的实例是很重要的一步,它能够将冲突的风险降至最低。如果有部分服务将 Postgres 当作主节点,而另一部分服务将 Cassandra 当作主节点,会发生什么情况呢?我们可能会在新建帐号时产生冲突,导致同一个用户名指向不同的帐号!
同时重启所有服务实例也有一些负面影响。重启过程大概会持续几秒钟,这段时间内无法创建帐号或进行登录操作。好在我们的客户都很聪明,他们会自己尝试重新登录。
这种切换方式也让我们产生了一个良好的回滚计划,一旦出了什么问题,我们就能够以同样的方式简单地撤消,重新使用 Postgres 作为主节点。
实际的切换结果如何?
首先,我们要确保处于一个良好的状态,通过执行验证脚本将当前的不一致数量降至最低。然后我们启动了切换过程,并注视着日志与图形信息。整个过程相当平静,我们并没有发现什么令人振奋或吃惊的事。事实上,这是我经历过的最乏味的一次部署了。接下来唯一要做的事就是手动地修复一些不一致的地方。
这一路所遇到的各种阻碍
在迁移至 Cassandra 的过程中,我们遇到了许多阻碍。其中有半数我已经记不太清了,但我还是想说明一下有哪些最大的阻碍是我们所必须克服的。
Paxos 算法出错?
Cassandra 1.2 版本中引入了一个别出心裁的的特性,名为 LWT“轻量级事务”,或者叫“条件式插入”。它的本质在于能够确保键的唯一性,这一点对于 Spotify 来说相当重要,它可以保证一个用户名仅属于一个用户。我们可不想在两个用户同时创建帐号时允许冲突或竞态,然后决定让他们共享同一个用户名。
因此,为了避免冲突,我们决定使用 LWT,它的底层使用了一个著名的分布式一致性算法 Paxos 。它在 Cassandra 上的实现需要为复制节点设置一个仲裁(quorum),并且需要经过四个来回的通信过程,这种操作的代价相当之高。
我们进行了一次基准测试,并得出了一个结论:在创建新帐号时使用 Paxos 算法的代价不算太高。但在生产环境中实际测试时,却发现有大量的创建帐号操作都失败了,因为 Paxos 认为这些用户名与帐号已经存在。
我们为此提交了一个错误报告 CASSANDRA-9086 ,并等待官方的回复。
而最终的回复表示,这是一种预期行为,我们可以在自己的服务代码中处理这一问题。
Paxos 需要求所有节点参与 CAS 操作?
这是个有趣的 bug。正如我之前所说,Paxos 算法要求设置仲裁节点以实现一致性。如果这一过程失败,整个操作也会失败,而后将返回一些额外的信息。比方说,你将得到能够参与 Paxos 过程的节点数量。
我们注意到了一点,在帐号创建失败时,错误消息中所显示的数字是所有复制节点的数目,而不是作为仲裁的复制节点的数目。这其实是我们所使用的 Cassandra 版本中的一个 bug( CASSANDRA-8640 ),随后我们将整个集群进行升级,简单地解决了这个问题。
Java 驱动 (撤消了 2.10.0 版本中的 JAVA-425 变更)
我们使用了一个由 Datastax 创建的开源 Cassandra 客户端,这家公司 雇用了大量的 Cassandra 贡献者。经过几个星期的运行,系统的负载也有所上升,此时我们注意到:从我们的服务到 Cassandra 的连接数量在下降,却没有重新建立起新的连接。
这个 bug 将影响真实环境中的访问量,我们使用了异步的 Java 驱动 API,而没有使用分离的线程池。一旦无法建立新的连接,API 就将开始阻塞。最后我们终于触及了并发的上限,连续几个小时不停地收到系统的警报。所幸经过手动重启后服务又能够继续运行了,同时我们也开始追踪其根本原因。
最后证明我们所使用的客户端版本中有一个新的回归缺陷(由 JAVA-425 修改所造成),于是我们升级至最新的版本,其中撤消了 JAVA-425 这个修改,问题得以解决。
对备份的依赖
当迁移至 Cassandra 后,我们需要开始对 Cassandra 数据库进行每日备份。这项任务之前是在 Postgres 数据库上进行的,有许多大数据批处理作业(用于进行个性化、业务分析等等)依赖于它。但 Cassandra 的备份与 Postgres 的备份有着细微的差别,而有些批处理作业无法处理这种差别。这个问题至今也没有完全解决,但我们正在积极地处理它。
总结
地球人都知道,软件项目所花的时间总是比预计的要长,我们的迁移项目也不例外。但我们确实做到在整个切换过程几乎没有什么闪失。通常来说,如果事情太过顺利,我们有时反而会认为一定出现了什么要命的错误。好在这一次是个例外,原因是我们在隐蔽式运行阶段已经触及了大量的问题,这对于终端用户几乎没有什么影响。我们也编写了验证脚本、跟踪日志,并且不断地查看各种图表。因此在最终切换时,我们非常有信心不会遇到太大的问题。
编后语
《他山之石》是 InfoQ 中文站新推出的一个专栏,精选来自国内外技术社区和个人博客上的技术文章,让更多的读者朋友受益,本栏目转载的内容都经过原作者授权。文章推荐可以发送邮件到 editors@cn.infoq.com。
感谢郭蕾对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流(欢迎加入 InfoQ 读者交流群)。
评论