Spotify是怎样从Postgres切换至Cassandra的?
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用户、接受某个许可或是连接到非死book时,也都要访问这个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的备份有着细微的差别,而有些批处理作业无法 处理这种差别。这个问题至今也没有完全解决,但我们正在积极地处理它。
总结
地球人都知道,软件项目所花的时间总是比预计的要长,我们的迁移项目也不例外。但我们确实做到在整个切换过程几乎没有什么闪失。通常来说,如果事 情太过顺利,我们有时反而会认为一定出现了什么要命的错误。好在这一次是个例外,原因是我们在隐蔽式运行阶段已经触及了大量的问题,这对于终端用户几乎没 有什么影响。我们也编写了验证脚本、跟踪日志,并且不断地查看各种图表。因此在最终切换时,我们非常有信心不会遇到太大的问题。